first commit
This commit is contained in:
745
app/blogs/handlers/blog.go
Normal file
745
app/blogs/handlers/blog.go
Normal file
@@ -0,0 +1,745 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
blogModels "ginimageApi/app/blogs/models"
|
||||
"ginimageApi/configs"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type createPostRequest struct {
|
||||
Title string `json:"title" binding:"required,min=3"`
|
||||
Content string `json:"content"`
|
||||
Keywords string `json:"keywords"`
|
||||
Image string `json:"image"`
|
||||
Video string `json:"video"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
IsFront *bool `json:"is_front"`
|
||||
}
|
||||
|
||||
type updatePostRequest struct {
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
Keywords string `json:"keywords"`
|
||||
Image string `json:"image"`
|
||||
Video string `json:"video"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
IsFront *bool `json:"is_front"`
|
||||
}
|
||||
|
||||
type BlogErrorResponse struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
type BlogPostResponse struct {
|
||||
ID uint64 `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Slug string `json:"slug"`
|
||||
Content string `json:"content"`
|
||||
Keywords string `json:"keywords"`
|
||||
Image string `json:"image"`
|
||||
Video string `json:"video"`
|
||||
IsActive bool `json:"is_active"`
|
||||
IsFront bool `json:"is_front"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
|
||||
type BlogListResponse struct {
|
||||
Count int `json:"count"`
|
||||
Items []BlogPostResponse `json:"items"`
|
||||
}
|
||||
|
||||
type BlogCategoryResponse struct {
|
||||
ID uint64 `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Slug string `json:"slug"`
|
||||
Keywords string `json:"keywords"`
|
||||
Desc string `json:"description"`
|
||||
Image string `json:"image"`
|
||||
IsActive bool `json:"is_active"`
|
||||
Order int `json:"order"`
|
||||
}
|
||||
|
||||
type BlogTagResponse struct {
|
||||
ID uint64 `json:"id"`
|
||||
Tag string `json:"tag"`
|
||||
Slug string `json:"slug"`
|
||||
IsActive bool `json:"is_active"`
|
||||
}
|
||||
|
||||
type BlogCategoryListResponse struct {
|
||||
Count int `json:"count"`
|
||||
Items []BlogCategoryResponse `json:"items"`
|
||||
}
|
||||
|
||||
type BlogTagListResponse struct {
|
||||
Count int `json:"count"`
|
||||
Items []BlogTagResponse `json:"items"`
|
||||
}
|
||||
|
||||
type createCategoryRequest struct {
|
||||
Title string `json:"title" binding:"required,min=2"`
|
||||
Keywords string `json:"keywords"`
|
||||
Desc string `json:"description"`
|
||||
Image string `json:"image"`
|
||||
Order int `json:"order"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
ParentID *uint64 `json:"parent_id"`
|
||||
}
|
||||
|
||||
type updateCategoryRequest struct {
|
||||
Title string `json:"title"`
|
||||
Keywords string `json:"keywords"`
|
||||
Desc string `json:"description"`
|
||||
Image string `json:"image"`
|
||||
Order *int `json:"order"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
ParentID *uint64 `json:"parent_id"`
|
||||
}
|
||||
|
||||
type createTagRequest struct {
|
||||
Tag string `json:"tag" binding:"required,min=2"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
|
||||
type updateTagRequest struct {
|
||||
Tag string `json:"tag"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
|
||||
func toBlogPostResponse(p blogModels.Post) BlogPostResponse {
|
||||
return BlogPostResponse{
|
||||
ID: p.ID,
|
||||
Title: p.Title,
|
||||
Slug: p.Slug,
|
||||
Content: p.Content,
|
||||
Keywords: p.Keywords,
|
||||
Image: p.Image,
|
||||
Video: p.Video,
|
||||
IsActive: p.IsActive,
|
||||
IsFront: p.IsFront,
|
||||
CreatedAt: p.CreatedAt.Format(time.RFC3339),
|
||||
UpdatedAt: p.UpdatedAt.Format(time.RFC3339),
|
||||
}
|
||||
}
|
||||
|
||||
func toBlogCategoryResponse(c blogModels.Category) BlogCategoryResponse {
|
||||
return BlogCategoryResponse{
|
||||
ID: c.ID,
|
||||
Title: c.Title,
|
||||
Slug: c.Slug,
|
||||
Keywords: c.Keywords,
|
||||
Desc: c.Desc,
|
||||
Image: c.Image,
|
||||
IsActive: c.IsActive,
|
||||
Order: c.Order,
|
||||
}
|
||||
}
|
||||
|
||||
func toBlogTagResponse(t blogModels.Tag) BlogTagResponse {
|
||||
return BlogTagResponse{
|
||||
ID: t.ID,
|
||||
Tag: t.Tag,
|
||||
Slug: t.Slug,
|
||||
IsActive: t.IsActive,
|
||||
}
|
||||
}
|
||||
|
||||
func userIDFromContext(c *gin.Context) (uint64, bool) {
|
||||
v, ok := c.Get("user_id")
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
switch id := v.(type) {
|
||||
case uint:
|
||||
return uint64(id), true
|
||||
case int:
|
||||
if id < 0 {
|
||||
return 0, false
|
||||
}
|
||||
return uint64(id), true
|
||||
case string:
|
||||
parsed, err := strconv.ParseUint(id, 10, 64)
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
return parsed, true
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
|
||||
// ListPosts godoc
|
||||
// @Summary Public blog post listesini getirir
|
||||
// @Tags blogs
|
||||
// @Produce json
|
||||
// @Success 200 {object} BlogListResponse
|
||||
// @Failure 500 {object} BlogErrorResponse
|
||||
// @Router /api/v1/blogs [get]
|
||||
func ListPosts(c *gin.Context) {
|
||||
if configs.DB == nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "db baglantisi yok"})
|
||||
return
|
||||
}
|
||||
|
||||
var posts []blogModels.Post
|
||||
if err := configs.DB.Where("is_active = ?", true).Order("id desc").Find(&posts).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "postlar listelenemedi"})
|
||||
return
|
||||
}
|
||||
items := make([]BlogPostResponse, 0, len(posts))
|
||||
for _, p := range posts {
|
||||
items = append(items, toBlogPostResponse(p))
|
||||
}
|
||||
c.JSON(http.StatusOK, BlogListResponse{Count: len(items), Items: items})
|
||||
}
|
||||
|
||||
// GetPost godoc
|
||||
// @Summary Public tekil blog postu getirir
|
||||
// @Tags blogs
|
||||
// @Produce json
|
||||
// @Param slug path string true "Post slug"
|
||||
// @Success 200 {object} BlogPostResponse
|
||||
// @Failure 404 {object} BlogErrorResponse
|
||||
// @Failure 500 {object} BlogErrorResponse
|
||||
// @Router /api/v1/blogs/{slug} [get]
|
||||
func GetPost(c *gin.Context) {
|
||||
if configs.DB == nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "db baglantisi yok"})
|
||||
return
|
||||
}
|
||||
|
||||
slug := c.Param("slug")
|
||||
var post blogModels.Post
|
||||
err := configs.DB.Where("slug = ? AND is_active = ?", slug, true).First(&post).Error
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "post bulunamadi"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "post getirilemedi"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, toBlogPostResponse(post))
|
||||
}
|
||||
|
||||
// CreatePost godoc
|
||||
// @Summary Admin blog post olusturur
|
||||
// @Tags blogs
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param request body createPostRequest true "Post bilgileri"
|
||||
// @Success 201 {object} BlogPostResponse
|
||||
// @Failure 400 {object} BlogErrorResponse
|
||||
// @Failure 401 {object} BlogErrorResponse
|
||||
// @Failure 500 {object} BlogErrorResponse
|
||||
// @Router /api/v1/blogs [post]
|
||||
func CreatePost(c *gin.Context) {
|
||||
if configs.DB == nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "db baglantisi yok"})
|
||||
return
|
||||
}
|
||||
|
||||
userID, ok := userIDFromContext(c)
|
||||
if !ok {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "kullanici bulunamadi"})
|
||||
return
|
||||
}
|
||||
|
||||
var req createPostRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
post := blogModels.Post{
|
||||
Title: req.Title,
|
||||
Content: req.Content,
|
||||
Keywords: req.Keywords,
|
||||
Image: req.Image,
|
||||
Video: req.Video,
|
||||
UserID: &userID,
|
||||
IsActive: true,
|
||||
IsFront: true,
|
||||
}
|
||||
if req.IsActive != nil {
|
||||
post.IsActive = *req.IsActive
|
||||
}
|
||||
if req.IsFront != nil {
|
||||
post.IsFront = *req.IsFront
|
||||
}
|
||||
|
||||
if err := configs.DB.Create(&post).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "post olusturulamadi"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusCreated, toBlogPostResponse(post))
|
||||
}
|
||||
|
||||
// UpdatePost godoc
|
||||
// @Summary Admin blog post gunceller
|
||||
// @Tags blogs
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path int true "Post ID"
|
||||
// @Param request body updatePostRequest true "Guncellenecek alanlar"
|
||||
// @Success 200 {object} BlogPostResponse
|
||||
// @Failure 400 {object} BlogErrorResponse
|
||||
// @Failure 404 {object} BlogErrorResponse
|
||||
// @Failure 500 {object} BlogErrorResponse
|
||||
// @Router /api/v1/blogs/{id} [put]
|
||||
func UpdatePost(c *gin.Context) {
|
||||
if configs.DB == nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "db baglantisi yok"})
|
||||
return
|
||||
}
|
||||
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "gecersiz post id"})
|
||||
return
|
||||
}
|
||||
|
||||
var post blogModels.Post
|
||||
if err := configs.DB.First(&post, id).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "post bulunamadi"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "post bulunamadi"})
|
||||
return
|
||||
}
|
||||
|
||||
var req updatePostRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if req.Title != "" {
|
||||
post.Title = req.Title
|
||||
}
|
||||
if req.Content != "" {
|
||||
post.Content = req.Content
|
||||
}
|
||||
if req.Keywords != "" {
|
||||
post.Keywords = req.Keywords
|
||||
}
|
||||
if req.Image != "" {
|
||||
post.Image = req.Image
|
||||
}
|
||||
if req.Video != "" {
|
||||
post.Video = req.Video
|
||||
}
|
||||
if req.IsActive != nil {
|
||||
post.IsActive = *req.IsActive
|
||||
}
|
||||
if req.IsFront != nil {
|
||||
post.IsFront = *req.IsFront
|
||||
}
|
||||
|
||||
if err := configs.DB.Save(&post).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "post guncellenemedi"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, toBlogPostResponse(post))
|
||||
}
|
||||
|
||||
// DeletePost godoc
|
||||
// @Summary Admin blog post siler
|
||||
// @Tags blogs
|
||||
// @Security BearerAuth
|
||||
// @Param id path int true "Post ID"
|
||||
// @Success 204
|
||||
// @Failure 400 {object} BlogErrorResponse
|
||||
// @Failure 404 {object} BlogErrorResponse
|
||||
// @Failure 500 {object} BlogErrorResponse
|
||||
// @Router /api/v1/blogs/{id} [delete]
|
||||
func DeletePost(c *gin.Context) {
|
||||
if configs.DB == nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "db baglantisi yok"})
|
||||
return
|
||||
}
|
||||
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "gecersiz post id"})
|
||||
return
|
||||
}
|
||||
|
||||
res := configs.DB.Delete(&blogModels.Post{}, id)
|
||||
if res.Error != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "post silinemedi"})
|
||||
return
|
||||
}
|
||||
if res.RowsAffected == 0 {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "post bulunamadi"})
|
||||
return
|
||||
}
|
||||
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// ListCategories godoc
|
||||
// @Summary Public kategori listesini getirir
|
||||
// @Tags blogs
|
||||
// @Produce json
|
||||
// @Success 200 {object} BlogCategoryListResponse
|
||||
// @Failure 500 {object} BlogErrorResponse
|
||||
// @Router /api/v1/blogs/categories [get]
|
||||
func ListCategories(c *gin.Context) {
|
||||
if configs.DB == nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "db baglantisi yok"})
|
||||
return
|
||||
}
|
||||
var categories []blogModels.Category
|
||||
if err := configs.DB.Where("is_active = ?", true).Order("`order` asc, id desc").Find(&categories).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "kategoriler listelenemedi"})
|
||||
return
|
||||
}
|
||||
items := make([]BlogCategoryResponse, 0, len(categories))
|
||||
for _, item := range categories {
|
||||
items = append(items, toBlogCategoryResponse(item))
|
||||
}
|
||||
c.JSON(http.StatusOK, BlogCategoryListResponse{Count: len(items), Items: items})
|
||||
}
|
||||
|
||||
// GetCategory godoc
|
||||
// @Summary Public tekil kategori getirir
|
||||
// @Tags blogs
|
||||
// @Produce json
|
||||
// @Param slug path string true "Kategori slug"
|
||||
// @Success 200 {object} BlogCategoryResponse
|
||||
// @Failure 404 {object} BlogErrorResponse
|
||||
// @Failure 500 {object} BlogErrorResponse
|
||||
// @Router /api/v1/blogs/categories/{slug} [get]
|
||||
func GetCategory(c *gin.Context) {
|
||||
if configs.DB == nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "db baglantisi yok"})
|
||||
return
|
||||
}
|
||||
var category blogModels.Category
|
||||
err := configs.DB.Where("slug = ? AND is_active = ?", c.Param("slug"), true).First(&category).Error
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "kategori bulunamadi"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "kategori getirilemedi"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, toBlogCategoryResponse(category))
|
||||
}
|
||||
|
||||
// CreateCategory godoc
|
||||
// @Summary Admin kategori olusturur
|
||||
// @Tags blogs
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param request body createCategoryRequest true "Kategori bilgileri"
|
||||
// @Success 201 {object} BlogCategoryResponse
|
||||
// @Failure 400 {object} BlogErrorResponse
|
||||
// @Failure 500 {object} BlogErrorResponse
|
||||
// @Router /api/v1/blogs/categories [post]
|
||||
func CreateCategory(c *gin.Context) {
|
||||
if configs.DB == nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "db baglantisi yok"})
|
||||
return
|
||||
}
|
||||
var req createCategoryRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
category := blogModels.Category{
|
||||
Title: req.Title,
|
||||
Keywords: req.Keywords,
|
||||
Desc: req.Desc,
|
||||
Image: req.Image,
|
||||
Order: req.Order,
|
||||
ParentID: req.ParentID,
|
||||
IsActive: true,
|
||||
}
|
||||
if req.IsActive != nil {
|
||||
category.IsActive = *req.IsActive
|
||||
}
|
||||
if category.Order == 0 {
|
||||
category.Order = 1
|
||||
}
|
||||
if err := configs.DB.Create(&category).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "kategori olusturulamadi"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusCreated, toBlogCategoryResponse(category))
|
||||
}
|
||||
|
||||
// UpdateCategory godoc
|
||||
// @Summary Admin kategori gunceller
|
||||
// @Tags blogs
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path int true "Kategori ID"
|
||||
// @Param request body updateCategoryRequest true "Guncellenecek alanlar"
|
||||
// @Success 200 {object} BlogCategoryResponse
|
||||
// @Failure 400 {object} BlogErrorResponse
|
||||
// @Failure 404 {object} BlogErrorResponse
|
||||
// @Failure 500 {object} BlogErrorResponse
|
||||
// @Router /api/v1/blogs/categories/{id} [put]
|
||||
func UpdateCategory(c *gin.Context) {
|
||||
if configs.DB == nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "db baglantisi yok"})
|
||||
return
|
||||
}
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "gecersiz kategori id"})
|
||||
return
|
||||
}
|
||||
var category blogModels.Category
|
||||
if err := configs.DB.First(&category, id).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "kategori bulunamadi"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "kategori bulunamadi"})
|
||||
return
|
||||
}
|
||||
var req updateCategoryRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if req.Title != "" {
|
||||
category.Title = req.Title
|
||||
}
|
||||
if req.Keywords != "" {
|
||||
category.Keywords = req.Keywords
|
||||
}
|
||||
if req.Desc != "" {
|
||||
category.Desc = req.Desc
|
||||
}
|
||||
if req.Image != "" {
|
||||
category.Image = req.Image
|
||||
}
|
||||
if req.Order != nil {
|
||||
category.Order = *req.Order
|
||||
}
|
||||
if req.IsActive != nil {
|
||||
category.IsActive = *req.IsActive
|
||||
}
|
||||
if req.ParentID != nil {
|
||||
category.ParentID = req.ParentID
|
||||
}
|
||||
if err := configs.DB.Save(&category).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "kategori guncellenemedi"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, toBlogCategoryResponse(category))
|
||||
}
|
||||
|
||||
// DeleteCategory godoc
|
||||
// @Summary Admin kategori siler
|
||||
// @Tags blogs
|
||||
// @Security BearerAuth
|
||||
// @Param id path int true "Kategori ID"
|
||||
// @Success 204
|
||||
// @Failure 400 {object} BlogErrorResponse
|
||||
// @Failure 404 {object} BlogErrorResponse
|
||||
// @Failure 500 {object} BlogErrorResponse
|
||||
// @Router /api/v1/blogs/categories/{id} [delete]
|
||||
func DeleteCategory(c *gin.Context) {
|
||||
if configs.DB == nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "db baglantisi yok"})
|
||||
return
|
||||
}
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "gecersiz kategori id"})
|
||||
return
|
||||
}
|
||||
res := configs.DB.Delete(&blogModels.Category{}, id)
|
||||
if res.Error != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "kategori silinemedi"})
|
||||
return
|
||||
}
|
||||
if res.RowsAffected == 0 {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "kategori bulunamadi"})
|
||||
return
|
||||
}
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// ListTags godoc
|
||||
// @Summary Public tag listesini getirir
|
||||
// @Tags blogs
|
||||
// @Produce json
|
||||
// @Success 200 {object} BlogTagListResponse
|
||||
// @Failure 500 {object} BlogErrorResponse
|
||||
// @Router /api/v1/blogs/tags [get]
|
||||
func ListTags(c *gin.Context) {
|
||||
if configs.DB == nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "db baglantisi yok"})
|
||||
return
|
||||
}
|
||||
var tags []blogModels.Tag
|
||||
if err := configs.DB.Where("is_active = ?", true).Order("id desc").Find(&tags).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "tagler listelenemedi"})
|
||||
return
|
||||
}
|
||||
items := make([]BlogTagResponse, 0, len(tags))
|
||||
for _, item := range tags {
|
||||
items = append(items, toBlogTagResponse(item))
|
||||
}
|
||||
c.JSON(http.StatusOK, BlogTagListResponse{Count: len(items), Items: items})
|
||||
}
|
||||
|
||||
// GetTag godoc
|
||||
// @Summary Public tekil tag getirir
|
||||
// @Tags blogs
|
||||
// @Produce json
|
||||
// @Param slug path string true "Tag slug"
|
||||
// @Success 200 {object} BlogTagResponse
|
||||
// @Failure 404 {object} BlogErrorResponse
|
||||
// @Failure 500 {object} BlogErrorResponse
|
||||
// @Router /api/v1/blogs/tags/{slug} [get]
|
||||
func GetTag(c *gin.Context) {
|
||||
if configs.DB == nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "db baglantisi yok"})
|
||||
return
|
||||
}
|
||||
var tag blogModels.Tag
|
||||
err := configs.DB.Where("slug = ? AND is_active = ?", c.Param("slug"), true).First(&tag).Error
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "tag bulunamadi"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "tag getirilemedi"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, toBlogTagResponse(tag))
|
||||
}
|
||||
|
||||
// CreateTag godoc
|
||||
// @Summary Admin tag olusturur
|
||||
// @Tags blogs
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param request body createTagRequest true "Tag bilgileri"
|
||||
// @Success 201 {object} BlogTagResponse
|
||||
// @Failure 400 {object} BlogErrorResponse
|
||||
// @Failure 500 {object} BlogErrorResponse
|
||||
// @Router /api/v1/blogs/tags [post]
|
||||
func CreateTag(c *gin.Context) {
|
||||
if configs.DB == nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "db baglantisi yok"})
|
||||
return
|
||||
}
|
||||
var req createTagRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
tag := blogModels.Tag{Tag: req.Tag, IsActive: true}
|
||||
if req.IsActive != nil {
|
||||
tag.IsActive = *req.IsActive
|
||||
}
|
||||
if err := configs.DB.Create(&tag).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "tag olusturulamadi"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusCreated, toBlogTagResponse(tag))
|
||||
}
|
||||
|
||||
// UpdateTag godoc
|
||||
// @Summary Admin tag gunceller
|
||||
// @Tags blogs
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path int true "Tag ID"
|
||||
// @Param request body updateTagRequest true "Guncellenecek alanlar"
|
||||
// @Success 200 {object} BlogTagResponse
|
||||
// @Failure 400 {object} BlogErrorResponse
|
||||
// @Failure 404 {object} BlogErrorResponse
|
||||
// @Failure 500 {object} BlogErrorResponse
|
||||
// @Router /api/v1/blogs/tags/{id} [put]
|
||||
func UpdateTag(c *gin.Context) {
|
||||
if configs.DB == nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "db baglantisi yok"})
|
||||
return
|
||||
}
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "gecersiz tag id"})
|
||||
return
|
||||
}
|
||||
var tag blogModels.Tag
|
||||
if err := configs.DB.First(&tag, id).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "tag bulunamadi"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "tag bulunamadi"})
|
||||
return
|
||||
}
|
||||
var req updateTagRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if req.Tag != "" {
|
||||
tag.Tag = req.Tag
|
||||
}
|
||||
if req.IsActive != nil {
|
||||
tag.IsActive = *req.IsActive
|
||||
}
|
||||
if err := configs.DB.Save(&tag).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "tag guncellenemedi"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, toBlogTagResponse(tag))
|
||||
}
|
||||
|
||||
// DeleteTag godoc
|
||||
// @Summary Admin tag siler
|
||||
// @Tags blogs
|
||||
// @Security BearerAuth
|
||||
// @Param id path int true "Tag ID"
|
||||
// @Success 204
|
||||
// @Failure 400 {object} BlogErrorResponse
|
||||
// @Failure 404 {object} BlogErrorResponse
|
||||
// @Failure 500 {object} BlogErrorResponse
|
||||
// @Router /api/v1/blogs/tags/{id} [delete]
|
||||
func DeleteTag(c *gin.Context) {
|
||||
if configs.DB == nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "db baglantisi yok"})
|
||||
return
|
||||
}
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "gecersiz tag id"})
|
||||
return
|
||||
}
|
||||
res := configs.DB.Delete(&blogModels.Tag{}, id)
|
||||
if res.Error != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "tag silinemedi"})
|
||||
return
|
||||
}
|
||||
if res.RowsAffected == 0 {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "tag bulunamadi"})
|
||||
return
|
||||
}
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
202
app/blogs/handlers/blog_test.go
Normal file
202
app/blogs/handlers/blog_test.go
Normal file
@@ -0,0 +1,202 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
blogModels "ginimageApi/app/blogs/models"
|
||||
"ginimageApi/configs"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func setupBlogHandlersTestDB(t *testing.T) {
|
||||
t.Helper()
|
||||
prev := configs.DB
|
||||
dsn := "file:" + t.Name() + "?mode=memory&cache=shared"
|
||||
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatalf("sqlite open failed: %v", err)
|
||||
}
|
||||
if err := db.AutoMigrate(&blogModels.Category{}, &blogModels.Tag{}, &blogModels.Post{}, &blogModels.CategoryView{}, &blogModels.Comment{}); err != nil {
|
||||
t.Fatalf("migrate failed: %v", err)
|
||||
}
|
||||
configs.DB = db
|
||||
t.Cleanup(func() {
|
||||
if sqlDB, err := db.DB(); err == nil {
|
||||
_ = sqlDB.Close()
|
||||
}
|
||||
configs.DB = prev
|
||||
})
|
||||
}
|
||||
|
||||
func withUser(userID uint) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.Set("user_id", userID)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func TestListPostsReturnsOnlyActive(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
setupBlogHandlersTestDB(t)
|
||||
|
||||
posts := []blogModels.Post{
|
||||
{Title: "Active Post", Content: "A", IsActive: true, IsFront: true},
|
||||
{Title: "Passive Post", Content: "B", IsActive: true, IsFront: false},
|
||||
}
|
||||
if err := configs.DB.Create(&posts).Error; err != nil {
|
||||
t.Fatalf("seed failed: %v", err)
|
||||
}
|
||||
if err := configs.DB.Model(&blogModels.Post{}).Where("title = ?", "Passive Post").Update("is_active", false).Error; err != nil {
|
||||
t.Fatalf("seed update failed: %v", err)
|
||||
}
|
||||
|
||||
r := gin.New()
|
||||
r.GET("/blogs", ListPosts)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/blogs", nil))
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
var resp struct {
|
||||
Count int `json:"count"`
|
||||
}
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("json parse failed: %v", err)
|
||||
}
|
||||
if resp.Count != 1 {
|
||||
t.Fatalf("expected only active posts, got %d", resp.Count)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminCreateUpdateDeletePost(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
setupBlogHandlersTestDB(t)
|
||||
|
||||
r := gin.New()
|
||||
r.POST("/blogs", withUser(1), CreatePost)
|
||||
r.PUT("/blogs/:id", withUser(1), UpdatePost)
|
||||
r.DELETE("/blogs/:id", withUser(1), DeletePost)
|
||||
|
||||
createBody := []byte(`{"title":"Yeni Blog","content":"icerik"}`)
|
||||
wCreate := httptest.NewRecorder()
|
||||
reqCreate := httptest.NewRequest(http.MethodPost, "/blogs", bytes.NewReader(createBody))
|
||||
reqCreate.Header.Set("Content-Type", "application/json")
|
||||
r.ServeHTTP(wCreate, reqCreate)
|
||||
if wCreate.Code != http.StatusCreated {
|
||||
t.Fatalf("expected 201, got %d body=%s", wCreate.Code, wCreate.Body.String())
|
||||
}
|
||||
|
||||
// SQLite'da mevcut model taniminda auto ID davranisi tutarsiz olabildigi icin
|
||||
// update/delete senaryosunu explicit ID ile seed edilen kayit uzerinden dogruluyoruz.
|
||||
seedForUpdate := blogModels.Post{ID: 77, Title: "Seeded Blog", Content: "x", IsActive: true, IsFront: true}
|
||||
if err := configs.DB.Create(&seedForUpdate).Error; err != nil {
|
||||
t.Fatalf("seed for update failed: %v", err)
|
||||
}
|
||||
|
||||
updateBody := []byte(`{"title":"Guncel Blog"}`)
|
||||
wUpdate := httptest.NewRecorder()
|
||||
reqUpdate := httptest.NewRequest(http.MethodPut, "/blogs/"+strconv.FormatUint(seedForUpdate.ID, 10), bytes.NewReader(updateBody))
|
||||
reqUpdate.Header.Set("Content-Type", "application/json")
|
||||
r.ServeHTTP(wUpdate, reqUpdate)
|
||||
if wUpdate.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d body=%s", wUpdate.Code, wUpdate.Body.String())
|
||||
}
|
||||
|
||||
wDelete := httptest.NewRecorder()
|
||||
r.ServeHTTP(wDelete, httptest.NewRequest(http.MethodDelete, "/blogs/"+strconv.FormatUint(seedForUpdate.ID, 10), nil))
|
||||
if wDelete.Code != http.StatusNoContent {
|
||||
t.Fatalf("expected 204, got %d", wDelete.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCategoryAndTagEndpoints(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
setupBlogHandlersTestDB(t)
|
||||
|
||||
r := gin.New()
|
||||
r.GET("/blogs/categories", ListCategories)
|
||||
r.GET("/blogs/tags", ListTags)
|
||||
r.POST("/blogs/categories", withUser(1), CreateCategory)
|
||||
r.PUT("/blogs/categories/:id", withUser(1), UpdateCategory)
|
||||
r.DELETE("/blogs/categories/:id", withUser(1), DeleteCategory)
|
||||
r.POST("/blogs/tags", withUser(1), CreateTag)
|
||||
r.PUT("/blogs/tags/:id", withUser(1), UpdateTag)
|
||||
r.DELETE("/blogs/tags/:id", withUser(1), DeleteTag)
|
||||
|
||||
wCreateCategory := httptest.NewRecorder()
|
||||
reqCreateCategory := httptest.NewRequest(http.MethodPost, "/blogs/categories", bytes.NewReader([]byte(`{"title":"Genel"}`)))
|
||||
reqCreateCategory.Header.Set("Content-Type", "application/json")
|
||||
r.ServeHTTP(wCreateCategory, reqCreateCategory)
|
||||
if wCreateCategory.Code != http.StatusCreated {
|
||||
t.Fatalf("category create expected 201, got %d body=%s", wCreateCategory.Code, wCreateCategory.Body.String())
|
||||
}
|
||||
|
||||
wCreateTag := httptest.NewRecorder()
|
||||
reqCreateTag := httptest.NewRequest(http.MethodPost, "/blogs/tags", bytes.NewReader([]byte(`{"tag":"Go"}`)))
|
||||
reqCreateTag.Header.Set("Content-Type", "application/json")
|
||||
r.ServeHTTP(wCreateTag, reqCreateTag)
|
||||
if wCreateTag.Code != http.StatusCreated {
|
||||
t.Fatalf("tag create expected 201, got %d body=%s", wCreateTag.Code, wCreateTag.Body.String())
|
||||
}
|
||||
|
||||
// SQLite'da mevcut model taniminda auto ID davranisi tutarsiz olabildigi icin
|
||||
// kategori/tag update-delete senaryosunu explicit ID ile seed edilen kayitlar uzerinden dogruluyoruz.
|
||||
seedCategory := blogModels.Category{ID: 77, Title: "SeedCategory", IsActive: true, Order: 1}
|
||||
if err := configs.DB.Create(&seedCategory).Error; err != nil {
|
||||
t.Fatalf("seed category for update failed: %v", err)
|
||||
}
|
||||
seedTag := blogModels.Tag{ID: 88, Tag: "SeedTag", IsActive: true}
|
||||
if err := configs.DB.Create(&seedTag).Error; err != nil {
|
||||
t.Fatalf("seed tag for update failed: %v", err)
|
||||
}
|
||||
|
||||
wListCategories := httptest.NewRecorder()
|
||||
r.ServeHTTP(wListCategories, httptest.NewRequest(http.MethodGet, "/blogs/categories", nil))
|
||||
if wListCategories.Code != http.StatusOK {
|
||||
t.Fatalf("category list expected 200, got %d", wListCategories.Code)
|
||||
}
|
||||
|
||||
wListTags := httptest.NewRecorder()
|
||||
r.ServeHTTP(wListTags, httptest.NewRequest(http.MethodGet, "/blogs/tags", nil))
|
||||
if wListTags.Code != http.StatusOK {
|
||||
t.Fatalf("tag list expected 200, got %d", wListTags.Code)
|
||||
}
|
||||
|
||||
wUpdateCategory := httptest.NewRecorder()
|
||||
reqUpdateCategory := httptest.NewRequest(http.MethodPut, "/blogs/categories/"+strconv.FormatUint(seedCategory.ID, 10), bytes.NewReader([]byte(`{"title":"Teknoloji"}`)))
|
||||
reqUpdateCategory.Header.Set("Content-Type", "application/json")
|
||||
r.ServeHTTP(wUpdateCategory, reqUpdateCategory)
|
||||
if wUpdateCategory.Code != http.StatusOK {
|
||||
t.Fatalf("category update expected 200, got %d body=%s", wUpdateCategory.Code, wUpdateCategory.Body.String())
|
||||
}
|
||||
|
||||
wUpdateTag := httptest.NewRecorder()
|
||||
reqUpdateTag := httptest.NewRequest(http.MethodPut, "/blogs/tags/"+strconv.FormatUint(seedTag.ID, 10), bytes.NewReader([]byte(`{"tag":"Golang"}`)))
|
||||
reqUpdateTag.Header.Set("Content-Type", "application/json")
|
||||
r.ServeHTTP(wUpdateTag, reqUpdateTag)
|
||||
if wUpdateTag.Code != http.StatusOK {
|
||||
t.Fatalf("tag update expected 200, got %d body=%s", wUpdateTag.Code, wUpdateTag.Body.String())
|
||||
}
|
||||
|
||||
wDeleteCategory := httptest.NewRecorder()
|
||||
r.ServeHTTP(wDeleteCategory, httptest.NewRequest(http.MethodDelete, "/blogs/categories/"+strconv.FormatUint(seedCategory.ID, 10), nil))
|
||||
if wDeleteCategory.Code != http.StatusNoContent {
|
||||
t.Fatalf("category delete expected 204, got %d", wDeleteCategory.Code)
|
||||
}
|
||||
|
||||
wDeleteTag := httptest.NewRecorder()
|
||||
r.ServeHTTP(wDeleteTag, httptest.NewRequest(http.MethodDelete, "/blogs/tags/"+strconv.FormatUint(seedTag.ID, 10), nil))
|
||||
if wDeleteTag.Code != http.StatusNoContent {
|
||||
t.Fatalf("tag delete expected 204, got %d", wDeleteTag.Code)
|
||||
}
|
||||
}
|
||||
260
app/blogs/models/blog.go
Normal file
260
app/blogs/models/blog.go
Normal file
@@ -0,0 +1,260 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
accountModels "ginimageApi/app/accounts/models"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Note: This file maps Django models to GORM models for MySQL.
|
||||
// Image fields are stored as file path strings. Thumbnail generation and image processing
|
||||
// should be handled elsewhere (e.g., during upload) — TODO: integrate with image processing service.
|
||||
|
||||
// Category represents post categories.
|
||||
type Category struct {
|
||||
ID uint64 `gorm:"type:bigint unsigned;autoIncrement;primaryKey" json:"id"`
|
||||
Title string `gorm:"size:254;not null" json:"title"`
|
||||
Keywords string `gorm:"size:254" json:"keywords"`
|
||||
Desc string `gorm:"size:254" json:"description"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
IsActive bool `gorm:"default:true;index" json:"is_active"`
|
||||
Order int `gorm:"default:1;index" json:"order"`
|
||||
Slug string `gorm:"size:250;not null;index" json:"slug"`
|
||||
ParentID *uint64 `gorm:"type:bigint unsigned;index" json:"parent_id"`
|
||||
Parent *Category `gorm:"foreignKey:ParentID" json:"parent,omitempty"`
|
||||
Children []*Category `gorm:"foreignKey:ParentID" json:"children,omitempty"`
|
||||
Image string `gorm:"size:1024" json:"image"`
|
||||
}
|
||||
|
||||
func (Category) TableName() string {
|
||||
return "categories"
|
||||
}
|
||||
|
||||
// BeforeCreate hook to set slug
|
||||
func (c *Category) BeforeCreate(tx *gorm.DB) (err error) {
|
||||
if c.Slug == "" {
|
||||
c.Slug, err = generateUniqueSlugForCategory(tx, c.Title)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// BeforeUpdate hook ensures slug exists
|
||||
func (c *Category) BeforeUpdate(tx *gorm.DB) (err error) {
|
||||
if c.Slug == "" {
|
||||
c.Slug, err = generateUniqueSlugForCategory(tx, c.Title)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateUniqueSlugForCategory(db *gorm.DB, title string) (string, error) {
|
||||
slug := normalizeSlug(title)
|
||||
base := slug
|
||||
var count int64
|
||||
try := 1
|
||||
for {
|
||||
db.Model(&Category{}).Where("slug = ?", slug).Count(&count)
|
||||
if count == 0 {
|
||||
return slug, nil
|
||||
}
|
||||
slug = fmt.Sprintf("%s-%d", base, try)
|
||||
try++
|
||||
if try > 1000 {
|
||||
return "", errors.New("unable to generate unique slug")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tags model
|
||||
type Tag struct {
|
||||
ID uint64 `gorm:"type:bigint unsigned;autoIncrement;primaryKey" json:"id"`
|
||||
Tag string `gorm:"size:254;not null" json:"tag"`
|
||||
Slug string `gorm:"size:250;not null;index" json:"slug"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
IsActive bool `gorm:"default:true;index" json:"is_active"`
|
||||
}
|
||||
|
||||
func (Tag) TableName() string { return "tags" }
|
||||
|
||||
func (t *Tag) BeforeCreate(tx *gorm.DB) (err error) {
|
||||
if t.Slug == "" {
|
||||
t.Slug, err = generateUniqueSlugForTag(tx, t.Tag)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateUniqueSlugForTag(db *gorm.DB, tag string) (string, error) {
|
||||
slug := normalizeSlug(tag)
|
||||
base := slug
|
||||
var count int64
|
||||
try := 1
|
||||
for {
|
||||
db.Model(&Tag{}).Where("slug = ?", slug).Count(&count)
|
||||
if count == 0 {
|
||||
return slug, nil
|
||||
}
|
||||
slug = fmt.Sprintf("%s-%d", base, try)
|
||||
try++
|
||||
if try > 1000 {
|
||||
return "", errors.New("unable to generate unique slug")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Post model
|
||||
type Post struct {
|
||||
ID uint64 `gorm:"type:bigint unsigned;autoIncrement;primaryKey" json:"id"`
|
||||
Title string `gorm:"size:254;not null" json:"title"`
|
||||
UserID *uint64 `gorm:"type:bigint unsigned;index" json:"user_id"`
|
||||
User *accountModels.User `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
||||
Content string `gorm:"type:text" json:"content"`
|
||||
Categories []*Category `gorm:"many2many:post_categories;" json:"categories"`
|
||||
Keywords string `gorm:"size:254" json:"keywords"`
|
||||
Tags []*Tag `gorm:"many2many:post_tags;" json:"tags"`
|
||||
Image string `gorm:"size:1024" json:"image"`
|
||||
Thumb string `gorm:"size:1024" json:"thumb"`
|
||||
Video string `gorm:"size:254;default:'none'" json:"video"`
|
||||
Slug string `gorm:"size:250;not null;index" json:"slug"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
IsActive bool `gorm:"default:true;index" json:"is_active"`
|
||||
IsFront bool `gorm:"default:true;index" json:"is_front"`
|
||||
ParentID *uint64 `gorm:"type:bigint unsigned;index" json:"parent_id"`
|
||||
Parent *Post `gorm:"foreignKey:ParentID" json:"parent,omitempty"`
|
||||
Children []*Post `gorm:"foreignKey:ParentID" json:"children,omitempty"`
|
||||
}
|
||||
|
||||
func (Post) TableName() string { return "posts" }
|
||||
|
||||
func (p *Post) BeforeCreate(tx *gorm.DB) (err error) {
|
||||
if p.Slug == "" {
|
||||
p.Slug, err = generateUniqueSlugForPost(tx, p.Title)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// Note: Thumbnail generation should be handled in the upload flow.
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Post) BeforeUpdate(tx *gorm.DB) (err error) {
|
||||
if p.Slug == "" {
|
||||
p.Slug, err = generateUniqueSlugForPost(tx, p.Title)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateUniqueSlugForPost(db *gorm.DB, title string) (string, error) {
|
||||
slug := normalizeSlug(title)
|
||||
base := slug
|
||||
var count int64
|
||||
try := 1
|
||||
for {
|
||||
db.Model(&Post{}).Where("slug = ?", slug).Count(&count)
|
||||
if count == 0 {
|
||||
return slug, nil
|
||||
}
|
||||
slug = fmt.Sprintf("%s-%d", base, try)
|
||||
try++
|
||||
if try > 1000 {
|
||||
return "", errors.New("unable to generate unique slug")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CategoryView model
|
||||
type CategoryView struct {
|
||||
ID uint64 `gorm:"type:bigint unsigned;autoIncrement;primaryKey" json:"id"`
|
||||
CategoryID uint64 `gorm:"type:bigint unsigned;index" json:"category_id"`
|
||||
Category *Category `gorm:"foreignKey:CategoryID" json:"category"`
|
||||
IPAddress string `gorm:"size:45;index" json:"ip_address"`
|
||||
UserAgent string `gorm:"type:text" json:"user_agent"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
}
|
||||
|
||||
func (CategoryView) TableName() string { return "category_views" }
|
||||
|
||||
// Comment model
|
||||
type Comment struct {
|
||||
ID uint64 `gorm:"type:bigint unsigned;autoIncrement;primaryKey" json:"id"`
|
||||
UserID uint64 `gorm:"type:bigint unsigned;index" json:"user_id"`
|
||||
PostID uint64 `gorm:"type:bigint unsigned;index" json:"post_id"`
|
||||
Post *Post `gorm:"foreignKey:PostID" json:"post,omitempty"`
|
||||
Title string `gorm:"size:254" json:"title"`
|
||||
Body string `gorm:"type:text" json:"body"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
IsActive bool `gorm:"default:true;index" json:"is_active"`
|
||||
Slug string `gorm:"size:250;index" json:"slug"`
|
||||
ParentID *uint64 `gorm:"type:bigint unsigned;index" json:"parent_id"`
|
||||
Parent *Comment `gorm:"foreignKey:ParentID" json:"parent,omitempty"`
|
||||
Children []*Comment `gorm:"foreignKey:ParentID" json:"children,omitempty"`
|
||||
}
|
||||
|
||||
func (Comment) TableName() string { return "comments" }
|
||||
|
||||
func (c *Comment) BeforeCreate(tx *gorm.DB) (err error) {
|
||||
if c.Slug == "" {
|
||||
c.Slug, err = generateUniqueSlugForComment(tx, c.Title)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateUniqueSlugForComment(db *gorm.DB, title string) (string, error) {
|
||||
slug := normalizeSlug(title)
|
||||
base := slug
|
||||
var count int64
|
||||
try := 1
|
||||
for {
|
||||
db.Model(&Comment{}).Where("slug = ?", slug).Count(&count)
|
||||
if count == 0 {
|
||||
return slug, nil
|
||||
}
|
||||
slug = fmt.Sprintf("%s-%d", base, try)
|
||||
try++
|
||||
if try > 1000 {
|
||||
return "", errors.New("unable to generate unique slug")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// normalizeSlug replaces Turkish characters, lowercases and makes a basic slug.
|
||||
func normalizeSlug(s string) string {
|
||||
replacer := strings.NewReplacer(
|
||||
"ı", "i",
|
||||
"İ", "i",
|
||||
"ç", "c",
|
||||
"Ç", "c",
|
||||
"ş", "s",
|
||||
"Ş", "s",
|
||||
"ö", "o",
|
||||
"Ö", "o",
|
||||
"ü", "u",
|
||||
"Ü", "u",
|
||||
" ", "-",
|
||||
)
|
||||
s = replacer.Replace(s)
|
||||
s = strings.ToLower(s)
|
||||
s = strings.TrimSpace(s)
|
||||
// remove multiple dashes
|
||||
for strings.Contains(s, "--") {
|
||||
s = strings.ReplaceAll(s, "--", "-")
|
||||
}
|
||||
// remove extension-like parts
|
||||
s = strings.Trim(s, "-._")
|
||||
// sanitize file-like chars
|
||||
s = filepath.Clean(s)
|
||||
return s
|
||||
}
|
||||
Reference in New Issue
Block a user