Skip to content

Commit b8c93d6

Browse files
committed
feat: add withdrawal, correction and archive pages
1 parent 32f6569 commit b8c93d6

12 files changed

Lines changed: 1548 additions & 43 deletions

‎README.md‎

Lines changed: 59 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,10 @@ The library stores knowledge **types** and **topics**, knowledge **items**, thei
2323

2424
### Version statuses
2525

26-
- `draft`: being edited, at most one per item; may be empty
27-
- `in_review`: submitted to a reviewer (`submitForReview()`), at most one per item
28-
- `published`: released (`publish()`); a draft can be published directly only if its type does not require a review
29-
- `withdrawn`: no longer valid
26+
- `draft`: being edited, at most one per item; may be empty. No draft can be created while a version of the item is in review or the item is archived
27+
- `in_review`: submitted to a reviewer (`submitForReview()`), at most one per item; the reviewer approves (`approve()`) or returns it as draft (`returnToDraft()`)
28+
- `published`: released (`publish()`, `approve()`); a draft can be published directly only if its type does not require a review
29+
- `withdrawn`: no longer valid, withdrawn (`withdraw()`) or replaced by a correction
3030

3131
Versions in review or published need content: a non-empty text or at least one main file.
3232

@@ -38,7 +38,7 @@ A published version has an effective state at a given date (`Version::getEffecti
3838
- `upcoming`: starts after the date
3939
- `historical`: ended before the date, or superseded
4040

41-
For types **with** a validity period a version is valid from `valid_from` until `valid_until` (open-ended if empty). A new version must start strictly after the latest published version; publishing it ends the predecessor the day before, unless the predecessor already ends earlier (gaps are allowed). Corrections (`corrects_version_id`) are exempt from this rule.
41+
For types **with** a validity period a version is valid from `valid_from` until `valid_until` (open-ended if empty). A new version must start strictly after the latest published version; publishing it ends the predecessor the day before, unless the predecessor already ends earlier (gaps are allowed). Corrections (`corrects_version_id`) are exempt from this rule, they keep the period of the corrected version (see [Withdrawal and corrections](#withdrawal-and-corrections)).
4242

4343
For types **without** a validity period versions have no dates; the published version with the highest number is in force, all older ones are historical.
4444

@@ -127,6 +127,8 @@ The backend module provides the following controllers. Routes are relative to th
127127
| `item/update` | Edit the master data of an item |
128128
| `item/source` | Edit the source data of an item |
129129
| `item/delete` | Delete an item with all its versions, files, relations and history (POST only) |
130+
| `item/archive` | Archive an item, body `reason` optional (POST only) |
131+
| `item/restore` | Restore an archived item, body `reason` optional (POST only) |
130132
| `type/index` | List of types |
131133
| `type/create` | Create a type |
132134
| `type/update` | Edit a type |
@@ -139,6 +141,12 @@ The backend module provides the following controllers. Routes are relative to th
139141
| `version/update` | Step of the version wizard (`step` 1 to 4) |
140142
| `version/publish` | Publish a draft of a type without review (POST only) |
141143
| `version/discard` | Discard a draft with its files (POST only) |
144+
| `version/review` | Review page of a version in review, for its reviewer |
145+
| `version/approve` | Approve and publish a version in review, body `note` optional (POST only) |
146+
| `version/return` | Return a version in review to its submitter, body `note` required (POST only) |
147+
| `version/reviewer` | Change the reviewer of a version in review, body `reviewer` and `reason` optional |
148+
| `version/withdraw` | Withdraw a published version, body `reason` and `successor` (`previous`, `correction` or `none`) |
149+
| `version/correct` | Start the correction of a published version, body `reason` optional (POST only) |
142150
| `file/download` | Download a file of a version |
143151
| `relation/create` | Add a relation from an item to another (POST only) |
144152
| `relation/delete` | Remove a relation (POST only) |
@@ -147,12 +155,47 @@ The views use `yii\bootstrap\ActiveForm` and `yii\grid\GridView` with Bootstrap
147155

148156
### Version wizard
149157

150-
New versions are created in a wizard that works on the draft of the item (at most one per item). A new draft takes over the text, the files and the details (title, summary, topics) of the latest published version. The wizard has four steps, each saved on its own; "Save as draft" returns to the detail page in every step:
158+
New versions are created in a wizard that works on the draft of the item (at most one per item). A new draft takes over the text, the files and the details (title, summary, topics) of the latest published version. The wizard has four steps, each saved on its own; "Save as draft" returns to the detail page in every step and writes the history entry `draft_saved`:
151159

152160
1. **Content**: Markdown text, main files (`mainFiles[]`, several at once) and attachments (`attachments[<i>]` with the title `attachmentTitles[<i>]`). Files of the draft can be removed (`remove[<file-id>]=1`); files uploaded in the draft are highlighted. An attachment whose content is already attached to another item shows a hint with a link to create an item of its own from it; the hint does not block. The step is complete with a text or at least one main file, attachments alone do not count. Rejected uploads are shown with the reason and keep the wizard on the step.
153-
2. **Validity**: Valid From and Valid Until for types with a validity period, with the consequences for the previous version and a preview of the timeline.
161+
2. **Validity**: Valid From and Valid Until for types with a validity period, with the consequences for the previous version and a preview of the timeline. A correction shows the period of the corrected version read-only.
154162
3. **Details**: title, topics and summary; they are applied to the item when the version is published.
155-
4. **Check**: summary of the version and "Publish" for types without review.
163+
4. **Check**: summary of the version and "Publish" for types without review; for types with review the reviewing person (`reviewer`), an optional message (`message`) and "Submit for approval" (`submit`).
164+
165+
The wizard of a correction is titled "Correct version n" with the number of the corrected version. Drafts of archived items cannot be continued, only discarded.
166+
167+
### Review workflow
168+
169+
Versions of a type with `requires_review` are published by a second person. The editor submits the draft in step 4 of the wizard to a reviewing person, optionally with a message; the version is then `in_review`, and no other draft can be created for the item until the review ends. The detail page shows who reviews the version; the list offers the filter "Awaiting my approval" (`ItemSearch[review]=mine`) with the number of items waiting for the current user.
170+
171+
The reviewer opens the review page (`version/review`) with the message, validity, consequences, topics, text and files of the version and either
172+
173+
- approves it (`version/approve`): the version is published like a direct publication, the note is kept as reason of the history entry, or
174+
- returns it (`version/return`) with a required note: the version becomes a draft again, the note and the reviewer are shown on the detail page and in the wizard, and submitting again clears them.
175+
176+
Only the chosen reviewer may approve or return a version, and nobody can submit a version to themselves (four-eyes principle, checked in `Version`, not only in the forms). Admins can hand a review over to another person (`version/reviewer`); the submitter and the current reviewer cannot be chosen.
177+
178+
The reviewing persons come from `UserProviderInterface::getReviewerOptions()`. The default provider offers the users with a direct assignment of the role `KnowledgeLibraryReviewer` or `KnowledgeLibraryAdmin` (all users if the application has no `authManager`). Root users of `dmstr\web\User` without a role assignment are therefore not offered as reviewers, and as the model checks the reviewer, they cannot approve a review of someone else either.
179+
180+
### Withdrawal and corrections
181+
182+
A published version in force or upcoming can be withdrawn on the page `version/withdraw` with a required reason. The page asks what applies instead in the period of the version:
183+
184+
- **Previous version remains valid** (`previous`): the published version with the latest Valid From before the withdrawn one takes over its Valid Until (open-ended if empty). Not available without previous version.
185+
- **Corrected version** (`correction`): continues to the correction below; the version stays published until the correction is published.
186+
- **Nothing applies** (`none`): the item has no valid version in that period.
187+
188+
For types without a validity period the published version with the highest number is valid anyway: "previous version remains valid" only documents that, and "nothing applies" is available only if there is no previous version.
189+
190+
"Correct" (`version/correct`) creates the draft of a correction of any published version and opens the wizard. The correction takes over text, files and validity period of the faulty version; its period cannot be changed. Publishing the correction (directly or through the review) withdraws the faulty version, other versions are not changed. For types without a validity period only the version in force can be corrected. A correction is not possible while the item has a draft or a version in review, or when it is archived. The reason given on the withdrawal page is kept with the faulty version until the correction is published and cleared when its draft is discarded. The confirmation of "Correct" names the period and, for a period in the past, the years whose answers change.
191+
192+
### Archive
193+
194+
Admins archive and restore items (`item/archive`, `item/restore`). Archived items are hidden in the list by default and marked "Archived". No new versions can be created, corrected, continued, approved or published for them: the routes redirect to the detail page with the message "The knowledge object is archived.". Returning a review, withdrawing a version, discarding a draft, master data, source and relations remain possible. Open drafts and reviews are kept when an item is archived.
195+
196+
### History
197+
198+
Every change of an item is logged in the table `history` and shown in the tab "History" of the detail page (when, who, what, reason; newest first). Version transitions (submit, hand over, return, approve, publish, withdraw, correct) are logged by `Version` in the same transaction, archiving and restoring by `Item`, the other entries (item created, master data and source changed, draft saved and discarded, relations added and removed) by the controllers. Structured facts such as the version number, the reviewer or the successor of a withdrawal are kept as JSON in `history.details`, so an entry can still be described after its version was deleted (`History::describe()`). Deleting an item deletes its history.
156199

157200
### Delete log
158201

@@ -194,6 +237,8 @@ The package brings the following migrations:
194237
| `m260928_185500_knowledge_library_routes` | Route permissions of items, types and topics |
195238
| `m260928_203000_knowledge_library_versions` | Draft details of versions (`draft_title`, `draft_summary`, `draft_topic_ids`) and the content hash of files (`content_hash`) |
196239
| `m260928_203100_knowledge_library_routes_2` | Route permissions of the version wizard, the file download and the relations |
240+
| `m260928_223000_knowledge_library_history_details` | Structured details of history entries (`history.details`) |
241+
| `m260928_223100_knowledge_library_routes_3` | Route permissions of review, withdrawal, correction and archive |
197242
| `i18n/m260928_100200_knowledge_library_translations` | Optional German translations, see [Translations](#translations) |
198243

199244
Add the migration path to the migrate controller of your console application:
@@ -258,19 +303,22 @@ They are grouped into roles that build on each other: `KnowledgeLibraryAdmin` co
258303

259304
### Route permissions
260305

261-
Every action of the backend module is checked against a permission named `<module-id>_<controller>_<action>`, e.g. `knowledge-library_item_delete`. The migrations `m260928_185500_knowledge_library_routes` and `m260928_203100_knowledge_library_routes_2` create one permission per action and assign them to the roles:
306+
Every action of the backend module is checked against a permission named `<module-id>_<controller>_<action>`, e.g. `knowledge-library_item_delete`. The migrations `m260928_185500_knowledge_library_routes`, `m260928_203100_knowledge_library_routes_2` and `m260928_223100_knowledge_library_routes_3` create one permission per action and assign them to the roles:
262307

263308
| Permission | Role |
264309
| --- | --- |
265310
| `knowledge-library_item_index`, `knowledge-library_item_create`, `knowledge-library_item_view`, `knowledge-library_item_update`, `knowledge-library_item_source` | `KnowledgeLibraryEditor` |
266-
| `knowledge-library_item_delete` | `KnowledgeLibraryAdmin` |
311+
| `knowledge-library_item_delete`, `knowledge-library_item_archive`, `knowledge-library_item_restore` | `KnowledgeLibraryAdmin` |
267312
| `knowledge-library_type_index`, `knowledge-library_type_create`, `knowledge-library_type_update`, `knowledge-library_type_delete` | `KnowledgeLibraryAdmin` |
268313
| `knowledge-library_topic_index`, `knowledge-library_topic_create`, `knowledge-library_topic_update`, `knowledge-library_topic_delete` | `KnowledgeLibraryAdmin` |
269314
| `knowledge-library_version_create`, `knowledge-library_version_update`, `knowledge-library_version_publish`, `knowledge-library_version_discard` | `KnowledgeLibraryEditor` |
315+
| `knowledge-library_version_withdraw`, `knowledge-library_version_correct` | `KnowledgeLibraryEditor` |
316+
| `knowledge-library_version_review`, `knowledge-library_version_approve`, `knowledge-library_version_return` | `KnowledgeLibraryReviewer` |
317+
| `knowledge-library_version_reviewer` | `KnowledgeLibraryAdmin` |
270318
| `knowledge-library_file_download` | `KnowledgeLibraryEditor` |
271319
| `knowledge-library_relation_create`, `knowledge-library_relation_delete` | `KnowledgeLibraryEditor` |
272320

273-
Reviewers and admins inherit the editor permissions through the role chain. The permission names assume the module ID `knowledge-library`; with another module ID the application creates the permissions itself.
321+
Reviewers and admins inherit the editor permissions, admins the reviewer permissions through the role chain. The route permission allows opening the review pages; approving or returning a version additionally requires being its chosen reviewer. The permission names assume the module ID `knowledge-library`; with another module ID the application creates the permissions itself.
274322

275323
Everyone who may open the detail page of an item may also download its files, including the files of drafts.
276324

‎src/controllers/BaseController.php‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,22 @@ protected function verbs(): array
4040
];
4141
}
4242

43+
/**
44+
* Whether the current user may use the route `<controller>/<action>` of
45+
* the module, checked like the access control of the module
46+
* (`AccessBehaviorTrait`).
47+
*/
48+
public function canRoute(string $controllerId, string $actionId): bool
49+
{
50+
$permission = str_replace(
51+
'/',
52+
'_',
53+
trim($this->module->getUniqueId(), '/') . '_' . $controllerId . '_' . $actionId
54+
);
55+
56+
return Yii::$app->getUser()->can($permission, ['route' => true]);
57+
}
58+
4359
/**
4460
* Root breadcrumb of all backend pages, linking to the item library.
4561
*/

‎src/controllers/ItemController.php‎

Lines changed: 78 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,14 @@ class ItemController extends BaseController
5757
*/
5858
private const SOURCE_ATTRIBUTES = ['source_name', 'source_reference', 'source_url', 'source_import_mode'];
5959

60+
protected function verbs(): array
61+
{
62+
return array_merge(parent::verbs(), [
63+
'archive' => ['POST'],
64+
'restore' => ['POST'],
65+
]);
66+
}
67+
6068
/**
6169
* Lists the items with filters, sorting and the state of each item.
6270
*
@@ -124,7 +132,10 @@ public function actionCreate()
124132
*
125133
* The header shows the version in review (with "Change reviewer" for
126134
* admins and "Review" for its reviewer) and the note of a returned draft;
127-
* the tab history lists the history entries, newest first.
135+
* the tab history lists the history entries, newest first. Admins get
136+
* "Archive" or "Restore"; archived items offer no new version and no
137+
* correction. The tab versions offers "Correct", "Withdraw" and
138+
* "Approve" per version, depending on its state and the route rights.
128139
*
129140
* @param string|null $version number of the version shown in the tab content
130141
* @param string|null $tab active tab
@@ -161,6 +172,9 @@ public function actionView(string $id, $version = null, $tab = null): string
161172
return $this->render('view', [
162173
'model' => $model,
163174
'canDelete' => $this->canDelete(),
175+
'canArchive' => $this->canArchive($model),
176+
'canCorrectRoute' => $this->canRoute('version', 'correct'),
177+
'canWithdrawRoute' => $this->canRoute('version', 'withdraw'),
164178
'uploadedByName' => $uploadedBy === null || $uploadedBy === ''
165179
? null
166180
: ($users->getDisplayName($uploadedBy) ?? $uploadedBy),
@@ -310,6 +324,65 @@ public function actionDelete(string $id): Response
310324
return $this->redirect(['index']);
311325
}
312326

327+
/**
328+
* Archives the item (body `reason`, optional), see Item::archive(). Open
329+
* drafts and reviews are kept; while archived, no new versions can be
330+
* created, corrected, continued, approved or published.
331+
*
332+
* @throws NotFoundHttpException
333+
*/
334+
public function actionArchive(string $id): Response
335+
{
336+
$model = $this->findModel($id);
337+
338+
return $this->redirectArchiveResult($model, $model->archive($this->postReason()), Yii::t(
339+
'knowledge-library',
340+
'Knowledge object archived.'
341+
));
342+
}
343+
344+
/**
345+
* Restores an archived item (body `reason`, optional), see
346+
* Item::restore().
347+
*
348+
* @throws NotFoundHttpException
349+
*/
350+
public function actionRestore(string $id): Response
351+
{
352+
$model = $this->findModel($id);
353+
354+
return $this->redirectArchiveResult($model, $model->restore($this->postReason()), Yii::t(
355+
'knowledge-library',
356+
'Knowledge object restored.'
357+
));
358+
}
359+
360+
/**
361+
* Flash message of archiving or restoring (the errors of the item on
362+
* failure) and redirect to the detail page.
363+
*/
364+
private function redirectArchiveResult(Item $model, bool $done, string $message): Response
365+
{
366+
$session = Yii::$app->getSession();
367+
if ($done) {
368+
$session->setFlash('success', $message);
369+
} else {
370+
$session->setFlash('error', implode(' ', $model->getFirstErrors()));
371+
}
372+
373+
return $this->redirect(['view', 'id' => $model->id]);
374+
}
375+
376+
/**
377+
* Body parameter `reason`, null if missing or empty.
378+
*/
379+
private function postReason(): ?string
380+
{
381+
$reason = $this->request->post('reason');
382+
383+
return is_string($reason) && trim($reason) !== '' ? trim($reason) : null;
384+
}
385+
313386
/**
314387
* Version shown in the tab content: the one with the given number, else
315388
* the version valid today, else the highest.
@@ -349,19 +422,12 @@ public function canDelete(): bool
349422
}
350423

351424
/**
352-
* Whether the current user may use the route `<controller>/<action>` of
353-
* the module, checked like the access control of the module
354-
* (`AccessBehaviorTrait`).
425+
* Whether the current user may archive the item, or restore it if it is
426+
* archived, checked like the access control of the module.
355427
*/
356-
public function canRoute(string $controllerId, string $actionId): bool
428+
public function canArchive(Item $model): bool
357429
{
358-
$permission = str_replace(
359-
'/',
360-
'_',
361-
trim($this->module->getUniqueId(), '/') . '_' . $controllerId . '_' . $actionId
362-
);
363-
364-
return Yii::$app->getUser()->can($permission, ['route' => true]);
430+
return $this->canRoute($this->id, $model->is_archived ? 'restore' : 'archive');
365431
}
366432

367433
/**

0 commit comments

Comments
 (0)