first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 21:40:14 +03:00
commit e04ba85564
129 changed files with 17541 additions and 0 deletions

745
app/blogs/handlers/blog.go Normal file
View 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)
}

View 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
View 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
}