School data - Update Schools in Basic details component - #6250
Conversation
a0496ce to
e3dd0a8
Compare
tomas-stefano
left a comment
There was a problem hiding this comment.
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.
3d55c99 to
da2793b
Compare
…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
… all schools into the memory just to count
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.
b890891 to
517799a
Compare
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
Siteand correspondingProvider::Schoolare expected to share the same UUID. This allows one submittedProvider::Schoolselection to be applied consistently to both models.Inline and queued updates are both transactional. Their handling of a missing
Provider::Schooldiffers deliberately:Provider::Schoolrecords removed while the job was waiting.Provider::Schoolwithout its expected legacySiteis treated as data drift and raises rather than silently leaving the models inconsistent.Changes proposed in this pull request
Provider::Schoolrecords and submitschool_uuidsinstead of legacy Site IDs.Course::School.GiasSchool.availablescope for form display and validation, excluding closed schools.Publish::Schools::UpdateCourseSchoolsServiceto:Provider::SchoolUUIDs once;schools_validated;provider.changed_atis updated for downstream Apply synchronisation;Course::Schoolselection.Publish::Schools::UpdateCourseProviderSchoolsServiceto synchroniseCourse::Schoolrecords from already-resolvedProvider::Schoolrecords.Publish::Schools::UpdateCourseSiteStatusesServiceto temporarily mirror the same UUID selection to legacySiteStatusrecords.Provider::SchoolUUIDs without disabling transaction protection for other failures.Provider::Schoolwith matching school data and UUID.changed_atupdates;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.rbapp/controllers/publish/courses/schools_controller.rbapp/views/publish/courses/schools/edit.html.erbconfig/locales/en.ymlapp/decorators/course_decorator.rbspec/forms/publish/course_school_form_spec.rbspec/decorators/course_decorator_spec.rbspec/support/page_objects/publish/course_school_edit.rbspec/system/publish/courses/editing_course_schools_spec.rbspec/system/publish/courses/schools/school_list_display_spec.rbCheck that the form displays available
Provider::Schoolrecords, submits UUIDs, restores its selection fromCourse::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.rbapp/jobs/update_course_schools_job.rbspec/services/publish/schools/update_course_schools_service_spec.rbspec/jobs/update_course_schools_job_spec.rbCheck that submitted UUIDs are resolved once and that both writers receive the same effective selection.
Pay particular attention to the queued path: stale
Provider::SchoolUUIDs 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 updateprovider.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.rbspec/services/publish/schools/update_course_provider_schools_service_spec.rbCheck that this service has one responsibility: synchronising
Course::Schoolrows from an already-resolved collection ofProvider::Schoolrecords.UUID resolution and missing-school policy deliberately remain in the orchestrator.
4. Legacy SiteStatus synchronisation
app/services/publish/schools/update_course_site_statuses_service.rbspec/services/publish/schools/update_course_site_statuses_service_spec.rbCheck that this temporary writer applies the same effective UUID selection to legacy
SiteStatusrecords.The important invariant is that matching
SiteandProvider::Schoolrecords 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.rbspec/system/publish/courses/publishing_a_course_without_schools_spec.rbspec/system/publish/providers/courses/schools_rollover_validation_spec.rbspec/system/publish/providers/newly_added_tag_add_schools_to_a_course_page_spec.rbCheck 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.rbapp/services/courses/creation_service.rbspec/services/courses/creation_service_spec.rbCheck 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.