feat: editorial article list

This commit is contained in:
Florian Sylvain committed 2026-11-02 07:28:44 +01:00
1 parent a0b3c58c52
commit a671b81f80
103 files changed
+6493 -1805

No files matched your search

@@ -37,6 +37,8 @@ type Filters struct {
Size int
Q string
Status string
Lang *string
AuthorID *uint32
Sort Sort
Unpaged bool
}
@@ -93,6 +95,10 @@ type Repository interface {
FindPage(filters Filters) (PageResult, error)
CountByVisibility(onlineOnly bool) (Counts, error)
CountVisibleTo(userID uint32) (Counts, error)
CountVersions(filters Filters) (Counts, error)
FindStoryPage(filters StoryFilters) (StoryPage, error)
CountStories(filters StoryFilters) (StoryCounts, error)
ListByStories(storyIDs []uint32) ([]Article, error)
Get(id uint32) (Article, error)
GetByName(name string) (Article, error)
GetAll() ([]Article, error)
@@ -0,0 +1,44 @@
package article
const (
TabAll = "all"
TabMissing = "missing"
)
type StoryFilters struct {
SkipMissing bool
OnlyOnline bool
VisibleTo *uint32
SiteID *uint32
Lang string
Missing bool
Status string
AuthorID *uint32
Q string
VersionIDs *[]uint32
Sort Sort
Page int
Size int
}
func (f StoryFilters) PerLanguage() bool {
return f.Lang != "" && !f.Missing
}
type StoryRef struct {
StoryID uint32
ArticleID uint32
}
type StoryPage struct {
Refs []StoryRef
Total int
}
type StoryCounts struct {
All int `json:"all"`
Online int `json:"online"`
Draft int `json:"draft"`
Suggestions int `json:"suggestions"`
Missing int `json:"missing"`
}
@@ -61,6 +61,12 @@ func (a *ArticleRepository) scoped(filters domain.Filters) *gorm.DB {
case domain.StatusDraft:
query = query.Where("is_online = ?", false)
}
if filters.Lang != nil {
query = query.Where("lang = ?", *filters.Lang)
}
if filters.AuthorID != nil {
query = query.Where("id IN (SELECT article_id FROM article_authors WHERE author_id = ?)", *filters.AuthorID)
}
return query
}
@@ -188,6 +194,22 @@ func (a *ArticleRepository) CountVisibleTo(userID uint32) (domain.Counts, error)
return domain.Counts{All: int(online + draft), Online: int(online), Draft: int(draft)}, nil
}
func (a *ArticleRepository) CountVersions(filters domain.Filters) (domain.Counts, error) {
narrowed := domain.Filters{Lang: filters.Lang}
var online, draft int64
if err := a.scoped(narrowed).Where("is_online = ?", true).Count(&online).Error; err != nil {
return domain.Counts{}, err
}
onlineOnly := filters.IsOnline != nil && *filters.IsOnline
if !onlineOnly {
narrowed.VisibleTo = filters.VisibleTo
if err := a.scoped(narrowed).Where("is_online = ?", false).Count(&draft).Error; err != nil {
return domain.Counts{}, err
}
}
return domain.Counts{All: int(online + draft), Online: int(online), Draft: int(draft)}, nil
}
func toDomainArticles(entityArticles []entity.Article) []domain.Article {
domainArticles := make([]domain.Article, 0, len(entityArticles))
for _, entityArticle := range entityArticles {
@@ -0,0 +1,262 @@
package persistence
import (
domain "RenewCMS/internal/domain/article"
entity "RenewCMS/internal/infrastructure/persistence/models"
"strings"
)
type storySQL struct {
conditions []string
args []any
}
func (s *storySQL) add(condition string, args ...any) {
s.conditions = append(s.conditions, condition)
s.args = append(s.args, args...)
}
func (s storySQL) where() string {
return strings.Join(s.conditions, " AND ")
}
func readableCondition(alias string, filters domain.StoryFilters) storySQL {
condition := storySQL{}
condition.add(alias + ".deleted_at IS NULL")
if filters.OnlyOnline {
condition.add(alias+".is_online = ?", true)
}
if filters.VisibleTo != nil {
condition.add("("+alias+".is_online = ? OR "+alias+".created_by = ?)", true, *filters.VisibleTo)
}
return condition
}
func (s *storySQL) merge(other storySQL) {
s.conditions = append(s.conditions, other.conditions...)
s.args = append(s.args, other.args...)
}
func lacksReadableVersionIn(languageExpression string, filters domain.StoryFilters, languageArgs ...any) storySQL {
readable := readableCondition("w", filters)
condition := storySQL{}
condition.add("NOT EXISTS (SELECT 1 FROM articles w WHERE w.story_id = q.story_id AND "+readable.where()+" AND w.lang = "+languageExpression+")", append(readable.args, languageArgs...)...)
return condition
}
func (a *ArticleRepository) versionRestriction(filters domain.StoryFilters) ([]uint32, bool, error) {
restricted := filters.VersionIDs != nil
var allowed []uint32
if restricted {
allowed = *filters.VersionIDs
}
query := strings.TrimSpace(filters.Q)
if query == "" {
return allowed, restricted, nil
}
readable := readableCondition("q", filters)
if filters.SiteID != nil {
readable.add("q.site_id = ?", *filters.SiteID)
}
var candidates []entity.Article
if err := a.db.Raw("SELECT q.id AS id, q.title AS title FROM articles q WHERE "+readable.where(), readable.args...).Scan(&candidates).Error; err != nil {
return nil, false, err
}
needle := foldForSearch(query)
permitted := map[uint32]bool{}
for _, id := range allowed {
permitted[id] = true
}
matching := make([]uint32, 0)
for _, candidate := range candidates {
if restricted && !permitted[candidate.ID] {
continue
}
if strings.Contains(foldForSearch(candidate.Title), needle) {
matching = append(matching, candidate.ID)
}
}
return matching, true, nil
}
func qualifyingVersions(filters domain.StoryFilters, allowed []uint32, restricted bool) storySQL {
condition := readableCondition("q", filters)
if filters.SiteID != nil {
condition.add("q.site_id = ?", *filters.SiteID)
}
if filters.PerLanguage() {
condition.add("q.lang = ?", filters.Lang)
}
switch filters.Status {
case domain.StatusOnline:
condition.add("q.is_online = ?", true)
case domain.StatusDraft:
condition.add("q.is_online = ?", false)
}
if filters.AuthorID != nil {
condition.add("q.id IN (SELECT aa.article_id FROM article_authors aa WHERE aa.author_id = ?)", *filters.AuthorID)
}
if restricted {
if len(allowed) == 0 {
condition.add("1 = 0")
} else {
condition.add("q.id IN ?", allowed)
}
}
if filters.Missing {
condition.merge(lacksReadableVersionIn("?", filters, ""))
condition.merge(missingCondition(filters))
}
return condition
}
func missingCondition(filters domain.StoryFilters) storySQL {
condition := storySQL{}
if filters.Lang != "" {
condition.add("EXISTS (SELECT 1 FROM site_languages sl WHERE sl.site_id = q.site_id AND sl.lang = ? AND sl.enabled = ?)", filters.Lang, true)
condition.merge(lacksReadableVersionIn("?", filters, filters.Lang))
return condition
}
absent := lacksReadableVersionIn("sl.lang", filters)
condition.add("EXISTS (SELECT 1 FROM site_languages sl WHERE sl.site_id = q.site_id AND sl.enabled = ? AND "+strings.Join(absent.conditions, " AND ")+")", append([]any{true}, absent.args...)...)
return condition
}
func unitExpression(filters domain.StoryFilters) string {
if filters.PerLanguage() {
return "COUNT(*)"
}
return "COUNT(DISTINCT q.story_id)"
}
func (a *ArticleRepository) countUnits(filters domain.StoryFilters, allowed []uint32, restricted bool) (int, error) {
qualifying := qualifyingVersions(filters, allowed, restricted)
var total int64
err := a.db.Raw("SELECT "+unitExpression(filters)+" FROM articles q WHERE "+qualifying.where(), qualifying.args...).Scan(&total).Error
return int(total), err
}
var storyRowOrder = map[domain.SortField]string{
domain.SortByTitle: "s.sort_title",
domain.SortByStatus: "s.sort_online",
domain.SortByCreatedAt: "s.sort_created",
domain.SortByUpdatedAt: "s.sort_updated",
}
var versionRowOrder = map[domain.SortField]string{
domain.SortByTitle: "LOWER(q.title)",
domain.SortByStatus: "q.is_online",
domain.SortByCreatedAt: "q.created_at",
domain.SortByUpdatedAt: "q.updated_at",
}
func storyOrderClause(columns map[domain.SortField]string, sort domain.Sort, storyColumn string) string {
column, ok := columns[sort.Field]
direction, directionOk := sortDirections[sort.Direction]
if !ok || !directionOk {
column, direction = columns[domain.SortByUpdatedAt], sortDirections[domain.SortDesc]
}
return column + " " + direction + ", " + storyColumn + " DESC"
}
type storyRefRow struct {
StoryID uint32
ArticleID uint32
}
func toStoryRefs(rows []storyRefRow) []domain.StoryRef {
refs := make([]domain.StoryRef, 0, len(rows))
for _, row := range rows {
refs = append(refs, domain.StoryRef{StoryID: row.StoryID, ArticleID: row.ArticleID})
}
return refs
}
func (a *ArticleRepository) FindStoryPage(filters domain.StoryFilters) (domain.StoryPage, error) {
filters.Page = max(filters.Page, 1)
filters.Size = max(filters.Size, 0)
allowed, restricted, err := a.versionRestriction(filters)
if err != nil {
return domain.StoryPage{}, err
}
total, err := a.countUnits(filters, allowed, restricted)
if err != nil {
return domain.StoryPage{}, err
}
page := domain.StoryPage{Refs: []domain.StoryRef{}, Total: total}
if total == 0 {
return page, nil
}
qualifying := qualifyingVersions(filters, allowed, restricted)
offset := (filters.Page - 1) * filters.Size
var rows []storyRefRow
if filters.PerLanguage() {
query := "SELECT q.story_id AS story_id, q.id AS article_id FROM articles q WHERE " + qualifying.where() +
" ORDER BY " + storyOrderClause(versionRowOrder, filters.Sort, "q.story_id") + " LIMIT ? OFFSET ?"
err = a.db.Raw(query, append(qualifying.args, filters.Size, offset)...).Scan(&rows).Error
} else {
query, args := storyRowsQuery(filters, qualifying)
err = a.db.Raw(query, append(args, filters.Size, offset)...).Scan(&rows).Error
}
if err != nil {
return domain.StoryPage{}, err
}
page.Refs = toStoryRefs(rows)
return page, nil
}
func storyRowsQuery(filters domain.StoryFilters, qualifying storySQL) (string, []any) {
readable := readableCondition("a", filters)
args := []any{true}
args = append(args, readable.args...)
args = append(args, qualifying.args...)
query := "SELECT s.story_id AS story_id, s.article_id AS article_id FROM (" +
"SELECT r.story_id AS story_id, r.id AS article_id, r.sort_title AS sort_title, r.sort_online AS sort_online, r.sort_updated AS sort_updated, r.sort_created AS sort_created FROM (" +
"SELECT a.id AS id, a.story_id AS story_id, LOWER(a.title) AS sort_title, a.is_online AS sort_online, " +
"MAX(a.updated_at) OVER (PARTITION BY a.story_id) AS sort_updated, " +
"MIN(a.created_at) OVER (PARTITION BY a.story_id) AS sort_created, " +
"ROW_NUMBER() OVER (PARTITION BY a.story_id ORDER BY CASE WHEN d.lang IS NULL THEN 1 ELSE 0 END, a.lang, a.id) AS row_rank " +
"FROM articles a LEFT JOIN site_languages d ON d.site_id = a.site_id AND d.lang = a.lang AND d.is_default = ? " +
"WHERE " + readable.where() + " AND a.story_id IN (SELECT q.story_id FROM articles q WHERE " + qualifying.where() + ")" +
") r WHERE r.row_rank = 1) s ORDER BY " + storyOrderClause(storyRowOrder, filters.Sort, "s.story_id") + " LIMIT ? OFFSET ?"
return query, args
}
func (a *ArticleRepository) CountStories(filters domain.StoryFilters) (domain.StoryCounts, error) {
filters.Status = ""
allowed, restricted, err := a.versionRestriction(filters)
if err != nil {
return domain.StoryCounts{}, err
}
counts := domain.StoryCounts{}
for _, target := range []struct {
status string
into *int
}{{"", &counts.All}, {domain.StatusOnline, &counts.Online}, {domain.StatusDraft, &counts.Draft}} {
scoped := filters
scoped.Missing = false
scoped.Status = target.status
if *target.into, err = a.countUnits(scoped, allowed, restricted); err != nil {
return domain.StoryCounts{}, err
}
}
if filters.SkipMissing {
return counts, nil
}
missing := filters
missing.Missing = true
counts.Missing, err = a.countUnits(missing, allowed, restricted)
return counts, err
}
func (a *ArticleRepository) ListByStories(storyIDs []uint32) ([]domain.Article, error) {
var found []entity.Article
for _, chunk := range chunkIDs(storyIDs) {
var loaded []entity.Article
if err := a.db.Model(&entity.Article{}).Where("story_id IN ?", chunk).Order("id ASC").Find(&loaded).Error; err != nil {
return nil, err
}
found = append(found, loaded...)
}
return a.withAuthors(a.db, toDomainArticles(found))
}
@@ -0,0 +1,262 @@
package useCases
import (
"RenewCMS/internal/domain/article"
"RenewCMS/internal/domain/author"
"RenewCMS/internal/domain/builtintype"
"RenewCMS/internal/domain/language"
"RenewCMS/internal/domain/localization"
"RenewCMS/internal/domain/role"
"slices"
"time"
)
type ListStoriesCommand struct {
Format article.BodyFormat
Caller ContentCaller
Page int
Size int
Q string
Status string
Sort article.Sort
SiteKey *string
SEO string
Lang string
Tab string
Author *uint32
}
type StoryRow struct {
Article article.Article
SEO article.SEOSummary
UpdatedAt time.Time
Authors []author.Byline
Coverage []article.Translation
MissingLangs []string
Capabilities []string
}
type StoryRowsPage struct {
Rows []StoryRow
Total int
Page int
Size int
Counts article.StoryCounts
}
func (u *ListArticlesUseCase) ListStoriesPage(command ListStoriesCommand) (StoryRowsPage, error) {
visibility := visibilityFor(command.Caller)
creatable := command.Caller.Grants.HoldsFor(role.KeyContentCreate, builtintype.KeyArticle)
empty := StoryRowsPage{Rows: []StoryRow{}, Page: command.Page, Size: command.Size}
filters := storyFilters(command, visibility, command.Lang == "" && !creatable)
owners := &siteOwners{repository: u.sites}
if command.SiteKey != nil {
siteID, known, err := u.siteIDOf(*command.SiteKey)
if err != nil {
return StoryRowsPage{}, err
}
if !known {
return empty, nil
}
filters.SiteID = &siteID
}
summaries, visibleVersions, err := u.summarizeVisibleVersions(visibility, owners)
if err != nil {
return StoryRowsPage{}, err
}
if command.SEO != "" {
allowed, err := u.versionsWithSEOStatus(command, filters, summaries)
if err != nil {
return StoryRowsPage{}, err
}
filters.VersionIDs = &allowed
}
counts, err := u.articleRepository.CountStories(filters)
if err != nil {
return StoryRowsPage{}, err
}
counts.Suggestions = countSuggestedStories(visibleVersions, summaries)
empty.Counts = counts
if command.Tab == article.TabMissing && command.Lang == "" && !creatable {
return empty, nil
}
found, err := u.articleRepository.FindStoryPage(filters)
if err != nil {
return StoryRowsPage{}, err
}
empty.Total = found.Total
if len(found.Refs) == 0 {
return empty, nil
}
rows, err := u.storyRows(command, found.Refs, creatable, owners)
if err != nil {
return StoryRowsPage{}, err
}
empty.Rows = rows
return empty, nil
}
func storyFilters(command ListStoriesCommand, visibility articleVisibility, skipMissing bool) article.StoryFilters {
return article.StoryFilters{
SkipMissing: skipMissing,
OnlyOnline: visibility.onlineOnly,
VisibleTo: visibility.userID,
Lang: command.Lang,
Missing: command.Tab == article.TabMissing,
Status: command.Status,
AuthorID: command.Author,
Q: command.Q,
Sort: command.Sort,
Page: command.Page,
Size: command.Size,
}
}
func (u *ListArticlesUseCase) summarizeVisibleVersions(visibility articleVisibility, owners *siteOwners) (map[uint32]article.SEOSummary, []article.Article, error) {
found, err := u.articleRepository.FindPage(visibility.apply(article.Filters{Unpaged: true}))
if err != nil {
return nil, nil, err
}
summaries, err := u.summarize(found.Items, owners)
return summaries, found.Items, err
}
func (u *ListArticlesUseCase) versionsWithSEOStatus(command ListStoriesCommand, filters article.StoryFilters, summaries map[uint32]article.SEOSummary) ([]uint32, error) {
narrowed := article.Filters{VisibleTo: filters.VisibleTo, SiteID: filters.SiteID, AuthorID: filters.AuthorID, Q: filters.Q, Unpaged: true}
if filters.OnlyOnline {
online := true
narrowed.IsOnline = &online
}
found, err := u.articleRepository.FindPage(narrowed)
if err != nil {
return nil, err
}
allowed := []uint32{}
for _, version := range found.Items {
if summaries[version.ID].Status == command.SEO {
allowed = append(allowed, version.ID)
}
}
return allowed, nil
}
func countSuggestedStories(versions []article.Article, summaries map[uint32]article.SEOSummary) int {
stories := map[uint32]bool{}
standalone := 0
for _, version := range versions {
if summaries[version.ID].Status != article.SEOStatusSuggestions {
continue
}
if version.StoryID == 0 {
standalone++
continue
}
stories[version.StoryID] = true
}
return len(stories) + standalone
}
func (u *ListArticlesUseCase) storyRows(command ListStoriesCommand, refs []article.StoryRef, creatable bool, owners *siteOwners) ([]StoryRow, error) {
storyIDs := make([]uint32, 0, len(refs))
for _, ref := range refs {
storyIDs = append(storyIDs, ref.StoryID)
}
loaded, err := u.articleRepository.ListByStories(storyIDs)
if err != nil {
return nil, err
}
byStory := map[uint32][]article.Article{}
for _, version := range command.Caller.readableVersions(loaded) {
byStory[version.StoryID] = append(byStory[version.StoryID], version)
}
siteRows := map[uint32][]localization.Row{}
rows := make([]StoryRow, 0, len(refs))
representatives := make([]article.Article, 0, len(refs))
for _, ref := range refs {
versions := byStory[ref.StoryID]
representative, found := versionByID(versions, ref.ArticleID)
if !found {
continue
}
languages, cached := siteRows[representative.SiteID]
if !cached {
if languages, err = u.localization.Rows(representative.SiteID); err != nil {
return nil, err
}
siteRows[representative.SiteID] = languages
}
rows = append(rows, buildStoryRow(command.Caller, representative, versions, languages, creatable))
representatives = append(representatives, representative)
}
summaries, err := u.summarize(representatives, owners)
if err != nil {
return nil, err
}
rendered, err := renderArticles(u.bodyRenderer, representatives, command.Format)
if err != nil {
return nil, err
}
for index := range rows {
rows[index].Article = rendered[index]
rows[index].SEO = summaries[rendered[index].ID]
}
return rows, nil
}
func versionByID(versions []article.Article, id uint32) (article.Article, bool) {
index := slices.IndexFunc(versions, func(version article.Article) bool { return version.ID == id })
if index < 0 {
return article.Article{}, false
}
return versions[index], true
}
func buildStoryRow(caller ContentCaller, representative article.Article, versions []article.Article, languages []localization.Row, creatable bool) StoryRow {
ordered := sortByLanguage(versions, languages)
coverage := make([]article.Translation, 0, len(ordered))
updatedAt := time.Time{}
authors := []author.Byline{}
seen := map[uint32]bool{}
for _, version := range ordered {
coverage = append(coverage, article.Translation{
ID: version.ID,
Lang: version.Lang,
Title: version.Title,
IsOnline: version.IsOnline,
LanguageEnabled: version.Lang == language.Unassigned || localization.EnabledTag(languages, version.Lang),
Capabilities: caller.capabilitiesFor(version),
})
if version.UpdatedAt.After(updatedAt) {
updatedAt = version.UpdatedAt
}
for _, byline := range version.Authors {
if !seen[byline.ID] {
seen[byline.ID] = true
authors = append(authors, byline)
}
}
}
return StoryRow{
UpdatedAt: updatedAt,
Authors: authors,
Coverage: coverage,
MissingLangs: missingLanguages(ordered, languages, creatable),
Capabilities: caller.capabilitiesFor(representative),
}
}
func missingLanguages(versions []article.Article, languages []localization.Row, creatable bool) []string {
missing := []string{}
if !creatable || slices.ContainsFunc(versions, func(version article.Article) bool { return version.Lang == language.Unassigned }) {
return missing
}
present := languagesOf(versions)
for _, row := range languages {
if row.Enabled && !slices.Contains(present, row.Tag) {
missing = append(missing, row.Tag)
}
}
defaultTag := localization.DefaultTag(languages)
slices.SortFunc(missing, func(left string, right string) int { return localization.CompareTags(defaultTag, left, right) })
return missing
}
@@ -3,6 +3,7 @@ package useCases
import (
"RenewCMS/internal/domain/article"
"RenewCMS/internal/domain/builtintype"
"RenewCMS/internal/domain/localization"
"RenewCMS/internal/domain/role"
"RenewCMS/internal/domain/site"
"errors"
@@ -12,10 +13,11 @@ type ListArticlesUseCase struct {
articleRepository article.Repository
bodyRenderer article.BodyRenderer
sites site.Repository
localization localization.Repository
}
func NewListArticlesUseCase(articleRepository article.Repository, bodyRenderer article.BodyRenderer, sites site.Repository) *ListArticlesUseCase {
return &ListArticlesUseCase{articleRepository: articleRepository, bodyRenderer: bodyRenderer, sites: sites}
func NewListArticlesUseCase(articleRepository article.Repository, bodyRenderer article.BodyRenderer, sites site.Repository, localizationRepository localization.Repository) *ListArticlesUseCase {
return &ListArticlesUseCase{articleRepository: articleRepository, bodyRenderer: bodyRenderer, sites: sites, localization: localizationRepository}
}
type ListArticlesCommand struct {
@@ -28,6 +30,8 @@ type ListArticlesCommand struct {
Sort article.Sort
SiteKey *string
SEO string
Lang *string
Author *uint32
}
type ArticlePage struct {
@@ -40,13 +44,17 @@ type ArticlePage struct {
}
func (u *ListArticlesUseCase) ListArticlesPage(command ListArticlesCommand) (ArticlePage, error) {
switch command.Caller.Grants.ScopeFor(role.KeyContentRead, builtintype.KeyArticle) {
return u.listVisibleByRepository(command, visibilityFor(command.Caller))
}
func visibilityFor(caller ContentCaller) articleVisibility {
switch caller.Grants.ScopeFor(role.KeyContentRead, builtintype.KeyArticle) {
case role.ScopeAll:
return u.listVisibleByRepository(command, articleVisibility{})
return articleVisibility{}
case role.ScopeOwn:
return u.listVisibleByRepository(command, articleVisibility{userID: &command.Caller.UserID})
return articleVisibility{userID: &caller.UserID}
default:
return u.listVisibleByRepository(command, articleVisibility{onlineOnly: true})
return articleVisibility{onlineOnly: true}
}
}
@@ -64,6 +72,13 @@ func (v articleVisibility) apply(filters article.Filters) article.Filters {
return filters
}
func (u *ListArticlesUseCase) countFlat(command ListArticlesCommand, visibility articleVisibility) (article.Counts, error) {
if command.Lang != nil {
return u.countVisibleVersions(command, visibility)
}
return u.countVisible(visibility)
}
func (u *ListArticlesUseCase) countVisible(visibility articleVisibility) (article.Counts, error) {
if visibility.userID != nil {
return u.articleRepository.CountVisibleTo(*visibility.userID)
@@ -73,7 +88,7 @@ func (u *ListArticlesUseCase) countVisible(visibility articleVisibility) (articl
func (u *ListArticlesUseCase) listVisibleByRepository(command ListArticlesCommand, visibility articleVisibility) (ArticlePage, error) {
onlineOnly := visibility.onlineOnly
counts, err := u.countVisible(visibility)
counts, err := u.countFlat(command, visibility)
if err != nil {
return ArticlePage{}, err
}
@@ -149,7 +164,12 @@ func (u *ListArticlesUseCase) summarizeVisible(visibility articleVisibility) (ma
if err != nil {
return nil, err
}
return u.summarize(found.Items)
return u.summarize(found.Items, &siteOwners{repository: u.sites})
}
func (u *ListArticlesUseCase) countVisibleVersions(command ListArticlesCommand, visibility articleVisibility) (article.Counts, error) {
filters := visibility.apply(article.Filters{Lang: command.Lang})
return u.articleRepository.CountVersions(filters)
}
func countSuggestions(summaries map[uint32]article.SEOSummary) int {
@@ -167,19 +187,35 @@ func pageBySEOStatus(items []article.Article, summaries map[uint32]article.SEOSu
return pageSlice(matching, command.Page, command.Size), len(matching)
}
func (u *ListArticlesUseCase) summarize(items []article.Article) (map[uint32]article.SEOSummary, error) {
type siteOwners struct {
repository site.Repository
byID map[uint32]site.Site
}
func (o *siteOwners) get() (map[uint32]site.Site, error) {
if o.byID != nil {
return o.byID, nil
}
known, err := o.repository.List()
if err != nil {
return nil, err
}
o.byID = make(map[uint32]site.Site, len(known))
for _, owner := range known {
o.byID[owner.ID] = owner
}
return o.byID, nil
}
func (u *ListArticlesUseCase) summarize(items []article.Article, lookup *siteOwners) (map[uint32]article.SEOSummary, error) {
summaries := make(map[uint32]article.SEOSummary, len(items))
if len(items) == 0 {
return summaries, nil
}
known, err := u.sites.List()
owners, err := lookup.get()
if err != nil {
return nil, err
}
owners := make(map[uint32]site.Site, len(known))
for _, owner := range known {
owners[owner.ID] = owner
}
for _, item := range items {
summaries[item.ID] = assessSEO(item, owners[item.SiteID]).Summary()
}
@@ -198,5 +234,5 @@ func (u *ListArticlesUseCase) siteIDOf(key string) (uint32, bool, error) {
}
func pageFilters(command ListArticlesCommand) article.Filters {
return article.Filters{Page: command.Page, Size: command.Size, Q: command.Q, Status: command.Status, Sort: command.Sort}
return article.Filters{Page: command.Page, Size: command.Size, Q: command.Q, Status: command.Status, Sort: command.Sort, Lang: command.Lang, AuthorID: command.Author}
}