From b74c009253e81a87982dfdf44396536294bcfa84 Mon Sep 17 00:00:00 2001 From: hissy Date: Tue, 28 Jul 2026 02:03:48 +0900 Subject: [PATCH] feat: Import groups by name --- CHANGELOG.md | 4 + README.md | 98 +++++++++++++++++-- controller.php | 2 +- src/Traits/CsvTrait.php | 2 + src/User/Command/ImportUserCommandHandler.php | 18 ++++ 5 files changed, 117 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 719a016..334ab3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,8 @@ # Release Notes +## [0.9.3] - Jul 28, 2026 +- [Added] Import groups by name via **Assign to Group** and **Unassign from Group** CSV fields +- [Docs] Expanded README with usage guide and package design notes + ## [0.9.2] - Dec 6, 2024 - [Added] Ability to configure export batch size in `concrete.export.csv.batch_size` config diff --git a/README.md b/README.md index 7c62f0c..528da1a 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,102 @@ # Concrete CMS add-on: Macareux CSV User Import & Export -This package enables it to export users as CSV files and import users from CSV files. +Import users from CSV files and export users as CSV files, with selectable columns. -Concrete Core has a feature to export users, but you can select columns to export with this package. +Concrete Core can export users, but this package lets you choose which columns to include. Import maps CSV columns to user properties, groups, and attributes. -This package supports Concrete CMS version 9+ only. -You can use the [legacy version](https://github.com/MacareuxDigital/csv_user_import_export) if you're running Concrete CMS version 8. +**Requirements:** Concrete CMS 9+. For version 8, use the [legacy package](https://github.com/MacareuxDigital/csv_user_import_export). + +## Installation + +1. Place the package under `packages/md_csv_user_import_export`. +2. Install (or update) it from **Dashboard → Extend Concrete**. +3. Dashboard pages appear under **Users → Import** and **Users → Export**. + +## How to use + +### Export + +1. Open **Dashboard → Users → Export**. +2. Check the columns to include (core fields, groups, attributes). +3. Optionally enable **Keep mapping** so the next visit remembers your selection. +4. Download the CSV. + +Group columns export as `1` (member) or `0` (not a member). Attribute columns use each attribute’s text representation. + +### Import + +1. Upload a CSV (header row required) into the File Manager. +2. Open **Dashboard → Users → Import** and select that file. +3. On the mapping screen, for each importable property, choose the matching CSV column (or **Ignore**). +4. Optionally enable **Keep mapping** and/or **Delete CSV file after imported**. +5. Run **Import**. Rows are processed in a progressive batch. + +Matching users: if a row has a **Username**, the user is looked up by name; otherwise by **Email**. Existing users are updated; missing users are created. Empty mapped cells leave the current value alone. + +### Group import + +Two modes (can be used together): + +| Mode | Mapping | Cell value | Effect | +|------|---------|------------|--------| +| Per-group column | Property = group display name (e.g. Administrators) | `1` or `0` | Join or leave that group | +| By group name | Property = **Assign to Group** or **Unassign from Group** | Exact group name (e.g. `Administrators`) | Join or leave that group | + +Rules for name-based assign/unassign: + +- One group name per cell only (no comma-separated lists). +- Empty or ignored → membership unchanged. +- Unknown group name → logged and skipped; the rest of the row still imports. + +Example CSV for name-based assign: + +```csv +Username,Email,Group +alice,alice@example.com,Administrators +bob,bob@example.com,Editors +``` + +Map `Group` → **Assign to Group**. + +Example CSV for per-group columns: + +```csv +Username,Email,Administrators,Editors +alice,alice@example.com,1,0 +bob,bob@example.com,0,1 +``` + +Map each group property to the matching column. + +## Package design + +### Architecture + +| Piece | Role | +|-------|------| +| `CsvTrait` | Shared list of exportable/importable headers (static fields, `g:{id}` groups, `a:{handle}` attributes) | +| Import controller | Upload → mapping UI → batch of `ImportUserCommand` per row | +| `ImportUserCommandHandler` | Create/update user, then apply groups and attributes | +| Export controller + `Exporter` | Selected columns → streamed CSV download | + +Saved column choices live in package file config (`csv.import_columns`, `csv.export_columns`) when **Keep mapping** is on. + +### Why two group import modes? + +- **Per-group boolean columns** match export output and suit spreadsheets that already have one column per group. Internal handles are `g:{groupID}` so renames of display labels do not break stored mappings keyed by ID. +- **Assign / Unassign by name** suits CSVs with a single “Group” (or similar) column whose *value* is the group name. Export does not emit these fields: one cell cannot represent membership in multiple groups, so export keeps the boolean columns only. + +### Other decisions + +- **Empty cell = no change** for groups and most fields, so partial CSVs do not wipe data. +- **Attributes** use Concrete’s text-export interfaces (`SimpleTextExportableAttributeInterface` / `MulticolumnTextExportableAttributeInterface`), same idea as core CSV tooling. +- **Missing assign/unassign groups** do not fail the row; they are logged so bulk imports stay resilient. +- **Home folder** import/export is not implemented yet (see ToDo). ## ToDo -Support export/import home folder +- Support export/import home folder ## License -MIT License. \ No newline at end of file +MIT License. diff --git a/controller.php b/controller.php index aee8131..6ec5124 100644 --- a/controller.php +++ b/controller.php @@ -19,7 +19,7 @@ class Controller extends Package /** * @var string package version */ - protected $pkgVersion = '0.9.2'; + protected $pkgVersion = '0.9.3'; /** * {@inheritdoc} diff --git a/src/Traits/CsvTrait.php b/src/Traits/CsvTrait.php index ada6bff..71702bd 100644 --- a/src/Traits/CsvTrait.php +++ b/src/Traits/CsvTrait.php @@ -46,6 +46,8 @@ protected function getImportableHeaders(bool $includeSubHeaders = true): array 'active' => t('Active'), 'validated' => t('Validated'), 'password' => t('Password'), + 'assign_group' => t('Assign to Group'), + 'unassign_group' => t('Unassign from Group'), ]; $groupHeaders = $this->getGroupHeaders(); diff --git a/src/User/Command/ImportUserCommandHandler.php b/src/User/Command/ImportUserCommandHandler.php index 2c21093..6f896e9 100644 --- a/src/User/Command/ImportUserCommandHandler.php +++ b/src/User/Command/ImportUserCommandHandler.php @@ -7,6 +7,7 @@ use Concrete\Core\Error\ErrorList\ErrorList; use Concrete\Core\Logging\LoggerFactory; use Concrete\Core\Support\Facade\Application; +use Concrete\Core\User\Group\Group; use Concrete\Core\User\RegistrationService; use Concrete\Core\User\UserInfoRepository; use Concrete\Core\Utility\Service\Identifier; @@ -144,6 +145,23 @@ public function __invoke(ImportUserCommand $command) } } + foreach (['assign_group' => true, 'unassign_group' => false] as $handle => $assign) { + $groupName = $this->getValue($row, $handle); + if ($groupName === null) { + continue; + } + $group = Group::getByName($groupName); + if ($group) { + if ($assign) { + $user->enterGroup($group); + } else { + $user->exitGroup($group); + } + } else { + $logger->info(t('Group not found: %s (row %s)', $groupName, $line)); + } + } + /** @var ErrorList $attributesWarnings */ $attributesWarnings = $app->make(ErrorList::class); foreach ($this->getAttributeKeys() as $keyHandle => $attributeKey) {