Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/muhammadbugaje/gobarau_backend/llms.txt

Use this file to discover all available pages before exploring further.

Gobarau Backend uses a layered identity model that cleanly separates authentication concerns from biographical data and role-specific records. At the base sits a User row that handles login credentials and role assignment. Linked one-to-one to that is a Person row that holds every human-facing detail about the individual — name, date of birth, contact information, and photo. On top of Person sit role-specific profile models (StudentProfile, TeacherProfile, ParentProfile, AlumniProfile) in the people app, each adding the fields relevant to that role without polluting the shared identity tables. Domain records — attendance, scores, health entries, payments — then hang off these profile models, keeping each app’s data scoped to the role it serves.
All models that extend BaseModel (or any of its subclasses) expose a public_id UUID field as the external-facing identifier. Always use public_id when referencing records in API requests and responses — never rely on the internal integer id.

Identity Layer

The identity layer consists of exactly two tables, both in the accounts app.

User — Authentication Record

User extends Django’s AbstractUser and is set as the project’s AUTH_USER_MODEL. It stores credentials (username, password, email) and carries two additional classification fields: role and wing.
# apps/accounts/models.py
class User(AbstractUser):
    """Custom user model with role and wing assignment."""
    role = models.CharField(
        max_length=20,
        choices=RoleChoices.choices,
        default=RoleChoices.STUDENT,
        db_index=True,
    )
    wing = models.CharField(
        max_length=20,
        choices=WingChoices.choices,
        default=WingChoices.REGULAR,
        db_index=True,
    )
    is_verified = models.BooleanField(default=False)
    preferences = models.JSONField(default=dict, blank=True)
FieldDescription
roleOne of ten RoleChoices values; gates permission class access
wingOne of three WingChoices values; scopes academic programme
is_verifiedBoolean flag set after identity or email verification
preferencesFree-form JSON for per-user UI and notification preferences

Person — Rich Identity Profile

Person is the canonical biographical record for every human in the system — students, teachers, parents, alumni, and staff alike. It is linked one-to-one to User via the user FK, but that link is nullable: a Person can exist before a User account is created (e.g. for a parent who has not yet been invited to log in). Person inherits from both SoftDeleteModel and AuditMixin, meaning every person record is soft-deletable and carries created_by / updated_by audit fields.
# apps/accounts/models.py
class Person(SoftDeleteModel, AuditMixin):
    """Single identity record for every human in the system."""
    user = models.OneToOneField(
        "accounts.User",
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="person",
        db_index=True,
    )
    first_name = models.CharField(max_length=100)
    last_name = models.CharField(max_length=100)
    middle_name = models.CharField(max_length=100, blank=True, default="")
    preferred_name = models.CharField(max_length=100, blank=True, default="")
    date_of_birth = models.DateField(null=True, blank=True)
    gender = models.CharField(max_length=1, choices=GenderChoices.choices, ...)
    state_of_origin = models.CharField(max_length=50, choices=NIGERIAN_STATES, ...)
    lga = models.CharField(max_length=100, blank=True, default="")
    religion = models.CharField(max_length=50, blank=True, default="")
    nationality = models.CharField(max_length=50, default="Nigerian")
    phone_primary = models.CharField(max_length=20, blank=True, default="")
    phone_secondary = models.CharField(max_length=20, blank=True, default="")
    email_personal = models.EmailField(blank=True, default="")
    address = models.TextField(blank=True, default="")
    photo = models.ImageField(upload_to="people/photos/", blank=True, null=True)
    national_id = models.CharField(max_length=50, blank=True, default="")
    external_ref = models.CharField(max_length=100, blank=True, default="")
    language = models.CharField(max_length=50, default="English")
    metadata = models.JSONField(default=dict, blank=True)
The full_name property joins first_name, middle_name, and last_name, omitting blank parts automatically.

Role Profile Layer

Once a Person record exists, a role-specific profile can be created for them in the people app. Each profile model links back to Person via a one-to-one person FK and adds the fields relevant to that role.

StudentProfile

Holds academic identity and current class placement.
class StudentProfile(SoftDeleteModel, AuditMixin):
    person = models.OneToOneField('accounts.Person', ...)
    admission_number = models.CharField(max_length=20, unique=True)
    enrollment_status = models.CharField(choices=EnrollmentStatusChoices.choices, ...)
    date_admitted = models.DateField(null=True, blank=True)
    class_assigned = models.ForeignKey('academics.Class', ...)
    wing = models.ForeignKey('administration.Wing', ...)
    campus = models.ForeignKey('administration.Campus', ...)
FieldDescription
admission_numberUnique identifier assigned at admission; used in display and reporting
enrollment_statusOne of: active, graduated, withdrawn, transferred, repeated
class_assignedThe student’s current class; nullable to allow unplaced students
wingFK to the Wing record (Regular / Islamiyyah / Tahfeez)
campusFK to the Campus record; supports multi-campus deployments

TeacherProfile

Holds employment and qualification data for teaching staff.
class TeacherProfile(SoftDeleteModel, AuditMixin):
    person = models.OneToOneField('accounts.Person', ...)
    staff_number = models.CharField(max_length=20, unique=True)
    date_employed = models.DateField(null=True, blank=True)
    qualification = models.CharField(max_length=200, blank=True)
    specialization = models.CharField(max_length=200, blank=True)
    is_active = models.BooleanField(default=True)
    show_on_website = models.BooleanField(default=True)
FieldDescription
staff_numberUnique staff identifier used in HR and payroll references
qualificationHighest academic or professional qualification
specializationSubject area or departmental specialism
show_on_websiteControls whether the teacher appears on public-facing staff listings

ParentProfile

Holds guardian-specific information for parents and guardians.
class ParentProfile(SoftDeleteModel, AuditMixin):
    person = models.OneToOneField('accounts.Person', ...)
    occupation = models.CharField(max_length=200, blank=True)
    employer = models.CharField(max_length=200, blank=True)
    is_primary_guardian = models.BooleanField(default=False)
FieldDescription
occupationParent’s declared occupation
employerParent’s current employer
is_primary_guardianFlags the principal guardian when a student has multiple parent records

AlumniProfile

Created when a student graduates, linking their new alumni identity back to their original student profile.
class AlumniProfile(SoftDeleteModel, AuditMixin):
    person = models.OneToOneField('accounts.Person', ...)
    student_profile = models.OneToOneField('people.StudentProfile', null=True, ...)
    graduation_year = models.PositiveIntegerField()
    final_class = models.ForeignKey('academics.Class', ...)
    career_field = models.CharField(max_length=200, blank=True)
    current_employer = models.CharField(max_length=200, blank=True)
    university_attended = models.CharField(max_length=200, blank=True)
    city = models.CharField(max_length=100, blank=True)
    country = models.CharField(max_length=100, default='Nigeria')
    linkedin_url = models.URLField(blank=True)
    is_available_for_mentorship = models.BooleanField(default=False)
    mentorship_areas = models.TextField(blank=True)
    is_verified = models.BooleanField(default=False)
    is_on_wall_of_fame = models.BooleanField(default=False)
FieldDescription
graduation_yearYear the alumnus graduated
student_profileOptional back-link to the original StudentProfile record
is_available_for_mentorshipOpt-in flag for the mentorship feature
is_on_wall_of_fameControls public wall-of-fame listing

Enrollment and Relationships

ClassEnrollment

ClassEnrollment records a student’s formal placement in a specific class for a given academic session and term. It is the temporal record of class membership — a student’s class_assigned FK on StudentProfile is their current placement, while ClassEnrollment rows preserve the full history.
class ClassEnrollment(BaseModel):
    student = models.ForeignKey('people.StudentProfile', related_name='enrollments', ...)
    class_assigned = models.ForeignKey('academics.Class', related_name='enrollments', ...)
    session = models.ForeignKey('administration.AcademicSession', ...)
    term = models.ForeignKey('administration.Term', ...)
    date_enrolled = models.DateField(auto_now_add=True)
    is_active = models.BooleanField(default=True)

    class Meta:
        unique_together = ('student', 'class_assigned', 'session', 'term')
A unique_together constraint prevents duplicate enrollment records for the same student, class, session, and term combination.

WardRelationship

WardRelationship links a ParentProfile to a StudentProfile, establishing who is authorised to act as guardian for a given student. One student can have multiple guardian links; one parent can be linked to multiple students.
class WardRelationship(BaseModel):
    parent = models.ForeignKey('people.ParentProfile', related_name='wards', ...)
    student = models.ForeignKey('people.StudentProfile', related_name='guardians', ...)
    relationship_type = models.CharField(max_length=50, blank=True)
    is_primary_guardian = models.BooleanField(default=False)
    can_pickup = models.BooleanField(default=True)

    class Meta:
        unique_together = ('parent', 'student')
The can_pickup flag is used by the transport and services apps to determine whether a parent is authorised to collect their ward from school.

Academic Records

A StudentProfile acts as the anchor point for all academic data tracked by the academics app. The following models link directly or indirectly to a student profile:
ModelApp moduleDescription
AttendanceRecordacademics.attendancePer-lesson or per-day attendance status for a student
AttendanceSummaryacademics.attendanceAggregated attendance counts per term
Scoreacademics.scoresCA and exam scores per subject per term
ReportCardacademics.scoresCompiled end-of-term report card record
Assignmentacademics.scoresAssignment definition issued by a teacher
AssignmentSubmissionacademics.scoresA student’s submission against an assignment
ExamRegistrationacademics.examsStudent registration for a formal exam sitting
ExamResultacademics.examsRecorded result from a formal exam
JuzProgressacademics.tahfeezTahfeez-wing students’ memorisation progress per Juz
RecitationSessionacademics.tahfeezIndividual recitation sessions with a teacher
The JuzProgress and RecitationSession models are only meaningful for students in the Tahfeez wing. All other wings will not have rows in these tables.

Soft Delete Pattern

Models that inherit SoftDeleteModel are never hard-deleted from the database. Instead, calling .archive(user) sets is_archived=True, records the timestamp in archived_at, and captures the requesting user in archived_by. Calling .restore() reverses this. Three managers control queryset visibility:
# core/managers.py
class ActiveManager(models.Manager):
    """Returns only non-archived records."""
    def get_queryset(self):
        return super().get_queryset().filter(is_archived=False)

class ArchivedManager(models.Manager):
    """Returns only archived records."""
    def get_queryset(self):
        return super().get_queryset().filter(is_archived=True)

class SoftDeleteManager(models.Manager):
    """Returns all records regardless of archive state."""
    def get_queryset(self):
        return super().get_queryset()
These are attached to SoftDeleteModel as:
ManagerAccessQueryset
.objectsStudentProfile.objects.all()Active records only (is_archived=False)
.all_objectsStudentProfile.all_objects.all()Every record, archived or not
.archivedStudentProfile.archived.all()Archived records only (is_archived=True)
The default .objects manager filters out archived records automatically. If you need to inspect or restore archived data — for example in admin operations or data migrations — always use .all_objects or .archived explicitly.

Audit Trail

Models that inherit AuditMixin carry two FK fields pointing back to accounts.User:
# core/models.py
class AuditMixin(BaseModel):
    """Abstract mixin tracking who created/modified a record."""
    created_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="+",
        db_index=True,
    )
    updated_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="+",
        db_index=True,
    )
FieldDescription
created_byThe User who created the record; set once at creation and never updated
updated_byThe User who last modified the record; updated on every subsequent save
Both FKs use on_delete=models.SET_NULL, so deleting a user account does not cascade to and destroy the records they created or modified — the FK is simply nullified, preserving the data integrity of the audit trail. AuditMixin is combined with SoftDeleteModel on key models such as Person, StudentProfile, TeacherProfile, ParentProfile, and AlumniProfile, giving those tables a complete record of who created them, who last touched them, who archived them, and when.

Build docs developers (and LLMs) love