From f4977c7a3484607a63ba32050c41ba206eeda1f6 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Mon, 13 Sep 2021 18:36:36 -0400 Subject: [PATCH 1/6] Support filtering blog posts by language Ref T805 --- collections.go | 97 ++++++++++++++++++++++++++++++++++++++++++++++++++ database.go | 55 ++++++++++++++++++++++++++++ routes.go | 1 + 3 files changed, 153 insertions(+) diff --git a/collections.go b/collections.go index beadf19..a3889a0 100644 --- a/collections.go +++ b/collections.go @@ -28,6 +28,7 @@ import ( "github.com/writeas/web-core/activitystreams" "github.com/writeas/web-core/auth" "github.com/writeas/web-core/bots" + "github.com/writeas/web-core/i18n" "github.com/writeas/web-core/log" waposts "github.com/writeas/web-core/posts" "github.com/writefreely/writefreely/author" @@ -366,6 +367,16 @@ func (c CollectionPage) DisplayMonetization() string { return displayMonetization(c.Monetization, c.Alias) } +func (c *DisplayCollection) Direction() string { + if c.Language == "" { + return "auto" + } + if i18n.LangIsRTL(c.Language) { + return "rtl" + } + return "ltr" +} + func newCollection(app *App, w http.ResponseWriter, r *http.Request) error { reqJSON := IsJSON(r) alias := r.FormValue("alias") @@ -991,6 +1002,92 @@ func handleViewCollectionTag(app *App, w http.ResponseWriter, r *http.Request) e return nil } +func handleViewCollectionLang(app *App, w http.ResponseWriter, r *http.Request) error { + vars := mux.Vars(r) + lang := vars["lang"] + + cr := &collectionReq{} + err := processCollectionRequest(cr, vars, w, r) + if err != nil { + return err + } + + u, err := checkUserForCollection(app, cr, r, false) + if err != nil { + return err + } + + page := getCollectionPage(vars) + + c, err := processCollectionPermissions(app, cr, u, w, r) + if c == nil || err != nil { + return err + } + + coll := newDisplayCollection(c, cr, page) + coll.Language = lang + + coll.Posts, _ = app.db.GetLangPosts(app.cfg, c, lang, page, cr.isCollOwner) + if err != nil { + return ErrCollectionPageNotFound + } + + // Serve collection + displayPage := struct { + CollectionPage + Tag string + }{ + CollectionPage: CollectionPage{ + DisplayCollection: coll, + StaticPage: pageForReq(app, r), + IsCustomDomain: cr.isCustomDomain, + }, + Tag: lang, + } + var owner *User + if u != nil { + displayPage.Username = u.Username + displayPage.IsOwner = u.ID == coll.OwnerID + if displayPage.IsOwner { + // Add in needed information for users viewing their own collection + owner = u + displayPage.CanPin = true + + pubColls, err := app.db.GetPublishableCollections(owner, app.cfg.App.Host) + if err != nil { + log.Error("unable to fetch collections: %v", err) + } + displayPage.Collections = pubColls + } + } + isOwner := owner != nil + if !isOwner { + // Current user doesn't own collection; retrieve owner information + owner, err = app.db.GetUserByID(coll.OwnerID) + if err != nil { + // Log the error and just continue + log.Error("Error getting user for collection: %v", err) + } + if owner.IsSilenced() { + return ErrCollectionNotFound + } + } + displayPage.Silenced = owner != nil && owner.IsSilenced() + displayPage.Owner = owner + coll.Owner = displayPage.Owner + // Add more data + // TODO: fix this mess of collections inside collections + displayPage.PinnedPosts, _ = app.db.GetPinnedPosts(coll.CollectionObj, isOwner) + displayPage.Monetization = app.db.GetCollectionAttribute(coll.ID, "monetization_pointer") + + err = templates["collection"].ExecuteTemplate(w, "collection", displayPage) + if err != nil { + log.Error("Unable to render collection lang page: %v", err) + } + + return nil +} + func handleCollectionPostRedirect(app *App, w http.ResponseWriter, r *http.Request) error { vars := mux.Vars(r) slug := vars["slug"] diff --git a/database.go b/database.go index f474ae9..455a617 100644 --- a/database.go +++ b/database.go @@ -1260,6 +1260,61 @@ func (db *datastore) GetPostsTagged(cfg *config.Config, c *Collection, tag strin return &posts, nil } +func (db *datastore) GetLangPosts(cfg *config.Config, c *Collection, lang string, page int, includeFuture bool) (*[]PublicPost, error) { + collID := c.ID + + cf := c.NewFormat() + order := "DESC" + if cf.Ascending() { + order = "ASC" + } + + pagePosts := cf.PostsPerPage() + start := page*pagePosts - pagePosts + if page == 0 { + start = 0 + pagePosts = 1000 + } + + limitStr := "" + if page > 0 { + limitStr = fmt.Sprintf(" LIMIT %d, %d", start, pagePosts) + } + timeCondition := "" + if !includeFuture { + timeCondition = "AND created <= " + db.now() + } + + rows, err := db.Query("SELECT "+postCols+" FROM posts WHERE collection_id = ? AND language = ? "+timeCondition+" ORDER BY created "+order+limitStr, collID, lang) + if err != nil { + log.Error("Failed selecting from posts: %v", err) + return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve collection posts."} + } + defer rows.Close() + + // TODO: extract this common row scanning logic for queries using `postCols` + posts := []PublicPost{} + for rows.Next() { + p := &Post{} + err = rows.Scan(&p.ID, &p.Slug, &p.Font, &p.Language, &p.RTL, &p.Privacy, &p.OwnerID, &p.CollectionID, &p.PinnedPosition, &p.Created, &p.Updated, &p.ViewCount, &p.Title, &p.Content) + if err != nil { + log.Error("Failed scanning row: %v", err) + break + } + p.extractData() + p.augmentContent(c) + p.formatContent(cfg, c, includeFuture, false) + + posts = append(posts, p.processPost()) + } + err = rows.Err() + if err != nil { + log.Error("Error after Next() on rows: %v", err) + } + + return &posts, nil +} + func (db *datastore) GetAPFollowers(c *Collection) (*[]RemoteUser, error) { rows, err := db.Query("SELECT actor_id, inbox, shared_inbox FROM remotefollows f INNER JOIN remoteusers u ON f.remote_user_id = u.id WHERE collection_id = ?", c.ID) if err != nil { diff --git a/routes.go b/routes.go index 213958d..22d2496 100644 --- a/routes.go +++ b/routes.go @@ -216,6 +216,7 @@ func InitRoutes(apper Apper, r *mux.Router) *mux.Router { func RouteCollections(handler *Handler, r *mux.Router) { r.HandleFunc("/logout", handler.Web(handleLogOutCollection, UserLevelOptional)) r.HandleFunc("/page/{page:[0-9]+}", handler.Web(handleViewCollection, UserLevelReader)) + r.HandleFunc("/lang:{lang}", handler.Web(handleViewCollectionLang, UserLevelOptional)) r.HandleFunc("/tag:{tag}", handler.Web(handleViewCollectionTag, UserLevelReader)) r.HandleFunc("/tag:{tag}/feed/", handler.Web(ViewFeed, UserLevelReader)) r.HandleFunc("/sitemap.xml", handler.AllReader(handleViewSitemap)) From c4b124e37c9edab7a58e25fb1f300c9deaf4bc0d Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Thu, 16 Sep 2021 14:05:52 -0400 Subject: [PATCH 2/6] Limit lang filter to 2 characters Ref T805 --- routes.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routes.go b/routes.go index 22d2496..aa3b4f2 100644 --- a/routes.go +++ b/routes.go @@ -216,7 +216,7 @@ func InitRoutes(apper Apper, r *mux.Router) *mux.Router { func RouteCollections(handler *Handler, r *mux.Router) { r.HandleFunc("/logout", handler.Web(handleLogOutCollection, UserLevelOptional)) r.HandleFunc("/page/{page:[0-9]+}", handler.Web(handleViewCollection, UserLevelReader)) - r.HandleFunc("/lang:{lang}", handler.Web(handleViewCollectionLang, UserLevelOptional)) + r.HandleFunc("/lang:{lang:[a-z]{2}}", handler.Web(handleViewCollectionLang, UserLevelOptional)) r.HandleFunc("/tag:{tag}", handler.Web(handleViewCollectionTag, UserLevelReader)) r.HandleFunc("/tag:{tag}/feed/", handler.Web(ViewFeed, UserLevelReader)) r.HandleFunc("/sitemap.xml", handler.AllReader(handleViewSitemap)) From 414d5b0a1c7addd89ad95d9b20737e8b25459a8b Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Thu, 16 Sep 2021 14:23:35 -0400 Subject: [PATCH 3/6] Add pagination routes on lang post filter Ref T805 --- collections.go | 14 ++++++++++++++ database.go | 15 ++++++++++++++- routes.go | 1 + 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/collections.go b/collections.go index a3889a0..db71295 100644 --- a/collections.go +++ b/collections.go @@ -1027,6 +1027,20 @@ func handleViewCollectionLang(app *App, w http.ResponseWriter, r *http.Request) coll := newDisplayCollection(c, cr, page) coll.Language = lang + ttlPosts, err := app.db.GetCollLangTotalPosts(coll.ID, lang) + if err != nil { + log.Error("Unable to getCollLangTotalPosts: %s", err) + } + pagePosts := coll.Format.PostsPerPage() + coll.TotalPages = int(math.Ceil(float64(ttlPosts) / float64(pagePosts))) + if coll.TotalPages > 0 && page > coll.TotalPages { + redirURL := fmt.Sprintf("/lang:%s/page/%d", lang, coll.TotalPages) + if !app.cfg.App.SingleUser { + redirURL = fmt.Sprintf("/%s%s%s", cr.prefix, coll.Alias, redirURL) + } + return impart.HTTPError{http.StatusFound, redirURL} + } + coll.Posts, _ = app.db.GetLangPosts(app.cfg, c, lang, page, cr.isCollOwner) if err != nil { return ErrCollectionPageNotFound diff --git a/database.go b/database.go index 455a617..227fc0e 100644 --- a/database.go +++ b/database.go @@ -1260,6 +1260,16 @@ func (db *datastore) GetPostsTagged(cfg *config.Config, c *Collection, tag strin return &posts, nil } +func (db *datastore) GetCollLangTotalPosts(collID int64, lang string) (uint64, error) { + var articles uint64 + err := db.QueryRow("SELECT COUNT(*) FROM posts WHERE collection_id = ? AND language = ?", collID, lang).Scan(&articles) + if err != nil && err != sql.ErrNoRows { + log.Error("Couldn't get total lang posts count for collection %d: %v", collID, err) + return 0, err + } + return articles, nil +} + func (db *datastore) GetLangPosts(cfg *config.Config, c *Collection, lang string, page int, includeFuture bool) (*[]PublicPost, error) { collID := c.ID @@ -1285,7 +1295,10 @@ func (db *datastore) GetLangPosts(cfg *config.Config, c *Collection, lang string timeCondition = "AND created <= " + db.now() } - rows, err := db.Query("SELECT "+postCols+" FROM posts WHERE collection_id = ? AND language = ? "+timeCondition+" ORDER BY created "+order+limitStr, collID, lang) + rows, err := db.Query(`SELECT `+postCols+` +FROM posts +WHERE collection_id = ? AND language = ? `+timeCondition+` +ORDER BY created `+order+limitStr, collID, lang) if err != nil { log.Error("Failed selecting from posts: %v", err) return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve collection posts."} diff --git a/routes.go b/routes.go index aa3b4f2..bdc3d04 100644 --- a/routes.go +++ b/routes.go @@ -217,6 +217,7 @@ func RouteCollections(handler *Handler, r *mux.Router) { r.HandleFunc("/logout", handler.Web(handleLogOutCollection, UserLevelOptional)) r.HandleFunc("/page/{page:[0-9]+}", handler.Web(handleViewCollection, UserLevelReader)) r.HandleFunc("/lang:{lang:[a-z]{2}}", handler.Web(handleViewCollectionLang, UserLevelOptional)) + r.HandleFunc("/lang:{lang:[a-z]{2}}/page/{page:[0-9]+}", handler.Web(handleViewCollectionLang, UserLevelOptional)) r.HandleFunc("/tag:{tag}", handler.Web(handleViewCollectionTag, UserLevelReader)) r.HandleFunc("/tag:{tag}/feed/", handler.Web(ViewFeed, UserLevelReader)) r.HandleFunc("/sitemap.xml", handler.AllReader(handleViewSitemap)) From e91748c0bcc783916553fcc0cb40734c607dc469 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Thu, 16 Sep 2021 15:53:07 -0400 Subject: [PATCH 4/6] Return correct count of currently-published lang posts Previously, we'd include scheduled posts, too. Ref T805 --- database.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database.go b/database.go index 227fc0e..efc1a10 100644 --- a/database.go +++ b/database.go @@ -1262,7 +1262,7 @@ func (db *datastore) GetPostsTagged(cfg *config.Config, c *Collection, tag strin func (db *datastore) GetCollLangTotalPosts(collID int64, lang string) (uint64, error) { var articles uint64 - err := db.QueryRow("SELECT COUNT(*) FROM posts WHERE collection_id = ? AND language = ?", collID, lang).Scan(&articles) + err := db.QueryRow("SELECT COUNT(*) FROM posts WHERE collection_id = ? AND language = ? AND created <= "+db.now(), collID, lang).Scan(&articles) if err != nil && err != sql.ErrNoRows { log.Error("Couldn't get total lang posts count for collection %d: %v", collID, err) return 0, err From ebeb45ac5a1e920e27ff25502089ef0131e5f1bd Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Thu, 21 Sep 2023 16:58:44 -0400 Subject: [PATCH 5/6] Support pagination when viewing language-filtered posts Ref T805 --- collections.go | 15 +++++++++------ templates/chorus-collection.tmpl | 12 ++++++------ templates/collection.tmpl | 12 ++++++------ 3 files changed, 21 insertions(+), 18 deletions(-) diff --git a/collections.go b/collections.go index ac08592..f8d7498 100644 --- a/collections.go +++ b/collections.go @@ -74,6 +74,7 @@ type ( DisplayCollection struct { *CollectionObj Prefix string + NavSuffix string IsTopLevel bool CurrentPage int TotalPages int @@ -260,16 +261,16 @@ func (c *Collection) RedirectingCanonicalURL(isRedir bool) string { // PrevPageURL provides a full URL for the previous page of collection posts, // returning a /page/N result for pages >1 -func (c *Collection) PrevPageURL(prefix string, n int, tl bool) string { +func (c *Collection) PrevPageURL(prefix, navSuffix string, n int, tl bool) string { u := "" if n == 2 { // Previous page is 1; no need for /page/ prefix if prefix == "" { - u = "/" + u = navSuffix + "/" } // Else leave off trailing slash } else { - u = fmt.Sprintf("/page/%d", n-1) + u = fmt.Sprintf("%s/page/%d", navSuffix, n-1) } if tl { @@ -279,11 +280,12 @@ func (c *Collection) PrevPageURL(prefix string, n int, tl bool) string { } // NextPageURL provides a full URL for the next page of collection posts -func (c *Collection) NextPageURL(prefix string, n int, tl bool) string { +func (c *Collection) NextPageURL(prefix, navSuffix string, n int, tl bool) string { + if tl { - return fmt.Sprintf("/page/%d", n+1) + return fmt.Sprintf("%s/page/%d", navSuffix, n+1) } - return fmt.Sprintf("/%s%s/page/%d", prefix, c.Alias, n+1) + return fmt.Sprintf("/%s%s%s/page/%d", prefix, c.Alias, navSuffix, n+1) } func (c *Collection) DisplayTitle() string { @@ -1084,6 +1086,7 @@ func handleViewCollectionLang(app *App, w http.ResponseWriter, r *http.Request) coll := newDisplayCollection(c, cr, page) coll.Language = lang + coll.NavSuffix = fmt.Sprintf("/lang:%s", lang) ttlPosts, err := app.db.GetCollLangTotalPosts(coll.ID, lang) if err != nil { diff --git a/templates/chorus-collection.tmpl b/templates/chorus-collection.tmpl index 2bc165d..0fb5eaf 100644 --- a/templates/chorus-collection.tmpl +++ b/templates/chorus-collection.tmpl @@ -9,8 +9,8 @@ {{if .CustomCSS}}{{end}} - {{if gt .CurrentPage 1}}{{end}} - {{if lt .CurrentPage .TotalPages}}{{end}} + {{if gt .CurrentPage 1}}{{end}} + {{if lt .CurrentPage .TotalPages}}{{end}} {{if not .IsPrivate}}{{end}} @@ -92,11 +92,11 @@ body#collection header nav.tabs a:first-child { {{if gt .TotalPages 1}}{{end}} diff --git a/templates/collection.tmpl b/templates/collection.tmpl index 6e3d2dc..16ef873 100644 --- a/templates/collection.tmpl +++ b/templates/collection.tmpl @@ -9,8 +9,8 @@ {{if .CustomCSS}}{{end}} - {{if gt .CurrentPage 1}}{{end}} - {{if lt .CurrentPage .TotalPages}}{{end}} + {{if gt .CurrentPage 1}}{{end}} + {{if lt .CurrentPage .TotalPages}}{{end}} {{if not .IsPrivate}}{{end}} @@ -107,11 +107,11 @@ {{if gt .TotalPages 1}}{{end}} From d3f312a1e2e3f644bac121d62541e11e11e49407 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Thu, 21 Sep 2023 16:59:32 -0400 Subject: [PATCH 6/6] Use Chorus template (if configured) when filtering by language Ref T805 --- collections.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/collections.go b/collections.go index f8d7498..c579df1 100644 --- a/collections.go +++ b/collections.go @@ -1155,7 +1155,11 @@ func handleViewCollectionLang(app *App, w http.ResponseWriter, r *http.Request) displayPage.PinnedPosts, _ = app.db.GetPinnedPosts(coll.CollectionObj, isOwner) displayPage.Monetization = app.db.GetCollectionAttribute(coll.ID, "monetization_pointer") - err = templates["collection"].ExecuteTemplate(w, "collection", displayPage) + collTmpl := "collection" + if app.cfg.App.Chorus { + collTmpl = "chorus-collection" + } + err = templates[collTmpl].ExecuteTemplate(w, "collection", displayPage) if err != nil { log.Error("Unable to render collection lang page: %v", err) }