1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
|
package models
import (
"fmt"
"imageboard/config"
"imageboard/utils/validators"
"strings"
"gorm.io/gorm"
)
type Tag struct {
gorm.Model
Name string `gorm:"not null;uniqueIndex;size:100" json:"name"`
Type config.TagType `gorm:"not null;default:'general';size:20" json:"type"`
Description string `gorm:"default:'';type:text" json:"description"`
Count int `gorm:"not null;default:0" json:"count"`
IsDeleted bool `gorm:"not null;default:false" json:"is_deleted"`
ParentID *uint `gorm:"index" json:"-"`
Parent *Tag `gorm:"foreignKey:ParentID" json:"parent,omitempty"`
Children []Tag `gorm:"foreignKey:ParentID" json:"children,omitempty"`
Images []Image `gorm:"many2many:image_tags" json:"images,omitempty"`
}
func (t *Tag) BeforeCreate(tx *gorm.DB) error {
t.Name = strings.TrimSpace(strings.ToLower(t.Name))
t.Description = strings.TrimSpace(t.Description)
if t.Name == "" {
return fmt.Errorf("tag name cannot be empty")
}
if len(t.Name) < 2 || len(t.Name) > 100 {
return fmt.Errorf("tag name must be between 2 and 100 characters")
}
if !validators.IsValidTagName(t.Name) {
return fmt.Errorf("tag name can only contain letters, numbers, and underscores")
}
var existingTag Tag
if err := tx.Where("name = ?", t.Name).First(&existingTag).Error; err == nil {
return fmt.Errorf("tag name '%s' is already taken", t.Name)
}
return nil
}
func (t *Tag) BeforeUpdate(tx *gorm.DB) error {
t.Name = strings.TrimSpace(strings.ToLower(t.Name))
t.Description = strings.TrimSpace(t.Description)
return nil
}
func (t *Tag) GetFullPath() string {
if t.Parent == nil {
return t.Name
}
return t.Parent.GetFullPath() + ":" + t.Name
}
func SearchTags(tx *gorm.DB, query string, limit int) ([]Tag, error) {
var tags []Tag
searchPattern := "%" + strings.TrimSpace(strings.ToLower(query)) + "%"
err := tx.Where("name LIKE ? AND is_deleted = ?", searchPattern, false).
Order("count DESC, name ASC").Limit(limit).Find(&tags).Error
return tags, err
}
func SearchTagsExcluding(tx *gorm.DB, query string, imageID uint, limit int) ([]Tag, error) {
var tags []Tag
searchPattern := "%" + strings.TrimSpace(strings.ToLower(query)) + "%"
err := tx.Where("name LIKE ? AND is_deleted = ? AND id NOT IN (?)",
searchPattern, false,
tx.Table("image_tags").Select("tag_id").Where("image_id = ?", imageID)).
Order("count DESC, name ASC").Limit(limit).Find(&tags).Error
return tags, err
}
func FindOrCreateTag(tx *gorm.DB, name string, tagType config.TagType) (*Tag, error) {
name = strings.TrimSpace(strings.ToLower(name))
// First check for active tag
var tag Tag
if err := tx.Where("name = ? AND is_deleted = ?", name, false).First(&tag).Error; err == nil {
return &tag, nil
}
// Check for deleted tag and restore it
if err := tx.Where("name = ? AND is_deleted = ?", name, true).First(&tag).Error; err == nil {
tag.IsDeleted = false
tag.Type = tagType // Update type in case it changed
if err := tx.Save(&tag).Error; err != nil {
return nil, fmt.Errorf("failed to restore tag: %v", err)
}
return &tag, nil
}
// Create new tag
tag = Tag{
Name: name,
Type: tagType,
}
if err := tx.Create(&tag).Error; err != nil {
return nil, err
}
return &tag, nil
}
func (t *Tag) DeleteTag(tx *gorm.DB) error {
if t.IsDeleted {
return fmt.Errorf("tag is already deleted")
}
if err := tx.Model(t).Association("Images").Clear(); err != nil {
return fmt.Errorf("failed to clear image associations: %v", err)
}
t.IsDeleted = true
t.Count = 0
return tx.Save(t).Error
}
|