Syndicate System - FreeUKGen/MyopicVicar GitHub Wiki

Syndicate System

Part of Transcriber & Coordinator Workflow.

Models: app/models/syndicate.rb, app/models/manage_syndicate.rb ยท Controllers: app/controllers/syndicates_controller.rb, app/controllers/manage_syndicates_controller.rb

A syndicate is a volunteer transcription group, usually scoped to a county/region, run by one syndicate_coordinator. There are two very different things here despite the similar names:

  • Syndicate is the real record โ€” the entity itself, plain CRUD.
  • ManageSyndicate is not a real persisted record despite including Mongoid::Document โ€” it's a throwaway form object for a syndicate picker, plus a namespace for two static query helpers. The actual class doing work is ManageSyndicatesController, which is a session-driven coordinator dashboard that never CRUDs a Syndicate itself โ€” it just redirects into other resources' views (freereg1_csv_files, physical_files, userid_details, image-group views) scoped to whichever syndicate is in session[:syndicate].

Syndicate

Key fields: syndicate_code, syndicate_description, syndicate_notes, accepting_transcribers (Boolean โ€” gates whether new transcribers can join), syndicate_coordinator (+ a _lower_case twin for case-insensitive lookup), and a rename/handoff audit trail: previous_syndicate_code, previous_syndicate_coordinator, changing_name.

Changing the coordinator or code has real side effects (Syndicate#update_fields_before_applying, run on update):

  • Old coordinator: the syndicate code is removed from their syndicate_groups array, and if that leaves them with zero remaining groups, they're demoted from person_role: 'syndicate_coordinator' back to 'transcriber'.
  • New coordinator: the code is added to their syndicate_groups, and if they were a 'transcriber' or 'researcher', they're promoted to person_role: 'syndicate_coordinator'.
  • None of this runs in a transaction โ€” it's a sequence of update_attributes/save(validate: false) calls, so a failure partway through can leave an old coordinator demoted with no new coordinator promoted.
  • after_save :propagate_change_in_code โ€” renaming a syndicate code triggers a bulk update of every UseridDetail whose syndicate field matches the old code. This is a full-collection scan/update on every rename, not scoped to syndicate members via a join.
  • destroy is blocked while any UseridDetail still has that syndicate code โ€” must be emptied of members first.

Relationships: has_many :assignments; reads/writes UseridDetail (syndicate, syndicate_groups, person_role) directly rather than through a service object; Syndicate.syndicate_code(code) is the standard lookup used elsewhere (e.g. Contact-System's FreeCEN routing).

Worth knowing: Syndicate.productive_volunteers(syndicate, start_date, end_date) is a stats helper that queries FreecenCsvFile โ€” despite the generic name, it's FreeCEN-specific, not template-set-agnostic.

Routes: resources :syndicates plus syndicates/display, syndicates/select, syndicates/selection.

ManageSyndicate / ManageSyndicatesController

The coordinator's "manage my syndicate" dashboard. Flow:

  1. new works out which syndicate(s) the logged-in coordinator can manage, role-gated (volunteer_coordinator/data_manager/system_administrator/etc., plus FreeCEN-only county_coordinator). Exactly one match โ†’ auto-selected into session[:syndicate], redirect straight past the picker to select_action. Multiple matches โ†’ shows the picker.
  2. select_action shows a menu of dashboard options (UseridRole::SYNDICATE_MANAGEMENT_OPTIONS).
  3. Every other action reads session[:syndicate] and either renders or redirects into another resource's view scoped to it โ€” batch lists (freereg1_csv_files/freecen_csv_files/physical_files), member management (userid_details), or image-transcription review (image_server_group).

Notable footgun โ€” routing dispatch via one-off constraint classes. Roughly 15 of these dashboard actions all share one route path, manage_syndicates/selection, and Rails picks which controller action actually runs via custom routing constraint classes in app/constraints/ that string-match params[:option] (e.g. a constraint checks request.query_parameters['option'] == 'Review All Members'). This is the same "dispatch on a human-readable label" pattern as Message-System's MessagesController#create (which dispatches on submit-button text) โ€” except here it's pushed down into the routing layer as a separate class per option. Adding a new dashboard option means adding both a route line and a matching constraint class, and there's already a live typo from this fragility: the list_fully_transcribed_group route (config/routes.rb:318) is registered under manage_syndicatess (double s) โ€” that path silently doesn't match rather than erroring.

Relationships: reads Syndicate, UseridDetail (syndicate_groups, person_role), ImageServerGroup.group_ids_by_syndicate, and the various CSV-file/PhysicalFile models for batch listings.

Routes: resources :manage_syndicates plus the constraint-dispatched manage_syndicates/selection (~15 distinct actions behind one path), manage_syndicates/select_action, manage_syndicates/display_files_not_processed, manage_syndicates/display_files_waiting_to_be_processed, manage_syndicates/:id/selected, manage_syndicates/manage_image_group, manage_syndicates/:id/list_fully_reviewed_group, and the typo'd manage_syndicatess/:id/list_fully_transcribed_group.