Skip to content

Move date_modified forward when an attendance session is edited - #15671

Merged
rtibbles merged 6 commits into
learningequality:developfrom
namanjain24-sudo:fix-attendance-date-modified
Nov 2, 2026
Merged

rtibbles merged 6 commits into
learningequality:developfrom
namanjain24-sudo:fix-attendance-date-modified

Conversation

@namanjain24-sudo

@namanjain24-sudo namanjain24-sudo commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Summary

AttendanceSession.date_modified is published through the API but never moves. It is declared with a creation-time default and no auto-update, and nothing assigns it anywhere else:

date_created = DateTimeTzField(default=local_now, editable=False)
date_modified = DateTimeTzField(default=local_now)

The serializer also lists it in read_only_fields, so a client cannot set it and update never receives it in validated_data. The result is that editing a session — including flipping a learner's present flag through the nested attendance_records write path — leaves date_modified reporting the creation time.

Other modified/updated timestamps in the tree are kept current, either with auto_now (device/models.py:517 and :629, discovery/models.py:102) or by explicit assignment (channel.last_updated = local_now() in channel_import.py:996). This one is neither.

Correction: an earlier version of this description said it was the only such field that is neither. That was wrong — Job.time_updated (kolibri/core/tasks/models.py:32) is never written either, as @rtibblesbot pointed out on #15670. I've verified that and corrected the claim here.

References

Fixes #15670

Reviewer guidance

  • pytest kolibri/core/attendance/test/test_api.py -k date_modified is the regression test. Against develop it fails with the two timestamps byte-for-byte equal; it passes here.
  • pytest kolibri/core/attendance/ — 60 passed. prek run --files … clean.
  • Why the serializer and not auto_now on the model. Morango deserialization saves through the same path (app_model.save(update_dirty_bit_to=False), morango/sync/operations.py:454), so an auto_now field would be rewritten with the receiving device's clock every time a session synced in, discarding the value the authoring device sent. Setting it in update confines the change to genuine API edits. I verified this rather than assuming it: a session given a date_modified three days in the past and then saved the way deserialization saves keeps that value unchanged on this branch.
  • This also matches how channel.last_updated — the other DateTimeTzField of this kind — is handled.
  • Severity is low and I would rather say so than oversell it: I could not find a frontend consumer of the field today. It is worth fixing because the field is already part of the serialized API surface, so a stale value is what any future consumer would be built on, and a wrong timestamp is harder to notice than a missing one.

AI usage

Claude Code (on my instruction to keep scanning the newer code) spotted that date_modified had no auto-update while reading the attendance models for an unrelated issue, and I had it confirm the behaviour before anything was written. It drove the real API as a coach to show the value unchanged after a PATCH that genuinely flipped the attendance, surveyed how every other modified timestamp in the tree is maintained — which is what makes this inconsistent rather than deliberate — and traced morango's deserialization save path to establish that auto_now would be the wrong fix here. It then worked test-first, with the regression test confirmed red before the change, and afterwards verified empirically that a synced-in value survives a deserialization-style save. I reviewed the survey of the other timestamp fields, the decision to put the fix in the serializer, and every test run above before opening this PR.

AttendanceSession.date_modified was declared with a creation-time default
and no auto-update, and nothing ever assigned it, so it reported the
creation time no matter how often the session's attendance was edited.
Every other modified timestamp in the tree is kept current, either with
auto_now or by explicit assignment.

Set it in the serializer's update rather than with auto_now on the model:
deserialization saves through the same path, so auto_now would overwrite
a value authored on another device with the receiving device's clock.

Fixes learningequality#15670
@namanjain24-sudo
namanjain24-sudo requested review from a team and leq-bot October 25, 2026 01:05
@github-actions github-actions Bot added DEV: backend Python, databases, networking, filesystem... SIZE: small labels Oct 25, 2026
@learning-equality-bot

Copy link
Copy Markdown
Contributor

👋 Hi @namanjain24-sudo, thanks for contributing!

For the review process to begin, please verify that the following is satisfied:

  • Contribution is aligned with our contributing guidelines

  • Pull request description has correctly filled AI usage section & follows our AI guidance:

    AI guidance

    State explicitly whether you didn't use or used AI & how.

    If you used it, ensure that the PR is aligned with Using AI as well as our DEEP framework. DEEP asks you:

    • Disclose — Be open about when you've used AI for support.
    • Engage critically — Question what is generated. Review code for correctness and unnecessary complexity.
    • Edit — Review and refine AI output. Remove unnecessary code and verify it still works after your edits.
    • Process sharing — Explain how you used the AI so others can learn.

    Examples of good disclosures:

    "I used Claude Code to implement the component, prompting it to follow the pattern in ComponentX. I reviewed the generated code, removed unnecessary error handling, and verified the tests pass."

    "I brainstormed the approach with Gemini, then had it write failing tests for the feature. After reviewing the tests, I used Claude Code to generate the implementation. I refactored the output to reduce verbosity and ran the full test suite."

📢✨ Before we assign a reviewer:

  • @rtibblesbot will pre-review this pull request. Its comments are generated by an LLM, and should be evaluated accordingly.
  • We'll also invite community pre-review. See the community review guidance for both authors and reviewers.

Also check that issue requirements are satisfied & you ran pre-commit locally.

Pull requests that don't follow the guidelines will be closed.

Reviewer assignment can take up to 2 weeks.

@namanjain24-sudo

Copy link
Copy Markdown
Contributor Author

Confirming both checklist items:

  • Contributing guidelines: small, contained bug fix with a regression test, linked to AttendanceSession.date_modified never updates after creation #15670 which describes the reproduction, and I ran pytest kolibri/core/attendance/ (60 passed) and prek locally before opening it. Same flag as on my other open PRs: AttendanceSession.date_modified never updates after creation #15670 is a bug I reported myself and hasn't been triaged or assigned, so this isn't against an assigned help wanted issue — happy to close and wait for triage if you'd prefer. I'd also rather be upfront that this is the lowest-severity of the ones I've filed; I found no frontend consumer of the field today, so it is a correctness gap in the published API surface rather than something users are hitting.
  • AI usage: the section in the description is accurate per DEEP. Claude Code found this while reading the attendance models for AttendanceRecord does not require its user and session to share a dataset #15666 and wrote the fix and test on my instruction, test-first — the test went in before the change and was confirmed red with the two timestamps byte-for-byte equal. The judgement call I reviewed most carefully was where the fix belongs: auto_now on the model is the obvious move but would let deserialization overwrite a synced-in value with the receiving device's clock, so it goes in the serializer instead, and that reasoning was checked by actually saving a session the way morango does and confirming the authored value survives.

@github-actions

github-actions Bot commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Setting date_modified in the session serializer's update did not cover
bulk_update, which edits a session's records through its own endpoint
and left the session's timestamp at its creation value.
Both attendance fixes add a test immediately before
test_bulk_update_creates_and_updates_records, which made them conflict
textually despite touching unrelated code. Move this one after it.
@namanjain24-sudo

Copy link
Copy Markdown
Contributor Author

Pushed two follow-up commits.

The first is a gap in my own fix that I'd rather surface than leave for review: setting date_modified in AttendanceSessionSerializer.update does not cover bulk_update, which edits a session's records through its own endpoint on AttendanceRecordViewSet. That is likely the busier path in practice — marking a whole class at once — and it left the session's timestamp at its creation value. Verified on this branch before fixing: date_modified changed by bulk_update = False.

The second is housekeeping. Both attendance branches were inserting a new test immediately before test_bulk_update_creates_and_updates_records, so they conflicted textually even though the code they change is unrelated. I moved this one after that test, and confirmed #15667 and this branch now merge together cleanly with both tests present and the suite green (63 passed on the combined tree). There should be nothing to hand-resolve whichever order they land in.

pytest kolibri/core/attendance/ is 61 passed on this branch on its own.

@rtibbles
rtibbles requested a review from rtibblesbot October 27, 2026 16:31
@learning-equality-bot

Copy link
Copy Markdown
Contributor

📢✨ Before we assign a reviewer, we'll turn on @rtibblesbot to pre-review. Its comments are generated by an LLM, and should be evaluated accordingly.

@rtibblesbot

rtibblesbot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

🟡 Waiting for changes

Outcome: reviewed b83a578

Last updated: 2026-10-28 21:41 UTC

@rtibblesbot rtibblesbot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR #15671: correct fix. Both write paths move date_modified. Morango's save() adds the dirty bit to update_fields, so the narrow save still syncs.

CI passing.

  • suggestion: the new assertGreater checks can tie on Windows CI (inline).
  • nitpick: one comment restates its code (inline).

Next step: Needs a full code review by a human engineer.

  • The author is outside the team: confirm this should be built at all.
  • Confirm that also moving the session's date_modified (a synced field) from the bulk record endpoint, which the issue didn't name, is wanted.

Written by rtibblesbot, an LLM-based coding agent.

Both tests compared two local_now() values one request apart. The Windows
clock ticks every ~15.6ms, so they can tie and the assertGreater fail
intermittently. Backdate the session a minute and compare against that.
@namanjain24-sudo

Copy link
Copy Markdown
Contributor Author

Both addressed, thanks — the tie risk was real and I had waved it away on the grounds that Windows CI happened to pass, which is not the same thing as it being safe.

suggestion — assertGreater can tie on Windows. Both tests now backdate date_modified by a minute with a queryset update and compare against that, so nothing depends on two local_now() calls landing in different clock ticks. I checked both still fail with the date_modified assignment removed from the endpoint they cover, so backdating has not made them pass trivially.

nitpick — the comment at attendance_record.py:136. Deleted. The auto_now one in attendance_session.py is kept, as you suggested; that one records why the field is not auto_now, which the code cannot show.

pytest kolibri/core/attendance/ — 61 passed. prek clean.

@rtibblesbot rtibblesbot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR #15671: all 3 prior findings resolved. No new findings. CI pending.

Prior-finding status

RESOLVED — kolibri/core/attendance/test/test_api.py:326 — assertGreater tie risk on Windows py3.8
RESOLVED — kolibri/core/attendance/test/test_api.py:615 — same tie risk in bulk-update test
RESOLVED — kolibri/core/attendance/viewsets/attendance_record.py — comment restates the next two lines

Next step: Needs a full code review by a human engineer.

  • The author is outside the team: confirm this should be built at all.

Written by rtibblesbot, an LLM-based coding agent.

@rtibbles rtibbles left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fix works; the timestamp should be maintained by the model rather than by each write path.

  • blocking: move the bump into AttendanceSession.pre_save (inline).
  • blocking: add a model test that a session saved with update_dirty_bit_to=False keeps a backdated date_modified. That is the property ruling out auto_now, and nothing tests it.
  • Keep both API tests as they are.

CI passing.

Every new writer had to remember to bump it, and the bulk endpoint already
needed a second copy. AttendanceSession.pre_save does it instead, skipping
the bump when update_dirty_bit_to is False, which is how deserialization
saves -- so a synced-in value is kept rather than overwritten with the
receiving device's clock, the property that ruled out auto_now.

Both write paths drop their own assignment, and a model test covers the
deserialization case, which nothing exercised before.

@rtibblesbot rtibblesbot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR #15671: CI lint fails on the new model test; fix it per the inline comment. All 3 prior findings are resolved.

Prior-finding status

RESOLVED — kolibri/core/attendance/test/test_api.py:326 — assertGreater tie risk on Windows py3.8
RESOLVED — kolibri/core/attendance/test/test_api.py:615 — same tie risk; backdate before the request
RESOLVED — kolibri/core/attendance/viewsets/attendance_record.py — comment restates the next two lines

Next step: Needs a full code review by a human engineer.

  • The author is outside the team: confirm this should be built at all.
  • Confirm bumping date_modified in AttendanceSession.pre_save (skipped only when update_dirty_bit_to is False) is the right rule for every local save path, including ones that don't change attendance content.

Written by rtibblesbot, an LLM-based coding agent.

@namanjain24-sudo

Copy link
Copy Markdown
Contributor Author

Both done, thank you — the model is the right home for it and I had not considered that every future writer inherits the problem otherwise.

blocking — bump in pre_save. AttendanceSession.pre_save now sets date_modified unless update_dirty_bit_to is False, exactly as you wrote it. Both write paths dropped their own assignment, and the bulk endpoint is back to a plain session.save().

blocking — model test for the deserialization case. Added test_deserialized_save_keeps_the_incoming_date_modified: a session with a backdated date_modified saved via save(update_dirty_bit_to=False) keeps it. I checked it is red if the guard is dropped and the bump becomes unconditional, which is the auto_now behaviour it rules out.

Both API tests kept as they are.

pytest kolibri/core/attendance/ — 62 passed. prek clean.

@rtibblesbot rtibblesbot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR #15671: all 4 prior findings are resolved. CI is still running.

Prior-finding status

RESOLVED — kolibri/core/attendance/test/test_api.py:326 — assertGreater tie risk on Windows py3.8
RESOLVED — kolibri/core/attendance/test/test_api.py:615 — same tie risk; backdate date_modified
RESOLVED — kolibri/core/attendance/viewsets/attendance_record.py — comment restates the code
RESOLVED — kolibri/core/attendance/test/test_models.py — ruff-format failure

Next step: Needs a full code review by a human engineer.

  • The author is outside the team: confirm this should be built at all.
  • Confirm bumping AttendanceSession.date_modified in pre_save on every dirty save (skipped only when update_dirty_bit_to=False) suits sync and any consumers ordering by it.

Written by rtibblesbot, an LLM-based coding agent.

@rtibblesbot
rtibblesbot dismissed their stale review October 28, 2026 21:40

Superseded by my review of b83a578: no blocking findings remain.

@rtibbles rtibbles left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent, well contained now! Thanks @namanjain24-sudo!

@rtibbles
rtibbles merged commit ace5202 into learningequality:develop Nov 2, 2026
89 checks passed
@rtibblesbot

Copy link
Copy Markdown
Contributor

Quick check on rtibblesbot's review of this PR: react 👍 helpful · 😕 mixed · 👎 not helpful. Reply with anything specific.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

DEV: backend Python, databases, networking, filesystem... SIZE: small

Projects

None yet

Development

Successfully merging this pull request may close these issues.

AttendanceSession.date_modified never updates after creation

3 participants