aboutsummaryrefslogtreecommitdiff
path: root/models/tags.go
blob: 83f0735bcb15491dcebcc604bcd0aff990d2beba (plain)
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
}