mirror of
https://github.com/Theodor-Springmann-Stiftung/musenalm.git
synced 2026-02-04 02:25:30 +00:00
1441 lines
45 KiB
Go
1441 lines
45 KiB
Go
package controllers
|
|
|
|
import (
|
|
"fmt"
|
|
"maps"
|
|
"net/http"
|
|
"net/url"
|
|
"slices"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"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/filesystem"
|
|
"github.com/pocketbase/pocketbase/tools/router"
|
|
)
|
|
|
|
const (
|
|
URL_ALMANACH_CONTENTS_EDIT = "contents/edit"
|
|
URL_ALMANACH_CONTENTS_NEW = "contents/new"
|
|
URL_ALMANACH_CONTENTS_ITEM_EDIT = "contents/{contentMusenalmId}/edit"
|
|
URL_ALMANACH_CONTENTS_DELETE = "contents/delete"
|
|
URL_ALMANACH_CONTENTS_EDIT_EXTENT = "contents/edit/extent"
|
|
URL_ALMANACH_CONTENTS_UPLOAD = "contents/upload"
|
|
URL_ALMANACH_CONTENTS_DELETE_SCAN = "contents/scan/delete"
|
|
TEMPLATE_ALMANACH_CONTENTS_EDIT = "/contents/edit/"
|
|
TEMPLATE_ALMANACH_CONTENTS_ITEM_EDIT = "/contents/edit_item/"
|
|
TEMPLATE_ALMANACH_CONTENTS_IMAGES_PANEL = "/contents/images_panel/"
|
|
)
|
|
|
|
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.GET(URL_ALMANACH_CONTENTS_NEW, p.GETNew(engine, app))
|
|
rg.GET(URL_ALMANACH_CONTENTS_ITEM_EDIT, p.GETItemEdit(engine, app))
|
|
rg.POST(URL_ALMANACH_CONTENTS_EDIT, p.POSTSave(engine, app))
|
|
rg.POST(URL_ALMANACH_CONTENTS_DELETE, p.POSTDelete(engine, app))
|
|
rg.POST(URL_ALMANACH_CONTENTS_EDIT_EXTENT, p.POSTUpdateExtent(engine, app))
|
|
rg.POST(URL_ALMANACH_CONTENTS_UPLOAD, p.POSTUploadScans(engine, app))
|
|
rg.POST(URL_ALMANACH_CONTENTS_DELETE_SCAN, p.POSTDeleteScan(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"] = paginationValuesSorted()
|
|
data["agent_relations"] = dbmodels.AGENT_RELATIONS
|
|
|
|
if msg := popFlashSuccess(e); msg != "" {
|
|
data["success"] = msg
|
|
}
|
|
data["edit_content_id"] = strings.TrimSpace(e.Request.URL.Query().Get("edit_content"))
|
|
data["new_content"] = strings.TrimSpace(e.Request.URL.Query().Get("new_content"))
|
|
|
|
return engine.Response200(e, p.Template, data, p.Layout)
|
|
}
|
|
}
|
|
|
|
func (p *AlmanachContentsEditPage) GETItemEdit(engine *templating.Engine, app core.App) HandleFunc {
|
|
return func(e *core.RequestEvent) error {
|
|
id := e.Request.PathValue("id")
|
|
contentMusenalmID := strings.TrimSpace(e.Request.PathValue("contentMusenalmId"))
|
|
if contentMusenalmID == "" {
|
|
return e.String(http.StatusBadRequest, "")
|
|
}
|
|
|
|
req := templating.NewRequest(e)
|
|
data := make(map[string]any)
|
|
result, err := NewAlmanachEditResult(app, id, BeitraegeFilterParameters{})
|
|
if err != nil {
|
|
engine.Response404(e, err, nil)
|
|
}
|
|
|
|
content, err := dbmodels.Contents_MusenalmID(app, contentMusenalmID)
|
|
if err != nil || content == nil {
|
|
return e.String(http.StatusNotFound, "")
|
|
}
|
|
if content.Entry() != result.Entry.Id {
|
|
return e.String(http.StatusNotFound, "")
|
|
}
|
|
|
|
contents, err := dbmodels.Contents_Entry(app, result.Entry.Id)
|
|
if err == nil && len(contents) > 1 {
|
|
sort.Slice(contents, func(i, j int) bool {
|
|
if contents[i].Numbering() == contents[j].Numbering() {
|
|
return contents[i].Id < contents[j].Id
|
|
}
|
|
return contents[i].Numbering() < contents[j].Numbering()
|
|
})
|
|
}
|
|
var prevContent *dbmodels.Content
|
|
var nextContent *dbmodels.Content
|
|
contentIndex := 0
|
|
contentTotal := 0
|
|
if len(contents) > 0 {
|
|
contentTotal = len(contents)
|
|
for i, c := range contents {
|
|
if c.Id != content.Id {
|
|
continue
|
|
}
|
|
contentIndex = i + 1
|
|
if i > 0 {
|
|
prevContent = contents[i-1]
|
|
}
|
|
if i < len(contents)-1 {
|
|
nextContent = contents[i+1]
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
agentsMap, contentAgentsMap, err := dbmodels.AgentsForContents(app, []*dbmodels.Content{content})
|
|
if err != nil {
|
|
agentsMap = map[string]*dbmodels.Agent{}
|
|
contentAgentsMap = map[string][]*dbmodels.RContentsAgents{}
|
|
}
|
|
|
|
data["result"] = result
|
|
data["csrf_token"] = req.Session().Token
|
|
data["content"] = content
|
|
data["content_id"] = content.Id
|
|
data["content_types"] = dbmodels.CONTENT_TYPE_VALUES
|
|
data["musenalm_types"] = dbmodels.MUSENALM_TYPE_VALUES
|
|
data["pagination_values"] = paginationValuesSorted()
|
|
data["agent_relations"] = dbmodels.AGENT_RELATIONS
|
|
data["agents"] = agentsMap
|
|
data["content_agents"] = contentAgentsMap[content.Id]
|
|
data["prev_content"] = prevContent
|
|
data["next_content"] = nextContent
|
|
data["content_index"] = contentIndex
|
|
data["content_total"] = contentTotal
|
|
|
|
if msg := popFlashSuccess(e); msg != "" {
|
|
data["success"] = msg
|
|
}
|
|
|
|
return engine.Response200(e, TEMPLATE_ALMANACH_CONTENTS_ITEM_EDIT, data, p.Layout)
|
|
}
|
|
}
|
|
|
|
func (p *AlmanachContentsEditPage) GETNew(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)
|
|
}
|
|
|
|
contentCollection, err := app.FindCollectionByNameOrId(dbmodels.CONTENTS_TABLE)
|
|
if err != nil {
|
|
return engine.Response404(e, err, nil)
|
|
}
|
|
record := core.NewRecord(contentCollection)
|
|
tempID := fmt.Sprintf("tmp-%d", time.Now().UnixNano())
|
|
record.Id = tempID
|
|
content := dbmodels.NewContent(record)
|
|
content.SetEntry(result.Entry.Id)
|
|
content.SetEditState("Edited")
|
|
|
|
data["result"] = result
|
|
data["csrf_token"] = req.Session().Token
|
|
data["content"] = content
|
|
data["content_id"] = content.Id
|
|
data["content_types"] = dbmodels.CONTENT_TYPE_VALUES
|
|
data["musenalm_types"] = dbmodels.MUSENALM_TYPE_VALUES
|
|
data["pagination_values"] = paginationValuesSorted()
|
|
data["agent_relations"] = dbmodels.AGENT_RELATIONS
|
|
data["agents"] = map[string]*dbmodels.Agent{}
|
|
data["content_agents"] = []*dbmodels.RContentsAgents{}
|
|
data["is_new"] = true
|
|
|
|
return engine.Response200(e, TEMPLATE_ALMANACH_CONTENTS_ITEM_EDIT, 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"] = paginationValuesSorted()
|
|
data["agent_relations"] = dbmodels.AGENT_RELATIONS
|
|
data["error"] = message
|
|
data["edit_content_id"] = strings.TrimSpace(e.Request.URL.Query().Get("edit_content"))
|
|
data["new_content"] = strings.TrimSpace(e.Request.URL.Query().Get("new_content"))
|
|
return engine.Response200(e, p.Template, data, p.Layout)
|
|
}
|
|
|
|
func (p *AlmanachContentsEditPage) renderItemError(engine *templating.Engine, app core.App, e *core.RequestEvent, contentID string, 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)
|
|
}
|
|
|
|
contents, err := dbmodels.Contents_IDs(app, []any{contentID})
|
|
if err != nil || len(contents) == 0 {
|
|
return p.renderError(engine, app, e, message)
|
|
}
|
|
content := contents[0]
|
|
if content.Entry() != result.Entry.Id {
|
|
return p.renderError(engine, app, e, message)
|
|
}
|
|
|
|
entryContents, err := dbmodels.Contents_Entry(app, result.Entry.Id)
|
|
if err == nil && len(entryContents) > 1 {
|
|
sort.Slice(entryContents, func(i, j int) bool {
|
|
if entryContents[i].Numbering() == entryContents[j].Numbering() {
|
|
return entryContents[i].Id < entryContents[j].Id
|
|
}
|
|
return entryContents[i].Numbering() < entryContents[j].Numbering()
|
|
})
|
|
}
|
|
var prevContent *dbmodels.Content
|
|
var nextContent *dbmodels.Content
|
|
contentIndex := 0
|
|
contentTotal := 0
|
|
if len(entryContents) > 0 {
|
|
contentTotal = len(entryContents)
|
|
for i, c := range entryContents {
|
|
if c.Id != content.Id {
|
|
continue
|
|
}
|
|
contentIndex = i + 1
|
|
if i > 0 {
|
|
prevContent = entryContents[i-1]
|
|
}
|
|
if i < len(entryContents)-1 {
|
|
nextContent = entryContents[i+1]
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
agentsMap, contentAgentsMap, err := dbmodels.AgentsForContents(app, []*dbmodels.Content{content})
|
|
if err != nil {
|
|
agentsMap = map[string]*dbmodels.Agent{}
|
|
contentAgentsMap = map[string][]*dbmodels.RContentsAgents{}
|
|
}
|
|
|
|
data["result"] = result
|
|
data["csrf_token"] = req.Session().Token
|
|
data["content"] = content
|
|
data["content_id"] = content.Id
|
|
data["content_types"] = dbmodels.CONTENT_TYPE_VALUES
|
|
data["musenalm_types"] = dbmodels.MUSENALM_TYPE_VALUES
|
|
data["pagination_values"] = paginationValuesSorted()
|
|
data["agent_relations"] = dbmodels.AGENT_RELATIONS
|
|
data["agents"] = agentsMap
|
|
data["content_agents"] = contentAgentsMap[content.Id]
|
|
data["prev_content"] = prevContent
|
|
data["next_content"] = nextContent
|
|
data["content_index"] = contentIndex
|
|
data["content_total"] = contentTotal
|
|
data["error"] = message
|
|
|
|
return engine.Response200(e, TEMPLATE_ALMANACH_CONTENTS_ITEM_EDIT, 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 e.Request.MultipartForm == nil {
|
|
if err := e.Request.ParseMultipartForm(router.DefaultMaxMemory); err != nil {
|
|
if e.Request.MultipartForm == nil {
|
|
if err := e.Request.ParseForm(); err != nil {
|
|
return p.renderError(engine, app, e, err.Error())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
contentID := strings.TrimSpace(e.Request.FormValue("content_id"))
|
|
renderError := func(message string) error {
|
|
if contentID != "" {
|
|
return p.renderItemError(engine, app, e, contentID, message)
|
|
}
|
|
return p.renderError(engine, app, e, message)
|
|
}
|
|
|
|
if err := req.CheckCSRF(e.Request.FormValue("csrf_token")); err != nil {
|
|
return renderError(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)
|
|
contentOrder := parseContentsOrder(e.Request.PostForm)
|
|
orderMap := buildContentOrderMap(contentOrder)
|
|
relationsByContent := map[string]contentAgentRelationsPayload{}
|
|
for contentID := range contentInputs {
|
|
payload := parseContentAgentRelations(e.Request.PostForm, contentID)
|
|
if err := validateContentAgentRelations(payload); err != nil {
|
|
return p.renderError(engine, app, e, err.Error())
|
|
}
|
|
relationsByContent[contentID] = payload
|
|
}
|
|
user := req.User()
|
|
existingByID := make(map[string]*dbmodels.Content, len(contents))
|
|
for _, content := range contents {
|
|
existingByID[content.Id] = content
|
|
}
|
|
newContentIDs := make([]string, 0)
|
|
for contentID := range contentInputs {
|
|
if _, exists := existingByID[contentID]; !exists {
|
|
newContentIDs = append(newContentIDs, contentID)
|
|
}
|
|
}
|
|
if len(newContentIDs) > 1 {
|
|
sort.Slice(newContentIDs, func(i, j int) bool {
|
|
return orderMap[newContentIDs[i]] < orderMap[newContentIDs[j]]
|
|
})
|
|
}
|
|
|
|
uploadedScans, _ := e.FindUploadedFiles(dbmodels.SCAN_FIELD)
|
|
deleteScans := valuesForKey(e.Request.PostForm, "scans_delete")
|
|
scansOrder := valuesForKey(e.Request.PostForm, "scans_order")
|
|
pendingScanIDs := valuesForKey(e.Request.PostForm, "scans_pending_ids")
|
|
targetContentID := contentID
|
|
if targetContentID == "" && len(contentInputs) == 1 {
|
|
for id := range contentInputs {
|
|
targetContentID = id
|
|
break
|
|
}
|
|
}
|
|
if contentID == "" {
|
|
contentID = targetContentID
|
|
}
|
|
|
|
tempToCreated := map[string]string{}
|
|
var createdContents []*dbmodels.Content
|
|
var updatedContents []*dbmodels.Content
|
|
if err := app.RunInTransaction(func(tx core.App) error {
|
|
if len(orderMap) > 0 {
|
|
for _, content := range contents {
|
|
numbering, ok := orderMap[content.Id]
|
|
if !ok {
|
|
continue
|
|
}
|
|
if content.Numbering() == numbering {
|
|
continue
|
|
}
|
|
content.SetNumbering(numbering)
|
|
if err := tx.Save(content); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
nextMusenalmID := 0
|
|
if len(newContentIDs) > 0 {
|
|
nextID, err := nextContentMusenalmID(tx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
nextMusenalmID = nextID
|
|
}
|
|
created := make([]*dbmodels.Content, 0, len(newContentIDs))
|
|
for _, tempID := range newContentIDs {
|
|
fields, ok := contentInputs[tempID]
|
|
if !ok {
|
|
continue
|
|
}
|
|
contentCollection, err := tx.FindCollectionByNameOrId(dbmodels.CONTENTS_TABLE)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
record := core.NewRecord(contentCollection)
|
|
content := dbmodels.NewContent(record)
|
|
content.SetMusenalmID(nextMusenalmID)
|
|
nextMusenalmID++
|
|
content.SetEditState("Edited")
|
|
numbering := orderMap[tempID]
|
|
if numbering <= 0 {
|
|
numbering = float64(len(contents) + len(created) + 1)
|
|
}
|
|
if err := applyContentForm(content, entry, fields, user, numbering); err != nil {
|
|
return err
|
|
}
|
|
if err := tx.Save(content); err != nil {
|
|
return err
|
|
}
|
|
tempToCreated[tempID] = content.Id
|
|
if relations, ok := relationsByContent[tempID]; ok {
|
|
if err := applyContentAgentRelations(tx, content, relations); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
created = append(created, content)
|
|
}
|
|
for _, content := range contents {
|
|
fields, ok := contentInputs[content.Id]
|
|
if !ok {
|
|
continue
|
|
}
|
|
numbering := orderMap[content.Id]
|
|
if err := applyContentForm(content, entry, fields, user, numbering); err != nil {
|
|
return err
|
|
}
|
|
if err := tx.Save(content); err != nil {
|
|
return err
|
|
}
|
|
if relations, ok := relationsByContent[content.Id]; ok {
|
|
if err := applyContentAgentRelations(tx, content, relations); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
effectiveContentID := targetContentID
|
|
if mappedID, ok := tempToCreated[effectiveContentID]; ok {
|
|
effectiveContentID = mappedID
|
|
}
|
|
if effectiveContentID != "" && (len(uploadedScans) > 0 || len(deleteScans) > 0 || len(scansOrder) > 0) {
|
|
record, err := tx.FindRecordById(dbmodels.CONTENTS_TABLE, effectiveContentID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
content := dbmodels.NewContent(record)
|
|
if content.Entry() != entry.Id {
|
|
return fmt.Errorf("Beitrag gehört zu einem anderen Band.")
|
|
}
|
|
deleteSet := map[string]struct{}{}
|
|
for _, scan := range deleteScans {
|
|
scan = strings.TrimSpace(scan)
|
|
if scan == "" {
|
|
continue
|
|
}
|
|
deleteSet[scan] = struct{}{}
|
|
}
|
|
if len(scansOrder) > 0 || len(pendingScanIDs) > 0 {
|
|
pendingMap := map[string]*filesystem.File{}
|
|
for idx, id := range pendingScanIDs {
|
|
if idx >= len(uploadedScans) {
|
|
break
|
|
}
|
|
id = strings.TrimSpace(id)
|
|
if id == "" {
|
|
continue
|
|
}
|
|
pendingMap[id] = uploadedScans[idx]
|
|
}
|
|
ordered := make([]any, 0, len(scansOrder)+len(uploadedScans))
|
|
seenExisting := map[string]struct{}{}
|
|
for _, token := range scansOrder {
|
|
token = strings.TrimSpace(token)
|
|
if token == "" {
|
|
continue
|
|
}
|
|
if strings.HasPrefix(token, "pending:") {
|
|
id := strings.TrimPrefix(token, "pending:")
|
|
if file, ok := pendingMap[id]; ok {
|
|
ordered = append(ordered, file)
|
|
}
|
|
continue
|
|
}
|
|
if strings.HasPrefix(token, "existing:") {
|
|
name := strings.TrimPrefix(token, "existing:")
|
|
if name == "" {
|
|
continue
|
|
}
|
|
if _, deleted := deleteSet[name]; deleted {
|
|
continue
|
|
}
|
|
ordered = append(ordered, name)
|
|
seenExisting[name] = struct{}{}
|
|
}
|
|
}
|
|
for _, name := range content.Scans() {
|
|
if _, deleted := deleteSet[name]; deleted {
|
|
continue
|
|
}
|
|
if _, seen := seenExisting[name]; seen {
|
|
continue
|
|
}
|
|
ordered = append(ordered, name)
|
|
}
|
|
content.Set(dbmodels.SCAN_FIELD, ordered)
|
|
} else {
|
|
if len(uploadedScans) > 0 {
|
|
content.Set(dbmodels.SCAN_FIELD+"+", uploadedScans)
|
|
}
|
|
if len(deleteScans) > 0 {
|
|
for _, scan := range deleteScans {
|
|
scan = strings.TrimSpace(scan)
|
|
if scan == "" {
|
|
continue
|
|
}
|
|
content.Set(dbmodels.SCAN_FIELD+"-", scan)
|
|
}
|
|
}
|
|
}
|
|
if user != nil {
|
|
content.SetEditor(user.Id)
|
|
}
|
|
if err := tx.Save(content); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
createdContents = append(createdContents, created...)
|
|
updatedContents = append(updatedContents, contents...)
|
|
updatedContents = append(updatedContents, created...)
|
|
return nil
|
|
}); err != nil {
|
|
app.Logger().Error("Failed to save contents", "entry_id", entry.Id, "error", err)
|
|
return renderError(err.Error())
|
|
}
|
|
|
|
if len(updatedContents) == 0 {
|
|
updatedContents = contents
|
|
}
|
|
shouldUpdateFTS := len(contentInputs) > 0 || len(newContentIDs) > 0
|
|
if shouldUpdateFTS {
|
|
touched := updatedContents
|
|
if len(contentInputs) > 0 {
|
|
touchedIDs := map[string]struct{}{}
|
|
for id := range contentInputs {
|
|
if createdID, ok := tempToCreated[id]; ok {
|
|
touchedIDs[createdID] = struct{}{}
|
|
continue
|
|
}
|
|
touchedIDs[id] = struct{}{}
|
|
}
|
|
filtered := make([]*dbmodels.Content, 0, len(touchedIDs))
|
|
for _, content := range updatedContents {
|
|
if _, ok := touchedIDs[content.Id]; ok {
|
|
filtered = append(filtered, content)
|
|
}
|
|
}
|
|
if len(filtered) > 0 {
|
|
touched = filtered
|
|
}
|
|
}
|
|
go updateContentsFTS5(app, entry, touched)
|
|
}
|
|
|
|
saveAction := strings.TrimSpace(e.Request.FormValue("save_action"))
|
|
savedMessage := "Änderungen gespeichert."
|
|
if contentID != "" {
|
|
effectiveContentID := contentID
|
|
if mappedID, ok := tempToCreated[effectiveContentID]; ok {
|
|
effectiveContentID = mappedID
|
|
}
|
|
if effectiveContentID != "" {
|
|
if resolved, err := dbmodels.Contents_IDs(app, []any{effectiveContentID}); err == nil && len(resolved) > 0 {
|
|
if saveAction == "view" {
|
|
redirect := fmt.Sprintf("/beitrag/%d", resolved[0].MusenalmID())
|
|
return e.Redirect(http.StatusSeeOther, redirect)
|
|
}
|
|
setFlashSuccess(e, savedMessage)
|
|
redirect := fmt.Sprintf("/almanach/%s/contents/%d/edit", id, resolved[0].MusenalmID())
|
|
return e.Redirect(http.StatusSeeOther, redirect)
|
|
}
|
|
}
|
|
}
|
|
setFlashSuccess(e, savedMessage)
|
|
redirect := fmt.Sprintf("/almanach/%s/contents/edit", id)
|
|
return e.Redirect(http.StatusSeeOther, redirect)
|
|
}
|
|
}
|
|
|
|
func (p *AlmanachContentsEditPage) POSTUpdateExtent(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)
|
|
}
|
|
|
|
entry.SetExtent(strings.TrimSpace(e.Request.FormValue("extent")))
|
|
if user := req.User(); user != nil {
|
|
entry.SetEditor(user.Id)
|
|
}
|
|
if err := app.Save(entry); err != nil {
|
|
app.Logger().Error("Failed to update entry extent", "entry_id", entry.Id, "error", err)
|
|
return p.renderError(engine, app, e, "Struktur/Umfang konnte nicht gespeichert werden.")
|
|
}
|
|
|
|
InvalidateSortedEntriesCache()
|
|
|
|
go func(appInstance core.App, entryRecord *dbmodels.Entry) {
|
|
if err := updateEntryFTS5WithContents(appInstance, entryRecord, false); err != nil {
|
|
appInstance.Logger().Error("Failed to update entry FTS5", "entry_id", entryRecord.Id, "error", err)
|
|
}
|
|
}(app, entry)
|
|
|
|
setFlashSuccess(e, "Struktur/Umfang gespeichert.")
|
|
redirect := fmt.Sprintf("/almanach/%s/contents/edit", id)
|
|
return e.Redirect(http.StatusSeeOther, redirect)
|
|
}
|
|
}
|
|
|
|
func (p *AlmanachContentsEditPage) POSTDelete(engine *templating.Engine, app core.App) HandleFunc {
|
|
return func(e *core.RequestEvent) error {
|
|
id := e.Request.PathValue("id")
|
|
req := templating.NewRequest(e)
|
|
isHTMX := strings.EqualFold(e.Request.Header.Get("HX-Request"), "true")
|
|
|
|
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())
|
|
}
|
|
|
|
contentID := strings.TrimSpace(e.Request.FormValue("content_id"))
|
|
if contentID == "" {
|
|
return p.renderError(engine, app, e, "Beitrag konnte nicht gelöscht werden.")
|
|
}
|
|
|
|
entry, err := dbmodels.Entries_MusenalmID(app, id)
|
|
if err != nil {
|
|
return engine.Response404(e, err, nil)
|
|
}
|
|
|
|
var remaining []*dbmodels.Content
|
|
if err := app.RunInTransaction(func(tx core.App) error {
|
|
record, err := tx.FindRecordById(dbmodels.CONTENTS_TABLE, contentID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
content := dbmodels.NewContent(record)
|
|
if content.Entry() != entry.Id {
|
|
return fmt.Errorf("Beitrag gehört zu einem anderen Band.")
|
|
}
|
|
|
|
relationsTable := dbmodels.RelationTableName(dbmodels.CONTENTS_TABLE, dbmodels.AGENTS_TABLE)
|
|
relations, err := dbmodels.RContentsAgents_Content(tx, contentID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, rel := range relations {
|
|
relRecord, err := tx.FindRecordById(relationsTable, rel.Id)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if err := tx.Delete(relRecord); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if err := tx.Delete(record); err != nil {
|
|
return err
|
|
}
|
|
|
|
remaining, err = dbmodels.Contents_Entry(tx, entry.Id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
dbmodels.Sort_Contents_Numbering(remaining)
|
|
for idx, content := range remaining {
|
|
content.SetNumbering(float64(idx + 1))
|
|
if err := tx.Save(content); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}); err != nil {
|
|
app.Logger().Error("Failed to delete content", "entry_id", entry.Id, "content_id", contentID, "error", err)
|
|
return p.renderError(engine, app, e, "Beitrag konnte nicht gelöscht werden.")
|
|
}
|
|
|
|
go func(contentID string) {
|
|
_ = dbmodels.DeleteFTS5Content(app, contentID)
|
|
}(contentID)
|
|
|
|
// Only delete the FTS5 record for the removed content.
|
|
|
|
if isHTMX {
|
|
success := `<div hx-swap-oob="innerHTML:#user-message"><div class="text-green-800 text-sm mt-2 rounded-xs bg-green-200 p-2 font-bold border-green-700 shadow border mb-3"><i class="ri-checkbox-circle-fill"></i> Beitrag geloescht.</div></div>`
|
|
return e.HTML(http.StatusOK, success)
|
|
}
|
|
|
|
redirect := fmt.Sprintf("/almanach/%s/contents/edit", id)
|
|
return e.Redirect(http.StatusSeeOther, redirect)
|
|
}
|
|
}
|
|
|
|
func (p *AlmanachContentsEditPage) POSTUploadScans(engine *templating.Engine, app core.App) HandleFunc {
|
|
return func(e *core.RequestEvent) error {
|
|
id := e.Request.PathValue("id")
|
|
req := templating.NewRequest(e)
|
|
isHTMX := strings.EqualFold(e.Request.Header.Get("HX-Request"), "true")
|
|
|
|
if e.Request.MultipartForm == nil {
|
|
if err := e.Request.ParseMultipartForm(router.DefaultMaxMemory); err != nil {
|
|
return renderContentsImagesHTMXError(e, "Upload fehlgeschlagen.", isHTMX)
|
|
}
|
|
}
|
|
|
|
if err := req.CheckCSRF(e.Request.FormValue("csrf_token")); err != nil {
|
|
return renderContentsImagesHTMXError(e, "Upload fehlgeschlagen.", isHTMX)
|
|
}
|
|
|
|
contentID := strings.TrimSpace(e.Request.FormValue("content_id"))
|
|
if contentID == "" || strings.HasPrefix(contentID, "tmp") {
|
|
return renderContentsImagesHTMXError(e, "Upload fehlgeschlagen.", isHTMX)
|
|
}
|
|
|
|
entry, err := dbmodels.Entries_MusenalmID(app, id)
|
|
if err != nil {
|
|
return engine.Response404(e, err, nil)
|
|
}
|
|
|
|
contents, err := dbmodels.Contents_IDs(app, []any{contentID})
|
|
if err != nil || len(contents) == 0 {
|
|
return renderContentsImagesHTMXError(e, "Beitrag nicht gefunden.", isHTMX)
|
|
}
|
|
content := contents[0]
|
|
if content.Entry() != entry.Id {
|
|
return renderContentsImagesHTMXError(e, "Beitrag nicht gefunden.", isHTMX)
|
|
}
|
|
|
|
files, err := e.FindUploadedFiles(dbmodels.SCAN_FIELD)
|
|
if err != nil || len(files) == 0 {
|
|
return renderContentsImagesHTMXError(e, "Bitte eine Datei auswaehlen.", isHTMX)
|
|
}
|
|
|
|
content.Set(dbmodels.SCAN_FIELD+"+", files)
|
|
if user := req.User(); user != nil {
|
|
content.SetEditor(user.Id)
|
|
}
|
|
if err := app.Save(content); err != nil {
|
|
app.Logger().Error("Failed to upload scans", "entry_id", entry.Id, "content_id", content.Id, "error", err)
|
|
return renderContentsImagesHTMXError(e, "Upload fehlgeschlagen.", isHTMX)
|
|
}
|
|
|
|
if !isHTMX {
|
|
redirect := fmt.Sprintf("/almanach/%s/contents/edit", id)
|
|
return e.Redirect(http.StatusSeeOther, redirect)
|
|
}
|
|
|
|
if refreshed, err := dbmodels.Contents_IDs(app, []any{content.Id}); err == nil && len(refreshed) > 0 {
|
|
content = refreshed[0]
|
|
}
|
|
|
|
data := map[string]any{
|
|
"content": content,
|
|
"entry": entry,
|
|
"csrf_token": req.Session().Token,
|
|
"is_new": false,
|
|
}
|
|
var builder strings.Builder
|
|
if err := engine.Render(&builder, TEMPLATE_ALMANACH_CONTENTS_IMAGES_PANEL, data, "fragment"); err != nil {
|
|
app.Logger().Error("Failed to render images panel", "entry_id", entry.Id, "content_id", content.Id, "error", err)
|
|
return e.String(http.StatusInternalServerError, "")
|
|
}
|
|
|
|
success := `<div hx-swap-oob="innerHTML:#user-message"><div class="text-green-800 text-sm mt-2 rounded-xs bg-green-200 p-2 font-bold border-green-700 shadow border mb-3"><i class="ri-checkbox-circle-fill"></i> Digitalisat gespeichert.</div></div>`
|
|
countOOB := renderContentImagesCountOOB(content)
|
|
return e.HTML(http.StatusOK, builder.String()+success+countOOB)
|
|
}
|
|
}
|
|
|
|
func (p *AlmanachContentsEditPage) POSTDeleteScan(engine *templating.Engine, app core.App) HandleFunc {
|
|
return func(e *core.RequestEvent) error {
|
|
id := e.Request.PathValue("id")
|
|
req := templating.NewRequest(e)
|
|
isHTMX := strings.EqualFold(e.Request.Header.Get("HX-Request"), "true")
|
|
|
|
if err := e.Request.ParseForm(); err != nil {
|
|
return renderContentsImagesHTMXError(e, "Loeschen fehlgeschlagen.", isHTMX)
|
|
}
|
|
|
|
if err := req.CheckCSRF(e.Request.FormValue("csrf_token")); err != nil {
|
|
return renderContentsImagesHTMXError(e, "Loeschen fehlgeschlagen.", isHTMX)
|
|
}
|
|
|
|
contentID := strings.TrimSpace(e.Request.FormValue("content_id"))
|
|
scan := strings.TrimSpace(e.Request.FormValue("scan"))
|
|
if contentID == "" || scan == "" {
|
|
return renderContentsImagesHTMXError(e, "Loeschen fehlgeschlagen.", isHTMX)
|
|
}
|
|
|
|
entry, err := dbmodels.Entries_MusenalmID(app, id)
|
|
if err != nil {
|
|
return engine.Response404(e, err, nil)
|
|
}
|
|
|
|
contents, err := dbmodels.Contents_IDs(app, []any{contentID})
|
|
if err != nil || len(contents) == 0 {
|
|
return renderContentsImagesHTMXError(e, "Beitrag nicht gefunden.", isHTMX)
|
|
}
|
|
content := contents[0]
|
|
if content.Entry() != entry.Id {
|
|
return renderContentsImagesHTMXError(e, "Beitrag nicht gefunden.", isHTMX)
|
|
}
|
|
if !slices.Contains(content.Scans(), scan) {
|
|
return renderContentsImagesHTMXError(e, "Datei nicht gefunden.", isHTMX)
|
|
}
|
|
|
|
content.Set(dbmodels.SCAN_FIELD+"-", scan)
|
|
if user := req.User(); user != nil {
|
|
content.SetEditor(user.Id)
|
|
}
|
|
if err := app.Save(content); err != nil {
|
|
app.Logger().Error("Failed to delete scan", "entry_id", entry.Id, "content_id", content.Id, "scan", scan, "error", err)
|
|
return renderContentsImagesHTMXError(e, "Loeschen fehlgeschlagen.", isHTMX)
|
|
}
|
|
|
|
if !isHTMX {
|
|
redirect := fmt.Sprintf("/almanach/%s/contents/edit", id)
|
|
return e.Redirect(http.StatusSeeOther, redirect)
|
|
}
|
|
|
|
if refreshed, err := dbmodels.Contents_IDs(app, []any{content.Id}); err == nil && len(refreshed) > 0 {
|
|
content = refreshed[0]
|
|
}
|
|
|
|
data := map[string]any{
|
|
"content": content,
|
|
"entry": entry,
|
|
"csrf_token": req.Session().Token,
|
|
"is_new": false,
|
|
}
|
|
var builder strings.Builder
|
|
if err := engine.Render(&builder, TEMPLATE_ALMANACH_CONTENTS_IMAGES_PANEL, data, "fragment"); err != nil {
|
|
app.Logger().Error("Failed to render images panel", "entry_id", entry.Id, "content_id", content.Id, "error", err)
|
|
return e.String(http.StatusInternalServerError, "")
|
|
}
|
|
|
|
success := `<div hx-swap-oob="innerHTML:#user-message"><div class="text-green-800 text-sm mt-2 rounded-xs bg-green-200 p-2 font-bold border-green-700 shadow border mb-3"><i class="ri-checkbox-circle-fill"></i> Digitalisat geloescht.</div></div>`
|
|
countOOB := renderContentImagesCountOOB(content)
|
|
return e.HTML(http.StatusOK, builder.String()+success+countOOB)
|
|
}
|
|
}
|
|
|
|
func renderContentImagesCountOOB(content *dbmodels.Content) string {
|
|
if content == nil {
|
|
return ""
|
|
}
|
|
count := len(content.Scans())
|
|
hiddenClass := ""
|
|
if count == 0 {
|
|
hiddenClass = " hidden"
|
|
}
|
|
return fmt.Sprintf(
|
|
`<span hx-swap-oob="outerHTML" id="content-%s-images-count" class="inline-flex items-center gap-1 text-sm font-semibold text-slate-600 mr-2.5%s"><i class="ri-image-line"></i><span>%d</span></span>`,
|
|
content.Id,
|
|
hiddenClass,
|
|
count,
|
|
)
|
|
}
|
|
|
|
func renderContentsImagesHTMXError(e *core.RequestEvent, message string, isHTMX bool) error {
|
|
if !isHTMX {
|
|
return e.String(http.StatusBadRequest, message)
|
|
}
|
|
e.Response.Header().Set("HX-Reswap", "none")
|
|
payload := fmt.Sprintf(
|
|
`<div hx-swap-oob="innerHTML:#user-message"><div class="text-red-800 text-sm mt-2 rounded-xs bg-red-200 p-2 font-bold border-red-700 shadow border mb-3"><i class="ri-error-warning-fill"></i> %s</div></div>`,
|
|
message,
|
|
)
|
|
return e.HTML(http.StatusBadRequest, payload)
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
type contentAgentRelationPayload struct {
|
|
ID string
|
|
TargetID string
|
|
Type string
|
|
Uncertain bool
|
|
}
|
|
|
|
type contentAgentRelationsPayload struct {
|
|
Relations []contentAgentRelationPayload
|
|
NewRelations []contentAgentRelationPayload
|
|
DeletedIDs []string
|
|
}
|
|
|
|
type contentAgentRender struct {
|
|
Id string
|
|
Agent string
|
|
Type string
|
|
Uncertain bool
|
|
}
|
|
|
|
func valuesForKey(form url.Values, key string) []string {
|
|
if values, ok := form[key]; ok {
|
|
return values
|
|
}
|
|
if values, ok := form[key+"[]"]; ok {
|
|
return values
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func hasKey(form url.Values, key string) bool {
|
|
_, ok := form[key]
|
|
return ok
|
|
}
|
|
|
|
func parseContentAgentRelations(form url.Values, contentID string) contentAgentRelationsPayload {
|
|
payload := contentAgentRelationsPayload{}
|
|
if contentID == "" {
|
|
return payload
|
|
}
|
|
prefix := fmt.Sprintf("content_%s_agents_", contentID)
|
|
idPrefix := prefix + "id["
|
|
|
|
for key, values := range form {
|
|
if !strings.HasPrefix(key, idPrefix) {
|
|
continue
|
|
}
|
|
relationKey := strings.TrimSuffix(strings.TrimPrefix(key, idPrefix), "]")
|
|
relationID := strings.TrimSpace(firstValue(values))
|
|
if relationKey == "" || relationID == "" {
|
|
continue
|
|
}
|
|
targetKey := fmt.Sprintf("%sagent[%s]", prefix, relationKey)
|
|
typeKey := fmt.Sprintf("%stype[%s]", prefix, relationKey)
|
|
deleteKey := fmt.Sprintf("%sdelete[%s]", prefix, relationKey)
|
|
uncertainKey := fmt.Sprintf("%suncertain[%s]", prefix, relationKey)
|
|
|
|
targetID := strings.TrimSpace(firstValue(valuesForKey(form, targetKey)))
|
|
if targetID == "" {
|
|
continue
|
|
}
|
|
if _, ok := form[deleteKey]; ok {
|
|
payload.DeletedIDs = append(payload.DeletedIDs, relationID)
|
|
continue
|
|
}
|
|
payload.Relations = append(payload.Relations, contentAgentRelationPayload{
|
|
ID: relationID,
|
|
TargetID: targetID,
|
|
Type: strings.TrimSpace(firstValue(valuesForKey(form, typeKey))),
|
|
Uncertain: hasKey(form, uncertainKey),
|
|
})
|
|
}
|
|
|
|
newIDs := valuesForKey(form, prefix+"new_id")
|
|
newTypes := valuesForKey(form, prefix+"new_type")
|
|
newUncertain := valuesForKey(form, prefix+"new_uncertain")
|
|
uncertainSet := map[string]struct{}{}
|
|
for _, value := range newUncertain {
|
|
trimmed := strings.TrimSpace(value)
|
|
if trimmed == "" {
|
|
continue
|
|
}
|
|
uncertainSet[trimmed] = struct{}{}
|
|
}
|
|
|
|
for index, targetID := range newIDs {
|
|
targetID = strings.TrimSpace(targetID)
|
|
if targetID == "" {
|
|
continue
|
|
}
|
|
relationType := ""
|
|
if index < len(newTypes) {
|
|
relationType = strings.TrimSpace(newTypes[index])
|
|
}
|
|
_, uncertain := uncertainSet[targetID]
|
|
payload.NewRelations = append(payload.NewRelations, contentAgentRelationPayload{
|
|
TargetID: targetID,
|
|
Type: relationType,
|
|
Uncertain: uncertain,
|
|
})
|
|
}
|
|
|
|
return payload
|
|
}
|
|
|
|
func validateContentAgentRelations(payload contentAgentRelationsPayload) error {
|
|
for _, relation := range payload.Relations {
|
|
if err := validateRelationTypeValue(relation.Type, dbmodels.AGENT_RELATIONS); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for _, relation := range payload.NewRelations {
|
|
if err := validateRelationTypeValue(relation.Type, dbmodels.AGENT_RELATIONS); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateRelationTypeValue(value string, allowed []string) error {
|
|
value = strings.TrimSpace(value)
|
|
if value == "" {
|
|
return fmt.Errorf("Ungültiger Beziehungstyp.")
|
|
}
|
|
if !slices.Contains(allowed, value) {
|
|
return fmt.Errorf("Ungültiger Beziehungstyp.")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func applyContentForm(content *dbmodels.Content, entry *dbmodels.Entry, fields map[string][]string, user *dbmodels.FixedUser, numbering float64) error {
|
|
preferredTitle := buildContentPreferredTitle(content, fields)
|
|
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)
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
musenalmTypes := sanitizeContentStrings(fields["musenalm_type"])
|
|
if len(musenalmTypes) == 0 {
|
|
return fmt.Errorf("Musenalm-Typ ist erforderlich (Beitrag %s).", content.Id)
|
|
}
|
|
|
|
if numbering <= 0 {
|
|
numbering = content.Numbering()
|
|
}
|
|
|
|
content.SetPreferredTitle(preferredTitle)
|
|
if value, ok := optionalFieldValue(fields, "variant_title"); ok {
|
|
content.SetVariantTitle(value)
|
|
}
|
|
if value, ok := optionalFieldValue(fields, "parallel_title"); ok {
|
|
content.SetParallelTitle(value)
|
|
}
|
|
if value, ok := optionalFieldValue(fields, "title_statement"); ok {
|
|
content.SetTitleStmt(value)
|
|
}
|
|
if value, ok := optionalFieldValue(fields, "subtitle_statement"); ok {
|
|
content.SetSubtitleStmt(value)
|
|
}
|
|
if value, ok := optionalFieldValue(fields, "incipit_statement"); ok {
|
|
content.SetIncipitStmt(value)
|
|
}
|
|
if value, ok := optionalFieldValue(fields, "responsibility_statement"); ok {
|
|
content.SetResponsibilityStmt(value)
|
|
}
|
|
if value, ok := optionalFieldValue(fields, "place_statement"); ok {
|
|
content.SetPlaceStmt(value)
|
|
}
|
|
content.SetYear(entry.Year())
|
|
content.SetExtent(strings.TrimSpace(firstValue(fields["extent"])))
|
|
content.SetLanguage(sanitizeContentStrings(fields["language"]))
|
|
if values, ok := fields["content_type"]; ok {
|
|
content.SetContentType(sanitizeContentStrings(values))
|
|
}
|
|
content.SetMusenalmType(musenalmTypes)
|
|
content.SetMusenalmPagination(strings.TrimSpace(firstValue(fields["musenalm_pagination"])))
|
|
content.SetNumbering(numbering)
|
|
content.SetEntry(entry.Id)
|
|
content.SetEditState(status)
|
|
if value, ok := optionalFieldValue(fields, "edit_comment"); ok {
|
|
content.SetComment(value)
|
|
}
|
|
if value, ok := optionalFieldValue(fields, "annotation"); ok {
|
|
content.SetAnnotation(value)
|
|
}
|
|
if user != nil {
|
|
content.SetEditor(user.Id)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func applyContentAgentRelations(tx core.App, content *dbmodels.Content, payload contentAgentRelationsPayload) error {
|
|
if content == nil {
|
|
return nil
|
|
}
|
|
tableName := dbmodels.RelationTableName(dbmodels.CONTENTS_TABLE, dbmodels.AGENTS_TABLE)
|
|
var collection *core.Collection
|
|
getCollection := func() (*core.Collection, error) {
|
|
if collection != nil {
|
|
return collection, nil
|
|
}
|
|
col, err := tx.FindCollectionByNameOrId(tableName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
collection = col
|
|
return collection, nil
|
|
}
|
|
|
|
for _, relation := range payload.Relations {
|
|
relationID := strings.TrimSpace(relation.ID)
|
|
if relationID == "" {
|
|
continue
|
|
}
|
|
record, err := tx.FindRecordById(tableName, relationID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
proxy := dbmodels.NewRContentsAgents(record)
|
|
if proxy.Content() != content.Id {
|
|
return fmt.Errorf("Relation %s gehört zu einem anderen Beitrag.", relationID)
|
|
}
|
|
proxy.SetContent(content.Id)
|
|
proxy.SetAgent(strings.TrimSpace(relation.TargetID))
|
|
proxy.SetType(strings.TrimSpace(relation.Type))
|
|
proxy.SetUncertain(relation.Uncertain)
|
|
if err := tx.Save(proxy); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
for _, relationID := range payload.DeletedIDs {
|
|
relationID = strings.TrimSpace(relationID)
|
|
if relationID == "" {
|
|
continue
|
|
}
|
|
record, err := tx.FindRecordById(tableName, relationID)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
proxy := dbmodels.NewRContentsAgents(record)
|
|
if proxy.Content() != content.Id {
|
|
continue
|
|
}
|
|
if err := tx.Delete(record); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
for _, relation := range payload.NewRelations {
|
|
targetID := strings.TrimSpace(relation.TargetID)
|
|
if targetID == "" {
|
|
continue
|
|
}
|
|
col, err := getCollection()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
proxy := dbmodels.NewRContentsAgents(core.NewRecord(col))
|
|
proxy.SetContent(content.Id)
|
|
proxy.SetAgent(targetID)
|
|
proxy.SetType(strings.TrimSpace(relation.Type))
|
|
proxy.SetUncertain(relation.Uncertain)
|
|
if err := tx.Save(proxy); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func applyContentFormDraft(content *dbmodels.Content, entry *dbmodels.Entry, fields map[string][]string, numbering float64) {
|
|
if value, ok := optionalFieldValue(fields, "variant_title"); ok {
|
|
content.SetVariantTitle(value)
|
|
}
|
|
if value, ok := optionalFieldValue(fields, "parallel_title"); ok {
|
|
content.SetParallelTitle(value)
|
|
}
|
|
if value, ok := optionalFieldValue(fields, "title_statement"); ok {
|
|
content.SetTitleStmt(value)
|
|
}
|
|
if value, ok := optionalFieldValue(fields, "subtitle_statement"); ok {
|
|
content.SetSubtitleStmt(value)
|
|
}
|
|
if value, ok := optionalFieldValue(fields, "incipit_statement"); ok {
|
|
content.SetIncipitStmt(value)
|
|
}
|
|
if value, ok := optionalFieldValue(fields, "responsibility_statement"); ok {
|
|
content.SetResponsibilityStmt(value)
|
|
}
|
|
if value, ok := optionalFieldValue(fields, "place_statement"); ok {
|
|
content.SetPlaceStmt(value)
|
|
}
|
|
if value, ok := optionalFieldValue(fields, "edit_comment"); ok {
|
|
content.SetComment(value)
|
|
}
|
|
if value, ok := optionalFieldValue(fields, "annotation"); ok {
|
|
content.SetAnnotation(value)
|
|
}
|
|
|
|
content.SetExtent(strings.TrimSpace(firstValue(fields["extent"])))
|
|
content.SetLanguage(sanitizeContentStrings(fields["language"]))
|
|
if values, ok := fields["content_type"]; ok {
|
|
content.SetContentType(sanitizeContentStrings(values))
|
|
}
|
|
content.SetMusenalmType(sanitizeContentStrings(fields["musenalm_type"]))
|
|
content.SetMusenalmPagination(strings.TrimSpace(firstValue(fields["musenalm_pagination"])))
|
|
content.SetEntry(entry.Id)
|
|
content.SetYear(entry.Year())
|
|
|
|
if status := strings.TrimSpace(firstValue(fields["edit_state"])); status != "" {
|
|
content.SetEditState(status)
|
|
}
|
|
if numbering > 0 {
|
|
content.SetNumbering(numbering)
|
|
}
|
|
if preferredTitle := buildContentPreferredTitle(content, fields); preferredTitle != "" {
|
|
content.SetPreferredTitle(preferredTitle)
|
|
}
|
|
}
|
|
|
|
func sanitizeContentStrings(values []string) []string {
|
|
cleaned := make([]string, 0, len(values))
|
|
seen := map[string]struct{}{}
|
|
for _, value := range values {
|
|
value = strings.TrimSpace(value)
|
|
if value == "" {
|
|
continue
|
|
}
|
|
if _, ok := seen[value]; ok {
|
|
continue
|
|
}
|
|
seen[value] = struct{}{}
|
|
cleaned = append(cleaned, value)
|
|
}
|
|
return cleaned
|
|
}
|
|
|
|
func buildContentPreferredTitle(content *dbmodels.Content, fields map[string][]string) string {
|
|
title := fieldOrCurrent(fields, "title_statement", content.TitleStmt())
|
|
if title != "" {
|
|
return title
|
|
}
|
|
subtitle := fieldOrCurrent(fields, "subtitle_statement", content.SubtitleStmt())
|
|
if subtitle != "" {
|
|
return subtitle
|
|
}
|
|
incipit := fieldOrCurrent(fields, "incipit_statement", content.IncipitStmt())
|
|
if incipit != "" {
|
|
return incipit
|
|
}
|
|
|
|
types := fields["musenalm_type"]
|
|
if _, ok := fields["musenalm_type"]; !ok {
|
|
types = content.MusenalmType()
|
|
}
|
|
types = sanitizeContentStrings(types)
|
|
typeLabel := strings.Join(types, ", ")
|
|
responsibility := fieldOrCurrent(fields, "responsibility_statement", content.ResponsibilityStmt())
|
|
if responsibility != "" && !strings.EqualFold(responsibility, "unbezeichnet") {
|
|
if typeLabel != "" {
|
|
return fmt.Sprintf("[%s] Unterzeichnet: %s", typeLabel, responsibility)
|
|
}
|
|
return fmt.Sprintf("Unterzeichnet: %s", responsibility)
|
|
}
|
|
|
|
extent := fieldOrCurrent(fields, "extent", content.Extent())
|
|
if typeLabel == "" {
|
|
typeLabel = "Beitrag"
|
|
}
|
|
if extent != "" {
|
|
return fmt.Sprintf("[%s %s]", typeLabel, extent)
|
|
}
|
|
return fmt.Sprintf("[%s]", typeLabel)
|
|
}
|
|
|
|
func optionalFieldValue(fields map[string][]string, key string) (string, bool) {
|
|
values, ok := fields[key]
|
|
if !ok {
|
|
return "", false
|
|
}
|
|
return strings.TrimSpace(firstValue(values)), true
|
|
}
|
|
|
|
func fieldOrCurrent(fields map[string][]string, key, current string) string {
|
|
if value, ok := optionalFieldValue(fields, key); ok {
|
|
return value
|
|
}
|
|
return strings.TrimSpace(current)
|
|
}
|
|
|
|
func firstValue(values []string) string {
|
|
if len(values) == 0 {
|
|
return ""
|
|
}
|
|
return values[0]
|
|
}
|
|
|
|
func parseContentsOrder(form url.Values) []string {
|
|
raw := form["content_order[]"]
|
|
if len(raw) == 0 {
|
|
raw = form["content_order"]
|
|
}
|
|
order := make([]string, 0, len(raw))
|
|
for _, value := range raw {
|
|
trimmed := strings.TrimSpace(value)
|
|
if trimmed == "" {
|
|
continue
|
|
}
|
|
order = append(order, trimmed)
|
|
}
|
|
return order
|
|
}
|
|
|
|
func buildContentOrderMap(order []string) map[string]float64 {
|
|
orderMap := make(map[string]float64, len(order))
|
|
for index, id := range order {
|
|
orderMap[id] = float64(index + 1)
|
|
}
|
|
return orderMap
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
func paginationValuesSorted() []string {
|
|
orderedKeys := []string{"", "ar", "röm", "alph", "sonst"}
|
|
for i := 1; i <= 8; i++ {
|
|
orderedKeys = append(orderedKeys, fmt.Sprintf("ar%d", i))
|
|
}
|
|
for i := 1; i <= 8; i++ {
|
|
orderedKeys = append(orderedKeys, fmt.Sprintf("röm%d", i))
|
|
}
|
|
|
|
values := make([]string, 0, len(dbmodels.MUSENALM_PAGINATION_VALUES))
|
|
seen := map[string]struct{}{}
|
|
for _, key := range orderedKeys {
|
|
value, ok := dbmodels.MUSENALM_PAGINATION_VALUES[key]
|
|
if !ok {
|
|
continue
|
|
}
|
|
if _, exists := seen[value]; exists {
|
|
continue
|
|
}
|
|
seen[value] = struct{}{}
|
|
values = append(values, value)
|
|
}
|
|
|
|
remainingKeys := slices.Collect(maps.Keys(dbmodels.MUSENALM_PAGINATION_VALUES))
|
|
sort.Strings(remainingKeys)
|
|
for _, key := range remainingKeys {
|
|
value := dbmodels.MUSENALM_PAGINATION_VALUES[key]
|
|
if _, exists := seen[value]; exists {
|
|
continue
|
|
}
|
|
seen[value] = struct{}{}
|
|
values = append(values, value)
|
|
}
|
|
|
|
return values
|
|
}
|