Back to blog
Article

Same student record, three access levels in a school ERP

Same student record, three access levels in a school ERP
S

StriveBit

4 min readCustom Software

Same student record, three access levels in a school ERP

A teacher opens a student's profile to mark today's attendance. She should not see the family's income bracket, which the administration collected during admission. The parent of the same student opens the same profile to check whether homework was submitted. They should not see the teacher's internal notes about behavioural concerns. The principal opens it to review academic performance across the term. They should not see individual fee transaction receipts unless they specifically request them from the accounts team.

The record is one row in a database. The access is three different applications built on top of it.

We learned this the hard way on a school ERP project last year. The first version had a single `Student` model with a `can_view` permission check at the controller level. A teacher, an admin, and a parent all passed `can_view: true` for the same student ID. The problem was that they all got the same JSON payload back — every field, every time.

The fix was not more roles. The fix was field-level serialization tied to the viewer's relationship to the record.

A teacher's relationship to a student is `teaches_class`. A parent's relationship is `guardian_of`. An admin's relationship is `staff_member`. Each relationship maps to a specific serialization context.

SERIALIZERS = {
    "teacher": TeacherStudentSerializer,
    "parent": ParentStudentSerializer,
    "admin": AdminStudentSerializer,
}

def get_student(request, student_id):
    student = Student.objects.get(id=student_id)
    if not can_access(request.user, student):
        raise PermissionDenied
    role = role_for(request.user, student)
    serializer = SERIALIZERS[role]
    return serializer(student).data

`TeacherStudentSerializer` exposes attendance, grades, and homework submission status. `ParentStudentSerializer` exposes homework assignments, upcoming tests, fee dues, and the class teacher's contact. `AdminStudentSerializer` exposes everything, including fee history, disciplinary records, and family financial details.

The tradeoff is maintenance. Three serializers for one model means three places to update when the schema changes. We considered a single serializer with a field whitelist, but it made the authorization logic harder to test. With separate serializers, each one has a focused test suite — you test the teacher serializer against the fields a teacher should see, and nothing else leaks in.

There is a second layer of complexity. Parents sometimes have more than one child. A parent with two children in different sections should not see other students in those sections. The `role_for` function handles this by checking whether the student ID is in the parent's list of dependents.

def role_for(user, student):
    if user.is_staff:
        return "admin"
    if student.id in user.dependent_ids:
        return "parent"
    if student.class_id in user.taught_class_ids:
        return "teacher"
    raise PermissionDenied

The `PermissionDenied` at the end matters. A teacher who teaches Class 6-A should not get a 404 when they request a student from Class 6-B — they should get a 403. A 404 tells them the student does not exist. A 403 tells them the student exists but they cannot access it. In a school setting, that distinction prevents confusion when a teacher searches for a student by name and gets inconsistent results.

We also had to handle the case where a parent is also a teacher at the same school. This is common in smaller schools. The `role_for` function checks `is_staff` first, which means a teacher who has a child enrolled will always see their own child as an admin, not as a parent. That was a deliberate decision — the admin view is a superset of the parent view, so no information is lost. The reverse would be worse: a teacher seeing their own child's admin-level data while other teachers get the parent view.

One thing we did not do is build a generic role-permission matrix. The school has six roles — teacher, class coordinator, section head, principal, accounts, parent — and the permissions do not map cleanly to a hierarchy. A class coordinator can see academic data for their section but not fee data. Accounts can see fee data but not academic records. A matrix would have required us to encode every field against every role, and the school's requirements were still shifting during development. Hardcoding the three serializers was faster, and when the requirements settled, the code was already readable enough to extend.

The school's registrar still emails us occasionally when a new field is added to the admission form. The change is usually a one-line addition to `AdminStudentSerializer` and a decision about whether the parent serializer should expose it. That conversation takes ten minutes. A generic matrix would have turned it into a configuration change with a testing matrix six roles wide.

Back to all articles

Ready to build something great?

We help ambitious teams build software that lasts. If you're interested in working with us or want to discuss your project, let's connect.

Get in touch