Skip to content

Migrate attendance viewsets to serializer-derived values pattern #14824

@rtibbles

Description

@rtibbles

This issue is not open for contribution. Visit Contributing guidelines to learn about the contributing process and how to find suitable issues.

Overview

Migrate AttendanceSessionViewSet and AttendanceRecordViewSet from explicit values tuples to the serializer-derived pattern as part of the BaseValuesViewset refactor (#14036). The attendance app postdates #14036's original scoping, so it's missing from the affected-viewsets table and sub-issues. As the only issue touching this app, it also moves the code into kolibri/core/attendance/viewsets/ and deletes the old modules, per the File Organization convention.

Complexity: Medium
Target branch: develop

Context

Both viewsets live in kolibri/core/attendance/api.py, with AttendanceRecordSerializer and AttendanceSessionSerializer in serializers.py:

  • AttendanceSessionViewSet(ValuesViewset) has serializer_class = AttendanceSessionSerializer plus an explicit values tuple including the present_count/total_count annotations and the date_created/date_modified model fields — none of which are serializer fields. Conversely, the serializer's nested attendance_records (write path) is excluded from values.
  • AttendanceRecordViewSet(ReadOnlyValuesViewset) has no serializer_class at all — reads are driven purely by its values tuple, with user_name/user_username annotations.
  • Both viewsets have custom actions (recent, bulk_update) that call queryset.values(*self.values) directly and build their own Response, bypassing the standard serialize() path.

The Change

Make the serializers the source of truth for the read path:

  • Extend AttendanceSessionSerializer with the read-path fields currently only in values (present_count, total_count, date_created, date_modified), and mark the nested attendance_records write-only so the read output is unchanged.
  • Define a read serializer for AttendanceRecordViewSet covering its current output (id, user, present, attendance_session, user_name, user_username).
  • Remove the explicit values tuples, and rework the recent and bulk_update actions to serialize via the standard path rather than queryset.values(*self.values).

See #14036 for the overall pattern and FacilityUserViewSet for a concrete example (currently in kolibri/core/auth/api.py; moving to kolibri/core/auth/viewsets/facility_user.py in #14817).

Then relocate: migration commits first, in place; then a pure-move commit creating kolibri/core/attendance/viewsets/ and moving both viewsets with their serializers inline (plus filters and permission classes). Update imports (api_urls.py, tests) and delete api.py and serializers.py.

Moving code breaks GitHub blame (local git blame -C -C still works), so digest the history the move obscures:

  1. Run blame over the code being moved; flag hunks whose purpose isn't evident from the code or tests (odd guards, magic orderings, special cases).
  2. Chase each flagged hunk to its originating PR/issue; where the rationale is non-obvious and load-bearing, carry it forward as a code comment citing the reference.
  3. Where the rationale turns out to be obsolete, remove the code as part of the migration commits rather than annotating it.

Out of Scope

  • Changes to the API response shape — the API contract must not change
  • Modifications to existing tests — if tests need to change, the migration is wrong
  • Write-path behavior changes — create/update/bulk_update semantics stay identical; only bulk_update's response serialization is reworked

Acceptance Criteria

  • Explicit values tuples removed from both viewsets
  • AttendanceSessionSerializer carries all read-path fields, with attendance_records write-only; AttendanceRecordViewSet has a read serializer
  • recent and bulk_update serialize via the standard path, with unchanged response output
  • Both viewsets, their serializers (inline), filters, and permission classes live in kolibri/core/attendance/viewsets/; api.py and serializers.py are deleted
  • Migration commits precede the pure-move commit, which is separate from import updates
  • Existing API tests pass unchanged
  • Benchmark comparison passes for both viewsets (data hash match + no performance regression)
  • Comments citing the originating PR/issue added where blame digestion found non-obvious, load-bearing rationale

Testing

  • Existing coverage is good (40 API tests in kolibri/core/attendance/test/test_api.py); they must pass before and after without modification.
  • Run the benchmark for both viewsets, baseline with the old dotted path, comparison with the new one:
    python integration_testing/scripts/viewset_serialization_benchmark.py \
        kolibri.core.attendance.api.AttendanceSessionViewSet \
        --inherit-kolibri-home -o attendance_session_baseline.json
    
    # ... migrate and move ...
    
    python integration_testing/scripts/viewset_serialization_benchmark.py \
        kolibri.core.attendance.viewsets.<module>.AttendanceSessionViewSet \
        --inherit-kolibri-home --compare attendance_session_baseline.json
    Likewise for AttendanceRecordViewSet. Both comparisons must pass; include the before/after output in the PR description.
  • Benchmark data setup: populate users via kolibri manage generateuserdata, then create attendance data in kolibri manage shell:
    from django.utils import timezone
    
    from kolibri.core.attendance.models import AttendanceRecord
    from kolibri.core.attendance.models import AttendanceSession
    from kolibri.core.auth.models import Classroom
    from kolibri.core.auth.models import FacilityUser
    
    classroom = Classroom.objects.first()
    coach = FacilityUser.objects.first()
    learners = list(FacilityUser.objects.filter(memberships__collection=classroom))
    for i in range(20):
        session = AttendanceSession.objects.create(
            collection=classroom,
            created_by=coach,
            session_start_datetime=timezone.now(),
        )
        for user in learners:
            AttendanceRecord.objects.create(
                attendance_session=session,
                user=user,
                present=bool(i % 2),
            )

AI usage

Drafted with AI assistance (Claude Code) from an interactive design discussion, section by section, with review and final edits by @rtibbles. Viewset internals, serializer contents, custom-action serialization paths, and test coverage were verified against the codebase during drafting.

Metadata

Metadata

Assignees

Labels

DEV: backendPython, databases, networking, filesystem...TAG: tech update / debtChange not visible to user

Type

No fields configured for Task.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions