Attendance: administration belongs to the ERP, collection belongs to teaching¶
Dr. Anita, an org admin at Sapthagiri University, opens Campus → Attendance
to check whether faculty are marking. The page lands on a Mark Attendance
tab that says "No subjects assigned. You don't have any subjects assigned yet.
Contact your administrator to get subject assignments." She is the
administrator. Meanwhile Prof. Ravi, who teaches three subjects and needs to
mark them, has no Attendance entry in his sidebar at all — the only way he
reaches his own marking tool on web is by typing /admin/attendance into the
address bar.
The split is inverted. Admins see a teacher's tool they can never use; teachers cannot find the tool they need. This doc re-homes attendance along the boundary that OpenEduCat has used for fifteen years: the ERP owns attendance data, policy, enrollment, consent and oversight; collection is a teaching act bound to the session being taught.
Status: Phase 1 implemented — Product Mindset Gate passed 2026-08-13, code in
MySetu-AI/kwiloai_webapp on feat/attendance-admin-vs-collection. Phase 2
(compliance) not started. Scope is apps/web + apps/backend only;
apps/mobile-b2b already has its attendance screens and is unchanged.
Related: attendance.md (the as-built system this
restructures — read it first for the K-12/higher-ed data model and the endpoint
inventory), erp-timetable-generation.md (the
sessions the teacher flow reads),
erp-higher-ed-scheduling.md (why higher-ed
cohorts hang off AcademicLevel rather than Class),
roles-and-permissions.md (the enforcement model
the admin/teacher boundary is expressed in).
Why this exists¶
-
Who asked: Directly observed by the founder on the Sapthagiri University staging tenant, 2026-08-13, with a screenshot of the empty Mark Attendance tab. Not a hypothesis — a reproducible failure on a real seeded tenant. The faculty half of the problem (no nav entry) is a code fact found while diagnosing the first half; no instructor has reported it, which is itself the signal — they stopped expecting web to work and mark on mobile or not at all.
-
User pain: Two people, one broken boundary.
Dr. Anita (org admin) wants to answer "is attendance actually being marked?" The page she opens answers a different question — "which subjects do you teach?" — and answers it correctly with "none," because she teaches nothing. The screen is not broken; it is the wrong screen. She has no way to reach the question she came for.
Prof. Ravi (faculty) wants to mark this morning's DSA class. The sidebar
under TEACHING has Courses, Assignments, Exams, Knowledge base, Publications
— no Attendance. The tool exists and works; it is filed under an /admin/
URL he has no reason to visit and no link to.
-
Cost of NOT doing it: Admins conclude attendance is broken and stop trusting it — this is an evaluation-stage feature for university deals, and an empty screen at the exact moment a registrar checks compliance is a bad demo. Faculty mark on mobile or not at all, so the web attendance record stays thin, so the defaulter and shortage reports institutions actually buy attendance for (the VTU 75% rule) run on incomplete data. The damage compounds: every unmarked session is unrecoverable once the term closes.
-
Validated or a guess? Validated for the admin half — observed directly, reproducible on staging. The faculty half is a verified code fact (
trainerNavigationinapps/web/src/constants/navigation.tscontains no attendance entry) but the user consequence is inferred, not reported. We are treating faculty silence as evidence of abandonment rather than satisfaction; that inference is the weakest claim in this doc. -
How we know it worked (user's perspective): An org admin opening Attendance sees enrollment and consent status for their institution within five seconds, and never sees a message telling them to contact their administrator. An instructor opening the app finds Attendance in the sidebar without being told where it is, and marks a session in under three taps from the sidebar.
Sean Ellis counterfactual: remove this a week after shipping and an instructor says "where did Attendance go from my sidebar — I was marking from my laptop between classes." That is a specific complaint, and it is the one we expect. The admin-side counterfactual is weaker until Phase 2 ships, because Phase 1 mostly removes a broken thing rather than adding a valued one — an honest reason to treat Phase 2 as the real admin win.
What OpenEduCat does (and why we follow it)¶
Attendance is a first-class ERP module in OpenEduCat (openeducat_attendance,
'application': True, depending on openeducat_timetable). The separation is
not module-versus-outside; it is by role, inside one module:
| Model | group_op_attendance_user (faculty) |
group_op_attendance_manager (back office) |
|---|---|---|
op.attendance.register — course + batch + subject config |
read | full CRUD |
op.attendance.sheet — one per session/date |
read, write, create | full CRUD |
op.attendance.line — one per student |
read, write, create | full CRUD |
op.attendance.type — present/absent/excused/late semantics |
read | full CRUD |
Faculty mark. Faculty cannot configure and cannot delete. Three further details shape our design:
- The faculty entry point is the timetable session, not the attendance menu.
openeducat_attendance/views/attendance_session_view.xmlinjects an "Attendance Sheet" stat button onto theop.sessionform, andop.session.get_attendance()opens a sheet pre-bound to that session and its register. Faculty reach marking through the class they are teaching now. - Capture is explicitly designed to happen off the ERP screens.
controllers/app_main.pyexposesPOST /openeducat-attendance/take-attendance, a JSON route for the mobile app that bulk-creates lines defaulted to present. - The record carries its author.
op.attendance.sheet.faculty_idplus adraft → start → done → cancelstate machine: the data lives in the ERP, attributed to the teacher who took it.
Where we deliberately diverge from the intuition that prompted this work: OpenEduCat gives the back-office Manager full write on sheets and lines, on purpose. Institutions need corrections — faculty on medical leave, a wrong entry, retroactive excusal. We keep that as regularization, not as a marking tool. See Design decisions.
Personas¶
| Persona | Role | What attendance means to them |
|---|---|---|
| Prof. Ravi | instructor |
"Mark my 9am DSA class before students leave." Wants the fewest possible taps and zero context-selection when the timetable already knows what he teaches. |
| Dr. Anita | org_admin |
"Is attendance being marked across the university, and is every under-16 student's face enrollment consented?" Never marks; audits, configures, corrects. |
| Mr. Suresh | unit_manager |
Same as Dr. Anita, scoped to one campus. Also the person who fixes a record when Prof. Ravi is on leave. |
| Priya | learner |
Unchanged by this work — sees her own percentages at /attendance. |
| Priya's father | guardian |
Unchanged — sees her attendance at /parent/attendance, and consents to face enrollment. |
ICP exclusions¶
- K-12 daily attendance is not restructured. Class-teacher daily marking
keeps its current shape. The teacher-surface move applies to it (a class
teacher also gets
/attendancein the sidebar) but the K-12 roster UI is untouched. - B2C has no attendance. Nothing here reaches
apps/siteor the B2C app.
Outcomes (user outcomes, not deliverables)¶
- An org admin opening Attendance can see and act on enrollment and consent for their institution, and is never shown a marking tool.
- An instructor finds Attendance in their own sidebar and marks a session without choosing a subject or a date, when a timetable exists.
- An instructor at a tenant with no published timetable can still mark, via the subject picker they use today — nobody loses a working flow.
- A wrongly-marked record can be corrected by an admin, with the reason and the correcting user recorded.
Non-goals¶
- Attendance-type configuration. OpenEduCat's
op.attendance.typelets an institution define its own statuses. We hardcode present/absent/late/excused and that is sufficient; no customer has asked otherwise. - A compliance dashboard in Phase 1. The "which sessions went unmarked, by whom" view is the single most valuable admin feature and it is net-new. It gets Phase 2 and its own section below, deliberately not bundled.
- QR and face capture flow changes. Recognition methods stay as built; only enrollment administration moves.
- Mobile.
apps/mobile-b2balready hasapp/(app)/attendance/and is out of scope. Its screens keep calling the same endpoints, which are unchanged. - Migrating attendance tables. They already live in the
erpschema (migration2944a280ea07). No schema change is needed for Phase 1.
Mental model¶
The ERP is the registrar's office. The teaching surface is the classroom.
A registrar keeps the attendance ledger, decides the rules, holds the consent forms, and corrects errors. A registrar does not walk into a lecture hall and call the roll. A lecturer calls the roll — in the room, for the session they are teaching — and the ledger records it under their name.
Every placement decision below follows from that one sentence. If a task is something a registrar does at a desk, it is an ERP screen. If it is something a lecturer does in a room, it is a teaching screen.
Surfaces after the change¶
/attendance — instructor (new sidebar entry under TEACHING)¶
Session-first when the tenant has a published timetable:
TEACHER — /attendance (timetable present)
┌─ Today · Mon 13 Aug ───────────────────┐
│ 09:00 DSA · CSE Sem 3 · A [ Mark ] │
│ 11:00 DBMS · CSE Sem 5 · B ✓ Marked │
│ 14:00 OS · ISE Sem 3 · A [ Mark ] │
└────────────────────────────────────────┘
Tapping a session opens the roster pre-bound to that section, subject and date —
no pickers. This is op.session.get_attendance() translated to our stack.
Subject-first fallback when the tenant has no published timetable, which is the shape that exists today and keeps working unchanged:
TEACHER — /attendance (no timetable)
┌────────────────────────────────────────┐
│ Subject ▾ DSA · CSE Sem 3 · A │
│ Date ▾ 13 Aug 2026 │
├────────────────────────────────────────┤
│ 1AB21CS001 Aditya P A L E │
│ 1AB21CS002 Bhavana P A L E │
└────────────────────────────────────────┘
The roster and marking components are reused verbatim from
CollegeAttendancePage — only the context-selection layer above them is new.
Note that /attendance is already the learner's route. It stays one path,
role-dispatched, which is how the sidebar already treats it: learnerNavigation
and trainerNavigation both point at /attendance and each role renders its
own view.
/admin/attendance — org_admin / unit_manager (ERP administration)¶
| Tab | State |
|---|---|
| Face Enrollment | Exists as FaceEnrollmentPage. Becomes the primary tab. |
| ~~Consent~~ | Not built — it already exists. Face Enrollment's Dashboard sub-tab shows the consent counts and its Students sub-tab is the filterable roster with the bulk "send consent requests" action. A separate tab would have duplicated working code for navigational prominence alone. Revisit only if admins report they cannot find it. |
| Analytics | Exists as StatisticsTab, unchanged. |
| Regularize | New. Find a record → correct it → reason required → writes the audit trail. Not as thin as it looks: the routes exist but PATCH /attendance/{record_id} and POST /attendance/{record_id}/verify are both 501 stubs today, so the service logic is real work. |
| ~~Mark Attendance~~ | Removed. This is the fix. |
Instructors landing on /admin/attendance redirect to /attendance, so
existing bookmarks and the mobile deep links keep working.
Learner /attendance and guardian /parent/attendance are untouched.
User flows¶
Prof. Ravi marks his 9am class (timetable present):
Sidebar → Attendance → today's sessions listed, 09:00 DSA shows [Mark]
→ tap → roster for CSE Sem 3 A, all defaulted Present
→ tap two absentees → Submit
→ session row flips to ✓ Marked
Three taps from the sidebar, no pickers.
Dr. Anita audits consent:
Campus → Attendance → Consent tab
→ roster: 412 students, 380 consented, 26 pending, 6 rejected
→ filter Pending → select all → Send consent requests
She is never offered a marking tool.
Mr. Suresh corrects a record for a faculty member on leave:
Campus → Attendance → Regularize
→ find student + subject + date
→ change Absent → Excused, reason "medical certificate submitted 12 Aug"
→ save → record shows corrected-by Mr. Suresh, original value retained
Design decisions¶
Admins regularize; they do not mark. OpenEduCat's Manager has full CRUD, and the honest reason is corrections, not routine marking. Giving admins a full marking tool is what produced the bug in the first place. Giving them nothing would mean any correction requires the original faculty member to log in and redo it — unworkable when faculty leave mid-semester or a section's teacher is unassigned. Regularize-with-audit is the narrow middle: it covers every real correction case without putting a classroom tool on a registrar's desk.
The teacher's session list is served by the attendance router, not the
timetable router. GET /erp/timetable/teachers/{user_id} exists and returns
exactly the right data, but it is gated by
require_erp_module(FeatureKey.ERP_TIMETABLE, "timetable"), which admits staff
holding a timetable module grant — instructors generally do not have one. Rather
than widen that gate, we add a self-scoped endpoint under the attendance
router's own gate:
scoped to current_user.id, reading TimetableEntry plus Substitution for
that weekday, annotated with whether a record already exists for that
section + subject + date. require_attendance_feature was written precisely
because attendance "serves teachers, admins, students AND parents with their own
per-endpoint role checks" and deliberately is not require_erp_module. This
is the same pattern as the Library borrower flow (/erp/library/my-loans +
require_erp_module_borrower), which solved the identical problem.
Empty array is the fallback trigger. When the endpoint returns no sessions — no timetable published, or none today — the frontend renders the subject-first picker. One endpoint decides which shape the teacher sees; the frontend has no separate "does this tenant have a timetable" query.
No migration, but Regularize is not free. FaceAttendanceRecord already
carries marked_by (non-null), verified_by (nullable), notes and status,
so no schema change is needed. The routes exist too — but
PATCH /attendance/{record_id} and POST /attendance/{record_id}/verify are
both 501 Not Implemented stubs with a # TODO where the logic should be. The
guards are right (require_teacher_or_admin and require_admin respectively);
the service methods have to be written. Corrections must require a reason,
preserve the original marked_by, and 404 rather than 403 on a record in
another tenant.
Rejected alternatives¶
Session-first only, strictly like OpenEduCat. The cleanest model, and it makes "unmarked session" a first-class concept Phase 2 could key off directly. Rejected because timetable adoption is not universal: any tenant without a published timetable gets an empty teacher screen — recreating the exact failure this doc exists to fix, only pointed at faculty instead of admins.
Subject-first only — just add the nav entry and move the page. Smallest possible change, no new endpoint, ships in a day. Rejected because the teacher still hand-picks subject and date every single session, and "which of my classes today have I not marked?" stays invisible to the person best placed to fix it. We would be re-filing the problem rather than solving it.
Keep admin marking, scoped properly — give admins an org-scoped section and subject picker instead of their own empty teaching assignments. This would have made the observed screen work. Rejected because it entrenches the boundary violation: a registrar would be calling the roll for a class they were not in, with no session context and no accountability for who was actually present. Regularize covers the legitimate subset of this with an audit trail attached.
Ship the split and the compliance view together. Admins would get a genuinely useful page on day one rather than one that is merely no longer broken. Rejected on sequencing, not merit: the compliance view needs its own endpoint and a timetable-coverage query, and holding the broken-tab fix behind it leaves Dr. Anita staring at "contact your administrator" for weeks longer than necessary. Phase 2 follows immediately.
Phasing¶
Phase 1 — the split (this spec)
- Instructor sidebar entry for
/attendance; role-dispatched page. GET /erp/attendance/my/sessionsand the session-first teacher view, with the existing subject-first picker as fallback./admin/attendanceloses the Mark tab; gains Consent as a first-class tab and a Regularize tab backed by real implementations of the two 501 stubs.- Instructor redirect from
/admin/attendanceto/attendance.
Almost entirely re-homing components that already exist, plus one endpoint.
Phase 2 — compliance (separate spec)
- Unmarked-session view: which sessions went unmarked, by which faculty, on which day, filterable by department and date range.
- Defaulter and shortage reports against a configurable minimum-attendance threshold (the VTU 75% rule), exportable.
This is what institutions actually buy attendance for, and it is the section that makes the admin-side Sean Ellis counterfactual sharp.
Risks & mitigations¶
| Risk | Mitigation |
|---|---|
/attendance already belongs to learners; role-dispatching one path could leak a teacher view to a student or vice versa. |
The path is already role-dispatched in the sidebar. Route guard asserts role before render, and an integration test per role asserts the correct component mounts. |
Instructors have muscle memory for /admin/attendance (it is the only path that works today). |
Redirect rather than 404, and keep it indefinitely — it costs one route. |
| Removing the Mark tab could look like removing a capability to an admin who was using it. | Nobody can be using it: the tab is driven by the caller's own teaching assignments, so it is empty for every admin who does not teach. Worth confirming against staging telemetry before the PR rather than assuming. |
| The session list could disagree with what the teacher actually taught (substitutions, cancelled classes). | The endpoint reads Substitution alongside TimetableEntry, which is why it cannot simply filter the section grid. Substituted sessions appear for the substitute, not the original. |
| Faculty at timetable-less tenants perceive the "new" flow as unchanged and assume nothing shipped. | Expected and acceptable — the sidebar entry alone is the win for them. Do not add a banner explaining the absent timetable. |
Success signals¶
- Zero admin sessions landing on a "No subjects assigned" empty state.
- Instructor-marked sessions originating from web rise from a baseline of
exactly zero — verified on prod 2026-08-14,
erp.face_attendance_recordsis empty and has always been. Prod also holds only 6 instructors, so it will not produce a usable signal; measure on the Sapthagiri staging tenants. - Median taps from sidebar to submitted roster ≤ 3 at timetable tenants.
- No increase in attendance-related support contacts from faculty.
Changelog¶
- 2026-08-13 — Doc created. Product Mindset Gate passed. Written before implementation, prompted by an observed empty Mark Attendance tab on the Sapthagiri University staging tenant. This doc is partly a retrofit: the attendance system it restructures was built without a product doc, and attendance.md describes it as-built without a "Why this exists" section. That gap is a fair part of why the boundary drifted.
- 2026-08-14 — Phase 1 implemented. Three findings changed the spec during
the build, all recorded above: (1)
/attendancehad to become one role-dispatched route inSharedRoutesrather than one per role tree, because every tree composes into a single<Route>parent and a duplicate path would silently shadow the learner's; (2) the Consent tab was dropped — the roster already exists in full inside Face Enrollment; (3) prod carries zero attendance records and only 6 instructors, so the "rise from a near-zero baseline" metric below cannot be measured there. Validation has to come from the Sapthagiri staging tenants. - 2026-08-13 — Corrected during implementation planning: the doc originally
claimed Regularize "composes existing primitives" because
POST /attendance/{record_id}/verifyexists. The route exists; its body is a501stub, as isPATCH /attendance/{record_id}. Regularize needs real service logic. No migration is still needed.