feat: #3631545: Turn Mapillary organizations into groups with members, roles and content

Turns Mapillary organizations into groups of the Group module: organizations get members, roles and Mapillary content, content is assigned to them automatically by its Mapillary organization and its creator, members-only organizations keep their content to themselves, and mismatches between images and memberships are shown with a notice that says how to fix them. Sites on 1.0.0-alpha6 are migrated by drush updatedb.

Changes

Organizations as groups

  • mapillary_organization depends on group:group (^3.3 || ^4.0, one code base) and ships the group type mapillary_organization with the fields mapillary_organization_id, mapillary_slug, mapillary_description (translatable), mapillary_visibility, mapillary_creator_matching, mapillary_fetch_enabled and mapillary_fetch_interval.
  • Four group roles (Admin, Editor, Importer, Member) and six group permissions in mapillary_organization.group.permissions.yml.
  • Group has no bypass permission, so two synchronized roles in config/optional ("Site administrator", outsider and insider, admin flag) tie the Drupal role administrator to every organization. A user with administer group or administer mapillary is never restricted by this module's own access code.
  • Relation plugins mapillary_content:<entity type> for images, sequences, map features and detections (each only when its entity type exists). Relationship types are created in code with createFromPlugin(), never shipped as config, because their config differs between Group 3 and 4; RelationshipTypeInstaller also creates them when a Mapillary submodule is installed later.
  • Group's relationship entity_id column is 32-bit, but Mapillary IDs have 15 to 17 digits. A subclass of Group's group_relationship_target field type makes an integer target_id 64-bit; RelationshipTargetSchema widens the live column on install (and in the update) and narrows it on uninstall only when every stored ID fits.
  • The old content entity type mapillary_organization, its controller and the Data Tuning tab (TuningForm, routes mapillary.tuning and mapillary.entity_settings.organization, config mapillary_organization.settings) are removed. The listing views.view.mapillary_organizations keeps its ID and page_1, now on groups.

People and images

  • User field mapillary_username, matched with creator_username of the sequence, trimmed and compared case-insensitively, unique (MapillaryUniqueValue). Site administrators set it; organization Admins set it for their members on Group's membership add and edit forms (MembershipUsernameForm), never for their own account. Users cannot set their own.
  • mapillary_organization_id of a group and the new image field organization_id (Mapillary Entity, filled from the API on import and thumbnail refresh) are editable by site administrators only, since together they decide which organization an image belongs to.
  • Memberships get a "Member since" date (mapillary_member_since), by default the time the membership was created; Admins can move it back to when a person actually started.

Assignment

  • OrganizationMatcher::classify() returns the active and passive organizations of an image from its Mapillary organization, its creator and the members' usernames: the organization with the image's Mapillary organization ID; organizations without an ID of their own (contractors, partners) through their members; content captured before a member's "Member since" date does not count for that organization.
  • OrganizationAssigner stores the active relationships for the image, its sequence, its detections and the map features detected in it. Assignments are sticky: nothing is removed automatically, so a worker who leaves keeps their past work in the old organization's numbers. Changes of usernames, memberships, "Member since" or organization IDs assign again; more than 50 affected images go to the queue mapillary_organization_assign.
  • Importers import in the context of one organization (route subscriber on the image, sequence and detection import routes; the base modules stay free of Group). After the API response, the import is rejected unless the content belongs to the organization; the image import checks the image, the sequence import its first image, the detection import the parent image.

Access

  • Visibility per organization: public or members only. Members-only content is hidden from non-members even with the site-wide view permission; a member with view mapillary content may list and view their organization's content without it.
  • When the Mapillary organization ID of an image belongs to a members-only organization, that organization governs the image: viewing and editing the image, its detections, and the sequences and map features that contain such images need the permission in every governing organization. Relationships with other organizations still count but give no access. Deleting governed content needs administer mapillary organization in every governing organization, so Editors can edit it but not delete it.
  • Entity access (OrganizationAccess, AccessHooks) and query access (QueryAccessSubscriber, contrib Entity API query access events, subqueries only) give the same answers, so listings, the map picker and entity queries match the access checks.

Mismatched content and notices

  • A "Mismatched content" tab on each organization lists, read only and 50 per page, the images whose creator is a member but which sit under another registered Mapillary organization; images the viewer may not view are counted across all pages.
  • Notices on the organization page (for view mapillary conflicts) and on the creator's page explain how to resolve each case. They are not stored as records: the viewer-independent list of each organization is cached and invalidated by the tags of organizations, users, memberships, image relationships, images and sequences, so a notice disappears once the data is right.
  • All lookups are bounded (subqueries or chunks of at most 1000 IDs).

Listings and logs

  • ListingColumns adds an Organization column and exposed filter to the image, sequence, map feature, detection and API call listings (install, later installed submodules and a post update); uninstalling Mapillary Organization removes only these handlers, not the views.
  • Mapillary Logs records the organizations of the logged-in user behind each API call in mapillary_logs_api_call_group, only when Group and Mapillary Organization are installed; a failure there never affects the call record.
  • README.md, CHANGELOG.md and AGENTS.md describe all of the above.

Upgrade

  • Deploying the code before drush updatedb keeps the site working even though Group is not installed yet: every service and hook of the module tolerates its absence.
  • mapillary_organization_update_11104() (runs after mapillary_entity_update_11108(), which adds the image field):
    1. Stops, changing nothing, when entity_reference fields or views other than the shipped listing use the old entity type, or when a field a site builder added has the name of an existing group storage with another type, cardinality or settings (default settings are filled in on both sides first, so a rerun after a failed update works).
    2. Uninstalls mapillary_organization when there are no organizations, no site-built fields, no organization group type yet and no other installed module depends on it; otherwise runs the full migration and names the dependents in the message.
    3. Installs Group, the group type, fields, roles and the optional site administrator roles, widens the relationship column, adds the username field to the user form and the "Member since" field.
    4. Copies every organization into a group: name, trimmed Mapillary organization ID (duplicates after trimming keep the first and are reported), slug, description, translations and site-built fields; translated organization IDs keep the default language only.
    5. Revokes the old entity permissions from every role, removes the old entity type with its last installed definition only after every row was copied, and assigns content of the migrated organizations.
  • mapillary_organization_post_update_remove_tuning_settings() deletes mapillary_organization.settings; mapillary_organization_post_update_listing_columns() adds the listing columns to existing views.
  • mapillary_entity_update_11108() adds mapillary_image.organization_id; mapillary_logs_update_11103() adds mapillary_logs_api_call_group.

Differences from the issue summary

  • "Member since" date: the capture-date rule compares with this editable date instead of the time a membership was created, so organizations set up today can take their staff's past work.
  • Governed content and delete rights: the summary said content in several organizations is visible when any of them is public. That let an Admin of another organization expose members-only content by adding its worker, so the registering members-only organization now governs the content, and only its Admins and site administrators may delete it.
  • Site administrators come through two synchronized group roles, since Group has no bypass permission.
  • The relationship column is widened to 64 bit, which the summary did not foresee.
  • Only site administrators edit an image's Mapillary organization ID; Admins set usernames on the membership forms.
  • The mismatched content is cached and paged; notices are still never stored.
  • The zero-organization uninstall has more conditions, and conflicting field storages stop the update.

Testing

New kernel tests in mapillary_organization: OrganizationGroupTypeTest, RelationshipTypeTest, WideRelationshipTargetTest, TrustedFieldsTest, AssignmentTest, ImportGateTest, ImportFormValidationTest, VisibilityTest, GovernedContentTest, ConflictsTest, BoundedQueriesTest, BootWithoutGroupTest and OrganizationMigrationTest (from a 1.0.0-alpha6 fixture: Group not installed and installed, translations, site-built fields, references, conflicting storages, dependents, empty sites). New functional tests: OrganizationImportFormTest, MembersOnlyListingTest, ConflictNoticeTest, OrganizationListingTest, MembershipUsernameTest; the existing organization tests were rewritten for groups. ImageOrganizationTest (Mapillary Entity) and ApiCallReportTest (Mapillary Logs) cover the new field and table. Kernel tests read Group's own dependencies (GroupDependenciesTrait), so they run on Group 3 (with flexible_permissions) and Group 4 (without it).

Run locally:

  • Drupal 11.4 with PostgreSQL and Group 3.3.5: unit 167 and kernel 209 tests pass; functional 82 of 83 pass. MapillaryThumbnailSettingsFormTest::testStatusSection failed once in the full run and passes on its own; this branch does not change that area.
  • MySQL with Group 3.3.5: WideRelationshipTargetTest, OrganizationMigrationTest, RelationshipTypeTest and AssignmentTest pass.
  • Drupal 12.0.0-beta1 with Group 4.0.0-alpha2 (earlier on the branch): the kernel tests pass except where Group's own user_update hook runs; it still reads $account->original, which Drupal 12 removed (https://www.drupal.org/project/group/issues/3631301, fixed in its MR !342, not merged yet).
  • phpcs (Drupal, DrupalPractice) and PHPStan level 6 report nothing new on the changed files.

Not run locally: the Drupal 12 run on the final commit and an update rehearsal from a real 1.0.0-alpha6 site; the pipeline covers the suites.

Closes #3631545

Merge request reports

Loading
Loading