From 9da5f0085a3c7923aed81e29c3c6c2c76a59504d Mon Sep 17 00:00:00 2001 From: Jan-Lukas Else Date: Thu, 7 Sep 2023 17:01:53 +0200 Subject: [PATCH] Add option to filter for parameters on index --- check.go | 6 ++-- feeds.go | 5 ++-- geoMap.go | 16 +++++----- posts.go | 34 ++++++++++++++++----- postsDb.go | 80 +++++++++++++++++++++++++++++++++---------------- postsDb_test.go | 2 +- sitemap.go | 13 ++++---- ui.go | 9 +++--- 8 files changed, 109 insertions(+), 56 deletions(-) diff --git a/check.go b/check.go index d2f2b4c..58b37e4 100644 --- a/check.go +++ b/check.go @@ -20,9 +20,9 @@ import ( func (a *goBlog) checkAllExternalLinks() error { posts, err := a.getPosts(&postsRequestConfig{ - status: []postStatus{statusPublished}, - visibility: []postVisibility{visibilityPublic, visibilityUnlisted}, - withoutParameters: true, + status: []postStatus{statusPublished}, + visibility: []postVisibility{visibilityPublic, visibilityUnlisted}, + fetchWithoutParams: true, }) if err != nil { return err diff --git a/feeds.go b/feeds.go index 44a4503..a9beaa0 100644 --- a/feeds.go +++ b/feeds.go @@ -3,7 +3,6 @@ package main import ( "io" "net/http" - "strings" "time" "github.com/araddon/dateparse" @@ -24,14 +23,14 @@ const ( minJsonFeed feedType = "min.json" ) -func (a *goBlog) generateFeed(blog string, f feedType, w http.ResponseWriter, r *http.Request, posts []*post, title, description string) { +func (a *goBlog) generateFeed(blog string, f feedType, w http.ResponseWriter, r *http.Request, posts []*post, title, description, path, query string) { now := time.Now() title = a.renderMdTitle(defaultIfEmpty(title, a.cfg.Blogs[blog].Title)) description = defaultIfEmpty(description, a.cfg.Blogs[blog].Description) feed := &feeds.Feed{ Title: title, Description: description, - Link: &feeds.Link{Href: a.getFullAddress(strings.TrimSuffix(r.URL.Path, "."+string(f)))}, + Link: &feeds.Link{Href: a.getFullAddress(path) + query}, Created: now, Author: &feeds.Author{ Name: a.cfg.User.Name, diff --git a/geoMap.go b/geoMap.go index 65e4189..e4b0545 100644 --- a/geoMap.go +++ b/geoMap.go @@ -17,9 +17,9 @@ func (a *goBlog) serveGeoMap(w http.ResponseWriter, r *http.Request) { canonical := a.getFullAddress(mapPath) allPostsWithLocationRequestConfig := &postsRequestConfig{ - blog: blog, - parameters: []string{a.cfg.Micropub.LocationParam, gpxParameter}, - withOnlyParameters: []string{a.cfg.Micropub.LocationParam, gpxParameter}, + blog: blog, + anyParams: []string{a.cfg.Micropub.LocationParam, gpxParameter}, + fetchParams: []string{a.cfg.Micropub.LocationParam, gpxParameter}, } allPostsWithLocationRequestConfig.status, allPostsWithLocationRequestConfig.visibility = a.getDefaultPostStates(r) @@ -58,8 +58,8 @@ func (a *goBlog) serveGeoMapTracks(w http.ResponseWriter, r *http.Request) { allPostsWithTracksRequestConfig := &postsRequestConfig{ blog: blog, - parameters: []string{gpxParameter}, - withOnlyParameters: []string{gpxParameter}, + anyParams: []string{gpxParameter}, + fetchParams: []string{gpxParameter}, excludeParameter: showRouteParam, excludeParameterValue: "false", // Don't show hidden route tracks } @@ -102,9 +102,9 @@ func (a *goBlog) serveGeoMapLocations(w http.ResponseWriter, r *http.Request) { blog, _ := a.getBlog(r) allPostsWithLocationRequestConfig := &postsRequestConfig{ - blog: blog, - parameters: []string{a.cfg.Micropub.LocationParam}, - withOnlyParameters: []string{a.cfg.Micropub.LocationParam}, + blog: blog, + anyParams: []string{a.cfg.Micropub.LocationParam}, + fetchParams: []string{a.cfg.Micropub.LocationParam}, } allPostsWithLocationRequestConfig.status, allPostsWithLocationRequestConfig.visibility = a.getDefaultPostStates(r) diff --git a/posts.go b/posts.go index b2afc41..089f1ba 100644 --- a/posts.go +++ b/posts.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "net/http" + "net/url" "path" "reflect" "strings" @@ -314,12 +315,31 @@ func (a *goBlog) serveIndex(w http.ResponseWriter, r *http.Request) { if len(visibility) == 0 { visibility = defaultVisibility } + // Parameter filter + params, paramValues := []string{}, []string{} + paramUrlValues := url.Values{} + for param, values := range r.URL.Query() { + if strings.HasPrefix(param, "p:") { + paramKey := strings.TrimPrefix(param, "p:") + for _, value := range values { + params, paramValues = append(params, paramKey), append(paramValues, value) + paramUrlValues.Add(param, value) + } + } + } + paramUrlQuery := "" + if len(paramUrlValues) > 0 { + paramUrlQuery += "?" + paramUrlValues.Encode() + } + // Create paginator p := paginator.New(&postPaginationAdapter{config: &postsRequestConfig{ blog: blog, sections: sections, taxonomy: ic.tax, taxonomyValue: ic.taxValue, parameter: ic.parameter, + allParams: params, + allParamValues: paramValues, search: ic.search, publishedYear: ic.year, publishedMonth: ic.month, @@ -356,11 +376,10 @@ func (a *goBlog) serveIndex(w http.ResponseWriter, r *http.Request) { } // Check if feed if ft := feedType(chi.URLParam(r, "feed")); ft != noFeed { - a.generateFeed(blog, ft, w, r, posts, title, description) + a.generateFeed(blog, ft, w, r, posts, title, description, ic.path, paramUrlQuery) return } // Navigation - path := ic.path var hasPrev, hasNext bool var prevPage, nextPage int var prevPath, nextPath string @@ -371,9 +390,9 @@ func (a *goBlog) serveIndex(w http.ResponseWriter, r *http.Request) { prevPage, _ = p.Page() } if prevPage < 2 { - prevPath = path + prevPath = ic.path } else { - prevPath = fmt.Sprintf("%s/page/%d", strings.TrimSuffix(path, "/"), prevPage) + prevPath = fmt.Sprintf("%s/page/%d", strings.TrimSuffix(ic.path, "/"), prevPage) } hasNext, _ = p.HasNext() if hasNext { @@ -381,23 +400,24 @@ func (a *goBlog) serveIndex(w http.ResponseWriter, r *http.Request) { } else { nextPage, _ = p.Page() } - nextPath = fmt.Sprintf("%s/page/%d", strings.TrimSuffix(path, "/"), nextPage) + nextPath = fmt.Sprintf("%s/page/%d", strings.TrimSuffix(ic.path, "/"), nextPage) summaryTemplate := ic.summaryTemplate if summaryTemplate == "" { summaryTemplate = defaultSummary } a.render(w, r, a.renderIndex, &renderData{ - Canonical: a.getFullAddress(path), + Canonical: a.getFullAddress(ic.path) + paramUrlQuery, Data: &indexRenderData{ title: title, description: description, posts: posts, hasPrev: hasPrev, hasNext: hasNext, - first: path, + first: ic.path, prev: prevPath, next: nextPath, summaryTemplate: summaryTemplate, + paramUrlQuery: paramUrlQuery, }, }) } diff --git a/postsDb.go b/postsDb.go index c1751ef..2750c48 100644 --- a/postsDb.go +++ b/postsDb.go @@ -373,21 +373,23 @@ type postsRequestConfig struct { visibility []postVisibility taxonomy *configTaxonomy taxonomyValue string - parameters []string // Ignores parameterValue - parameter string // Ignores parameters - parameterValue string - excludeParameter string // exclude posts that have a certain parameter (with non-empty value) - excludeParameterValue string // ... with exactly this value + anyParams []string // filter for posts that have any of these parameters (with non-empty values) + allParams []string // filter for posts that have all these parameters (with non-empty values) + allParamValues []string // ... with exactly these values + parameter string // filter for posts that have this parameter (with non-empty value) + parameterValue string // ... with exactly this value + excludeParameter string // exclude posts that have this parameter (with non-empty value) + excludeParameterValue string // ... with exactly this value publishedYear, publishedMonth, publishedDay int publishedBefore time.Time randomOrder bool priorityOrder bool - withoutParameters bool - withOnlyParameters []string - withoutRenderedTitle bool + fetchWithoutParams bool // fetch posts without parameters + fetchParams []string // only fetch these parameters + withoutRenderedTitle bool // fetch posts without rendered title } -func buildPostsQuery(c *postsRequestConfig, selection string) (query string, args []any) { +func buildPostsQuery(c *postsRequestConfig, selection string) (query string, args []any, err error) { queryBuilder := builderpool.Get() defer builderpool.Put(queryBuilder) // Selection @@ -437,21 +439,40 @@ func buildPostsQuery(c *postsRequestConfig, selection string) (query string, arg queryBuilder.WriteString(" and blog = @blog") args = append(args, sql.Named("blog", c.blog)) } - if c.parameter != "" { - if c.parameterValue != "" { - queryBuilder.WriteString(" and path in (select path from post_parameters where parameter = @param and value = @paramval)") - args = append(args, sql.Named("param", c.parameter), sql.Named("paramval", c.parameterValue)) - } else { - queryBuilder.WriteString(" and path in (select path from post_parameters where parameter = @param and length(coalesce(value, '')) > 0)") - args = append(args, sql.Named("param", c.parameter)) + allParams := append(c.allParams, c.parameter) + allParamValues := append(c.allParamValues, c.parameterValue) + if len(allParams) > 0 { + if len(allParamValues) > 0 && len(allParamValues) != len(allParams) { + return "", nil, errors.New("number of parameters != number of parameter values") } - } else if len(c.parameters) > 0 { + for i, param := range allParams { + if param == "" { + continue + } + named := "allparam" + strconv.Itoa(i) + paramValue := allParamValues[i] + queryBuilder.WriteString(" and path in (select path from post_parameters where parameter = @") + queryBuilder.WriteString(named) + queryBuilder.WriteString(" and ") + if paramValue != "" { + namedVal := "allparamval" + strconv.Itoa(i) + queryBuilder.WriteString("value = @") + queryBuilder.WriteString(namedVal) + args = append(args, sql.Named(namedVal, paramValue)) + } else { + queryBuilder.WriteString("length(coalesce(value, '')) > 0") + } + queryBuilder.WriteString(")") + args = append(args, sql.Named(named, param)) + } + } + if len(c.anyParams) > 0 { queryBuilder.WriteString(" and path in (select path from post_parameters where parameter in (") - for i, param := range c.parameters { + for i, param := range c.anyParams { if i > 0 { queryBuilder.WriteString(", ") } - named := "param" + strconv.Itoa(i) + named := "anyparam" + strconv.Itoa(i) queryBuilder.WriteString("@") queryBuilder.WriteString(named) args = append(args, param) @@ -514,7 +535,7 @@ func buildPostsQuery(c *postsRequestConfig, selection string) (query string, arg queryBuilder.WriteString(" limit @limit offset @offset") args = append(args, sql.Named("limit", c.limit), sql.Named("offset", c.offset)) } - return queryBuilder.String(), args + return queryBuilder.String(), args, nil } func (d *database) loadPostParameters(posts []*post, parameters ...string) (err error) { @@ -582,7 +603,10 @@ func (d *database) loadPostParameters(posts []*post, parameters ...string) (err func (a *goBlog) getPosts(config *postsRequestConfig) (posts []*post, err error) { // Query posts - query, queryParams := buildPostsQuery(config, "path, coalesce(content, ''), coalesce(published, ''), coalesce(updated, ''), blog, coalesce(section, ''), status, visibility, priority") + query, queryParams, err := buildPostsQuery(config, "path, coalesce(content, ''), coalesce(published, ''), coalesce(updated, ''), blog, coalesce(section, ''), status, visibility, priority") + if err != nil { + return nil, err + } rows, err := a.db.Query(query, queryParams...) if err != nil { return nil, err @@ -608,8 +632,8 @@ func (a *goBlog) getPosts(config *postsRequestConfig) (posts []*post, err error) } posts = append(posts, p) } - if !config.withoutParameters { - err = a.db.loadPostParameters(posts, config.withOnlyParameters...) + if !config.fetchWithoutParams { + err = a.db.loadPostParameters(posts, config.fetchParams...) if err != nil { return nil, err } @@ -636,7 +660,10 @@ func (a *goBlog) getPost(path string) (*post, error) { } func (d *database) countPosts(config *postsRequestConfig) (count int, err error) { - query, params := buildPostsQuery(config, "path") + query, params, err := buildPostsQuery(config, "path") + if err != nil { + return + } row, err := d.QueryRow("select count(distinct path) from ("+query+")", params...) if err != nil { return @@ -647,7 +674,7 @@ func (d *database) countPosts(config *postsRequestConfig) (count int, err error) func (a *goBlog) getRandomPostPath(blog string) (path string, err error) { sections := lo.Keys(a.cfg.Blogs[blog].Sections) - query, params := buildPostsQuery(&postsRequestConfig{ + query, params, err := buildPostsQuery(&postsRequestConfig{ randomOrder: true, limit: 1, blog: blog, @@ -655,6 +682,9 @@ func (a *goBlog) getRandomPostPath(blog string) (path string, err error) { visibility: []postVisibility{visibilityPublic}, status: []postStatus{statusPublished}, }, "path") + if err != nil { + return + } row, err := a.db.QueryRow(query, params...) if err != nil { return diff --git a/postsDb_test.go b/postsDb_test.go index 43290bf..66446ea 100644 --- a/postsDb_test.go +++ b/postsDb_test.go @@ -79,7 +79,7 @@ func Test_postsDb(t *testing.T) { is.Equal(0, count) // Check by multiple parameters - count, err = app.db.countPosts(&postsRequestConfig{parameters: []string{"tags", "title"}}) + count, err = app.db.countPosts(&postsRequestConfig{anyParams: []string{"tags", "title"}}) must.NoError(err) is.Equal(1, count) diff --git a/sitemap.go b/sitemap.go index 6d4e9d0..10de348 100644 --- a/sitemap.go +++ b/sitemap.go @@ -162,10 +162,10 @@ func (a *goBlog) serveSitemapBlogPosts(w http.ResponseWriter, r *http.Request) { // Request posts blog, _ := a.getBlog(r) posts, _ := a.getPosts(&postsRequestConfig{ - status: []postStatus{statusPublished}, - visibility: []postVisibility{visibilityPublic}, - blog: blog, - withoutParameters: true, + status: []postStatus{statusPublished}, + visibility: []postVisibility{visibilityPublic}, + blog: blog, + fetchWithoutParams: true, }) // Add posts to sitemap for _, p := range posts { @@ -220,12 +220,15 @@ select distinct '/x/x/' || day from alldates; ` func (a *goBlog) sitemapDatePaths(blog string, sections []string) (paths []string, err error) { - query, args := buildPostsQuery(&postsRequestConfig{ + query, args, err := buildPostsQuery(&postsRequestConfig{ blog: blog, sections: sections, status: []postStatus{statusPublished}, visibility: []postVisibility{visibilityPublic}, }, "published") + if err != nil { + return + } rows, err := a.db.Query(fmt.Sprintf(sitemapDatePathsSql, query), args...) if err != nil { return nil, err diff --git a/ui.go b/ui.go index 3e8a5b4..1c46390 100644 --- a/ui.go +++ b/ui.go @@ -340,6 +340,7 @@ type indexRenderData struct { posts []*post hasPrev, hasNext bool first, prev, next string + paramUrlQuery string summaryTemplate summaryTyp } @@ -359,9 +360,9 @@ func (a *goBlog) renderIndex(hb *htmlbuilder.HtmlBuilder, rd *renderData) { if renderedIndexTitle != "" { feedTitle = " (" + renderedIndexTitle + ")" } - hb.WriteElementOpen("link", "rel", "alternate", "type", "application/rss+xml", "title", "RSS"+feedTitle, "href", a.getFullAddress(id.first+".rss")) - hb.WriteElementOpen("link", "rel", "alternate", "type", "application/atom+xml", "title", "ATOM"+feedTitle, "href", a.getFullAddress(id.first+".atom")) - hb.WriteElementOpen("link", "rel", "alternate", "type", "application/feed+json", "title", "JSON Feed"+feedTitle, "href", a.getFullAddress(id.first+".json")) + hb.WriteElementOpen("link", "rel", "alternate", "type", "application/rss+xml", "title", "RSS"+feedTitle, "href", a.getFullAddress(id.first+".rss")+id.paramUrlQuery) + hb.WriteElementOpen("link", "rel", "alternate", "type", "application/atom+xml", "title", "ATOM"+feedTitle, "href", a.getFullAddress(id.first+".atom")+id.paramUrlQuery) + hb.WriteElementOpen("link", "rel", "alternate", "type", "application/feed+json", "title", "JSON Feed"+feedTitle, "href", a.getFullAddress(id.first+".json")+id.paramUrlQuery) }, func(hb *htmlbuilder.HtmlBuilder) { hb.WriteElementOpen("main", "class", "h-feed") @@ -393,7 +394,7 @@ func (a *goBlog) renderIndex(hb *htmlbuilder.HtmlBuilder, rd *renderData) { hb.WriteElementClose("p") } // Navigation - a.renderPagination(hb, rd.Blog, id.hasPrev, id.hasNext, id.prev, id.next) + a.renderPagination(hb, rd.Blog, id.hasPrev, id.hasNext, id.prev+id.paramUrlQuery, id.next+id.paramUrlQuery) // Author a.renderAuthor(hb) hb.WriteElementClose("main")