diff --git a/dbmodels/agents.go b/dbmodels/agents.go new file mode 100644 index 0000000..e2a2b80 --- /dev/null +++ b/dbmodels/agents.go @@ -0,0 +1,17 @@ +package dbmodels + +import ( + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/core" +) + +func AgentForId(app core.App, id string) (*Agent, error) { + agent := &Agent{} + err := app.RecordQuery(AGENTS_TABLE). + Where(dbx.HashExp{ID_FIELD: id}). + One(agent) + if err != nil { + return nil, err + } + return agent, nil +} diff --git a/dbmodels/dbdata.go b/dbmodels/dbdata.go index 4fe9896..a4fe777 100644 --- a/dbmodels/dbdata.go +++ b/dbmodels/dbdata.go @@ -423,6 +423,7 @@ const ( CONTENTS_TABLE = "contents" ITEMS_TABLE = "items" + ID_FIELD = "id" ANNOTATION_FIELD = "annotation" MUSENALMID_FIELD = "musenalm_id" diff --git a/dbmodels/entries.go b/dbmodels/entries.go new file mode 100644 index 0000000..680bd68 --- /dev/null +++ b/dbmodels/entries.go @@ -0,0 +1,90 @@ +package dbmodels + +import ( + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/core" +) + +type EntriesAgents map[string][]*REntriesAgents + +func YearsForEntries(app core.App) ([]int, error) { + rec := []core.Record{} + err := app.RecordQuery(ENTRIES_TABLE). + Select(YEAR_FIELD + " AS id"). + Distinct(true). + OrderBy("id"). + All(&rec) + if err != nil { + return nil, err + } + + years := []int{} + for _, r := range rec { + years = append(years, r.GetInt("id")) + } + + return years, nil +} + +func EntriesForYear(app core.App, year int) ([]*Entry, error) { + entries := []*Entry{} + err := app.RecordQuery(ENTRIES_TABLE). + Where(dbx.HashExp{YEAR_FIELD: year}). + All(&entries) + if err != nil { + return nil, err + } + + return entries, nil +} + +func EntriesForAgent(app core.App, agentId string) ([]*Entry, EntriesAgents, error) { + relations := []*core.Record{} + err := app.RecordQuery(RelationTableName(ENTRIES_TABLE, AGENTS_TABLE)). + Where(dbx.HashExp{AGENTS_TABLE: agentId}). + All(&relations) + if err != nil { + return nil, nil, err + } + + app.ExpandRecords(relations, []string{ENTRIES_TABLE}, nil) + entries := []*Entry{} + for _, r := range relations { + record := r.ExpandedOne(ENTRIES_TABLE) + if record == nil { + continue + } + entries = append(entries, NewEntry(record)) + } + + agents := map[string][]*REntriesAgents{} + for _, r := range relations { + agent := NewREntriesAgents(r) + agents[agent.Entry()] = append(agents[agent.Entry()], agent) + } + + return entries, agents, nil +} + +func EntriesForPlace(app core.App, placeId string) ([]*Entry, error) { + entries := []*Entry{} + err := app.RecordQuery(ENTRIES_TABLE). + Where(dbx.Like(PLACES_TABLE, placeId).Match(true, true)). + All(&entries) + if err != nil { + return nil, err + } + + return entries, nil +} + +func EntryForId(app core.App, id string) (*Entry, error) { + entry := &Entry{} + err := app.RecordQuery(ENTRIES_TABLE). + Where(dbx.HashExp{ID_FIELD: id}). + One(entry) + if err != nil { + return nil, err + } + return entry, nil +} diff --git a/dbmodels/functions.go b/dbmodels/functions.go index 575d030..66e225e 100644 --- a/dbmodels/functions.go +++ b/dbmodels/functions.go @@ -82,14 +82,18 @@ func BasicRelationCollection(app core.App, sourcetablename, targettablename stri return collection, nil } -type IDable interface { - ID() string -} - -func GetIDs(records []IDable) []string { - ids := []string{} +func GetIDs(records []*core.Record) []any { + ids := []any{} for _, r := range records { - ids = append(ids, r.ID()) + ids = append(ids, r.Id) } return ids } + +func GetFields(records []*core.Record, field string) []any { + fields := []any{} + for _, r := range records { + fields = append(fields, r.GetString(field)) + } + return fields +} diff --git a/dbmodels/places.go b/dbmodels/places.go new file mode 100644 index 0000000..82f567e --- /dev/null +++ b/dbmodels/places.go @@ -0,0 +1,40 @@ +package dbmodels + +import ( + "slices" + + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/core" + "golang.org/x/text/collate" + "golang.org/x/text/language" +) + +func AllPlaces(app core.App) ([]*Place, error) { + places := []*Place{} + err := app.RecordQuery(PLACES_TABLE). + OrderBy(PLACES_NAME_FIELD). + All(&places) + if err != nil { + return nil, err + } + + return places, nil +} + +func SortPlacesByName(places []*Place) { + collator := collate.New(language.German) + slices.SortFunc(places, func(i, j *Place) int { + return collator.CompareString(i.Name(), j.Name()) + }) +} + +func PlaceForId(app core.App, id string) (*Place, error) { + place := &Place{} + err := app.RecordQuery(PLACES_TABLE). + Where(dbx.HashExp{ID_FIELD: id}). + One(place) + if err != nil { + return nil, err + } + return place, nil +} diff --git a/dbmodels/series.go b/dbmodels/series.go index 69b2882..18fe0fa 100644 --- a/dbmodels/series.go +++ b/dbmodels/series.go @@ -1,11 +1,7 @@ package dbmodels import ( - "slices" - "github.com/pocketbase/pocketbase/core" - "golang.org/x/text/collate" - "golang.org/x/text/language" ) var _ core.RecordProxy = (*Series)(nil) @@ -91,10 +87,3 @@ func (s *Series) Frequency() string { func (s *Series) SetFrequency(frequency string) { s.Set(SERIES_FREQUENCY_FIELD, frequency) } - -func SortSeriesByTitle(series []*Series) { - collator := collate.New(language.German) - slices.SortFunc(series, func(i, j *Series) int { - return collator.CompareString(i.Title(), j.Title()) - }) -} diff --git a/dbmodels/seriesses.go b/dbmodels/seriesses.go new file mode 100644 index 0000000..950def0 --- /dev/null +++ b/dbmodels/seriesses.go @@ -0,0 +1,248 @@ +package dbmodels + +import ( + "slices" + + "github.com/pocketbase/dbx" + "github.com/pocketbase/pocketbase/core" + "golang.org/x/text/collate" + "golang.org/x/text/language" +) + +type SeriesEntries map[string][]*REntriesSeries + +func SortSeriessesByTitle(series []*Series) { + collator := collate.New(language.German) + slices.SortFunc(series, func(i, j *Series) int { + return collator.CompareString(i.Title(), j.Title()) + }) +} + +func BasicSearchSeries(app core.App, query string) ([]*Series, []*Series, error) { + series, err := TitleSearchSeries(app, query) + if err != nil { + return nil, nil, err + } + + altseries, err := AltSearchSeries(app, query) + if err != nil { + return nil, nil, err + } + return series, altseries, nil +} + +func TitleSearchSeries(app core.App, query string) ([]*Series, error) { + series := []*Series{} + err := app.RecordQuery(SERIES_TABLE). + Where(dbx.Like(SERIES_TITLE_FIELD, query).Match(true, true)). + OrderBy(SERIES_TITLE_FIELD). + All(&series) + if err != nil { + return nil, err + } + + return series, nil +} + +func AltSearchSeries(app core.App, query string) ([]*Series, error) { + series := []*Series{} + err := app.RecordQuery(SERIES_TABLE). + Where(dbx.Like(ANNOTATION_FIELD, query).Match(true, true)). + OrderBy(SERIES_TITLE_FIELD). + All(&series) + if err != nil { + return nil, err + } + + return series, nil +} + +func IDsForSeriesses(series []*Series) []any { + ids := []any{} + for _, s := range series { + ids = append(ids, s.Id) + } + return ids +} + +func makeMapForEnrySeries(relations []*REntriesSeries, entries map[string]*Entry) SeriesEntries { + m := map[string][]*REntriesSeries{} + for _, r := range relations { + m[r.Id] = append(m[r.Id], r) + } + + for _, rel := range m { + slices.SortFunc(rel, func(i, j *REntriesSeries) int { + ientry := entries[i.Entry()] + jentry := entries[j.Entry()] + return ientry.Year() - jentry.Year() + }) + } + + return m +} + +func EntriesForSeriesses(app core.App, series []*Series) ( + SeriesEntries, + map[string]*Entry, + error) { + ids := IDsForSeriesses(series) + relations := []*core.Record{} + + err := app.RecordQuery(RelationTableName(ENTRIES_TABLE, SERIES_TABLE)). + Where(dbx.HashExp{ + SERIES_TABLE: ids, + }). + All(&relations) + if err != nil { + return nil, nil, err + } + + app.ExpandRecords(relations, []string{ENTRIES_TABLE}, nil) + bmap := map[string]*Entry{} + for _, r := range relations { + record := r.ExpandedOne(ENTRIES_TABLE) + if record == nil { + continue + } + entry := NewEntry(record) + bmap[entry.Id] = entry + } + + smap := map[string][]*REntriesSeries{} + for _, r := range relations { + series := NewREntriesSeries(r) + smap[series.Id] = append(smap[series.Id], series) + } + + for _, rel := range smap { + slices.SortFunc(rel, func(i, j *REntriesSeries) int { + ientry := bmap[i.Entry()] + jentry := bmap[j.Entry()] + return ientry.Year() - jentry.Year() + }) + } + + return smap, bmap, nil +} + +func LettersForSeries(app core.App) ([]string, error) { + letters := []core.Record{} + ids := []string{} + + err := app.RecordQuery(SERIES_TABLE). + Select("upper(substr(" + SERIES_TITLE_FIELD + ", 1, 1)) AS id"). + Distinct(true). + All(&letters) + if err != nil { + return nil, err + } + + for _, l := range letters { + ids = append(ids, l.GetString("id")) + } + return ids, nil +} + +func AllAgentsForSeries(app core.App) ([]*Agent, error) { + rels := []*core.Record{} + // INFO: we could just fetch all relations here + err := app.RecordQuery(RelationTableName(ENTRIES_TABLE, AGENTS_TABLE)). + GroupBy(AGENTS_TABLE). + All(&rels) + if err != nil { + return nil, err + } + + app.ExpandRecords(rels, []string{AGENTS_TABLE}, nil) + agents := []*Agent{} + for _, r := range rels { + record := r.ExpandedOne(AGENTS_TABLE) + if record == nil { + continue + } + agent := NewAgent(record) + agents = append(agents, agent) + } + + SortAgentsByName(agents) + + return agents, err +} + +func SeriesForLetter(app core.App, letter string) ([]*Series, error) { + series := []*Series{} + err := app.RecordQuery(SERIES_TABLE). + Where(dbx.Like(SERIES_TITLE_FIELD, letter).Match(false, true)). + OrderBy(SERIES_TITLE_FIELD). + All(&series) + if err != nil { + return nil, err + } + + return series, nil +} + +func SeriesForAgent(app core.App, id string) ([]*Series, SeriesEntries, map[string]*Entry, error) { + entries, _, err := EntriesForAgent(app, id) + if err != nil { + return nil, nil, nil, err + } + + return SeriesForEntries(app, entries) +} + +func SeriesForPlace(app core.App, id string) ([]*Series, SeriesEntries, map[string]*Entry, error) { + entries, err := EntriesForPlace(app, id) + if err != nil { + return nil, nil, nil, err + } + + return SeriesForEntries(app, entries) +} + +func SeriesForEntries(app core.App, entries []*Entry) ([]*Series, SeriesEntries, map[string]*Entry, error) { + bids := make([]any, 0, len(entries)) + for _, e := range entries { + bids = append(bids, e.Id) + } + + srels := []*REntriesSeries{} + err := app.RecordQuery(RelationTableName(ENTRIES_TABLE, SERIES_TABLE)). + Where(dbx.HashExp{ENTRIES_TABLE: bids}). + All(&srels) + if err != nil { + return nil, nil, nil, err + } + + sids := []any{} + for _, s := range srels { + sids = append(sids, s.Series()) + } + + series := []*Series{} + err = app.RecordQuery(SERIES_TABLE). + Where(dbx.HashExp{ID_FIELD: sids}). + All(&series) + if err != nil { + return nil, nil, nil, err + } + + bmap := make(map[string]*Entry, len(entries)) + for _, e := range entries { + bmap[e.Id] = e + } + + smap := makeMapForEnrySeries(srels, bmap) + + return series, smap, bmap, nil +} + +func SeriesForYear(app core.App, year int) ([]*Series, SeriesEntries, map[string]*Entry, error) { + series, err := EntriesForYear(app, year) + if err != nil { + return nil, nil, nil, err + } + + return SeriesForEntries(app, series) +} diff --git a/go.mod b/go.mod index aeb01fb..e957de1 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/Theodor-Springmann-Stiftung/musenalm -go 1.23.6 +go 1.24 require ( github.com/fsnotify/fsnotify v1.7.0 diff --git a/pages/reihen.go b/pages/reihen.go index cdefeb4..8060196 100644 --- a/pages/reihen.go +++ b/pages/reihen.go @@ -2,22 +2,25 @@ package pages import ( "net/http" - "slices" + "strconv" "strings" "github.com/Theodor-Springmann-Stiftung/musenalm/app" "github.com/Theodor-Springmann-Stiftung/musenalm/dbmodels" "github.com/Theodor-Springmann-Stiftung/musenalm/pagemodels" "github.com/Theodor-Springmann-Stiftung/musenalm/templating" - "github.com/pocketbase/dbx" "github.com/pocketbase/pocketbase/core" "github.com/pocketbase/pocketbase/tools/router" ) const ( URL_REIHEN = "/reihen/" + URL_REIHE = "/reihen/{id}/" PARAM_LETTER = "letter" PARAM_SEARCH = "search" + PARAM_PERSON = "agent" + PARAM_PLACE = "place" + PARAM_YEAR = "year" ) func init() { @@ -47,179 +50,177 @@ func (p *ReihenPage) Setup(router *router.Router[*core.RequestEvent], app core.A if search != "" { return p.SearchRequest(app, engine, e) } + person := e.Request.URL.Query().Get(PARAM_PERSON) + if person != "" { + return p.PersonRequest(app, engine, e) + } + place := e.Request.URL.Query().Get(PARAM_PLACE) + if place != "" { + return p.PlaceRequest(app, engine, e) + } + year := e.Request.URL.Query().Get(PARAM_YEAR) + if year != "" { + return p.YearRequest(app, engine, e) + } return p.LetterRequest(app, engine, e) }) return nil } +func (p *ReihenPage) YearRequest(app core.App, engine *templating.Engine, e *core.RequestEvent) error { + year := e.Request.URL.Query().Get(PARAM_YEAR) + data := map[string]interface{}{} + data[PARAM_YEAR] = year + + y, err := strconv.Atoi(year) + if err != nil { + return err + } + + series, relations, entries, err := dbmodels.SeriesForYear(app, y) + if err != nil { + return err + } + data["entries"] = entries + data["relations"] = relations + data["series"] = series + + return p.Get(e, engine, data) +} + func (p *ReihenPage) LetterRequest(app core.App, engine *templating.Engine, e *core.RequestEvent) error { letter := e.Request.URL.Query().Get(PARAM_LETTER) + data := map[string]interface{}{} if letter == "" { letter = "A" } - series := []*dbmodels.Series{} - err := app.RecordQuery(dbmodels.SERIES_TABLE). - Where(dbx.Like(dbmodels.SERIES_TITLE_FIELD, letter).Match(false, true)). - OrderBy(dbmodels.SERIES_TITLE_FIELD). - All(&series) - // INFO: this does not return an error if the result set is empty + data[PARAM_LETTER] = letter + + series, err := dbmodels.SeriesForLetter(app, letter) if err != nil { return err } - // INFO: We sort again since the query can't sort german umlauts correctly - dbmodels.SortSeriesByTitle(series) + dbmodels.SortSeriessesByTitle(series) + data["series"] = series - smap, bmap := p.EntriesForSeries(app, series) - agents, _ := p.GetAgents(app) - - var builder strings.Builder - err = engine.Render(&builder, URL_REIHEN, map[string]interface{}{ - PARAM_LETTER: letter, - "series": series, - "letters": p.Letters(app), - "entries": bmap, - "relations": smap, - "agents": agents, - }) + rmap, bmap, err := dbmodels.EntriesForSeriesses(app, series) if err != nil { return err } + data["entries"] = bmap + data["relations"] = rmap - return e.HTML(http.StatusOK, builder.String()) + return p.Get(e, engine, data) +} + +func (p *ReihenPage) PersonRequest(app core.App, engine *templating.Engine, e *core.RequestEvent) error { + person := e.Request.URL.Query().Get(PARAM_PERSON) + data := map[string]interface{}{} + data[PARAM_PERSON] = person + + agent, err := dbmodels.AgentForId(app, person) + if err != nil { + return err + } + data["a"] = agent + + series, relations, entries, err := dbmodels.SeriesForAgent(app, person) + if err != nil { + return err + } + data["series"] = series + data["relations"] = relations + data["entries"] = entries + + return p.Get(e, engine, data) +} + +func (p *ReihenPage) PlaceRequest(app core.App, engine *templating.Engine, e *core.RequestEvent) error { + place := e.Request.URL.Query().Get(PARAM_PLACE) + data := map[string]interface{}{} + data[PARAM_PLACE] = place + + pl, err := dbmodels.PlaceForId(app, place) + if err != nil { + return err + } + data["p"] = pl + + series, relations, entries, err := dbmodels.SeriesForPlace(app, place) + if err != nil { + return err + } + data["series"] = series + data["relations"] = relations + data["entries"] = entries + + return p.Get(e, engine, data) } func (p *ReihenPage) SearchRequest(app core.App, engine *templating.Engine, e *core.RequestEvent) error { search := e.Request.URL.Query().Get(PARAM_SEARCH) - series := []*dbmodels.Series{} - err := app.RecordQuery(dbmodels.SERIES_TABLE). - Where(dbx.Like(dbmodels.SERIES_TITLE_FIELD, search).Match(true, true)). - OrderBy(dbmodels.SERIES_TITLE_FIELD). - All(&series) + data := map[string]interface{}{} + data[PARAM_SEARCH] = search + series, altseries, err := dbmodels.BasicSearchSeries(app, search) if err != nil { return err } + dbmodels.SortSeriessesByTitle(series) + dbmodels.SortSeriessesByTitle(altseries) + data["series"] = series + data["altseries"] = altseries - altseries := []*dbmodels.Series{} - err = app.RecordQuery(dbmodels.SERIES_TABLE). - Where(dbx.Like(dbmodels.ANNOTATION_FIELD, search).Match(true, true)). - OrderBy(dbmodels.SERIES_TITLE_FIELD). - All(&altseries) + rmap, bmap, err := dbmodels.EntriesForSeriesses(app, series) if err != nil { return err } + data["entries"] = bmap + data["relations"] = rmap - dbmodels.SortSeriesByTitle(series) - dbmodels.SortSeriesByTitle(altseries) + return p.Get(e, engine, data) +} - smap, bmap := p.EntriesForSeries(app, series) - agents, _ := p.GetAgents(app) +func (p *ReihenPage) CommonData(app core.App, data map[string]interface{}) error { + agents, err := dbmodels.AllAgentsForSeries(app) + if err != nil { + return err + } + data["agents"] = agents + + letters, err := dbmodels.LettersForSeries(app) + if err != nil { + return err + } + data["letters"] = letters + + places, err := dbmodels.AllPlaces(app) + if err != nil { + return err + } + dbmodels.SortPlacesByName(places) + data["places"] = places + + years, err := dbmodels.YearsForEntries(app) + if err != nil { + return err + } + data["years"] = years + + return nil +} + +func (p *ReihenPage) Get(request *core.RequestEvent, engine *templating.Engine, data map[string]interface{}) error { + err := p.CommonData(request.App, data) + if err != nil { + return err + } var builder strings.Builder - err = engine.Render(&builder, URL_REIHEN, map[string]interface{}{ - PARAM_SEARCH: search, - "series": series, - "altseries": altseries, - "letters": p.Letters(app), - "entries": bmap, - "relations": smap, - "agents": agents, - }) + err = engine.Render(&builder, URL_REIHEN, data) if err != nil { return err } - - return e.HTML(http.StatusOK, builder.String()) -} - -func (p *ReihenPage) Letters(app core.App) []string { - letters := []core.Record{} - ids := []string{} - - err := app.RecordQuery(dbmodels.SERIES_TABLE). - Select("upper(substr(" + dbmodels.SERIES_TITLE_FIELD + ", 1, 1)) AS id"). - Distinct(true). - All(&letters) - if err != nil { - return ids - } - - for _, l := range letters { - ids = append(ids, l.GetString("id")) - } - return ids -} - -func (p *ReihenPage) EntriesForSeries(app core.App, series []*dbmodels.Series) ( - map[string][]*dbmodels.REntriesSeries, - map[string]*dbmodels.Entry) { - ids := []any{} - for _, s := range series { - ids = append(ids, s.Id) - } - - relations := []*core.Record{} - - err := app.RecordQuery(dbmodels.RelationTableName(dbmodels.ENTRIES_TABLE, dbmodels.SERIES_TABLE)). - Where(dbx.HashExp{ - dbmodels.SERIES_TABLE: ids, - }). - All(&relations) - if err != nil { - return nil, nil - } - - app.ExpandRecords(relations, []string{dbmodels.ENTRIES_TABLE}, nil) - bmap := map[string]*dbmodels.Entry{} - for _, r := range relations { - record := r.ExpandedOne(dbmodels.ENTRIES_TABLE) - if record == nil { - continue - } - entry := dbmodels.NewEntry(record) - bmap[entry.Id] = entry - } - - smap := map[string][]*dbmodels.REntriesSeries{} - for _, r := range relations { - series := dbmodels.NewREntriesSeries(r) - smap[series.Id] = append(smap[series.Id], series) - } - - for _, rel := range smap { - slices.SortFunc(rel, func(i, j *dbmodels.REntriesSeries) int { - ientry := bmap[i.Entry()] - jentry := bmap[j.Entry()] - return ientry.Year() - jentry.Year() - }) - } - - return smap, bmap -} - -func (p *ReihenPage) GetAgents(app core.App) ([]*dbmodels.Agent, error) { - rels := []*core.Record{} - // INFO: we could just fetch all relations here - err := app.RecordQuery(dbmodels.RelationTableName(dbmodels.ENTRIES_TABLE, dbmodels.AGENTS_TABLE)). - GroupBy(dbmodels.AGENTS_TABLE). - All(&rels) - if err != nil { - return nil, err - } - - app.ExpandRecords(rels, []string{dbmodels.AGENTS_TABLE}, nil) - agents := []*dbmodels.Agent{} - for _, r := range rels { - record := r.ExpandedOne(dbmodels.AGENTS_TABLE) - if record == nil { - continue - } - agent := dbmodels.NewAgent(record) - agents = append(agents, agent) - } - - dbmodels.SortAgentsByName(agents) - - return agents, err + return request.HTML(http.StatusOK, builder.String()) } diff --git a/templating/engine.go b/templating/engine.go index 064682d..ed0ffb2 100644 --- a/templating/engine.go +++ b/templating/engine.go @@ -100,10 +100,13 @@ func (e *Engine) AddFuncs(funcs map[string]interface{}) { } func (e *Engine) Render(out io.Writer, path string, ld map[string]interface{}, layout ...string) error { - // TODO: check if a reload is needed if files on disk have changed gd := e.GlobalData - for k, v := range ld { - gd[k] = v + // INFO: don't pollute the global data space + for k, v := range gd { + _, ok := ld[k] + if !ok { + ld[k] = v + } } e.mu.Lock() @@ -135,7 +138,7 @@ func (e *Engine) Render(out io.Writer, path string, ld map[string]interface{}, l return err } - err = lay.Execute(out, gd) + err = lay.Execute(out, ld) if err != nil { return err } diff --git a/views/routes/reihen/body.gohtml b/views/routes/reihen/body.gohtml index 479268d..5ec5432 100644 --- a/views/routes/reihen/body.gohtml +++ b/views/routes/reihen/body.gohtml @@ -15,15 +15,55 @@ {{ end }} -{{ if .search }} -