diff --git a/README.md b/README.md index dddb957..2466fca 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # golem15.quizzes -A reusable quiz plugin for SummerCMS applications: quizzes with single-choice, multiple-choice and self-check (flashcard) questions, scored attempts for signed-in users, an admin backend and a JSON importer. +A reusable quiz plugin for SummerCMS applications: quizzes with single-choice, multiple-choice and self-check (flashcard) questions, a learning and a test mode that withholds solutions until an attempt is finished, scored attempts for signed-in users, an admin backend, a JSON importer and owner management over a token API and MCP tools. ```go import _ "git.golem15.com/golem15/sm-quizzes-plugin" @@ -234,12 +234,39 @@ Finishing sums the stored points, floors the total at 0, sets `score_percent = s ### Packages -- `classes`: `Score`, `Totals` and `ScoringConfigFrom` (scoring); `Service` (the API's queries and writes); `IsTest`, `Withheld`, `OneWay`, `AnswerDeadline`, `Expired`, `SecondsLeft`, `AttemptsLeft`, `AttemptLockKey` and `TestGraceFrom` (the test-mode policy); `ParseImport` and `Import` (the importer). +- `classes`: `Score`, `Totals` and `ScoringConfigFrom` (scoring); `Service` (the API's queries and writes); `IsTest`, `Withheld`, `OneWay`, `AnswerDeadline`, `Expired`, `SecondsLeft`, `AttemptsLeft`, `AttemptLockKey` and `TestGraceFrom` (the test-mode policy); `ParseImport`, `Import` and `CheckQuestion` (the importer and its per-question rules); `ListManagedQuizzes`, `ManagedQuiz`, `CreateQuiz`, `UpdateQuiz`, `DeleteQuiz`, `SetPublished`, `CreateQuestion`, `UpdateQuestion`, `MergeQuestion`, `DeleteQuestion`, `ReorderQuestions`, `QuizStats` and `BuildStats` (owner management). - `controllers/api`: `Handlers`, the HTTP handlers. - `controllers`: the admin controllers `QuizzesController`, `QuestionsController`, `AnswersController` and `AttemptsController`. - `models`: `Quiz`, `Question`, `Answer`, `Attempt`, `AttemptAnswer` and `Slugify`. - `console`: `ImportCommand`. +## Management API and MCP tools + +The owner manages quizzes from an MCP client (or any HTTP client) with a `golem15.user` API token. Every route below runs behind `user.api_token`, the per-user throttle `golem15.quizzes.manage` (120 requests a minute), the token scope named in the table and the `golem15.quizzes.access_quizzes` permission. The plugin declares that permission through `pact.HasFrontendPermissions`, so it shows up in golem15.user's User Groups editor. Grant it to the owner's group only, because the stats show student emails and names. Declaring it grants nothing. There is no owner column: a holder of the permission manages every quiz, as in the admin. Quizzes are addressed by slug. + +| Method and path under `/api/v1/quizzes` | Scope | MCP tool | Answer | +|---|---|---|---| +| `GET ""` (`?context=`) | read | `quizzes_list_quizzes` | Every quiz, published or not, ordered by context (none last) and title. | +| `GET /{slug}` | read | `quizzes_show_quiz` | The quiz with its questions and answers, solutions included. | +| `POST ""` | write | `quizzes_create_quiz` | 201. The new quiz starts unpublished. A taken slug is a 409. | +| `PATCH /{slug}` | write | `quizzes_update_quiz` | Only sent fields change. `0` clears `time_limit_seconds`, `max_attempts` and `round_size`, and `""` clears `description` and `context`. It cannot publish or change the slug. | +| `DELETE /{slug}?confirm={slug}` | write | `quizzes_delete_quiz` | `{id, slug, title, questions, attempts}`. A 422 unless `confirm` repeats the slug. | +| `POST /{slug}/publish`, `POST /{slug}/unpublish` | write | `quizzes_publish`, `quizzes_unpublish` | The quiz. Both are idempotent. | +| `GET /{slug}/stats` | read | `quizzes_quiz_stats` | `students` and `questions` (see below). | +| `POST /{slug}/questions` | write | `quizzes_create_question` | 201 with the question. It goes last unless `sort_order` is given, and its answers come inline. | +| `PATCH /{slug}/questions/{id}` | write | `quizzes_update_question` | The question. Only sent fields change. A sent `answers` list keeps and updates the answers with an `id`, creates the ones without and deletes the missing ones. | +| `DELETE /{slug}/questions/{id}` | write | `quizzes_delete_question` | `{id, quiz_slug}`. | +| `PUT /{slug}/questions/order` | write | `quizzes_reorder_questions` | The quiz with its questions. `question_ids` must list every question exactly once. | +| `POST /import?replace=` | write | `quizzes_import_quiz` | The import route (see CLI commands). It has its own `throttle:60,1` and is unchanged. | + +The quiz shape is `id, slug, title, description, context, mode, navigation, max_attempts, time_limit_seconds, passing_score, shuffle_questions, round_size, is_published, question_count, attempts_count, created_at, updated_at`. The question shape is `id, text, type, category, answer_text, explanation, points, sort_order, answers [{id, text, is_correct, explanation, sort_order}]`. These owner shapes are never used by a student route. + +Input is checked before the database is read. Invalid JSON, an unknown key or a broken rule is a 422 `{"message"}`. A merged question must pass the importer's rules (`classes.CheckQuestion`), and an answer `id` that belongs to another question is refused. A missing quiz or question is a 404. + +**History (D-16).** A question can be edited while its quiz has attempts. Finished attempts keep their stored scores, and their reviews show the current question. Answers are updated by id and never deleted and recreated, so the answer ids stored in past attempts stay valid. Deleting a question cascades to its answers and to the attempt answers given to it, and an import with `replace` does the same for every question. On a quiz with attempts, prefer the per-question tools. + +**Stats.** The stats route first closes the quiz's expired open test attempts at their deadline. `students [{user_id, email, name, surname, attempts, best_score_percent, last_score_percent, last_completed_at, passed}]` counts each student's completed whole-quiz attempts. Learning rounds of one category are left out, and `passed` is `best_score_percent >= passing_score`. `questions [{id, text, type, answered, correct, correct_percent, most_chosen_wrong}]` counts the answers of every completed attempt. `correct_percent` is `null` when nobody answered. `most_chosen_wrong` is `{answer_id, text, count}`, or `null` for self_check questions and when no wrong answer was chosen. + ## Configuration Defaults live in `config/config.yaml` and are merged under `golem15.quizzes.*`. diff --git a/plugin.yaml b/plugin.yaml index 6ca9863..716fe1c 100644 --- a/plugin.yaml +++ b/plugin.yaml @@ -4,7 +4,7 @@ id: golem15.quizzes module: git.golem15.com/golem15/sm-quizzes-plugin package: git.golem15.com/golem15/sm-quizzes-plugin name: Quizzes -description: "A reusable quiz plugin for SummerCMS applications: quizzes with single-choice, multiple-choice and self-check (flashcard) questions, scored attempts for signed-in users, an admin backend and a JSON importer." +description: "A reusable quiz plugin for SummerCMS applications: quizzes with single-choice, multiple-choice and self-check (flashcard) questions, a learning and a test mode that withholds solutions until an attempt is finished, scored attempts for signed-in users, an admin backend, a JSON importer and owner management over a token API and MCP tools." framework: v0.6.0 requires: golem15.user: v0.1.0