Skip to content

School data - Update Schools in Basic details component - #6250

Draft
langesi wants to merge 19 commits into
mainfrom
sl/course_school_existing_updates
Draft

School data - Update Schools in Basic details component#6250
langesi wants to merge 19 commits into
mainfrom
sl/course_school_existing_updates

Conversation

@langesi

@langesi langesi commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Context

This is the course school update part of the school data remodel. It only takes care of the school adding in the basic details component of a course in publish.

The legacy Site and corresponding Provider::School are expected to share the same UUID. This allows one submitted Provider::School selection to be applied consistently to both models.

Inline and queued updates are both transactional. Their handling of a missing Provider::School differs deliberately:

  • Inline requests fail and show a form error because the submitted selection is no longer valid.
  • Queued requests log and exclude Provider::School records removed while the job was waiting.
  • The resulting effective selection is then written atomically to both models.
  • A valid Provider::School without its expected legacy Site is treated as data drift and raises rather than silently leaving the models inconsistent.

Changes proposed in this pull request

  • Updates the existing course schools form to display available Provider::School records and submit school_uuids instead of legacy Site IDs.
  • Reads the course's current selection and attached-school count from Course::School.
  • Uses the same GiasSchool.available scope for form display and validation, excluding closed schools.
  • Validates that submitted UUIDs belong to an available school for the course provider.
  • Reports unresolved inline selections to Sentry and displays the existing form error.
  • Refactors Publish::Schools::UpdateCourseSchoolsService to:
    • resolve submitted Provider::School UUIDs once;
    • lock resolved records while the update is applied;
    • pass the same effective selection to both writers;
    • keep inline and queued dual writes within one transaction;
    • persist attributes such as schools_validated;
    • save the course so provider.changed_at is updated for downstream Apply synchronisation;
    • send school-update notifications from the resulting Course::School selection.
  • Adds Publish::Schools::UpdateCourseProviderSchoolsService to synchronise Course::School records from already-resolved Provider::School records.
  • Adds Publish::Schools::UpdateCourseSiteStatusesService to temporarily mirror the same UUID selection to legacy SiteStatus records.
  • Makes missing legacy Site mappings explicit instead of silently skipping them.
  • Updates the queued job to tolerate stale Provider::School UUIDs without disabling transaction protection for other failures.
  • Counts unique, non-blank UUIDs consistently when deciding whether to enqueue and which success message to show.
  • Adds a Site factory trait that creates a corresponding GIAS school and Provider::School with matching school data and UUID.
  • Adds coverage for:
    • adding, removing and replacing schools in both models;
    • removing every school where this is permitted;
    • stale queued selections;
    • transaction rollback after writer or course-save failures;
    • missing legacy Site mappings;
    • inline form errors and Sentry reporting;
    • notifications;
    • provider changed_at updates;
    • dual writing after the remodel cycle;
    • closed schools;
    • queue boundaries, duplicate UUIDs and blank values;
    • select-all and collapsed school lists.

There are no intended visual changes beyond the form being backed by the remodelled school data.

Guidance to review

Suggested review order:

1. Existing course edit form and validation

  • app/forms/publish/course_school_form.rb
  • app/controllers/publish/courses/schools_controller.rb
  • app/views/publish/courses/schools/edit.html.erb
  • config/locales/en.yml
  • app/decorators/course_decorator.rb
  • spec/forms/publish/course_school_form_spec.rb
  • spec/decorators/course_decorator_spec.rb
  • spec/support/page_objects/publish/course_school_edit.rb
  • spec/system/publish/courses/editing_course_schools_spec.rb
  • spec/system/publish/courses/schools/school_list_display_spec.rb

Check that the form displays available Provider::School records, submits UUIDs, restores its selection from Course::School, rejects unavailable or unrelated schools, and renders a normal form error when an inline update can no longer resolve the selection.

2. Update orchestration and queued behaviour

  • app/services/publish/schools/update_course_schools_service.rb
  • app/jobs/update_course_schools_job.rb
  • spec/services/publish/schools/update_course_schools_service_spec.rb
  • spec/jobs/update_course_schools_job_spec.rb

Check that submitted UUIDs are resolved once and that both writers receive the same effective selection.

Pay particular attention to the queued path: stale Provider::School UUIDs are logged and excluded, but the remaining dual write is still transactional. Unexpected writer, validation or course-save failures therefore roll back both models.

Also check the comment around course.save!. The save is required to update provider.changed_at, which Apply uses to decide whether the provider's courses need synchronising.

3. New Course::School synchronisation

  • app/services/publish/schools/update_course_provider_schools_service.rb
  • spec/services/publish/schools/update_course_provider_schools_service_spec.rb

Check that this service has one responsibility: synchronising Course::School rows from an already-resolved collection of Provider::School records.

UUID resolution and missing-school policy deliberately remain in the orchestrator.

4. Legacy SiteStatus synchronisation

  • app/services/publish/schools/update_course_site_statuses_service.rb
  • spec/services/publish/schools/update_course_site_statuses_service_spec.rb

Check that this temporary writer applies the same effective UUID selection to legacy SiteStatus records.

The important invariant is that matching Site and Provider::School records share a UUID. Missing Site mappings now raise before mutation rather than being silently skipped.

Also check that published status is applied only to active SiteStatus rows and that suspended or discontinued rows are not reactivated.

5. Queue boundaries and user journeys

  • spec/system/publish/courses/schools/select_all_schools_spec.rb
  • spec/system/publish/courses/publishing_a_course_without_schools_spec.rb
  • spec/system/publish/providers/courses/schools_rollover_validation_spec.rb
  • spec/system/publish/providers/newly_added_tag_add_schools_to_a_course_page_spec.rb

Check select-all and collapsed-list behaviour, the asynchronous path above 30 unique schools, rollover validation, newly added labels, and providers that are allowed to remove every school.

6. Supporting test and course-creation changes

  • spec/factories/sites.rb
  • app/services/courses/creation_service.rb
  • spec/services/courses/creation_service_spec.rb

Check that the Site factory trait creates matching legacy and remodelled school records with the same UUID, URN and location information. The course-creation changes in this PR are limited to keeping the existing creation behavior and comments consistent with the completed backfill.

@langesi langesi added the deploy A Review App will be created for PRs with this label label Jul 24, 2026
@github-actions

Copy link
Copy Markdown

@langesi
langesi force-pushed the sl/course_school_existing_updates branch from a0496ce to e3dd0a8 Compare July 24, 2026 15:48
@langesi
langesi marked this pull request as ready for review July 24, 2026 23:43
@langesi
langesi requested a review from a team as a code owner July 24, 2026 23:43
Comment thread app/services/publish/schools/update_course_site_statuses_service.rb
Comment thread app/services/publish/schools/update_course_provider_schools_service.rb Outdated
Comment thread app/services/publish/schools/update_course_site_statuses_service.rb Outdated
Comment thread app/services/publish/schools/update_course_provider_schools_service.rb Outdated
Comment thread app/services/publish/schools/update_course_schools_service.rb Outdated
Comment thread app/services/publish/schools/update_course_schools_service.rb Outdated
Comment thread app/views/publish/courses/schools/edit.html.erb
Comment thread app/services/publish/schools/update_course_provider_schools_service.rb Outdated
Comment thread app/services/publish/schools/update_course_schools_service.rb Outdated
Comment thread app/controllers/publish/courses/schools_controller.rb

@tomas-stefano tomas-stefano 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.

The switch from site_ids to school_uuids is the right direction, but the new-model write hinges on an assumption that isn't guaranteed anywhere: during the remodel cycle the form submits Site UUIDs, and UpdateCourseProviderSchoolsService resolves them against provider_school.uuid. ProviderSchools::Identity deliberately maps site → provider_school by gias_school.urn + site_code precisely because, in its own words, "both models are written to and their UUIDs may diverge".

So we now have two contradictory mappings for the same relationship, and when they disagree this one degrades to a logger.warn nothing alerts on. That's compounded by the all-or-nothing guard: a single unresolvable UUID skips the entire Course::School sync — attaches that did resolve, and removals too. The backfill only inserts provider schools for sites with site_type = 0 AND urn IS NOT NULL, so any provider holding one URN-less or un-backfilled site silently stops syncing to the new model on every subsequent course edit. That's permanent divergence, not the "reconciles later" the comment promises, and it's inconsistent with Courses::CreationService#update_schools, which still skips per-school.

The legacy path has its own regression: sites_by_uuid now resolves through course.provider.sites (scoped site_type: :school + .kept), while course.sites goes through site_statuses with no kept scope. A discarded site that is still attached to a course therefore fails to resolve, detach_school swallows it via return if site.blank?, and the SiteStatus is never suspended — the school stays on the course forever with no way to remove it, since it isn't rendered in the checkbox list either. The old unscoped Site.where(id:) handled this. Two smaller ones worth fixing before merge: the queue is not deploy-safe — jobs already enqueued carry site_ids, so post-deploy params.key?(:school_uuids) is false, the update is silently dropped, and site_ids still reaches course.assign_attributes, where ids_writer on a has_many :through calls replace and delete_alls the join rows, bypassing the suspend-vs-destroy semantics in Course#remove_site!. And the enqueue threshold is now checked against two different operands (controller compacts blanks, service doesn't), so at exactly 30 selected schools the job is enqueued but the flash claims the change was saved.

Comment thread app/services/publish/schools/update_course_site_statuses_service.rb Outdated
Comment thread app/controllers/publish/courses/schools_controller.rb
@langesi
langesi marked this pull request as draft July 28, 2026 16:15
@langesi
langesi force-pushed the sl/course_school_existing_updates branch from 3d55c99 to da2793b Compare July 29, 2026 17:29
@langesi
langesi marked this pull request as ready for review July 30, 2026 17:54
@langesi
langesi marked this pull request as draft July 31, 2026 13:36
langesi added 19 commits July 31, 2026 21:24
…y when a matching Provider::School exists.

Dual writing 2026, 2027 course_school only in new data model when adding schools in basics detail component
Resolve submitted Provider::School UUIDs in the orchestrator and pass the same effective selection to the legacy and new-model writers.

Keep queued updates atomic while logging and excluding schools removed before the job runs. Cover stale selections, rollback failures, notifications, empty selections and post-remodel dual writes.
Validate that every resolved Provider::School UUID maps to a legacy Site before changing SiteStatus rows, rather than silently skipping missing mappings.

Render the school selection error for inline failures, report it to Sentry and cover queued rollback plus the user-facing error path.
Use the same GIAS-available Provider::School scope for form display and validation so closed schools cannot be submitted.

Count unique non-blank UUIDs consistently for enqueueing and flash messages, cover boundary inputs and correct the Course::School count documentation.
@langesi
langesi force-pushed the sl/course_school_existing_updates branch from b890891 to 517799a Compare July 31, 2026 20:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

deploy A Review App will be created for PRs with this label

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants