Skip to content

Programme-scoped academic roles, and replacements derived from the course being cleared - #1957

Merged
vikrantwiz02 merged 1 commit into
FusionIIIT:prod/acad-reactfrom
vikrantwiz02:acad-programme-roles
Aug 18, 2026
Merged

Programme-scoped academic roles, and replacements derived from the course being cleared#1957
vikrantwiz02 merged 1 commit into
FusionIIIT:prod/acad-reactfrom
vikrantwiz02:acad-programme-roles

Conversation

@vikrantwiz02

@vikrantwiz02 vikrantwiz02 commented Aug 18, 2026

Copy link
Copy Markdown
Member

Backend half of the Acad UG / Acad PG / Acad Ph.D. roles, plus a family of replacement bugs found while testing them. Frontend counterpart: FusionIIIT/Fusion-client#293.

Programme-scoped roles

Three new designations administer one programme level each. applications/globals/programme_scope.py resolves the scope from the role the user is acting as (last_selected_role, guarded so it can never widen access beyond the designations actually held) and every academic view filters through it:

  • reads limited to that level's students, batches, programmes, curriculums and grade rows — including the raw-SQL grade summary, via a scoped subquery
  • writes reject a target outside scope (404/403 rather than a silent no-op)
  • announcements can only be aimed at a programme the sender administers, and recipient lists are intersected with it
  • acadadmin and Dean Academic stay unscoped
  • Disciplines, Courses, Course Instructors, Theses and the Academic Calendar are deliberately shared — scoping Courses by curriculum slot would have hidden 177 courses that sit in no slot

Verified against a copy of production data: batches 43 = 30 UG + 13 PG; programmes 5/13/8 filtered to 5/0/0, 0/13/0, 0/0/8; grade summary 211 → 202/21/0; an Acad UG attempt to announce to PG returns 403; cross-programme mutations are refused.

Migrations create the designations, add the announcement target, and clone acadadmin's ModuleAccess row to the three roles.

The replacement bug this started from

Approving a BL request derived the registration type and the replaced registration from the course being registered instead of the one being cleared. A retake happened to work (same course); a stand-in for an open elective did not: no prior grade for OE3C42 meant Regular, and no prior registration of it meant no course_replacement row at all — so the cleared course's low grade also kept counting in CPI, since superseded_course_codes had no link to follow.

Approval now works off source_course, and a BL slot accepts a retake of that source rather than only the slot's stand-ins (the source is not in any BL slot's course list, so this was a 404 before).

Re-approving the affected request through the endpoint: OE3C42 went from Regular/no link to Improvement replacing CS8007, and semester 1's CS8007 row now reports "Replaced By OE3C42".

The same pattern everywhere else it appears

before now
BL source list filtered low grades then took the latest, so a course whose retake cleared it stayed clearable forever latest grade decides; agrees with the slot-eligibility check
BL slots one backlog could be cleared through two slots and earn its credits twice (both returned 201) refused on submit, and left out of the dropdown via claimed_by
transcript symbol S only for SW-prefixed courses; a stand-in got no marker at all follows the replacement link: S substitute, R repeat
result sheet S(IM) / S(BL) bare S for a substitute; repeats keep R(BL) / R(IM)
bulk replacement upload link created, type left Regular type derived from the replaced course's grade
pre-registration registration_type='Backlog' hardcoded, and prev_registration_id trusted from the request body derived from the repeated course's grade; a registration belonging to another student is refused before any row is written
Swayam approval unordered .first() on the source's grades — a different grade for 1131 student-course pairs in current data (e.g. EC203b: F → Backlog vs latest D → Improvement) latest grade, via one shared helper
manual add course (API + legacy view) replaced registration looked up by id alone, so a stale id could link another student's row scoped to the student being edited
Grade Validation a substituted course read Regular reads Substitute

One shared latest_grade() / registration_type_for_grade() pair replaces the four copies of the grade lists that had drifted apart.

Checks

Every changed path exercised end to end against a copy of production data, with all probe rows and tokens removed afterwards; manage.py check clean and no missing migrations. The repo's two backend test modules can't run here — the test database is missing tables, which predates this branch.

Deploy

migrate, then normalize_student_categories and sync_student_accounts (in that order) if not already run from #1956.

… course being cleared

Acad UG, Acad PG and Acad Ph.D. are academic administrators for one programme
level. A new applications/globals/programme_scope.py resolves the scope from
the role the user is acting as, and every academic view now filters through it:
reads are limited to that level's students, batches, programmes and grade rows,
writes reject a target outside it, and an announcement can only be aimed at a
programme the sender administers. acadadmin and Dean Academic stay unscoped.
Disciplines, Courses, Course Instructors, Theses and the Academic Calendar are
deliberately shared, since they are not per-programme data.

Approving a BL request read the grade and the prior registration of the course
being *registered* rather than the one being cleared, so a stand-in for an open
elective was filed as Regular with no replacement link -- which also meant the
old low grade kept counting in CPI, since superseded_course_codes has nothing
to follow. Approval now works off the source course, and a BL slot accepts a
retake of that source, not only one of the slot's stand-ins.

The same missing-source pattern is fixed everywhere else it appears:

- a course whose latest attempt cleared it is no longer offered as a BL source
  (the low-grade filter ran before "latest", so it returned the latest *low*
  grade); the source list and the slot-eligibility check now agree
- one backlog can no longer be cleared through two slots at once, which would
  have earned its credits twice; a source already in flight is left out of the
  dropdown and refused on submit
- the transcript marks a substitute S and a repeat R by following the
  replacement link, instead of only recognising SW-prefixed courses
- the bulk replacement upload sets the new registration's type from the
  replaced course's grade instead of leaving it Regular
- pre-registration derives backlog vs improvement from the repeated course
  instead of hardcoding Backlog, and refuses a prev_registration_id that
  belongs to another student
- the Swayam approval reads the source's *latest* grade (an unordered .first()
  picked a different grade for 1131 student-course pairs in the current data)
- the manual add-course paths only link a replaced registration that belongs to
  the student being edited
- Grade Validation labels a substituted course Substitute rather than Regular
@vikrantwiz02
vikrantwiz02 merged commit 9cbc2b0 into FusionIIIT:prod/acad-react Aug 18, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant