Skip to content

6009: add organizations to invitation querysets and sync#6039

Draft
yasinelmi wants to merge 3 commits into
learningequality:unstablefrom
yasinelmi:invitation_api
Draft

6009: add organizations to invitation querysets and sync#6039
yasinelmi wants to merge 3 commits into
learningequality:unstablefrom
yasinelmi:invitation_api

Conversation

@yasinelmi

@yasinelmi yasinelmi commented Jul 12, 2026

Copy link
Copy Markdown

Summary

Following #6008 (which added Invitation.organization), this extends the invitation querysets and the /sync mechanism to actually support organization invitations end-to-end, per #6009.

  • Organization.filter_edit_queryset/filter_view_queryset — new classmethods based on active OrganizationRole membership (admin = edit, admin/editor/viewer = view), mirroring the existing Channel pattern.
  • Invitation.filter_edit_queryset/filter_view_queryset — extended to grant the same access to organization invitations.
  • InvitationSerializer — accepts organization in place of channel; channel is now optional. Validation requires exactly one of channel/organization (not neither, not both) — this is the "Organizations XOR channels" requirement from Add Organizations to Invitation Model #5971 that was deliberately deferred to this API-layer work (per rtibbles' comment on Add Organizations to Invitation API Route (And Sync Events) #6009 and the review thread on 5971: added organizations to invitation model #6008).
  • /sync endpoint (handle_changes) — organization-scoped changes (e.g. an org admin creating/revoking an invitation for someone else) didn't fit either existing permission branch (channel-scoped or self-scoped), so this adds a permission check via Organization.filter_edit_queryset and routes allowed org-scoped changes through the existing per-user change queue, enqueuing apply_user_changes_task for the actual target user rather than just the requester.
  • generate_create_event/generate_update_event/generate_delete_event (viewsets/sync/utils.py) gained an organization_id parameter so events can carry it, matching channel_id.

Scope note: this reuses the existing per-user Change processing queue rather than adding a dedicated organization_id field/task to the Change model (which would mirror channel_id's three roles: permission gate, task routing, and broadcast). That means an org admin creating/revoking an invitation is fully functional and the invitee gets realtime sync, but other org admins won't see it appear live without a page refresh (the list view always reflects live DB state regardless). Full parity with channel-scoped broadcast would need a Change.organization migration + dedicated task, which felt like more than this issue's "Low" complexity warranted, and is expected to overlap with #5967's org permission infrastructure.

Testing

  • New tests in test_models.py (OrganizationTestCase, InvitationOrganizationTestCase) covering the queryset permission logic directly.
  • New tests in test_invitation.py (OrganizationInvitationSyncTestCase) covering create/accept/revoke via /sync, non-admin rejection, the "requires at least one of channel/organization" validation, and a new test asserting an invitation with both channel and organization set is rejected (mutual exclusivity).
  • Full existing suite and pre-commit hooks pass locally.

References

Closes #6009. Builds on #6008 (Invitation.organization field) and #5953 (Organization/OrganizationRole models).

AI usage

Implemented with Claude Code: I worked through the codebase with it to trace how the existing channel-invitation sync mechanism works end-to-end (handle_changes, Change model, apply_channel_changes_task/apply_user_changes_task), identified that organization-scoped changes didn't fit either existing permission branch, and reviewed/directed the resulting implementation and test coverage rather than accepting it as-is. The channel/organization mutual-exclusivity requirement was added after re-reading the #6008 review thread and rtibbles' follow-up comment on #6009 flagging it as still outstanding. This PR is a draft pending review, including coordination with #5967's assignee on the sync-scoping approach described above.

Extends the channel-invitation flow to support organization invitations,
per learningequality#6009 (following learningequality#6008's Invitation.organization field):

- Add Organization.filter_edit_queryset/filter_view_queryset based on
  active OrganizationRole membership, and extend Invitation's equivalents
  to grant org admins edit access and org members view access.
- Allow InvitationSerializer to accept an organization in place of (or
  alongside) a channel, requiring at least one of the two.
- Teach the /sync endpoint's permission gate to recognize organization-
  scoped changes, since org admins acting on another user's invitation
  don't fit the existing channel-scoped or self-scoped checks.
@learning-equality-bot

Copy link
Copy Markdown

👋 Hi @yasinelmi, 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."

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.

…tures

- generate_create_event/generate_update_event/generate_delete_event
  (viewsets/sync/utils.py) never gained an organization_id parameter
  when handle_changes() was taught to read it, causing a TypeError in
  every organization invitation test that passed organization_id.
- Two OrganizationInvitationSyncTestCase fixtures were missing
  invited=self.invited_user, which InvitationSerializer.get_fields()
  requires to unlock the accepted field for sync-based updates,
  matching the existing SyncTestCase fixture pattern.
Per the "Organizations XOR channels" requirement from learningequality#5971 and
rtibbles' comment on learningequality#6009 confirming this was deliberately deferred
to the API layer: InvitationSerializer.validate() now rejects an
invitation that has both channel and organization set, not just one
that has neither.

This invalidates the "co-owner" scenario (one invitation with both
fields set) the earlier test exercised, so that test is replaced with
one asserting the combination is rejected.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add Organizations to Invitation API Route (And Sync Events)

2 participants