Runbook
Caution THIS RUNBOOK IS THE PRIMARY DELIVERABLE. It will be used for a clean-room install on a fresh site. Every command must be copy-paste ready and produce the documented result.
When you discover ANY deviation during implementation — a wrong service name, a character limit, an extra module that must be enabled, a property name correction — you must update this runbook IMMEDIATELY, before moving to the next step. Do not defer runbook updates. Do not report deviations only in chat. The runbook must always reflect what actually works.
If it’s not in the runbook, it didn’t happen.
Important All changes must be made via config entities — create or modify config YAML files, then import them with
ddev drush cimorddev drush php:eval. Never make changes through the Drupal admin UI or browser automation (Playwright, etc.).“Navigate to
/admin/...” in implementation steps describes what is being configured — the actual implementation must create or modify the corresponding config YAML and import it. In verification steps, “Navigate to” is fine — visit the admin UI to visually confirm the result.Workflow:
php:evalcreates the entity →config:exportcaptures it as YAML → commit the YAML. The exported YAML is the deliverable, not thephp:evalcommand.
Caution Config import sequencing. Within each phase, the order is:
- Install modules (
ddev drush en <module> -y) — registers the module incore.extension - Run setup scripts (
ddev drush php:script ...) — creates entities programmatically - Export config (
ddev drush config:export -y) — captures new entities as YAML - Copy pre-made YAML from
docs/groups/config/toconfig/sync/only when the runbook explicitly says to - Import config (
ddev drush cim -y) — only when the runbook explicitly says to
Never run cim before the required modules are installed — Drupal will reject YAML that depends on uninstalled modules.
All docs/groups/config/ YAML files are reference copies. The scripts create the same entities. You only need the YAML for verification or to restore state.
Important Checkpoint after every phase. After completing all steps and verification for a phase, stop and check in with the user before proceeding to the next phase.
Important Prefer contributed modules over custom code. Before porting any custom module from Open Social, search drupal.org/project for an existing contributed module that provides the same functionality. Only write custom code when no suitable contributed module exists or when the contributed options are unmaintained / incompatible with Drupal 10 + Group 3.x. Document the search results and rationale for each decision.
Important
Copy module source before enabling. Custom module source files live in docs/groups/modules/. Before running ddev drush en <module> for any do_* module, copy its directory into web/modules/custom/:
cp -r docs/groups/modules/do_group_extras web/modules/custom/Each phase notes which module to copy. Do this before the ddev drush en command for that module.
Important All custom modules must follow the Services over Hooks pattern. Before writing or porting any custom module, read
ai_guidance/drupal/BEST_PRACTICES.md. Procedural hook-based modules are not acceptable in this project.
This runbook documents the step-by-step process for adding groups functionality to the standard Drupal 10 codebase (pl-drupalorg).
See GROUPS_CONVERSION_PLAN.md for the gap analysis, key differences between the projects, and the overall phase plan.
Prerequisites (all phases)
Section titled “Prerequisites (all phases)”- DDEV running (
ddev statusshows all services healthy) - Site accessible at
https://pl-groups-on-d11.ddev.site:8493 - Git working tree clean on
aa/initial-planbranch
Important Before running any commands or tests, review TROUBLESHOOTING.md for known gotchas with DDEV, Playwright, Drupal configuration, and process cleanup. Many of the issues documented there (opcode cache staleness, port variability,
networkidlehangs, config import locks) apply to this project.
Contributed Module Dependencies
Section titled “Contributed Module Dependencies”All contributed modules referenced in this runbook, verified on drupal.org:
| Module | Composer package | drupal.org | Used in | Notes |
|---|---|---|---|---|
| Group | drupal/group | drupal.org/project/group | Phase 1 | Core dependency; use ^3.0 for group_relationship API |
| Group Node (gnode) | sub-module of drupal/group | — | Phase 1 | Sub-module bundled with Group; enables group_node:* relationship plugins |
| Flag | drupal/flag | drupal.org/project/flag | Phase 4 | Includes flag_count sub-module (not a separate package) |
| Linkit | drupal/linkit | drupal.org/project/linkit | Phase 1 | Optional — inline autocomplete linking |
| Search API | drupal/search_api | drupal.org/project/search_api | Phase 7 | Required by Search API Solr |
| Search API Solr | drupal/search_api_solr | drupal.org/project/search_api_solr | Phase 7 | Optional — only if using Solr backend |
| Message | drupal/message | drupal.org/project/message | Phase 5 | Optional — structured notification queue entities |
| Message Notify | drupal/message_notify | drupal.org/project/message_notify | Phase 5 | Optional — pluggable notifier framework (delivery handled by external system) |
Warning
statisticsmodule: Deprecated in Drupal 10.3.0, removed in Drupal 11.0.0. If on Drupal 10.3+, install from contrib:ddev composer require drupal/statistics(drupal.org/project/statistics). On Drupal ≤10.2, useddev drush en statistics -y(still in core).
Caution
flag_countis NOT a separate Composer package. It is a sub-module bundled insidedrupal/flag. Do NOT runddev composer require drupal/flag_count— it will fail. Instead, requiredrupal/flagand enable the sub-module withddev drush en flag_count -y.
Rollback Strategy
Section titled “Rollback Strategy”A database snapshot is taken before each phase begins. To roll back to the start of any phase:
- Code:
git log --oneline→ find the commit before the phase →git checkout <commit> - Database:
ddev import-db --file=backups/pre-phaseN-YYYYMMDD-HHMM.sql.gz - Cache:
ddev drush cr
Tip After every
ddev drush config:export -y, commit the exported YAML to git. This creates the rollback points listed above.
| Snapshot file | Taken before |
|---|---|
pre-phase1-*.sql.gz | Phase 1 (Foundation) |
pre-phase2-*.sql.gz | Phase 2 (Group Types) |
pre-phase3-*.sql.gz | Phase 3 (Content) |
pre-phase4-*.sql.gz | Phase 4 (Discovery) |
pre-phase5-*.sql.gz | Phase 5 (Notifications) |
pre-phase6-*.sql.gz | Phase 6 (Profiles) |
pre-phase7-*.sql.gz | Phase 7 (Demo Data) |
pre-phase8-*.sql.gz | Phase 8 (Feature Tour) |
demo-empty-*.sql.gz | After clean slate (Step 700) |
demo-complete-*.sql.gz | After full demo data (Step 730) |
Phase 1 — Foundation & Module Installation
Section titled “Phase 1 — Foundation & Module Installation”Goal: Install the Drupal Group module, create the base group type, and establish the 5 group-postable content types.
Important YAML-first policy. All Drupal configuration in this project is managed as YAML config entities, not PHP scripts. Config entities (content types, field storage, field instances, group types, relationship types, vocabularies, views) live in
docs/groups/config/. Data entities (taxonomy terms, users, demo content) use scripts indocs/groups/scripts/.
Group-Postable Content Types
Section titled “Group-Postable Content Types”This project creates the following 5 content types from scratch. They do not pre-exist.
| Content type | Purpose | Config file |
|---|---|---|
forum | Community discussion threads | node.type.forum.yml |
documentation | Long-form reference docs | node.type.documentation.yml |
event | Community events | node.type.event.yml |
post | Short-form posts | node.type.post.yml |
page | General static pages | node.type.page.yml |
Step 100 — Create Baseline Snapshot
Section titled “Step 100 — Create Baseline Snapshot”Important This must be the first step before ANY changes. It captures the pristine state of the site for rollback.
mkdir -p backupsddev export-db --file=backups/pre-phase1-$(date +%Y%m%d-%H%M).sql.gzgit add -A && git statusVerify the backup file exists and note its filename in the rollback table above.
Step 105 — Configure config sync directory
Section titled “Step 105 — Configure config sync directory”DDEV defaults config sync to sites/default/files/sync. Override it to config/sync (outside the webroot, under version control).
mkdir -p config/syncIn web/sites/default/settings.php, uncomment and set:
$settings['config_sync_directory'] = '../config/sync';Capture the initial baseline config:
ddev drush config:export -ygit add config/sync && git commit -m "chore: initial config export"Important The
config_sync_directoryline insettings.phpmust appear beforesettings.ddev.phpis included. DDEV’ssettings.ddev.phpsets this tosites/default/files/syncas a fallback — your explicit setting overrides it.
Step 110 — Install the Group Module
Section titled “Step 110 — Install the Group Module”ddev composer require drupal/groupStandard Drupal Group module (not Open Social’s bundled version):
- 3.x:
GroupRelationshipentities (Drupal 10+, this project)
✅ Resolved:
drupal/group3.3.5 is already installed. Requires Drupal^10.3 || ^11.
Step 120 — Enable All Required Modules
Section titled “Step 120 — Enable All Required Modules”Enable the group modules plus all Drupal core modules needed for Phase 1 field types:
ddev drush en group gnode image taxonomy views views_ui text -yImportant Export immediately after enabling modules. This updates
core.extensioninconfig/syncto reflect the newly enabled modules. If you skip this export and add custom YAML configs first,config:importwill fail becausecore.extensionwon’t list the modules as dependencies.
ddev drush config:export -yVerify:
ddev drush pm:list --status=enabled | grep -E "group|gnode"# Should show: group, gnode (both enabled)Step 125 — Create the 5 Group-Postable Content Types
Section titled “Step 125 — Create the 5 Group-Postable Content Types”These content types do not exist on a clean install. Create them via YAML config import.
cp docs/groups/config/node.type.forum.yml config/sync/cp docs/groups/config/node.type.documentation.yml config/sync/cp docs/groups/config/node.type.event.yml config/sync/cp docs/groups/config/node.type.post.yml config/sync/cp docs/groups/config/node.type.page.yml config/sync/ddev drush config:import -yddev drush config:export -yVerify:
ddev drush php:eval 'foreach (\Drupal::entityTypeManager()->getStorage("node_type")->loadMultiple() as $t) echo $t->id() . "\n";'# Should show: documentation, event, forum, page, postStep 130 — Create Group Type and Fields
Section titled “Step 130 — Create Group Type and Fields”Import the group type, admin role, and group fields via YAML:
cp docs/groups/config/group.type.community_group.yml config/sync/cp docs/groups/config/group.role.community_group-admin.yml config/sync/cp docs/groups/config/group.settings.yml config/sync/cp docs/groups/config/group.relationship_type.community_group-group_membership.yml config/sync/cp docs/groups/config/field.storage.group.field_group_description.yml config/sync/cp docs/groups/config/field.storage.group.field_group_visibility.yml config/sync/cp docs/groups/config/field.storage.group.field_group_image.yml config/sync/cp docs/groups/config/field.storage.group_relationship.group_roles.yml config/sync/cp docs/groups/config/field.field.group.community_group.field_group_description.yml config/sync/cp docs/groups/config/field.field.group.community_group.field_group_visibility.yml config/sync/cp docs/groups/config/field.field.group.community_group.field_group_image.yml config/sync/cp docs/groups/config/field.field.group_relationship.community_group-group_membership.group_roles.yml config/sync/cp docs/groups/config/core.entity_form_display.group_relationship.community_group-group_membership.default.yml config/sync/cp docs/groups/config/core.entity_view_display.group_relationship.community_group-group_membership.default.yml config/sync/ddev drush config:import -yddev drush config:export -yGroup type settings:
| Setting | Value |
|---|---|
| Label | Community Group |
| Machine name | community_group |
| Creator is member | Yes |
| Creator role | community_group-admin |
Fields added to group type:
| Field | Type | Machine name |
|---|---|---|
| Description | Text (formatted, long) | field_group_description |
| Visibility | List (text) | field_group_visibility |
| Image | Image | field_group_image |
Note
field_group_typeandfield_group_languageare added in later phases.
Caution
Group::create() does NOT auto-join the creator. The creator_membership group type setting only applies when groups are created through the form UI. When creating groups programmatically, explicitly add the creator as a member:
$group->addMember(\Drupal\user\Entity\User::load($uid));Step 140 — Configure Group-Node Relationships
Section titled “Step 140 — Configure Group-Node Relationships”Import the 5 group-node relationship type configs:
cp docs/groups/config/group.relationship_type.community_group-group_node-forum.yml config/sync/cp docs/groups/config/group.relationship_type.community_group-group_node-doc.yml config/sync/cp docs/groups/config/group.relationship_type.community_group-group_node-event.yml config/sync/cp docs/groups/config/group.relationship_type.community_group-group_node-post.yml config/sync/cp docs/groups/config/group.relationship_type.community_group-group_node-page.yml config/sync/ddev drush config:import -yddev drush config:export -y| Relationship type ID | Content type |
|---|---|
community_group-group_node-forum | forum |
community_group-group_node-doc | documentation |
community_group-group_node-event | event |
community_group-group_node-post | post |
community_group-group_node-page | page |
Caution 32-character ID limit.
community_group-group_node-documentationis 40 chars. Use the abbreviated IDcommunity_group-group_node-doc.
Step 150 — Set Up Basic Permissions
Section titled “Step 150 — Set Up Basic Permissions”Drupal-level permissions
Section titled “Drupal-level permissions”Grant the following Drupal-level permissions (admin UI: /admin/people/permissions):
| Permission | Roles |
|---|---|
create community_group group | Authenticated users |
access group overview | Anonymous + Authenticated users |
ddev drush role:perm:add authenticated "create community_group group"ddev drush role:perm:add anonymous "access group overview"ddev drush role:perm:add authenticated "access group overview"Group-level permissions
Section titled “Group-level permissions”Configure the following group-level permissions (admin UI: /admin/group/types/manage/community_group/permissions):
| Permission | Member | Admin | Outsider |
|---|---|---|---|
| View group | ✅ | ✅ | ✅ |
| Edit group | ❌ | ✅ | ❌ |
| Delete group | ❌ | ✅ | ❌ |
| Administer members | ❌ | ✅ | ❌ |
| Join group | — | — | ✅ |
| Leave group | ✅ | ✅ | — |
| Create content in group | ✅ | ✅ | ❌ |
| Edit own content in group | ✅ | ✅ | ❌ |
| Edit any content in group | ❌ | ✅ | ❌ |
| Delete own content in group | ✅ | ✅ | ❌ |
| Delete any content in group | ❌ | ✅ | ❌ |
Note Group-level permissions are configured via the Group module’s permission system, not Drupal’s core role:perm system. These are set via the admin UI or by importing
group.role.community_group-*.ymlconfig files afterconfig:export.
ddev drush config:export -yStep 160 — Create Group Listing View + Tag Taxonomy
Section titled “Step 160 — Create Group Listing View + Tag Taxonomy”Import the All Groups view and the event_types and group_tags vocabularies:
cp docs/groups/config/views.view.all_groups.yml config/sync/cp docs/groups/config/taxonomy.vocabulary.event_types.yml config/sync/cp docs/groups/config/taxonomy.vocabulary.group_tags.yml config/sync/ddev drush config:import -yddev drush config:export -yVerify: navigate to /all-groups — should load (empty list, no groups yet).
Step 170 — Add Basic Permissions
Section titled “Step 170 — Add Basic Permissions”ddev drush role:perm:add authenticated "create community_group group"ddev drush role:perm:add authenticated "access group overview"ddev drush role:perm:add anonymous "access group overview"ddev drush config:export -yGroup-level permissions (configured via /admin/group/types/manage/community_group/permissions):
| Permission | Member | Admin | Outsider |
|---|---|---|---|
| View group | ✅ | ✅ | ✅ |
| Edit group | ❌ | ✅ | ❌ |
| Delete group | ❌ | ✅ | ❌ |
| Administer members | ❌ | ✅ | ❌ |
| Join group | — | — | ✅ |
| Leave group | ✅ | ✅ | — |
| Create content in group | ✅ | ✅ | ❌ |
| Edit own content in group | ✅ | ✅ | ❌ |
| Edit any content | ❌ | ✅ | ❌ |
| Delete own content | ✅ | ✅ | ❌ |
| Delete any content | ❌ | ✅ | ❌ |
Note Group-level permissions are set via the Group module’s permission system, not
drush role:perm. Configure via admin UI or importgroup.role.community_group-*.ymlconfigs afterconfig:export.
Step 180 — Add Required Fields to Content Types
Section titled “Step 180 — Add Required Fields to Content Types”Add body, field_group_tags, and field_event_type (events only) to all 5 content types:
cp docs/groups/config/field.storage.node.body.yml config/sync/cp docs/groups/config/field.field.node.forum.body.yml config/sync/cp docs/groups/config/field.field.node.documentation.body.yml config/sync/cp docs/groups/config/field.field.node.event.body.yml config/sync/cp docs/groups/config/field.field.node.post.body.yml config/sync/cp docs/groups/config/field.field.node.page.body.yml config/sync/cp docs/groups/config/field.storage.node.field_group_tags.yml config/sync/cp docs/groups/config/field.field.node.forum.field_group_tags.yml config/sync/cp docs/groups/config/field.field.node.documentation.field_group_tags.yml config/sync/cp docs/groups/config/field.field.node.event.field_group_tags.yml config/sync/cp docs/groups/config/field.field.node.post.field_group_tags.yml config/sync/cp docs/groups/config/field.field.node.page.field_group_tags.yml config/sync/cp docs/groups/config/field.storage.node.field_event_type.yml config/sync/cp docs/groups/config/field.field.node.event.field_event_type.yml config/sync/ddev drush config:import -yddev drush config:export -yVerify with the field check script:
ddev drush php:script docs/groups/scripts/step_160.phpExpected output:
=== Content type field verification ===forum: body=YES files=NO tags=YES comment=NONEdocumentation: body=YES files=NO tags=YES comment=NONEevent: body=YES files=NO tags=YES comment=NONEpost: body=YES files=NO tags=YES comment=NONEpage: body=YES files=NO tags=YES comment=NONENote
files=NOandcomment=NONEare expected at this stage. File attachments and comments are Phase 2+ concerns.
Step 190 — Phase 1 Tests
Section titled “Step 190 — Phase 1 Tests”Warning Review TROUBLESHOOTING.md before running tests. Kill zombie processes first:
ddev exec kill-zombies.sh
PHPUnit — Integration (do_tests)
Section titled “PHPUnit — Integration (do_tests)”Create web/modules/custom/do_tests/tests/src/Kernel/Phase1Test.php:
testGroupTypeExists()—community_groupgroup type config entity loadstestRelationshipTypesExist()— 5 relationship types:community_group-group_node-{forum,doc,event,post,page}testGroupMembershipType()—community_group-group_membershiprelationship type existstestGroupFields()—field_group_description,field_group_visibility,field_group_imageoncommunity_grouptestEventTypesVocabulary()—event_typesvocabulary exists with expected terms
ddev exec phpunit web/modules/custom/do_tests/tests/src/Kernel/Phase1Test.phpNote Phase 1 has no custom modules yet — all tests are integration tests in
do_tests.
Playwright (E2E)
Section titled “Playwright (E2E)”Create tests/e2e/phase1.spec.ts:
test('group listing page loads')— navigate to/groups, verify 200 and page titletest('authenticated user can create group')— log in, create group, verify redirecttest('group type fields render')— verify form fields on/group/add/community_grouptest('group-node relationship works')— create group, post forum topic, verify in group streamtest('permissions enforced')— anonymous gets 403 on group create
npx playwright test tests/e2e/phase1.spec.tsPhase 1 — Verification
Section titled “Phase 1 — Verification”-
drupal/groupincomposer.jsonandcomposer.lock -
ddev drush pm:list --status=enabled | grep -E "group|gnode"shows both enabled -
config/sync/exists andddev drush config:statusshows clean (no differences) - 5 content types exist:
forum,documentation,event,post,page -
community_groupgroup type exists at/admin/group/types - Group type has fields:
field_group_description,field_group_visibility,field_group_image - 5 group-node relationship types exist (
community_group-group_node-{forum,doc,event,post,page}) - Group listing view accessible at
/all-groups -
event_typesandgroup_tagsvocabularies exist -
ddev drush php:script docs/groups/scripts/step_160.phpshowsbody=YES tags=YESfor all 5 types -
ddev drush cr— no PHP errors - DB snapshot in
backups/phase1-complete-*.sql.gz
Schema Changes (Phase 1)
Section titled “Schema Changes (Phase 1)”| Type | Name | Notes |
|---|---|---|
| Entity type | group (via drupal/group) | Core dependency |
| Group type | community_group | Custom group type |
| Group roles | community_group-{admin,member,outsider} | 3 roles |
| Group relationships | community_group-group_node-{forum,doc,event,post,page} | 5 content types (doc = abbreviated documentation due to 32-char ID limit) |
| Group relationship | community_group-group_membership | User membership |
| Field (group) | field_group_description | Text (formatted, long) |
| Field (group) | field_group_visibility | List (public/community/secret) |
| Field (group) | field_group_image | Image |
| Taxonomy | event_types | 7 terms (DrupalCon, Sprint, etc.) |
| View | groups at /groups | Group listing page |
| Module | linkit | Optional — inline linking |
Open Questions (Phase 1)
Section titled “Open Questions (Phase 1)”Which content types should be postable to groups?✅ Confirmed:forum,documentation,event,post,pageWhich version of✅ Resolved: 3.3.5 already installeddrupal/group?Group creation — open to all authenticated users, or restricted role?✅ Confirmed: any authenticated user can create groups
Phase 2 — Group Types & Membership Models
Section titled “Phase 2 — Group Types & Membership Models”Goal: Configure group types, membership models, group directory, and the do_group_extras module.
Source: IMPLEMENTATION_PLAN.md (removed) Phase 3
Pre-Phase 2 Snapshot
Section titled “Pre-Phase 2 Snapshot”ddev export-db --file=backups/pre-phase2-$(date +%Y%m%d-%H%M).sql.gzStep 200 — Create group_type Taxonomy Vocabulary + Terms
Section titled “Step 200 — Create group_type Taxonomy Vocabulary + Terms”group_type is a config entity (vocabulary) plus data entities (terms). Import the vocabulary via YAML, then create terms via script:
cp docs/groups/config/taxonomy.vocabulary.group_type.yml config/sync/ddev drush config:import -yddev drush config:export -y
# Create the 5 terms (data entities — script is appropriate here)ddev drush php:script docs/groups/scripts/step_200.phpExpected output:
group_type vocabulary already existsCreated: Geographical (tid=1)Created: Working group (tid=2)Created: Distribution (tid=3)Created: Event planning (tid=4)Created: Archive (tid=5)5 group_type terms totalNote Terms are data entities (not config YAML). TIDs will vary per environment. The script is idempotent — safe to re-run.
Step 210 — Add field_group_type to Group Entity
Section titled “Step 210 — Add field_group_type to Group Entity”Import the field storage and instance via YAML:
cp docs/groups/config/field.storage.group.field_group_type.yml config/sync/cp docs/groups/config/field.field.group.community_group.field_group_type.yml config/sync/ddev drush config:import -yddev drush config:export -y| Setting | Value |
|---|---|
| Field type | Entity reference |
| Machine name | field_group_type |
| Reference type | Taxonomy term → group_type vocabulary |
| Required | No |
| Cardinality | 1 |
Important Verify
handler_settings.target_bundlesis set togroup_typeinfield.field.group.community_group.field_group_type.yml. Mismatch causes empty dropdowns.
Step 220 — Configure Membership Models
Section titled “Step 220 — Configure Membership Models”The field_group_visibility field (added in Phase 1 via YAML) controls join behaviour:
| Membership model | field_group_visibility value | Join behaviour |
|---|---|---|
| Open | open | Instant join |
| Moderated | moderated | Request → approval |
| Invite Only | invite_only | Admin-managed |
Group-level permissions for each model are configured at /admin/group/types/manage/community_group/permissions.
Step 230 — Build Group Directory View
Section titled “Step 230 — Build Group Directory View”✅ Already done in Phase 1 Step 160.
views.view.all_groups.ymlwas imported at/all-groups. No action needed here.
Step 240 — Enable do_group_extras Module
Section titled “Step 240 — Enable do_group_extras Module”Archive enforcement, submission guidelines, and moderation defaults for groups.
Source files (in repo at docs/groups/modules/do_group_extras/, deployed to web/modules/custom/do_group_extras/):
do_group_extras.info.ymldo_group_extras.services.yml— registersDoGroupExtrasHooksservicesrc/Hook/DoGroupExtrasHooks.php— all hook implementations as#[Hook]attributed methodsdo_group_extras.module— empty (just@filedocblock)do_group_extras.libraries.ymlcss/do_group_extras.css
Important This module uses the Drupal 11
#[Hook]attribute system (OOP service-based hooks). All hook logic is insrc/Hook/DoGroupExtrasHooks.php. The.modulefile is intentionally empty. This complies with BEST_PRACTICES.md.
Enable
Section titled “Enable”Caution Order of operations matters. Enable the module and export BEFORE adding any View YAMLs. If you add View YAML to
config/sync/and runconfig:importbefore exporting the module’s extension entry, the import will uninstall the module.
# Step 1: Copy module from docs to webcp -r docs/groups/modules/do_group_extras web/modules/custom/# Step 2: Enable moduleddev drush en do_group_extras -y# Step 3: Export to capture module in core.extension.ymlddev drush config:export -y# Step 4: Clear cacheddev drush crHook implementations (all in DoGroupExtrasHooks):
formAlter— submission guidelines on group create/edit formsentityPresave— non-admin groups default to unpublishedentityInsert— queues pending notification forsite_moderatorpreprocessGroup— “Archived” badge on archived groupsnodeAccess— denies content creation in archived groups
Step 250 — Create Pending Groups View
Section titled “Step 250 — Create Pending Groups View”cp docs/groups/config/views.view.pending_groups.yml config/sync/ddev drush config:import -yddev drush config:export -yVerify: navigate to /admin/groups/pending — should show an empty table (requires administer group permission).
Step 260 — Evaluate do_wiki Module
Section titled “Step 260 — Evaluate do_wiki Module”Decision needed: Should
[[Title]]wiki-link syntax be supported in pl-drupalorg?
The do_wiki module provides a text filter that converts [[Title]] patterns into clickable links to nodes with matching titles. It is a single-class module:
src/Plugin/Filter/WikiLinkFilter.php— extendsFilterBase, implementsprocess()to find[[...]]patterns and replace with<a>links- No dependencies beyond
drupal:filter
Note Adaptation difficulty: 🟢 Low. The filter is entirely Drupal-generic. Only the
.info.ymlmetadata needs updating.
If needed:
mkdir -p web/modules/custom/do_wiki/src/Plugin/FilterWarning
do_wikiis not indocs/groups/modules/— it was not ported as part of this project’s runbook. If needed, source it from thepl-opensocialrepo or write it from scratch using the description above. No code changes are needed from the Open Social version beyond updating the.info.ymlmetadata.
ddev drush en do_wiki -yddev restart # Required to flush PHP opcode cacheddev drush crCaution After enabling any new module with plugin classes, run
ddev restart(not justddev drush cr) to flush the PHP opcode cache so the web process can discover the new class.
Then add the WikiLink filter to full_html text format at /admin/config/content/formats/manage/full_html.
Step 270 — Phase 2 Tests
Section titled “Step 270 — Phase 2 Tests”Warning Review TROUBLESHOOTING.md before running tests. Kill zombie processes first:
ddev exec kill-zombies.sh
PHPUnit — Module: do_group_extras
Section titled “PHPUnit — Module: do_group_extras”Create web/modules/custom/do_group_extras/tests/src/Kernel/GroupExtrasTest.php:
testModuleEnabled()—do_group_extrasmodule is enabledtestArchiveHookAccess()— archived group denies node creationtestGuidelinesFormAlter()— form alter hook fires on group create form
ddev exec phpunit web/modules/custom/do_group_extras/tests/src/Kernel/GroupExtrasTest.phpPHPUnit — Integration (do_tests)
Section titled “PHPUnit — Integration (do_tests)”Create web/modules/custom/do_tests/tests/src/Kernel/Phase2Test.php:
testGroupTypeVocabulary()—group_typevocabulary exists with expected termstestFieldGroupType()—field_group_typeexists oncommunity_grouptestPendingGroupsView()—pending_groupsView config exists
ddev exec phpunit web/modules/custom/do_tests/tests/src/Kernel/Phase2Test.phpPlaywright (E2E)
Section titled “Playwright (E2E)”Create tests/e2e/phase2.spec.ts:
test('group directory displays groups')— navigate to/groups, verify group cards with type badgestest('group type filter works')— filter by group type, verify results narrowtest('pending group visible to site_moderator')— create unpublished group, verify pending viewtest('archived group is read-only')— archive a group, verify content creation blockedtest('do_wiki WikiLink filter')— create node with[[wiki link]], verify renders as link
npx playwright test tests/e2e/phase2.spec.tsPhase 2 — Verification
Section titled “Phase 2 — Verification”-
group_typevocabulary exists at/admin/structure/taxonomy/manage/group_type/overview - 5 terms: Geographical, Working group, Distribution, Event planning, Archive
-
field_group_typeexists oncommunity_group - Group directory at
/all-groupswith exposed filters - Archived groups hidden from default
/all-groupslisting -
do_group_extrasenabled:drush pm:list --filter=do_group_extras - Submission guidelines on group creation form
- Non-admin group creation defaults to unpublished
- Notification queued on pending group submission (email delivery handled by external system)
- Pending groups at
/admin/groups/pending - Archive enforcement blocks content creation
- “Archived” badge on archived groups
- Membership model tests pass (open/moderated/invite-only)
-
do_wikievaluated; enabled if wiki-link[[Title]]syntax is needed - If enabled: WikiLink filter added to
full_htmltext format - Config exported clean;
ddev drush cr— no errors; Nightwatch passes
Schema Changes (Phase 2)
Section titled “Schema Changes (Phase 2)”| Type | Name | Notes |
|---|---|---|
| Taxonomy | group_type | 5 terms (Geographical, Working group, Distribution, Event planning, Archive) |
| Field (group) | field_group_type | Entity reference → group_type vocabulary |
| View | all_groups at /all-groups | Directory with exposed filters |
| View | pending_groups at /admin/groups/pending | Moderation queue |
| Module | do_group_extras | Archive, guidelines, moderation queue |
| Module | do_wiki | Optional — [[Title]] wiki links |
Open Questions (Phase 2)
Section titled “Open Questions (Phase 2)”- Does
drupal/group3.x natively support request/approval, or requiregroup_membership_requestsub-module? ⏳ Deferred — will research the installed Group 3.3.5 code when we reach this step - What is the exact
form_idforcommunity_groupcreate/edit form? ⏳ Deferred — inspect at implementation time Notification role —✅ Confirmed:site_moderator,content_moderator, orcontent_administrator?site_moderator(maps to OSsitemanager)Archived groups — unpublish or keep published but read-only?✅ Confirmed: Both. Archived = published + read-only (visible with badge, no new content). Unpublished = completely hidden (Group entitystatusfield). Two separate mechanisms.
Phase 3 — Content in Groups
Section titled “Phase 3 — Content in Groups”Goal: Enable posting content to groups, build group streams, port multi-group posting, enable tags.
Source: IMPLEMENTATION_PLAN.md (removed) Phase 5
Pre-Phase 3 Snapshot
Section titled “Pre-Phase 3 Snapshot”ddev export-db --file=backups/pre-phase3-$(date +%Y%m%d-%H%M).sql.gzStep 300 — Configure Relationship Cardinality for Multi-Group Posting
Section titled “Step 300 — Configure Relationship Cardinality for Multi-Group Posting”✅ Already done in Phase 1. All 5 relationship type YAMLs were written with
entity_cardinality: 0from the start. No script needed.
Verify:
ddev drush php:eval 'echo \Drupal::entityTypeManager()->getStorage("group_relationship_type")->load("community_group-group_node-forum")->getPlugin()->getConfiguration()["entity_cardinality"] . "\n";'Expected: 0
Step 310 — Build Group Stream Views
Section titled “Step 310 — Build Group Stream Views”All three view YAMLs exist in docs/groups/config/. Batch-import:
cp docs/groups/config/views.view.group_content_stream.yml config/sync/cp docs/groups/config/views.view.group_events.yml config/sync/cp docs/groups/config/views.view.group_members.yml config/sync/ddev drush config:import -yddev drush config:export -y| View | Machine name | URL |
|---|---|---|
| Group Content Stream | group_content_stream | /group/{gid}/stream |
| Group Events | group_events | /group/{gid}/events |
| Group Members | group_members | /group/{gid}/members |
Sort: Created, newest first. Settings: distinct: true, Use AJAX = Yes.
Group Events View
Section titled “Group Events View”| Setting | Value |
|---|---|
| Machine name | group_events |
| Show | Content — type: Event |
| Format | Table |
Fields: Title (linked), Event date, Location. Same relationship and contextual filter.
Group Members View
Section titled “Group Members View”| Setting | Value |
|---|---|
| Machine name | group_members |
| Show | Group relationship (membership type) |
| Format | Grid / table |
Fields: Username (linked), Avatar, Group role, Joined date.
ddev drush config:export -yStep 320 — Sitewide Activity Stream (Post Wall)
Section titled “Step 320 — Sitewide Activity Stream (Post Wall)”320a — Create stream_card view mode
Section titled “320a — Create stream_card view mode”cp docs/groups/config/core.entity_view_mode.node.stream_card.yml config/sync/ddev drush config:import -yddev drush config:export -yCaution Must import
stream_cardview mode BEFORE importingactivity_streamview. The view depends on the view mode existing.
320b — (Intentional gap)
Section titled “320b — (Intentional gap)”320c — Create the Activity Stream View
Section titled “320c — Create the Activity Stream View”Important
Enable comment module BEFORE importing this view — the view has a comment join.
ddev drush en comment -yddev drush config:export -ycp docs/groups/config/views.view.activity_stream.yml config/sync/ddev drush config:import -yddev drush config:export -y| Setting | Value |
|---|---|
| View name | Activity Stream |
| Machine name | activity_stream |
| Show | Content (nodes) |
| Page path | /stream |
| Format | Unformatted list |
| Row | Content → stream_card view mode |
Sort: Last comment timestamp DESC (falls back to node created if no comments). Uses the comment_entity_statistics table which core creates automatically for any content type with a comment field.
Filters:
- Published = Yes
- Content type IN (
forum,documentation,event,post,page)
Settings: Distinct = true, Use AJAX = Yes, Pager = Full (10 items per page).
Config: views.view.activity_stream.yml — page at /stream (10 items) + block display (5 items)
cp docs/groups/config/views.view.activity_stream.yml config/sync/ddev drush cim -yTip To make the activity stream the site’s front page:
ddev drush config:set system.site page.front /stream -yAlternatively, keep the existing front page and place the stream View as a block.
320d — Add “Group badge” to stream cards
Section titled “320d — Add “Group badge” to stream cards”Each stream card should show which group(s) the content belongs to. Create a Views field or custom preprocess function that queries group_relationship for each node and renders the group name(s) as linked badges.
Option 1: Views relationship — Add a Group relationship to the activity_stream View and include the Group label field. This shows the group name for each node but may cause duplicate rows if a node is in multiple groups. Use distinct: true and GROUP_CONCAT or a custom Views field plugin.
Option 2: Preprocess — In do_group_extras.module, add:
/** * Implements hook_preprocess_node(). */function do_group_extras_preprocess_node(&$variables) { if ($variables['view_mode'] !== 'stream_card') { return; } $node = $variables['node']; $relationships = \Drupal::entityTypeManager() ->getStorage('group_relationship') ->loadByProperties(['entity_id' => $node->id()]);
$group_links = []; foreach ($relationships as $rel) { $group = $rel->getGroup(); if ($group) { $group_links[] = [ 'label' => $group->label(), 'url' => $group->toUrl()->toString(), ]; } } $variables['group_badges'] = $group_links;}Then in templates/node--stream-card.html.twig (theme template):
{% if group_badges %} <div class="stream-card__groups"> {% for badge in group_badges %} <a href="{{ badge.url }}" class="stream-card__group-badge">{{ badge.label }}</a> {% endfor %} </div>{% endif %}320e — Verify the activity stream
Section titled “320e — Verify the activity stream”# Verify view mode exists:ddev drush php:eval '$vm = \Drupal::entityTypeManager()->getStorage("entity_view_mode")->load("node.stream_card");echo "stream_card view mode: " . ($vm ? "OK" : "MISSING") . "\n";'
# Verify /stream page responds (after demo data exists):# ddev drush php:eval '# echo \Drupal::httpClient()->get("https://drupalorg.ddev.site/stream")->getStatusCode() . "\n";# 'Caution Key differences from Open Social’s stream:
- No Activity entities — we show nodes directly, not Activity wrappers
- No “post composer” — OS has an inline quick-post widget at the top of the stream; add this in Phase 4 if needed
- No activity grouping — OS groups related activities (“Maria posted 3 topics”); our stream shows each node individually
- Comment rendering — OS uses a custom AJAX comment loader; we use core’s comment display with a per-view-mode override for count limiting
ddev drush config:export -yStep 330 — Enable do_multigroup Module
Section titled “Step 330 — Enable do_multigroup Module”Allows content to be posted in multiple groups simultaneously.
Important Adaptation difficulty: 🔴 High (moved to ✅ Done). Deeply uses
group_relationshipAPI;group.membership_loaderservice does not exist in Group 3.x — replaced with a directgroup_relationshipentity storage query.
Source files (in repo at docs/groups/modules/do_multigroup/, deployed to web/modules/custom/do_multigroup/):
do_multigroup.info.ymldo_multigroup.services.yml— registersDoMultigroupHooksservicesrc/Hook/DoMultigroupHooks.php— all hook implementations as#[Hook]attributed methodsdo_multigroup.module— empty (@filedocblock only)do_multigroup.libraries.ymlcss/do_multigroup.css
Important
Group 3.x migration note: group.membership_loader service was removed in Group 3.x. User memberships are now loaded by querying group_relationship storage directly:
// Group 3.x — load memberships for current user:$memberships = $this->entityTypeManager ->getStorage('group_relationship') ->loadByProperties([ 'entity_id' => $account->id(), 'type' => 'community_group-group_membership', ]);Important This module uses the Drupal 11
#[Hook]attribute system. All hook logic is insrc/Hook/DoMultigroupHooks.php. The.modulefile is intentionally empty.
Enable
Section titled “Enable”# Step 1: Copy module from docs to webcp -r docs/groups/modules/do_multigroup web/modules/custom/# Step 2: Enable moduleddev drush en do_multigroup -y# Step 3: Export configddev drush config:export -y# Step 4: Clear cacheddev drush crStep 340 — Enable Tags on Group Content
Section titled “Step 340 — Enable Tags on Group Content”✅ Already done in Phase 1.
taxonomy.vocabulary.group_tagsandfield_group_tagson all 5 content types were imported via YAML in Steps 170–180.
Verify:
ddev drush php:eval '\Drupal\field\Entity\FieldStorageConfig::loadByName("node", "field_group_tags") ? print "OK\n" : print "MISSING\n";'Step 350 — Tags Aggregation View
Section titled “Step 350 — Tags Aggregation View”cp docs/groups/config/views.view.tags_aggregation.yml config/sync/ddev drush config:import -yddev drush config:export -yVerify: /tags/{term-name} returns content tagged with that term.
Step 360 — Phase 3 Tests
Section titled “Step 360 — Phase 3 Tests”Warning Review TROUBLESHOOTING.md before running tests. Kill zombie processes first:
ddev exec kill-zombies.sh
PHPUnit — Module: do_multigroup
Section titled “PHPUnit — Module: do_multigroup”Create web/modules/custom/do_multigroup/tests/src/Kernel/MultigroupTest.php:
testModuleEnabled()—do_multigroupmodule enabledtestGroupAudienceFormAlter()— form alter adds group audience fieldsettestCrossPostBadge()—hook_preprocess_node()adds cross-posted badge to multi-group content
ddev exec phpunit web/modules/custom/do_multigroup/tests/src/Kernel/MultigroupTest.phpPHPUnit — Integration (do_tests)
Section titled “PHPUnit — Integration (do_tests)”Create web/modules/custom/do_tests/tests/src/Kernel/Phase3Test.php:
testMultiGroupCardinality()— relationship types haveentity_cardinality: 0testGroupTagsVocabulary()—group_tagsvocabulary existstestFieldGroupTags()—field_group_tagsexists on all 5 content typestestGroupContentStreamView()—group_content_streamView config existstestStreamCardViewMode()—node.stream_cardview mode existstestStreamCardDisplayEnabled()—stream_carddisplay enabled for all 5 group-postable typestestActivityStreamView()—activity_streamView config exists with path/stream
ddev exec phpunit web/modules/custom/do_tests/tests/src/Kernel/Phase3Test.phpPlaywright (E2E)
Section titled “Playwright (E2E)”Create tests/e2e/phase3.spec.ts:
test('content appears in group stream')— create content in group, verify in group streamtest('multi-group posting')— post to Group A and B, verify in both streamstest('cross-posted badge')— multi-group content shows badge in teasertest('group audience fieldset')— as group member, verify fieldset on node create formtest('tag aggregation page')— tagged content at/tags/{term-name}test('activity stream page loads')— navigate to/stream, verify HTTP 200 and page titletest('stream shows recent content')— create a node in a group, navigate to/stream, verify node title appearstest('stream shows group badge')— verify group name badge appears on stream cardstest('stream shows inline comments')— post with comments shows comment text in the stream cardtest('stream pagination')— verify AJAX pager loads next page of content
npx playwright test tests/e2e/phase3.spec.tsPhase 3 — Verification
Section titled “Phase 3 — Verification”- Relationship types configured for: forum, documentation, event, post, page
- Each relationship type has
entity_cardinality: 0 - Content can be created within group context
- Group content stream displays posted content
- Group events and members Views work
-
distinct: trueprevents duplicates -
do_multigroupenabled:drush pm:list --filter=do_multigroup - “Group Audience” fieldset appears when user is a group member
- Fieldset shows only user’s groups
- Content posted to Groups A & B appears in both streams (once each)
- Full view shows “Posted in: Group A, Group B” with links
- Teaser shows “Cross-posted from Group X” badge
-
group_tagsvocabulary exists -
field_group_tagson all 5 content types - Tags aggregation page at
/tags/{term-name}works - Activity Stream (Step 320):
-
stream_cardview mode exists -
stream_carddisplay enabled forforum,documentation,event,post,page -
activity_streamView exists at/stream -
/streamrenders content sorted by last activity (newest first) - Stream cards show trimmed body, author, date, tags
- Stream cards show group badge(s) linking to source group(s)
- Stream cards show last 2-3 inline comments
- AJAX pagination works on stream page
-
- Config exported clean;
ddev drush cr— no errors; Nightwatch passes
Schema Changes (Phase 3)
Section titled “Schema Changes (Phase 3)”| Type | Name | Notes |
|---|---|---|
| Config change | group_relationship_type.* | Set entity_cardinality: 0 (unlimited groups per node) |
| Vocabulary | group_tags | Group-scoped tags (separate from sitewide tags) |
| Field (node) | field_group_tags on 5 content types | Entity reference → group_tags vocabulary |
| View | group_content_stream | Group-scoped content stream |
| View | group_events | Group-scoped events |
| View | group_members | Group membership listing |
| View | tags_aggregation at /tags | Sitewide tag cloud/list |
| View mode | node.stream_card | Stream card display for activity feed |
| View | activity_stream at /stream | Sitewide activity stream (post wall) |
| Module | do_multigroup | Multi-group posting UI + submit handler |
Custom module files (Phase 3)
Section titled “Custom module files (Phase 3)”docs/groups/modules/do_multigroup/ ← source of truth├── src/Hook/DoMultigroupHooks.php ← all #[Hook] logic├── do_multigroup.services.yml├── do_multigroup.info.yml├── do_multigroup.libraries.yml├── do_multigroup.module ← empty (@file docblock)└── css/do_multigroup.css
web/modules/custom/do_multigroup/ ← deployed copy (synced via cp -r)Open Questions (Phase 3)
Section titled “Open Questions (Phase 3)”Does Group 3.x use✅ Confirmed: yes, exact format.group_relationship_type.{group_type}-group_node-{bundle}.ymlformat?Should all 5 content types support multi-group posting, or only a subset?✅ Confirmed: all 5 (forum,documentation,event,post,page)Do any content types already have a tags field?✅ Resolved:documentationhasfield_tags→ sitewidetags. Groups use a separategroup_tagsvocabulary withfield_group_tags.Does Group 3.x still provide✅ No. Removed in Group 3.x. Usegroup.membership_loaderservice?group_relationshipstorage query:loadByProperties(['entity_id' => $uid, 'type' => 'community_group-group_membership'])instead.
Phase 4 — Discovery & Feeds
Section titled “Phase 4 — Discovery & Feeds”Goal: Document hot content scoring, promoted content, RSS feeds, and iCal feeds.
Source: IMPLEMENTATION_PLAN.md (removed) Phase 4
Important Adaptation difficulty: 🟢 Low for the core scoring module. 🟡 Medium for the iCal controller (Open Social-specific event fields and enrollment entity must be adapted).
Pre-Phase 4 Snapshot
Section titled “Pre-Phase 4 Snapshot”ddev export-db --file=backups/pre-phase4-$(date +%Y%m%d-%H%M).sql.gzStep 400 — Statistics Module
Section titled “Step 400 — Statistics Module”⚠️ Removed from Drupal 11 core. The statistics module was deprecated in Drupal 10.3 and removed from core in 11.0.
do_discovery checks for node_counter table gracefully (using tableExists()) and falls back to view_count = 0 if the table is missing. For now, view counting is deactivated and only comment counts drive hot scores.
If view counting is needed, install the ––––– contrib module:
ddev composer require drupal/statisticsddev drush en statistics -yNote: drupal:statistics was removed from the do_discovery.info.yml hard-dependency list so the module can be enabled on Drupal 11 without the statistics contrib module.
Step 410 — Install Flag Module
Section titled “Step 410 — Install Flag Module”Caution
Pre-flight: If composer.json has drush/drush: ^12 but the lock file has 13.x, Composer will refuse to install new packages. Fix the constraint first:
sed -i '' 's/"drush\/drush": "\^12"/"drush\/drush": "^13"/' composer.jsonddev composer require "drupal/flag:^4.0" --no-interactionddev drush en flag flag_count -yddev drush config:export -yVersion installed: drupal/flag 4.0.0-beta7 (compatible with Drupal 11).
Verify:
ddev drush pm:list --filter=flag --status=enabledStep 420 — Enable do_discovery Module
Section titled “Step 420 — Enable do_discovery Module”Hot content scoring, iCal feeds, promoted content, and per-group RSS.
Source files (in repo at docs/groups/modules/do_discovery/, deployed to web/modules/custom/do_discovery/):
do_discovery.info.yml—core_version_requirement: ^10 || ^11; depends ondrupal:views,drupal:node,drupal:taxonomy,drupal:flag(NOTdrupal:statistics— removed, handled gracefully)do_discovery.install—hook_schema()createsdo_discovery_hot_scoretabledo_discovery.services.yml— registersDoDiscoveryHooksservicesrc/Hook/DoDiscoveryHooks.php—hook_cron,hook_views_data,hook_node_insertvia#[Hook]do_discovery.module— empty (@filedocblock)do_discovery.routing.yml— 3 iCal routessrc/Controller/IcalController.php— 3 iCal endpointsconfig/install/flag.flag.promote_homepage.yml— bundled flag (auto-imports on enable)
Important
Enable comment module BEFORE enabling do_discovery. The activity_stream view (already in config/sync) has a comment join; if comment is not enabled, config:import will fail. Enable in this sequence:
ddev drush en comment -y && ddev drush config:export -yImportant This module uses the Drupal 11
#[Hook]attribute system. All hook logic is insrc/Hook/DoDiscoveryHooks.php.
Enable
Section titled “Enable”# Step 1: Copy module from docs to webcp -r docs/groups/modules/do_discovery web/modules/custom/# Step 2: Enable module (also auto-installs flag.flag.promote_homepage)ddev drush en do_discovery -y# Step 3: Export config (captures module + promote_homepage flag)ddev drush config:export -y# Step 4: Verify hot_score table was createdddev drush sqlq "SHOW TABLES LIKE 'do_discovery%'"Expected: do_discovery_hot_score
Step 430 — Create RSVP Flag (for iCal user events)
Section titled “Step 430 — Create RSVP Flag (for iCal user events)”rsvp_event flag YAML exists in docs/groups/config/. Import it alongside its system actions after enabling do_discovery:
cp docs/groups/config/flag.flag.rsvp_event.yml config/sync/cp docs/groups/config/system.action.flag_action.rsvp_event_flag.yml config/sync/cp docs/groups/config/system.action.flag_action.rsvp_event_unflag.yml config/sync/ddev drush config:import -yddev drush config:export -yFlag spec:
| Setting | Value |
|---|---|
| Label | RSVP for event |
| Machine name | rsvp_event |
| Flag type | Content (Node) |
| Bundles | Event only |
| Global | No (per-user) |
Grant permissions:
ddev drush role:perm:add authenticated "flag rsvp_event,unflag rsvp_event"ddev drush config:export -yStep 440 — Grant Flag Permissions
Section titled “Step 440 — Grant Flag Permissions”Promote homepage flag permissions were auto-imported with do_discovery (bundled config). Grant them to the appropriate roles:
ddev drush role:perm:add content_administrator "flag promote_homepage,unflag promote_homepage"ddev drush role:perm:add site_moderator "flag promote_homepage,unflag promote_homepage"ddev drush config:export -yStep 450 — Hot Content View
Section titled “Step 450 — Hot Content View”cp docs/groups/config/views.view.hot_content.yml config/sync/ddev drush config:import -yddev drush config:export -yVerify: navigate to /hot.
Relationship
Section titled “Relationship”Add a relationship to do_discovery_hot_score (exposed by hook_views_data()):
- Table: Hot Content
- Field: Node ID → Scored Content relationship
- Required: No (nodes without scores should still appear)
Fields
Section titled “Fields”| Field | Configuration |
|---|---|
| Title | Linked to node |
| Content type | Machine name label |
| Hot Score | Numeric, from relationship |
| Created date | Medium format |
| Comment count | From comment_entity_statistics |
Sort Criteria
Section titled “Sort Criteria”- Hot Score: Descending (primary sort)
- Created date: Descending (secondary, for nodes with equal scores)
Filter Criteria (non-exposed)
Section titled “Filter Criteria (non-exposed)”| Filter | Value |
|---|---|
| Published | Yes |
| Created date | Last 7 days (rolling window) |
Exposed Filters
Section titled “Exposed Filters”| Filter | Type | Notes |
|---|---|---|
| Content type | Select list | Options: forum, documentation, event, post, page |
| ”In my groups” | Boolean | Requires a group_relationship join — only show content from groups the current user is a member of |
Note The “In my groups” exposed filter requires a Views relationship to
group_relationship_field_dataand a contextual filter comparinggidto the current user’s group memberships. This is complex in Views UI — consider implementing it as a custom Views filter plugin in a future phase.
ddev drush config:export -yStep 460 — Promoted Content View
Section titled “Step 460 — Promoted Content View”cp docs/groups/config/views.view.promoted_content.yml config/sync/ddev drush config:import -yddev drush config:export -yVerify: /admin/content/promoted (page) and front page block.
Step 470 — Group RSS Feed
Section titled “Step 470 — Group RSS Feed”cp docs/groups/config/views.view.group_rss_feed.yml config/sync/ddev drush config:import -yddev drush config:export -yVerify: /group/{gid}/stream/feed returns valid RSS XML.
Step 480 — iCal Feed Verification
Section titled “Step 480 — iCal Feed Verification”After enabling do_discovery, verify the iCal endpoints:
Site-wide iCal
Section titled “Site-wide iCal”ddev drush php:eval 'echo \Drupal::httpClient() ->get("https://drupalorg.ddev.site/upcoming-events/ical", ["verify" => FALSE]) ->getStatusCode() . "\n";'Expected: 200 with Content-Type: text/calendar
Group iCal
Section titled “Group iCal”# Replace {gid} with a test group ID:curl -k https://drupalorg.ddev.site/group/{gid}/events/icalExpected: BEGIN:VCALENDAR … END:VCALENDAR
User iCal
Section titled “User iCal”# Replace {uid} with a test user ID:curl -k https://drupalorg.ddev.site/user/{uid}/events/icalStep 490 — Phase 4 Tests
Section titled “Step 490 — Phase 4 Tests”Warning Review TROUBLESHOOTING.md before running tests. Kill zombie processes first:
ddev exec kill-zombies.sh
PHPUnit — Module: do_discovery
Section titled “PHPUnit — Module: do_discovery”Create web/modules/custom/do_discovery/tests/src/Kernel/DiscoveryTest.php:
testModuleEnabled()—do_discoverymodule enabled;do_discovery_hot_scoretable existstestHotScoreCalculation()— create node with views/comments, run cron, verify hot score > 0testICalRoute()—/group/{gid}/events/icalroute is registeredtestICalController()— controller returns valid iCal response for group events
PHPUnit — Integration (do_tests)
Section titled “PHPUnit — Integration (do_tests)”Create web/modules/custom/do_tests/tests/src/Kernel/Phase4Test.php:
testFlagsExist()—rsvp_event,promote_homepageflags exist (note: RUNBOOK previously usedpromote_to_homepage— correct machine name ispromote_homepage)testHotContentView()—hot_contentView config exists with expected filters— Skip — statistics module not available for Drupal 11testStatisticsEnabled()
ddev exec phpunit web/modules/custom/do_tests/tests/src/Kernel/Phase4Test.phpPlaywright (E2E)
Section titled “Playwright (E2E)”Create tests/e2e/phase4.spec.ts:
test('hot content page loads')— navigate to/hot, verify content sorted by hot scoretest('promoted content page loads')— navigate to/promoted, verify promoted contenttest('event RSVP toggle')— flag/unflag event, verify toggletest('RSS feed valid XML')— fetch/group/{gid}/rss.xml, verify valid RSStest('iCal feed valid')— fetch/group/{gid}/events/ical, verifyBEGIN:VCALENDARtest('statistics tracks views')— visit node, verify view count increments
npx playwright test tests/e2e/phase4.spec.tsPhase 4 — Verification
Section titled “Phase 4 — Verification”- Statistics module enabled:
drush pm:list --filter=statistics - Flag + flag_count modules enabled:
drush pm:list --filter=flag -
do_discoveryenabled:drush pm:list --filter=do_discovery -
do_discovery_hot_scoretable exists:ddev drush sqlq "SHOW TABLES LIKE 'do_discovery%'" -
promote_homepageflag exists at/admin/structure/flags -
rsvp_eventflag exists (if implementing event RSVP via Flag) - Flag permissions granted to
content_administratorandsite_moderator -
/hotpage loads with content sorted by score - Hot content View has exposed filters for content type
- Promoted content block on front page works
-
/admin/content/promotedlists flagged content - iCal endpoint
/upcoming-events/icalreturnstext/calendar - iCal endpoint
/group/{gid}/events/icalreturns group-scoped events - iCal endpoint
/user/{uid}/events/icalreturns user’s RSVP’d events - Group RSS feed at
/group/{gid}/stream/feedreturns valid RSS XML - Config exported clean:
ddev drush config:status -
ddev drush cr— no PHP errors - Existing Nightwatch tests still pass:
composer nightwatch
Schema Changes (Phase 4)
Section titled “Schema Changes (Phase 4)”| Type | Name | Notes |
|---|---|---|
| DB table | do_discovery_hot_score | nid INT PK, score FLOAT, computed INT |
| DB table | node_counter | Page view counts (via statistics module) |
| Flag | promote_homepage | Global, entity:node |
| Flag | rsvp_event | Per-user, entity:node (event RSVP) |
| View | hot_content at /hot | Score-sorted content with exposed filters |
| View | promoted_content at /admin/content/promoted | Flagged content management |
| View | group_rss_feed | Per-group RSS at /group/{gid}/rss.xml |
| Route | /user/{user}/events/ical | User’s RSVP’d events iCal feed |
| Route | /group/{group}/events/ical | Group events iCal feed |
| Route | /upcoming-events/ical | All events iCal feed |
| Module | statistics | Core (10.2) or contrib (10.3+) |
| Module | flag + flag_count | Flagging API + counts sub-module |
| Module | do_discovery | Hot scores, iCal, Views data |
Custom module files (Phase 4)
Section titled “Custom module files (Phase 4)”docs/groups/modules/do_discovery/ ← source of truth├── config/install/│ └── flag.flag.promote_homepage.yml ← auto-imported on module enable├── src/│ ├── Controller/│ │ └── IcalController.php ← 3 iCal endpoints (site/group/user)│ └── Hook/│ └── DoDiscoveryHooks.php ← #[Hook] cron, views_data, node_insert├── do_discovery.info.yml├── do_discovery.install ← hook_schema (hot_score table)├── do_discovery.module ← empty (@file docblock)├── do_discovery.routing.yml ← 3 iCal routes└── do_discovery.services.yml ← registers DoDiscoveryHooks service
web/modules/custom/do_discovery/ ← deployed copy (synced via cp -r)Note DI autowire gotcha (Drupal 11): Do NOT use
Psr\Log\LoggerInterfaceorDrupal\Core\Logger\LoggerChannelInterfaceas constructor argument type hints inhook_implementations-tagged services — Drupal’sDefinitionErrorExceptionPassvalidates the type hint even whenautowire: falseis set. Use\Drupal::logger()statically in private helper methods instead.
Key Adaptations from Open Social (Phase 4)
Section titled “Key Adaptations from Open Social (Phase 4)”| Aspect | Open Social (pl-opensocial) | Standard Drupal (pl-drupalorg) |
|---|---|---|
| Event date field | field_event_date | field_date_of_event |
| Event end date | field_event_date_end | field_date_of_event end_value (daterange) |
| Event enrollment | event_enrollment entity | Flag: rsvp_event (per-user) |
| Promote bundles | topic, event, page | forum, documentation, event, post, page |
| Flag roles | contentmanager, sitemanager | content_administrator, site_moderator |
| Group content table | group_content_field_data | group_relationship_field_data |
Open Questions (Phase 4)
Section titled “Open Questions (Phase 4)”Event date field type: ✅ Resolved — keep existingfield_date_of_event(datetime).Event RSVP mechanism: ✅ Resolved — Flag module (rsvp_eventflag). Simple, already installed.Hot content “In my groups” filter: ✅ Confirmed — implement as a custom Views filter plugin (Phase 6 scope).Statistics module deprecation: ✅ Resolved — removed as hard dep.do_discoveryhandles missingnode_countertable gracefully with atableExists()guard.
Phase 5 — Notifications & Subscriptions
Section titled “Phase 5 — Notifications & Subscriptions”Goal: Document follow/subscription infrastructure, per-post notification opt-out, subscription management page, and mute capabilities.
Source: IMPLEMENTATION_PLAN.md (removed) Phase 6
Note Drupal does NOT send email. Drupal only records notification events (“what happened”). An external system reads the queue and handles all recipient resolution, suppression checks, frequency batching, and email delivery.
Important Adaptation difficulty: 🟢 Low. The subscription management UI (controller, form) uses Flag service API which is portable. The notification event recording is a trivial queue insert — all complexity lives in the external system.
Pre-Phase 5 Snapshot
Section titled “Pre-Phase 5 Snapshot”ddev export-db --file=backups/pre-phase5-$(date +%Y%m%d-%H%M).sql.gzStep 500 — Evaluate Existing Notification Infrastructure
Section titled “Step 500 — Evaluate Existing Notification Infrastructure”Before implementing, check what pl-drupalorg already has:
# Check for message/notification modules:ddev drush pm:list --filter=message --status=enabledddev drush pm:list --filter=rabbitmq --status=enabledddev drush pm:list --filter=symfony_mailer --status=enabled
# Check mail configuration:ddev drush config:get system.mail interface
# Check if RabbitMQ is configured in DDEV:ddev describe | grep -i rabbitNote pl-drupalorg has RabbitMQ available as a DDEV service. Email is never sent by Drupal. Drupal only enqueues notification items (queue table or RabbitMQ). An external system consumes the queue and handles actual email delivery.
Step 510 — Import Follow Flags
Section titled “Step 510 — Import Follow Flags”Flag (from Phase 4) is already installed. All flag YAMLs exist in docs/groups/config/. Batch-import:
cp docs/groups/config/flag.flag.follow_content.yml config/sync/cp docs/groups/config/flag.flag.follow_user.yml config/sync/cp docs/groups/config/flag.flag.follow_term.yml config/sync/cp docs/groups/config/flag.flag.mute_group_notifications.yml config/sync/cp docs/groups/config/system.action.flag_action.follow_content_flag.yml config/sync/cp docs/groups/config/system.action.flag_action.follow_content_unflag.yml config/sync/cp docs/groups/config/system.action.flag_action.follow_user_flag.yml config/sync/cp docs/groups/config/system.action.flag_action.follow_user_unflag.yml config/sync/cp docs/groups/config/system.action.flag_action.follow_term_flag.yml config/sync/cp docs/groups/config/system.action.flag_action.follow_term_unflag.yml config/sync/cp docs/groups/config/system.action.flag_action.mute_group_notifications_flag.yml config/sync/cp docs/groups/config/system.action.flag_action.mute_group_notifications_unflag.yml config/sync/ddev drush config:import -yddev drush config:export -yCaution
entity:profile→entity:user:follow_userusesentity:user(notentity:profile) because pl-drupalorg uses standard Drupal user entities, not the contributedprofilemodule.
Caution
show_in_linksview mode adaption:mute_group_notificationsusesfull: full, teaser: teaser(nothero: hero, teaser: teaser) because theheroview mode doesn’t exist in Olivero/Claro.
510e — Grant flag permissions
Section titled “510e — Grant flag permissions”ddev drush role:perm:add authenticated "flag follow_content,unflag follow_content"ddev drush role:perm:add authenticated "flag follow_user,unflag follow_user"ddev drush role:perm:add authenticated "flag follow_term,unflag follow_term"ddev drush role:perm:add authenticated "flag mute_group_notifications,unflag mute_group_notifications"ddev drush config:export -yCreate web/modules/custom/do_notifications/config/install/flag.flag.follow_content.yml:
langcode: enstatus: truedependencies: module: - nodeid: follow_contentlabel: 'Follow content'bundles: {}entity_type: nodeglobal: falseweight: 0flag_short: 'Follow content'flag_long: ''flag_message: ''unflag_short: 'Unfollow content'unflag_long: ''unflag_message: ''unflag_denied_text: ''flag_type: 'entity:node'link_type: ajax_linkflagTypeConfig: show_in_links: full: full show_as_field: true show_on_form: false show_contextual_link: false extra_permissions: {} access_author: ''linkTypeConfig: {}No changes from the Open Social version — entity:node per-user flag with AJAX link.
510b — Follow User flag
Section titled “510b — Follow User flag”Create web/modules/custom/do_notifications/config/install/flag.flag.follow_user.yml:
langcode: enstatus: truedependencies: module: - userid: follow_userlabel: 'Follow user'bundles: {}entity_type: userglobal: falseweight: 0flag_short: Followflag_long: ''flag_message: ''unflag_short: Unfollowunflag_long: ''unflag_message: ''unflag_denied_text: ''flag_type: 'entity:user'link_type: ajax_linkflagTypeConfig: show_in_links: {} show_as_field: true show_on_form: false show_contextual_link: false extra_permissions: owner: '0'linkTypeConfig: {}Caution Key change from Open Social version: The
entity_typechanges fromprofiletouser, and theflag_typechanges fromentity:profiletoentity:user. Open Social uses the contributedprofilemodule for user profiles; pl-drupalorg uses standard Drupal user entities. Thesocial_follow_userenforced dependency is also removed.
510c — Follow Term flag
Section titled “510c — Follow Term flag”Create web/modules/custom/do_notifications/config/install/flag.flag.follow_term.yml:
langcode: enstatus: truedependencies: module: - taxonomyid: follow_termlabel: 'Follow term'bundles: {}entity_type: taxonomy_termglobal: falseweight: 0flag_short: Followflag_long: ''flag_message: ''unflag_short: Unfollowunflag_long: ''unflag_message: ''unflag_denied_text: ''flag_type: 'entity:taxonomy_term'link_type: ajax_linkflagTypeConfig: show_in_links: {} show_as_field: false show_on_form: false show_contextual_link: false extra_permissions: {}linkTypeConfig: {}No changes — entity:taxonomy_term is standard.
510d — Mute Group Notifications flag
Section titled “510d — Mute Group Notifications flag”Create web/modules/custom/do_notifications/config/install/flag.flag.mute_group_notifications.yml:
langcode: enstatus: truedependencies: module: - groupid: mute_group_notificationslabel: 'Mute Group Notifications'bundles: {}entity_type: groupglobal: falseweight: 0flag_short: 'Mute group'flag_long: 'Muting the group notifications will result in their notifications not being sent to you anymore.'flag_message: ''unflag_short: 'Unmute group'unflag_long: 'Receive the notifications of this group again.'unflag_message: ''unflag_denied_text: ''flag_type: 'entity:group'link_type: ajax_linkflagTypeConfig: show_in_links: full: full teaser: teaser show_as_field: true show_on_form: false show_contextual_link: false extra_permissions: owner: '0'linkTypeConfig: {}Note Changed
show_in_linksfromhero: hero, teaser: teaser(Open Social view modes) tofull: full, teaser: teaser(standard Drupal view modes). Theheroview mode doesn’t exist in bluecheese.
510e — Grant flag permissions
Section titled “510e — Grant flag permissions”ddev drush role:perm:add authenticated "flag follow_content,unflag follow_content"ddev drush role:perm:add authenticated "flag follow_user,unflag follow_user"ddev drush role:perm:add authenticated "flag follow_term,unflag follow_term"ddev drush role:perm:add authenticated "flag mute_group_notifications,unflag mute_group_notifications"ddev drush config:export -yStep 520 — Enable do_notifications Module
Section titled “Step 520 — Enable do_notifications Module”Per-post opt-out, follow subscriptions, and subscription management page.
Important Enable
commentmodule before enablingdo_notifications. The module’scommentInserthook depends on comment entities. (Comment was already enabled in Phase 4 for activity_stream view.)
Source files (in repo at docs/groups/modules/do_notifications/, deployed to web/modules/custom/do_notifications/):
do_notifications.info.yml—core_version_requirement: ^10 || ^11; depends ondrupal:flag,drupal:node,drupal:userdo_notifications.services.yml— registersDoNotificationsHooksservicesrc/Hook/DoNotificationsHooks.php—hook_form_node_form_alter,hook_node_insert,hook_comment_insertvia#[Hook]do_notifications.module— empty (@filedocblock)do_notifications.routing.yml— 3 routes (settings, cancel, admin defaults)do_notifications.links.task.yml— “Notifications” tab on user profilessrc/Controller/NotificationSettingsController.php— subscription management pagesrc/Form/CancelAllSubscriptionsForm.php— cancel-all confirmation formsrc/Form/NotificationDefaultsForm.php— admin defaults config formconfig/optional/do_notifications.settings.yml— default config (frequency, auto-subscribe)config/optional/flag.flag.*.yml— follow flags (skipped if already active)
Important This module uses the Drupal 11
#[Hook]attribute system. All hook logic is insrc/Hook/DoNotificationsHooks.php.
Caution
config/optional/vsconfig/install/: Flag configs are inconfig/optional/(notconfig/install/). If the flags are already in active config (imported globally in Step 510),config/install/would throw aPreExistingConfigException.config/optional/silently skips them.
Caution DI type-check gotcha:
FlagServiceInterfacecannot be injected intohook_implementations-tagged services — Drupal’sDefinitionErrorExceptionPassrejects interface types it can’t alias. Use\Drupal::service('flag')statically in the method body. This is a known pattern documented in the DI autowire gotcha note in Phase 4.
Enable
Section titled “Enable”# Step 1: Copy module from docs to webrm -rf web/modules/custom/do_notificationscp -r docs/groups/modules/do_notifications web/modules/custom/# Step 2: Enable moduleddev drush en do_notifications -y# Step 3: Export configddev drush config:export -yVerify:
ddev drush pm:list --filter=do_notifications --status=enabledStep 530 — Notification Event Recording Architecture
Section titled “Step 530 — Notification Event Recording Architecture”Note Drupal only records what happened. On entity insert/update, Drupal writes a lightweight event item to a queue. The external system reads the queue and handles everything else: recipient resolution (who follows what), suppression/mute checks, frequency batching, email rendering, and delivery.
What Drupal records (one item per triggering event)
Section titled “What Drupal records (one item per triggering event)”| Field | Value | Example |
|---|---|---|
event | What happened | node_created, comment_created, group_membership_added |
entity_type | The entity that triggered it | node, comment |
entity_id | Entity ID | 42 |
bundle | Entity bundle | forum, event, documentation |
author_uid | Who triggered it | 5 |
group_ids | Groups this entity belongs to | [1, 3] (from group_relationship) |
timestamp | When it happened | 1742300000 |
What the external system handles
Section titled “What the external system handles”- Reads
flaggingtable to find followers (follow_content,follow_user,follow_term) - Reads
do_notifications_suppress_{nid}State API key for per-post opt-out - Reads
do_notifications_disabled_{uid}State API key for user-level disable - Reads
mute_group_notificationsflaggings for group muting - Reads
field_notification_frequencyon user entity for batching - Renders email content and delivers
Step 540 — Notification Settings (Two-Tier Design)
Section titled “Step 540 — Notification Settings (Two-Tier Design)”Note Notification settings use a two-tier design: a site-wide admin form sets defaults, and each user can override on their own notification settings page.
540a — Admin defaults form
Section titled “540a — Admin defaults form”The admin form and default config are included in the do_notifications module files (see Step 520):
NotificationDefaultsForm.phpdo_notifications.settings.yml
| Setting | Config key | Default |
|---|---|---|
| Default frequency | do_notifications.settings.default_frequency | immediately |
| Auto-subscribe on comment | do_notifications.settings.auto_subscribe_comment | true |
Access: administer site configuration permission.
540b — Per-user notification frequency field
Section titled “540b — Per-user notification frequency field”YAML exists in docs/groups/config/. Import it alongside the flag configs in Step 510:
cp docs/groups/config/field.storage.user.field_notification_frequency.yml config/sync/cp docs/groups/config/field.field.user.user.field_notification_frequency.yml config/sync/ddev drush config:import -yddev drush config:export -y| Setting | Value |
|---|---|
| Field type | List (text) |
| Label | Notification frequency |
| Machine name | field_notification_frequency |
| Allowed values | immediately → Immediately, daily → Daily digest, weekly → Weekly digest |
Verify:
ddev drush php:eval '\Drupal\field\Entity\FieldStorageConfig::loadByName("user", "field_notification_frequency") ? print "OK\n" : print "MISSING\n";'Step 550 — Notification Event Recording
Section titled “Step 550 — Notification Event Recording”Important All hook logic is in
DoNotificationsHooksvia#[Hook]. The.modulefile is intentionally empty. The hooks only record what happened — no follower lookups, no suppression checks, no email.
Hooks implemented in DoNotificationsHooks:
formNodeFormAlter— adds “Do not send notifications” checkbox to group-postable node formsnodeFormSubmit(static) — stores suppression flag per-node in State APInodeInsert— queuesnode_createdevent for published group-postable contentcommentInsert— queuescomment_createdevent; auto-subscribes commenter viafollow_contentflag
Drupal only records what happened (one queue item per event, not per recipient):
/** * Implements hook_node_insert(). * * Records a notification event. External system handles delivery. */function do_notifications_node_insert(NodeInterface $node) { _do_notifications_record_event('node_created', $node);}
/** * Records a notification event to the queue. * * One item per triggering event (not per recipient). * External system resolves recipients from flags/subscriptions. */function _do_notifications_record_event(string $event, $entity): void { $item = [ 'event' => $event, 'entity_type' => $entity->getEntityTypeId(), 'entity_id' => $entity->id(), 'bundle' => $entity->bundle(), 'author_uid' => $entity->getOwnerId(), 'group_ids' => _do_notifications_get_group_ids($entity), 'timestamp' => \Drupal::time()->getRequestTime(), ]; \Drupal::queue('do_notifications')->createItem($item);}
/** * Returns group IDs this entity belongs to (via group_relationship). */function _do_notifications_get_group_ids($entity): array { if ($entity->getEntityTypeId() !== 'node') { return []; } $relationships = \Drupal::entityTypeManager() ->getStorage('group_relationship') ->loadByProperties([ 'entity_id' => $entity->id(), // Step 140 abbreviated documentation→doc for the 32-char ID limit. 'type' => 'community_group-group_node-' . ($entity->bundle() === 'documentation' ? 'doc' : $entity->bundle()), ]); return array_map(fn($r) => $r->getGroup()->id(), $relationships);}Note This creates one queue item per event, not one per recipient. The external system reads the
do_notificationsqueue, resolves recipients from theflaggingandgroup_relationshiptables, checks suppression/mute/frequency preferences, and delivers.
Step 560 — Phase 5 Tests
Section titled “Step 560 — Phase 5 Tests”Warning Review TROUBLESHOOTING.md before running tests. Kill zombie processes first:
ddev exec kill-zombies.sh
PHPUnit — Module: do_notifications
Section titled “PHPUnit — Module: do_notifications”Create web/modules/custom/do_notifications/tests/src/Kernel/NotificationsTest.php:
testModuleEnabled()—do_notificationsmodule enabledtestEventRecording()— create a node, verify queue item with correct structure (event,entity_type,entity_id,bundle,author_uid,group_ids,timestamp)testPerPostSuppression()— setdo_notifications_suppress_{nid}via State API, verify it reads backtestAdminDefaultsConfig()—do_notifications.settingsconfig exists withdefault_frequencyandauto_subscribe_commentkeystestCommentAutoSubscribe()— create comment, verifyfollow_contentflag auto-set
PHPUnit — Integration (do_tests)
Section titled “PHPUnit — Integration (do_tests)”Create web/modules/custom/do_tests/tests/src/Kernel/Phase5Test.php:
testFollowFlags()—follow_content,follow_user,follow_termflags exist with correct entity typestestMuteFlag()—mute_group_notificationsflag exists, targetsgroupentitytestNotificationQueue()— queuedo_notificationsexiststestNotificationFrequencyField()—field_notification_frequencyexists on user entity with correct allowed values
ddev exec phpunit web/modules/custom/do_tests/tests/src/Kernel/Phase5Test.phpPlaywright (E2E)
Section titled “Playwright (E2E)”Create tests/e2e/phase5.spec.ts:
test('follow content toggle')— log in, navigate to a node, click Follow, verify flaggedtest('follow user toggle')— navigate to user profile, click Follow Usertest('mute group notifications')— navigate to group, click Mutetest('per-post opt-out checkbox')— create node, verify “Disable notifications” checkboxtest('notification settings page')— navigate to/user/{uid}/notification-settings, verify UItest('admin defaults form')— log in as admin, verify/admin/config/people/notification-defaultstest('comment auto-subscribes')— comment on a node, verify auto-follow
npx playwright test tests/e2e/phase5.spec.tsPhase 5 — Verification
Section titled “Phase 5 — Verification”- Follow flags exist:
follow_content,follow_user,follow_term,mute_group_notifications - Flag permissions granted to authenticated users
-
do_notificationsenabled:drush pm:list --filter=do_notifications - “Notifications” tab appears on user profile pages
-
/user/{uid}/notification-settingspage loads for own account - Page shows “Active Subscriptions: 0” when no flags
- Follow a node → subscription count increments to 1
- Subscriptions table shows Type, Title, Remove action
- “Remove” link unflaqs the entity (count drops)
- “Temporarily disable all” toggle works (warning message appears)
- “Re-enable all” toggle reverses the disable
- “Cancel all subscriptions” shows confirmation form
- Confirming cancellation removes all flaggings (count = 0)
- “Do not notify” checkbox on node create/edit forms
- Checkbox stores suppression flag in State API (
do_notifications_suppress_{nid}) -
field_notification_frequencyexists on user entity -
hook_node_insert()records event todo_notificationsqueue - Queue items contain: event, entity_type, entity_id, bundle, author_uid, group_ids, timestamp
- Config exported clean:
ddev drush config:status -
ddev drush cr— no PHP errors - Existing Nightwatch tests still pass:
composer nightwatch
Schema Changes (Phase 5)
Section titled “Schema Changes (Phase 5)”| Type | Name | Notes |
|---|---|---|
| Flag | follow_content | Per-user, entity:node |
| Flag | follow_user | Per-user, entity:user |
| Flag | follow_term | Per-user, entity:taxonomy_term |
| Flag | mute_group_notifications | Per-user, entity:group |
| Field (user) | field_notification_frequency | List (immediate/daily/weekly) |
| Route | /user/{uid}/notification-settings | Subscription management page |
| Route | /user/{uid}/notification-settings/cancel | Cancel all subscriptions form |
| Module | do_notifications | Subscriptions, opt-out, event recording |
Custom module files (Phase 5)
Section titled “Custom module files (Phase 5)”docs/groups/modules/do_notifications/ ← source of truth├── config/│ └── optional/ ← skipped if flags already in active config│ ├── do_notifications.settings.yml│ ├── flag.flag.follow_content.yml│ ├── flag.flag.follow_user.yml│ ├── flag.flag.follow_term.yml│ └── flag.flag.mute_group_notifications.yml├── src/│ ├── Controller/│ │ └── NotificationSettingsController.php│ ├── Form/│ │ ├── CancelAllSubscriptionsForm.php│ │ └── NotificationDefaultsForm.php│ └── Hook/│ └── DoNotificationsHooks.php ← #[Hook] form_alter, node_insert, comment_insert├── do_notifications.info.yml├── do_notifications.links.task.yml├── do_notifications.module ← empty (@file docblock)├── do_notifications.routing.yml└── do_notifications.services.yml ← registers DoNotificationsHooks service
web/modules/custom/do_notifications/ ← deployed copy (synced via rm -rf + cp -r)Note
config/optional/vsconfig/install/: Useconfig/optional/for flag configs that may already exist in active config.config/install/would throwPreExistingConfigExceptionif the flags were imported globally before the module was enabled.
Key Adaptations from Open Social (Phase 5)
Section titled “Key Adaptations from Open Social (Phase 5)”| Aspect | Open Social (pl-opensocial) | Standard Drupal (pl-drupalorg) |
|---|---|---|
| Follow user entity | entity:profile (profile module) | entity:user (core user) |
| Follow user dependency | social_follow_user (enforced) | None (Flag is sufficient) |
| Notification approach | activity entity + activity_send_email pipeline + ActivityDigestWorker | Event recording only — one queue item per event; external system resolves recipients and delivers |
| Content types | topic, event, page | forum, documentation, event, post, page |
| View mode for mute | hero, teaser (OS view modes) | full, teaser (standard view modes) |
Open Questions (Phase 5)
Section titled “Open Questions (Phase 5)”Notification sending: ✅ Resolved — Drupal enqueues notification items only; an external system handles email delivery.Digest implementation: ✅ Resolved — external delivery system is responsible for batching by frequency preference.Group notification triggers: ✅ Confirmed: only when explicitly following (group membership alone does not trigger notifications)Comment notifications: ✅ Confirmed: commenting auto-subscribes the commenter to the thread (follow_contentflag auto-set on comment insert)Notification frequency UI: ✅ Confirmed: two-tier design. Admin form at/admin/config/people/notification-defaultssets site-wide defaults; per-user settings page at/user/{uid}/notification-settingslets users override.
Phase 6 — User Profiles & Group Admin
Section titled “Phase 6 — User Profiles & Group Admin”Goal: Port profile contribution stats, content pinning within groups, group mission sidebar, and group-level language negotiation.
Source: IMPLEMENTATION_PLAN.md (removed) Phases 7–8
Pre-Phase 6 Snapshot
Section titled “Pre-Phase 6 Snapshot”ddev export-db --file=backups/pre-phase6-$(date +%Y%m%d-%H%M).sql.gzStep 600 — Enable do_profile_stats Module
Section titled “Step 600 — Enable do_profile_stats Module”Contribution stats block and profile completeness indicator for user profiles.
Source files (in repo at docs/groups/modules/do_profile_stats/, deployed to web/modules/custom/do_profile_stats/):
do_profile_stats.info.yml—core_version_requirement: ^10 || ^11do_profile_stats.services.yml— registersDoProfileStatsHooksservicesrc/Hook/DoProfileStatsHooks.php—hook_theme,hook_page_attachmentsvia#[Hook]do_profile_stats.module— empty (@filedocblock)do_profile_stats.libraries.ymlsrc/Plugin/Block/ContributionStatsBlock.php— counts topics, events, comments, groups, days active (topic→forum)src/Plugin/Block/ProfileCompletenessBlock.php— usesuserentity (not Open Socialprofile)templates/pl-contribution-stats.html.twigtemplates/pl-profile-completeness.html.twigcss/do_profile_stats.css
Warning Profile completeness fields: Update the
$fields_to_checkarray inProfileCompletenessBlock.phpafter researching which user fields actually exist on this site.
Enable
Section titled “Enable”# Step 1: Copy module from docs to webcp -r docs/groups/modules/do_profile_stats web/modules/custom/# Step 2: Enable moduleddev drush en do_profile_stats -y# Step 3: Export configddev drush config:export -y600h — Place blocks in bluecheese
Section titled “600h — Place blocks in bluecheese”Note Replace
"theme" => "bluecheese"with the actual active theme name if different.
ddev drush php:eval '$block_storage = \Drupal::entityTypeManager()->getStorage("block");
// Contribution Stats block.if (!$block_storage->load("do_contribution_stats")) { $block_storage->create([ "id" => "do_contribution_stats", "plugin" => "do_contribution_stats", "region" => "content", "theme" => "bluecheese", "weight" => 50, "settings" => [ "id" => "do_contribution_stats", "label" => "Contribution Stats", "label_display" => "0", "provider" => "do_profile_stats", ], "visibility" => [ "request_path" => [ "id" => "request_path", "pages" => "/user/*", "negate" => FALSE, ], ], ])->save(); echo "Contribution Stats block placed\n";}
// Profile Completeness block (visible only to the profile owner).if (!$block_storage->load("do_profile_completeness")) { $block_storage->create([ "id" => "do_profile_completeness", "plugin" => "do_profile_completeness", "region" => "content", "theme" => "bluecheese", "weight" => 10, "settings" => [ "id" => "do_profile_completeness", "label" => "Profile Completeness", "label_display" => "0", "provider" => "do_profile_stats", ], "visibility" => [ "request_path" => [ "id" => "request_path", "pages" => "/user/*", "negate" => FALSE, ], ], ])->save(); echo "Profile Completeness block placed\n";}ddev drush config:export -y
Step 610 — Enable do_group_pin Module
Section titled “Step 610 — Enable do_group_pin Module”Content pinning within groups. Allows group managers to pin topics above the chronological stream.
Source files (in repo at docs/groups/modules/do_group_pin/, deployed to web/modules/custom/do_group_pin/):
do_group_pin.info.yml—core_version_requirement: ^10 || ^11; depends ondrupal:flag,drupal:viewsdo_group_pin.services.yml— registersDoGroupPinHooksservicesrc/Hook/DoGroupPinHooks.php—hook_page_attachments,hook_preprocess_node,hook_views_query_altervia#[Hook]do_group_pin.module— empty (@filedocblock)do_group_pin.libraries.ymlcss/do_group_pin.css—.pin-badgeand.node--pinnedstylesconfig/optional/flag.flag.pin_in_group.yml— bundled flag (skipped if already in active config)
Caution View ID must match:
DoGroupPinHooks::viewsQueryAlter()checks$view->id() === 'group_content_stream'. Open Social usedgroup_topics; this implementation already targetsgroup_content_stream(from Phase 3 Step 310). ✅ Already correct.
Caution
config/optional/:pin_in_groupflag is imported globally before the module is enabled (fromdocs/groups/config/). Module usesconfig/optional/to avoidPreExistingConfigException. Same pattern as Phases 4 and 5.
610c — Import pin_in_group flag
Section titled “610c — Import pin_in_group flag”cp docs/groups/config/flag.flag.pin_in_group.yml config/sync/cp docs/groups/config/system.action.flag_action.pin_in_group_flag.yml config/sync/cp docs/groups/config/system.action.flag_action.pin_in_group_unflag.yml config/sync/ddev drush config:import -yddev drush config:export -y610e — Grant permissions
Section titled “610e — Grant permissions”# Remove role:perm add for roles that don't exist yet:# site_moderator and content_administrator are NOT yet created.# Permissions will be assigned after roles are created in Phase 7.# For now, only administrator can pin.ddev drush config:export -yImportant
Roles: Only anonymous and authenticated roles exist in this installation. content_administrator and site_moderator were referenced in the RUNBOOK but never created. Create them before assigning pin permissions:
ddev drush role:create content_administrator 'Content Administrator'ddev drush role:create site_moderator 'Site Moderator'ddev drush role:perm:add content_administrator "flag pin_in_group,unflag pin_in_group"ddev drush role:perm:add site_moderator "flag pin_in_group,unflag pin_in_group"ddev drush config:export -yEnable
Section titled “Enable”# Step 1: Copy module from docs to webcp -r docs/groups/modules/do_group_pin web/modules/custom/# Step 2: Enable moduleddev drush en do_group_pin -y# Step 3: Export configddev drush config:export -yStep 620 — Enable do_group_mission Module
Section titled “Step 620 — Enable do_group_mission Module”Group mission statement sidebar block. Displays a summary of the group description on all group pages.
Source files (in repo at docs/groups/modules/do_group_mission/, deployed to web/modules/custom/do_group_mission/):
do_group_mission.info.yml—core_version_requirement: ^10 || ^11; depends ondrupal:groupsrc/Plugin/Block/GroupMissionBlock.php— context-aware block, readsfield_group_description(129 lines, no changes from OS version)
Note Prerequisite:
field_group_descriptionmust exist oncommunity_group(created in Phase 1 Step 130).
ddev drush php:eval '$field = \Drupal\field\Entity\FieldConfig::loadByName("group", "community_group", "field_group_description");echo $field ? "field_group_description exists\n" : "field_group_description MISSING\n";'Enable
Section titled “Enable”cp -r docs/groups/modules/do_group_mission web/modules/custom/ddev drush en do_group_mission -yddev drush config:export -y620c — Place block in sidebar
Section titled “620c — Place block in sidebar”ddev drush php:script docs/groups/scripts/step_620c.phpddev drush config:export -yStep 630 — Enable do_group_language Module (optional)
Section titled “Step 630 — Enable do_group_language Module (optional)”Deferred: Group-level language switching — confirm whether drupal.org groups need this feature before enabling.
Source files (already copied to web/modules/custom/do_group_language/ but not yet enabled):
do_group_language.info.ymldo_group_language.services.yml— registersLanguageNegotiationGrouppluginsrc/Plugin/LanguageNegotiation/LanguageNegotiationGroup.php— raw path parsing for group ID (67 lines, no changes needed)
Important Why raw path parsing? Language negotiation runs before Drupal’s route matching.
\Drupal::routeMatch()->getParameter('group')is always NULL at negotiation time. This is by design.
630d — Create field_group_language (if enabling)
Section titled “630d — Create field_group_language (if enabling)”ddev drush php:script docs/groups/scripts/step_630d.phpddev drush config:export -y630e — Enable (when confirmed needed)
Section titled “630e — Enable (when confirmed needed)”cp -r docs/groups/modules/do_group_language web/modules/custom/ddev drush en do_group_language -yddev drush config:export -yAfter enabling, configure language negotiation at /admin/config/regional/language/detection — set “Group language” weight to 5.
Step 640 — Multilingual Infrastructure
Section titled “Step 640 — Multilingual Infrastructure”Important Multilingual support is required for groups (group-level language switching, translated content, multilingual demo data). The
do_group_languagemodule must be enabled.
640a — Enable core translation modules
Section titled “640a — Enable core translation modules”Caution
interface_translationdoes not exist as a standalone Drupal 11 module. The correct module list is:language,locale,config_translation,content_translation. Thelocalemodule provides interface translation.
ddev drush en language locale config_translation content_translation -yddev drush config:export -y640b — Add languages
Section titled “640b — Add languages”Add languages matching g.d.o’s multilingual needs:
Script: step_640.php — adds 14 languages, configures negotiation chain, enables content translation for all 5 types
ddev drush php:script docs/groups/scripts/step_640.phpExpected: 15 languages total (14 + English)
640c — Download translations
Section titled “640c — Download translations”ddev drush locale:checkddev drush locale:updateNote Downloads translation strings for all enabled languages (~139k strings, 1-2 minutes).
640e — Grant translation permissions
Section titled “640e — Grant translation permissions”Important
Roles do not yet exist. At this point only anonymous and authenticated roles are in the site. content_administrator and site_moderator must be created first:
ddev drush role:create content_administrator 'Content Administrator'ddev drush role:create site_moderator 'Site Moderator'ddev drush role:perm:add site_moderator "translate any entity,create content translations,update content translations,delete content translations"ddev drush role:perm:add content_administrator "translate any entity,create content translations,update content translations,delete content translations"ddev drush config:export -yStep 650 — Phase 6 Tests
Section titled “Step 650 — Phase 6 Tests”Warning Review TROUBLESHOOTING.md before running tests. Kill zombie processes first:
ddev exec kill-zombies.sh
PHPUnit — Module: do_profile_stats
Section titled “PHPUnit — Module: do_profile_stats”Create web/modules/custom/do_profile_stats/tests/src/Kernel/ProfileStatsTest.php:
testContributionStatsBlock()— block plugin exists; counts all 5 content types
ddev exec phpunit web/modules/custom/do_profile_stats/tests/src/Kernel/ProfileStatsTest.phpPHPUnit — Module: do_group_pin
Section titled “PHPUnit — Module: do_group_pin”Create web/modules/custom/do_group_pin/tests/src/Kernel/GroupPinTest.php:
testPinnedSortOrder()— pinned content sorts above unpinned
ddev exec phpunit web/modules/custom/do_group_pin/tests/src/Kernel/GroupPinTest.phpPHPUnit — Module: do_group_mission
Section titled “PHPUnit — Module: do_group_mission”Create web/modules/custom/do_group_mission/tests/src/Kernel/GroupMissionTest.php:
testMissionBlock()— block plugin exists; readsfield_group_descriptiontestTruncation()— description truncated at 300 chars with “Read more” link
ddev exec phpunit web/modules/custom/do_group_mission/tests/src/Kernel/GroupMissionTest.phpPHPUnit — Module: do_group_language
Section titled “PHPUnit — Module: do_group_language”Create web/modules/custom/do_group_language/tests/src/Kernel/GroupLanguageTest.php:
testLanguageNegotiationPlugin()—language-groupnegotiation method registeredtestGroupLanguageField()—field_group_languageexists oncommunity_group
ddev exec phpunit web/modules/custom/do_group_language/tests/src/Kernel/GroupLanguageTest.phpWarning Review TROUBLESHOOTING.md before running tests. Kill zombie processes first:
ddev exec kill-zombies.sh
PHPUnit — Integration (do_tests)
Section titled “PHPUnit — Integration (do_tests)”Create web/modules/custom/do_tests/tests/src/Kernel/Phase6Test.php:
testContentTranslation()— content translation enabled for all 5 group-postable typestestLanguagesConfigured()— at least 15 languages configured (14 + English)testBlockPlacements()— all blocks placed inbluecheesetheme regions
ddev exec phpunit web/modules/custom/do_tests/tests/src/Kernel/Phase6Test.phpPlaywright (E2E)
Section titled “Playwright (E2E)”Create tests/e2e/phase6.spec.ts:
test('contribution stats render')— navigate to/user/{uid}, verify all 8 stat itemstest('profile completeness percentage')— verify completeness block with missing fields listtest('pin content in group')— log in as group admin, pin a post, verify badge and top placementtest('unpin content')— unpin, verify normal sort ordertest('group mission sidebar')— verify mission block with truncated text and “Read more”test('group language switching')— set group language to French, verify interface changestest('site_moderator and group admin can pin')— verify both roles see Pin action
npx playwright test tests/e2e/phase6.spec.tsPhase 6 — Verification
Section titled “Phase 6 — Verification”-
do_profile_statsenabled:drush pm:list --filter=do_profile_stats - Contribution Stats block visible on
/user/{uid}profile pages - Stats show correct counts (forum topics, events, comments, groups, days)
- Profile Completeness block visible in sidebar on own profile
- Completeness percentage updates when fields are filled
- Missing fields list shown in collapsible details element
-
do_group_pinenabled:drush pm:list --filter=do_group_pin -
pin_in_groupflag exists at/admin/structure/flags - Pin/Unpin action visible to site_moderator and content_administrator
- Pinned content shows “Pinned” badge
- Pinned content appears above chronological content in group stream
-
.node--pinnedCSS class applied (orange left border) -
do_group_missionenabled:drush pm:list --filter=do_group_mission - Mission block visible in sidebar on
/group/{gid}pages - Mission text truncated at 300 chars with “Read more” link
- “Read more” links to
/group/{gid}/about - Block not visible on non-group pages
-
do_group_languageenabled:drush pm:list --filter=do_group_language - If enabled: language negotiation order configured at
/admin/config/regional/language/detection - All blocks placed in
bluecheesetheme regions (content,sidebar_first) - Multilingual (Step 640):
- 5 core translation modules enabled (language, locale, interface_translation, config_translation, content_translation)
- 15 languages configured (14 + English)
- Translation strings downloaded (
drush locale:update) - Language negotiation order: user → group → URL → selected
- Translation permissions granted to
site_moderatorandcontent_administrator - Content translation enabled for forum, documentation, event, post, page
-
field_group_languageexists oncommunity_groupwith form display widget - Config exported clean:
ddev drush config:status -
ddev drush cr— no PHP errors - Existing Nightwatch tests still pass:
composer nightwatch
Schema Changes (Phase 6)
Section titled “Schema Changes (Phase 6)”| Type | Name | Notes |
|---|---|---|
| Block | do_contribution_stats | Profile page — topics, events, comments, groups, days |
| Block | do_profile_completeness | Profile page — field fill percentage |
| Block | do_group_mission | Group sidebar — description block |
| Flag | pin_in_group | Global, entity:node |
| Field (group) | field_group_language | Language type field |
| Config change | language.types | Negotiation order: user → group → URL → selected |
| Languages | 14 added | de, es, fr, it, ja, ko, nl, pl, pt-br, ru, tr, uk, zh-hans, ar |
| Module | do_profile_stats | Stats + completeness blocks |
| Module | do_group_pin | Pin flag + Views query alter + badge |
| Module | do_group_mission | Mission sidebar block |
| Module | do_group_language | Language negotiation plugin |
| Module | language, locale, config_translation, content_translation | Core multilingual stack (interface_translation does not exist in D11) |
Custom module files (Phase 6)
Section titled “Custom module files (Phase 6)”docs/groups/modules/do_profile_stats/ ← source of truth├── css/do_profile_stats.css├── src/│ ├── Hook/│ │ └── DoProfileStatsHooks.php ← #[Hook] theme, page_attachments│ └── Plugin/Block/│ ├── ContributionStatsBlock.php ← topic → forum adaptation│ └── ProfileCompletenessBlock.php ← profile → user entity├── templates/│ ├── pl-contribution-stats.html.twig│ └── pl-profile-completeness.html.twig├── do_profile_stats.info.yml├── do_profile_stats.libraries.yml├── do_profile_stats.module ← empty (@file docblock)└── do_profile_stats.services.yml ← registers DoProfileStatsHooks
docs/groups/modules/do_group_pin/ ← source of truth├── config/optional/│ └── flag.flag.pin_in_group.yml ← global node flag (skipped if already active)├── css/do_group_pin.css├── src/Hook/│ └── DoGroupPinHooks.php ← #[Hook] page_attachments, preprocess_node, views_query_alter├── do_group_pin.info.yml├── do_group_pin.libraries.yml├── do_group_pin.module ← empty (@file docblock)└── do_group_pin.services.yml ← registers DoGroupPinHooks
docs/groups/modules/do_group_mission/ ← source of truth├── src/Plugin/Block/│ └── GroupMissionBlock.php ← no changes from OS version└── do_group_mission.info.yml
docs/groups/modules/do_group_language/ ← source of truth (not yet enabled)├── src/Plugin/LanguageNegotiation/│ └── LanguageNegotiationGroup.php ← raw path parsing (no changes)├── do_group_language.info.yml└── do_group_language.services.yml
web/modules/custom/{do_profile_stats,do_group_pin,do_group_mission,do_group_language}/ ← deployed copies (synced via cp -r)Key Adaptations from Open Social (Phase 6)
Section titled “Key Adaptations from Open Social (Phase 6)”| Aspect | Open Social (pl-opensocial) | Standard Drupal (pl-drupalorg) |
|---|---|---|
| Profile entity | profile module entity | Standard user entity |
| Profile fields | field_profile_first_name, etc. | field_first_name, field_bio, etc. |
| Content type for stats | topic | forum |
| Pin View ID | group_topics | group_content_stream |
| Mission block region | complementary_bottom (socialblue) | sidebar_first (bluecheese) |
| Block theme | socialblue | bluecheese |
| Flag dependency notation | flag:flag | drupal:flag |
Open Questions (Phase 6)
Section titled “Open Questions (Phase 6)”Profile completeness fields: ⏳ Still deferred — research whichuserfields this site actually uses and update$fields_to_checkinProfileCompletenessBlock.php.Contribution stats expansion: ✅ Confirmed: count all 5 group-postable content types (forum, documentation, event, post, page) plus comments, groups, and days active.Group mission field: ✅ Resolved:field_group_descriptiondefined in Step 130 as a required field oncommunity_group.Group language: ⏳ Deferred —do_group_languagecopied to web but NOT enabled pending decision on whether d.o. groups need per-group language switching.Pin permissions: ✅ Note: onlyanonymousandauthenticatedroles currently exist.content_administratorandsite_moderatormust be created before assigning pin permissions. Role creation added to Step 640e.: ✅ Resolved — this module does not exist in Drupal 11. Correct list:interface_translationmodulelanguage locale config_translation content_translation.
Phase 7 — Demo Data
Section titled “Phase 7 — Demo Data”Goal: Populate the site with realistic test data — users, groups, content, comments, flags — to validate all features from Phases 1–6.
Source: DEMO_DATA_PLAN.md (removed — was Open Social version, 1,334 lines)
Warning The existing
DEMO_DATA_PLAN.md(removed) was written for Open Social’s data model. It must be adapted for pl-drupalorg’s entity types, field names, content type names, and relationship patterns. This section documents the required adaptations phase by phase.
Pre-Phase 7 Snapshot
Section titled “Pre-Phase 7 Snapshot”ddev export-db --file=backups/pre-phase7-$(date +%Y%m%d-%H%M).sql.gzPre-Phase 7 Prerequisites
Section titled “Pre-Phase 7 Prerequisites”Caution
language.content_settings malformed config: step_640.php stores content language settings without target_entity_type_id, which crashes Drupal during bootstrap. Fix BEFORE creating roles or running demo data:
for bundle in forum documentation event post page; docat > config/sync/language.content_settings.node.${bundle}.yml << YAMLlangcode: enstatus: truedependencies: config: - node.type.${bundle} module: - content_translationid: node.${bundle}target_entity_type_id: nodetarget_bundle: ${bundle}default_langcode: site_defaultlanguage_alterable: falsethird_party_settings: content_translation: enabled: trueYAMLdoneddev mysql -e "DELETE FROM config WHERE name LIKE 'language.content_settings%'"ddev mysql -e "TRUNCATE TABLE cache_bootstrap; TRUNCATE TABLE cache_config; TRUNCATE TABLE cache_container; TRUNCATE TABLE cache_data; TRUNCATE TABLE cache_default; TRUNCATE TABLE cache_discovery; TRUNCATE TABLE cache_entity; TRUNCATE TABLE cachetags;"ddev drush config:import -yCaution
language-group plugin not found: language.types negotiation config references language-group (set by step_640.php) but do_group_language must be enabled first. Enable it BEFORE creating roles:
ddev drush en do_group_language -yCaution
Roles don’t exist yet: content_administrator and site_moderator must be created BEFORE running step_700_demo_data.php. Create them and export to config/sync first — otherwise config:import after demo data will delete them:
ddev drush role:create content_administrator "Content Administrator"ddev drush role:create site_moderator "Site Moderator"ddev drush role:perm:add content_administrator "flag pin_in_group,unflag pin_in_group,flag promote_homepage,unflag promote_homepage,translate any entity,create content translations,update content translations,delete content translations"ddev drush role:perm:add site_moderator "flag pin_in_group,unflag pin_in_group,flag promote_homepage,unflag promote_homepage,translate any entity,create content translations,update content translations,delete content translations"ddev drush config:export -ySteps 700–750 — Create Demo Data
Section titled “Steps 700–750 — Create Demo Data”Script: docs/groups/scripts/step_700_demo_data.php — all-in-one idempotent demo data
ddev drush php:script docs/groups/scripts/step_700_demo_data.phpNote Known miss:
ERROR: No comment field on forum nodesis expected. Forum content type does not have a comment field configured yet. Comments work on other content types.
Creates (all idempotent):
- 6 users: maria_chen, james_okafor, elena_garcia, ravi_patel, sophie_mueller, alex_novak
- 20 taxonomy tags: sprint, drupalcon, logistics, core, roadmap, etc.
- 8 groups: DrupalCon Portland 2026, Drupal France, Core Committers, Thunder Distribution, Leadership Council, Camp Organizers EMEA, Drupal Deutschland, Legacy Infrastructure
- ~17 nodes: forum topics + events across groups
- Flags: pin (Sprint Planning), promote (2 topics), follow content/term/user, 9 RSVPs
- Archive: Legacy Infrastructure group set to unpublished
Step 710 — Multilingual Demo Data
Section titled “Step 710 — Multilingual Demo Data”Important
do_group_languagemust be enabled before Step 710 or the language negotiation plugin (language-group) registered by step_640.php will be missing, causing a Drupal bootstrap crash.
ddev drush php:script docs/groups/scripts/step_760.php710e — Download locale translation strings
Section titled “710e — Download locale translation strings”ddev drush locale:checkddev drush locale:updateddev drush config:export -yNote This downloads translation strings for all 17 enabled languages (~139k strings, 1-2 minutes).
710f — Verify multilingual demo data
Section titled “710f — Verify multilingual demo data”# Verify group languages are set:ddev drush php:eval 'foreach (["Drupal France" => "fr", "Drupal Deutschland" => "de"] as $label => $expected) { $groups = \Drupal::entityTypeManager()->getStorage("group")->loadByProperties(["label" => $label]); $group = reset($groups); $actual = $group->get("field_group_language")->value; echo "$label language: $actual " . ($actual === $expected ? "OK" : "MISMATCH (expected $expected)") . "\n";}'
# Verify French and German content exists:ddev drush php:eval 'foreach (["fr", "de"] as $lang) { $count = \Drupal::entityQuery("node")->condition("langcode", $lang)->accessCheck(FALSE)->count()->execute(); echo "$lang-language nodes: $count\n";}'
# Verify translations downloaded:ddev drush php:eval '$count = \Drupal::database()->select("locales_target", "lt")->countQuery()->execute()->fetchField();echo "Translation strings: $count\n";'Expected results:
- Drupal France language:
fr - Drupal Deutschland language:
de - fr-language nodes: ≥3
- de-language nodes: ≥2
- Translation strings: >0 (typically thousands)
Step 720 — Search Setup (Solr or Core)
Section titled “Step 720 — Search Setup (Solr or Core)”Important pl-drupalorg may already have a search backend configured. Check current status before setting up Solr.
720a — Check existing search configuration
Section titled “720a — Check existing search configuration”Script: step_770.php — checks existing config and creates Solr server
ddev drush php:script docs/groups/scripts/step_770.phpGenerate and upload Drupal configset to Solr:
ddev drush search-api-solr:get-server-config solr_server /tmp/solr-config.zipddev exec bash -c 'mkdir -p /tmp/solr-configset && cd /tmp/solr-configset && unzip -o /tmp/solr-config.zip'# Copy to Solr container and uploaddocker cp $(ddev describe -j | python3 -c "import sys,json; print(json.load(sys.stdin)['raw']['name'])")-web:/tmp/solr-configset /tmp/solr-configsetddev solr zk upconfig -n drupal -d /tmp/solr-configsetddev solr create -c drupal -n drupal720c — Option B: Core Search (minimal setup)
Section titled “720c — Option B: Core Search (minimal setup)”If Solr is not needed, enable core search:
ddev drush en search -yddev drush search:index720d — Index all content
Section titled “720d — Index all content”ddev drush cronddev drush search-api:index # if using search_apiddev drush crVerify search works:
ddev drush search-api:status # should show 0 items remainingStep 730 — Final Snapshot
Section titled “Step 730 — Final Snapshot”Snapshot complete demo database:
ddev export-db --file=backups/demo-complete-$(date +%Y%m%d-%H%M).sql.gzStep 740 — Phase 7 Tests
Section titled “Step 740 — Phase 7 Tests”PHPUnit — Integration (do_tests)
Section titled “PHPUnit — Integration (do_tests)”Create web/modules/custom/do_tests/tests/src/Kernel/Phase7Test.php:
testDemoUsersExist()— 6 demo users created with expected rolestestDemoGroupsExist()— 8 demo groups created with correct group types and membershiptestDemoContentExists()— expected node counts per content type (including French and German)testDemoFlagsSet()— follow_content, follow_user, pin_in_group flags on expected entitiestestSearchIndexPopulated()— search index has entries for demo content
Note Phase 7 is demo data — all tests are integration tests in
do_tests. No custom module changes.
ddev exec phpunit web/modules/custom/do_tests/tests/src/Kernel/Phase7Test.phpPlaywright (E2E)
Section titled “Playwright (E2E)”Create tests/e2e/phase7.spec.ts:
test('demo user can log in')— log in as each demo user, verify profile loadstest('demo groups listed')— navigate to/groups, verify all 7 groupstest('demo content in group streams')— navigate to demo group, verify contenttest('pinned content at top')— verify pinned demo content above other contenttest('promoted content visible')— navigate to/promoted, verify demo contenttest('search returns results')— search for demo content titletest('RSVP flag on demo events')— verify demo events have RSVP counts
npx playwright test tests/e2e/phase7.spec.tsPhase 7 — Verification
Section titled “Phase 7 — Verification”- 6 demo users exist with correct roles:
ddev drush user:list --format=table - sophie_mueller preferred language =
de, elena_garcia =es - ravi_patel profile is intentionally incomplete (no bio, no country)
- alex_novak has no profile photo
- 8 groups created as
community_grouptype - Membership matrix matches plan (Portland=6+, Council=2, etc.)
- 12+ forum topics created across groups (including 3 French)
- 5 events created with correct date fields
- Thunder 7.0 Roadmap cross-posted to 2 groups
- Weekly Standup cross-posted to 2 groups
- 8 comments on correct topics (Venue=3, RFC=4, Roadmap=1)
-
pin_in_groupflagging: 1 (Sprint Planning) -
promote_homepageflaggings: 2 -
follow_contentflaggings: ≥2 -
follow_termflagging: 1 (elena → core) -
follow_userflagging: 1 (ravi → maria) -
rsvp_eventflaggings: ≥7 - Legacy Infrastructure archived (status=0)
- Multilingual (Step 710):
- Drupal France
field_group_language=fr - Drupal Deutschland
field_group_language=de -
field_group_languagewidget visible on group add/edit form - ≥3 French-language nodes in Drupal France group
- ≥2 German-language nodes in Drupal Deutschland group
- Locale translation strings downloaded (
drush locale:update) - Visiting Drupal France group page switches interface to French
- Visiting Drupal Deutschland group page switches interface to German
- Drupal France
- Database snapshot saved
- Search configured (Solr or core search)
- If Solr:
ddev drush search-api:statusshows 0 items remaining -
ddev drush cr— no PHP errors
Schema Changes (Phase 7)
Section titled “Schema Changes (Phase 7)”| Type | Name | Notes |
|---|---|---|
| Data | 6 users | admin + 5 demo users with roles |
| Data | 8 groups | Community groups (1 archived, 2 multilingual) |
| Data | ~17 nodes | 12 English + 3 French + 2 German forum topics + 5 events |
| Data | ~8 comments | Threaded discussions |
| Data | ~20 flaggings | follow_content, follow_term, follow_user, rsvp_event, pin_in_group, promote_homepage |
| Data | ~25 group_relationships | Users + content assigned to groups |
| Data | ~22 taxonomy terms | Tags on content |
| Data | Translation strings | Downloaded via drush locale:update for 17 languages |
| Config | field_group_language form widget | Language select on group add/edit form |
| Config | Search API server + index | If Solr chosen (Step 720) |
| Snapshot | demo-empty-*.sql.gz | After clean slate |
| Snapshot | demo-complete-*.sql.gz | After full population |
Key Adaptations from Open Social (Phase 7)
Section titled “Key Adaptations from Open Social (Phase 7)”| Aspect | Open Social DEMO_DATA_PLAN | pl-drupalorg adaptation |
|---|---|---|
| Profile storage | profile entity + field_profile_* | user entity + field_* |
| Roles | contentmanager, sitemanager | content_administrator, site_moderator |
| Group type | flexible_group | community_group |
| Group role | flexible_group-group_manager | community_group-admin |
| Content type | topic | forum |
| Tag vocabulary | social_tagging | tags |
| Tag field | social_tagging | field_group_tags |
| Content visibility | field_content_visibility | Not available (remove) |
| Event dates | field_event_date + field_event_date_end | field_date_of_event |
| Event enrollment | EventEnrollment entity | rsvp_event flag |
| Group relationship | group_content + group_node:topic | group_relationship + group_node:forum |
| Follow user target | profile entity | user entity |
Open Questions (Phase 7)
Section titled “Open Questions (Phase 7)”Profile fields: ⏳ Still open — does pl-drupalorg havefield_organizationor equivalent? Check viaddev drush php:eval '\Drupal\field\Entity\FieldStorageConfig::loadByName("user", "field_organization") ? print "yes" : print "no";'Tag vocabulary: ✅ Confirmed — machine name istags(sitewide) andgroup_tags(group-specific). Demo data usesgroup_tags.Comment field name: ✅ Confirmed — no comment field on forum nodes.ERROR: No comment field on forum nodesin demo script is expected. Comment field not yet configured for forum content type.Event date field type: ✅ Resolved —field_date_of_event(datetime).Group visibility: ⏳ Deferred — group access control via Group module policies, not custom field.: ✅ Fixed — step_640.php stored onlylanguage.content_settingsmalformedthird_party_settings; corrected YAMLs add requiredtarget_entity_type_id,target_bundle,default_langcode,language_alterablekeys.Roles bootstrap issue: ✅ Fixed — create roles AFTER clearing all caches + fixing content settings, BEFORE running config:import.
Step 750 — Install Asset Injector
Section titled “Step 750 — Install Asset Injector”Install the asset_injector module to allow adding custom CSS and JS snippets via the admin UI. This is useful for quick theming adjustments (e.g. hiding the drupal.org mega-menu on a development instance) without modifying theme files.
ddev composer require drupal/asset_injectorddev drush en asset_injector -yddev drush crNavigate to /admin/config/development/asset-injector to verify the module is available.
Tip Use CSS injector snippets at
/admin/config/development/asset-injector/cssto hide or restyle elements without touching the bluecheese theme.
Phase 8 — Feature Tour
Section titled “Phase 8 — Feature Tour”Goal: Create a visual feature tour document with screenshots from the bluecheese theme, modelled on the Open Social Feature Tour (reference only — not in this repo).
Source: feature_tour/FEATURE_TOUR.md (reference only — not in this repo) (245 lines, 7 screenshots), bluecheese theme
Note The existing feature tour at
feature_tour/FEATURE_TOUR.mddocuments the Open Social (socialblue theme) platform. The new tour will document the pl-drupalorg (bluecheese theme) platform with updated screenshots and adapted descriptions.
Pre-Phase 8 Snapshot
Section titled “Pre-Phase 8 Snapshot”ddev export-db --file=backups/pre-phase8-$(date +%Y%m%d-%H%M).sql.gzStep 790 — Create All Topics and All Events Views
Section titled “Step 790 — Create All Topics and All Events Views”Important These views are prerequisites for Phase 8 screenshots. They were intentionally deferred from earlier phases. Create them now via config import before capturing screenshots.
The YAML files are already in config/sync/:
views.view.all_topics.yml— Forum topic listing at/all-topicsviews.view.all_events.yml— Event listing at/all-events
ddev drush cim -yddev drush crVerify:
/all-topicsloads and shows forum nodes/all-eventsloads and shows event nodes sorted by date
Step 800 — Capture Screenshots
Section titled “Step 800 — Capture Screenshots”Take screenshots of the bluecheese-themed site using the demo data from Phase 7.
Screenshots to capture
Section titled “Screenshots to capture”| # | Page | URL | Key elements to show |
|---|---|---|---|
| 1 | Homepage | / | Activity stream, sidebar widgets, bluecheese header |
| 2 | Logged-in homepage | / (authenticated) | Admin toolbar, quick-post composer, follow buttons |
| 3 | Group directory | /all-groups | Group cards, search, type filter |
| 4 | Group page | /group/{portland_gid} | Member count, tabs, mission sidebar, pinned content |
| 5 | Forum topics listing | /all-topics or equivalent | Topic cards, filters, multilingual content |
| 6 | Topic detail | /node/{nid} | Pinned badge, tags, comments, follow button |
| 7 | Events listing | /all-events | Event cards with dates, event type facets |
Note
/all-topicsand/all-eventsare created in Step 790 (Pre-Phase 8). Ensure Step 790 has been run before capturing these screenshots. | 8 | User profile |/user/{maria_uid}| Contribution stats, completeness, follow button | | 9 | Notification settings |/user/{uid}/notification-settings| Subscriptions table, disable toggle | | 10 | Archived group |/group/{legacy_gid}| Archive badge, read-only indicator | | 11 | Multilingual group (French) |/group/{france_gid}| French interface, French forum topics,field_group_languagebadge | | 12 | Multilingual group (German) |/group/{deutschland_gid}| German interface, German forum topics, language switching |
# Get IDs for screenshot URLs:ddev drush php:eval '$groups = \Drupal::entityTypeManager()->getStorage("group")->loadMultiple();foreach ($groups as $g) echo "gid=" . $g->id() . " " . $g->label() . "\n";$maria = user_load_by_name("maria_chen");echo "maria uid=" . $maria->id() . "\n";'Tip Use the browser tool to capture screenshots at 1280×800 resolution. Log in as admin for authenticated views.
Step 810 — Write Feature Tour Document
Section titled “Step 810 — Write Feature Tour Document”Create docs/groups/feature_tour_drupalorg/FEATURE_TOUR.md modelled on the Open Social version but adapted for pl-drupalorg:
Sections to include
Section titled “Sections to include”1. Homepage & Activity Stream
- Describe the bluecheese theme layout
- Note differences from socialblue (header, sidebar regions, styling)
- Annotate: activity stream, sidebar widgets, navigation
2. Forum Topics (was “Topics” in OS tour)
- Note content type rename:
topic→forum - Content creation, tagging, sorting
- Multilingual content examples
3. Events
- Event date fields (
field_date_of_event) - RSVP via Flag (replaces
event_enrollment) - Cross-group events
4. Groups
- Group type:
community_group(notflexible_group) - Membership management
- Mission sidebar (do_group_mission)
- Content pinning (do_group_pin)
- Archive enforcement (do_group_extras)
5. Multi-Group Posting
- Same as OS tour —
do_multigroupmodule - Show cross-posted content in Thunder + Portland groups
6. User Profiles
- Profile data on user entity (not profile entity)
- Contribution stats block (do_profile_stats)
- Profile completeness indicator
- Follow user flag
7. Discovery & Content Surfacing
- Hot content scoring (do_discovery)
- Promoted content (promote_homepage flag)
- iCal/RSS feeds
8. Notifications & Subscriptions
- Follow flags: content, user, term
- Mute group notifications
- Notification settings page
- Per-post opt-out
9. Technical Stack
| Component | Version |
|---|---|
| Drupal core | 10.x |
| Group module | 3.x |
| PHP | 8.3 |
| Theme | bluecheese |
| Deployment | DDEV |
Custom modules summary table
Section titled “Custom modules summary table”| Module | Purpose | Phase |
|---|---|---|
do_group_extras | Archive, guidelines, moderation | 2 |
do_multigroup | Multi-group posting | 3 |
do_discovery | Hot scores, promoted content, iCal/RSS | 4 |
do_notifications | Subscriptions, event recording | 5 |
do_profile_stats | Contribution stats + completeness | 6 |
do_group_pin | Pin content in group streams | 6 |
do_group_mission | Group mission sidebar | 6 |
do_group_language | Group-level language negotiation | 6 |
Step 820 — Generate Annotated Screenshots
Section titled “Step 820 — Generate Annotated Screenshots”For each screenshot, annotate key features using numbered callouts:
mkdir -p docs/groups/feature_tour_drupalorgUse the generate_image tool to create annotated versions of each screenshot with numbered callouts and a legend below.
Step 830 — Review and Publish
Section titled “Step 830 — Review and Publish”# Verify all images referenced in the markdown exist:grep -oP '!\[.*?\]\((.*?)\)' docs/groups/feature_tour_drupalorg/FEATURE_TOUR.md | while read -r line; do file=$(echo "$line" | grep -oP '\(.*?\)' | tr -d '()') if [ ! -f "docs/groups/feature_tour_drupalorg/$file" ]; then echo "MISSING: $file" fidoneSchema Changes (Phase 8)
Section titled “Schema Changes (Phase 8)”| Type | Name | Notes |
|---|---|---|
| Directory | docs/groups/feature_tour_drupalorg/ | Screenshots + markdown |
| Document | FEATURE_TOUR.md | 9-section visual walkthrough |
| Files | ≥8 PNG screenshots | Bluecheese theme UI captures |
Note Phase 8 makes no database or config changes. All output is documentation files committed to git.
Phase 8 — Verification
Section titled “Phase 8 — Verification”-
feature_tour_drupalorg/directory created -
FEATURE_TOUR.mddocument written with all 9 sections - ≥8 screenshots captured from bluecheese theme
- Screenshots annotated with numbered callouts
- All image files referenced in markdown exist
- Document accurately describes pl-drupalorg features (not Open Social)
- Custom modules table matches actual installed modules
- Content type names correct:
forum(nottopic),community_group(notflexible_group) - No references to Open Social-specific features (activity_send_email, socialblue, etc.)
- Committed to Git
Key Adaptations from Open Social (Reference)
Section titled “Key Adaptations from Open Social (Reference)”| Aspect | Open Social (pl-opensocial) | Standard Drupal (pl-drupalorg) |
|---|---|---|
| Group bundle | flexible_group | community_group |
| Entity type | group_content | group_relationship (Group 3.x) |
| Relationship pattern | flexible_group-group_node-{bundle} | community_group-group_node-{bundle} |
| Content types | topic, event, page | forum, documentation, event, post, page |
| Admin role | sitemanager | site_moderator |
| Module dependency | social_group:social_group | drupal:group |
| Theme | socialblue | bluecheese |
| Theme regions | complementary_bottom/top | sidebar_first, sidebar_second, content |
| Tags | social_tagging (OS-specific) | group_tags vocabulary + field_group_tags |
| Notifications | activity_send_email pipeline | Queue-only — Drupal enqueues; external system delivers |
Custom Modules Summary
Section titled “Custom Modules Summary”| Module | Phase | Difficulty | Status |
|---|---|---|---|
do_group_extras | 2 | 🟡 Medium | Archive, guidelines, moderation |
do_multigroup | 3 | 🔴 High | Multi-group posting |
do_discovery | 4 | 🟢 Low | Hot content scoring |
do_notifications | 5 | 🟢 Low | Subscriptions, event recording |
do_profile_stats | 6 | 🟡 Medium | Contribution stats |
do_group_pin | 6 | 🟡 Medium | Content pinning |
do_group_mission | 6 | 🟢 Low | Mission sidebar |
do_group_language | 6 | 🟡 Medium | Group language switching |
do_wiki | — | 🟢 Low | [[Title]] wiki links (evaluate need) |
Cumulative Schema Inventory
Section titled “Cumulative Schema Inventory”Everything added across all 8 phases:
Entity Types & Bundles
Section titled “Entity Types & Bundles”| Entity type | Bundle | Phase | Notes |
|---|---|---|---|
group | community_group | 1 | Via drupal/group ^3.0 |
group_role | community_group-{admin,member,outsider} | 1 | 3 roles |
group_relationship_type | community_group-group_node-{forum,doc,event,post,page} | 1 | 5 content types |
group_relationship_type | community_group-group_membership | 1 | User membership |
Fields
Section titled “Fields”| Entity | Field | Type | Phase |
|---|---|---|---|
group | field_group_description | Text (formatted, long) | 1 |
group | field_group_visibility | List (string) | 1 |
group | field_group_image | Image | 1 |
group | field_group_type | Entity reference → taxonomy | 2 |
group | field_group_language | Language | 6 |
node (5 types) | field_group_tags | Entity reference → group_tags taxonomy | 3 |
user | field_notification_frequency | List (immediate/daily/weekly) | 5 |
Taxonomies
Section titled “Taxonomies”| Vocabulary | Terms | Phase |
|---|---|---|
event_types | 7 (DrupalCon, Sprint, Camp, etc.) | 1 |
group_type | 5 (Geographical, Working group, etc.) | 2 |
tags | User-created | 3 |
| View | Path | Phase |
|---|---|---|
groups | /groups | 1 |
all_groups | /all-groups | 2 |
pending_groups | /admin/groups/pending | 2 |
group_content_stream | (group context) | 3 |
group_events | (group context) | 3 |
group_members | (group context) | 3 |
tags_aggregation | /tags | 3 |
hot_content | /hot | 4 |
promoted_content | /admin/content/promoted | 4 |
group_rss_feed | /group/{gid}/rss.xml | 4 |
| Flag ID | Scope | Entity | Phase |
|---|---|---|---|
promote_homepage | Global | node | 4 |
rsvp_event | Per-user | node | 4 |
follow_content | Per-user | node | 5 |
follow_user | Per-user | user | 5 |
follow_term | Per-user | taxonomy_term | 5 |
mute_group_notifications | Per-user | group | 5 |
pin_in_group | Global | node | 6 |
Custom DB Tables
Section titled “Custom DB Tables”| Table | Columns | Phase |
|---|---|---|
do_discovery_hot_score | nid INT PK, score FLOAT, computed INT | 4 |
node_counter | nid, totalcount, daycount, timestamp (via statistics) | 4 |
Custom Routes
Section titled “Custom Routes”| Path | Controller | Phase |
|---|---|---|
/user/{user}/events/ical | IcalController::userEvents | 4 |
/group/{group}/events/ical | IcalController::groupEvents | 4 |
/upcoming-events/ical | IcalController::siteEvents | 4 |
/user/{uid}/notification-settings | NotificationSettingsController::page | 5 |
/user/{uid}/notification-settings/cancel | CancelAllSubscriptionsForm | 5 |
Block Placements
Section titled “Block Placements”| Block ID | Region | Theme | Phase |
|---|---|---|---|
do_contribution_stats | content | bluecheese | 6 |
do_profile_completeness | content | bluecheese | 6 |
do_group_mission | sidebar_first | bluecheese | 6 |
Languages (Phase 6)
Section titled “Languages (Phase 6)”15 total: en (default) + de, es, fr, it, ja, ko, nl, pl, pt-br, ru, tr, uk, zh-hans, ar
Contributed Modules
Section titled “Contributed Modules”| Package | Module(s) enabled | Phase |
|---|---|---|
drupal/group | group, gnode | 1 |
drupal/linkit | linkit | 1 (optional) |
drupal/flag | flag, flag_count | 4 |
drupal/statistics | statistics | 4 |
drupal/search_api | search_api | 7 (optional) |
drupal/search_api_solr | search_api_solr | 7 (optional) |
drupal/message | message | 5 (optional) |
drupal/message_notify | message_notify | 5 (optional) |
drupal/asset_injector | asset_injector | 7 |
| (core) | language, locale, interface_translation, config_translation, content_translation | 6 |
Custom Modules
Section titled “Custom Modules”| Module | Phase | Source |
|---|---|---|
do_group_extras | 2 | Ported from Open Social |
do_wiki | 2 | Ported (optional) |
do_multigroup | 3 | Ported from Open Social |
do_discovery | 4 | Ported from Open Social |
do_notifications | 5 | Ported from Open Social |
do_profile_stats | 6 | Ported (major rewrite: profile→user) |
do_group_pin | 6 | Ported from Open Social |
do_group_mission | 6 | Ported from Open Social |
do_group_language | 6 | Ported from Open Social |
