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

+1 -1
View File
@@ -397,7 +397,7 @@ func getUseCases(repos *Repositories, imageStorage domainImage.Storage, settings
Token: authtoken.New([]byte(settings.JWTSecret), authtoken.WithRevocationStore(sessionRevocation)),
CreateArticleUseCase: useCases.NewCreateArticleUseCase(repos.Transactor, renderer, recorder, repos.SiteRepo, repos.AuthorRepo),
GetArticleUseCase: useCases.NewGetArticleUseCase(repos.ArticleRepo, renderer),
ListArticlesUseCase: useCases.NewListArticlesUseCase(repos.ArticleRepo, renderer, repos.SiteRepo),
ListArticlesUseCase: useCases.NewListArticlesUseCase(repos.ArticleRepo, renderer, repos.SiteRepo, repos.LocalizationRepo),
UpdateArticleUseCase: useCases.NewUpdateArticleUseCase(repos.ArticleRepo, renderer, recorder, repos.SiteRepo, repos.AuthorRepo, repos.ImageRepo, repos.LocalizationRepo, moveStory),
DeleteArticleUseCase: useCases.NewDeleteArticleUseCase(repos.ArticleRepo, recorder),
CreateTranslationUseCase: useCases.NewCreateTranslationUseCase(repos.Transactor, repos.ArticleRepo, repos.LocalizationRepo, renderer, recorder),
+116 -5
View File
@@ -7,6 +7,7 @@ import (
validator "RenewCMS/api/pkg/validator"
domain "RenewCMS/internal/domain/article"
"RenewCMS/internal/domain/author"
"RenewCMS/internal/domain/language"
"RenewCMS/internal/infrastructure/useCases"
"encoding/json"
"errors"
@@ -559,6 +560,29 @@ type listQuery struct {
Site *string `validate:"omitempty,min=1"`
SEO string `validate:"omitempty,oneof=ready suggestions hidden"`
Sort string `validate:"oneof=title:asc title:desc status:asc status:desc created_at:asc created_at:desc updated_at:asc updated_at:desc"`
Group *string `validate:"required_if=Tab missing,omitempty,eq=story"`
Tab *string `validate:"omitempty,oneof=all missing"`
Lang *string `validate:"omitempty,min=1"`
Author *uint32 `validate:"omitempty,gte=1"`
}
const groupByStory = "story"
type storyItemResponse struct {
listItemResponse
StoryUpdatedAt time.Time `json:"story_updated_at"`
StoryAuthors []author.Byline `json:"story_authors"`
Coverage []domain.Translation `json:"coverage"`
MissingLangs []string `json:"missing_langs"`
Capabilities []string `json:"capabilities"`
}
type storyListResponse struct {
Items []storyItemResponse `json:"items"`
Total int `json:"total"`
Page int `json:"page"`
Size int `json:"size"`
Counts domain.StoryCounts `json:"counts"`
}
const defaultListSort = "updated_at:desc"
@@ -612,7 +636,38 @@ func readListQuery(values url.Values) (listQuery, error) {
if values.Has("seo") && seoStatus == "" {
return listQuery{}, errors.New("empty seo")
}
return listQuery{Page: page, Size: size, Q: values.Get("q"), Status: status, Sort: sortValue, Site: siteKey, SEO: seoStatus}, nil
query := listQuery{Page: page, Size: size, Q: values.Get("q"), Status: status, Sort: sortValue, Site: siteKey, SEO: seoStatus}
if err := readLocalizedQuery(values, &query); err != nil {
return listQuery{}, err
}
return query, nil
}
func readLocalizedQuery(values url.Values, query *listQuery) error {
if values.Has("group") {
group := values.Get("group")
query.Group = &group
}
if values.Has("tab") {
tab := values.Get("tab")
query.Tab = &tab
}
if values.Has("lang") {
tag, err := language.Normalize(values.Get("lang"))
if err != nil {
return err
}
query.Lang = &tag
}
if values.Has("author") {
id, err := strconv.ParseUint(values.Get("author"), 10, 32)
if err != nil {
return err
}
authorID := uint32(id)
query.Author = &authorID
}
return nil
}
func (h *Handler) ListArticles(w http.ResponseWriter, r *http.Request) {
@@ -622,14 +677,21 @@ func (h *Handler) ListArticles(w http.ResponseWriter, r *http.Request) {
}
query, err := readListQuery(r.URL.Query())
if err != nil || validator.Validate.Struct(query) != nil {
if err != nil || validator.Validate.Struct(query) != nil || query.Group != nil && access.CallerOf(r).UserID == 0 {
http.Error(w, apperr.BodyErrorMessage, http.StatusBadRequest)
return
}
if stringOrEmpty(query.Group) == groupByStory {
h.listStories(w, r, format, query)
return
}
page, err := h.listUseCase.ListArticlesPage(useCases.ListArticlesCommand{
Format: format,
Caller: access.CallerOf(r),
Lang: query.Lang,
Author: query.Author,
Page: query.Page,
Size: query.Size,
Q: query.Q,
@@ -653,17 +715,66 @@ func (h *Handler) ListArticles(w http.ResponseWriter, r *http.Request) {
responses = append(responses, response)
}
articlesJson, err := json.Marshal(listResponse{Items: responses, Total: page.Total, Page: page.Page, Size: page.Size, Counts: page.Counts})
writeListJSON(w, listResponse{Items: responses, Total: page.Total, Page: page.Page, Size: page.Size, Counts: page.Counts})
}
func writeListJSON(w http.ResponseWriter, payload any) {
body, err := json.Marshal(payload)
if err != nil {
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
return
}
if _, err := w.Write(articlesJson); err != nil {
if _, err := w.Write(body); err != nil {
log.Printf("Failed to write response: %v", err)
}
}
func (h *Handler) listStories(w http.ResponseWriter, r *http.Request, format domain.BodyFormat, query listQuery) {
caller := access.CallerOf(r)
page, err := h.listUseCase.ListStoriesPage(useCases.ListStoriesCommand{
Format: format,
Caller: caller,
Page: query.Page,
Size: query.Size,
Q: query.Q,
Status: query.Status,
Sort: parseSort(query.Sort),
SiteKey: query.Site,
SEO: query.SEO,
Lang: stringOrEmpty(query.Lang),
Tab: stringOrEmpty(query.Tab),
Author: query.Author,
})
if err != nil {
http.Error(w, "Failed to retrieve articles", http.StatusInternalServerError)
return
}
responses := make([]storyItemResponse, 0, len(page.Rows))
for _, row := range page.Rows {
item, err := toListItemResponse(row.Article, row.SEO, format, caller)
if err != nil {
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
return
}
responses = append(responses, storyItemResponse{
listItemResponse: item,
StoryUpdatedAt: row.UpdatedAt,
StoryAuthors: row.Authors,
Coverage: row.Coverage,
MissingLangs: row.MissingLangs,
Capabilities: row.Capabilities,
})
}
writeListJSON(w, storyListResponse{Items: responses, Total: page.Total, Page: page.Page, Size: page.Size, Counts: page.Counts})
}
func stringOrEmpty(value *string) string {
if value == nil {
return ""
}
return *value
}
func (h *Handler) DeleteArticle(w http.ResponseWriter, r *http.Request) {
id, err := parseArticleId(r)
if err != nil {