mirror of
https://github.com/Theodor-Springmann-Stiftung/musenalm.git
synced 2025-10-29 01:05:32 +00:00
pages -> controllers
This commit is contained in:
35
controllers/404.go
Normal file
35
controllers/404.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"github.com/Theodor-Springmann-Stiftung/musenalm/app"
|
||||
"github.com/Theodor-Springmann-Stiftung/musenalm/pagemodels"
|
||||
"github.com/Theodor-Springmann-Stiftung/musenalm/templating"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/tools/router"
|
||||
)
|
||||
|
||||
const URL_ERROR_404 = "/errors/404/"
|
||||
const URL_ERROR_500 = "/errors/500/"
|
||||
|
||||
func init() {
|
||||
rp := &ErrorPage{
|
||||
StaticPage: pagemodels.StaticPage{
|
||||
Name: URL_ERROR_404,
|
||||
},
|
||||
}
|
||||
app.Register(rp)
|
||||
}
|
||||
|
||||
type ErrorPage struct {
|
||||
pagemodels.StaticPage
|
||||
}
|
||||
|
||||
func (p *ErrorPage) Setup(router *router.Router[*core.RequestEvent], app core.App, engine *templating.Engine) error {
|
||||
router.GET(URL_ERROR_404, func(e *core.RequestEvent) error {
|
||||
return engine.Response404(e, nil, nil)
|
||||
})
|
||||
router.GET(URL_ERROR_500, func(e *core.RequestEvent) error {
|
||||
return engine.Response500(e, nil, nil)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
225
controllers/almanach.go
Normal file
225
controllers/almanach.go
Normal file
@@ -0,0 +1,225 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"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/pagemodels"
|
||||
"github.com/Theodor-Springmann-Stiftung/musenalm/templating"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/tools/router"
|
||||
)
|
||||
|
||||
const (
|
||||
URL_ALMANACH = "/almanach/{id}/"
|
||||
TEMPLATE_ALMANACH = "/almanach/"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rp := &AlmanachPage{
|
||||
StaticPage: pagemodels.StaticPage{
|
||||
Name: pagemodels.P_REIHEN_NAME,
|
||||
URL: URL_ALMANACH,
|
||||
Template: TEMPLATE_ALMANACH,
|
||||
Layout: templating.DEFAULT_LAYOUT_NAME,
|
||||
},
|
||||
}
|
||||
app.Register(rp)
|
||||
}
|
||||
|
||||
type AlmanachPage struct {
|
||||
pagemodels.StaticPage
|
||||
}
|
||||
|
||||
func (p *AlmanachPage) Setup(router *router.Router[*core.RequestEvent], app core.App, engine *templating.Engine) error {
|
||||
router.GET(p.URL, p.GET(engine, app))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *AlmanachPage) GET(engine *templating.Engine, app core.App) HandleFunc {
|
||||
return func(e *core.RequestEvent) error {
|
||||
id := e.Request.PathValue("id")
|
||||
data := make(map[string]any)
|
||||
filters := NewBeitraegeFilterParameters(e)
|
||||
result, err := NewAlmanachResult(app, id, filters)
|
||||
if err != nil {
|
||||
engine.Response404(e, err, nil)
|
||||
}
|
||||
data["result"] = result
|
||||
data["filters"] = filters
|
||||
|
||||
abbrs, err := pagemodels.GetAbks(app)
|
||||
if err == nil {
|
||||
data["abbrs"] = abbrs
|
||||
}
|
||||
|
||||
return engine.Response200(e, p.Template, data)
|
||||
}
|
||||
}
|
||||
|
||||
type AlmanachResult struct {
|
||||
Entry *dbmodels.Entry
|
||||
Places []*dbmodels.Place
|
||||
Series []*dbmodels.Series
|
||||
Contents []*dbmodels.Content
|
||||
Agents map[string]*dbmodels.Agent // <- Key is agent id
|
||||
EntriesSeries map[string]*dbmodels.REntriesSeries // <- Key is series id
|
||||
EntriesAgents []*dbmodels.REntriesAgents
|
||||
ContentsAgents map[string][]*dbmodels.RContentsAgents // <- Key is content id
|
||||
|
||||
Types []string
|
||||
HasScans bool
|
||||
}
|
||||
|
||||
func NewAlmanachResult(app core.App, id string, params BeitraegeFilterParameters) (*AlmanachResult, error) {
|
||||
// INFO: what about sql.ErrNoRows?
|
||||
// We don't get sql.ErrNoRows here, since dbx converts every empty slice or
|
||||
// empty id to a WHERE 0=1 query, which will not error.
|
||||
entry, err := dbmodels.Entries_MusenalmID(app, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
places, err := dbmodels.Places_IDs(app, datatypes.ToAny(entry.Places()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
srelations, err := dbmodels.REntriesSeries_Entry(app, entry.Id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sids := []any{}
|
||||
srelationsMap := map[string]*dbmodels.REntriesSeries{}
|
||||
for _, r := range srelations {
|
||||
sids = append(sids, r.Series())
|
||||
srelationsMap[r.Series()] = r
|
||||
}
|
||||
|
||||
series, err := dbmodels.Series_IDs(app, sids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
contents, err := dbmodels.Contents_Entry(app, entry.Id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
types := Types_Contents(contents)
|
||||
|
||||
hs := HasScans(contents)
|
||||
|
||||
if params.OnlyScans {
|
||||
cscans := []*dbmodels.Content{}
|
||||
for _, c := range contents {
|
||||
if len(c.Scans()) > 0 {
|
||||
cscans = append(cscans, c)
|
||||
}
|
||||
}
|
||||
contents = cscans
|
||||
}
|
||||
|
||||
if params.Type != "" {
|
||||
cfiltered := []*dbmodels.Content{}
|
||||
outer:
|
||||
for _, c := range contents {
|
||||
for _, t := range c.MusenalmType() {
|
||||
if t == params.Type {
|
||||
cfiltered = append(cfiltered, c)
|
||||
continue outer
|
||||
}
|
||||
}
|
||||
}
|
||||
contents = cfiltered
|
||||
}
|
||||
|
||||
dbmodels.Sort_Contents_Numbering(contents)
|
||||
|
||||
contentsagents, err := dbmodels.RContentsAgents_Contents(app, dbmodels.Ids(contents))
|
||||
caids := []any{}
|
||||
caMap := map[string][]*dbmodels.RContentsAgents{}
|
||||
for _, r := range contentsagents {
|
||||
caids = append(caids, r.Agent())
|
||||
caMap[r.Content()] = append(caMap[r.Content()], r)
|
||||
}
|
||||
|
||||
entriesagents, err := dbmodels.REntriesAgents_Entry(app, entry.Id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, r := range entriesagents {
|
||||
caids = append(caids, r.Agent())
|
||||
}
|
||||
|
||||
agents, err := dbmodels.Agents_IDs(app, caids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
agentsMap := map[string]*dbmodels.Agent{}
|
||||
for _, a := range agents {
|
||||
agentsMap[a.Id] = a
|
||||
}
|
||||
|
||||
ret := &AlmanachResult{
|
||||
Entry: entry,
|
||||
Places: places,
|
||||
Series: series,
|
||||
Contents: contents,
|
||||
Agents: agentsMap,
|
||||
EntriesSeries: srelationsMap,
|
||||
EntriesAgents: entriesagents,
|
||||
ContentsAgents: caMap,
|
||||
Types: types,
|
||||
HasScans: hs,
|
||||
}
|
||||
|
||||
ret.Collections()
|
||||
return ret, nil
|
||||
|
||||
}
|
||||
|
||||
func (r *AlmanachResult) Collections() {
|
||||
ids := []int{}
|
||||
collections := []*dbmodels.Content{}
|
||||
for _, s := range r.Contents {
|
||||
ids = append(ids, s.MusenalmID())
|
||||
for _, t := range s.MusenalmType() {
|
||||
if t == "Sammlung" {
|
||||
collections = append(collections, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Types_Contents(contents []*dbmodels.Content) []string {
|
||||
types := map[string]bool{}
|
||||
for _, c := range contents {
|
||||
for _, t := range c.MusenalmType() {
|
||||
types[t] = true
|
||||
}
|
||||
}
|
||||
|
||||
ret := make([]string, 0, len(types))
|
||||
for t, _ := range types {
|
||||
ret = append(ret, t)
|
||||
}
|
||||
|
||||
sort.Strings(ret)
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func HasScans(contents []*dbmodels.Content) bool {
|
||||
for _, c := range contents {
|
||||
if len(c.Scans()) > 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
59
controllers/almanach_edit.go
Normal file
59
controllers/almanach_edit.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"github.com/Theodor-Springmann-Stiftung/musenalm/app"
|
||||
"github.com/Theodor-Springmann-Stiftung/musenalm/middleware"
|
||||
"github.com/Theodor-Springmann-Stiftung/musenalm/pagemodels"
|
||||
"github.com/Theodor-Springmann-Stiftung/musenalm/templating"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/tools/router"
|
||||
)
|
||||
|
||||
const (
|
||||
URL_ALMANACH_EDIT = "edit/"
|
||||
TEMPLATE_ALMANACH_EDIT = "/almanach/edit/"
|
||||
)
|
||||
|
||||
func init() {
|
||||
ep := &AlmanachEditPage{
|
||||
StaticPage: pagemodels.StaticPage{
|
||||
Name: pagemodels.P_ALMANACH_EDIT_NAME,
|
||||
URL: URL_ALMANACH_EDIT,
|
||||
Template: TEMPLATE_ALMANACH_EDIT,
|
||||
Layout: pagemodels.LAYOUT_LOGIN_PAGES,
|
||||
},
|
||||
}
|
||||
app.Register(ep)
|
||||
}
|
||||
|
||||
type AlmanachEditPage struct {
|
||||
pagemodels.StaticPage
|
||||
}
|
||||
|
||||
func (p *AlmanachEditPage) Setup(router *router.Router[*core.RequestEvent], app core.App, engine *templating.Engine) error {
|
||||
rg := router.Group(URL_ALMANACH)
|
||||
rg.BindFunc(middleware.IsAdminOrEditor())
|
||||
rg.GET(URL_ALMANACH_EDIT, p.GET(engine, app))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *AlmanachEditPage) GET(engine *templating.Engine, app core.App) HandleFunc {
|
||||
return func(e *core.RequestEvent) error {
|
||||
id := e.Request.PathValue("id")
|
||||
data := make(map[string]any)
|
||||
filters := NewBeitraegeFilterParameters(e)
|
||||
result, err := NewAlmanachResult(app, id, filters)
|
||||
if err != nil {
|
||||
engine.Response404(e, err, nil)
|
||||
}
|
||||
data["result"] = result
|
||||
data["filters"] = filters
|
||||
|
||||
abbrs, err := pagemodels.GetAbks(app)
|
||||
if err == nil {
|
||||
data["abbrs"] = abbrs
|
||||
}
|
||||
|
||||
return engine.Response200(e, p.Template, data, p.Layout)
|
||||
}
|
||||
}
|
||||
140
controllers/beitrag.go
Normal file
140
controllers/beitrag.go
Normal file
@@ -0,0 +1,140 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"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/pagemodels"
|
||||
"github.com/Theodor-Springmann-Stiftung/musenalm/templating"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/tools/router"
|
||||
)
|
||||
|
||||
const (
|
||||
URL_BEITRAG = "/beitrag/{id}"
|
||||
TEMPLATE_BEITRAG = "/beitrag/"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rp := &BeitragPage{
|
||||
StaticPage: pagemodels.StaticPage{
|
||||
Name: pagemodels.P_BEITRAG_NAME,
|
||||
URL: URL_BEITRAG,
|
||||
Template: TEMPLATE_BEITRAG,
|
||||
Layout: templating.DEFAULT_LAYOUT_NAME,
|
||||
},
|
||||
}
|
||||
app.Register(rp)
|
||||
}
|
||||
|
||||
type BeitragPage struct {
|
||||
pagemodels.StaticPage
|
||||
}
|
||||
|
||||
func (p *BeitragPage) Setup(router *router.Router[*core.RequestEvent], app core.App, engine *templating.Engine) error {
|
||||
router.GET(p.URL, func(e *core.RequestEvent) error {
|
||||
id := e.Request.PathValue("id")
|
||||
data := make(map[string]interface{})
|
||||
result, err := NewBeitragResult(app, id)
|
||||
if err != nil {
|
||||
engine.Response404(e, err, nil)
|
||||
}
|
||||
data["result"] = result
|
||||
|
||||
abbrs, err := pagemodels.GetAbks(app)
|
||||
if err == nil {
|
||||
data["abbrs"] = abbrs
|
||||
}
|
||||
|
||||
return engine.Response200(e, p.Template, data)
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type BeitragResult struct {
|
||||
Entry *dbmodels.Entry
|
||||
Places []*dbmodels.Place
|
||||
Series []*dbmodels.Series
|
||||
Content *dbmodels.Content
|
||||
Agents map[string]*dbmodels.Agent // <- Key is agent id
|
||||
EntriesSeries map[string]*dbmodels.REntriesSeries // <- Key is series id
|
||||
EntriesAgents []*dbmodels.REntriesAgents
|
||||
ContentsAgents []*dbmodels.RContentsAgents // <- Key is content id
|
||||
}
|
||||
|
||||
func NewBeitragResult(app core.App, id string) (*BeitragResult, error) {
|
||||
content, err := dbmodels.Contents_MusenalmID(app, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
entry, err := dbmodels.Entries_ID(app, content.Entry())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
places, err := dbmodels.Places_IDs(app, datatypes.ToAny(entry.Places()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
srelations, err := dbmodels.REntriesSeries_Entry(app, entry.Id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sids := []any{}
|
||||
srelationsMap := make(map[string]*dbmodels.REntriesSeries)
|
||||
for _, s := range srelations {
|
||||
sids = append(sids, s.Series())
|
||||
srelationsMap[s.Series()] = s
|
||||
}
|
||||
|
||||
series, err := dbmodels.Series_IDs(app, sids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
arelations, err := dbmodels.REntriesAgents_Entry(app, entry.Id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
acrelations, err := dbmodels.RContentsAgents_Content(app, content.Id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
aids := []any{}
|
||||
arelationsMap := make(map[string]*dbmodels.REntriesAgents)
|
||||
for _, r := range arelations {
|
||||
aids = append(aids, r.Agent())
|
||||
arelationsMap[r.Agent()] = r
|
||||
}
|
||||
|
||||
for _, r := range acrelations {
|
||||
aids = append(aids, r.Agent())
|
||||
}
|
||||
|
||||
agents, err := dbmodels.Agents_IDs(app, aids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
agentsMap := make(map[string]*dbmodels.Agent)
|
||||
for _, a := range agents {
|
||||
agentsMap[a.Id] = a
|
||||
}
|
||||
|
||||
return &BeitragResult{
|
||||
Entry: entry,
|
||||
Places: places,
|
||||
Series: series,
|
||||
Content: content,
|
||||
Agents: agentsMap,
|
||||
EntriesSeries: srelationsMap,
|
||||
EntriesAgents: arelations,
|
||||
ContentsAgents: acrelations,
|
||||
}, nil
|
||||
}
|
||||
10
controllers/contents.go
Normal file
10
controllers/contents.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package controllers
|
||||
|
||||
type ContentListFilterParams struct {
|
||||
Agent string
|
||||
Type string
|
||||
Page int
|
||||
OnlyScans bool
|
||||
AlmNumber int
|
||||
Entry string
|
||||
}
|
||||
62
controllers/index.go
Normal file
62
controllers/index.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/Theodor-Springmann-Stiftung/musenalm/app"
|
||||
"github.com/Theodor-Springmann-Stiftung/musenalm/pagemodels"
|
||||
"github.com/Theodor-Springmann-Stiftung/musenalm/templating"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/tools/router"
|
||||
"math/rand"
|
||||
)
|
||||
|
||||
func init() {
|
||||
ip := &IndexPage{
|
||||
DefaultPage: pagemodels.DefaultPage[*pagemodels.IndexTexte]{
|
||||
Name: pagemodels.P_INDEX_NAME,
|
||||
},
|
||||
}
|
||||
app.Register(ip)
|
||||
}
|
||||
|
||||
type IndexPage struct {
|
||||
pagemodels.DefaultPage[*pagemodels.IndexTexte]
|
||||
}
|
||||
|
||||
// TODO:
|
||||
func (p *IndexPage) Setup(router *router.Router[*core.RequestEvent], app core.App, engine *templating.Engine) error {
|
||||
router.GET("/{$}", func(e *core.RequestEvent) error {
|
||||
bilder := []*pagemodels.IndexBilder{}
|
||||
err := app.RecordQuery(pagemodels.GeneratePageTableName(pagemodels.P_INDEX_NAME, pagemodels.T_INDEX_BILDER)).
|
||||
All(&bilder)
|
||||
if err != nil {
|
||||
return engine.Response404(e, err, nil)
|
||||
}
|
||||
texte := []*pagemodels.IndexTexte{}
|
||||
err = app.RecordQuery(pagemodels.GeneratePageTableName(pagemodels.P_INDEX_NAME)).
|
||||
All(&texte)
|
||||
if err != nil {
|
||||
return engine.Response404(e, err, nil)
|
||||
}
|
||||
|
||||
Shuffle(bilder)
|
||||
data := map[string]interface{}{
|
||||
"bilder": bilder,
|
||||
"texte": texte[0],
|
||||
}
|
||||
|
||||
return engine.Response200(e, "/", data, "blank")
|
||||
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func Shuffle[T any](arr []T) {
|
||||
rand.Seed(time.Now().UnixNano()) // Ensure random seed
|
||||
n := len(arr)
|
||||
for i := n - 1; i > 0; i-- {
|
||||
j := rand.Intn(i + 1) // Get a random index
|
||||
arr[i], arr[j] = arr[j], arr[i] // Swap
|
||||
}
|
||||
}
|
||||
171
controllers/login.go
Normal file
171
controllers/login.go
Normal file
@@ -0,0 +1,171 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/Theodor-Springmann-Stiftung/musenalm/app"
|
||||
"github.com/Theodor-Springmann-Stiftung/musenalm/dbmodels"
|
||||
"github.com/Theodor-Springmann-Stiftung/musenalm/helpers/security"
|
||||
"github.com/Theodor-Springmann-Stiftung/musenalm/pagemodels"
|
||||
"github.com/Theodor-Springmann-Stiftung/musenalm/templating"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/tools/router"
|
||||
)
|
||||
|
||||
const (
|
||||
URL_LOGIN = "/login/"
|
||||
TEMPLATE_LOGIN = "/login/"
|
||||
)
|
||||
|
||||
var CSRF_CACHE *security.CSRFProtector
|
||||
|
||||
// TODO:
|
||||
// - rate limiting
|
||||
|
||||
func init() {
|
||||
csrf_cache, err := security.NewCSRFProtector(time.Minute*10, time.Minute)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
CSRF_CACHE = csrf_cache
|
||||
|
||||
lp := &LoginPage{
|
||||
StaticPage: pagemodels.StaticPage{
|
||||
Name: pagemodels.P_LOGIN_NAME,
|
||||
Layout: "blank",
|
||||
Template: TEMPLATE_LOGIN,
|
||||
URL: URL_LOGIN,
|
||||
},
|
||||
}
|
||||
app.Register(lp)
|
||||
}
|
||||
|
||||
type LoginPage struct {
|
||||
pagemodels.StaticPage
|
||||
}
|
||||
|
||||
func (p *LoginPage) Setup(router *router.Router[*core.RequestEvent], app core.App, engine *templating.Engine) error {
|
||||
router.GET(URL_LOGIN, p.GET(engine, app))
|
||||
router.POST(URL_LOGIN, p.POST(engine, app))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *LoginPage) GET(engine *templating.Engine, app core.App) HandleFunc {
|
||||
return func(e *core.RequestEvent) error {
|
||||
data := make(map[string]any)
|
||||
data["record"] = p
|
||||
nonce, token, err := CSRF_CACHE.GenerateTokenBundle()
|
||||
if err != nil {
|
||||
return engine.Response500(e, err, data)
|
||||
}
|
||||
data["csrf_nonce"] = nonce
|
||||
data["csrf_token"] = token
|
||||
|
||||
Logout(e, &app)
|
||||
|
||||
SetRedirect(data, e)
|
||||
|
||||
return engine.Response200(e, p.Template, data, p.Layout)
|
||||
}
|
||||
}
|
||||
|
||||
func Unauthorized(
|
||||
engine *templating.Engine,
|
||||
e *core.RequestEvent,
|
||||
error error,
|
||||
data map[string]any) error {
|
||||
|
||||
nonce, token, err := CSRF_CACHE.GenerateTokenBundle()
|
||||
if err != nil {
|
||||
return engine.Response500(e, err, data)
|
||||
}
|
||||
|
||||
data["csrf_nonce"] = nonce
|
||||
data["csrf_token"] = token
|
||||
data["error"] = error.Error()
|
||||
|
||||
SetRedirect(data, e)
|
||||
|
||||
htm, err := engine.RenderToString(e, data, TEMPLATE_LOGIN, "blank")
|
||||
if err != nil {
|
||||
return engine.Response500(e, err, data)
|
||||
}
|
||||
|
||||
return e.HTML(http.StatusUnauthorized, htm)
|
||||
}
|
||||
|
||||
func (p *LoginPage) POST(engine *templating.Engine, app core.App) HandleFunc {
|
||||
return func(e *core.RequestEvent) error {
|
||||
data := make(map[string]any)
|
||||
data["record"] = p
|
||||
|
||||
formdata := struct {
|
||||
Username string `json:"username" form:"username"`
|
||||
Password string `json:"password" form:"password"`
|
||||
CsrfNonce string `json:"csrf_nonce" form:"csrf_nonce"`
|
||||
CsrfToken string `json:"csrf_token" form:"csrf_token"`
|
||||
Persistent string `json:"persist" form:"persist"`
|
||||
}{}
|
||||
|
||||
if err := e.BindBody(&formdata); err != nil {
|
||||
return engine.Response500(e, err, data)
|
||||
}
|
||||
|
||||
data["formdata"] = formdata
|
||||
|
||||
if _, err := CSRF_CACHE.ValidateTokenBundle(formdata.CsrfNonce, formdata.CsrfToken); err != nil {
|
||||
return Unauthorized(engine, e, fmt.Errorf("Ungültiges CSRF-Token oder Zeit abgelaufen. Bitte versuchen Sie es erneut."), data)
|
||||
}
|
||||
|
||||
if formdata.Username == "" || formdata.Password == "" {
|
||||
return Unauthorized(engine, e, fmt.Errorf("Benuztername oder Passwort falsch. Bitte versuchen Sie es erneut."), data)
|
||||
}
|
||||
|
||||
record, err := app.FindFirstRecordByData(dbmodels.USERS_TABLE, dbmodels.USERS_EMAIL_FIELD, formdata.Username)
|
||||
if err != nil || !record.ValidatePassword(formdata.Password) {
|
||||
return Unauthorized(engine, e, fmt.Errorf("Benuztername oder Passwort falsch. Bitte versuchen Sie es erneut."), data)
|
||||
}
|
||||
|
||||
user := dbmodels.NewUser(record)
|
||||
if user.Deactivated() {
|
||||
return Unauthorized(engine, e, fmt.Errorf("Ihr Benutzerkonto ist deaktiviert. Bitte kontaktieren Sie den Administrator."), data)
|
||||
}
|
||||
|
||||
duration := time.Minute * 60
|
||||
if formdata.Persistent == "on" {
|
||||
duration = time.Hour * 24 * 90
|
||||
}
|
||||
|
||||
token, err := dbmodels.CreateSessionToken(app, record.Id, e.RealIP(), e.Request.UserAgent(), formdata.Persistent == "on", duration)
|
||||
if err != nil {
|
||||
return engine.Response500(e, err, data)
|
||||
}
|
||||
|
||||
if formdata.Persistent == "on" {
|
||||
e.SetCookie(&http.Cookie{
|
||||
Name: dbmodels.SESSION_COOKIE_NAME,
|
||||
Path: "/",
|
||||
MaxAge: int(duration.Seconds()),
|
||||
Value: token.Token(),
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
HttpOnly: true,
|
||||
Secure: true,
|
||||
})
|
||||
} else {
|
||||
e.SetCookie(&http.Cookie{
|
||||
Name: dbmodels.SESSION_COOKIE_NAME,
|
||||
Path: "/",
|
||||
Value: token.Token(),
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
HttpOnly: true,
|
||||
Secure: true,
|
||||
})
|
||||
}
|
||||
|
||||
SetRedirect(data, e)
|
||||
|
||||
return RedirectTo(e)
|
||||
}
|
||||
}
|
||||
73
controllers/logout.go
Normal file
73
controllers/logout.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/Theodor-Springmann-Stiftung/musenalm/app"
|
||||
"github.com/Theodor-Springmann-Stiftung/musenalm/dbmodels"
|
||||
"github.com/Theodor-Springmann-Stiftung/musenalm/middleware"
|
||||
"github.com/Theodor-Springmann-Stiftung/musenalm/pagemodels"
|
||||
"github.com/Theodor-Springmann-Stiftung/musenalm/templating"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/tools/router"
|
||||
)
|
||||
|
||||
func init() {
|
||||
lp := &LogoutPage{
|
||||
StaticPage: pagemodels.StaticPage{
|
||||
Name: pagemodels.P_LOGOUT_NAME,
|
||||
Layout: "blank",
|
||||
Template: "/logout/",
|
||||
URL: "/logout/",
|
||||
},
|
||||
}
|
||||
app.Register(lp)
|
||||
}
|
||||
|
||||
type LogoutPage struct {
|
||||
pagemodels.StaticPage
|
||||
}
|
||||
|
||||
func (p *LogoutPage) Setup(router *router.Router[*core.RequestEvent], app core.App, engine *templating.Engine) error {
|
||||
router.GET(p.URL, p.GET(app))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *LogoutPage) GET(app core.App) HandleFunc {
|
||||
return func(e *core.RequestEvent) error {
|
||||
Logout(e, &app)
|
||||
return RedirectTo(e)
|
||||
}
|
||||
}
|
||||
|
||||
func Logout(e *core.RequestEvent, app *core.App) {
|
||||
e.SetCookie(&http.Cookie{
|
||||
Name: dbmodels.SESSION_COOKIE_NAME,
|
||||
Path: "/",
|
||||
MaxAge: -1,
|
||||
})
|
||||
|
||||
e.Response.Header().Set("Clear-Site-Data", "\"cookies\"")
|
||||
|
||||
cookie, err := e.Request.Cookie(dbmodels.SESSION_COOKIE_NAME)
|
||||
if err == nil && app != nil {
|
||||
go func() {
|
||||
app := *app
|
||||
record, err := app.FindFirstRecordByData(dbmodels.SESSIONS_TABLE, dbmodels.SESSIONS_TOKEN_FIELD, cookie.Value)
|
||||
if err == nil && record != nil {
|
||||
app.Delete(record)
|
||||
}
|
||||
|
||||
middleware.SESSION_CACHE.Delete(cookie.Value)
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
func RedirectTo(e *core.RequestEvent) error {
|
||||
redirect := "/reihen"
|
||||
|
||||
if r := e.Request.URL.Query().Get("redirectTo"); r != "" {
|
||||
redirect = r
|
||||
}
|
||||
return e.Redirect(303, redirect)
|
||||
}
|
||||
242
controllers/person.go
Normal file
242
controllers/person.go
Normal file
@@ -0,0 +1,242 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"maps"
|
||||
"slices"
|
||||
|
||||
"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/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/tools/router"
|
||||
)
|
||||
|
||||
const (
|
||||
URL_PERSON = "/person/{id}"
|
||||
TEMPLATE_PERSON = "/person/"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rp := &PersonPage{
|
||||
StaticPage: pagemodels.StaticPage{
|
||||
Name: URL_PERSON,
|
||||
Template: TEMPLATE_PERSON,
|
||||
Layout: templating.DEFAULT_LAYOUT_NAME,
|
||||
URL: URL_PERSON,
|
||||
},
|
||||
}
|
||||
app.Register(rp)
|
||||
}
|
||||
|
||||
type PersonPage struct {
|
||||
pagemodels.StaticPage
|
||||
}
|
||||
|
||||
func (p *PersonPage) Setup(router *router.Router[*core.RequestEvent], app core.App, engine *templating.Engine) error {
|
||||
router.GET(URL_PERSON, func(e *core.RequestEvent) error {
|
||||
person := e.Request.PathValue("id")
|
||||
data := make(map[string]interface{})
|
||||
data[PARAM_PERSON] = person
|
||||
|
||||
result, err := NewAgentResult(app, person)
|
||||
if err != nil {
|
||||
return engine.Response404(e, err, data)
|
||||
}
|
||||
data["result"] = result
|
||||
|
||||
return engine.Response200(e, p.Template, data, p.Layout)
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type AgentResult struct {
|
||||
Agent *dbmodels.Agent
|
||||
|
||||
BResult []*dbmodels.Series // Sorted
|
||||
Entries map[string]*dbmodels.Entry // KEY: Entry ID
|
||||
EntriesSeries map[string][]*dbmodels.REntriesSeries // KEY: Series ID
|
||||
EntriesAgents map[string][]*dbmodels.REntriesAgents // KEY: Entry ID
|
||||
|
||||
// INFO: we could save a DB query by quering the entries table only once
|
||||
CResult []*dbmodels.Entry /// Sorted
|
||||
Contents map[string][]*dbmodels.Content // KEY: entry ID
|
||||
ContentsAgents map[string][]*dbmodels.RContentsAgents // KEY: Content ID
|
||||
Agents map[string]*dbmodels.Agent // KEY: Agent ID
|
||||
}
|
||||
|
||||
func NewAgentResult(app core.App, id string) (*AgentResult, error) {
|
||||
agent, err := dbmodels.Agents_ID(app, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := &AgentResult{
|
||||
Agent: agent,
|
||||
}
|
||||
|
||||
err = res.FilterEntriesByPerson(app, id, res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = res.FilterContentsByEntry(app, id, res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (p *AgentResult) FilterEntriesByPerson(app core.App, id string, res *AgentResult) error {
|
||||
// 1. DB Hit
|
||||
relations, err := dbmodels.REntriesAgents_Agent(app, id)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(relations) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
entriesagents := make(map[string][]*dbmodels.REntriesAgents)
|
||||
entryIds := []any{}
|
||||
for _, r := range relations {
|
||||
entryIds = append(entryIds, r.Entry())
|
||||
entriesagents[r.Entry()] = append(entriesagents[r.Entry()], r)
|
||||
}
|
||||
res.EntriesAgents = entriesagents
|
||||
|
||||
// 2. DB Hit
|
||||
entries, err := dbmodels.Entries_IDs(app, entryIds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
entryMap := make(map[string]*dbmodels.Entry, len(entries))
|
||||
for _, e := range entries {
|
||||
entryMap[e.Id] = e
|
||||
}
|
||||
res.Entries = entryMap
|
||||
|
||||
// 3. DB Hit
|
||||
entriesseries, err := dbmodels.REntriesSeries_Entries(app, entryIds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
entriesseriesmap := make(map[string][]*dbmodels.REntriesSeries, len(entriesseries))
|
||||
for _, r := range entriesseries {
|
||||
entriesseriesmap[r.Series()] = append(entriesseriesmap[r.Series()], r)
|
||||
}
|
||||
|
||||
for _, r := range entriesseriesmap {
|
||||
dbmodels.Sort_REntriesSeries_Year(r, entryMap)
|
||||
}
|
||||
|
||||
res.EntriesSeries = entriesseriesmap
|
||||
|
||||
seriesIds := []any{}
|
||||
for _, s := range entriesseries {
|
||||
seriesIds = append(seriesIds, s.Series())
|
||||
}
|
||||
|
||||
// 4. DB Hit
|
||||
series, err := dbmodels.Series_IDs(app, seriesIds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res.BResult = series
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *AgentResult) FilterContentsByEntry(app core.App, id string, res *AgentResult) error {
|
||||
relations, err := dbmodels.RContentsAgents_Agent(app, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(relations) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
contentsagents := make(map[string][]*dbmodels.RContentsAgents)
|
||||
contentIds := []any{}
|
||||
agentids := []any{}
|
||||
for _, r := range relations {
|
||||
contentIds = append(contentIds, r.Content())
|
||||
agentids = append(agentids, r.Agent())
|
||||
contentsagents[r.Content()] = append(contentsagents[r.Content()], r)
|
||||
}
|
||||
res.ContentsAgents = contentsagents
|
||||
|
||||
agents, err := dbmodels.Agents_IDs(app, agentids)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
aMap := make(map[string]*dbmodels.Agent, len(agents))
|
||||
for _, a := range agents {
|
||||
aMap[a.Id] = a
|
||||
}
|
||||
res.Agents = aMap
|
||||
|
||||
contents, err := dbmodels.Contents_IDs(app, contentIds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
contentMap := make(map[string][]*dbmodels.Content, len(contents))
|
||||
entrykeys := []any{}
|
||||
for _, c := range contents {
|
||||
contentMap[c.Entry()] = append(contentMap[c.Entry()], c)
|
||||
entrykeys = append(entrykeys, c.Entry())
|
||||
}
|
||||
res.Contents = contentMap
|
||||
|
||||
for _, c := range contentMap {
|
||||
dbmodels.Sort_Contents_Numbering(c)
|
||||
}
|
||||
|
||||
entries, err := dbmodels.Entries_IDs(app, entrykeys)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dbmodels.Sort_Entries_Year_Title(entries)
|
||||
res.CResult = entries
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *AgentResult) LenEntries() int {
|
||||
return len(p.Entries)
|
||||
}
|
||||
|
||||
func (p *AgentResult) LenSeries() int {
|
||||
return len(p.BResult)
|
||||
}
|
||||
|
||||
func (p *AgentResult) LenContents() int {
|
||||
i := 0
|
||||
for _, c := range p.Contents {
|
||||
i += len(c)
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
func (p *AgentResult) Types() []string {
|
||||
types := make(map[string]bool)
|
||||
|
||||
// INFO: this is just a handful of entries usuallly so we're fine
|
||||
for _, c := range p.Contents {
|
||||
for _, c := range c {
|
||||
for _, c := range c.MusenalmType() {
|
||||
types[c] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return slices.Collect(maps.Keys(types))
|
||||
}
|
||||
180
controllers/personen.go
Normal file
180
controllers/personen.go
Normal file
@@ -0,0 +1,180 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"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/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/tools/router"
|
||||
)
|
||||
|
||||
// INFO: V0 of these
|
||||
const (
|
||||
URL_PERSONEN = "/personen/"
|
||||
PARAM_FILTER = "filter"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rp := &PersonenPage{
|
||||
StaticPage: pagemodels.StaticPage{
|
||||
Name: pagemodels.P_REIHEN_NAME,
|
||||
},
|
||||
}
|
||||
app.Register(rp)
|
||||
}
|
||||
|
||||
type PersonenPage struct {
|
||||
pagemodels.StaticPage
|
||||
}
|
||||
|
||||
func (p *PersonenPage) Setup(router *router.Router[*core.RequestEvent], app core.App, engine *templating.Engine) error {
|
||||
router.GET(URL_PERSONEN, func(e *core.RequestEvent) error {
|
||||
if e.Request.URL.Query().Get(PARAM_SEARCH) != "" {
|
||||
return p.SearchRequest(app, engine, e)
|
||||
}
|
||||
|
||||
return p.FilterRequest(app, engine, e)
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *PersonenPage) CommonData(app core.App, data map[string]interface{}) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *PersonenPage) FilterRequest(app core.App, engine *templating.Engine, e *core.RequestEvent) error {
|
||||
filter := e.Request.URL.Query().Get(PARAM_FILTER)
|
||||
letter := e.Request.URL.Query().Get(PARAM_LETTER)
|
||||
if letter == "" {
|
||||
letter = "A"
|
||||
}
|
||||
|
||||
if filter == "" {
|
||||
filter = "noorg"
|
||||
}
|
||||
|
||||
data := map[string]interface{}{}
|
||||
|
||||
var err error = nil
|
||||
agents := []*dbmodels.Agent{}
|
||||
if filter == "org" {
|
||||
agents, err = dbmodels.AgentsForOrg(app, true, letter)
|
||||
}
|
||||
|
||||
if filter == "noorg" {
|
||||
agents, err = dbmodels.AgentsForOrg(app, false, letter)
|
||||
}
|
||||
|
||||
if filter == "musik" {
|
||||
agents, err = dbmodels.AgentsForProfession(app, "Musik", letter)
|
||||
}
|
||||
|
||||
if filter == "text" {
|
||||
agents, err = dbmodels.AgentsForProfession(app, "Text", letter)
|
||||
}
|
||||
|
||||
if filter == "graphik" {
|
||||
agents, err = dbmodels.AgentsForProfession(app, "Graphik", letter)
|
||||
}
|
||||
|
||||
if filter == "hrsg" {
|
||||
agents, err = dbmodels.AgentsForProfession(app, "Hrsg", letter)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return engine.Response404(e, err, data)
|
||||
}
|
||||
dbmodels.Sort_Agents_Name(agents)
|
||||
data["agents"] = agents
|
||||
data["filter"] = filter
|
||||
data["letter"] = letter
|
||||
|
||||
ids := []any{}
|
||||
for _, a := range agents {
|
||||
ids = append(ids, a.Id)
|
||||
}
|
||||
|
||||
bcount, err := dbmodels.CountAgentsBaende(app, ids)
|
||||
if err == nil {
|
||||
data["bcount"] = bcount
|
||||
}
|
||||
|
||||
count, err := dbmodels.CountAgentsContents(app, ids)
|
||||
if err == nil {
|
||||
data["ccount"] = count
|
||||
}
|
||||
|
||||
letters, err := dbmodels.LettersForAgents(app, filter)
|
||||
if err != nil {
|
||||
return engine.Response404(e, err, data)
|
||||
}
|
||||
|
||||
data["letters"] = letters
|
||||
|
||||
return p.Get(e, engine, data)
|
||||
}
|
||||
|
||||
func (p *PersonenPage) SearchRequest(app core.App, engine *templating.Engine, e *core.RequestEvent) error {
|
||||
search := e.Request.URL.Query().Get(PARAM_SEARCH)
|
||||
data := map[string]interface{}{}
|
||||
agents := []*dbmodels.Agent{}
|
||||
altagents := []*dbmodels.Agent{}
|
||||
|
||||
a, err := dbmodels.FTS5SearchAgents(app, search)
|
||||
if err != nil {
|
||||
return engine.Response404(e, err, data)
|
||||
}
|
||||
agents = a
|
||||
|
||||
if len(agents) == 0 {
|
||||
// INFO: Fallback to regular search, if FTS5 fails
|
||||
a, aa, err := dbmodels.BasicSearchAgents(app, search)
|
||||
if err != nil {
|
||||
return engine.Response404(e, err, data)
|
||||
}
|
||||
agents = a
|
||||
altagents = aa
|
||||
} else {
|
||||
data["FTS"] = true
|
||||
}
|
||||
|
||||
dbmodels.Sort_Agents_Name(agents)
|
||||
dbmodels.Sort_Agents_Name(altagents)
|
||||
|
||||
data["search"] = search
|
||||
data["agents"] = agents
|
||||
data["altagents"] = altagents
|
||||
|
||||
ids := []any{}
|
||||
for _, a := range agents {
|
||||
ids = append(ids, a.Id)
|
||||
}
|
||||
|
||||
for _, a := range altagents {
|
||||
ids = append(ids, a.Id)
|
||||
}
|
||||
|
||||
bcount, err := dbmodels.CountAgentsBaende(app, ids)
|
||||
if err == nil {
|
||||
data["bcount"] = bcount
|
||||
}
|
||||
|
||||
count, err := dbmodels.CountAgentsContents(app, ids)
|
||||
if err == nil {
|
||||
data["ccount"] = count
|
||||
}
|
||||
|
||||
return p.Get(e, engine, data)
|
||||
}
|
||||
|
||||
func (p *PersonenPage) Get(request *core.RequestEvent, engine *templating.Engine, data map[string]interface{}) error {
|
||||
err := p.CommonData(request.App, data)
|
||||
if err != nil {
|
||||
return engine.Response404(request, err, data)
|
||||
}
|
||||
|
||||
return engine.Response200(request, URL_PERSONEN, data)
|
||||
}
|
||||
70
controllers/reihe.go
Normal file
70
controllers/reihe.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"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/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/tools/router"
|
||||
)
|
||||
|
||||
const (
|
||||
URL_REIHE = "/reihe/{id}/"
|
||||
TEMPLATE_REIHE = "/reihe/"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rp := &ReihePage{
|
||||
StaticPage: pagemodels.StaticPage{
|
||||
Name: pagemodels.P_REIHEN_NAME,
|
||||
URL: URL_REIHE,
|
||||
Layout: templating.DEFAULT_LAYOUT_NAME,
|
||||
Template: TEMPLATE_REIHE,
|
||||
},
|
||||
}
|
||||
app.Register(rp)
|
||||
}
|
||||
|
||||
type ReihePage struct {
|
||||
pagemodels.StaticPage
|
||||
}
|
||||
|
||||
// TODO: data richtig seutzen, damit die Reihe mit dem template _reihe angezeigt wird
|
||||
func (p *ReihePage) Setup(router *router.Router[*core.RequestEvent], app core.App, engine *templating.Engine) error {
|
||||
router.GET(URL_REIHE, func(e *core.RequestEvent) error {
|
||||
id := e.Request.PathValue("id")
|
||||
data := make(map[string]interface{})
|
||||
reihe, err := dbmodels.Series_MusenalmID(app, id)
|
||||
if err != nil || reihe == nil || reihe.Id == "" {
|
||||
return engine.Response404(e, err, data)
|
||||
}
|
||||
data["series"] = reihe
|
||||
|
||||
entries, relations, err := Entries_Series_IDs(app, []any{reihe.Id})
|
||||
if err != nil {
|
||||
return engine.Response404(e, err, data)
|
||||
}
|
||||
|
||||
emap := make(map[string]*dbmodels.Entry)
|
||||
for _, entry := range entries {
|
||||
emap[entry.Id] = entry
|
||||
}
|
||||
|
||||
rmap := make(map[string][]*dbmodels.REntriesSeries)
|
||||
for _, relation := range relations {
|
||||
rmap[relation.Series()] = append(rmap[relation.Series()], relation)
|
||||
}
|
||||
|
||||
data["relations"] = rmap[reihe.Id]
|
||||
data["entries"] = emap
|
||||
|
||||
return p.Get(e, engine, data)
|
||||
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *ReihePage) Get(request *core.RequestEvent, engine *templating.Engine, data map[string]interface{}) error {
|
||||
return engine.Response200(request, TEMPLATE_REIHE, data)
|
||||
}
|
||||
569
controllers/reihen.go
Normal file
569
controllers/reihen.go
Normal file
@@ -0,0 +1,569 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"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/"
|
||||
PARAM_LETTER = "letter"
|
||||
PARAM_SEARCH = "search"
|
||||
PARAM_PERSON = "agent"
|
||||
PARAM_PLACE = "place"
|
||||
PARAM_YEAR = "year"
|
||||
PARAM_HIDDEN = "hidden"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rp := &ReihenPage{
|
||||
DefaultPage: pagemodels.DefaultPage[*pagemodels.DefaultPageRecord]{
|
||||
Name: pagemodels.P_REIHEN_NAME,
|
||||
URL: URL_REIHEN,
|
||||
Template: URL_REIHEN,
|
||||
Layout: templating.DEFAULT_LAYOUT_NAME,
|
||||
Record: &pagemodels.DefaultPageRecord{},
|
||||
},
|
||||
}
|
||||
app.Register(rp)
|
||||
}
|
||||
|
||||
type ReihenPage struct {
|
||||
pagemodels.DefaultPage[*pagemodels.DefaultPageRecord]
|
||||
}
|
||||
|
||||
func (p *ReihenPage) Setup(router *router.Router[*core.RequestEvent], app core.App, engine *templating.Engine) error {
|
||||
router.GET(URL_REIHEN, func(e *core.RequestEvent) error {
|
||||
search := e.Request.URL.Query().Get(PARAM_SEARCH)
|
||||
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 := make(map[string]any)
|
||||
data[PARAM_HIDDEN] = e.Request.URL.Query().Get(PARAM_HIDDEN)
|
||||
data[PARAM_YEAR] = year
|
||||
|
||||
y, err := strconv.Atoi(year)
|
||||
if err != nil {
|
||||
return engine.Response404(e, err, data)
|
||||
}
|
||||
|
||||
result, err := NewSeriesResult_Year(app, y)
|
||||
if err != nil {
|
||||
return engine.Response404(e, err, data)
|
||||
}
|
||||
|
||||
data["result"] = result
|
||||
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{}{}
|
||||
data[PARAM_HIDDEN] = e.Request.URL.Query().Get(PARAM_HIDDEN)
|
||||
if letter == "" {
|
||||
data["startpage"] = true
|
||||
letter = "A"
|
||||
}
|
||||
data[PARAM_LETTER] = letter
|
||||
|
||||
result, err := NewSeriesListResult_Letter(app, letter)
|
||||
if err != nil {
|
||||
return engine.Response404(e, err, data)
|
||||
}
|
||||
|
||||
data["result"] = result
|
||||
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
|
||||
data[PARAM_HIDDEN] = e.Request.URL.Query().Get(PARAM_HIDDEN)
|
||||
|
||||
result, err := NewSeriesResult_Agent(app, person)
|
||||
if err != nil {
|
||||
return engine.Response404(e, err, data)
|
||||
}
|
||||
|
||||
data["result"] = result
|
||||
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
|
||||
data[PARAM_HIDDEN] = e.Request.URL.Query().Get(PARAM_HIDDEN)
|
||||
|
||||
result, err := NewSeriesResult_Place(app, place)
|
||||
if err != nil {
|
||||
return engine.Response404(e, err, data)
|
||||
}
|
||||
|
||||
data["result"] = result
|
||||
return p.Get(e, engine, data)
|
||||
}
|
||||
|
||||
// TODO: Suchverhalten bei gefilterten Personen, Orten und Jahren
|
||||
func (p *ReihenPage) SearchRequest(app core.App, engine *templating.Engine, e *core.RequestEvent) error {
|
||||
search := e.Request.URL.Query().Get(PARAM_SEARCH)
|
||||
data := map[string]interface{}{}
|
||||
data[PARAM_SEARCH] = search
|
||||
|
||||
result, err := NewSeriesResult_Search(app, search)
|
||||
if err != nil {
|
||||
return engine.Response404(e, err, data)
|
||||
}
|
||||
|
||||
data["result"] = result
|
||||
return p.Get(e, engine, data)
|
||||
}
|
||||
|
||||
func (p *ReihenPage) Get(request *core.RequestEvent, engine *templating.Engine, data map[string]interface{}) error {
|
||||
data["common"] = NewCommonReihenData(request.App)
|
||||
record, _ := p.GetLatestData(request.App)
|
||||
if record != nil {
|
||||
data["record"] = pagemodels.NewReihen(record)
|
||||
}
|
||||
return engine.Response200(request, URL_REIHEN, data)
|
||||
}
|
||||
|
||||
type CommonReihenData struct {
|
||||
Years []int
|
||||
Places []*dbmodels.Place
|
||||
Letters []string
|
||||
Agents []*dbmodels.Agent
|
||||
}
|
||||
|
||||
func NewCommonReihenData(app core.App) CommonReihenData {
|
||||
arels := []*core.Record{}
|
||||
err := app.RecordQuery(
|
||||
dbmodels.RelationTableName(dbmodels.ENTRIES_TABLE, dbmodels.AGENTS_TABLE)).
|
||||
GroupBy(dbmodels.AGENTS_TABLE).
|
||||
All(&arels)
|
||||
if err != nil {
|
||||
app.Logger().Error("Failed to fetch agents", "error", err)
|
||||
}
|
||||
|
||||
ids := []any{}
|
||||
for _, a := range arels {
|
||||
ids = append(ids, a.GetString(dbmodels.AGENTS_TABLE))
|
||||
}
|
||||
|
||||
agents, err := dbmodels.Agents_IDs(app, ids)
|
||||
if err != nil {
|
||||
app.Logger().Error("Failed to fetch agents", "error", err)
|
||||
}
|
||||
|
||||
letterrecs := []core.Record{}
|
||||
letters := []string{}
|
||||
|
||||
err = app.RecordQuery(dbmodels.SERIES_TABLE).
|
||||
Select("upper(substr(" + dbmodels.SERIES_TITLE_FIELD + ", 1, 1)) AS id").
|
||||
Distinct(true).
|
||||
OrderBy("id").
|
||||
All(&letterrecs)
|
||||
if err != nil {
|
||||
app.Logger().Error("Failed to fetch letters", "error", err)
|
||||
}
|
||||
|
||||
for _, l := range letterrecs {
|
||||
letters = append(letters, l.GetString("id"))
|
||||
}
|
||||
|
||||
places := []*dbmodels.Place{}
|
||||
err = app.RecordQuery(dbmodels.PLACES_TABLE).
|
||||
OrderBy(dbmodels.PLACES_NAME_FIELD).
|
||||
All(&places)
|
||||
if err != nil {
|
||||
app.Logger().Error("Failed to fetch places", "error", err)
|
||||
}
|
||||
dbmodels.Sort_Places_Name(places)
|
||||
|
||||
rec := []core.Record{}
|
||||
err = app.RecordQuery(dbmodels.ENTRIES_TABLE).
|
||||
Select(dbmodels.YEAR_FIELD + " AS id").
|
||||
Distinct(true).
|
||||
OrderBy("id").
|
||||
All(&rec)
|
||||
if err != nil {
|
||||
app.Logger().Error("Failed to fetch years", "error", err)
|
||||
}
|
||||
|
||||
years := []int{}
|
||||
for _, r := range rec {
|
||||
years = append(years, r.GetInt("id"))
|
||||
}
|
||||
|
||||
return CommonReihenData{
|
||||
Years: years,
|
||||
Places: places,
|
||||
Letters: letters,
|
||||
Agents: agents,
|
||||
}
|
||||
}
|
||||
|
||||
type SeriesListResult struct {
|
||||
Series []*dbmodels.Series
|
||||
Entries map[string]*dbmodels.Entry // <-- Key is Entry.ID
|
||||
EntriesSeries map[string][]*dbmodels.REntriesSeries // <-- Key is Series.ID
|
||||
|
||||
// INFO: Only on agent request
|
||||
Agent *dbmodels.Agent
|
||||
EntriesAgents map[string][]*dbmodels.REntriesAgents // <-- Key is Entry.ID
|
||||
|
||||
// INFO: Only on search request
|
||||
IDSeries []*dbmodels.Series
|
||||
AltSeries []*dbmodels.Series
|
||||
|
||||
// INFO: Only on place request
|
||||
Place *dbmodels.Place
|
||||
}
|
||||
|
||||
func NewSeriesListResult_Letter(app core.App, letter string) (*SeriesListResult, error) {
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dbmodels.Sort_Series_Title(series)
|
||||
|
||||
relations, err := dbmodels.REntriesSeries_Seriess(app, dbmodels.Ids(series))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
eids := []any{}
|
||||
relationsMap := map[string][]*dbmodels.REntriesSeries{}
|
||||
for _, r := range relations {
|
||||
relationsMap[r.Series()] = append(relationsMap[r.Series()], r)
|
||||
eids = append(eids, r.Entry())
|
||||
}
|
||||
|
||||
entries, err := dbmodels.Entries_IDs(app, eids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
entriesMap := map[string]*dbmodels.Entry{}
|
||||
for _, e := range entries {
|
||||
entriesMap[e.Id] = e
|
||||
}
|
||||
|
||||
for _, r := range relationsMap {
|
||||
dbmodels.Sort_REntriesSeries_Year(r, entriesMap)
|
||||
}
|
||||
|
||||
return &SeriesListResult{
|
||||
Series: series,
|
||||
Entries: entriesMap,
|
||||
EntriesSeries: relationsMap,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewSeriesResult_Agent(app core.App, person string) (*SeriesListResult, error) {
|
||||
agent, err := dbmodels.Agents_ID(app, person)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
entriesagentsrels, err := dbmodels.REntriesAgents_Agent(app, agent.Id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
eids := []any{}
|
||||
entriesagents := map[string][]*dbmodels.REntriesAgents{}
|
||||
for _, r := range entriesagentsrels {
|
||||
eids = append(eids, r.Entry())
|
||||
entriesagents[r.Entry()] = append(entriesagents[r.Entry()], r)
|
||||
}
|
||||
|
||||
entries, err := dbmodels.Entries_IDs(app, eids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
entriesMap := map[string]*dbmodels.Entry{}
|
||||
for _, e := range entries {
|
||||
entriesMap[e.Id] = e
|
||||
}
|
||||
|
||||
entriesseriesrels, err := dbmodels.REntriesSeries_Entries(app, eids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sids := []any{}
|
||||
entriesseries := map[string][]*dbmodels.REntriesSeries{}
|
||||
for _, r := range entriesseriesrels {
|
||||
sids = append(sids, r.Series())
|
||||
entriesseries[r.Series()] = append(entriesseries[r.Series()], r)
|
||||
}
|
||||
|
||||
series, err := dbmodels.Series_IDs(app, sids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dbmodels.Sort_Series_Title(series)
|
||||
|
||||
return &SeriesListResult{
|
||||
Series: series,
|
||||
Entries: entriesMap,
|
||||
EntriesSeries: entriesseries,
|
||||
Agent: agent,
|
||||
EntriesAgents: entriesagents,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewSeriesResult_Year(app core.App, year int) (*SeriesListResult, error) {
|
||||
entries := []*dbmodels.Entry{}
|
||||
err := app.RecordQuery(dbmodels.ENTRIES_TABLE).
|
||||
Where(dbx.HashExp{dbmodels.YEAR_FIELD: year}).
|
||||
All(&entries)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
entriesMap := map[string]*dbmodels.Entry{}
|
||||
eids := []any{}
|
||||
for _, e := range entries {
|
||||
eids = append(eids, e.Id)
|
||||
entriesMap[e.Id] = e
|
||||
}
|
||||
|
||||
entriesseriesrels, err := dbmodels.REntriesSeries_Entries(app, eids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sids := []any{}
|
||||
entriesseries := map[string][]*dbmodels.REntriesSeries{}
|
||||
for _, r := range entriesseriesrels {
|
||||
sids = append(sids, r.Series())
|
||||
entriesseries[r.Series()] = append(entriesseries[r.Series()], r)
|
||||
}
|
||||
|
||||
series, err := dbmodels.Series_IDs(app, sids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dbmodels.Sort_Series_Title(series)
|
||||
|
||||
return &SeriesListResult{
|
||||
Series: series,
|
||||
Entries: entriesMap,
|
||||
EntriesSeries: entriesseries,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewSeriesResult_Place(app core.App, place string) (*SeriesListResult, error) {
|
||||
p, err := dbmodels.Places_ID(app, place)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
entries := []*dbmodels.Entry{}
|
||||
err = app.RecordQuery(dbmodels.ENTRIES_TABLE).
|
||||
Where(dbx.Like(dbmodels.PLACES_TABLE, place).Match(true, true)).
|
||||
All(&entries)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
entriesMap := map[string]*dbmodels.Entry{}
|
||||
eids := []any{}
|
||||
for _, e := range entries {
|
||||
eids = append(eids, e.Id)
|
||||
entriesMap[e.Id] = e
|
||||
}
|
||||
|
||||
entriesseriesrels, err := dbmodels.REntriesSeries_Entries(app, eids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sids := []any{}
|
||||
entriesseries := map[string][]*dbmodels.REntriesSeries{}
|
||||
for _, r := range entriesseriesrels {
|
||||
sids = append(sids, r.Series())
|
||||
entriesseries[r.Series()] = append(entriesseries[r.Series()], r)
|
||||
}
|
||||
|
||||
series, err := dbmodels.Series_IDs(app, sids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dbmodels.Sort_Series_Title(series)
|
||||
|
||||
return &SeriesListResult{
|
||||
Series: series,
|
||||
Entries: entriesMap,
|
||||
EntriesSeries: entriesseries,
|
||||
Place: p,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewSeriesResult_Search(app core.App, search string) (*SeriesListResult, error) {
|
||||
series, altseries, err := dbmodels.BasicSearchSeries(app, search)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dbmodels.Sort_Series_Title(series)
|
||||
dbmodels.Sort_Series_Title(altseries)
|
||||
|
||||
keys := []any{}
|
||||
keys = append(keys, dbmodels.Ids(series)...)
|
||||
keys = append(keys, dbmodels.Ids(altseries)...)
|
||||
|
||||
entries, relations, err := Entries_Series_IDs(app, keys)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
relationsMap := make(map[string][]*dbmodels.REntriesSeries)
|
||||
entriesMap := make(map[string]*dbmodels.Entry)
|
||||
for _, v := range relations {
|
||||
relationsMap[v.Series()] = append(relationsMap[v.Series()], v)
|
||||
}
|
||||
|
||||
for _, v := range entries {
|
||||
entriesMap[v.Id] = v
|
||||
}
|
||||
|
||||
ret := &SeriesListResult{
|
||||
Series: series,
|
||||
AltSeries: altseries,
|
||||
Entries: entriesMap,
|
||||
EntriesSeries: relationsMap,
|
||||
}
|
||||
|
||||
if _, err := strconv.Atoi(strings.TrimSpace(search)); err == nil {
|
||||
identries := []*dbmodels.Entry{}
|
||||
err := app.RecordQuery(dbmodels.ENTRIES_TABLE).
|
||||
Where(dbx.HashExp{dbmodels.MUSENALMID_FIELD: search}).
|
||||
All(&identries)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(identries) != 0 {
|
||||
app.Logger().Info("Found entries by musenalmid", "count", len(identries))
|
||||
idseries, idrelations, err := Series_Entries(app, identries)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dbmodels.Sort_Series_Title(idseries)
|
||||
ret.IDSeries = idseries
|
||||
|
||||
for _, v := range idrelations {
|
||||
ret.EntriesSeries[v.Series()] = append(relationsMap[v.Series()], v)
|
||||
}
|
||||
|
||||
for _, v := range identries {
|
||||
ret.Entries[v.Id] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *SeriesListResult) Count() int {
|
||||
return len(r.Series) + len(r.AltSeries) + len(r.IDSeries)
|
||||
}
|
||||
|
||||
func Entries_Series(app core.App, series []*dbmodels.Series) ([]*dbmodels.Entry, []*dbmodels.REntriesSeries, error) {
|
||||
relations, err := dbmodels.REntriesSeries_Seriess(app, dbmodels.Ids(series))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
eids := []any{}
|
||||
for _, r := range relations {
|
||||
eids = append(eids, r.Entry())
|
||||
}
|
||||
|
||||
entries, err := dbmodels.Entries_IDs(app, eids)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return entries, relations, nil
|
||||
}
|
||||
|
||||
func Entries_Series_IDs(app core.App, ids []any) ([]*dbmodels.Entry, []*dbmodels.REntriesSeries, error) {
|
||||
relations, err := dbmodels.REntriesSeries_Seriess(app, ids)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
eids := []any{}
|
||||
for _, r := range relations {
|
||||
eids = append(eids, r.Entry())
|
||||
}
|
||||
|
||||
entries, err := dbmodels.Entries_IDs(app, eids)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return entries, relations, nil
|
||||
}
|
||||
|
||||
func Series_Entries(app core.App, entries []*dbmodels.Entry) ([]*dbmodels.Series, []*dbmodels.REntriesSeries, error) {
|
||||
relations, err := dbmodels.REntriesSeries_Entries(app, dbmodels.Ids(entries))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
sids := []any{}
|
||||
for _, r := range relations {
|
||||
sids = append(sids, r.Series())
|
||||
}
|
||||
|
||||
series, err := dbmodels.Series_IDs(app, sids)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return series, relations, nil
|
||||
}
|
||||
64
controllers/static.go
Normal file
64
controllers/static.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"github.com/Theodor-Springmann-Stiftung/musenalm/app"
|
||||
"github.com/Theodor-Springmann-Stiftung/musenalm/pagemodels"
|
||||
"github.com/Theodor-Springmann-Stiftung/musenalm/templating"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
)
|
||||
|
||||
type HandleFunc func(e *core.RequestEvent) error
|
||||
|
||||
func init() {
|
||||
RegisterStaticPage("/datenschutz/", pagemodels.P_DATENSCHUTZ_NAME)
|
||||
RegisterTextPage("/redaktion/kontakt/", pagemodels.P_KONTAKT_NAME)
|
||||
RegisterTextPage("/redaktion/danksagungen/", pagemodels.P_DANK_NAME)
|
||||
RegisterTextPage("/redaktion/literatur/", pagemodels.P_LIT_NAME)
|
||||
RegisterTextPage("/redaktion/einleitung/", pagemodels.P_EINFUEHRUNG_NAME)
|
||||
RegisterTextPage("/redaktion/benutzerhinweise/", pagemodels.P_DOK_NAME)
|
||||
RegisterTextPage("/redaktion/lesekabinett/", pagemodels.P_KABINETT_NAME)
|
||||
}
|
||||
|
||||
func RegisterStaticPage(url, name string, layout ...string) {
|
||||
layoutName := templating.DEFAULT_LAYOUT_NAME
|
||||
if len(layout) > 0 {
|
||||
layoutName = layout[0]
|
||||
}
|
||||
|
||||
app.Register(&pagemodels.StaticPage{
|
||||
Name: name,
|
||||
Layout: layoutName,
|
||||
Template: url,
|
||||
URL: url,
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: mocve textpage to defaultpage with T = TextPageRecord
|
||||
func RegisterTextPage(url, name string, layout ...string) {
|
||||
layoutName := templating.DEFAULT_LAYOUT_NAME
|
||||
if len(layout) > 0 {
|
||||
layoutName = layout[0]
|
||||
}
|
||||
|
||||
app.Register(&pagemodels.TextPage{
|
||||
Name: name,
|
||||
Layout: layoutName,
|
||||
Template: url,
|
||||
URL: url,
|
||||
})
|
||||
}
|
||||
|
||||
func RegisterDefaultPage(url string, name string, layout ...string) {
|
||||
layoutName := templating.DEFAULT_LAYOUT_NAME
|
||||
if len(layout) > 0 {
|
||||
layoutName = layout[0]
|
||||
}
|
||||
|
||||
app.Register(&pagemodels.DefaultPage[*pagemodels.DefaultPageRecord]{
|
||||
Name: name,
|
||||
Layout: layoutName,
|
||||
Template: url,
|
||||
URL: url,
|
||||
Record: &pagemodels.DefaultPageRecord{},
|
||||
})
|
||||
}
|
||||
143
controllers/suche.go
Normal file
143
controllers/suche.go
Normal file
@@ -0,0 +1,143 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"slices"
|
||||
|
||||
"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/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/tools/router"
|
||||
)
|
||||
|
||||
type SuchePage struct {
|
||||
pagemodels.DefaultPage[*pagemodels.DefaultPageRecord]
|
||||
}
|
||||
|
||||
const (
|
||||
URL_SUCHE = "/suche/{type}"
|
||||
URL_SUCHE_ALT = "/suche/{$}"
|
||||
DEFAULT_SUCHE = "/suche/baende"
|
||||
PARAM_EXTENDED = "extended"
|
||||
TEMPLATE_SUCHE = "/suche/"
|
||||
PARAM_QUERY = "q"
|
||||
PARAM_PLACEHOLDER = "p"
|
||||
)
|
||||
|
||||
var availableTypes = []string{"baende", "beitraege"}
|
||||
|
||||
func init() {
|
||||
rp := &SuchePage{
|
||||
DefaultPage: pagemodels.DefaultPage[*pagemodels.DefaultPageRecord]{
|
||||
Record: &pagemodels.DefaultPageRecord{},
|
||||
Name: pagemodels.P_SUCHE_NAME,
|
||||
Template: TEMPLATE_SUCHE,
|
||||
Layout: templating.DEFAULT_LAYOUT_NAME,
|
||||
URL: URL_SUCHE,
|
||||
},
|
||||
}
|
||||
app.Register(rp)
|
||||
}
|
||||
|
||||
func (p *SuchePage) Setup(router *router.Router[*core.RequestEvent], app core.App, engine *templating.Engine) error {
|
||||
router.GET(URL_SUCHE_ALT, func(e *core.RequestEvent) error {
|
||||
return e.Redirect(http.StatusPermanentRedirect, DEFAULT_SUCHE)
|
||||
})
|
||||
|
||||
router.GET(URL_SUCHE, func(e *core.RequestEvent) error {
|
||||
paras, err := NewParameters(e)
|
||||
if err != nil {
|
||||
return engine.Response404(e, err, nil)
|
||||
}
|
||||
|
||||
allparas, _ := NewSearchParameters(e, *paras)
|
||||
|
||||
if allparas.IsBaendeSearch() {
|
||||
return p.SearchBaendeRequest(app, engine, e, *allparas)
|
||||
}
|
||||
|
||||
if allparas.IsBeitraegeSearch() {
|
||||
return p.SearchBeitraegeRequest(app, engine, e, *allparas)
|
||||
}
|
||||
|
||||
data := make(map[string]any)
|
||||
data["parameters"] = allparas
|
||||
data["types"] = dbmodels.MUSENALM_TYPE_VALUES
|
||||
return engine.Response200(e, p.Template+paras.Collection+"/", data, p.Layout)
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *SuchePage) SimpleSearchReihenRequest(app core.App, engine *templating.Engine, e *core.RequestEvent) error {
|
||||
return engine.Response404(e, nil, nil)
|
||||
}
|
||||
|
||||
func (p *SuchePage) SearchBeitraegeRequest(app core.App, engine *templating.Engine, e *core.RequestEvent, params SearchParameters) error {
|
||||
data := make(map[string]any)
|
||||
filterparams := NewBeitraegeFilterParameters(e)
|
||||
result, err := NewSearchBeitraege(app, params, filterparams)
|
||||
if err != nil {
|
||||
return engine.Response404(e, err, nil)
|
||||
}
|
||||
|
||||
data["filters"] = filterparams
|
||||
data["parameters"] = params
|
||||
data["result"] = result
|
||||
data["types"] = dbmodels.MUSENALM_TYPE_VALUES
|
||||
return engine.Response200(e, p.Template+params.Collection+"/", data, p.Layout)
|
||||
}
|
||||
|
||||
func (p *SuchePage) SearchBaendeRequest(app core.App, engine *templating.Engine, e *core.RequestEvent, params SearchParameters) error {
|
||||
data := make(map[string]any)
|
||||
|
||||
result, err := NewSearchBaende(app, params)
|
||||
if err != nil {
|
||||
return engine.Response404(e, err, nil)
|
||||
}
|
||||
|
||||
data["parameters"] = params
|
||||
data["result"] = result
|
||||
return engine.Response200(e, p.Template+params.Collection+"/", data, p.Layout)
|
||||
}
|
||||
|
||||
var ErrInvalidCollectionType = fmt.Errorf("Invalid collection type")
|
||||
|
||||
type Parameters struct {
|
||||
Extended bool
|
||||
Collection string
|
||||
Query string
|
||||
Placeholder string
|
||||
}
|
||||
|
||||
func NewParameters(e *core.RequestEvent) (*Parameters, error) {
|
||||
collection := e.Request.PathValue("type")
|
||||
if !slices.Contains(availableTypes, collection) {
|
||||
return nil, ErrInvalidCollectionType
|
||||
}
|
||||
|
||||
extended := false
|
||||
if e.Request.URL.Query().Get(PARAM_EXTENDED) == "true" {
|
||||
extended = true
|
||||
}
|
||||
|
||||
query := e.Request.URL.Query().Get(PARAM_QUERY)
|
||||
placeholder := query
|
||||
if query == "" {
|
||||
placeholder = e.Request.URL.Query().Get(PARAM_PLACEHOLDER)
|
||||
}
|
||||
|
||||
return &Parameters{
|
||||
Collection: collection,
|
||||
Extended: extended,
|
||||
Query: query,
|
||||
Placeholder: placeholder,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *Parameters) NormalizeQuery() dbmodels.Query {
|
||||
return dbmodels.NormalizeQuery(p.Query)
|
||||
}
|
||||
230
controllers/suche_baende.go
Normal file
230
controllers/suche_baende.go
Normal file
@@ -0,0 +1,230 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/Theodor-Springmann-Stiftung/musenalm/dbmodels"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
)
|
||||
|
||||
const (
|
||||
DEFAULT_PAGESIZE_BAENDE = 40
|
||||
)
|
||||
|
||||
type BaendeFilterParameters struct {
|
||||
Agent string
|
||||
Year string
|
||||
Place string
|
||||
State string
|
||||
}
|
||||
|
||||
func NewBaendeFilterParameters(ev *core.RequestEvent) BaendeFilterParameters {
|
||||
agent := ev.Request.URL.Query().Get("agent")
|
||||
year := ev.Request.URL.Query().Get("year")
|
||||
place := ev.Request.URL.Query().Get("place")
|
||||
state := ev.Request.URL.Query().Get("state")
|
||||
return BaendeFilterParameters{
|
||||
Agent: agent,
|
||||
Year: year,
|
||||
Place: place,
|
||||
State: state,
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
Pages []int
|
||||
}
|
||||
|
||||
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{}
|
||||
pages := []int{}
|
||||
if params.Sort == "series" {
|
||||
dbmodels.Sort_Series_Title(series)
|
||||
for _, s := range series {
|
||||
hits = append(hits, s.Id)
|
||||
}
|
||||
pages = PagesMap(hits, invrelationsmap, DEFAULT_PAGESIZE_BAENDE)
|
||||
} else {
|
||||
dbmodels.Sort_Entries_Year_Title(entries)
|
||||
for _, e := range entries {
|
||||
hits = append(hits, e.Id)
|
||||
}
|
||||
pages = PagesArray(hits, DEFAULT_PAGESIZE_BAENDE)
|
||||
}
|
||||
|
||||
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 &SearchResultBaende{
|
||||
Hits: hits,
|
||||
Series: seriesmap,
|
||||
Entries: entriesmap,
|
||||
Places: placesmap,
|
||||
Agents: agentsmap,
|
||||
EntriesSeries: relationsmap,
|
||||
SeriesEntries: invrelationsmap,
|
||||
EntriesAgents: relationsagentsmap,
|
||||
Pages: pages,
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
||||
func (r SearchResultBaende) PagesCount() int {
|
||||
return len(r.Pages) - 1
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
341
controllers/suche_beitraege.go
Normal file
341
controllers/suche_beitraege.go
Normal file
@@ -0,0 +1,341 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"maps"
|
||||
"slices"
|
||||
"sort"
|
||||
|
||||
"github.com/Theodor-Springmann-Stiftung/musenalm/dbmodels"
|
||||
"github.com/Theodor-Springmann-Stiftung/musenalm/helpers/datatypes"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
)
|
||||
|
||||
const (
|
||||
DEFAULT_PAGESIZE = 80
|
||||
|
||||
FILTER_PARAM_BEIAEGE_AGENT = "agentfilter"
|
||||
FILTER_PARAM_BEIAEGE_TYPE = "typefilter"
|
||||
FILTER_PARAM_BEIAEGE_ONLYSCANS = "onlyscans"
|
||||
FILTER_PARAM_BEIAEGE_YEAR = "yearfilter"
|
||||
)
|
||||
|
||||
type BeitraegeFilterParameters struct {
|
||||
Agent string
|
||||
Type string
|
||||
Year string
|
||||
OnlyScans bool
|
||||
}
|
||||
|
||||
func NewBeitraegeFilterParameters(ev *core.RequestEvent) BeitraegeFilterParameters {
|
||||
agent := ev.Request.URL.Query().Get(FILTER_PARAM_BEIAEGE_AGENT)
|
||||
typ := ev.Request.URL.Query().Get(FILTER_PARAM_BEIAEGE_TYPE)
|
||||
year := ev.Request.URL.Query().Get(FILTER_PARAM_BEIAEGE_YEAR)
|
||||
onlyscans := ev.Request.URL.Query().Get(FILTER_PARAM_BEIAEGE_ONLYSCANS) == "on"
|
||||
return BeitraegeFilterParameters{
|
||||
Agent: agent,
|
||||
Type: typ,
|
||||
Year: year,
|
||||
OnlyScans: onlyscans,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *BeitraegeFilterParameters) FieldSetBeitraege() []dbmodels.FTS5QueryRequest {
|
||||
ret := []dbmodels.FTS5QueryRequest{}
|
||||
|
||||
if p.Agent != "" {
|
||||
q := "\"" + p.Agent + "\""
|
||||
que := dbmodels.NormalizeQuery(q)
|
||||
req := dbmodels.IntoQueryRequests([]string{dbmodels.AGENTS_TABLE}, que)
|
||||
ret = append(ret, req...)
|
||||
}
|
||||
|
||||
if p.Type != "" {
|
||||
q := "\"" + p.Type + "\""
|
||||
que := dbmodels.NormalizeQuery(q)
|
||||
req := dbmodels.IntoQueryRequests([]string{dbmodels.MUSENALM_INHALTE_TYPE_FIELD}, que)
|
||||
ret = append(ret, req...)
|
||||
}
|
||||
|
||||
if p.Year != "" {
|
||||
q := "\"" + p.Year + "\""
|
||||
que := dbmodels.NormalizeQuery(q)
|
||||
req := dbmodels.IntoQueryRequests([]string{dbmodels.ENTRIES_TABLE}, que)
|
||||
ret = append(ret, req...)
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func (p BeitraegeFilterParameters) ToQueryParams() string {
|
||||
r := ""
|
||||
if p.Agent != "" {
|
||||
r += "&" + FILTER_PARAM_BEIAEGE_AGENT + "=" + p.Agent
|
||||
}
|
||||
if p.Type != "" {
|
||||
r += "&" + FILTER_PARAM_BEIAEGE_TYPE + "=" + p.Type
|
||||
}
|
||||
if p.Year != "" {
|
||||
r += "&" + FILTER_PARAM_BEIAEGE_YEAR + "=" + p.Year
|
||||
}
|
||||
if p.OnlyScans {
|
||||
r += "&" + FILTER_PARAM_BEIAEGE_ONLYSCANS + "=on"
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (p BeitraegeFilterParameters) ToQueryParamsWOScans() string {
|
||||
r := ""
|
||||
if p.Agent != "" {
|
||||
r += "&" + FILTER_PARAM_BEIAEGE_AGENT + "=" + p.Agent
|
||||
}
|
||||
if p.Type != "" {
|
||||
r += "&" + FILTER_PARAM_BEIAEGE_TYPE + "=" + p.Type
|
||||
}
|
||||
if p.Year != "" {
|
||||
r += "&" + FILTER_PARAM_BEIAEGE_YEAR + "=" + p.Year
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
AgentsList []*dbmodels.Agent
|
||||
TypesList []string
|
||||
YearList []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, filters BeitraegeFilterParameters) (*SearchResultBeitraege, error) {
|
||||
contents := []*dbmodels.Content{}
|
||||
queries := params.FieldSetBeitraege()
|
||||
fqueries := filters.FieldSetBeitraege()
|
||||
queries = append(queries, fqueries...)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
if filters.OnlyScans {
|
||||
scans := []*dbmodels.Content{}
|
||||
for _, c := range cs {
|
||||
if len(c.Scans()) > 0 {
|
||||
scans = append(scans, c)
|
||||
}
|
||||
}
|
||||
cs = scans
|
||||
}
|
||||
|
||||
contents = append(contents, cs...)
|
||||
}
|
||||
|
||||
resultids := []any{}
|
||||
uniqueresultentryids := map[string]bool{}
|
||||
types := make(map[string]bool)
|
||||
for _, content := range contents {
|
||||
resultids = append(resultids, content.Id)
|
||||
uniqueresultentryids[content.Entry()] = true
|
||||
for _, typ := range content.MusenalmType() {
|
||||
types[typ] = true
|
||||
}
|
||||
}
|
||||
|
||||
resultentryids := []any{}
|
||||
for entryid, _ := range uniqueresultentryids {
|
||||
resultentryids = append(resultentryids, entryid)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
uniqueaids := map[string]bool{}
|
||||
for _, a := range arels {
|
||||
uniqueaids[a.Agent()] = true
|
||||
}
|
||||
|
||||
aids := []any{}
|
||||
for aid, _ := range uniqueaids {
|
||||
aids = append(aids, aid)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
for _, c := range contentsmap {
|
||||
dbmodels.Sort_Contents_Numbering(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)
|
||||
years := make(map[int]bool)
|
||||
for _, e := range entries {
|
||||
entriesmap[e.Id] = e
|
||||
years[e.Year()] = true
|
||||
}
|
||||
|
||||
hits := []string{}
|
||||
for _, e := range entries {
|
||||
hits = append(hits, e.Id)
|
||||
}
|
||||
|
||||
pages := PagesMap(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]]
|
||||
}
|
||||
|
||||
tL := slices.Collect(maps.Keys(types))
|
||||
sort.Strings(tL)
|
||||
|
||||
yL := slices.Collect(maps.Keys(years))
|
||||
sort.Ints(yL)
|
||||
|
||||
dbmodels.Sort_Agents_Name(agents)
|
||||
|
||||
return &SearchResultBeitraege{
|
||||
Queries: queries,
|
||||
Hits: hits,
|
||||
Entries: entriesmap,
|
||||
Agents: agentsmap,
|
||||
Contents: contentsmap,
|
||||
ContentsAgents: contentsagents,
|
||||
Pages: pages,
|
||||
AgentsList: agents,
|
||||
TypesList: tL,
|
||||
YearList: yL,
|
||||
}, 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 PagesMap[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
|
||||
}
|
||||
|
||||
func PagesArray[T any](hits []T, pagesize int) []int {
|
||||
ret := []int{0}
|
||||
m := 0
|
||||
for i := range hits {
|
||||
m++
|
||||
if m >= pagesize {
|
||||
ret = append(ret, i)
|
||||
m = 0
|
||||
}
|
||||
}
|
||||
|
||||
if m > 0 {
|
||||
ret = append(ret, len(hits))
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
486
controllers/suche_parameters.go
Normal file
486
controllers/suche_parameters.go
Normal file
@@ -0,0 +1,486 @@
|
||||
package controllers
|
||||
|
||||
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"
|
||||
BEITRAEGE_PARAM_TYPE = "type"
|
||||
)
|
||||
|
||||
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
|
||||
TypeString 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")
|
||||
typestring := e.Request.URL.Query().Get(BEITRAEGE_PARAM_TYPE + "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,
|
||||
TypeString: typestring,
|
||||
}, 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)...)
|
||||
res = append(res, p.includedParams(p.TypeString)...)
|
||||
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) SortToQueryParams() string {
|
||||
return fmt.Sprintf("&sort=%s", p.Sort)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
if p.TypeString != "" {
|
||||
q += fmt.Sprintf("&typestring=%s", p.TypeString)
|
||||
}
|
||||
|
||||
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.TypeString != "" || 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...)
|
||||
}
|
||||
|
||||
if p.TypeString != "" {
|
||||
que := dbmodels.NormalizeQuery(p.TypeString)
|
||||
req := dbmodels.IntoQueryRequests([]string{dbmodels.MUSENALM_INHALTE_TYPE_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
|
||||
}
|
||||
143
controllers/user_create.go
Normal file
143
controllers/user_create.go
Normal file
@@ -0,0 +1,143 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/Theodor-Springmann-Stiftung/musenalm/app"
|
||||
"github.com/Theodor-Springmann-Stiftung/musenalm/dbmodels"
|
||||
"github.com/Theodor-Springmann-Stiftung/musenalm/middleware"
|
||||
"github.com/Theodor-Springmann-Stiftung/musenalm/pagemodels"
|
||||
"github.com/Theodor-Springmann-Stiftung/musenalm/templating"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/tools/router"
|
||||
)
|
||||
|
||||
const (
|
||||
URL_USER_CREATE = "/user/new/"
|
||||
PATH_VALUE_ROLE = "role"
|
||||
TEMPLATE_USER_CREATE = "/user/new/"
|
||||
)
|
||||
|
||||
func init() {
|
||||
ucp := &UserCreatePage{
|
||||
StaticPage: pagemodels.StaticPage{
|
||||
Name: pagemodels.P_USER_CREATE_NAME,
|
||||
Layout: "blank",
|
||||
Template: TEMPLATE_USER_CREATE,
|
||||
URL: URL_USER_CREATE,
|
||||
},
|
||||
}
|
||||
app.Register(ucp)
|
||||
}
|
||||
|
||||
type UserCreatePage struct {
|
||||
pagemodels.StaticPage
|
||||
}
|
||||
|
||||
func (p *UserCreatePage) Setup(router *router.Router[*core.RequestEvent], app core.App, engine *templating.Engine) error {
|
||||
rg := router.Group(URL_USER_CREATE)
|
||||
rg.BindFunc(middleware.HasToken())
|
||||
rg.GET("{"+PATH_VALUE_ROLE+"}", p.GET(engine, app))
|
||||
rg.POST("{"+PATH_VALUE_ROLE+"}", p.POST(engine, app))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *UserCreatePage) GET(engine *templating.Engine, app core.App) HandleFunc {
|
||||
return func(e *core.RequestEvent) error {
|
||||
role := e.Request.PathValue("role")
|
||||
if role != "User" && role != "Editor" && role != "Admin" {
|
||||
return engine.Response404(e, fmt.Errorf("invalid role: %s", role), nil)
|
||||
}
|
||||
|
||||
// TODO: check access token
|
||||
data := make(map[string]any)
|
||||
nonce, token, err := CSRF_CACHE.GenerateTokenBundle()
|
||||
if err != nil {
|
||||
return engine.Response500(e, err, data)
|
||||
}
|
||||
|
||||
data["role"] = role
|
||||
data["csrf_nonce"] = nonce
|
||||
data["csrf_token"] = token
|
||||
|
||||
SetRedirect(data, e)
|
||||
|
||||
return engine.Response200(e, p.Template, data, p.Layout)
|
||||
}
|
||||
}
|
||||
|
||||
func InvalidSignupResponse(engine *templating.Engine, e *core.RequestEvent, error string) error {
|
||||
data := make(map[string]any)
|
||||
data["error"] = error
|
||||
nonce, token, err := CSRF_CACHE.GenerateTokenBundle()
|
||||
if err != nil {
|
||||
return engine.Response500(e, err, data)
|
||||
}
|
||||
|
||||
data["csrf_nonce"] = nonce
|
||||
data["csrf_token"] = token
|
||||
|
||||
SetRedirect(data, e)
|
||||
|
||||
str, err := engine.RenderToString(e, data, TEMPLATE_USER_CREATE, "blank")
|
||||
if err != nil {
|
||||
return engine.Response500(e, err, data)
|
||||
}
|
||||
|
||||
return e.HTML(400, str)
|
||||
}
|
||||
|
||||
func (p *UserCreatePage) POST(engine *templating.Engine, app core.App) HandleFunc {
|
||||
return func(e *core.RequestEvent) error {
|
||||
role := e.Request.PathValue("role")
|
||||
if role != "User" && role != "Editor" && role != "Admin" {
|
||||
return engine.Response404(e, fmt.Errorf("invalid role: %s", role), nil)
|
||||
}
|
||||
|
||||
// TODO: check access token
|
||||
data := make(map[string]any)
|
||||
data["role"] = role
|
||||
|
||||
formdata := struct {
|
||||
Username string `form:"username"`
|
||||
Password string `form:"password"`
|
||||
PasswordRepeat string `form:"password_repeat"`
|
||||
Name string `form:"name"`
|
||||
CsrfNonce string `form:"csrf_nonce"`
|
||||
CsrfToken string `form:"csrf_token"`
|
||||
}{}
|
||||
|
||||
if err := e.BindBody(&formdata); err != nil {
|
||||
return engine.Response500(e, err, data)
|
||||
}
|
||||
|
||||
data["formdata"] = formdata
|
||||
|
||||
if _, err := CSRF_CACHE.ValidateTokenBundle(formdata.CsrfNonce, formdata.CsrfToken); err != nil {
|
||||
return InvalidSignupResponse(engine, e, "CSRF-Token ungültig.")
|
||||
}
|
||||
|
||||
if formdata.Username == "" || formdata.Password == "" || formdata.Name == "" {
|
||||
return InvalidSignupResponse(engine, e, "Bitte alle Felder ausfüllen.")
|
||||
}
|
||||
|
||||
if formdata.Password != formdata.PasswordRepeat {
|
||||
return InvalidSignupResponse(engine, e, "Passwörter stimmen nicht überein.")
|
||||
}
|
||||
|
||||
_, err := app.FindAuthRecordByEmail(dbmodels.USERS_TABLE, formdata.Username)
|
||||
if err == nil {
|
||||
return InvalidSignupResponse(engine, e, "Benutzer existiert bereits.")
|
||||
}
|
||||
|
||||
user, err := dbmodels.CreateUser(app, formdata.Username, formdata.Password, formdata.Name, role)
|
||||
if err != nil {
|
||||
return InvalidSignupResponse(engine, e, fmt.Sprintf("Fehler beim Erstellen des Benutzers: %s", err.Error()))
|
||||
}
|
||||
|
||||
SetRedirect(data, e)
|
||||
|
||||
data["user"] = user
|
||||
return engine.Response200(e, p.Template, data, p.Layout)
|
||||
}
|
||||
}
|
||||
327
controllers/user_edit.go
Normal file
327
controllers/user_edit.go
Normal file
@@ -0,0 +1,327 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/Theodor-Springmann-Stiftung/musenalm/app"
|
||||
"github.com/Theodor-Springmann-Stiftung/musenalm/dbmodels"
|
||||
"github.com/Theodor-Springmann-Stiftung/musenalm/middleware"
|
||||
"github.com/Theodor-Springmann-Stiftung/musenalm/pagemodels"
|
||||
"github.com/Theodor-Springmann-Stiftung/musenalm/templating"
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/tools/router"
|
||||
)
|
||||
|
||||
const (
|
||||
URL_USER = "/user/{uid}/"
|
||||
URL_USER_EDIT = "edit/"
|
||||
URL_USER_LOGOUT = "logout/"
|
||||
URL_USER_DEACTIVATE = "deactivate/"
|
||||
URL_USER_ACTIVATE = "activate/"
|
||||
UID_PATH_VALUE = "uid"
|
||||
TEMPLATE_USER_EDIT = "/user/edit/"
|
||||
)
|
||||
|
||||
func init() {
|
||||
ump := &UserEditPage{
|
||||
StaticPage: pagemodels.StaticPage{
|
||||
Name: pagemodels.P_USER_EDIT_NAME,
|
||||
Layout: pagemodels.LAYOUT_LOGIN_PAGES,
|
||||
Template: TEMPLATE_USER_EDIT,
|
||||
URL: URL_USER_EDIT,
|
||||
},
|
||||
}
|
||||
app.Register(ump)
|
||||
}
|
||||
|
||||
type UserEditPage struct {
|
||||
pagemodels.StaticPage
|
||||
}
|
||||
|
||||
func (p *UserEditPage) Setup(router *router.Router[*core.RequestEvent], app core.App, engine *templating.Engine) error {
|
||||
rg := router.Group(URL_USER)
|
||||
rg.BindFunc(middleware.IsAdminOrUser())
|
||||
rg.GET(URL_USER_EDIT, p.GET(engine, app))
|
||||
rg.POST(URL_USER_EDIT, p.POST(engine, app))
|
||||
rg.POST(URL_USER_DEACTIVATE, p.POSTDeactivate(engine, app))
|
||||
rg.POST(URL_USER_ACTIVATE, p.POSTActivate(engine, app))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *UserEditPage) GET(engine *templating.Engine, app core.App) HandleFunc {
|
||||
return func(e *core.RequestEvent) error {
|
||||
data := make(map[string]any)
|
||||
err := p.getData(app, data, e)
|
||||
if err != nil {
|
||||
return engine.Response500(e, err, data)
|
||||
}
|
||||
|
||||
return engine.Response200(e, TEMPLATE_USER_EDIT, data, p.Layout)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *UserEditPage) getData(app core.App, data map[string]any, e *core.RequestEvent) error {
|
||||
uid := e.Request.PathValue(UID_PATH_VALUE)
|
||||
u, err := app.FindRecordById(dbmodels.USERS_TABLE, uid)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Konnte Nutzer nicht finden: %w", err)
|
||||
}
|
||||
|
||||
user := dbmodels.NewUser(u)
|
||||
fu := user.Fixed()
|
||||
|
||||
data["user"] = &fu
|
||||
data["db_user"] = user
|
||||
|
||||
nonce, token, err := CSRF_CACHE.GenerateTokenBundleWithExpiration(2 * time.Hour)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Konnte CSRF-Token nicht generieren: %w", err)
|
||||
}
|
||||
data["csrf_token"] = token
|
||||
data["csrf_nonce"] = nonce
|
||||
|
||||
SetRedirect(data, e)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func DeleteSessionsForUser(app core.App, uid string) error {
|
||||
defer middleware.SESSION_CACHE.DeleteSessionByUserID(uid)
|
||||
records := []*core.Record{}
|
||||
err := app.RecordQuery(dbmodels.SESSIONS_TABLE).
|
||||
Where(dbx.HashExp{dbmodels.SESSIONS_USER_FIELD: uid}).
|
||||
All(&records)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = app.RunInTransaction(func(tx core.App) error {
|
||||
for _, r := range records {
|
||||
session := dbmodels.NewSession(r)
|
||||
session.SetStatus(dbmodels.TOKEN_STATUS_VALUES[3])
|
||||
if err := tx.Save(r); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *UserEditPage) InvalidDataResponse(engine *templating.Engine, e *core.RequestEvent, error string, user dbmodels.FixedUser) error {
|
||||
data := make(map[string]any)
|
||||
err := p.getData(e.App, data, e)
|
||||
if err != nil {
|
||||
return engine.Response500(e, err, data)
|
||||
}
|
||||
|
||||
str, err := engine.RenderToString(e, data, p.Template, p.Layout)
|
||||
if err != nil {
|
||||
return engine.Response500(e, err, data)
|
||||
}
|
||||
|
||||
return e.HTML(400, str)
|
||||
}
|
||||
|
||||
func (p *UserEditPage) POSTDeactivate(engine *templating.Engine, app core.App) HandleFunc {
|
||||
return func(e *core.RequestEvent) error {
|
||||
data := make(map[string]any)
|
||||
err := p.getData(app, data, e)
|
||||
if err != nil {
|
||||
return engine.Response500(e, err, data)
|
||||
}
|
||||
|
||||
formdata := struct {
|
||||
CSRF string `form:"csrf_token"`
|
||||
Nonce string `form:"csrf_nonce"`
|
||||
}{}
|
||||
|
||||
user := data["db_user"].(*dbmodels.User)
|
||||
|
||||
if err := e.BindBody(&formdata); err != nil {
|
||||
return p.InvalidDataResponse(engine, e, "Formulardaten ungültig.", user.Fixed())
|
||||
}
|
||||
|
||||
if _, err := CSRF_CACHE.ValidateTokenBundle(formdata.Nonce, formdata.CSRF); err != nil {
|
||||
return p.InvalidDataResponse(engine, e, err.Error(), user.Fixed())
|
||||
}
|
||||
|
||||
user.SetDeactivated(true)
|
||||
|
||||
if err := app.Save(user); err != nil {
|
||||
return p.InvalidDataResponse(engine, e, "Konnte Nutzer nicht deaktivieren: "+err.Error(), user.Fixed())
|
||||
}
|
||||
|
||||
DeleteSessionsForUser(app, user.Id)
|
||||
|
||||
req := templating.NewRequest(e)
|
||||
if req.User() != nil && req.User().Id == user.Id {
|
||||
return e.Redirect(303, "/login/")
|
||||
}
|
||||
|
||||
fu := user.Fixed()
|
||||
data["user"] = &fu
|
||||
data["success"] = "Nutzer " + fu.Name + "(" + fu.Email + ") wurde deaktiviert."
|
||||
|
||||
e.Response.Header().Add("HX-Push-Url", "false")
|
||||
return engine.Response200(e, p.Template, data, p.Layout)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *UserEditPage) POSTActivate(engine *templating.Engine, app core.App) HandleFunc {
|
||||
return func(e *core.RequestEvent) error {
|
||||
data := make(map[string]any)
|
||||
err := p.getData(app, data, e)
|
||||
if err != nil {
|
||||
return engine.Response500(e, err, data)
|
||||
}
|
||||
|
||||
user := data["db_user"].(*dbmodels.User)
|
||||
|
||||
formdata := struct {
|
||||
CSRF string `form:"csrf_token"`
|
||||
Nonce string `form:"csrf_nonce"`
|
||||
}{}
|
||||
|
||||
if err := e.BindBody(&formdata); err != nil {
|
||||
return p.InvalidDataResponse(engine, e, "Formulardaten ungültig.", user.Fixed())
|
||||
}
|
||||
|
||||
if _, err := CSRF_CACHE.ValidateTokenBundle(formdata.Nonce, formdata.CSRF); err != nil {
|
||||
return p.InvalidDataResponse(engine, e, err.Error(), user.Fixed())
|
||||
}
|
||||
|
||||
user.SetDeactivated(false)
|
||||
|
||||
if err := app.Save(user); err != nil {
|
||||
return p.InvalidDataResponse(engine, e, "Konnte Nutzer nicht aktivieren: "+err.Error(), user.Fixed())
|
||||
}
|
||||
|
||||
fu := user.Fixed()
|
||||
data["user"] = &fu
|
||||
data["success"] = "Nutzer " + fu.Name + "(" + fu.Email + ") wurde aktiviert."
|
||||
|
||||
e.Response.Header().Add("HX-Push-Url", "false")
|
||||
return engine.Response200(e, p.Template, data, p.Layout)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *UserEditPage) POST(engine *templating.Engine, app core.App) HandleFunc {
|
||||
return func(e *core.RequestEvent) error {
|
||||
data := make(map[string]any)
|
||||
|
||||
uid := e.Request.PathValue(UID_PATH_VALUE)
|
||||
req := templating.NewRequest(e)
|
||||
user := req.User()
|
||||
|
||||
u, err := app.FindRecordById(dbmodels.USERS_TABLE, uid)
|
||||
if err != nil {
|
||||
return engine.Response404(e, err, nil)
|
||||
}
|
||||
user_proxy := dbmodels.NewUser(u)
|
||||
fu := user_proxy.Fixed()
|
||||
|
||||
formdata := struct {
|
||||
Email string `form:"username"`
|
||||
Name string `form:"name"`
|
||||
Role string `form:"role"`
|
||||
CsrfNonce string `form:"csrf_nonce"`
|
||||
CsrfToken string `form:"csrf_token"`
|
||||
Password string `form:"password"`
|
||||
PasswordRepeat string `form:"password_repeat"`
|
||||
OldPassword string `form:"old_password"`
|
||||
Logout string `form:"logout"`
|
||||
}{}
|
||||
|
||||
if err := e.BindBody(&formdata); err != nil {
|
||||
return p.InvalidDataResponse(engine, e, err.Error(), fu)
|
||||
}
|
||||
|
||||
if formdata.CsrfNonce != "" && formdata.CsrfToken != "" {
|
||||
if _, err := CSRF_CACHE.ValidateTokenBundle(formdata.CsrfNonce, formdata.CsrfToken); err != nil {
|
||||
return p.InvalidDataResponse(engine, e, "CSRF ungültig oder abgelaufen", fu)
|
||||
}
|
||||
} else {
|
||||
return p.InvalidDataResponse(engine, e, "CSRF ungültig oder abgelaufen", fu)
|
||||
}
|
||||
|
||||
if formdata.Email == "" || formdata.Name == "" {
|
||||
return p.InvalidDataResponse(engine, e, "Bitte alle Felder ausfüllen", fu)
|
||||
}
|
||||
|
||||
// INFO: at this point email and name changes are allowed
|
||||
user_proxy.SetEmail(formdata.Email)
|
||||
user_proxy.SetName(formdata.Name)
|
||||
|
||||
rolechanged := false
|
||||
if formdata.Role != "" && formdata.Role != user_proxy.Role() {
|
||||
if user.Role == "Admin" &&
|
||||
(formdata.Role == "User" || formdata.Role == "Editor" || formdata.Role == "Admin") {
|
||||
user_proxy.SetRole(formdata.Role)
|
||||
rolechanged = true
|
||||
} else {
|
||||
return p.InvalidDataResponse(engine, e, "Rolle nicht erlaubt", fu)
|
||||
}
|
||||
}
|
||||
|
||||
passwordchanged := false
|
||||
if formdata.Password != "" || formdata.PasswordRepeat != "" || formdata.OldPassword != "" {
|
||||
if user.Role != "Admin" && formdata.OldPassword == "" {
|
||||
return p.InvalidDataResponse(engine, e, "Altes Passwort erforderlich", fu)
|
||||
} else if user.Role != "Admin" && !user_proxy.ValidatePassword(formdata.OldPassword) {
|
||||
return p.InvalidDataResponse(engine, e, "Altes Passwort falsch", fu)
|
||||
}
|
||||
|
||||
if formdata.Password != formdata.PasswordRepeat {
|
||||
return p.InvalidDataResponse(engine, e, "Passwörter stimmen nicht überein", fu)
|
||||
}
|
||||
|
||||
user_proxy.SetPassword(formdata.Password)
|
||||
passwordchanged = true
|
||||
}
|
||||
|
||||
if err := app.Save(user_proxy); err != nil {
|
||||
return p.InvalidDataResponse(engine, e, err.Error(), fu)
|
||||
}
|
||||
|
||||
slog.Info("UserEditPage: User edited", "user_id", user_proxy.Id, "role_changed", rolechanged, "password_changed", passwordchanged, "formdata", formdata)
|
||||
if rolechanged || (passwordchanged && formdata.Logout == "on") {
|
||||
slog.Error("UserEditPage: Deleting sessions for user", "user_id", user_proxy.Id, "role_changed", rolechanged, "password_changed", passwordchanged)
|
||||
if err := DeleteSessionsForUser(app, user_proxy.Id); err != nil {
|
||||
return p.InvalidDataResponse(engine, e, "Fehler beim Löschen der Sitzungen: "+err.Error(), fu)
|
||||
}
|
||||
|
||||
if user_proxy.Id == user.Id {
|
||||
// INFO: user changed his own role, so we log him out
|
||||
return e.Redirect(303, "/login/")
|
||||
}
|
||||
}
|
||||
|
||||
go middleware.SESSION_CACHE.DeleteSessionByUserID(user_proxy.Id)
|
||||
|
||||
fu = user_proxy.Fixed()
|
||||
data["user"] = &fu
|
||||
if user_proxy.Id == user.Id {
|
||||
e.Set("user", &fu)
|
||||
}
|
||||
|
||||
data["success"] = "Benutzer erfolgreich bearbeitet"
|
||||
|
||||
nonce, token, err := CSRF_CACHE.GenerateTokenBundleWithExpiration(2 * time.Hour)
|
||||
if err != nil {
|
||||
return engine.Response500(e, err, nil)
|
||||
}
|
||||
data["csrf_token"] = token
|
||||
data["csrf_nonce"] = nonce
|
||||
|
||||
return engine.Response200(e, TEMPLATE_USER_EDIT, data, p.Layout)
|
||||
}
|
||||
}
|
||||
266
controllers/user_management.go
Normal file
266
controllers/user_management.go
Normal file
@@ -0,0 +1,266 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/Theodor-Springmann-Stiftung/musenalm/app"
|
||||
"github.com/Theodor-Springmann-Stiftung/musenalm/dbmodels"
|
||||
"github.com/Theodor-Springmann-Stiftung/musenalm/middleware"
|
||||
"github.com/Theodor-Springmann-Stiftung/musenalm/pagemodels"
|
||||
"github.com/Theodor-Springmann-Stiftung/musenalm/templating"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/tools/router"
|
||||
)
|
||||
|
||||
const (
|
||||
URL_USER_MANAGEMENT = "/user/management"
|
||||
TEMPLATE_USER_MANAGEMENT = "/user/management/"
|
||||
URL_DEACTIVATE_USER = "/deactivate/"
|
||||
URL_ACTIVATE_USER = "/activate/"
|
||||
URL_LOGOUT_USER = "/logout/"
|
||||
)
|
||||
|
||||
type SessionCount struct {
|
||||
Count int `json:"count" db:"count"`
|
||||
UserId string `json:"user" db:"user"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
ump := &UserManagementPage{
|
||||
StaticPage: pagemodels.StaticPage{
|
||||
Name: pagemodels.P_USER_MGMT_NAME,
|
||||
Layout: "blankfooter",
|
||||
Template: TEMPLATE_USER_MANAGEMENT,
|
||||
URL: URL_USER_MANAGEMENT,
|
||||
},
|
||||
}
|
||||
app.Register(ump)
|
||||
}
|
||||
|
||||
type UserManagementPage struct {
|
||||
pagemodels.StaticPage
|
||||
}
|
||||
|
||||
func (p *UserManagementPage) Setup(router *router.Router[*core.RequestEvent], app core.App, engine *templating.Engine) error {
|
||||
rg := router.Group(URL_USER_MANAGEMENT)
|
||||
rg.BindFunc(middleware.IsAdmin())
|
||||
rg.GET("", p.GET(engine, app))
|
||||
rg.POST(URL_DEACTIVATE_USER, p.POSTDeactivate(engine, app))
|
||||
rg.POST(URL_ACTIVATE_USER, p.POSTActivate(engine, app))
|
||||
rg.POST(URL_LOGOUT_USER, p.POSTLogout(engine, app))
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetSessionsCounts(app core.App) ([]*SessionCount, error) {
|
||||
query := app.RecordQuery(dbmodels.SESSIONS_TABLE).
|
||||
Select("COUNT(*) AS count", dbmodels.SESSIONS_USER_FIELD).
|
||||
GroupBy(dbmodels.SESSIONS_USER_FIELD).
|
||||
OrderBy("count DESC")
|
||||
|
||||
var counts []*SessionCount
|
||||
err := query.All(&counts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get session counts: %w", err)
|
||||
}
|
||||
|
||||
return counts, nil
|
||||
}
|
||||
|
||||
func (p *UserManagementPage) GET(engine *templating.Engine, app core.App) HandleFunc {
|
||||
return func(e *core.RequestEvent) error {
|
||||
data := make(map[string]any)
|
||||
p.getData(app, data)
|
||||
SetRedirect(data, e)
|
||||
|
||||
return engine.Response200(e, p.Template, data, p.Layout)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *UserManagementPage) getData(app core.App, data map[string]any) error {
|
||||
records := []*core.Record{}
|
||||
err := app.RecordQuery(dbmodels.USERS_TABLE).OrderBy(dbmodels.USERS_NAME_FIELD).All(&records)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Konnte keine Nutzer laden: %w", err)
|
||||
}
|
||||
|
||||
users := make([]*dbmodels.User, 0, len(records))
|
||||
for _, record := range records {
|
||||
users = append(users, dbmodels.NewUser(record))
|
||||
}
|
||||
|
||||
sessionCounts, err := GetSessionsCounts(app)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Konnte keine Sitzungsanzahlen laden: %w", err)
|
||||
}
|
||||
|
||||
scmap := make(map[string]int)
|
||||
for _, sc := range sessionCounts {
|
||||
scmap[sc.UserId] = sc.Count
|
||||
}
|
||||
|
||||
data["users"] = users
|
||||
data["len"] = len(users)
|
||||
data["session_counts"] = scmap
|
||||
|
||||
csrfNonce, csrfToken, err := CSRF_CACHE.GenerateTokenBundleWithExpiration(2 * time.Hour)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Konnte kein CSRF-Token generieren.")
|
||||
}
|
||||
data["csrf_nonce"] = csrfNonce
|
||||
data["csrf_token"] = csrfToken
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *UserManagementPage) ErrorResponse(engine *templating.Engine, e *core.RequestEvent, err error) error {
|
||||
data := make(map[string]any)
|
||||
data["error"] = err.Error()
|
||||
|
||||
err = p.getData(e.App, data)
|
||||
if err != nil {
|
||||
engine.Response500(e, fmt.Errorf("Nutzerdaten konnten nicht geladen werden: %w", err), data)
|
||||
}
|
||||
|
||||
str, err := engine.RenderToString(e, data, p.Template, p.Layout)
|
||||
if err != nil {
|
||||
engine.Response500(e, fmt.Errorf("Konnte Fehlerseite nicht rendern: %w", err), data)
|
||||
}
|
||||
|
||||
e.Response.Header().Add("HX-Push-Url", "false")
|
||||
return e.HTML(400, str)
|
||||
}
|
||||
|
||||
func (p *UserManagementPage) POSTDeactivate(engine *templating.Engine, app core.App) HandleFunc {
|
||||
return func(e *core.RequestEvent) error {
|
||||
formdata := struct {
|
||||
User string `form:"uid"`
|
||||
CSRF string `form:"csrf_token"`
|
||||
Nonce string `form:"csrf_nonce"`
|
||||
}{}
|
||||
|
||||
if err := e.BindBody(&formdata); err != nil {
|
||||
return p.ErrorResponse(engine, e, fmt.Errorf("Formulardaten ungültig: %w", err))
|
||||
}
|
||||
|
||||
if _, err := CSRF_CACHE.ValidateTokenBundle(formdata.Nonce, formdata.CSRF); err != nil {
|
||||
return p.ErrorResponse(engine, e, err)
|
||||
}
|
||||
|
||||
user, err := app.FindRecordById(dbmodels.USERS_TABLE, formdata.User)
|
||||
if err != nil {
|
||||
return p.ErrorResponse(engine, e, fmt.Errorf("Konnte Nutzer nicht finden."))
|
||||
}
|
||||
|
||||
u := dbmodels.NewUser(user)
|
||||
|
||||
u.SetDeactivated(true)
|
||||
|
||||
if err := app.Save(u); err != nil {
|
||||
return p.ErrorResponse(engine, e, fmt.Errorf("Konnte Nutzer nicht deaktivieren: %w", err))
|
||||
}
|
||||
|
||||
DeleteSessionsForUser(app, u.Id)
|
||||
|
||||
data := make(map[string]any)
|
||||
data["success"] = "Nutzer " + u.Name() + "(" + u.Email() + ") wurde deaktiviert."
|
||||
|
||||
p.getData(app, data)
|
||||
|
||||
req := templating.NewRequest(e)
|
||||
if req.User() != nil && req.User().Id == u.Id {
|
||||
return e.Redirect(303, "/login/")
|
||||
}
|
||||
|
||||
e.Response.Header().Add("HX-Push-Url", "false")
|
||||
return engine.Response200(e, p.Template, data, p.Layout)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *UserManagementPage) POSTActivate(engine *templating.Engine, app core.App) HandleFunc {
|
||||
return func(e *core.RequestEvent) error {
|
||||
formdata := struct {
|
||||
User string `form:"uid"`
|
||||
CSRF string `form:"csrf_token"`
|
||||
Nonce string `form:"csrf_nonce"`
|
||||
}{}
|
||||
|
||||
if err := e.BindBody(&formdata); err != nil {
|
||||
return p.ErrorResponse(engine, e, fmt.Errorf("Formulardaten ungültig: %w", err))
|
||||
}
|
||||
|
||||
if _, err := CSRF_CACHE.ValidateTokenBundle(formdata.Nonce, formdata.CSRF); err != nil {
|
||||
return p.ErrorResponse(engine, e, err)
|
||||
}
|
||||
|
||||
user, err := app.FindRecordById(dbmodels.USERS_TABLE, formdata.User)
|
||||
if err != nil {
|
||||
return p.ErrorResponse(engine, e, fmt.Errorf("Konnte Nutzer nicht finden."))
|
||||
}
|
||||
|
||||
u := dbmodels.NewUser(user)
|
||||
|
||||
u.SetDeactivated(false)
|
||||
|
||||
if err := app.Save(u); err != nil {
|
||||
return p.ErrorResponse(engine, e, fmt.Errorf("Konnte Nutzer nicht aktivieren: %w", err))
|
||||
}
|
||||
|
||||
go DeleteSessionsForUser(app, u.Id)
|
||||
|
||||
data := make(map[string]any)
|
||||
data["success"] = "Nutzer " + u.Name() + "(" + u.Email() + ") wurde aktiviert."
|
||||
|
||||
p.getData(app, data)
|
||||
|
||||
req := templating.NewRequest(e)
|
||||
if req.User() != nil && req.User().Id == u.Id {
|
||||
return e.Redirect(303, "/login/")
|
||||
}
|
||||
|
||||
e.Response.Header().Add("HX-Push-Url", "false")
|
||||
return engine.Response200(e, p.Template, data, p.Layout)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *UserManagementPage) POSTLogout(engine *templating.Engine, app core.App) HandleFunc {
|
||||
return func(e *core.RequestEvent) error {
|
||||
formdata := struct {
|
||||
User string `form:"uid"`
|
||||
CSRF string `form:"csrf_token"`
|
||||
Nonce string `form:"csrf_nonce"`
|
||||
}{}
|
||||
|
||||
if err := e.BindBody(&formdata); err != nil {
|
||||
return p.ErrorResponse(engine, e, fmt.Errorf("Formulardaten ungültig: %w", err))
|
||||
}
|
||||
|
||||
if _, err := CSRF_CACHE.ValidateTokenBundle(formdata.Nonce, formdata.CSRF); err != nil {
|
||||
return p.ErrorResponse(engine, e, err)
|
||||
}
|
||||
|
||||
user, err := app.FindRecordById(dbmodels.USERS_TABLE, formdata.User)
|
||||
if err != nil {
|
||||
return p.ErrorResponse(engine, e, fmt.Errorf("Konnte Nutzer nicht finden."))
|
||||
}
|
||||
|
||||
u := dbmodels.NewUser(user)
|
||||
DeleteSessionsForUser(app, u.Id)
|
||||
|
||||
data := make(map[string]any)
|
||||
data["success"] = "Nutzer " + u.Name() + "(" + u.Email() + ") wurde überall ausgeloggt."
|
||||
|
||||
p.getData(app, data)
|
||||
|
||||
req := templating.NewRequest(e)
|
||||
if req.User() != nil && req.User().Id == u.Id {
|
||||
return e.Redirect(301, "/login/")
|
||||
}
|
||||
|
||||
// TODO: is there a better way to do this?
|
||||
// This destroys the URL FullPath thing, bc fullURL is set to /user/management/logout/
|
||||
// Same above
|
||||
e.Response.Header().Add("HX-Push-Url", "false")
|
||||
return engine.Response200(e, p.Template, data, p.Layout)
|
||||
}
|
||||
}
|
||||
125
controllers/user_management_access.go
Normal file
125
controllers/user_management_access.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/Theodor-Springmann-Stiftung/musenalm/app"
|
||||
"github.com/Theodor-Springmann-Stiftung/musenalm/dbmodels"
|
||||
"github.com/Theodor-Springmann-Stiftung/musenalm/middleware"
|
||||
"github.com/Theodor-Springmann-Stiftung/musenalm/pagemodels"
|
||||
"github.com/Theodor-Springmann-Stiftung/musenalm/templating"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/tools/router"
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
)
|
||||
|
||||
const (
|
||||
URL_USER_MANAGEMENT_ACCESS = "/user/management/access/"
|
||||
TEMPLATE_USER_MANAGEMENT_ACCESS = "/user/management/access/"
|
||||
)
|
||||
|
||||
func init() {
|
||||
ump := &UserManagementAccessPage{
|
||||
StaticPage: pagemodels.StaticPage{
|
||||
Name: pagemodels.P_USER_MGMT_ACCESS_NAME,
|
||||
Layout: "blankfooter",
|
||||
Template: TEMPLATE_USER_MANAGEMENT_ACCESS,
|
||||
URL: URL_USER_MANAGEMENT_ACCESS,
|
||||
},
|
||||
}
|
||||
app.Register(ump)
|
||||
}
|
||||
|
||||
type UserManagementAccessPage struct {
|
||||
pagemodels.StaticPage
|
||||
}
|
||||
|
||||
func (p *UserManagementAccessPage) Setup(router *router.Router[*core.RequestEvent], app core.App, engine *templating.Engine) error {
|
||||
rg := router.Group(URL_USER_MANAGEMENT_ACCESS)
|
||||
rg.BindFunc(middleware.IsAdmin())
|
||||
rg.GET("{"+PATH_VALUE_ROLE+"}", p.GET(engine, app))
|
||||
rg.POST("{"+PATH_VALUE_ROLE+"}", p.POST(engine, app))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *UserManagementAccessPage) GET(engine *templating.Engine, app core.App) HandleFunc {
|
||||
return func(e *core.RequestEvent) error {
|
||||
role := e.Request.PathValue("role")
|
||||
if role != "User" && role != "Editor" && role != "Admin" {
|
||||
return engine.Response404(e, fmt.Errorf("invalid role: %s", role), nil)
|
||||
}
|
||||
|
||||
path_access := URL_USER_CREATE + role
|
||||
record, err := app.FindFirstRecordByData(dbmodels.ACCESS_TOKENS_TABLE, dbmodels.ACCESS_TOKENS_URL_FIELD, path_access)
|
||||
var access_token *dbmodels.AccessToken
|
||||
if err != nil {
|
||||
token, err := dbmodels.CreateAccessToken(app, "", path_access, 7*24*time.Hour)
|
||||
if err != nil {
|
||||
return engine.Response500(e, err, nil)
|
||||
}
|
||||
access_token = token
|
||||
} else {
|
||||
access_token = dbmodels.NewAccessToken(record)
|
||||
access_token.SetExpires(types.NowDateTime().Add(7 * 24 * time.Hour))
|
||||
if err := app.Save(access_token); err != nil {
|
||||
return engine.Response500(e, err, nil)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: check if access token exists, if not generate
|
||||
data := make(map[string]any)
|
||||
data["role"] = role
|
||||
data["access_url"] = "https://musenalm.de" + path_access + "?token=" + access_token.Token()
|
||||
data["relative_url"] = path_access + "?token=" + access_token.Token()
|
||||
data["validUntil"] = access_token.Expires().Time().Local().Format("02.01.2006 15:04")
|
||||
|
||||
nonce, token, err := CSRF_CACHE.GenerateTokenBundle()
|
||||
if err != nil {
|
||||
return engine.Response500(e, err, data)
|
||||
}
|
||||
data["csrf_nonce"] = nonce
|
||||
data["csrf_token"] = token
|
||||
|
||||
SetRedirect(data, e)
|
||||
|
||||
return engine.Response200(e, p.Template, data, p.Layout)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *UserManagementAccessPage) POST(engine *templating.Engine, app core.App) HandleFunc {
|
||||
return func(e *core.RequestEvent) error {
|
||||
role := e.Request.PathValue("role")
|
||||
if role != "User" && role != "Editor" && role != "Admin" {
|
||||
return engine.Response404(e, fmt.Errorf("invalid role: %s", role), nil)
|
||||
}
|
||||
|
||||
path_access := URL_USER_CREATE + role
|
||||
record, err := app.FindFirstRecordByData(dbmodels.ACCESS_TOKENS_TABLE, dbmodels.ACCESS_TOKENS_URL_FIELD, path_access)
|
||||
if err == nil {
|
||||
go app.Delete(record)
|
||||
}
|
||||
|
||||
token, err := dbmodels.CreateAccessToken(app, "", path_access, 7*24*time.Hour)
|
||||
if err != nil {
|
||||
return engine.Response500(e, err, nil)
|
||||
}
|
||||
|
||||
data := make(map[string]any)
|
||||
data["role"] = role
|
||||
data["access_url"] = "https://musenalm.de" + path_access + "?token=" + token.Token()
|
||||
data["relative_url"] = path_access + "?token=" + token.Token()
|
||||
data["validUntil"] = token.Expires().Time().Format("02.01.2006 15:04")
|
||||
|
||||
SetRedirect(data, e)
|
||||
|
||||
return engine.Response200(e, p.Template, data, p.Layout)
|
||||
}
|
||||
}
|
||||
|
||||
func SetRedirect(data map[string]any, e *core.RequestEvent) {
|
||||
redirect_url := e.Request.URL.Query().Get("redirectTo")
|
||||
if redirect_url != "" {
|
||||
data["redirect_url"] = redirect_url
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user