Basis Suche Inhalte

This commit is contained in:
Simon Martens
2025-02-28 22:31:19 +01:00
parent 4581f34dd9
commit ca33ec7be3
20 changed files with 1290 additions and 594 deletions

View File

@@ -417,6 +417,8 @@ var PUBLIC_VIEW_RULE = types.Pointer("")
var PUBLIC_LIST_RULE = types.Pointer("")
const (
FTS_LIMIT = 100000
PLACES_TABLE = "places"
AGENTS_TABLE = "agents"
SERIES_TABLE = "series"

View File

@@ -204,6 +204,10 @@ func (e *Entry) MusenalmID() int {
return e.GetInt(MUSENALMID_FIELD)
}
func (e *Entry) MusenalmIDString() string {
return e.GetString(MUSENALMID_FIELD)
}
func (e *Entry) SetMusenalmID(musenalmID int) {
e.Set(MUSENALMID_FIELD, musenalmID)
}

View File

@@ -2,7 +2,6 @@ package dbmodels
import (
"errors"
"fmt"
"strconv"
"strings"
"unicode"
@@ -162,7 +161,6 @@ func NormalizeQuery(query string) Query {
}
for _, r := range query {
fmt.Printf("Rune: %v\n", r)
if r == '"' {
if isInQuotes {
addToken()
@@ -185,12 +183,6 @@ func NormalizeQuery(query string) Query {
at()
}
fmt.Printf("Query: %v\n", query)
fmt.Printf("Include: %v\n", include)
fmt.Printf("Exclude: %v\n", exclude)
fmt.Printf("UnsafeI: %v\n", unsafeI)
fmt.Printf("UnsafeE: %v\n", unsafeE)
return Query{
Include: include,
Exclude: exclude,

View File

@@ -1,6 +1,7 @@
package dbmodels
import (
"strconv"
"strings"
"github.com/Theodor-Springmann-Stiftung/musenalm/helpers/datatypes"
@@ -146,7 +147,7 @@ func (q *FTS5Query) Query() string {
query += " { " + strings.Join(m.Fields, " ") + " } : \"" + q.Escape(m.Value) + "\""
}
query += "'"
query += "' LIMIT " + strconv.Itoa(FTS_LIMIT)
return query
}

View File

@@ -14,7 +14,7 @@ import (
// For scanning, with an Iter_ prefix, yields single row results
func REntriesAgents_Agent(app core.App, id string) ([]*REntriesAgents, error) {
return TableByFields[[]*REntriesAgents](
return TableByFields[*REntriesAgents](
app,
RelationTableName(ENTRIES_TABLE, AGENTS_TABLE),
AGENTS_TABLE,
@@ -23,7 +23,7 @@ func REntriesAgents_Agent(app core.App, id string) ([]*REntriesAgents, error) {
}
func REntriesAgents_Entry(app core.App, id string) ([]*REntriesAgents, error) {
return TableByFields[[]*REntriesAgents](
return TableByFields[*REntriesAgents](
app,
RelationTableName(ENTRIES_TABLE, AGENTS_TABLE),
ENTRIES_TABLE,
@@ -32,7 +32,7 @@ func REntriesAgents_Entry(app core.App, id string) ([]*REntriesAgents, error) {
}
func REntriesAgents_Entries(app core.App, ids []any) ([]*REntriesAgents, error) {
return TableByFields[[]*REntriesAgents](
return TableByFields[*REntriesAgents](
app,
RelationTableName(ENTRIES_TABLE, AGENTS_TABLE),
ENTRIES_TABLE,
@@ -41,7 +41,7 @@ func REntriesAgents_Entries(app core.App, ids []any) ([]*REntriesAgents, error)
}
func RContentsAgents_Agent(app core.App, id string) ([]*RContentsAgents, error) {
return TableByFields[[]*RContentsAgents](
return TableByFields[*RContentsAgents](
app,
RelationTableName(CONTENTS_TABLE, AGENTS_TABLE),
AGENTS_TABLE,
@@ -50,7 +50,7 @@ func RContentsAgents_Agent(app core.App, id string) ([]*RContentsAgents, error)
}
func RContentsAgents_Contents(app core.App, id []any) ([]*RContentsAgents, error) {
return TableByFields[[]*RContentsAgents](
return TableByFields[*RContentsAgents](
app,
RelationTableName(CONTENTS_TABLE, AGENTS_TABLE),
CONTENTS_TABLE,
@@ -59,7 +59,7 @@ func RContentsAgents_Contents(app core.App, id []any) ([]*RContentsAgents, error
}
func RContentsAgents_Content(app core.App, id string) ([]*RContentsAgents, error) {
return TableByFields[[]*RContentsAgents](
return TableByFields[*RContentsAgents](
app,
RelationTableName(CONTENTS_TABLE, AGENTS_TABLE),
CONTENTS_TABLE,
@@ -68,7 +68,7 @@ func RContentsAgents_Content(app core.App, id string) ([]*RContentsAgents, error
}
func REntriesSeries_Entries(app core.App, ids []any) ([]*REntriesSeries, error) {
return TableByFields[[]*REntriesSeries](
return TableByFields[*REntriesSeries](
app,
RelationTableName(ENTRIES_TABLE, SERIES_TABLE),
ENTRIES_TABLE,
@@ -77,7 +77,7 @@ func REntriesSeries_Entries(app core.App, ids []any) ([]*REntriesSeries, error)
}
func REntriesSeries_Entry(app core.App, id string) ([]*REntriesSeries, error) {
return TableByFields[[]*REntriesSeries](
return TableByFields[*REntriesSeries](
app,
RelationTableName(ENTRIES_TABLE, SERIES_TABLE),
ENTRIES_TABLE,
@@ -86,7 +86,7 @@ func REntriesSeries_Entry(app core.App, id string) ([]*REntriesSeries, error) {
}
func REntriesSeries_Seriess(app core.App, ids []any) ([]*REntriesSeries, error) {
return TableByFields[[]*REntriesSeries](
return TableByFields[*REntriesSeries](
app,
RelationTableName(ENTRIES_TABLE, SERIES_TABLE),
SERIES_TABLE,
@@ -100,7 +100,7 @@ func Agents_ID(app core.App, id string) (*Agent, error) {
}
func Agents_IDs(app core.App, ids []any) ([]*Agent, error) {
return TableByIDs[[]*Agent](app, AGENTS_TABLE, ids)
return TableByIDs[*Agent](app, AGENTS_TABLE, ids)
}
func Entries_ID(app core.App, id string) (*Entry, error) {
@@ -114,11 +114,11 @@ func Entries_MusenalmID(app core.App, id string) (*Entry, error) {
}
func Entries_IDs(app core.App, ids []any) ([]*Entry, error) {
return TableByIDs[[]*Entry](app, ENTRIES_TABLE, ids)
return TableByIDs[*Entry](app, ENTRIES_TABLE, ids)
}
func Series_IDs(app core.App, ids []any) ([]*Series, error) {
return TableByIDs[[]*Series](app, SERIES_TABLE, ids)
return TableByIDs[*Series](app, SERIES_TABLE, ids)
}
func Series_MusenalmID(app core.App, id string) (*Series, error) {
@@ -132,15 +132,15 @@ func Series_ID(app core.App, id string) (*Series, error) {
}
func Places_IDs(app core.App, ids []any) ([]*Place, error) {
return TableByIDs[[]*Place](app, PLACES_TABLE, ids)
return TableByIDs[*Place](app, PLACES_TABLE, ids)
}
func Contents_IDs(app core.App, ids []any) ([]*Content, error) {
return TableByIDs[[]*Content](app, CONTENTS_TABLE, ids)
return TableByIDs[*Content](app, CONTENTS_TABLE, ids)
}
func Contents_Entry(app core.App, id string) ([]*Content, error) {
return TableByFields[[]*Content](
return TableByFields[*Content](
app,
CONTENTS_TABLE,
ENTRIES_TABLE,

View File

@@ -2,6 +2,7 @@ package dbmodels
import (
"iter"
"reflect"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/core"
@@ -61,6 +62,11 @@ import (
// github.com/pocketbase/pocketbase/apis.NewRouter.BodyLimit.func7(0xc0002da000)
//
// /home
const (
QUERY_PARTITION_SIZE = 1200
)
func Iter_TableByField[T interface{}](app core.App, table, field string, value interface{}) (iter.Seq2[*T, error], error) {
rows, err := app.RecordQuery(table).
Where(dbx.HashExp{field: value}).
@@ -111,14 +117,43 @@ func TableByField[T interface{}](app core.App, table, field string, value string
return ret, nil
}
func TableByFields[T interface{}](app core.App, table, field string, values any) (T, error) {
var ret T
func TableByFields[T interface{}](app core.App, table, field string, values any) ([]T, error) {
var ret []T
if reflect.TypeOf(values).Kind() == reflect.Slice {
v := values.([]any)
if len(v) > QUERY_PARTITION_SIZE {
for i := 0; i < len(v); i += QUERY_PARTITION_SIZE {
part := v[i:]
if len(part) > QUERY_PARTITION_SIZE {
part = part[:QUERY_PARTITION_SIZE]
}
var partret []T
err := app.RecordQuery(table).
Where(dbx.HashExp{field: part}).
All(&partret)
if err != nil {
return ret, err
}
ret = append(ret, partret...)
}
} else {
err := app.RecordQuery(table).
Where(dbx.HashExp{field: values}).
All(&ret)
if err != nil {
return ret, err
}
}
} else {
err := app.RecordQuery(table).
Where(dbx.HashExp{field: values}).
All(&ret)
if err != nil {
return ret, err
}
}
return ret, nil
}
@@ -135,14 +170,33 @@ func TableByID[T interface{}](app core.App, table, id string) (T, error) {
return ret, nil
}
func TableByIDs[T interface{}](app core.App, table string, ids []any) (T, error) {
var ret T
func TableByIDs[T interface{}](app core.App, table string, ids []any) ([]T, error) {
var ret []T
if len(ids) > QUERY_PARTITION_SIZE {
for i := 0; i < len(ids); i += QUERY_PARTITION_SIZE {
part := ids[i:]
if len(part) > QUERY_PARTITION_SIZE {
part = part[:QUERY_PARTITION_SIZE]
}
var partret []T
err := app.RecordQuery(table).
Where(dbx.HashExp{ID_FIELD: part}).
All(&partret)
if err != nil {
return ret, err
}
ret = append(ret, partret...)
}
} else {
err := app.RecordQuery(table).
Where(dbx.HashExp{ID_FIELD: ids}).
All(&ret)
if err != nil {
return ret, err
}
}
return ret, nil
}

View File

@@ -44,11 +44,11 @@ func First(s string) string {
return string(r[0])
}
func LinksAnnotation(s string) string {
func LinksAnnotation(s, link string) string {
annotation := linksexp.ReplaceAllStringFunc(s, func(match string) string {
submatches := linksexp.FindStringSubmatch(match)
if len(submatches) > 1 {
return fmt.Sprintf(`<a href="#%s" class="link-default oldstyle-nums">%s</a>`, submatches[1], match)
return fmt.Sprintf(`<a href="`+link+`#%s" class="link-default oldstyle-nums">%s</a>`, submatches[1], match)
}
return match
})

View File

@@ -1,11 +1,9 @@
package pages
import (
"database/sql"
"fmt"
"net/http"
"slices"
"strings"
"github.com/Theodor-Springmann-Stiftung/musenalm/app"
"github.com/Theodor-Springmann-Stiftung/musenalm/dbmodels"
@@ -23,9 +21,9 @@ const (
URL_SUCHE = "/suche/{type}"
URL_SUCHE_ALT = "/suche/{$}"
DEFAULT_SUCHE = "/suche/baende"
PARAM_QUERY = "q"
PARAM_EXTENDED = "extended"
TEMPLATE_SUCHE = "/suche/"
PARAM_QUERY = "q"
)
var availableTypes = []string{"baende", "beitraege"}
@@ -60,6 +58,10 @@ func (p *SuchePage) Setup(router *router.Router[*core.RequestEvent], app core.Ap
return p.SearchBaendeRequest(app, engine, e, *allparas)
}
if allparas.IsBeitraegeSearch() {
return p.SearchBeitraegeRequest(app, engine, e, *allparas)
}
data := make(map[string]interface{})
data["parameters"] = allparas
return engine.Response200(e, p.Template+paras.Collection+"/", data, p.Layout)
@@ -72,10 +74,10 @@ func (p *SuchePage) SimpleSearchReihenRequest(app core.App, engine *templating.E
return engine.Response404(e, nil, nil)
}
func (p *SuchePage) SearchBaendeRequest(app core.App, engine *templating.Engine, e *core.RequestEvent, params SearchParameters) error {
func (p *SuchePage) SearchBeitraegeRequest(app core.App, engine *templating.Engine, e *core.RequestEvent, params SearchParameters) error {
data := make(map[string]interface{})
result, err := SimpleSearchBaende(app, params)
result, err := NewSearchBeitraege(app, params)
if err != nil {
return engine.Response404(e, err, nil)
}
@@ -85,38 +87,20 @@ func (p *SuchePage) SearchBaendeRequest(app core.App, engine *templating.Engine,
return engine.Response200(e, p.Template+params.Collection+"/", data, p.Layout)
}
const (
BEITRAEGE_PARAM_ALM_NR = "nr"
BEITRAEGE_PARAM_TITLE = "title"
BEITRAEGE_PARAM_INCIPT = "incipit"
BEITRAEGE_PARAM_PERSONS = "persons"
BEITRAEGE_PARAM_ANNOTATIONS = "annotations"
// INFO: this is expanded search only:
BEITRAEGE_PARAM_PSEUDONYMS = "pseudonyms"
// INFO: these are filter types & expanded search:
BEITRAEGE_PARAM_TYPE = "type"
BEITRAEGE_PARAM_SCANS = "scans"
func (p *SuchePage) SearchBaendeRequest(app core.App, engine *templating.Engine, e *core.RequestEvent, params SearchParameters) error {
data := make(map[string]interface{})
REIHEN_PARAM_TITLE = "title"
REIHEN_PARAM_ANNOTATIONS = "annotations"
REIHEN_PARAM_REFERENCES = "references"
result, err := NewSearchBaende(app, params)
if err != nil {
return engine.Response404(e, err, nil)
}
BAENDE_PARAM_ALM_NR = "alm"
BAENDE_PARAM_TITLE = "title"
BAENDE_PARAM_SERIES = "series"
BAENDE_PARAM_PERSONS = "persons"
BAENDE_PARAM_PLACES = "places"
BAENDE_PARAM_REFS = "references"
BAENDE_PARAM_ANNOTATIONS = "annotations"
BAENDE_PARAM_YEAR = "year"
// INFO: this is expanded search only:
BAENDE_PARAM_PSEUDONYMS = "pseudonyms"
// INFO: this is a filter type & expanded search:
BAENDE_PARAM_STATE = "state" // STATE: "full" "partial" "none"
)
data["parameters"] = params
data["result"] = result
return engine.Response200(e, p.Template+params.Collection+"/", data, p.Layout)
}
var ErrInvalidCollectionType = fmt.Errorf("Invalid collection type")
var ErrNoQuery = fmt.Errorf("No query")
type Parameters struct {
Extended bool
@@ -145,437 +129,3 @@ func NewParameters(e *core.RequestEvent) (*Parameters, error) {
func (p *Parameters) NormalizeQuery() dbmodels.Query {
return dbmodels.NormalizeQuery(p.Query)
}
type SearchParameters struct {
Parameters
Sort string
Annotations bool
Persons bool
Title bool
Series bool
Places bool
Refs bool
Year bool
AnnotationsString string
PersonsString string
TitleString string
AlmString string
SeriesString string
PlacesString string
RefsString string
YearString string
TypeFilter string
}
func NewSearchParameters(e *core.RequestEvent, p Parameters) (*SearchParameters, error) {
title := e.Request.URL.Query().Get(BAENDE_PARAM_TITLE) == "on"
series := e.Request.URL.Query().Get(BAENDE_PARAM_SERIES) == "on"
persons := e.Request.URL.Query().Get(BAENDE_PARAM_PERSONS) == "on"
places := e.Request.URL.Query().Get(BAENDE_PARAM_PLACES) == "on"
refs := e.Request.URL.Query().Get(BAENDE_PARAM_REFS) == "on"
annotations := e.Request.URL.Query().Get(BAENDE_PARAM_ANNOTATIONS) == "on"
year := e.Request.URL.Query().Get(BAENDE_PARAM_YEAR) == "on"
almstring := e.Request.URL.Query().Get(BAENDE_PARAM_ALM_NR + "string")
titlestring := e.Request.URL.Query().Get(BAENDE_PARAM_TITLE + "string")
seriesstring := e.Request.URL.Query().Get(BAENDE_PARAM_SERIES + "string")
personsstring := e.Request.URL.Query().Get(BAENDE_PARAM_PERSONS + "string")
placesstring := e.Request.URL.Query().Get(BAENDE_PARAM_PLACES + "string")
annotationsstring := e.Request.URL.Query().Get(BAENDE_PARAM_ANNOTATIONS + "string")
yearstring := e.Request.URL.Query().Get(BAENDE_PARAM_YEAR + "string")
refss := e.Request.URL.Query().Get(BAENDE_PARAM_REFS + "string")
if refss != "" {
refss = "\"" + refss + "\""
}
sort := e.Request.URL.Query().Get("sort")
if sort == "" {
sort = "year"
}
return &SearchParameters{
Parameters: p,
Sort: sort,
// INFO: Common parameters
Title: title,
Persons: persons,
Annotations: annotations,
// INFO: Baende parameters
Places: places,
Refs: refs,
Year: year,
Series: series,
// INFO: Expanded search
AlmString: almstring,
TitleString: titlestring,
SeriesString: seriesstring,
PersonsString: personsstring,
PlacesString: placesstring,
RefsString: refss,
AnnotationsString: annotationsstring,
YearString: yearstring,
}, nil
}
func (p SearchParameters) AllSearchTerms() string {
res := []string{}
res = append(res, p.includedParams(p.Query)...)
res = append(res, p.includedParams(p.AnnotationsString)...)
res = append(res, p.includedParams(p.PersonsString)...)
res = append(res, p.includedParams(p.TitleString)...)
res = append(res, p.includedParams(p.SeriesString)...)
res = append(res, p.includedParams(p.PlacesString)...)
res = append(res, p.includedParams(p.RefsString)...)
res = append(res, p.includedParams(p.YearString)...)
res = append(res, p.AlmString)
return strings.Join(res, " ")
}
func (p SearchParameters) includedParams(q string) []string {
res := []string{}
que := dbmodels.NormalizeQuery(q)
for _, qq := range que.Include {
res = append(res, qq)
}
for _, qq := range que.UnsafeI {
res = append(res, qq)
}
return res
}
func (p SearchParameters) ToQueryParams() string {
q := "?"
// TODO: use variables, url escape
if p.Extended {
q += "extended=true"
}
if p.Query != "" {
q += fmt.Sprintf("q=%s", p.Query)
if p.Title {
q += "&title=on"
}
if p.Persons {
q += "&persons=on"
}
if p.Annotations {
q += "&annotations=on"
}
if p.Series {
q += "&series=on"
}
if p.Places {
q += "&places=on"
}
if p.Refs {
q += "&references=on"
}
if p.Year {
q += "&year=on"
}
}
if p.YearString != "" {
q += fmt.Sprintf("&yearstring=%s", p.YearString)
}
if p.AnnotationsString != "" {
q += fmt.Sprintf("&annotationsstring=%s", p.AnnotationsString)
}
if p.PersonsString != "" {
q += fmt.Sprintf("&personsstring=%s", p.PersonsString)
}
if p.TitleString != "" {
q += fmt.Sprintf("&titlestring=%s", p.TitleString)
}
if p.SeriesString != "" {
q += fmt.Sprintf("&seriesstring=%s", p.SeriesString)
}
if p.PlacesString != "" {
q += fmt.Sprintf("&placesstring=%s", p.PlacesString)
}
if p.RefsString != "" {
q += fmt.Sprintf("&refsstring=%s", p.RefsString)
}
return q
}
func (p SearchParameters) IsBaendeSearch() bool {
return p.Collection == "baende" && (p.Query != "" || p.AlmString != "" || p.AnnotationsString != "" || p.PersonsString != "" || p.TitleString != "" || p.SeriesString != "" || p.PlacesString != "" || p.RefsString != "" || p.YearString != "")
}
func (p SearchParameters) FieldSetBaende() []dbmodels.FTS5QueryRequest {
ret := []dbmodels.FTS5QueryRequest{}
if p.Query != "" {
fields := []string{dbmodels.ID_FIELD}
if p.Title {
// INFO: Preferred Title is not here to avoid hitting the Reihentitel
fields = append(fields,
dbmodels.TITLE_STMT_FIELD,
dbmodels.SUBTITLE_STMT_FIELD,
dbmodels.INCIPIT_STMT_FIELD,
dbmodels.VARIANT_TITLE_FIELD,
dbmodels.PARALLEL_TITLE_FIELD,
)
}
if p.Series {
fields = append(fields, dbmodels.SERIES_TABLE)
}
if p.Persons {
fields = append(fields, dbmodels.RESPONSIBILITY_STMT_FIELD, dbmodels.AGENTS_TABLE)
}
if p.Places {
fields = append(fields, dbmodels.PLACES_TABLE, dbmodels.PLACE_STMT_FIELD)
}
if p.Refs {
fields = append(fields, dbmodels.REFERENCES_FIELD)
}
if p.Annotations {
fields = append(fields, dbmodels.ANNOTATION_FIELD)
}
if p.Year {
fields = append(fields, dbmodels.YEAR_FIELD)
}
que := p.NormalizeQuery()
req := dbmodels.IntoQueryRequests(fields, que)
ret = append(ret, req...)
}
if p.AnnotationsString != "" {
que := dbmodels.NormalizeQuery(p.AnnotationsString)
req := dbmodels.IntoQueryRequests([]string{dbmodels.ANNOTATION_FIELD}, que)
ret = append(ret, req...)
}
if p.PersonsString != "" {
que := dbmodels.NormalizeQuery(p.PersonsString)
req := dbmodels.IntoQueryRequests([]string{dbmodels.AGENTS_TABLE, dbmodels.RESPONSIBILITY_STMT_FIELD}, que)
ret = append(ret, req...)
}
if p.TitleString != "" {
que := dbmodels.NormalizeQuery(p.TitleString)
req := dbmodels.IntoQueryRequests([]string{dbmodels.TITLE_STMT_FIELD, dbmodels.SUBTITLE_STMT_FIELD, dbmodels.INCIPIT_STMT_FIELD, dbmodels.VARIANT_TITLE_FIELD, dbmodels.PARALLEL_TITLE_FIELD}, que)
ret = append(ret, req...)
}
if p.SeriesString != "" {
que := dbmodels.NormalizeQuery(p.SeriesString)
req := dbmodels.IntoQueryRequests([]string{dbmodels.SERIES_TABLE}, que)
ret = append(ret, req...)
}
if p.PlacesString != "" {
que := dbmodels.NormalizeQuery(p.PlacesString)
req := dbmodels.IntoQueryRequests([]string{dbmodels.PLACES_TABLE, dbmodels.PLACE_STMT_FIELD}, que)
ret = append(ret, req...)
}
if p.RefsString != "" {
que := dbmodels.NormalizeQuery(p.RefsString)
req := dbmodels.IntoQueryRequests([]string{dbmodels.REFERENCES_FIELD}, que)
ret = append(ret, req...)
}
if p.YearString != "" {
que := dbmodels.NormalizeQuery(p.YearString)
req := dbmodels.IntoQueryRequests([]string{dbmodels.YEAR_FIELD}, que)
ret = append(ret, req...)
}
return ret
}
func (p SearchParameters) IsExtendedSearch() bool {
return p.AnnotationsString != "" || p.PersonsString != "" || p.TitleString != "" || p.AlmString != "" || p.SeriesString != "" || p.PlacesString != "" || p.RefsString != "" || p.YearString != ""
}
func (p SearchParameters) NormalizeQuery() dbmodels.Query {
return dbmodels.NormalizeQuery(p.Query)
}
type SearchResultBaende struct {
Queries []dbmodels.FTS5QueryRequest
// these are the sorted IDs for hits
Hits []string
Series map[string]*dbmodels.Series // <- Key: Series ID
Entries map[string]*dbmodels.Entry // <- Key: Entry ID
Places map[string]*dbmodels.Place // <- All places, Key: Place IDs
Agents map[string]*dbmodels.Agent // <- Key: Agent IDs
// INFO: this is as they say doppelt gemoppelt bc of a logic error i made while tired
EntriesSeries map[string][]*dbmodels.REntriesSeries // <- Key: Entry ID
SeriesEntries map[string][]*dbmodels.REntriesSeries // <- Key: Series ID
EntriesAgents map[string][]*dbmodels.REntriesAgents // <- Key: Entry ID
}
func EmptyResultBaende() *SearchResultBaende {
return &SearchResultBaende{
Hits: []string{},
Series: make(map[string]*dbmodels.Series),
Entries: make(map[string]*dbmodels.Entry),
Places: make(map[string]*dbmodels.Place),
Agents: make(map[string]*dbmodels.Agent),
EntriesSeries: make(map[string][]*dbmodels.REntriesSeries),
SeriesEntries: make(map[string][]*dbmodels.REntriesSeries),
EntriesAgents: make(map[string][]*dbmodels.REntriesAgents),
}
}
func SimpleSearchBaende(app core.App, params SearchParameters) (*SearchResultBaende, error) {
entries := []*dbmodels.Entry{}
queries := params.FieldSetBaende()
if params.AlmString != "" {
e, err := dbmodels.Entries_MusenalmID(app, params.AlmString)
if err != nil && err == sql.ErrNoRows {
return EmptyResultBaende(), nil
} else if err != nil {
return nil, err
}
entries = append(entries, e)
} else {
if len(queries) == 0 {
return nil, ErrNoQuery
}
ids, err := dbmodels.FTS5Search(app, dbmodels.ENTRIES_TABLE, queries...)
if err != nil {
return nil, err
}
resultids := []any{}
for _, id := range ids {
resultids = append(resultids, id.ID)
}
e, err := dbmodels.Entries_IDs(app, resultids)
if err != nil {
return nil, err
}
entries = e
}
resultids := []any{}
for _, entry := range entries {
resultids = append(resultids, entry.Id)
}
entriesmap := make(map[string]*dbmodels.Entry)
for _, entry := range entries {
entriesmap[entry.Id] = entry
}
series, relations, err := Series_Entries(app, entries)
if err != nil {
return nil, err
}
seriesmap := make(map[string]*dbmodels.Series)
for _, s := range series {
seriesmap[s.Id] = s
}
relationsmap := make(map[string][]*dbmodels.REntriesSeries)
invrelationsmap := make(map[string][]*dbmodels.REntriesSeries)
for _, r := range relations {
invrelationsmap[r.Series()] = append(invrelationsmap[r.Series()], r)
relationsmap[r.Entry()] = append(relationsmap[r.Entry()], r)
}
agents, arelations, err := Agents_Entries_IDs(app, resultids)
if err != nil {
return nil, err
}
agentsmap := make(map[string]*dbmodels.Agent)
for _, a := range agents {
agentsmap[a.Id] = a
}
relationsagentsmap := make(map[string][]*dbmodels.REntriesAgents)
for _, r := range arelations {
relationsagentsmap[r.Entry()] = append(relationsagentsmap[r.Entry()], r)
}
placesids := []any{}
for _, entry := range entries {
for _, place := range entry.Places() {
placesids = append(placesids, place)
}
}
places, err := dbmodels.Places_IDs(app, placesids)
if err != nil {
return nil, err
}
placesmap := make(map[string]*dbmodels.Place)
for _, place := range places {
placesmap[place.Id] = place
}
hits := []string{}
if params.Sort == "series" {
dbmodels.Sort_Series_Title(series)
for _, s := range series {
hits = append(hits, s.Id)
}
} else {
dbmodels.Sort_Entries_Year_Title(entries)
for _, e := range entries {
hits = append(hits, e.Id)
}
}
return &SearchResultBaende{
Hits: hits,
Series: seriesmap,
Entries: entriesmap,
Places: placesmap,
Agents: agentsmap,
EntriesSeries: relationsmap,
SeriesEntries: invrelationsmap,
EntriesAgents: relationsagentsmap,
}, nil
}
func (r SearchResultBaende) Count() int {
return len(r.Entries)
}
func (r SearchResultBaende) SeriesCount() int {
return len(r.Series)
}
func Agents_Entries_IDs(app core.App, ids []any) ([]*dbmodels.Agent, []*dbmodels.REntriesAgents, error) {
relations, err := dbmodels.REntriesAgents_Entries(app, ids)
if err != nil {
return nil, nil, err
}
agentids := []any{}
for _, r := range relations {
agentids = append(agentids, r.Agent())
}
agents, err := dbmodels.Agents_IDs(app, agentids)
if err != nil {
return nil, nil, err
}
return agents, relations, nil
}

186
pages/suche_baende.go Normal file
View File

@@ -0,0 +1,186 @@
package pages
import (
"database/sql"
"github.com/Theodor-Springmann-Stiftung/musenalm/dbmodels"
"github.com/pocketbase/pocketbase/core"
)
type SearchResultBaende struct {
Queries []dbmodels.FTS5QueryRequest
// these are the sorted IDs for hits
Hits []string
Series map[string]*dbmodels.Series // <- Key: Series ID
Entries map[string]*dbmodels.Entry // <- Key: Entry ID
Places map[string]*dbmodels.Place // <- All places, Key: Place IDs
Agents map[string]*dbmodels.Agent // <- Key: Agent IDs
// INFO: this is as they say doppelt gemoppelt bc of a logic error i made while tired
EntriesSeries map[string][]*dbmodels.REntriesSeries // <- Key: Entry ID
SeriesEntries map[string][]*dbmodels.REntriesSeries // <- Key: Series ID
EntriesAgents map[string][]*dbmodels.REntriesAgents // <- Key: Entry ID
}
func EmptyResultBaende() *SearchResultBaende {
return &SearchResultBaende{
Hits: []string{},
Series: make(map[string]*dbmodels.Series),
Entries: make(map[string]*dbmodels.Entry),
Places: make(map[string]*dbmodels.Place),
Agents: make(map[string]*dbmodels.Agent),
EntriesSeries: make(map[string][]*dbmodels.REntriesSeries),
SeriesEntries: make(map[string][]*dbmodels.REntriesSeries),
EntriesAgents: make(map[string][]*dbmodels.REntriesAgents),
}
}
func NewSearchBaende(app core.App, params SearchParameters) (*SearchResultBaende, error) {
entries := []*dbmodels.Entry{}
queries := params.FieldSetBaende()
if params.AlmString != "" {
e, err := dbmodels.Entries_MusenalmID(app, params.AlmString)
if err != nil && err == sql.ErrNoRows {
return EmptyResultBaende(), nil
} else if err != nil {
return nil, err
}
entries = append(entries, e)
} else {
if len(queries) == 0 {
return nil, ErrNoQuery
}
ids, err := dbmodels.FTS5Search(app, dbmodels.ENTRIES_TABLE, queries...)
if err != nil {
return nil, err
} else if len(ids) == 0 {
return EmptyResultBaende(), nil
}
resultids := []any{}
for _, id := range ids {
resultids = append(resultids, id.ID)
}
e, err := dbmodels.Entries_IDs(app, resultids)
if err != nil {
return nil, err
}
entries = e
}
resultids := []any{}
for _, entry := range entries {
resultids = append(resultids, entry.Id)
}
entriesmap := make(map[string]*dbmodels.Entry)
for _, entry := range entries {
entriesmap[entry.Id] = entry
}
series, relations, err := Series_Entries(app, entries)
if err != nil {
return nil, err
}
seriesmap := make(map[string]*dbmodels.Series)
for _, s := range series {
seriesmap[s.Id] = s
}
relationsmap := make(map[string][]*dbmodels.REntriesSeries)
invrelationsmap := make(map[string][]*dbmodels.REntriesSeries)
for _, r := range relations {
invrelationsmap[r.Series()] = append(invrelationsmap[r.Series()], r)
relationsmap[r.Entry()] = append(relationsmap[r.Entry()], r)
}
agents, arelations, err := Agents_Entries_IDs(app, resultids)
if err != nil {
return nil, err
}
agentsmap := make(map[string]*dbmodels.Agent)
for _, a := range agents {
agentsmap[a.Id] = a
}
relationsagentsmap := make(map[string][]*dbmodels.REntriesAgents)
for _, r := range arelations {
relationsagentsmap[r.Entry()] = append(relationsagentsmap[r.Entry()], r)
}
placesids := []any{}
for _, entry := range entries {
for _, place := range entry.Places() {
placesids = append(placesids, place)
}
}
places, err := dbmodels.Places_IDs(app, placesids)
if err != nil {
return nil, err
}
placesmap := make(map[string]*dbmodels.Place)
for _, place := range places {
placesmap[place.Id] = place
}
hits := []string{}
if params.Sort == "series" {
dbmodels.Sort_Series_Title(series)
for _, s := range series {
hits = append(hits, s.Id)
}
} else {
dbmodels.Sort_Entries_Year_Title(entries)
for _, e := range entries {
hits = append(hits, e.Id)
}
}
return &SearchResultBaende{
Hits: hits,
Series: seriesmap,
Entries: entriesmap,
Places: placesmap,
Agents: agentsmap,
EntriesSeries: relationsmap,
SeriesEntries: invrelationsmap,
EntriesAgents: relationsagentsmap,
}, nil
}
func (r SearchResultBaende) Count() int {
return len(r.Entries)
}
func (r SearchResultBaende) SeriesCount() int {
return len(r.Series)
}
func Agents_Entries_IDs(app core.App, ids []any) ([]*dbmodels.Agent, []*dbmodels.REntriesAgents, error) {
relations, err := dbmodels.REntriesAgents_Entries(app, ids)
if err != nil {
return nil, nil, err
}
agentids := []any{}
for _, r := range relations {
agentids = append(agentids, r.Agent())
}
agents, err := dbmodels.Agents_IDs(app, agentids)
if err != nil {
return nil, nil, err
}
return agents, relations, nil
}

188
pages/suche_beitraege.go Normal file
View File

@@ -0,0 +1,188 @@
package pages
import (
"database/sql"
"github.com/Theodor-Springmann-Stiftung/musenalm/dbmodels"
"github.com/Theodor-Springmann-Stiftung/musenalm/helpers/datatypes"
"github.com/pocketbase/pocketbase/core"
)
const (
DEFAULT_PAGESIZE = 80
)
type SearchResultBeitraege struct {
Queries []dbmodels.FTS5QueryRequest
// these are the sorted IDs for hits
Hits []string
Entries map[string]*dbmodels.Entry // <- Key: Entry ID
Agents map[string]*dbmodels.Agent // <- Key: Agent IDs
Contents map[string][]*dbmodels.Content // <- Key: Entry ID, or year
ContentsAgents map[string][]*dbmodels.RContentsAgents // <- Key: Content ID
Pages []int
}
func EmptyResultBeitraege() *SearchResultBeitraege {
return &SearchResultBeitraege{
Hits: []string{},
Entries: make(map[string]*dbmodels.Entry),
Agents: make(map[string]*dbmodels.Agent),
Contents: make(map[string][]*dbmodels.Content),
ContentsAgents: make(map[string][]*dbmodels.RContentsAgents),
}
}
func NewSearchBeitraege(app core.App, params SearchParameters) (*SearchResultBeitraege, error) {
contents := []*dbmodels.Content{}
queries := params.FieldSetBeitraege()
if params.AlmString != "" {
e, err := dbmodels.Contents_MusenalmID(app, params.AlmString)
if err != nil && err == sql.ErrNoRows {
return EmptyResultBeitraege(), nil
} else if err != nil {
return nil, err
}
contents = append(contents, e)
} else {
if len(queries) == 0 {
return nil, ErrNoQuery
}
hits, err := dbmodels.FTS5Search(app, dbmodels.CONTENTS_TABLE, queries...)
if err != nil {
return nil, err
} else if len(hits) == 0 {
return EmptyResultBeitraege(), nil
}
ids := []any{}
for _, hit := range hits {
ids = append(ids, hit.ID)
}
cs, err := dbmodels.Contents_IDs(app, ids)
if err != nil {
return nil, err
}
contents = append(contents, cs...)
}
resultids := []any{}
resultentryids := []string{}
for _, content := range contents {
resultids = append(resultids, content.Id)
resultentryids = append(resultentryids, content.Entry())
}
entries, err := dbmodels.Entries_IDs(app, datatypes.ToAny(resultentryids))
if err != nil {
return nil, err
}
if params.Sort == "year" {
dbmodels.Sort_Entries_Year_Title(entries)
} else {
dbmodels.Sort_Entries_Title_Year(entries)
}
arels, err := dbmodels.RContentsAgents_Contents(app, resultids)
if err != nil {
return nil, err
}
aids := []any{}
for _, a := range arels {
aids = append(aids, a.Agent())
}
agents, err := dbmodels.Agents_IDs(app, aids)
if err != nil {
return nil, err
}
contentsmap := make(map[string][]*dbmodels.Content)
for _, c := range contents {
contentsmap[c.Entry()] = append(contentsmap[c.Entry()], c)
}
contentsagents := make(map[string][]*dbmodels.RContentsAgents)
for _, a := range arels {
contentsagents[a.Content()] = append(contentsagents[a.Content()], a)
}
agentsmap := make(map[string]*dbmodels.Agent)
for _, a := range agents {
agentsmap[a.Id] = a
}
entriesmap := make(map[string]*dbmodels.Entry)
for _, e := range entries {
entriesmap[e.Id] = e
}
hits := []string{}
for _, e := range entries {
hits = append(hits, e.Id)
}
pages := PagesEntries(hits, contentsmap, DEFAULT_PAGESIZE)
if params.Page < 1 || params.Page > len(pages) {
params.Page = 1
}
if params.Page == len(pages) {
hits = hits[pages[params.Page-1]:]
} else {
hits = hits[pages[params.Page-1]:pages[params.Page]]
}
return &SearchResultBeitraege{
Queries: queries,
Hits: hits,
Entries: entriesmap,
Agents: agentsmap,
Contents: contentsmap,
ContentsAgents: contentsagents,
Pages: pages,
}, nil
}
func (p *SearchResultBeitraege) CountEntries() int {
return len(p.Entries)
}
func (p *SearchResultBeitraege) Count() int {
cnt := 0
for _, c := range p.Contents {
cnt += len(c)
}
return cnt
}
func (p *SearchResultBeitraege) PagesCount() int {
return len(p.Pages) - 1
}
func PagesEntries[T any](hits []string, hitmap map[string][]*T, pagesize int) []int {
ret := []int{0}
m := 0
for i, hit := range hits {
m += len(hitmap[hit])
if m >= pagesize {
ret = append(ret, i)
m = 0
}
}
if m > 0 {
ret = append(ret, len(hits))
}
return ret
}

465
pages/suche_parameters.go Normal file
View File

@@ -0,0 +1,465 @@
package pages
import (
"fmt"
"strconv"
"strings"
"github.com/Theodor-Springmann-Stiftung/musenalm/dbmodels"
"github.com/pocketbase/pocketbase/core"
)
var ErrNoQuery = fmt.Errorf("No query")
const (
SEARCH_PARAM_ALM_NR = "alm"
SEARCH_PARAM_TITLE = "title"
SEARCH_PARAM_PERSONS = "persons"
SEARCH_PARAM_ANNOTATIONS = "annotations"
SEARCH_PARAM_YEAR = "year"
BAENDE_PARAM_PLACES = "places"
BAENDE_PARAM_SERIES = "series"
BAENDE_PARAM_REFS = "references"
BEITRAEGE_PARAM_ENTRY = "entry"
BEITRAEGE_PARAM_INCIPT = "incipit"
)
type SearchParameters struct {
Parameters
Sort string // year, entry,
Annotations bool
Persons bool
Title bool
Series bool
Places bool
Refs bool
Year bool
Entry bool
Incipit bool
AnnotationsString string
PersonsString string
TitleString string
AlmString string
SeriesString string
PlacesString string
RefsString string
YearString string
EntryString string
IncipitString string
Page int
}
func NewSearchParameters(e *core.RequestEvent, p Parameters) (*SearchParameters, error) {
almstring := e.Request.URL.Query().Get(SEARCH_PARAM_ALM_NR + "string")
title := e.Request.URL.Query().Get(SEARCH_PARAM_TITLE) == "on"
titlestring := e.Request.URL.Query().Get(SEARCH_PARAM_TITLE + "string")
persons := e.Request.URL.Query().Get(SEARCH_PARAM_PERSONS) == "on"
personsstring := e.Request.URL.Query().Get(SEARCH_PARAM_PERSONS + "string")
annotations := e.Request.URL.Query().Get(SEARCH_PARAM_ANNOTATIONS) == "on"
annotationsstring := e.Request.URL.Query().Get(SEARCH_PARAM_ANNOTATIONS + "string")
year := e.Request.URL.Query().Get(SEARCH_PARAM_YEAR) == "on"
yearstring := e.Request.URL.Query().Get(SEARCH_PARAM_YEAR + "string")
series := e.Request.URL.Query().Get(BAENDE_PARAM_SERIES) == "on"
seriesstring := e.Request.URL.Query().Get(BAENDE_PARAM_SERIES + "string")
places := e.Request.URL.Query().Get(BAENDE_PARAM_PLACES) == "on"
placesstring := e.Request.URL.Query().Get(BAENDE_PARAM_PLACES + "string")
refs := e.Request.URL.Query().Get(BAENDE_PARAM_REFS) == "on"
refss := e.Request.URL.Query().Get(BAENDE_PARAM_REFS + "string")
if refss != "" {
refss = "\"" + refss + "\""
}
incipit := e.Request.URL.Query().Get(BEITRAEGE_PARAM_INCIPT) == "on"
incipitstring := e.Request.URL.Query().Get(BEITRAEGE_PARAM_INCIPT + "string")
entry := e.Request.URL.Query().Get(BEITRAEGE_PARAM_ENTRY) == "on"
entrystring := e.Request.URL.Query().Get(BEITRAEGE_PARAM_ENTRY + "string")
sort := e.Request.URL.Query().Get("sort")
if sort == "" {
sort = "year"
}
page := e.Request.URL.Query().Get("page")
if page == "" {
page = "1"
}
pageint, err := strconv.Atoi(page)
if err != nil {
return nil, err
}
return &SearchParameters{
Parameters: p,
Sort: sort,
Page: pageint,
// INFO: Common parameters
Title: title,
Persons: persons,
Annotations: annotations,
Year: year,
// INFO: Baende parameters
Places: places,
Refs: refs,
Series: series,
// INFO: Beitraege parameters
Entry: entry,
Incipit: incipit,
// INFO: Expanded search
AlmString: almstring,
TitleString: titlestring,
SeriesString: seriesstring,
PersonsString: personsstring,
PlacesString: placesstring,
RefsString: refss,
AnnotationsString: annotationsstring,
YearString: yearstring,
EntryString: entrystring,
IncipitString: incipitstring,
}, nil
}
func (p SearchParameters) AllSearchTermsBaende() string {
res := []string{}
res = append(res, p.includedParams(p.Query)...)
res = append(res, p.includedParams(p.AnnotationsString)...)
res = append(res, p.includedParams(p.PersonsString)...)
res = append(res, p.includedParams(p.TitleString)...)
res = append(res, p.includedParams(p.SeriesString)...)
res = append(res, p.includedParams(p.PlacesString)...)
res = append(res, p.includedParams(p.RefsString)...)
res = append(res, p.includedParams(p.YearString)...)
res = append(res, p.AlmString)
return strings.Join(res, " ")
}
func (p SearchParameters) AllSearchTermsBeitraege() string {
res := []string{}
res = append(res, p.includedParams(p.Query)...)
res = append(res, p.includedParams(p.AnnotationsString)...)
res = append(res, p.includedParams(p.PersonsString)...)
res = append(res, p.includedParams(p.TitleString)...)
res = append(res, p.includedParams(p.YearString)...)
res = append(res, p.includedParams(p.EntryString)...)
res = append(res, p.includedParams(p.IncipitString)...)
return strings.Join(res, " ")
}
func (p SearchParameters) includedParams(q string) []string {
res := []string{}
que := dbmodels.NormalizeQuery(q)
for _, qq := range que.Include {
res = append(res, qq)
}
for _, qq := range que.UnsafeI {
res = append(res, qq)
}
return res
}
func (p SearchParameters) ToQueryParamsBeitraege() string {
q := "?"
if p.Extended {
q += "extended=true"
}
if p.Query != "" {
q += fmt.Sprintf("q=%s", p.Query)
if p.Title {
q += "&title=on"
}
if p.Persons {
q += "&persons=on"
}
if p.Annotations {
q += "&annotations=on"
}
if p.Year {
q += "&year=on"
}
if p.Entry {
q += "&entry=on"
}
if p.Incipit {
q += "&incipit=on"
}
}
if p.YearString != "" {
q += fmt.Sprintf("&yearstring=%s", p.YearString)
}
if p.AnnotationsString != "" {
q += fmt.Sprintf("&annotationsstring=%s", p.AnnotationsString)
}
if p.PersonsString != "" {
q += fmt.Sprintf("&personsstring=%s", p.PersonsString)
}
if p.TitleString != "" {
q += fmt.Sprintf("&titlestring=%s", p.TitleString)
}
if p.EntryString != "" {
q += fmt.Sprintf("&entrystring=%s", p.EntryString)
}
if p.IncipitString != "" {
q += fmt.Sprintf("&incipitstring=%s", p.IncipitString)
}
return q
}
func (p SearchParameters) ToQueryParamsBaende() string {
q := "?"
// TODO: use variables, url escape
if p.Extended {
q += "extended=true"
}
if p.Query != "" {
q += fmt.Sprintf("q=%s", p.Query)
if p.Title {
q += "&title=on"
}
if p.Persons {
q += "&persons=on"
}
if p.Annotations {
q += "&annotations=on"
}
if p.Series {
q += "&series=on"
}
if p.Places {
q += "&places=on"
}
if p.Refs {
q += "&references=on"
}
if p.Year {
q += "&year=on"
}
}
if p.YearString != "" {
q += fmt.Sprintf("&yearstring=%s", p.YearString)
}
if p.AnnotationsString != "" {
q += fmt.Sprintf("&annotationsstring=%s", p.AnnotationsString)
}
if p.PersonsString != "" {
q += fmt.Sprintf("&personsstring=%s", p.PersonsString)
}
if p.TitleString != "" {
q += fmt.Sprintf("&titlestring=%s", p.TitleString)
}
if p.SeriesString != "" {
q += fmt.Sprintf("&seriesstring=%s", p.SeriesString)
}
if p.PlacesString != "" {
q += fmt.Sprintf("&placesstring=%s", p.PlacesString)
}
if p.RefsString != "" {
q += fmt.Sprintf("&refsstring=%s", p.RefsString)
}
return q
}
func (p SearchParameters) IsBeitraegeSearch() bool {
return p.Collection == "beitraege" && (p.Query != "" || p.AlmString != "" || p.AnnotationsString != "" || p.PersonsString != "" || p.TitleString != "" || p.YearString != "" || p.EntryString != "" || p.IncipitString != "")
}
func (p SearchParameters) IsBaendeSearch() bool {
return p.Collection == "baende" && (p.Query != "" || p.AlmString != "" || p.AnnotationsString != "" || p.PersonsString != "" || p.TitleString != "" || p.SeriesString != "" || p.PlacesString != "" || p.RefsString != "" || p.YearString != "")
}
func (p SearchParameters) FieldSetBeitraege() []dbmodels.FTS5QueryRequest {
ret := []dbmodels.FTS5QueryRequest{}
if p.Query != "" {
fields := []string{dbmodels.ID_FIELD}
if p.Title {
fields = append(fields, dbmodels.TITLE_STMT_FIELD, dbmodels.SUBTITLE_STMT_FIELD, dbmodels.VARIANT_TITLE_FIELD, dbmodels.PARALLEL_TITLE_FIELD)
}
if p.Persons {
fields = append(fields, dbmodels.RESPONSIBILITY_STMT_FIELD, dbmodels.AGENTS_TABLE)
}
if p.Annotations {
fields = append(fields, dbmodels.ANNOTATION_FIELD)
}
if p.Year {
fields = append(fields, dbmodels.YEAR_FIELD)
}
if p.Entry {
fields = append(fields, dbmodels.ENTRIES_TABLE)
}
if p.Incipit {
fields = append(fields, dbmodels.INCIPIT_STMT_FIELD)
}
que := p.NormalizeQuery()
req := dbmodels.IntoQueryRequests(fields, que)
ret = append(ret, req...)
}
if p.AnnotationsString != "" {
que := dbmodels.NormalizeQuery(p.AnnotationsString)
req := dbmodels.IntoQueryRequests([]string{dbmodels.ANNOTATION_FIELD}, que)
ret = append(ret, req...)
}
if p.PersonsString != "" {
que := dbmodels.NormalizeQuery(p.PersonsString)
req := dbmodels.IntoQueryRequests([]string{dbmodels.AGENTS_TABLE, dbmodels.RESPONSIBILITY_STMT_FIELD}, que)
ret = append(ret, req...)
}
if p.TitleString != "" {
que := dbmodels.NormalizeQuery(p.TitleString)
req := dbmodels.IntoQueryRequests([]string{dbmodels.TITLE_STMT_FIELD, dbmodels.SUBTITLE_STMT_FIELD, dbmodels.VARIANT_TITLE_FIELD, dbmodels.PARALLEL_TITLE_FIELD}, que)
ret = append(ret, req...)
}
if p.YearString != "" {
que := dbmodels.NormalizeQuery(p.YearString)
req := dbmodels.IntoQueryRequests([]string{dbmodels.YEAR_FIELD}, que)
ret = append(ret, req...)
}
if p.EntryString != "" {
que := dbmodels.NormalizeQuery(p.EntryString)
req := dbmodels.IntoQueryRequests([]string{dbmodels.ENTRIES_TABLE}, que)
ret = append(ret, req...)
}
if p.IncipitString != "" {
que := dbmodels.NormalizeQuery(p.IncipitString)
req := dbmodels.IntoQueryRequests([]string{dbmodels.INCIPIT_STMT_FIELD}, que)
ret = append(ret, req...)
}
return ret
}
func (p SearchParameters) FieldSetBaende() []dbmodels.FTS5QueryRequest {
ret := []dbmodels.FTS5QueryRequest{}
if p.Query != "" {
fields := []string{dbmodels.ID_FIELD}
if p.Title {
// INFO: Preferred Title is not here to avoid hitting the Reihentitel
fields = append(fields,
dbmodels.TITLE_STMT_FIELD,
dbmodels.SUBTITLE_STMT_FIELD,
dbmodels.INCIPIT_STMT_FIELD,
dbmodels.VARIANT_TITLE_FIELD,
dbmodels.PARALLEL_TITLE_FIELD,
)
}
if p.Series {
fields = append(fields, dbmodels.SERIES_TABLE)
}
if p.Persons {
fields = append(fields, dbmodels.RESPONSIBILITY_STMT_FIELD, dbmodels.AGENTS_TABLE)
}
if p.Places {
fields = append(fields, dbmodels.PLACES_TABLE, dbmodels.PLACE_STMT_FIELD)
}
if p.Refs {
fields = append(fields, dbmodels.REFERENCES_FIELD)
}
if p.Annotations {
fields = append(fields, dbmodels.ANNOTATION_FIELD)
}
if p.Year {
fields = append(fields, dbmodels.YEAR_FIELD)
}
que := p.NormalizeQuery()
req := dbmodels.IntoQueryRequests(fields, que)
ret = append(ret, req...)
}
if p.AnnotationsString != "" {
que := dbmodels.NormalizeQuery(p.AnnotationsString)
req := dbmodels.IntoQueryRequests([]string{dbmodels.ANNOTATION_FIELD}, que)
ret = append(ret, req...)
}
if p.PersonsString != "" {
que := dbmodels.NormalizeQuery(p.PersonsString)
req := dbmodels.IntoQueryRequests([]string{dbmodels.AGENTS_TABLE, dbmodels.RESPONSIBILITY_STMT_FIELD}, que)
ret = append(ret, req...)
}
if p.TitleString != "" {
que := dbmodels.NormalizeQuery(p.TitleString)
req := dbmodels.IntoQueryRequests([]string{dbmodels.TITLE_STMT_FIELD, dbmodels.SUBTITLE_STMT_FIELD, dbmodels.INCIPIT_STMT_FIELD, dbmodels.VARIANT_TITLE_FIELD, dbmodels.PARALLEL_TITLE_FIELD}, que)
ret = append(ret, req...)
}
if p.SeriesString != "" {
que := dbmodels.NormalizeQuery(p.SeriesString)
req := dbmodels.IntoQueryRequests([]string{dbmodels.SERIES_TABLE}, que)
ret = append(ret, req...)
}
if p.PlacesString != "" {
que := dbmodels.NormalizeQuery(p.PlacesString)
req := dbmodels.IntoQueryRequests([]string{dbmodels.PLACES_TABLE, dbmodels.PLACE_STMT_FIELD}, que)
ret = append(ret, req...)
}
if p.RefsString != "" {
que := dbmodels.NormalizeQuery(p.RefsString)
req := dbmodels.IntoQueryRequests([]string{dbmodels.REFERENCES_FIELD}, que)
ret = append(ret, req...)
}
if p.YearString != "" {
que := dbmodels.NormalizeQuery(p.YearString)
req := dbmodels.IntoQueryRequests([]string{dbmodels.YEAR_FIELD}, que)
ret = append(ret, req...)
}
return ret
}
func (p SearchParameters) IsExtendedSearch() bool {
return p.AnnotationsString != "" || p.PersonsString != "" || p.TitleString != "" || p.AlmString != "" || p.SeriesString != "" || p.PlacesString != "" || p.RefsString != "" || p.YearString != "" || p.EntryString != "" || p.IncipitString != ""
}
func (p SearchParameters) NormalizeQuery() dbmodels.Query {
return dbmodels.NormalizeQuery(p.Query)
}
func (p SearchParameters) Prev() int {
if p.Page > 1 {
return p.Page - 1
}
return 1
}
func (p SearchParameters) Next() int {
return p.Page + 1
}

View File

@@ -64,6 +64,8 @@ TODO danach:
- HTMX + Smooth scroll
- Personen: related
- Inhaltsliste: Personen sehen komisch aus
- V\Boosted links gen by webcomponents
- Sammlungen u. Querverweise müssen die URL als Parameter bekommen
- Sammlungen neuer versuch
- Inhaltsliste Personen
- Sortierung nach Band A-Z?

View File

@@ -2,6 +2,9 @@
<html class="w-full h-full" {{ if .lang }}lang="{{ .lang }}"{{ end }}>
<head>
<meta charset="UTF-8" />
<meta
name="htmx-config"
content='{"defaultSwapStyle":"outerHTML", "scrollBehavior": "instant"}' />
{{ block "head" . }}
<!-- Default Head elements -->

View File

@@ -4,6 +4,32 @@
.3 - []*RContentsAgents
.4 - map[string]*Agent
.5 bool SingleView
.6 .parameters:
type SearchParameters struct {
Parameters
Sort string
Annotations bool
Persons bool
Title bool
Series bool
Places bool
Refs bool
Year bool
Entry bool
Incipit bool
AnnotationsString string
PersonsString string
TitleString string
AlmString string
SeriesString string
PlacesString string
RefsString string
YearString string
EntryString string
IncipitString string
}
*/}}
{{- $content := index . 0 -}}
@@ -21,6 +47,27 @@
{{- $entrySubView = index . 5 -}}
{{- end -}}
{{ $isAlm := false }}
{{ $isTitle := false }}
{{ $isYear := false }}
{{ $isPerson := false }}
{{ $isAnnotation := false }}
{{ $isIncipit := false }}
{{ $isEntry := false }}
{{- $searchparameters := false -}}
{{- if gt (len .) 6 -}}
{{- $searchparameters = index . 6 -}}
{{- $isAlm = $searchparameters.AlmString -}}
{{- $isTitle = or $searchparameters.Title $searchparameters.TitleString -}}
{{- $isYear = or $searchparameters.Year $searchparameters.YearString -}}
{{- $isPerson = or $searchparameters.Persons $searchparameters.PersonsString -}}
{{- $isAnnotation = or $searchparameters.Annotations $searchparameters.AnnotationsString -}}
{{- $isIncipit = or $searchparameters.Incipit $searchparameters.IncipitString -}}
{{- $isEntry = or $searchparameters.Entry $searchparameters.EntryString -}}
{{- end -}}
<div
class="content flex flex-row font-serif {{ if or $entrySubView $singleView -}}text-lg{{- end -}}"
@@ -50,8 +97,8 @@
<div class="fields">
{{- if or $singleView $entrySubView -}}
<div class="fieldlabel">Almanach</div>
<div class="fieldvalue">
<a href="/almanach/{{- $entry.MusenalmID -}}">
<div class="fieldvalue {{ if $isEntry }}search-text{{ end }}">
<a href="/almanach/{{- $entry.MusenalmID -}}#{{- $content.MusenalmID -}}">
{{- $entry.PreferredTitle -}}
</a>
{{- if $content.Extent -}}
@@ -62,15 +109,21 @@
{{- end -}}
{{- if $content.TitleStmt -}}
<div class="fieldlabel">Titel</div>
<div class="italic fieldvalue">{{- $content.TitleStmt -}}</div>
<div class="italic fieldvalue {{ if $isTitle }}search-text{{ end }}">
{{- $content.TitleStmt -}}
</div>
{{- end -}}
{{- if $content.IncipitStmt -}}
<div class="fieldlabel">Incipit</div>
<div class="italic fieldvalue">{{ $content.IncipitStmt }}…</div>
<div class="italic fieldvalue {{ if $isIncipit }}search-text{{ end }}">
{{ $content.IncipitStmt }}…
</div>
{{- end -}}
{{- if $content.ResponsibilityStmt -}}
<div class="fieldlabel">Autorangabe</div>
<div class="fieldvalue italic">{{- $content.ResponsibilityStmt -}}</div>
<div class="fieldvalue italic {{ if $isPerson }}search-text{{ end }}">
{{- $content.ResponsibilityStmt -}}
</div>
{{- end -}}
{{- if $rcas -}}
<div class="fieldlabel">Personen</div>
@@ -79,7 +132,11 @@
{{- range $_, $rca := $rcas -}}
{{- $agent := index $agents $rca.Agent -}}
<div class="font-sans text-base bg-stone-100 px-1 py-0.5 rounded w-max">
<a href="/person/{{- $agent.Id -}}">{{- $agent.Name -}}</a>
<a
href="/person/{{- $agent.Id -}}"
class="inline-block {{ if $isPerson }}search-text{{ end }}">
{{- $agent.Name -}}
</a>
({{ $agent.BiographicalData -}})
</div>
{{- end -}}
@@ -87,10 +144,11 @@
</div>
{{- end -}}
{{- if $content.Annotation -}}
{{- $link := printf "%s%s" "/almanach/" $entry.MusenalmIDString -}}
<div class="fieldlabel">Anmerkung</div>
<div class="fieldvalue">
<div class="fieldvalue {{ if $isAnnotation }}search-text{{ end }}">
{{- Safe (LinksAnnotation (ReplaceSlashParen
$content.Annotation))
$content.Annotation) $link)
-}}
</div>
{{- end -}}
@@ -131,7 +189,9 @@
<div class="w-24 shrink-0 grow-0 items-end flex flex-col gap-y-1 columnfour">
<div class="font-sans text-sm bg-stone-100 px-2 font-bold py-0.5 rounded w-max">
<span class="text-xs font-normal pr-1">NR</span>
{{ $content.MusenalmID -}}
<span class="{{ if $isAlm }}search-text{{ end }}">
{{- $content.MusenalmID -}}
</span>
</div>
{{- if $entrySubView -}}
{{- if $content.MusenalmType -}}

View File

@@ -1,5 +1,5 @@
<div class="container-normal">
<div class="text">
<div class="text mt-6">
{{ Safe .record.Text }}
</div>
</div>

View File

@@ -68,25 +68,7 @@
<div id="searchcontrol" class="container-normal">
{{- template "_heading" $model.parameters -}}
<div id="searchform" class="border-l border-zinc-300 px-8 py-10 relative">
<form
id="lookupform"
class="w-full font-serif grid grid-cols-12 gap-x-4 mb-4"
method="get"
action="/suche/baende"
autocomplete="off">
<label for="almstring" class="col-span-3 align-middle hidden">Almanach-Nummer:</label>
<input
autocomplete="off"
minlength="1"
required="true"
type="search"
name="almstring"
id="almstring"
value="{{ $model.parameters.AlmString }}"
placeholder="Alm-Nummer"
class="w-full col-span-3 placeholder:italic" />
<button id="submitbutton" type="submit" class="col-span-2">Nachschlagen</button>
</form>
{{- template "_musenalmidbox" Arr $model.parameters.AlmString "baende" -}}
<form
id="simplesearchform"
class="w-full font-serif"
@@ -122,11 +104,7 @@
type="checkbox"
name="persons"
id="persons"
{{ if or $isBase
$isPersons
-}}
checked
{{- end -}} />
{{ if or $isBase $isPersons -}}checked{{- end -}} />
<label for="persons">Personen &amp; Verlage</label>
</div>
<div class="selectgroup-option">
@@ -266,7 +244,7 @@
class="h-min pb-1 border-b-4 border-zinc-300 px-1.5"
name="sort"
id="sort"
hx-get="{{- $model.parameters.ToQueryParams -}}"
hx-get="{{- $model.parameters.ToQueryParamsBaende -}}"
trigger="change"
hx-push-url="true"
hx-select="main"
@@ -315,7 +293,7 @@
let mark_instance = new Mark(elements);
// INFO: we wait a little bit before marking, to settle everything
setTimeout(() => {
mark_instance.mark('{{ $model.parameters.AllSearchTerms }}', {
mark_instance.mark('{{ $model.parameters.AllSearchTermsBaende }}', {
"seperateWordSearch": true,
});
}, 200);
@@ -326,40 +304,3 @@
<div class="container-normal">Keine Bände gefunden.</div>
{{- end -}}
{{- end -}}
<script type="module">
const form = document.getElementById("simplesearchform");
let submitBtn = null;
if (form) {
submitBtn = form.querySelector("#submitbutton");
}
function checkValidity(f, btn) {
if (f.checkValidity()) {
btn.disabled = false;
} else {
btn.disabled = true;
}
}
if (form && submitBtn) {
checkValidity(form, submitBtn);
form.addEventListener("input", (event) => {
checkValidity(form, submitBtn);
});
}
const lookupform = document.getElementById("lookupform");
let lookupsubmitBtn = null;
if (lookupform) {
lookupsubmitBtn = lookupform.querySelector("#submitbutton");
}
if (lookupform && lookupsubmitBtn) {
checkValidity(lookupform, lookupsubmitBtn);
lookupform.addEventListener("input", (event) => {
checkValidity(lookupform, lookupsubmitBtn);
});
}
</script>

View File

@@ -1,11 +1,69 @@
{{ $model := . }}
{{/* .result:
type SearchResultBeitraege struct {
Queries []dbmodels.FTS5QueryRequest
// these are the sorted IDs for hits
Hits []string
Entries map[string]*dbmodels.Entry // <- Key: Entry ID
Agents map[string]*dbmodels.Agent // <- Key: Agent IDs
Contents map[string][]*dbmodels.Content // <- Key: Entry ID, or year
ContentsAgents map[string][]*dbmodels.RContentsAgents // <- Key: Content ID
}
.parameters:
type SearchParameters struct {
Parameters
Sort string
Annotations bool
Persons bool
Title bool
Series bool
Places bool
Refs bool
Year bool
Entry bool
Incipit bool
AnnotationsString string
PersonsString string
TitleString string
AlmString string
SeriesString string
PlacesString string
RefsString string
YearString string
EntryString string
IncipitString string
}
*/}}
{{ $isAlm := false }}
{{ $isTitle := false }}
{{ $isYear := false }}
{{ $isPerson := false }}
{{ $isAnnotation := false }}
{{ $isIncipit := false }}
{{ $isEntry := false }}
{{- $isAlm = $model.parameters.AlmString -}}
{{- $isTitle = or $model.parameters.Title $model.parameters.TitleString -}}
{{- $isYear = or $model.parameters.Year $model.parameters.YearString -}}
{{- $isPerson = or $model.parameters.Persons $model.parameters.PersonsString -}}
{{- $isAnnotation = or $model.parameters.Annotations $model.parameters.AnnotationsString -}}
{{- $isIncipit = or $model.parameters.Incipit $model.parameters.IncipitString -}}
{{- $isEntry = or $model.parameters.Entry $model.parameters.EntryString -}}
{{- $isBase := not (or $isTitle $isYear $isPerson $isAnnotation $isIncipit $isEntry) -}}
<div id="searchcontrol" class="container-normal">
{{- template "_heading" $model.parameters -}}
<div id="" class="border-l border-zinc-300 px-8 py-10 relative">
<div id="searchform" class="border-l border-zinc-300 px-8 py-10 relative">
{{- template "_musenalmidbox" Arr $model.parameters.AlmString "beitraege" -}}
<form
id="searchform"
id="simplesearchform"
class="w-full font-serif"
method="get"
action="/suche/beitraege"
@@ -18,29 +76,35 @@
{{ template "_searchboxsimple" Arr $model.parameters true $q }}
<fieldset class="selectgroup">
<div class="selectgroup-option">
<input type="checkbox" name="number" id="number" checked />
<label for="number">Almanach-Nr.</label>
<input type="checkbox" name="title" id="title"
{{ if or $isBase $isTitle -}}checked{{- end -}} />
<label for="title">Titel</label>
</div>
<div class="selectgroup-option">
<input type="checkbox" name="title" id="title" checked />
<label for="title">Titelinformationen</label>
<input type="checkbox" name="incipit" id="incipit"
{{ if or $isBase $isIncipit -}}checked{{- end -}} />
<label for="incipit">Incipit</label>
</div>
<div class="selectgroup-option">
<input type="checkbox" name="entry" id="entry" checked />
<label for="entry">Bandtitel</label>
</div>
<div class="selectgroup-option">
<input type="checkbox" name="person" id="person" checked />
<input type="checkbox" name="persons" id="persons"
{{ if or $isBase $isPerson -}}checked{{- end -}} />
<label for="person">Personen &amp; Pseudonyme</label>
</div>
<div class="selectgroup-option">
<input type="checkbox" name="annotations" id="annotations" checked />
<label for="annotations">Anmerkungen</label>
<input type="checkbox" name="entry" id="entry"
{{ if or $isBase $isEntry -}}checked{{- end -}} />
<label for="entry">Bandtitel</label>
</div>
<div class="selectgroup-option">
<input type="checkbox" name="year" id="year" checked />
<input type="checkbox" name="year" id="year"
{{ if or $isBase $isYear -}}checked{{- end -}} />
<label for="year">Jahr</label>
</div>
<div class="selectgroup-option">
<input type="checkbox" name="annotations" id="annotations"
{{ if or $isBase $isAnnotation -}}checked{{- end -}} />
<label for="annotations">Anmerkungen</label>
</div>
</fieldset>
{{ template "_infotextsimple" true }}
</div>
@@ -49,3 +113,127 @@
</div>
{{- template "_fieldscript" -}}
{{- if $model.parameters.IsBeitraegeSearch -}}
<div class="container-normal" id="searchresults">
<div class="border-b border-zinc-300 flex flex-row justify-between">
<div>
{{ if $model.parameters.Query -}}
Suche nach <b>»{{ $model.parameters.Query }}«</b> &middot;
{{- end -}}
{{- if $isAlm -}}
Inhaltsnummer <b>»{{ $model.parameters.AlmString }}«</b> &middot;
{{- end -}}
<i class="ri-book-line"></i>
{{ if eq $model.result.Count 1 -}}
Ein Beitrag
{{ else -}}
{{ $model.result.Count }} Beiträge
{{- end }}
in
{{ if eq ($model.result.Entries | len) 1 -}}
einem Band
{{ else -}}
{{ $model.result.Entries | len }} Bänden
{{- end -}}
</div>
{{- if gt (len $model.result.Pages) 1 }}
<div>
{{ if gt $model.parameters.Page 1 -}}
<a
href="{{- $model.parameters.ToQueryParamsBeitraege -}}&page={{ $model.parameters.Prev
}}" class="mr-1.5">
<i class="ri-arrow-left-long-line"></i>
</a>
{{- end -}}
S. {{ $model.parameters.Page }} /
{{ $model.result.PagesCount }}
{{ if lt $model.parameters.Page ($model.result.PagesCount) -}}
<a
href="{{- $model.parameters.ToQueryParamsBeitraege -}}&page={{ $model.parameters.Next
}}" class="ml-1.5">
<i class="ri-arrow-right-long-line"></i>
</a>
{{- end -}}
</div>
{{- end -}}
{{- if not $isAlm -}}
<div>
<label
for="sort"
class="align-baseline h-min self-end pb-1 mr-2 text-sm font-sans
text-stone-700"
>Sortierung</label
>
{{/* INFO: We always redrect to letter = A bc some letters dont exist for other professions */}}
<select
class="h-min pb-1 border-b-4 border-zinc-300 px-1.5"
name="sort"
id="sort"
autocomplete="off"
hx-get="{{- $model.parameters.ToQueryParamsBeitraege -}}"
trigger="change"
hx-push-url="true"
hx-select="main"
hx-target="main">
<option
value="year"
{{ if eq $model.parameters.Sort "year" -}}
selected
{{- end -}}>
Jahr
</option>
<option value="entry" {{ if eq $model.parameters.Sort "entry" -}}selected{{- end -}}>
Bände A-Z
</option>
</select>
</div>
{{- end -}}
</div>
{{- if $model.result.Hits -}}
<div class="mt-8" id="almanachcontents">
{{- range $_, $hit := $model.result.Hits -}}
{{- $e := index $model.result.Entries $hit -}}
{{- $contents := index $model.result.Contents $e.Id -}}
<div
class="font-serif flex flex-row justify-between text-stone-800
font-bold border-b pb-0.5 mb-2 tab-list-head items-end">
<div>
{{ $e.PreferredTitle }}
</div>
<div
class="inline-block font-sans bg-slate-800 text-white h-max text-sm px-1.5 rounded">
{{- len $contents -}}
</div>
</div>
<div class="mb-7 tab-list-panel">
{{- range $i, $c := $contents -}}
{{- $rels := index $model.result.ContentsAgents $c.Id -}}
{{- template "_content" Arr $c $e $rels $model.result.Agents false true
$model.parameters
-}}
{{- end -}}
</div>
{{- end -}}
</div>
{{- end -}}
</div>
<script type="module">
let elements = document.querySelectorAll('.search-text');
let mark_instance = new Mark(elements);
// INFO: we wait a little bit before marking, to settle everything
setTimeout(() => {
mark_instance.mark('{{ $model.parameters.AllSearchTermsBaende }}', {
"seperateWordSearch": true,
});
}, 200);
</script>
</div>
{{- end -}}

View File

@@ -14,4 +14,38 @@
}
}
});
const form = document.getElementById("simplesearchform");
let submitBtn = null;
if (form) {
submitBtn = form.querySelector("#submitbutton");
}
function checkValidity(f, btn) {
if (f.checkValidity()) {
btn.disabled = false;
} else {
btn.disabled = true;
}
}
if (form && submitBtn) {
checkValidity(form, submitBtn);
form.addEventListener("input", (event) => {
checkValidity(form, submitBtn);
});
}
const lookupform = document.getElementById("lookupform");
let lookupsubmitBtn = null;
if (lookupform) {
lookupsubmitBtn = lookupform.querySelector("#submitbutton");
}
if (lookupform && lookupsubmitBtn) {
checkValidity(lookupform, lookupsubmitBtn);
lookupform.addEventListener("input", (event) => {
checkValidity(lookupform, lookupsubmitBtn);
});
}
</script>

View File

@@ -0,0 +1,21 @@
{{ $p := index . 0 }}
{{ $t := index . 1 }}
<form
id="lookupform"
class="w-full font-serif grid grid-cols-12 gap-x-4 mb-4"
method="get"
action="/suche/{{- $t -}}"
autocomplete="off">
<label for="almstring" class="col-span-3 align-middle hidden">Almanach-Nummer:</label>
<input
autocomplete="off"
minlength="1"
required="true"
type="search"
name="almstring"
id="almstring"
value="{{ $p }}"
placeholder="Alm-Nummer"
class="w-full col-span-3 placeholder:italic" />
<button id="submitbutton" type="submit" class="col-span-2">Nachschlagen</button>
</form>

View File

@@ -46,6 +46,7 @@
@layer components {
html {
font-size: 16px;
scroll-behavior: auto !important;
}
@media (max-width: 1280px) {
html {
@@ -458,4 +459,8 @@
@apply bg-stone-100 pr-6 py-4;
/*direction: rtl;*/
}
.tab-list-head[aria-pressed="true"] {
@apply !text-slate-800 bg-stone-50;
}
}