diff --git a/controllers/almanach_contents_edit.go b/controllers/almanach_contents_edit.go new file mode 100644 index 0000000..7bf707f --- /dev/null +++ b/controllers/almanach_contents_edit.go @@ -0,0 +1,264 @@ +package controllers + +import ( + "fmt" + "maps" + "net/http" + "net/url" + "slices" + "strconv" + "strings" + + "github.com/Theodor-Springmann-Stiftung/musenalm/app" + "github.com/Theodor-Springmann-Stiftung/musenalm/dbmodels" + "github.com/Theodor-Springmann-Stiftung/musenalm/middleware" + "github.com/Theodor-Springmann-Stiftung/musenalm/pagemodels" + "github.com/Theodor-Springmann-Stiftung/musenalm/templating" + "github.com/pocketbase/pocketbase/core" + "github.com/pocketbase/pocketbase/tools/router" +) + +const ( + URL_ALMANACH_CONTENTS_EDIT = "contents/edit" + TEMPLATE_ALMANACH_CONTENTS_EDIT = "/almanach/contents/edit/" +) + +func init() { + ep := &AlmanachContentsEditPage{ + StaticPage: pagemodels.StaticPage{ + Name: pagemodels.P_ALMANACH_CONTENTS_EDIT_NAME, + URL: URL_ALMANACH_CONTENTS_EDIT, + Template: TEMPLATE_ALMANACH_CONTENTS_EDIT, + Layout: pagemodels.LAYOUT_LOGIN_PAGES, + }, + } + app.Register(ep) +} + +type AlmanachContentsEditPage struct { + pagemodels.StaticPage +} + +func (p *AlmanachContentsEditPage) Setup(router *router.Router[*core.RequestEvent], ia pagemodels.IApp, engine *templating.Engine) error { + app := ia.Core() + rg := router.Group(URL_ALMANACH) + rg.BindFunc(middleware.IsAdminOrEditor()) + rg.GET(URL_ALMANACH_CONTENTS_EDIT, p.GET(engine, app)) + rg.POST(URL_ALMANACH_CONTENTS_EDIT, p.POSTSave(engine, app)) + return nil +} + +func (p *AlmanachContentsEditPage) GET(engine *templating.Engine, app core.App) HandleFunc { + return func(e *core.RequestEvent) error { + id := e.Request.PathValue("id") + req := templating.NewRequest(e) + data := make(map[string]any) + result, err := NewAlmanachEditResult(app, id, BeitraegeFilterParameters{}) + if err != nil { + engine.Response404(e, err, nil) + } + data["result"] = result + data["csrf_token"] = req.Session().Token + data["content_types"] = dbmodels.CONTENT_TYPE_VALUES + data["musenalm_types"] = dbmodels.MUSENALM_TYPE_VALUES + data["pagination_values"] = slices.Collect(maps.Values(dbmodels.MUSENALM_PAGINATION_VALUES)) + + if msg := e.Request.URL.Query().Get("saved_message"); msg != "" { + data["success"] = msg + } + + return engine.Response200(e, p.Template, data, p.Layout) + } +} + +func (p *AlmanachContentsEditPage) renderError(engine *templating.Engine, app core.App, e *core.RequestEvent, message string) error { + id := e.Request.PathValue("id") + req := templating.NewRequest(e) + data := make(map[string]any) + result, err := NewAlmanachEditResult(app, id, BeitraegeFilterParameters{}) + if err != nil { + return engine.Response404(e, err, nil) + } + data["result"] = result + data["csrf_token"] = req.Session().Token + data["content_types"] = dbmodels.CONTENT_TYPE_VALUES + data["musenalm_types"] = dbmodels.MUSENALM_TYPE_VALUES + data["pagination_values"] = slices.Collect(maps.Values(dbmodels.MUSENALM_PAGINATION_VALUES)) + data["error"] = message + return engine.Response200(e, p.Template, data, p.Layout) +} + +func (p *AlmanachContentsEditPage) POSTSave(engine *templating.Engine, app core.App) HandleFunc { + return func(e *core.RequestEvent) error { + id := e.Request.PathValue("id") + req := templating.NewRequest(e) + + if err := e.Request.ParseForm(); err != nil { + return p.renderError(engine, app, e, "Formulardaten ungültig.") + } + + if err := req.CheckCSRF(e.Request.FormValue("csrf_token")); err != nil { + return p.renderError(engine, app, e, err.Error()) + } + + entry, err := dbmodels.Entries_MusenalmID(app, id) + if err != nil { + return engine.Response404(e, err, nil) + } + + contents, err := dbmodels.Contents_Entry(app, entry.Id) + if err != nil { + return p.renderError(engine, app, e, "Beiträge konnten nicht geladen werden.") + } + + contentInputs := parseContentsForm(e.Request.PostForm) + user := req.User() + + if err := app.RunInTransaction(func(tx core.App) error { + for _, content := range contents { + fields, ok := contentInputs[content.Id] + if !ok { + continue + } + if err := applyContentForm(content, entry, fields, user); err != nil { + return err + } + if err := tx.Save(content); err != nil { + return err + } + } + return nil + }); err != nil { + app.Logger().Error("Failed to save contents", "entry_id", entry.Id, "error", err) + return p.renderError(engine, app, e, err.Error()) + } + + go updateContentsFTS5(app, entry, contents) + + redirect := fmt.Sprintf("/almanach/%s/contents/edit?saved_message=%s", id, url.QueryEscape("Änderungen gespeichert.")) + return e.Redirect(http.StatusSeeOther, redirect) + } +} + +func parseContentsForm(form url.Values) map[string]map[string][]string { + contentInputs := map[string]map[string][]string{} + for key, values := range form { + if key == "csrf_token" || key == "last_edited" { + continue + } + trimmed := strings.TrimSuffix(key, "[]") + if !strings.HasPrefix(trimmed, "content_") { + continue + } + rest := strings.TrimPrefix(trimmed, "content_") + sep := strings.Index(rest, "_") + if sep < 0 { + continue + } + contentID := rest[:sep] + field := rest[sep+1:] + if field == "" || contentID == "" { + continue + } + if _, ok := contentInputs[contentID]; !ok { + contentInputs[contentID] = map[string][]string{} + } + contentInputs[contentID][field] = values + } + return contentInputs +} + +func applyContentForm(content *dbmodels.Content, entry *dbmodels.Entry, fields map[string][]string, user *dbmodels.FixedUser) error { + preferredTitle := strings.TrimSpace(firstValue(fields["preferred_title"])) + if preferredTitle == "" { + label := content.Id + if content.Numbering() > 0 { + label = strconv.FormatFloat(content.Numbering(), 'f', -1, 64) + } + return fmt.Errorf("Kurztitel ist erforderlich (Beitrag %s).", label) + } + + yearValue := strings.TrimSpace(firstValue(fields["year"])) + year := 0 + if yearValue != "" { + parsed, err := strconv.Atoi(yearValue) + if err != nil { + return fmt.Errorf("Ungültiges Jahr (Beitrag %s).", content.Id) + } + year = parsed + } + + numberingValue := strings.TrimSpace(firstValue(fields["numbering"])) + numbering := 0.0 + if numberingValue != "" { + parsed, err := strconv.ParseFloat(numberingValue, 64) + if err != nil { + return fmt.Errorf("Ungültige Nummerierung (Beitrag %s).", content.Id) + } + numbering = parsed + } + + status := strings.TrimSpace(firstValue(fields["edit_state"])) + if status == "" { + status = content.EditState() + } + if !slices.Contains(dbmodels.EDITORSTATE_VALUES, status) { + return fmt.Errorf("Ungültiger Status (Beitrag %s).", content.Id) + } + + content.SetPreferredTitle(preferredTitle) + content.SetVariantTitle(strings.TrimSpace(firstValue(fields["variant_title"]))) + content.SetParallelTitle(strings.TrimSpace(firstValue(fields["parallel_title"]))) + content.SetTitleStmt(strings.TrimSpace(firstValue(fields["title_statement"]))) + content.SetSubtitleStmt(strings.TrimSpace(firstValue(fields["subtitle_statement"]))) + content.SetIncipitStmt(strings.TrimSpace(firstValue(fields["incipit_statement"]))) + content.SetResponsibilityStmt(strings.TrimSpace(firstValue(fields["responsibility_statement"]))) + content.SetPlaceStmt(strings.TrimSpace(firstValue(fields["place_statement"]))) + content.SetPublicationStmt(strings.TrimSpace(firstValue(fields["publication_statement"]))) + content.SetYear(year) + content.SetExtent(strings.TrimSpace(firstValue(fields["extent"]))) + content.SetDimensions(strings.TrimSpace(firstValue(fields["dimensions"]))) + content.SetLanguage(fields["language"]) + content.SetContentType(fields["content_type"]) + content.SetMusenalmType(fields["musenalm_type"]) + content.SetMusenalmPagination(strings.TrimSpace(firstValue(fields["musenalm_pagination"]))) + content.SetNumbering(numbering) + content.SetEntry(entry.Id) + content.SetMusenalmID(entry.MusenalmID()) + content.SetEditState(status) + content.SetComment(strings.TrimSpace(firstValue(fields["edit_comment"]))) + content.SetAnnotation(strings.TrimSpace(firstValue(fields["annotation"]))) + if user != nil { + content.SetEditor(user.Id) + } + return nil +} + +func firstValue(values []string) string { + if len(values) == 0 { + return "" + } + return values[0] +} + +func updateContentsFTS5(app core.App, entry *dbmodels.Entry, contents []*dbmodels.Content) { + if len(contents) == 0 { + return + } + agents, relations, err := dbmodels.AgentsForContents(app, contents) + if err != nil { + app.Logger().Error("Failed to load content agents for FTS5 update", "entry_id", entry.Id, "error", err) + return + } + for _, content := range contents { + contentAgents := []*dbmodels.Agent{} + for _, rel := range relations[content.Id] { + if agent := agents[rel.Agent()]; agent != nil { + contentAgents = append(contentAgents, agent) + } + } + if err := dbmodels.UpdateFTS5Content(app, content, entry, contentAgents); err != nil { + app.Logger().Error("Failed to update FTS5 content", "content_id", content.Id, "error", err) + } + } +} diff --git a/pagemodels/pagedata.go b/pagemodels/pagedata.go index 400bc91..72e5874 100644 --- a/pagemodels/pagedata.go +++ b/pagemodels/pagedata.go @@ -46,6 +46,7 @@ const ( P_SEITEN_EDIT_NAME = "seiten_edit" P_ALMANACH_EDIT_NAME = "almanach_edit" + P_ALMANACH_CONTENTS_EDIT_NAME = "almanach_contents_edit" P_ALMANACH_NEW_NAME = "almanach_new" P_REIHE_EDIT_NAME = "reihe_edit" P_REIHE_NEW_NAME = "reihe_new" diff --git a/views/routes/almanach/components/entrydata.gohtml b/views/routes/almanach/components/entrydata.gohtml index bdd0ba4..9952109 100644 --- a/views/routes/almanach/components/entrydata.gohtml +++ b/views/routes/almanach/components/entrydata.gohtml @@ -55,6 +55,10 @@ Bearbeiten +
+ + Beiträge bearbeiten +
{{- end -}} diff --git a/views/routes/almanach/contents/edit/body.gohtml b/views/routes/almanach/contents/edit/body.gohtml new file mode 100644 index 0000000..fd561d2 --- /dev/null +++ b/views/routes/almanach/contents/edit/body.gohtml @@ -0,0 +1,202 @@ +{{ $model := . }} + + +
+
+
+
+ Inhalte +
+

+ {{- if $model.result -}} + {{- $model.result.Entry.PreferredTitle -}} + {{- else -}} + Inhalte bearbeiten + {{- end -}} +

+ {{- if $model.result -}} +
+ + · +
+ {{- if $model.result.PrevByID -}} +
+ + + +
+ {{- end -}} +
+ {{ $model.result.Entry.MusenalmID }} +
+ {{- if $model.result.NextByID -}} +
+ + + +
+ {{- end -}} +
+ · + +
+ {{- end -}} +
+ {{- if $model.result -}} +
+
+
+
+ Navigation +
+
+ {{- if $model.result.PrevByTitle -}} + +
{{ $model.result.PrevByTitle.PreferredTitle }}
+ + + +
+ {{- end -}} + + A - Z + + {{- if $model.result.NextByTitle -}} + +
{{ $model.result.NextByTitle.PreferredTitle }}
+ + + +
+ {{- end -}} +
+
+
+
+
+
+ Datenbank-ID +
+
{{ $model.result.Entry.Id }}
+
+
+
+
+
Zuletzt bearbeitet
+
+
+ {{ GermanDate $model.result.Entry.Updated }}, + {{ GermanTime $model.result.Entry.Updated }}h +
+
+ + {{- if $model.result.User -}}{{ $model.result.User.Name }}{{- end -}} +
+
+
+
+
+ {{- end -}} +
+
+ +
+ {{ template "_usermessage" $model }} +
+ + +
+ {{- range $_, $content := $model.result.Contents -}} + {{- template "_content_edit" (Dict + "content" $content + "entry" $model.result.Entry + "content_types" $model.content_types + "musenalm_types" $model.musenalm_types + "pagination_values" $model.pagination_values + ) -}} + {{- end -}} +
+ +
+

+
+ + + Abbrechen + + + + Reset + + +
+
+
+
+
+ + diff --git a/views/routes/almanach/contents/edit/head.gohtml b/views/routes/almanach/contents/edit/head.gohtml new file mode 100644 index 0000000..7c7ad19 --- /dev/null +++ b/views/routes/almanach/contents/edit/head.gohtml @@ -0,0 +1,8 @@ +{{ $model := . }} + +{{ if $model.result }} + Inhalte bearbeiten: {{ $model.result.Entry.PreferredTitle }} - Musenalm +{{ else }} + Inhalte bearbeiten - Musenalm +{{ end }} + diff --git a/views/routes/components/_content_edit.gohtml b/views/routes/components/_content_edit.gohtml new file mode 100644 index 0000000..2a9fec9 --- /dev/null +++ b/views/routes/components/_content_edit.gohtml @@ -0,0 +1,204 @@ +{{- $content := index . "content" -}} +{{- $entry := index . "entry" -}} +{{- $contentTypes := index . "content_types" -}} +{{- $musenalmTypes := index . "musenalm_types" -}} +{{- $paginationValues := index . "pagination_values" -}} +{{- $prefix := printf "content_%s_" $content.Id -}} +{{- $baseID := printf "content-%s" $content.Id -}} + +
+ + + + +
+
+ +
Alm-ID {{ $entry.MusenalmID }}
+
+
+ Scans folgen +
+
+ +
+
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+ +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+ + Weitere Felder & Anmerkungen + +
+
+
+
+ +
+ +
+
+
+ +
+ +
+
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+
+
+