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_organizationdepends ongroup:group(^3.3 || ^4.0, one code base) and ships the group typemapillary_organizationwith the fieldsmapillary_organization_id,mapillary_slug,mapillary_description(translatable),mapillary_visibility,mapillary_creator_matching,mapillary_fetch_enabledandmapillary_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 roleadministratorto every organization. A user withadminister grouporadminister mapillaryis 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 withcreateFromPlugin(), never shipped as config, because their config differs between Group 3 and 4;RelationshipTypeInstalleralso creates them when a Mapillary submodule is installed later. - Group's relationship
entity_idcolumn is 32-bit, but Mapillary IDs have 15 to 17 digits. A subclass of Group'sgroup_relationship_targetfield type makes an integertarget_id64-bit;RelationshipTargetSchemawidens 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, routesmapillary.tuningandmapillary.entity_settings.organization, configmapillary_organization.settings) are removed. The listingviews.view.mapillary_organizationskeeps its ID andpage_1, now on groups.
People and images
- User field
mapillary_username, matched withcreator_usernameof 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_idof a group and the new image fieldorganization_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.OrganizationAssignerstores 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 queuemapillary_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 contentmay 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 organizationin 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
ListingColumnsadds 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.mdandAGENTS.mddescribe all of the above.
Upgrade
- Deploying the code before
drush updatedbkeeps 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 aftermapillary_entity_update_11108(), which adds the image field):- Stops, changing nothing, when
entity_referencefields 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). - Uninstalls
mapillary_organizationwhen 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. - 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.
- 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.
- 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.
- Stops, changing nothing, when
mapillary_organization_post_update_remove_tuning_settings()deletesmapillary_organization.settings;mapillary_organization_post_update_listing_columns()adds the listing columns to existing views.mapillary_entity_update_11108()addsmapillary_image.organization_id;mapillary_logs_update_11103()addsmapillary_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::testStatusSectionfailed 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,RelationshipTypeTestandAssignmentTestpass. - 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_updatehook 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