package admin import ( "context" "errors" "strconv" "git.dmitriygnatenko.ru/dima/go-common/db" "git.dmitriygnatenko.ru/dima/go-common/logger" validation "github.com/go-ozzo/ozzo-validation/v4" "github.com/gofiber/fiber/v2" "git.dmitriygnatenko.ru/dima/dmitriygnatenko-v2/internal/dto" customErrors "git.dmitriygnatenko.ru/dima/dmitriygnatenko-v2/internal/helper/errors" "git.dmitriygnatenko.ru/dima/dmitriygnatenko-v2/internal/helper/request" "git.dmitriygnatenko.ru/dima/dmitriygnatenko-v2/internal/mapper" "git.dmitriygnatenko.ru/dima/dmitriygnatenko-v2/internal/model" "git.dmitriygnatenko.ru/dima/dmitriygnatenko-v2/internal/service/handler" "git.dmitriygnatenko.ru/dima/dmitriygnatenko-v2/internal/service/i18n" ) type editArticlePage struct { tm TransactionManager articleRepository ArticleRepository tagRepository TagRepository articleTagRepository ArticleTagRepository languageRepository LanguageRepository cacheService CacheService } func NewEditArticlePageHandler( tm TransactionManager, articleRepository ArticleRepository, tagRepository TagRepository, articleTagRepository ArticleTagRepository, languageRepository LanguageRepository, cacheService CacheService, ) fiber.Handler { h := editArticlePage{ tm: tm, articleRepository: articleRepository, tagRepository: tagRepository, articleTagRepository: articleTagRepository, languageRepository: languageRepository, cacheService: cacheService, } return h.handler() } func (h editArticlePage) handler() fiber.Handler { return func(fctx *fiber.Ctx) error { ctx := fctx.Context() lang := i18n.LanguageFromContext(fctx) validationErrors := make(map[string]string) articleID, err := request.ConvertToUint64(fctx, "id") if err != nil { logger.Error(ctx, customErrors.Wrap(err, "edit article page: can't get id from request").Error()) return err } tags, err := h.getTags(ctx) if err != nil { logger.Error(ctx, customErrors.Wrap(err, "edit article page: can't get tags").Error()) return err } languages, err := h.getLanguages(ctx) if err != nil { logger.Error(ctx, customErrors.Wrap(err, "edit article page: can't get languages").Error()) return err } article, err := h.getArticle(ctx, articleID) if err != nil { return err // logging in the getArticle method } articleTags, err := h.tagRepository.GetByArticleID(ctx, articleID) if err != nil { logger.Error(ctx, customErrors.Wrap(err, "edit article page: can't get article tags").Error()) return err } var form dto.ArticleForm if fctx.Method() == fiber.MethodGet { form = mapper.ToArticleForm(*article, articleTags) } else if fctx.Method() == fiber.MethodPost { form = dto.ArticleForm{ ID: articleID, ActiveTags: make(map[uint64]bool), } if err = fctx.BodyParser(&form); err != nil { logger.Info(ctx, customErrors.Wrap(err, "edit article page: can't parse body").Error()) return err } if err = h.validateArticleForm(ctx, form, *article); err != nil { logger.Info(ctx, customErrors.Wrap(err, "edit article page: validation").Error()) validationErrors = mapper.ToErrorsMap(err) } tagIDs, err := h.formatTags(form.Tags) if err != nil { logger.Error(ctx, customErrors.Wrap(err, "edit article page: can't format tags").Error()) return err } for i := range tagIDs { form.ActiveTags[tagIDs[i]] = true } if len(validationErrors) == 0 { articleModel, err := mapper.ToArticleModel(form) if err != nil { logger.Error( ctx, customErrors.Wrap(err, "edit article page: can't create article model").Error(), ) return err } var tagsToAdd, tagsToDelete []uint64 oldTagsMap := make(map[int]struct{}, len(articleTags)) for i := range articleTags { oldTagsMap[int(articleTags[i].ID)] = struct{}{} if _, ok := form.ActiveTags[articleTags[i].ID]; !ok { tagsToDelete = append(tagsToDelete, articleTags[i].ID) } } for i := range tagIDs { if _, ok := oldTagsMap[int(tagIDs[i])]; !ok { tagsToAdd = append(tagsToAdd, tagIDs[i]) } } err = h.tm.ReadCommitted(ctx, func(ctx context.Context) error { txErr := h.articleRepository.Update(ctx, *articleModel) if txErr != nil { return customErrors.Wrap(txErr, "can't update article") } if len(tagsToAdd) > 0 { if txErr = h.articleTagRepository.Add(ctx, articleID, tagsToAdd); txErr != nil { return customErrors.Wrap(txErr, "can't add tags") } } if len(tagsToDelete) > 0 { if txErr = h.articleTagRepository.Delete(ctx, articleID, tagsToDelete); txErr != nil { return customErrors.Wrap(txErr, "can't delete tags") } } return nil }) if err != nil { logger.Error(ctx, customErrors.Wrap(err, "edit article page").Error()) return err } h.cacheService.Delete(handler.ArticleCacheKey + form.URL + form.Language) h.cacheService.Delete(handler.RecentArticlesCacheKey + form.Language) h.cacheService.Delete(handler.UsedTagsCacheKey + form.Language) h.cacheService.Delete(handler.AllPreviewArticlesCacheKey + form.Language) if fctx.FormValue("action", "save") == "save" { return fctx.Redirect("/admin") } } } return fctx.Render("admin/article_edit", fiber.Map{ "lang": lang, "title": i18n.T(lang, "admin_article_edit_title"), "section": "article", "show_apply": true, "form": form, "errors": validationErrors, "tags": tags, "languages": languages, }, "admin/_layout") } } func (h editArticlePage) getArticle(ctx context.Context, id uint64) (*model.Article, error) { article, err := h.articleRepository.GetByID(ctx, id) if err != nil { if db.IsNotFoundError(err) { return nil, fiber.ErrNotFound } logger.Error(ctx, customErrors.Wrap(err, "edit article page: can't get article").Error()) return nil, err } return article, nil } func (h editArticlePage) getTags(ctx context.Context) ([]dto.Tag, error) { tagModels, err := h.tagRepository.GetAll(ctx) if err != nil { return nil, err } return mapper.ToTagsList(tagModels), nil } func (h editArticlePage) getLanguages(ctx context.Context) ([]string, error) { langModels, err := h.languageRepository.GetAll(ctx) if err != nil { return nil, err } res := make([]string, 0, len(langModels)) for i := range langModels { res = append(res, langModels[i].URL) } return res, nil } func (h editArticlePage) formatTags(tags []string) ([]uint64, error) { ids := make([]uint64, 0, len(tags)) for i := range tags { id, err := strconv.ParseUint(tags[i], 10, 64) if err != nil { return nil, err } ids = append(ids, id) } return ids, nil } func (h editArticlePage) validateArticleForm( ctx context.Context, req dto.ArticleForm, article model.Article, ) error { lang := i18n.DefaultLanguage() requiredMsg := i18n.T(lang, "admin_err_required") lengthMsg := i18n.T(lang, "admin_err_length_255") return validation.ValidateStructWithContext( ctx, &req, validation.Field( &req.Title, validation.Required.Error(requiredMsg), validation.Length(1, 255).Error(lengthMsg), ), validation.Field( &req.Image, validation.Required.Error(requiredMsg), validation.Length(1, 255).Error(lengthMsg), ), validation.Field( &req.URL, validation.Required.Error(requiredMsg), validation.Length(1, 255).Error(lengthMsg), validation.By(h.validateDuplicateURL(article)), ), validation.Field( &req.Text, validation.Required.Error(requiredMsg), ), validation.Field( &req.MetaKeywords, validation.Length(1, 255).Error(lengthMsg), ), validation.Field( &req.MetaDescription, validation.Length(1, 255).Error(lengthMsg), ), validation.Field( &req.PublishTime, validation.Required.Error(requiredMsg), ), validation.Field( &req.Language, validation.Required.Error(requiredMsg), ), ) } func (h editArticlePage) validateDuplicateURL( article model.Article, ) func(value interface{}) error { return func(value interface{}) error { url, _ := value.(string) if url == article.URL { return nil } ctx := context.Background() _, err := h.articleRepository.GetByURL(ctx, url, article.Language) if err != nil { if !db.IsNotFoundError(err) { logger.Error(ctx, err.Error()) } return nil } return errors.New(i18n.T(i18n.DefaultLanguage(), "admin_err_article_duplicate", url)) } }