浏览代码

Update admin article page

Dima 2 年之前
父节点
当前提交
715b757815

+ 12 - 6
internal/app/app.go

@@ -19,12 +19,13 @@ import (
 const webDir = "../../web"
 
 type App struct {
-	db                *sql.DB
-	env               interfaces.IEnv
-	cache             interfaces.ICache
-	mailer            interfaces.IMailer
-	articleRepository interfaces.IArticleRepository
-	tagRepository     interfaces.ITagRepository
+	db                   *sql.DB
+	env                  interfaces.IEnv
+	cache                interfaces.ICache
+	mailer               interfaces.IMailer
+	articleRepository    interfaces.IArticleRepository
+	tagRepository        interfaces.ITagRepository
+	articleTagRepository interfaces.IArticleTagRepository
 
 	fiber *fiber.App
 }
@@ -49,6 +50,7 @@ func Init() (*App, error) {
 	// Init repositories
 	app.articleRepository = repositories.InitArticleRepository(app.GetDBService())
 	app.tagRepository = repositories.InitTagRepository(app.GetDBService())
+	app.articleTagRepository = repositories.InitArticleTagRepository(app.GetDBService())
 
 	// Init cache service
 	app.cache = cacheService.Init()
@@ -102,6 +104,10 @@ func (a *App) GetTagRepository() interfaces.ITagRepository {
 	return a.tagRepository
 }
 
+func (a *App) GetArticleTagRepository() interfaces.IArticleTagRepository {
+	return a.articleTagRepository
+}
+
 func (a *App) GetFiber() *fiber.App {
 	return a.fiber
 }

+ 1 - 0
internal/interfaces/app.go

@@ -13,5 +13,6 @@ type IApp interface {
 	GetMailerService() IMailer
 	GetArticleRepository() IArticleRepository
 	GetTagRepository() ITagRepository
+	GetArticleTagRepository() IArticleTagRepository
 	GetFiber() *fiber.App
 }

+ 6 - 0
internal/interfaces/repository.go

@@ -12,6 +12,7 @@ type IArticleRepository interface {
 	GetPreviewByTagID(ctx context.Context, tagID int) ([]models.ArticlePreview, error)
 	GetByURL(ctx context.Context, url string) (*models.Article, error)
 	GetByID(ctx context.Context, ID int) (*models.Article, error)
+	Add(ctx context.Context, m models.Article) (int, error)
 }
 
 type ITagRepository interface {
@@ -25,3 +26,8 @@ type ITagRepository interface {
 	Update(ctx context.Context, m models.Tag) error
 	Delete(ctx context.Context, ID int) error
 }
+
+type IArticleTagRepository interface {
+	Add(ctx context.Context, articleID int, tagIDs []int) error
+	Delete(ctx context.Context, articleID int, tagIDs []int) error
+}

+ 48 - 1
internal/mapper/article.go

@@ -1,6 +1,7 @@
 package mapper
 
 import (
+	"database/sql"
 	"github.com/dmitriygnatenko/internal/dto"
 	"github.com/dmitriygnatenko/internal/helpers"
 	"github.com/dmitriygnatenko/internal/models"
@@ -92,6 +93,52 @@ func ConvertArticleModelToForm(a models.Article, tags []models.Tag) (*models.Art
 		MetaKeywords:    a.MetaKeywords.String,
 		MetaDescription: a.MetaDescription.String,
 		PublishTime:     helpers.FormatDateForm(parsedDateTime),
-		Tags:            tagMap,
+		ActiveTags:      tagMap,
 	}, nil
 }
+
+func ConvertArticleFormToModel(f models.ArticleForm) models.Article {
+	var previewText, image, metaKeywords, metaDesc sql.NullString
+
+	if f.PreviewText != "" {
+		previewText = sql.NullString{
+			String: f.PreviewText,
+			Valid:  true,
+		}
+	}
+
+	if f.Image != "" {
+		image = sql.NullString{
+			String: f.Image,
+			Valid:  true,
+		}
+	}
+
+	if f.MetaKeywords != "" {
+		metaKeywords = sql.NullString{
+			String: f.MetaKeywords,
+			Valid:  true,
+		}
+	}
+
+	if f.MetaDescription != "" {
+		metaDesc = sql.NullString{
+			String: f.MetaDescription,
+			Valid:  true,
+		}
+	}
+
+	return models.Article{
+		ID:              0,
+		URL:             f.URL,
+		Title:           f.Title,
+		PublishTime:     f.PublishTime,
+		Text:            f.Text,
+		PreviewText:     previewText,
+		IsActive:        f.IsActive,
+		Image:           image,
+		MetaKeywords:    metaKeywords,
+		MetaDescription: metaDesc,
+	}
+
+}

+ 11 - 10
internal/models/article.go

@@ -26,14 +26,15 @@ type Article struct {
 
 type ArticleForm struct {
 	ID              int
-	Title           string `form:"title" validate:"required,max=255"`
-	Image           string `form:"image" validate:"uri,max=255"`
-	URL             string `form:"url" validate:"required,max=255"`
-	Text            string `form:"text" validate:"required"`
-	PreviewText     string `form:"preview_text" validate:"max=255"`
-	MetaKeywords    string `form:"meta_keywords" validate:"max=255"`
-	MetaDescription string `form:"meta_description" validate:"max=255"`
-	IsActive        bool   `form:"is_active" validate:"required"`
-	PublishTime     string `form:"publish_time" validate:"required"`
-	Tags            map[int]bool
+	Title           string   `form:"title" validate:"required,max=255"`
+	Image           string   `form:"image" validate:"uri,max=255"`
+	URL             string   `form:"url" validate:"required,max=255"`
+	Text            string   `form:"text" validate:"required"`
+	PreviewText     string   `form:"preview_text" validate:"max=255"`
+	MetaKeywords    string   `form:"meta_keywords" validate:"max=255"`
+	MetaDescription string   `form:"meta_description" validate:"max=255"`
+	IsActive        bool     `form:"is_active" validate:"boolean"`
+	PublishTime     string   `form:"publish_time" validate:"required"`
+	Tags            []string `form:"tag"`
+	ActiveTags      map[int]bool
 }

+ 18 - 0
internal/repositories/article.go

@@ -158,3 +158,21 @@ func (a articleRepository) GetByID(ctx context.Context, id int) (*models.Article
 
 	return &res, nil
 }
+
+func (a articleRepository) Add(ctx context.Context, m models.Article) (int, error) {
+	query := "INSERT INTO " + articleTableName + " " +
+		"(url, publish_time, title, image, text, preview_text, meta_keywords, meta_desc, is_active) " +
+		"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"
+
+	res, err := a.db.ExecContext(ctx, query, m.URL, m.PublishTime, m.Title, m.Image, m.Text, m.PreviewText, m.MetaKeywords, m.MetaDescription, m.IsActive)
+	if err != nil {
+		return 0, err
+	}
+
+	id, err := res.LastInsertId()
+	if err != nil {
+		return 0, err
+	}
+
+	return int(id), err
+}

+ 39 - 0
internal/repositories/article_tag.go

@@ -0,0 +1,39 @@
+package repositories
+
+import (
+	"context"
+	"database/sql"
+
+	"github.com/dmitriygnatenko/internal/interfaces"
+)
+
+type articleTagRepository struct {
+	db *sql.DB
+}
+
+func InitArticleTagRepository(db *sql.DB) interfaces.IArticleTagRepository {
+	return articleTagRepository{db: db}
+}
+
+func (a articleTagRepository) Add(ctx context.Context, articleID int, tagIDs []int) error {
+	if len(tagIDs) == 0 {
+		return nil
+	}
+
+	query := "INSERT INTO article_tag (article_id, tag_id) VALUES "
+
+	var vals []interface{}
+	for _, tagID := range tagIDs {
+		query += "(?, ?),"
+		vals = append(vals, articleID, tagID)
+	}
+	query = query[0 : len(query)-1]
+
+	_, err := a.db.ExecContext(ctx, query, vals...)
+
+	return err
+}
+
+func (a articleTagRepository) Delete(ctx context.Context, articleID int, tagIDs []int) error {
+	return nil
+}

+ 81 - 1
internal/services/handler/admin/article.go

@@ -6,10 +6,13 @@ import (
 	"github.com/dmitriygnatenko/internal/helpers"
 	"github.com/dmitriygnatenko/internal/interfaces"
 	"github.com/dmitriygnatenko/internal/mapper"
+	"github.com/dmitriygnatenko/internal/models"
 	"github.com/go-playground/validator/v10"
 	"github.com/gofiber/fiber/v2"
 )
 
+const errArticleExists = "Статья с данным URL уже существует"
+
 func ArticleHandler(app interfaces.IApp) fiber.Handler {
 	return func(ctx *fiber.Ctx) error {
 		articles, err := app.GetArticleRepository().GetAll(ctx.Context())
@@ -31,7 +34,84 @@ func ArticleHandler(app interfaces.IApp) fiber.Handler {
 
 func AddArticleHandler(app interfaces.IApp) fiber.Handler {
 	return func(ctx *fiber.Ctx) error {
-		return nil
+		var validate = validator.New()
+		validateErrors := make(map[string]string)
+
+		trans, err := helpers.GetDefaultTranslator(validate)
+		if err != nil {
+			return err
+		}
+
+		form := models.ArticleForm{
+			ActiveTags: make(map[int]bool),
+		}
+
+		tags, err := app.GetTagRepository().GetAll(ctx.Context())
+		if err != nil {
+			return err
+		}
+
+		tagsDTO, err := mapper.ConvertTagModelsToDTO(tags)
+		if err != nil {
+			return err
+		}
+
+		if ctx.Method() == fiber.MethodPost {
+			if err = ctx.BodyParser(&form); err != nil {
+				return err
+			}
+
+			if err = validate.Struct(form); err != nil {
+				validateErrors = helpers.FormatValidateErrors(err, trans)
+			}
+
+			if res, _ := app.GetArticleRepository().GetByURL(ctx.Context(), form.URL); res != nil {
+				validateErrors["ArticleForm.URL"] = errArticleExists
+			}
+
+			tagIDs := make([]int, 0, len(form.Tags))
+			for i := range form.Tags {
+				tagID, tagErr := strconv.Atoi(form.Tags[i])
+				if tagErr != nil {
+					return tagErr
+				}
+
+				tagIDs = append(tagIDs, tagID)
+			}
+
+			if len(validateErrors) == 0 {
+				articleID, articleErr := app.GetArticleRepository().Add(ctx.Context(), mapper.ConvertArticleFormToModel(form))
+				if articleErr != nil {
+					return articleErr
+				}
+
+				if len(form.Tags) > 0 {
+
+					if err = app.GetArticleTagRepository().Add(ctx.Context(), articleID, tagIDs); err != nil {
+						return err
+					}
+				}
+
+				app.GetCacheService().FlushAll()
+
+				if err = ctx.Redirect("/admin"); err != nil {
+					return err
+				}
+			}
+
+			for i := range tagIDs {
+				form.ActiveTags[tagIDs[i]] = true
+			}
+		}
+
+		return ctx.Render("admin/article_edit", fiber.Map{
+			"form":       form,
+			"errors":     validateErrors,
+			"tags":       tagsDTO,
+			"section":    "article",
+			"title":      "Добавление статьи",
+			"show_apply": false,
+		}, "admin/_layout")
 	}
 }
 

+ 24 - 24
internal/templates/admin/article_edit.html

@@ -1,21 +1,23 @@
 {{ define "admin/custom_bottom_static" }}
 <script>
     tinymce.init({
-        selector: 'textarea#editor-text',
-        height: 800,
+        selector: 'textarea#editor-preview',
+        height: 300,
         language: 'ru',
         statusbar: false,
         promotion: false,
-        plugins: "code"
+        plugins: "code",
+        toolbar: 'code | bold italic underline forecolor | copy cut paste'
     });
-
     tinymce.init({
-        selector: 'textarea#editor-preview',
-        height: 200,
+        selector: 'textarea#editor-text',
+        height: 1000,
         language: 'ru',
+        menubar: false,
         statusbar: false,
         promotion: false,
-        plugins: "code"
+        plugins: 'code',
+        toolbar: 'code | bold italic underline forecolor | copy cut paste | alignleft aligncenter alignjustify alignright | h1 h2 h3 h4 h5 h6'
     });
 </script>
 {{ end }}
@@ -32,11 +34,6 @@
                 </div>
             </div>
         </div>
-
-
-
-
-
         <div class="row mb-3 mt-3">
             <label class="col-sm-2 col-form-label col-form-label-sm">Название</label>
             <div class="col-sm-10">
@@ -53,7 +50,7 @@
             <label class="col-sm-2 col-form-label col-form-label-sm">Картинка</label>
             <div class="col-sm-10">
                 {{ $imageErr := index .errors "ArticleForm.Image" }}
-                <input type="text" class="form-control form-control-sm {{ if $imageErr }}is-invalid{{ end }}" name="title" value="{{ .form.Image }}">
+                <input type="text" class="form-control form-control-sm {{ if $imageErr }}is-invalid{{ end }}" name="image" value="{{ .form.Image }}">
                 {{ if $imageErr }}
                 <div class="invalid-feedback">
                     {{ $imageErr }}
@@ -88,11 +85,11 @@
         <div class="row mb-3 mt-3">
             <label class="col-sm-2 col-form-label col-form-label-sm">Краткий текст</label>
             <div class="col-sm-10">
-                {{ $ptErr := index .errors "ArticleForm.PreviewText" }}
-                <textarea id="editor-preview" class="form-control form-control-sm {{ if $ptErr }}is-invalid{{ end }}" name="preview_text">{{ .form.PreviewText }}</textarea>
-                {{ if $ptErr }}
+                {{ $previewErr := index .errors "ArticleForm.PreviewText" }}
+                <textarea id="editor-preview" class="form-control form-control-sm {{ if $previewErr }}is-invalid{{ end }}" name="preview_text">{{ .form.PreviewText }}</textarea>
+                {{ if $previewErr }}
                 <div class="invalid-feedback">
-                    {{ $ptErr }}
+                    {{ $previewErr }}
                 </div>
                 {{ end }}
             </div>
@@ -100,11 +97,11 @@
         <div class="row mb-3 mt-3">
             <label class="col-sm-2 col-form-label col-form-label-sm">Текст</label>
             <div class="col-sm-10">
-                {{ $tErr := index .errors "ArticleForm.Text" }}
-                <textarea id="editor-text" class="form-control form-control-sm {{ if $tErr }}is-invalid{{ end }}" name="text">{{ .form.Text }}</textarea>
-                {{ if $tErr }}
+                {{ $textErr := index .errors "ArticleForm.Text" }}
+                <textarea id="editor-text" class="form-control form-control-sm {{ if $textErr }}is-invalid{{ end }}" name="text">{{ .form.Text }}</textarea>
+                {{ if $textErr }}
                 <div class="invalid-feedback">
-                    {{ $tErr }}
+                    {{ $textErr }}
                 </div>
                 {{ end }}
             </div>
@@ -136,10 +133,10 @@
         <div class="row mb-3 mt-3 border-bottom">
             <label class="col-sm-2 col-form-label col-form-label-sm">Теги</label>
             <div class="col-sm-10">
-                {{ $tags := .form.Tags }}
+                {{ $tags := .form.ActiveTags }}
                 {{ range .tags }}
                 <div class="form-check">
-                    <input class="form-check-input" type="checkbox" name="tag{{ .ID }}" {{ if index $tags .ID }}checked{{ end }}>
+                    <input class="form-check-input" type="checkbox" name="tag" value="{{ .ID }}" {{ if index $tags .ID }}checked{{ end }}>
                     <label class="form-check-label">
                         {{ .Tag }}
                     </label>
@@ -148,6 +145,9 @@
             </div>
         </div>
         <a href="/admin" class="btn btn-sm btn-secondary">Назад</a>
-        <button type="submit" class="btn btn-sm btn-primary">Сохранить</button>
+        {{ if .show_apply }}
+        <button type="submit" class="btn btn-sm btn-primary" name="apply">Применить</button>
+        {{ end }}
+        <button type="submit" class="btn btn-sm btn-primary" name="save">Сохранить</button>
     </form>
 </main>