Further changes to the bände list

This commit is contained in:
Simon Martens
2026-01-25 00:24:54 +01:00
parent 4fc97139c8
commit d3dbefa73e
5 changed files with 923 additions and 563 deletions

View File

@@ -3,9 +3,11 @@ package controllers
import (
"net/url"
"strings"
"unicode/utf8"
"github.com/Theodor-Springmann-Stiftung/musenalm/app"
"github.com/Theodor-Springmann-Stiftung/musenalm/dbmodels"
"github.com/Theodor-Springmann-Stiftung/musenalm/helpers/datatypes"
"github.com/Theodor-Springmann-Stiftung/musenalm/middleware"
"github.com/Theodor-Springmann-Stiftung/musenalm/pagemodels"
"github.com/Theodor-Springmann-Stiftung/musenalm/templating"
@@ -15,8 +17,9 @@ import (
)
const (
URL_BAENDE = "/baende/"
TEMPLATE_BAENDE = "/baende/"
URL_BAENDE = "/baende/"
URL_BAENDE_RESULTS = "/baende/results/"
TEMPLATE_BAENDE = "/baende/"
)
func init() {
@@ -49,115 +52,408 @@ func (p *BaendePage) Setup(router *router.Router[*core.RequestEvent], ia pagemod
app := ia.Core()
rg := router.Group(URL_BAENDE)
rg.BindFunc(middleware.Authenticated(app))
rg.GET("", func(e *core.RequestEvent) error {
rg.GET("", p.handlePage(engine, app))
rg.GET("results/", p.handleResults(engine, app))
return nil
}
func (p *BaendePage) handlePage(engine *templating.Engine, app core.App) HandleFunc {
return func(e *core.RequestEvent) error {
req := templating.NewRequest(e)
if req.User() == nil {
redirectTo := url.QueryEscape(req.FullURL())
return e.Redirect(303, "/login/?redirectTo="+redirectTo)
}
letter := strings.ToUpper(strings.TrimSpace(e.Request.URL.Query().Get("letter")))
if letter == "" {
letter = "A"
data, err := p.buildResultData(app, e, req)
if err != nil {
return engine.Response404(e, err, data)
}
if len(letter) > 1 {
letter = letter[:1]
}
if letter < "A" || letter > "Z" {
letter = "A"
return engine.Response200(e, p.Template, data, p.Layout)
}
}
func (p *BaendePage) handleResults(engine *templating.Engine, app core.App) HandleFunc {
return func(e *core.RequestEvent) error {
req := templating.NewRequest(e)
if req.User() == nil {
redirectTo := url.QueryEscape(req.FullURL())
return e.Redirect(303, "/login/?redirectTo="+redirectTo)
}
entries := []*dbmodels.Entry{}
data, err := p.buildResultData(app, e, req)
if err != nil {
return engine.Response404(e, err, data)
}
return engine.Response200(e, URL_BAENDE_RESULTS, data, "fragment")
}
}
func (p *BaendePage) buildResultData(app core.App, e *core.RequestEvent, req *templating.Request) (map[string]any, error) {
data := map[string]any{}
search := strings.TrimSpace(e.Request.URL.Query().Get("search"))
if search != "" {
data["search"] = search
}
letter := strings.ToUpper(strings.TrimSpace(e.Request.URL.Query().Get("letter")))
if letter == "" {
letter = "A"
}
if len(letter) > 1 {
letter = letter[:1]
}
if letter < "A" || letter > "Z" {
letter = "A"
}
entries := []*dbmodels.Entry{}
if search != "" {
var err error
entries, err = searchBaendeEntries(app, search)
if err != nil {
return data, err
}
} else {
if err := app.RecordQuery(dbmodels.ENTRIES_TABLE).
Where(dbx.Like(dbmodels.PREFERRED_TITLE_FIELD, letter).Match(false, true)).
OrderBy(dbmodels.PREFERRED_TITLE_FIELD).
All(&entries); err != nil {
return engine.Response500(e, err, nil)
return data, err
}
}
entryIDs := []any{}
for _, entry := range entries {
entryIDs = append(entryIDs, entry.Id)
}
entryIDs := []any{}
for _, entry := range entries {
entryIDs = append(entryIDs, entry.Id)
}
seriesMap := map[string]*dbmodels.Series{}
entrySeriesMap := map[string][]*dbmodels.REntriesSeries{}
if len(entries) > 0 {
series, relations, err := Series_Entries(app, entries)
if err != nil {
return engine.Response500(e, err, nil)
}
for _, s := range series {
seriesMap[s.Id] = s
}
for _, r := range relations {
entrySeriesMap[r.Entry()] = append(entrySeriesMap[r.Entry()], r)
}
seriesMap := map[string]*dbmodels.Series{}
entrySeriesMap := map[string][]*dbmodels.REntriesSeries{}
if len(entries) > 0 {
series, relations, err := Series_Entries(app, entries)
if err != nil {
return data, err
}
for _, s := range series {
seriesMap[s.Id] = s
}
for _, r := range relations {
entrySeriesMap[r.Entry()] = append(entrySeriesMap[r.Entry()], r)
}
}
agentsMap := map[string]*dbmodels.Agent{}
entryAgentsMap := map[string][]*dbmodels.REntriesAgents{}
if len(entryIDs) > 0 {
agents, arelations, err := Agents_Entries_IDs(app, entryIDs)
if err != nil {
return engine.Response500(e, err, nil)
}
for _, a := range agents {
agentsMap[a.Id] = a
}
for _, r := range arelations {
entryAgentsMap[r.Entry()] = append(entryAgentsMap[r.Entry()], r)
}
agentsMap := map[string]*dbmodels.Agent{}
entryAgentsMap := map[string][]*dbmodels.REntriesAgents{}
if len(entryIDs) > 0 {
agents, arelations, err := Agents_Entries_IDs(app, entryIDs)
if err != nil {
return data, err
}
for _, a := range agents {
agentsMap[a.Id] = a
}
for _, r := range arelations {
entryAgentsMap[r.Entry()] = append(entryAgentsMap[r.Entry()], r)
}
}
placesMap := map[string]*dbmodels.Place{}
placesIDs := []any{}
for _, entry := range entries {
for _, placeID := range entry.Places() {
placesIDs = append(placesIDs, placeID)
}
placesMap := map[string]*dbmodels.Place{}
placesIDs := []any{}
for _, entry := range entries {
for _, placeID := range entry.Places() {
placesIDs = append(placesIDs, placeID)
}
if len(placesIDs) > 0 {
places, err := dbmodels.Places_IDs(app, placesIDs)
if err != nil {
return engine.Response500(e, err, nil)
}
for _, place := range places {
placesMap[place.Id] = place
}
}
if len(placesIDs) > 0 {
places, err := dbmodels.Places_IDs(app, placesIDs)
if err != nil {
return data, err
}
for _, place := range places {
placesMap[place.Id] = place
}
}
itemsMap := map[string][]*dbmodels.Item{}
for _, entry := range entries {
items, err := dbmodels.Items_Entry(app, entry.Id)
if err != nil {
return engine.Response500(e, err, nil)
}
if len(items) > 0 {
itemsMap[entry.Id] = items
}
itemsMap := map[string][]*dbmodels.Item{}
for _, entry := range entries {
items, err := dbmodels.Items_Entry(app, entry.Id)
if err != nil {
return data, err
}
if len(items) > 0 {
itemsMap[entry.Id] = items
}
}
letters := []string{
"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M",
"N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z",
}
letters := []string{
"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M",
"N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z",
}
data := map[string]any{
"result": &BaendeResult{
Entries: entries,
Series: seriesMap,
EntriesSeries: entrySeriesMap,
Places: placesMap,
Agents: agentsMap,
EntriesAgents: entryAgentsMap,
Items: itemsMap,
},
"letter": letter,
"letters": letters,
"csrf_token": req.Session().Token,
}
return engine.Response200(e, p.Template, data, p.Layout)
})
return nil
data["result"] = &BaendeResult{
Entries: entries,
Series: seriesMap,
EntriesSeries: entrySeriesMap,
Places: placesMap,
Agents: agentsMap,
EntriesAgents: entryAgentsMap,
Items: itemsMap,
}
data["letter"] = letter
data["letters"] = letters
data["csrf_token"] = req.Session().Token
return data, nil
}
func searchBaendeEntries(app core.App, search string) ([]*dbmodels.Entry, error) {
query := strings.TrimSpace(search)
if query == "" {
return []*dbmodels.Entry{}, nil
}
if utf8.RuneCountInString(query) < 3 {
return searchBaendeEntriesQuick(app, query)
}
entries, err := searchBaendeEntriesFTS(app, query)
if err != nil {
return searchBaendeEntriesLike(app, query)
}
return entries, nil
}
func searchBaendeEntriesFTS(app core.App, query string) ([]*dbmodels.Entry, error) {
entryIDs := map[string]struct{}{}
terms := dbmodels.NormalizeQuery(query)
entryRequests := dbmodels.IntoQueryRequests(dbmodels.ENTRIES_FTS5_FIELDS, terms)
if len(entryRequests) > 0 {
ids, err := dbmodels.FTS5Search(app, dbmodels.ENTRIES_TABLE, entryRequests...)
if err != nil {
return nil, err
}
for _, id := range ids {
entryIDs[id.ID] = struct{}{}
}
}
itemRequests := dbmodels.IntoQueryRequests(dbmodels.ITEMS_FTS5_FIELDS, terms)
if len(itemRequests) > 0 {
ids, err := dbmodels.FTS5Search(app, dbmodels.ITEMS_TABLE, itemRequests...)
if err != nil {
return nil, err
}
itemIDs := []any{}
for _, id := range ids {
itemIDs = append(itemIDs, id.ID)
}
if len(itemIDs) > 0 {
items, err := dbmodels.TableByIDs[*dbmodels.Item](app, dbmodels.ITEMS_TABLE, itemIDs)
if err != nil {
return nil, err
}
for _, item := range items {
if item == nil {
continue
}
if entryID := item.Entry(); entryID != "" {
entryIDs[entryID] = struct{}{}
}
}
}
}
if len(entryIDs) == 0 {
return []*dbmodels.Entry{}, nil
}
entryIDList := []any{}
for id := range entryIDs {
entryIDList = append(entryIDList, id)
}
entries, err := dbmodels.Entries_IDs(app, entryIDList)
if err != nil {
return nil, err
}
dbmodels.Sort_Entries_Title_Year(entries)
return entries, nil
}
func searchBaendeEntriesLike(app core.App, query string) ([]*dbmodels.Entry, error) {
trimmed := strings.TrimSpace(query)
if trimmed == "" {
return []*dbmodels.Entry{}, nil
}
terms := []string{trimmed}
normalized := datatypes.NormalizeString(trimmed)
normalized = strings.Join(strings.Fields(normalized), " ")
if normalized != "" && normalized != trimmed {
terms = append(terms, normalized)
}
collapsed := strings.ReplaceAll(trimmed, "/", "")
if collapsed != "" && collapsed != trimmed && collapsed != normalized {
terms = append(terms, collapsed)
}
entryFields := []string{
dbmodels.PREFERRED_TITLE_FIELD,
dbmodels.VARIANT_TITLE_FIELD,
dbmodels.PARALLEL_TITLE_FIELD,
dbmodels.TITLE_STMT_FIELD,
dbmodels.SUBTITLE_STMT_FIELD,
dbmodels.INCIPIT_STMT_FIELD,
dbmodels.RESPONSIBILITY_STMT_FIELD,
dbmodels.PUBLICATION_STMT_FIELD,
dbmodels.PLACE_STMT_FIELD,
dbmodels.EDITION_FIELD,
dbmodels.YEAR_FIELD,
dbmodels.EXTENT_FIELD,
dbmodels.DIMENSIONS_FIELD,
dbmodels.REFERENCES_FIELD,
dbmodels.ANNOTATION_FIELD,
dbmodels.COMMENT_FIELD,
dbmodels.MUSENALMID_FIELD,
}
entryConditions := []dbx.Expression{}
for _, term := range terms {
for _, field := range entryFields {
entryConditions = append(entryConditions, dbx.Like(field, term).Match(true, true))
}
}
entries := []*dbmodels.Entry{}
if len(entryConditions) > 0 {
if err := app.RecordQuery(dbmodels.ENTRIES_TABLE).
Where(dbx.Or(entryConditions...)).
All(&entries); err != nil {
return nil, err
}
}
entryIDs := map[string]struct{}{}
for _, entry := range entries {
entryIDs[entry.Id] = struct{}{}
}
itemFields := []string{
dbmodels.ITEMS_IDENTIFIER_FIELD,
dbmodels.ITEMS_MEDIA_FIELD,
dbmodels.ITEMS_LOCATION_FIELD,
dbmodels.ITEMS_OWNER_FIELD,
dbmodels.ITEMS_CONDITION_FIELD,
dbmodels.URI_FIELD,
dbmodels.ANNOTATION_FIELD,
dbmodels.COMMENT_FIELD,
}
itemConditions := []dbx.Expression{}
for _, term := range terms {
for _, field := range itemFields {
itemConditions = append(itemConditions, dbx.Like(field, term).Match(true, true))
}
}
if len(itemConditions) > 0 {
items := []*dbmodels.Item{}
if err := app.RecordQuery(dbmodels.ITEMS_TABLE).
Where(dbx.Or(itemConditions...)).
All(&items); err != nil {
return nil, err
}
itemEntryIDs := []any{}
for _, item := range items {
if item == nil {
continue
}
if entryID := item.Entry(); entryID != "" {
if _, exists := entryIDs[entryID]; !exists {
entryIDs[entryID] = struct{}{}
itemEntryIDs = append(itemEntryIDs, entryID)
}
}
}
if len(itemEntryIDs) > 0 {
itemEntries, err := dbmodels.Entries_IDs(app, itemEntryIDs)
if err != nil {
return nil, err
}
entries = append(entries, itemEntries...)
}
}
if len(entries) == 0 {
return entries, nil
}
dbmodels.Sort_Entries_Title_Year(entries)
return entries, nil
}
func searchBaendeEntriesQuick(app core.App, query string) ([]*dbmodels.Entry, error) {
trimmed := strings.TrimSpace(query)
if trimmed == "" {
return []*dbmodels.Entry{}, nil
}
if utf8.RuneCountInString(trimmed) == 1 {
return []*dbmodels.Entry{}, nil
}
entryFields := []string{
dbmodels.PREFERRED_TITLE_FIELD,
}
entryConditions := make([]dbx.Expression, 0, len(entryFields))
for _, field := range entryFields {
entryConditions = append(entryConditions, dbx.Like(field, trimmed).Match(true, true))
}
entryIDs := map[string]struct{}{}
if len(entryConditions) > 0 {
entries := []*dbmodels.Entry{}
if err := app.RecordQuery(dbmodels.ENTRIES_TABLE).
Where(dbx.Or(entryConditions...)).
All(&entries); err != nil {
return nil, err
}
for _, entry := range entries {
entryIDs[entry.Id] = struct{}{}
}
}
itemConditions := []dbx.Expression{
dbx.Like(dbmodels.ITEMS_IDENTIFIER_FIELD, trimmed).Match(true, true),
}
if len(itemConditions) > 0 {
items := []*dbmodels.Item{}
if err := app.RecordQuery(dbmodels.ITEMS_TABLE).
Where(dbx.Or(itemConditions...)).
All(&items); err != nil {
return nil, err
}
for _, item := range items {
if entryID := item.Entry(); entryID != "" {
entryIDs[entryID] = struct{}{}
}
}
}
if len(entryIDs) == 0 {
return []*dbmodels.Entry{}, nil
}
entryIDList := make([]any, 0, len(entryIDs))
for id := range entryIDs {
entryIDList = append(entryIDList, id)
}
entries, err := dbmodels.Entries_IDs(app, entryIDList)
if err != nil {
return nil, err
}
dbmodels.Sort_Entries_Title_Year(entries)
return entries, nil
}