Skip to content

Commit e7bc94e

Browse files
authored
Merge pull request #1923 from vikrantwiz02/placement-pbi-v1
Placement Cell: drives, applications, calendar, CPI, off-campus, announcements + docs
2 parents 2ff621f + dfeb6b1 commit e7bc94e

66 files changed

Lines changed: 11640 additions & 12403 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

FusionIIIT/Fusion/urls.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
url(r'^complaint/', include('applications.complaint_system.urls')),
4848
url(r'^healthcenter/', include('applications.health_center.urls')),
4949
url(r'^leave/', include('applications.leave.urls')),
50-
url(r'^placement/', include('applications.placement_cell.urls')),
50+
url(r'^placement/', include('applications.placement_cell.api.urls')),
5151
url(r'^filetracking/', include('applications.filetracking.urls')),
5252
url(r'^spacs/', include('applications.scholarships.urls')),
5353
url(r'^visitorhostel/', include('applications.visitor_hostel.urls')),
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
# Placement Cell
2+
3+
The Placement Cell module manages the full campus placement lifecycle for
4+
IIITDM Jabalpur — company drives, student applications and offers, interview
5+
rounds, statistics and reports, debarments and restrictions, an interactive
6+
placement calendar, an alumni network, and off-campus / published-CPI tracking.
7+
8+
It is an API-only Django app (`applications/placement_cell`) consumed by the
9+
React placement module in `Fusion-client` (`src/Modules/PlacementCell`).
10+
11+
---
12+
13+
## Roles
14+
15+
The module recognises four roles (the user's selected designation, exposed to
16+
the frontend as `state.user.role`):
17+
18+
| Role (designation) | Capabilities |
19+
|----------------------|--------------|
20+
| `placement officer` | TPO: schedules/drives, applications, interview rounds, statistics, reports, debarments, restrictions, fields, notifications, off-campus records, published-CPI export, calendar management, announcements |
21+
| `placement chairman` | Admin oversight: officer capabilities plus placement policies |
22+
| `student` | Browse drives, apply, manage placement profile, view offers/timeline, download CV, read announcements/calendar, alumni network |
23+
| `alumni` | Alumni profile, post job referrals, mentorship sessions, student network |
24+
25+
**How roles resolve**
26+
27+
- **Backend** authorizes via `HoldsDesignation(working=user, designation__name=…)`.
28+
`selectors.is_tpo` is true for `placement officer` / `placement chairman`;
29+
officer/chairman-only endpoints return `403` with `{ "detail": … }`.
30+
- **Sidebar visibility** comes from `ModuleAccess.placement_cell` for the
31+
designation.
32+
- **Frontend** chooses the tab set in `PlacementCellPage` from `state.user.role`.
33+
34+
---
35+
36+
## Features
37+
38+
### Students
39+
- **Placement Schedule** — browse drives as a chronological **Agenda**
40+
(Today / This Week / Upcoming / Closed) or a filterable **card** view, with
41+
per-drive eligibility and deadline countdowns; apply, withdraw, track status.
42+
- **My Applications / My Offers** — application timeline; accept/decline offers.
43+
- **Placement Calendar** — read-only, colour-coded view of drives, tests,
44+
interviews and deadlines.
45+
- **Announcements** — read placement-cell announcements.
46+
- **Download CV**, placement profile, notification preferences.
47+
- **Alumni Network** — connect with alumni, browse referrals, mentorship.
48+
49+
### Placement Officer / Chairman
50+
- **Add / edit drives** with eligibility (min CPI, branches from the live
51+
department list, passout year, gender) and custom **application fields**
52+
(creatable inline).
53+
- **Student CPI** — per-batch published CPI (computed from the examination
54+
module's announced results), with off-campus companies and **Excel export**.
55+
- **Off-Campus Placements** — record offers students received off campus
56+
(company autocomplete from registered companies).
57+
- **Placement Statistics & Reports** — stats, report generation and export.
58+
- **Debarred Students** and **Restrictions** (institute-wide eligibility bars).
59+
- **Send Notifications**, **Company Registration**, **Fields** management.
60+
- **Placement Calendar** — Google-Calendar-style: click a date/slot to add an
61+
event, edit/delete events; merged with drives and deadlines.
62+
- **Announcements** — post / pin / delete.
63+
- **Placement Appeals**, **Higher Studies**, **Alumni Verification**.
64+
65+
### Chairman
66+
- All officer capabilities plus **Placement Policies**.
67+
68+
---
69+
70+
## Backend
71+
72+
- **Models** (`models.py`, ~45): profiles & academic info (Education, Skill,
73+
Experience, …), `NotifyStudent`, `PlacementSchedule`, `PlacementApplication`,
74+
`PlacementStatus`, `PlacementRound`, `PlacementRecord`, `StudentRecord`,
75+
`PlacementRestriction`, `PlacementPolicy`, `PlacementAppeal`, alumni models,
76+
and the additive `PlacementAnnouncement`, `OffCampusPlacement`,
77+
`PlacementCalendarEvent`.
78+
- **API** (`api/urls.py`, `api/views.py`, `api/serializers.py`): DRF, Token
79+
authentication. URLs are mounted under `placement/` (see `Fusion/urls.py`),
80+
so routes are `…/placement/api/<route>/`.
81+
82+
### Endpoint groups
83+
84+
| Area | Routes |
85+
|------|--------|
86+
| Drives & schedule | `api/placement/`, `api/placement/<id>/`, `api/calender/`, `api/timeline/<id>/`, `api/nextround/<id>/` |
87+
| Applications & offers | `api/apply-for-placement/`, `api/my-applications/`, `api/my-offers/`, `api/offer/<id>/`, `api/offer/<id>/respond/`, `api/student-applications/<id>/`, `api/application-detail/<id>/`, `api/download-applications/<id>/` |
88+
| Statistics & reports | `api/statistics/`, `api/delete-statistics/<id>/`, `api/reports/`, `api/reports/export/`, `api/report-schedules/`, `api/higher-studies/` |
89+
| Eligibility & policy | `api/restrictions/`, `api/policies/`, `api/branches/` |
90+
| Debarment | `api/debared-students/`, `api/debared-status/<roll_no>/` |
91+
| Fields & profile | `api/add-field/`, `api/form-fields/`, `api/profile/`, `api/notification-preferences/`, `api/registration/`, `api/generate-cv/` |
92+
| Notifications | `api/send-notification/` |
93+
| Announcements | `api/announcements/`, `api/announcements/<id>/` |
94+
| Off-campus | `api/offcampus/`, `api/offcampus/<id>/` |
95+
| Published CPI | `api/cpi-batches/`, `api/cpi-students/` (`?batch_id=` , `?export=excel`) |
96+
| Calendar events | `api/calendar-events/`, `api/calendar-events/<id>/` |
97+
| Appeals | `api/placement-appeals/`, `api/placement-appeals/<id>/` |
98+
| Alumni | `api/alumni/profile/`, `api/alumni/directory/`, `api/alumni/verification/`, `api/alumni/referrals/`, `api/alumni/connections/`, `api/alumni/sessions/` |
99+
100+
**Authorization** — all endpoints require authentication; write/sensitive
101+
operations are gated on `selectors.is_tpo` (officer/chairman). Server-controlled
102+
fields (`created_by`, `added_by`, `posted_by`) are never client-settable.
103+
104+
### Published CPI
105+
106+
`selectors.get_student_published_cpi` derives a student's CPI from the
107+
examination module's latest **announced** `ResultAnnouncement` (not the static
108+
`Student.cpi`). The per-batch view memoises each student's computed CPI in the
109+
cache (keyed by roll number + semester), so reloading a batch is near-instant.
110+
111+
---
112+
113+
## Frontend (`Fusion-client/src/Modules/PlacementCell`)
114+
115+
- React 18 + Vite, Mantine v7, Redux Toolkit, axios, `mantine-react-table`,
116+
`react-big-calendar`.
117+
- `pages/PlacementCellPage.jsx` renders the shared `ModuleTabs` navbar and the
118+
role-specific tab set. **Every tab is lazy-loaded** behind a `Suspense`
119+
boundary so opening the module only downloads the active tab's code; Vite
120+
`manualChunks` splits the heavy vendors into cacheable chunks.
121+
- `api.js` (+ `services/api.js` re-export) holds `placementApi` with one method
122+
per endpoint and `buildAuthConfig()` for the token header.
123+
- Date inputs use native `datetime-local` / `date` controls for reliability.
124+
125+
---
126+
127+
## Setup — role accounts
128+
129+
`manage.py setup_placement_roles` creates one idempotent login per role and
130+
enables `ModuleAccess.placement_cell`. The password is supplied at runtime and
131+
is **not** stored in the repo:
132+
133+
```bash
134+
cd FusionIIIT
135+
python manage.py setup_placement_roles --password '<password>'
136+
# or set PLACEMENT_ROLE_PASSWORD
137+
```
138+
139+
| Username | Role |
140+
|----------------------|--------------------|
141+
| `placement_officer` | placement officer |
142+
| `placement_chairman` | placement chairman |
143+
| `placement_student` | student |
144+
| `placement_alumni` | alumni |
145+
146+
---
147+
148+
## Tests
149+
150+
See [`tests/README.md`](tests/README.md). Run with the dedicated settings
151+
(migrations disabled so the historical chain cannot block test-DB creation):
152+
153+
```bash
154+
cd FusionIIIT
155+
python manage.py test \
156+
applications.placement_cell.tests.test_placement_api \
157+
applications.placement_cell.tests.test_use_cases \
158+
applications.placement_cell.tests.test_business_rules \
159+
applications.placement_cell.tests.test_workflows \
160+
applications.placement_cell.tests.test_module \
161+
--settings=test_settings
162+
```
163+
164+
`test_placement_api` covers schema regressions, API-only URL wiring,
165+
authentication and role authorization (including announcements, off-campus,
166+
published-CPI export and calendar-event CRUD).
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+

FusionIIIT/applications/placement_cell/admin.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ class StudentRecordAdmin(admin.ModelAdmin):
8282

8383

8484
class ChairmanVisitAdmin(admin.ModelAdmin):
85-
list_display = ('company_name', 'location', 'visiting_date', 'timestamp')
85+
list_display = ('company_name', 'location', 'visiting_date', 'start_date', 'end_date', 'timestamp')
8686

8787

8888
class PlacementScheduleAdmin(admin.ModelAdmin):
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+

FusionIIIT/applications/placement_cell/api/serializers.py

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,16 @@
44
from applications.placement_cell.models import (Achievement, Course, Education,
55
Experience, Has, Patent,
66
Project, Publication, Skill,
7-
PlacementStatus, NotifyStudent)
7+
PlacementAppeal, PlacementStatus,
8+
NotifyStudent, PlacementAnnouncement,
9+
OffCampusPlacement,
10+
PlacementCalendarEvent)
11+
12+
13+
class PlacementAppealSerializer(serializers.ModelSerializer):
14+
class Meta:
15+
model = PlacementAppeal
16+
fields = '__all__'
817

918
class SkillSerializer(serializers.ModelSerializer):
1019

@@ -17,7 +26,7 @@ class HasSerializer(serializers.ModelSerializer):
1726

1827
class Meta:
1928
model = Has
20-
fields = ('skill_id','skill_rating')
29+
fields = ('id', 'skill_id', 'skill_rating')
2130

2231
def create(self, validated_data):
2332
skill = validated_data.pop('skill_id')
@@ -28,6 +37,22 @@ def create(self, validated_data):
2837
raise serializers.ValidationError({'skill': 'This skill is already present'})
2938
return has_obj
3039

40+
def update(self, instance, validated_data):
41+
skill = validated_data.pop('skill_id', None)
42+
if skill:
43+
skill_id, _ = Skill.objects.get_or_create(**skill)
44+
duplicate = Has.objects.filter(
45+
unique_id=instance.unique_id,
46+
skill_id=skill_id,
47+
).exclude(pk=instance.pk).exists()
48+
if duplicate:
49+
raise serializers.ValidationError({'skill': 'This skill is already present'})
50+
instance.skill_id = skill_id
51+
if 'skill_rating' in validated_data:
52+
instance.skill_rating = validated_data['skill_rating']
53+
instance.save()
54+
return instance
55+
3156
class EducationSerializer(serializers.ModelSerializer):
3257

3358
class Meta:
@@ -82,3 +107,59 @@ class PlacementStatusSerializer(serializers.ModelSerializer):
82107
class Meta:
83108
model = PlacementStatus
84109
fields = ('notify_id', 'invitation', 'placed', 'timestamp', 'no_of_days')
110+
111+
112+
class PlacementAnnouncementSerializer(serializers.ModelSerializer):
113+
posted_by_name = serializers.SerializerMethodField()
114+
115+
class Meta:
116+
model = PlacementAnnouncement
117+
fields = ('id', 'title', 'body', 'posted_by_name', 'posted_at', 'is_pinned')
118+
read_only_fields = ('id', 'posted_at')
119+
120+
def get_posted_by_name(self, obj):
121+
if obj.posted_by:
122+
full_name = '{} {}'.format(
123+
obj.posted_by.first_name, obj.posted_by.last_name
124+
).strip()
125+
return full_name or obj.posted_by.username
126+
return None
127+
128+
129+
class PlacementAnnouncementWriteSerializer(serializers.ModelSerializer):
130+
131+
class Meta:
132+
model = PlacementAnnouncement
133+
fields = ('title', 'body', 'is_pinned')
134+
135+
136+
class OffCampusPlacementSerializer(serializers.ModelSerializer):
137+
roll_no = serializers.CharField(source='student.user.username', read_only=True)
138+
student_name = serializers.SerializerMethodField()
139+
140+
class Meta:
141+
model = OffCampusPlacement
142+
fields = ('id', 'roll_no', 'student_name', 'company_name', 'role',
143+
'offer_type', 'ctc', 'stipend', 'offer_date', 'notes', 'created_at')
144+
read_only_fields = ('id', 'created_at')
145+
146+
def get_student_name(self, obj):
147+
user = obj.student.user
148+
return '{} {}'.format(user.first_name, user.last_name).strip()
149+
150+
151+
class OffCampusPlacementWriteSerializer(serializers.ModelSerializer):
152+
153+
class Meta:
154+
model = OffCampusPlacement
155+
fields = ('student', 'company_name', 'role', 'offer_type',
156+
'ctc', 'stipend', 'offer_date', 'notes')
157+
158+
159+
class PlacementCalendarEventSerializer(serializers.ModelSerializer):
160+
161+
class Meta:
162+
model = PlacementCalendarEvent
163+
fields = ('id', 'title', 'description', 'start', 'end', 'all_day',
164+
'category', 'location', 'created_at')
165+
read_only_fields = ('id', 'created_at')
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
from django.conf.urls import url
2+
3+
from . import views
4+
5+
app_name = "placement"
6+
7+
urlpatterns = [
8+
url(r"^api/placement/$", views.placement_api, name="placement_api"),
9+
url(r"^api/placement/(?P<schedule_id>[0-9]+)/$", views.placement_detail_api, name="placement_detail_api"),
10+
url(r"^api/statistics/$", views.placement_statistics_api, name="placement_statistics_api"),
11+
url(r"^api/reports/$", views.placement_reports_api, name="placement_reports_api"),
12+
url(r"^api/reports/export/$", views.placement_reports_export_api, name="placement_reports_export_api"),
13+
url(r"^api/report-schedules/$", views.placement_report_schedules_api, name="placement_report_schedules_api"),
14+
url(r"^api/report-schedules/(?P<schedule_id>[0-9]+)/$", views.placement_report_schedule_detail_api, name="placement_report_schedule_detail_api"),
15+
url(r"^api/delete-statistics/(?P<record_id>[0-9]+)/$", views.delete_placement_statistics_api, name="delete_placement_statistics_api"),
16+
url(r"^api/higher-studies/$", views.higher_studies_api, name="higher_studies_api"),
17+
url(r"^api/higher-studies/(?P<record_id>[0-9]+)/$", views.higher_studies_detail_api, name="higher_studies_detail_api"),
18+
url(r"^api/registration/$", views.registration_api, name="registration_api"),
19+
url(r"^api/add-field/$", views.placement_fields_api, name="placement_fields_api"),
20+
url(r"^api/form-fields/$", views.form_fields_api, name="form_fields_api"),
21+
url(r"^api/profile/$", views.placement_profile_api, name="placement_profile_api"),
22+
url(r"^api/notification-preferences/$", views.notification_preferences_api, name="notification_preferences_api"),
23+
url(r"^api/apply-for-placement/$", views.apply_for_placement_api, name="apply_for_placement_api"),
24+
url(r"^api/apply-for-placement/(?P<schedule_id>[0-9]+)/$", views.withdraw_application_api, name="withdraw_application_api"),
25+
url(r"^api/my-applications/$", views.my_applications_api, name="my_applications_api"),
26+
url(r"^api/my-offers/$", views.my_offers_api, name="my_offers_api"),
27+
url(r"^api/offer/(?P<offer_id>[0-9]+)/$", views.offer_detail_api, name="offer_detail_api"),
28+
url(r"^api/offer/(?P<offer_id>[0-9]+)/respond/$", views.offer_respond_api, name="offer_respond_api"),
29+
url(r"^api/student-applications/(?P<identifier>[0-9]+)/$", views.student_applications_api, name="student_applications_api"),
30+
url(r"^api/application-detail/(?P<application_id>[0-9]+)/$", views.application_detail_api, name="application_detail_api"),
31+
url(r"^api/application-detail/(?P<application_id>[0-9]+)/interview/$", views.application_interview_schedule_api, name="application_interview_schedule_api"),
32+
url(r"^api/download-applications/(?P<schedule_id>[0-9]+)/$", views.download_applications_api, name="download_applications_api"),
33+
url(r"^api/nextround/(?P<schedule_id>[0-9]+)/$", views.next_round_api, name="next_round_api"),
34+
url(r"^api/timeline/(?P<schedule_id>[0-9]+)/$", views.timeline_api, name="timeline_api"),
35+
url(r"^api/calender/$", views.calendar_api, name="calendar_api"),
36+
url(r"^api/generate-cv/$", views.generate_cv_api, name="generate_cv_api"),
37+
url(r"^api/debared-students/$", views.debarred_students_api, name="debarred_students_api"),
38+
url(r"^api/debared-status/(?P<roll_no>[A-Za-z0-9]+)/$", views.debarred_status_api, name="debarred_status_api"),
39+
url(r"^api/send-notification/$", views.send_notification_api, name="send_notification_api"),
40+
url(r"^api/restrictions/$", views.restrictions_api, name="restrictions_api"),
41+
url(r"^api/restrictions/(?P<restriction_id>[0-9]+)/$", views.restriction_detail_api, name="restriction_detail_api"),
42+
url(r"^api/policies/$", views.placement_policies_api, name="placement_policies_api"),
43+
url(r"^api/policies/(?P<policy_id>[0-9]+)/$", views.placement_policy_detail_api, name="placement_policy_detail_api"),
44+
url(r"^api/alumni/profile/$", views.alumni_profile_api, name="alumni_profile_api"),
45+
url(r"^api/alumni/directory/$", views.alumni_directory_api, name="alumni_directory_api"),
46+
url(r"^api/alumni/verification/$", views.alumni_verification_list_api, name="alumni_verification_list_api"),
47+
url(r"^api/alumni/verification/(?P<profile_id>[0-9]+)/$", views.alumni_verification_detail_api, name="alumni_verification_detail_api"),
48+
url(r"^api/alumni/referrals/$", views.alumni_referrals_api, name="alumni_referrals_api"),
49+
url(r"^api/alumni/connections/$", views.alumni_connections_api, name="alumni_connections_api"),
50+
url(r"^api/alumni/connections/(?P<connection_id>[0-9]+)/$", views.alumni_connection_detail_api, name="alumni_connection_detail_api"),
51+
url(r"^api/alumni/sessions/$", views.alumni_sessions_api, name="alumni_sessions_api"),
52+
url(r"^api/alumni/sessions/(?P<session_id>[0-9]+)/$", views.alumni_session_detail_api, name="alumni_session_detail_api"),
53+
# PlacementAppeal API endpoints
54+
url(r"^api/placement-appeals/$", views.placement_appeal_list_create_api, name="placement_appeal_list_create_api"),
55+
url(r"^api/placement-appeals/(?P<pk>[0-9]+)/$", views.placement_appeal_detail_api, name="placement_appeal_detail_api"),
56+
# Placement Announcements API endpoints
57+
url(r"^api/announcements/$", views.placement_announcements_api, name="placement_announcements_api"),
58+
url(r"^api/announcements/(?P<announcement_id>[0-9]+)/$", views.placement_announcement_detail_api, name="placement_announcement_detail_api"),
59+
# Off-Campus Placements API endpoints
60+
url(r"^api/offcampus/$", views.offcampus_placements_api, name="offcampus_placements_api"),
61+
url(r"^api/offcampus/(?P<placement_id>[0-9]+)/$", views.offcampus_placement_detail_api, name="offcampus_placement_detail_api"),
62+
# Published-CPI student view + export API endpoints
63+
url(r"^api/cpi-batches/$", views.placement_cpi_batches_api, name="placement_cpi_batches_api"),
64+
url(r"^api/cpi-students/$", views.placement_cpi_students_api, name="placement_cpi_students_api"),
65+
# Branch (department) reference list for placement forms
66+
url(r"^api/branches/$", views.placement_branches_api, name="placement_branches_api"),
67+
# Free-form placement calendar events (Google-Calendar style)
68+
url(r"^api/calendar-events/$", views.placement_calendar_events_api, name="placement_calendar_events_api"),
69+
url(r"^api/calendar-events/(?P<event_id>[0-9]+)/$", views.placement_calendar_event_detail_api, name="placement_calendar_event_detail_api"),
70+
]

0 commit comments

Comments
 (0)