feat: editorial article list
This commit is contained in:
1 parent
a0b3c58c52
commit
a671b81f80
103 files changed
+6493
-1805
No files matched your search
@@ -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),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user