docs: tags hold topics, the zjazd is meeting_id
The backend materials list shows the zjazd (meeting, by number). The MCP save and update material tools, the README material example and the management token test stop modelling a zjazd as a tag, and the README describes the one-way zjazd tag migration.
This commit is contained in:
@@ -290,14 +290,16 @@ The material body (POST and PUT), unknown keys are a 422:
|
||||
```json
|
||||
{"course_id": 7, "meeting_id": 2, "title": "Definicje globalizacji", "type": "notatka",
|
||||
"body": "# Robertson\n\n> …", "source": null, "url": null,
|
||||
"published_at": "2026-10-16T20:00:00+02:00", "tags": ["Globalizacja", "Zjazd 2"]}
|
||||
"published_at": "2026-10-16T20:00:00+02:00", "tags": ["Globalizacja"]}
|
||||
```
|
||||
|
||||
- `type`: `pdf`, `notatka`, `link`, `inne`. `body` is Markdown, rendered by the student app (headings start at h2 there; the title is the page's h1).
|
||||
- `course_id`: null or an existing course. `body`, `source` and `url` are replaced as sent (absent means null).
|
||||
- `meeting_id`: absent keeps the stored meeting (so scripts that never send it keep working), `null` clears it, otherwise an existing meeting (else a 422 on `meeting_id`). The managed material JSON shows `meeting_id`, and the material then lists under that zjazd in `GET /_bm/api/v1/materials?meeting=`.
|
||||
- `published_at`: absent publishes a new material now and keeps an existing one as it is; `null` makes it a draft; otherwise a time as in the seed file.
|
||||
- `tags`: absent keeps the tags; otherwise the list of names replaces them, and unknown tags are created (slug from the name, Polish letters folded).
|
||||
- `tags`: absent keeps the tags; otherwise the list of names replaces them, and unknown tags are created (slug from the name, Polish letters folded). Tags hold topics only: the zjazd is `meeting_id`, never a tag (the student app's Zjazd filter and badge read `meeting_id`).
|
||||
|
||||
The one-way migration `20261108140000_move_zjazd_tags_to_meetings` moved the old zjazd tags onto `meeting_id`: a material tagged `zjazd-N` (slug matching `^zjazd-[0-9]{1,6}$`) with no meeting got meeting number N (the lowest when it had several), an existing `meeting_id` was kept, and every such tag was deleted with its links; a slug such as `zjazd-3-24-10` is a topic and stays. A tag naming a missing meeting, a tag disagreeing with `meeting_id` and a material with several zjazd tags are logged at Warn under `jz.bm`. Its Rollback does nothing: the only way back is the `pg_dump --data-only -t jz_bm_tags -t jz_bm_material_tag` taken before `migrate`.
|
||||
- `is_private`: absent keeps it (a new material is public). `author_user_id`: absent keeps it (a new material gets the token user as author), `null` clears it, otherwise an existing user (else a 422 on `author_user_id`). `share_user_ids`: absent keeps the shares; otherwise the list of user ids replaces them (else a 422 on `share_user_ids`, as for the access routes). The managed material JSON shows `is_private`, `author_user_id`, `author` (`{"id", "name", "email"}` or null) and `shares` (a list, never null).
|
||||
- Files go through `POST /materials/{id}/files`, `/files/inline` or a signed upload. All three use one helper, `storeMaterialFile`, which applies the 20-file cap, stores through `attach.Store` (the default document and image extensions, content type sniffed from the bytes, a random disk name; `file_name` keeps the client's name) and binds the file to the material; a refused or failed store deletes the blobs it wrote. The file `url` is under `storage.uploads.public_path_prefix`, served by the application when `storage.uploads.serve` is true (the app's `config/storage.yaml` sets it): GET only, `is_public` rows only, anything but a raster image as `Content-Disposition: attachment`.
|
||||
|
||||
@@ -358,7 +360,7 @@ Controllers (admin API path `<backend.uri>/api/v1/jz/bm/<name>`), all in the "St
|
||||
| `jz.bm.meetings`, `jz.bm.class_sessions`, `jz.bm.deadlines` | `jz.bm.access_schedule` |
|
||||
| `jz.bm.materials`, `jz.bm.tags` | `jz.bm.access_materials` |
|
||||
|
||||
Courses, meetings, sessions and deadlines are picked through relation fields; a material's tags through a belongsToMany relation field. The material form has the `is_private` switch, the author picker (`author`, a belongsTo of `users` on `author_user_id`, by email) and `shared_with` (a belongsToMany of `users` through `jz_bm_material_shares`, by email); the list shows `is_private`. Dates and times are edited with date pickers (`type: datepicker`); the admin shows times in the browser's time zone and stores them in UTC. Labels are in Polish (`lang/pl`) and English (`lang/en`).
|
||||
Courses, meetings, sessions and deadlines are picked through relation fields; a material's tags through a belongsToMany relation field. The material form has the `is_private` switch, the author picker (`author`, a belongsTo of `users` on `author_user_id`, by email) and `shared_with` (a belongsToMany of `users` through `jz_bm_material_shares`, by email); the list shows `is_private` and the zjazd (`meeting`, by number). Dates and times are edited with date pickers (`type: datepicker`); the admin shows times in the browser's time zone and stores them in UTC. Labels are in Polish (`lang/pl`) and English (`lang/en`).
|
||||
|
||||
A material's files are uploaded in its form (`type: fileupload` on the attachMany `files`, public, up to 20 files of 50 MB). The course form embeds relation managers for its readings and class sessions, which create, edit and delete them, also before the course is first saved: `course_id` on both tables is nullable for that, the pending rows are bound to the form session and get the course on its first save, and `deferred:purge` deletes abandoned ones. The student API leaves out sessions and readings without a course. A class session added from the course form has no meeting; set it on the class sessions screen.
|
||||
|
||||
|
||||
@@ -139,7 +139,7 @@ RETURNING id`).Scan(&groupID).Error; err != nil {
|
||||
}
|
||||
idPath := func(id any) string { return "/api/v1/bm/materials/" + strconv.Itoa(int(id.(float64))) }
|
||||
|
||||
note := map[string]any{"title": "Definicje globalizacji", "type": "notatka", "body": "# Robertson\n\n> single place", "tags": []string{"Globalizacja", "Zjazd 2"}}
|
||||
note := map[string]any{"title": "Definicje globalizacji", "type": "notatka", "body": "# Robertson\n\n> single place", "tags": []string{"Globalizacja", "Teoria 2"}}
|
||||
rec, out := send(http.MethodPost, "/api/v1/bm/materials", note)
|
||||
if rec.Code != http.StatusCreated || out["created"] != true {
|
||||
t.Fatalf("create = %d %s", rec.Code, rec.Body.String())
|
||||
@@ -148,7 +148,7 @@ RETURNING id`).Scan(&groupID).Error; err != nil {
|
||||
if first["published_at"] == nil {
|
||||
t.Error("a new material without published_at is not published")
|
||||
}
|
||||
if tags, _ := first["tags"].([]any); len(tags) != 2 || tags[1].(map[string]any)["slug"] != "zjazd-2" {
|
||||
if tags, _ := first["tags"].([]any); len(tags) != 2 || tags[1].(map[string]any)["slug"] != "teoria-2" {
|
||||
t.Errorf("tags = %v", first["tags"])
|
||||
}
|
||||
|
||||
|
||||
@@ -207,7 +207,7 @@ type saveMaterialInput struct {
|
||||
Source *string `json:"source" jsonschema:"source, at most 500 characters, or null; replaced as sent"`
|
||||
URL *string `json:"url" jsonschema:"link, at most 2048 characters, or null; replaced as sent"`
|
||||
PublishedAt *string `json:"published_at,omitempty" jsonschema:"publication time: RFC 3339, or Europe/Warsaw wall clock such as 2026-10-16 20:00; absent publishes a new material now and keeps an existing one as it is; null makes a draft"`
|
||||
Tags *[]string `json:"tags,omitempty" jsonschema:"tag names (1 to 100 characters each); absent keeps the tags, a list replaces them"`
|
||||
Tags *[]string `json:"tags,omitempty" jsonschema:"topic tag names (1 to 100 characters each); absent keeps the tags, a list replaces them; tags hold topics only, the zjazd is set with meeting_id, never with a tag"`
|
||||
IsPrivate *bool `json:"is_private,omitempty" jsonschema:"true makes the material private: only its author and the users it is shared with see it; absent keeps it, a new material is public"`
|
||||
AuthorUserID *uint `json:"author_user_id,omitempty" jsonschema:"the author's user id from bm_manage_list_users; absent keeps it, a new material gets the caller as author; null clears it"`
|
||||
ShareUserIDs *[]uint `json:"share_user_ids,omitempty" jsonschema:"user ids from bm_manage_list_users the material is shared with (at most 200); absent keeps the shares, a list replaces them"`
|
||||
@@ -224,7 +224,7 @@ type updateMaterialInput struct {
|
||||
Source *string `json:"source" jsonschema:"source, at most 500 characters, or null; replaced as sent"`
|
||||
URL *string `json:"url" jsonschema:"link, at most 2048 characters, or null; replaced as sent"`
|
||||
PublishedAt *string `json:"published_at,omitempty" jsonschema:"publication time: RFC 3339, or Europe/Warsaw wall clock such as 2026-10-16 20:00; absent keeps it; null makes a draft"`
|
||||
Tags *[]string `json:"tags,omitempty" jsonschema:"tag names (1 to 100 characters each); absent keeps the tags, a list replaces them"`
|
||||
Tags *[]string `json:"tags,omitempty" jsonschema:"topic tag names (1 to 100 characters each); absent keeps the tags, a list replaces them; tags hold topics only, the zjazd is set with meeting_id, never with a tag"`
|
||||
IsPrivate *bool `json:"is_private,omitempty" jsonschema:"true makes the material private: only its author and the users it is shared with see it; absent keeps it, a new material is public"`
|
||||
AuthorUserID *uint `json:"author_user_id,omitempty" jsonschema:"the author's user id from bm_manage_list_users; absent keeps it, a new material gets the caller as author; null clears it"`
|
||||
ShareUserIDs *[]uint `json:"share_user_ids,omitempty" jsonschema:"user ids from bm_manage_list_users the material is shared with (at most 200); absent keeps the shares, a list replaces them"`
|
||||
|
||||
@@ -11,6 +11,10 @@ columns:
|
||||
label: jz.bm::lang.common.course
|
||||
relation: course
|
||||
select: name
|
||||
meeting:
|
||||
label: jz.bm::lang.common.meeting
|
||||
relation: meeting
|
||||
select: number
|
||||
published_at:
|
||||
label: jz.bm::lang.materials.published_at
|
||||
created_at:
|
||||
|
||||
Reference in New Issue
Block a user