mirror of
https://github.com/photoprism/photoprism.git
synced 2026-01-23 02:24:24 +00:00
Merge c04236c99b into 26b5cbafcd
This commit is contained in:
commit
89421380e5
23 changed files with 1778 additions and 73 deletions
|
|
@ -229,3 +229,48 @@ test.meta("testID", "labels-005").meta({ mode: "public" })("Common: Test mark la
|
|||
.click(Selector("button.action-confirm"));
|
||||
await label.checkHoverActionState("uid", FirstLabelUid, "favorite", true);
|
||||
});
|
||||
|
||||
test.meta("testID", "labels-006").meta({ mode: "public"})("Common: Test homophone label", async(t) => {
|
||||
await menu.openPage("browse");
|
||||
await t.click(toolbar.cardsViewAction);
|
||||
const FirstPhotoUid = await photo.getNthPhotoUid("image", 0);
|
||||
await page.clickCardTitleOfUID(FirstPhotoUid);
|
||||
await t
|
||||
.click(photoedit.labelsTab)
|
||||
.typeText(photoedit.inputLabelName, "上海")
|
||||
.click(Selector(photoedit.addLabel))
|
||||
.typeText(photoedit.inputLabelName, "伤害")
|
||||
.click(Selector(photoedit.addLabel))
|
||||
.click(photoedit.detailsTab);
|
||||
|
||||
await t.click(photoedit.labelsTab);
|
||||
await t.expect(Selector('.text-start').withText("上海").visible).ok();
|
||||
await t.expect(Selector('.text-start').withText("伤害").visible).ok();
|
||||
|
||||
await t.click(photoedit.dialogClose);
|
||||
|
||||
await menu.openPage("labels");
|
||||
|
||||
await toolbar.search("上海");
|
||||
const LabelTest1 = await label.getNthLabeltUid(0);
|
||||
await label.openLabelWithUid(LabelTest1);
|
||||
const LabelPhotoTest1 = await photo.getNthPhotoUid("image", 0);
|
||||
await page.clickCardTitleOfUID(LabelPhotoTest1);
|
||||
await t.click(photoedit.labelsTab);
|
||||
await t.expect(Selector('.text-start').withText("上海").visible).ok();
|
||||
await t.expect(Selector('.text-start').withText("伤害").visible).ok();
|
||||
await t.click(photoedit.dialogClose);
|
||||
|
||||
await menu.openPage("labels");
|
||||
|
||||
await toolbar.search("伤害");
|
||||
const LabelTest2 = await label.getNthLabeltUid(0);
|
||||
await label.openLabelWithUid(LabelTest2);
|
||||
const LabelPhotoTest2 = await photo.getNthPhotoUid("image", 0);
|
||||
await page.clickCardTitleOfUID(LabelPhotoTest2);
|
||||
await t.click(photoedit.labelsTab);
|
||||
await t.expect(Selector('.text-start').withText("上海").visible).ok();
|
||||
await t.expect(Selector('.text-start').withText("伤害").visible).ok();
|
||||
await t.click(photoedit.dialogClose);
|
||||
|
||||
});
|
||||
|
|
@ -105,7 +105,7 @@ func TestBatchPhotosEdit(t *testing.T) {
|
|||
assert.Contains(t, albumsBefore.String(), "{\"value\":\"as6sg6bxpogaaba8\",\"title\":\"Holiday 2030\",\"mixed\":true,\"action\":\"none\"}")
|
||||
labelsBefore := gjson.Get(editValues, "Labels")
|
||||
assert.Contains(t, labelsBefore.String(), "{\"value\":\"ls6sg6b1wowuy316\",\"title\":\"\\u0026friendship\",\"mixed\":true,\"action\":\"none\"}")
|
||||
assert.Contains(t, labelsBefore.String(), "{\"value\":\"ls6sg6b1wowuy3c4\",\"title\":\"Cake\",\"mixed\":false,\"action\":\"none\"}")
|
||||
assert.Contains(t, labelsBefore.String(), "{\"value\":\"ls6sg6b1wowuy3c4\",\"title\":\"Kuchen\",\"mixed\":false,\"action\":\"none\"}")
|
||||
assert.Contains(t, labelsBefore.String(), "{\"value\":\"ls6sg6b1wowuy3c3\",\"title\":\"Flower\",\"mixed\":true,\"action\":\"none\"}")
|
||||
cameraBefore := gjson.Get(editValues, "CameraID")
|
||||
assert.Equal(t, "{\"value\":-2,\"mixed\":true,\"action\":\"none\"}", cameraBefore.String())
|
||||
|
|
@ -478,8 +478,8 @@ func TestBatchPhotosEdit(t *testing.T) {
|
|||
assert.Contains(t, albumsBefore.String(), "{\"value\":\"as6sg6bxpogaaba8\",\"title\":\"Holiday 2030\",\"mixed\":true,\"action\":\"none\"}")
|
||||
labelsBefore := gjson.Get(editValues, "Labels")
|
||||
assert.Contains(t, labelsBefore.String(), "{\"value\":\"ls6sg6b1wowuy316\",\"title\":\"\\u0026friendship\",\"mixed\":true,\"action\":\"none\"}")
|
||||
assert.Contains(t, labelsBefore.String(), "{\"value\":\"ls6sg6b1wowuy3c4\",\"title\":\"Cake\",\"mixed\":false,\"action\":\"none\"}")
|
||||
assert.Contains(t, labelsBefore.String(), "{\"value\":\"ls6sg6b1wowuy3c5\",\"title\":\"COW\",\"mixed\":false,\"action\":\"none\"}")
|
||||
assert.Contains(t, labelsBefore.String(), "{\"value\":\"ls6sg6b1wowuy3c4\",\"title\":\"Kuchen\",\"mixed\":false,\"action\":\"none\"}")
|
||||
assert.Contains(t, labelsBefore.String(), "{\"value\":\"ls6sg6b1wowuy3c5\",\"title\":\"Kuh\",\"mixed\":false,\"action\":\"none\"}")
|
||||
assert.Contains(t, labelsBefore.String(), "{\"value\":\"ls6sg6b1wowuy3c2\",\"title\":\"Landscape\",\"mixed\":false,\"action\":\"none\"}")
|
||||
assert.Contains(t, labelsBefore.String(), "{\"value\":\"ls6sg6b1wowuy3c3\",\"title\":\"Flower\",\"mixed\":true,\"action\":\"none\"}")
|
||||
assert.Contains(t, labelsBefore.String(), "{\"value\":\"ls6sg6b1wowuy317\",\"title\":\"construction\\u0026failure\",\"mixed\":true,\"action\":\"none\"}")
|
||||
|
|
@ -528,10 +528,10 @@ func TestBatchPhotosEdit(t *testing.T) {
|
|||
assert.Equal(t, "&friendship", gjson.Get(r1.Body.String(), "Labels.2.Label.Name").String())
|
||||
assert.Equal(t, "batch", gjson.Get(r1.Body.String(), "Labels.2.LabelSrc").String())
|
||||
assert.Equal(t, "100", gjson.Get(r1.Body.String(), "Labels.2.Uncertainty").String())
|
||||
assert.Equal(t, "COW", gjson.Get(r1.Body.String(), "Labels.3.Label.Name").String())
|
||||
assert.Equal(t, "Kuh", gjson.Get(r1.Body.String(), "Labels.3.Label.Name").String())
|
||||
assert.Equal(t, "batch", gjson.Get(r1.Body.String(), "Labels.3.LabelSrc").String())
|
||||
assert.Equal(t, "100", gjson.Get(r1.Body.String(), "Labels.3.Uncertainty").String())
|
||||
assert.Equal(t, "Cake", gjson.Get(r1.Body.String(), "Labels.4.Label.Name").String())
|
||||
assert.Equal(t, "Kuchen", gjson.Get(r1.Body.String(), "Labels.4.Label.Name").String())
|
||||
assert.Equal(t, "batch", gjson.Get(r1.Body.String(), "Labels.4.LabelSrc").String())
|
||||
assert.Equal(t, "100", gjson.Get(r1.Body.String(), "Labels.4.Uncertainty").String())
|
||||
assert.Equal(t, "Landscape", gjson.Get(r1.Body.String(), "Labels.5.Label.Name").String())
|
||||
|
|
@ -549,7 +549,7 @@ func TestBatchPhotosEdit(t *testing.T) {
|
|||
assert.Equal(t, "Flower", gjson.Get(r2.Body.String(), "Labels.1.Label.Name").String())
|
||||
assert.Equal(t, "batch", gjson.Get(r2.Body.String(), "Labels.1.LabelSrc").String())
|
||||
assert.Equal(t, "0", gjson.Get(r2.Body.String(), "Labels.1.Uncertainty").String())
|
||||
assert.Equal(t, "COW", gjson.Get(r2.Body.String(), "Labels.2.Label.Name").String())
|
||||
assert.Equal(t, "Kuh", gjson.Get(r2.Body.String(), "Labels.2.Label.Name").String())
|
||||
assert.Equal(t, "batch", gjson.Get(r2.Body.String(), "Labels.2.LabelSrc").String())
|
||||
assert.Equal(t, "100", gjson.Get(r2.Body.String(), "Labels.2.Uncertainty").String())
|
||||
assert.Equal(t, "Landscape", gjson.Get(r2.Body.String(), "Labels.3.Label.Name").String())
|
||||
|
|
|
|||
|
|
@ -40,7 +40,19 @@ func TestAddPhotoLabel(t *testing.T) {
|
|||
r := PerformRequestWithBody(app, "POST", "/api/v1/photos/ps6sg6be2lvl0yh8/label", `{"Name": 123, "Uncertainty": 10, "Priority": 2}`)
|
||||
assert.Equal(t, http.StatusBadRequest, r.Code)
|
||||
})
|
||||
|
||||
t.Run("add homophone labels", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
AddPhotoLabel(router)
|
||||
r := PerformRequestWithBody(app, "POST", "/api/v1/photos/ps6sg6be2lvl0yh8/label", `{"Name": "老板", "Priority": 10}`)
|
||||
assert.Equal(t, http.StatusOK, r.Code)
|
||||
assert.Contains(t, r.Body.String(), "老板")
|
||||
// t.Log(r.Body.String())
|
||||
r2 := PerformRequestWithBody(app, "POST", "/api/v1/photos/ps6sg6be2lvl0yh8/label", `{"Name": "老伴", "Priority": 10}`)
|
||||
assert.Equal(t, http.StatusOK, r2.Code)
|
||||
assert.Contains(t, r2.Body.String(), "老板")
|
||||
assert.Contains(t, r2.Body.String(), "老伴")
|
||||
// t.Log(r2.Body.String())
|
||||
})
|
||||
}
|
||||
|
||||
func TestRemovePhotoLabel(t *testing.T) {
|
||||
|
|
@ -113,7 +125,7 @@ func TestUpdatePhotoLabel(t *testing.T) {
|
|||
name := gjson.Get(r.Body.String(), "Labels.#(LabelID==1000003).Label.Name")
|
||||
assert.Equal(t, "0", uncertainty.String())
|
||||
assert.Equal(t, "manual", src.String())
|
||||
assert.Equal(t, "COW", name.String())
|
||||
assert.Equal(t, "Kuh", name.String())
|
||||
})
|
||||
t.Run("photo not found", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
|
|
|
|||
|
|
@ -3262,6 +3262,48 @@ var FileFixtures = FileMap{
|
|||
UpdatedIn: 123789,
|
||||
DeletedAt: nil,
|
||||
},
|
||||
"2024112_090718_51A301B5.jpg": {
|
||||
ID: 1000079,
|
||||
Photo: PhotoFixtures.Pointer("Photo59"),
|
||||
PhotoID: PhotoFixtures.Pointer("Photo59").ID,
|
||||
PhotoUID: PhotoFixtures.Pointer("Photo59").PhotoUID,
|
||||
InstanceID: "a698ac56-6e7e-42b9-9c3e-homophone01",
|
||||
FileUID: "ft3cs9f4enhosfcl",
|
||||
FileName: "2024/11/2024112_090718_51A301B5.jpg",
|
||||
FileRoot: RootOriginals,
|
||||
OriginalName: "shanghai-damage.jpg",
|
||||
FileHash: "a0d1b2c3d4e3f3a3b3c9d0e1f2a3b4c5d6e7f8a9",
|
||||
FileSize: 514000,
|
||||
FileCodec: "jpeg",
|
||||
FileType: "jpg",
|
||||
MediaType: media.Image.String(),
|
||||
FileMime: "image/jpeg",
|
||||
FilePrimary: true,
|
||||
FileSidecar: false,
|
||||
FileVideo: false,
|
||||
FileMissing: false,
|
||||
FilePortrait: false,
|
||||
FileDuration: 0,
|
||||
FileWidth: 3840,
|
||||
FileHeight: 2160,
|
||||
FileOrientation: 1,
|
||||
FileProjection: "",
|
||||
FileAspectRatio: 1.7778,
|
||||
FileMainColor: "black",
|
||||
FileColors: "112233445",
|
||||
FileLuminance: "556677889",
|
||||
FileDiff: 512,
|
||||
FileChroma: 18,
|
||||
FileError: "",
|
||||
Share: []FileShare{},
|
||||
Sync: []FileSync{},
|
||||
ModTime: time.Date(2024, 3, 15, 10, 5, 0, 0, time.UTC).Unix(),
|
||||
CreatedAt: time.Date(2024, 3, 15, 10, 0, 0, 0, time.UTC),
|
||||
CreatedIn: 123456,
|
||||
UpdatedAt: time.Date(2024, 3, 15, 10, 5, 0, 0, time.UTC),
|
||||
UpdatedIn: 123789,
|
||||
DeletedAt: nil,
|
||||
},
|
||||
}
|
||||
|
||||
var FileFixturesExampleJPG = FileFixtures["exampleFileName.jpg"]
|
||||
|
|
|
|||
|
|
@ -3,10 +3,14 @@ package entity
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/jinzhu/inflection"
|
||||
"github.com/ulule/deepcopier"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/ai/classify"
|
||||
|
|
@ -48,6 +52,7 @@ type Label struct {
|
|||
PublishedAt *time.Time `sql:"index" json:"PublishedAt,omitempty" yaml:"PublishedAt,omitempty"`
|
||||
DeletedAt *time.Time `sql:"index" json:"DeletedAt,omitempty" yaml:"-"`
|
||||
New bool `gorm:"-" json:"-" yaml:"-"`
|
||||
MaxHomophone string `gorm:"type:VARCHAR(1);" yaml:"-"`
|
||||
}
|
||||
|
||||
// TableName returns the entity table name.
|
||||
|
|
@ -224,19 +229,59 @@ func (m *Label) Updates(values interface{}) error {
|
|||
return UnscopedDb().Model(m).Updates(values).Error
|
||||
}
|
||||
|
||||
// isASCII checks if there are any runes that exceed the ASCII codeset.
|
||||
func isASCII(s string) bool {
|
||||
for _, r := range s {
|
||||
if r > unicode.MaxASCII {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// FirstOrCreateLabel reuses an existing label matched by slug/custom slug or creates and returns a new one; nil signals lookup/create failure.
|
||||
func FirstOrCreateLabel(m *Label) *Label {
|
||||
if m.LabelSlug == "" && m.CustomSlug == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := &Label{}
|
||||
var labels []Label
|
||||
slugLike := m.LabelSlug + "-c-_"
|
||||
|
||||
nameASCII := isASCII(m.LabelName)
|
||||
|
||||
if err := UnscopedDb().
|
||||
Where("(custom_slug <> '' AND custom_slug = ? OR label_slug <> '' AND label_slug = ?)", m.CustomSlug, m.LabelSlug).
|
||||
First(result).Error; err == nil {
|
||||
return result
|
||||
} else if createErr := m.Create(); createErr == nil {
|
||||
Where("((custom_slug <> '' AND custom_slug = ?) OR (label_slug <> '' AND (label_slug = ? OR label_slug like ?)))", m.CustomSlug, m.LabelSlug, slugLike).
|
||||
Find(&labels).Error; err == nil {
|
||||
slugChar := byte('a')
|
||||
for _, l := range labels {
|
||||
if strings.EqualFold(clean.LabelName(l.LabelName), clean.LabelName(m.LabelName)) || (nameASCII && isASCII(l.LabelName)) {
|
||||
return &l
|
||||
} else {
|
||||
sl := len(l.LabelSlug)
|
||||
if sl > 5 {
|
||||
if l.LabelSlug[sl-4:sl-1] == "-c-" && l.LabelSlug[sl-1] >= byte(slugChar) {
|
||||
slugChar = l.LabelSlug[len(l.LabelSlug)-1] + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(labels) > 0 {
|
||||
if slugChar > byte('z') {
|
||||
log.Errorf("label: %s (find or create %s)", fmt.Errorf("too many homophones for slug"), m.LabelSlug)
|
||||
return nil
|
||||
} else {
|
||||
if err := UnscopedDb().Model(&Label{}).Where("label_slug = ? or label_slug like ?", m.LabelSlug, slugLike).Update("max_homophone", string(slugChar)).Error; err != nil {
|
||||
log.Errorf("label: %s (find or create %s)", err, m.LabelSlug)
|
||||
return nil
|
||||
}
|
||||
m.LabelSlug = fmt.Sprintf("%s-c-%s", txt.Clip(m.LabelSlug, txt.ClipSlug-4), string(slugChar))
|
||||
m.CustomSlug = m.LabelSlug
|
||||
m.MaxHomophone = string(slugChar)
|
||||
}
|
||||
}
|
||||
}
|
||||
if createErr := m.Create(); createErr == nil {
|
||||
if m.LabelPriority >= 0 {
|
||||
event.EntitiesCreated("labels", []*Label{m})
|
||||
|
||||
|
|
@ -246,10 +291,15 @@ func FirstOrCreateLabel(m *Label) *Label {
|
|||
}
|
||||
|
||||
return m
|
||||
} else if err = UnscopedDb().
|
||||
Where("(custom_slug <> '' AND custom_slug = ? OR label_slug <> '' AND label_slug = ?)", m.CustomSlug, m.LabelSlug).
|
||||
First(result).Error; err == nil {
|
||||
return result
|
||||
} else if err := UnscopedDb().
|
||||
Where("((custom_slug <> '' AND custom_slug = ?) OR (label_slug <> '' AND (label_slug = ? OR label_slug like ?)))", m.CustomSlug, m.LabelSlug, slugLike).
|
||||
Find(&labels).Error; err == nil {
|
||||
for _, l := range labels {
|
||||
if strings.EqualFold(clean.LabelName(l.LabelName), clean.LabelName(m.LabelName)) || (nameASCII && isASCII(l.LabelName)) {
|
||||
return &l
|
||||
}
|
||||
}
|
||||
log.Errorf("label: %s (find or create %s)", fmt.Errorf("record not found"), m.LabelSlug)
|
||||
} else {
|
||||
log.Errorf("label: %s (find or create %s)", createErr, m.LabelSlug)
|
||||
}
|
||||
|
|
@ -371,3 +421,69 @@ func (m *Label) UpdateClassify(label classify.Label) error {
|
|||
func (m *Label) Links() Links {
|
||||
return FindLinks("", m.LabelUID)
|
||||
}
|
||||
|
||||
// FindLabels finds all labels that match a name (ie. handles homophones)
|
||||
func FindLabels(names string, sep string, unscoped bool) (labels []Label, err error) {
|
||||
if names == "" {
|
||||
return nil, fmt.Errorf("no names provided")
|
||||
}
|
||||
|
||||
var db *gorm.DB
|
||||
if unscoped {
|
||||
db = UnscopedDb()
|
||||
} else {
|
||||
db = Db()
|
||||
}
|
||||
|
||||
// Generate Where and Values
|
||||
var wheres []string
|
||||
var ins []string
|
||||
var values []interface{}
|
||||
for _, w := range strings.Split(names, sep) {
|
||||
w = txt.Slug(w)
|
||||
if !slices.Contains(ins, w) {
|
||||
ins = append(ins, w)
|
||||
values = append(values, fmt.Sprintf("%s-c-_", w))
|
||||
wheres = append(wheres, "label_slug like ?")
|
||||
}
|
||||
if !txt.ContainsASCIILetters(w) {
|
||||
continue
|
||||
}
|
||||
singular := inflection.Singular(w)
|
||||
|
||||
if singular != w {
|
||||
singular = txt.Slug(singular)
|
||||
if !slices.Contains(ins, singular) {
|
||||
ins = append(ins, singular)
|
||||
values = append(values, fmt.Sprintf("%s-c-_", singular))
|
||||
wheres = append(wheres, "label_slug like ?")
|
||||
}
|
||||
}
|
||||
}
|
||||
wheres = append(wheres, "label_slug IN (?) OR custom_slug IN (?)")
|
||||
values = append(values, ins)
|
||||
values = append(values, ins)
|
||||
|
||||
where := strings.Join(wheres, " OR ")
|
||||
|
||||
var ls []Label
|
||||
if err := db.Where(where, values...).Find(&ls).Error; err != nil {
|
||||
return labels, err
|
||||
}
|
||||
|
||||
nameSlice := strings.Split(names, sep)
|
||||
for _, l := range ls {
|
||||
if l.MaxHomophone == "" {
|
||||
labels = append(labels, l)
|
||||
} else {
|
||||
for _, w := range nameSlice {
|
||||
w = strings.TrimSpace(w)
|
||||
// If the cleansed Name matches or it's an exact match between the name and the Slug, append.
|
||||
if strings.EqualFold(clean.LabelName(l.LabelName), clean.LabelName(w)) || l.LabelSlug == w || l.CustomSlug == w {
|
||||
labels = append(labels, l)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return labels, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package entity
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
|
|
@ -102,6 +103,7 @@ func FindLabel(name string, cached bool) (*Label, error) {
|
|||
|
||||
// Use the label slug as natural key cache.
|
||||
cacheKey := txt.Slug(name)
|
||||
cleanName := clean.LabelName(name)
|
||||
|
||||
if cacheKey == "" {
|
||||
return &Label{}, fmt.Errorf("invalid label slug %s", clean.LogQuote(cacheKey))
|
||||
|
|
@ -110,12 +112,32 @@ func FindLabel(name string, cached bool) (*Label, error) {
|
|||
// Return cached label, if found.
|
||||
if cached {
|
||||
if cacheData, ok := labelCache.Get(cacheKey); ok {
|
||||
log.Tracef("label: cache hit for %s", cacheKey)
|
||||
|
||||
// Get cached data.
|
||||
if result := cacheData.(*Label); result.HasID() {
|
||||
// Return cached entity.
|
||||
return result, nil
|
||||
if result.MaxHomophone != "" {
|
||||
if strings.EqualFold(clean.LabelName(result.LabelName), cleanName) {
|
||||
log.Tracef("label: homophone cache hit for %s", cacheKey)
|
||||
return result, nil
|
||||
} else {
|
||||
// Walk the cache
|
||||
for c := range result.MaxHomophone[0] - byte('a') + 1 {
|
||||
key := fmt.Sprintf("%s-c-%c", cacheKey, c+byte('a'))
|
||||
if cacheData2, ok := labelCache.Get(key); ok {
|
||||
if result2 := cacheData2.(*Label); result2.HasID() {
|
||||
if strings.EqualFold(clean.LabelName(result2.LabelName), cleanName) {
|
||||
log.Tracef("label: homophone cache hit for %s", key)
|
||||
return result2, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.Tracef("label: cache hit for %s", cacheKey)
|
||||
// Return cached entity.
|
||||
return result, nil
|
||||
}
|
||||
} else {
|
||||
// Return cached "not found" error.
|
||||
return &Label{}, fmt.Errorf("label not found")
|
||||
|
|
@ -125,15 +147,24 @@ func FindLabel(name string, cached bool) (*Label, error) {
|
|||
|
||||
// Fetch and cache label.
|
||||
result := &Label{}
|
||||
|
||||
if find := Db().First(result, "(label_slug <> '' AND label_slug = ? OR custom_slug <> '' AND custom_slug = ?)", cacheKey, cacheKey); find.RecordNotFound() {
|
||||
var labels []Label
|
||||
slugLike := cacheKey + "-c-_"
|
||||
if dbResult := Db().Where("(label_slug <> '' AND (label_slug = ? OR label_slug like ?)) OR (custom_slug <> '' AND custom_slug = ?)", cacheKey, slugLike, cacheKey).Find(&labels); dbResult.Error != nil {
|
||||
labelCache.Set(cacheKey, result, labelCacheErrorExpiration)
|
||||
log.Errorf("findlabel: label %s not found with error %s", name, dbResult.Error)
|
||||
return result, dbResult.Error
|
||||
}
|
||||
if len(labels) == 0 {
|
||||
labelCache.Set(cacheKey, result, labelCacheErrorExpiration)
|
||||
return result, fmt.Errorf("label not found")
|
||||
} else if find.Error != nil {
|
||||
labelCache.Set(cacheKey, result, labelCacheErrorExpiration)
|
||||
return result, find.Error
|
||||
} else {
|
||||
labelCache.SetDefault(result.LabelSlug, result)
|
||||
for _, label := range labels {
|
||||
if label.MaxHomophone == "" || strings.EqualFold(clean.LabelName(label.LabelName), cleanName) {
|
||||
labelCache.SetDefault(label.LabelSlug, &label)
|
||||
log.Tracef("label: db hit for %s", label.LabelSlug)
|
||||
return &label, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
|
|
|
|||
|
|
@ -37,6 +37,115 @@ func TestFindLabel(t *testing.T) {
|
|||
assert.Error(t, err)
|
||||
assert.NotNil(t, result)
|
||||
})
|
||||
t.Run("Homophone", func(t *testing.T) {
|
||||
label1 := FirstOrCreateLabel(NewLabel("老板", 10))
|
||||
if !assert.NotNil(t, label1) {
|
||||
t.Fatal("label should not be nil")
|
||||
}
|
||||
label2 := FirstOrCreateLabel(NewLabel("老伴", 10))
|
||||
if !assert.NotNil(t, label2) {
|
||||
t.Fatal("label should not be nil")
|
||||
}
|
||||
label3 := FirstOrCreateLabel(NewLabel("lao-ban", 10))
|
||||
if !assert.NotNil(t, label3) {
|
||||
t.Fatal("label should not be nil")
|
||||
}
|
||||
|
||||
uncached1, findErr := FindLabel("老板", false)
|
||||
assert.NoError(t, findErr)
|
||||
assert.Equal(t, "老板", uncached1.LabelName)
|
||||
uncached2, findErr := FindLabel("老伴", false)
|
||||
assert.NoError(t, findErr)
|
||||
assert.Equal(t, "老伴", uncached2.LabelName)
|
||||
uncached3, findErr := FindLabel("lao-ban", false)
|
||||
assert.NoError(t, findErr)
|
||||
assert.Equal(t, "Lao-Ban", uncached3.LabelName)
|
||||
|
||||
cached1, cacheErr := FindLabel("老板", true)
|
||||
|
||||
assert.NoError(t, cacheErr)
|
||||
assert.Equal(t, "老板", cached1.LabelName)
|
||||
assert.Equal(t, uncached1.LabelSlug, cached1.LabelSlug)
|
||||
assert.Equal(t, uncached1.ID, cached1.ID)
|
||||
assert.Equal(t, uncached1.LabelUID, cached1.LabelUID)
|
||||
|
||||
cached2, cacheErr := FindLabel("老伴", true)
|
||||
|
||||
assert.NoError(t, cacheErr)
|
||||
assert.Equal(t, "老伴", cached2.LabelName)
|
||||
assert.Equal(t, uncached2.LabelSlug, cached2.LabelSlug)
|
||||
assert.Equal(t, uncached2.ID, cached2.ID)
|
||||
assert.Equal(t, uncached2.LabelUID, cached2.LabelUID)
|
||||
|
||||
cached3, cacheErr := FindLabel("lao-ban", true)
|
||||
|
||||
assert.NoError(t, cacheErr)
|
||||
assert.Equal(t, "Lao-Ban", cached3.LabelName)
|
||||
assert.Equal(t, uncached3.LabelSlug, cached3.LabelSlug)
|
||||
assert.Equal(t, uncached3.ID, cached3.ID)
|
||||
assert.Equal(t, uncached3.LabelUID, cached3.LabelUID)
|
||||
|
||||
assert.NoError(t, UnscopedDb().Delete(&label1).Error)
|
||||
assert.NoError(t, UnscopedDb().Delete(&label2).Error)
|
||||
assert.NoError(t, UnscopedDb().Delete(&label3).Error)
|
||||
})
|
||||
|
||||
t.Run("HomophoneCacheInvalidation", func(t *testing.T) {
|
||||
label1 := FirstOrCreateLabel(NewLabel("老板", 10))
|
||||
if !assert.NotNil(t, label1) {
|
||||
t.Fatal("label should not be nil")
|
||||
}
|
||||
// Cache it before the MaxHomophone is populated
|
||||
uncached1, findErr := FindLabel("老板", false)
|
||||
assert.NoError(t, findErr)
|
||||
assert.Equal(t, "老板", uncached1.LabelName)
|
||||
|
||||
label2 := FirstOrCreateLabel(NewLabel("老伴", 10))
|
||||
if !assert.NotNil(t, label2) {
|
||||
t.Fatal("label should not be nil")
|
||||
}
|
||||
label3 := FirstOrCreateLabel(NewLabel("lao-ban", 10))
|
||||
if !assert.NotNil(t, label3) {
|
||||
t.Fatal("label should not be nil")
|
||||
}
|
||||
|
||||
// if the cache hasn't been invalidated, then this will fail.
|
||||
uncached2, findErr := FindLabel("老伴", false)
|
||||
assert.NoError(t, findErr)
|
||||
assert.Equal(t, "老伴", uncached2.LabelName)
|
||||
|
||||
uncached3, findErr := FindLabel("lao-ban", false)
|
||||
assert.NoError(t, findErr)
|
||||
assert.Equal(t, "Lao-Ban", uncached3.LabelName)
|
||||
|
||||
cached1, cacheErr := FindLabel("老板", true)
|
||||
|
||||
assert.NoError(t, cacheErr)
|
||||
assert.Equal(t, "老板", cached1.LabelName)
|
||||
assert.Equal(t, uncached1.LabelSlug, cached1.LabelSlug)
|
||||
assert.Equal(t, uncached1.ID, cached1.ID)
|
||||
assert.Equal(t, uncached1.LabelUID, cached1.LabelUID)
|
||||
|
||||
cached2, cacheErr := FindLabel("老伴", true)
|
||||
|
||||
assert.NoError(t, cacheErr)
|
||||
assert.Equal(t, "老伴", cached2.LabelName)
|
||||
assert.Equal(t, uncached2.LabelSlug, cached2.LabelSlug)
|
||||
assert.Equal(t, uncached2.ID, cached2.ID)
|
||||
assert.Equal(t, uncached2.LabelUID, cached2.LabelUID)
|
||||
|
||||
cached3, cacheErr := FindLabel("lao-ban", true)
|
||||
|
||||
assert.NoError(t, cacheErr)
|
||||
assert.Equal(t, "Lao-Ban", cached3.LabelName)
|
||||
assert.Equal(t, uncached3.LabelSlug, cached3.LabelSlug)
|
||||
assert.Equal(t, uncached3.ID, cached3.ID)
|
||||
assert.Equal(t, uncached3.LabelUID, cached3.LabelUID)
|
||||
|
||||
assert.NoError(t, UnscopedDb().Delete(&label1).Error)
|
||||
assert.NoError(t, UnscopedDb().Delete(&label2).Error)
|
||||
assert.NoError(t, UnscopedDb().Delete(&label3).Error)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFindPhotoLabel(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ var LabelFixtures = LabelMap{
|
|||
LabelUID: "ls6sg6b1wowuy3c4",
|
||||
LabelSlug: "cake",
|
||||
CustomSlug: "kuchen",
|
||||
LabelName: "Cake",
|
||||
LabelName: "Kuchen",
|
||||
LabelPriority: 5,
|
||||
LabelFavorite: false,
|
||||
LabelDescription: "",
|
||||
|
|
@ -84,7 +84,7 @@ var LabelFixtures = LabelMap{
|
|||
LabelUID: "ls6sg6b1wowuy3c5",
|
||||
LabelSlug: "cow",
|
||||
CustomSlug: "kuh",
|
||||
LabelName: "COW",
|
||||
LabelName: "Kuh",
|
||||
LabelPriority: -1,
|
||||
LabelFavorite: true,
|
||||
LabelDescription: "",
|
||||
|
|
@ -572,6 +572,42 @@ var LabelFixtures = LabelMap{
|
|||
DeletedAt: nil,
|
||||
New: false,
|
||||
},
|
||||
"shanghai1": {
|
||||
ID: 1000032,
|
||||
LabelUID: "ls6sg6b1wowuy334",
|
||||
LabelSlug: "shang-hai",
|
||||
CustomSlug: "shang-hai",
|
||||
LabelName: "上海",
|
||||
LabelPriority: 1,
|
||||
LabelFavorite: true,
|
||||
LabelDescription: "",
|
||||
LabelNotes: "",
|
||||
PhotoCount: 1,
|
||||
LabelCategories: []*Label{},
|
||||
CreatedAt: Now(),
|
||||
UpdatedAt: Now(),
|
||||
DeletedAt: nil,
|
||||
New: false,
|
||||
MaxHomophone: "a",
|
||||
},
|
||||
"shanghai2": {
|
||||
ID: 1000033,
|
||||
LabelUID: "ls6sg6b1wowuy335",
|
||||
LabelSlug: "shang-hai-c-a",
|
||||
CustomSlug: "shang-hai-c-a",
|
||||
LabelName: "伤害",
|
||||
LabelPriority: 1,
|
||||
LabelFavorite: true,
|
||||
LabelDescription: "",
|
||||
LabelNotes: "",
|
||||
PhotoCount: 1,
|
||||
LabelCategories: []*Label{},
|
||||
CreatedAt: Now(),
|
||||
UpdatedAt: Now(),
|
||||
DeletedAt: nil,
|
||||
New: false,
|
||||
MaxHomophone: "a",
|
||||
},
|
||||
}
|
||||
|
||||
// CreateLabelFixtures inserts known entities into the database for testing.
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import (
|
|||
"github.com/photoprism/photoprism/internal/ai/classify"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
)
|
||||
|
||||
func TestNewLabel(t *testing.T) {
|
||||
|
|
@ -26,6 +27,16 @@ func TestNewLabel(t *testing.T) {
|
|||
assert.Equal(t, "unknown", label.LabelSlug)
|
||||
assert.Equal(t, -6, label.LabelPriority)
|
||||
})
|
||||
t.Run("Homophones", func(t *testing.T) {
|
||||
label := NewLabel("老板", 10)
|
||||
assert.Equal(t, "老板", label.LabelName)
|
||||
assert.Equal(t, "lao-ban", label.LabelSlug)
|
||||
assert.Equal(t, 10, label.LabelPriority)
|
||||
label = NewLabel("老伴", 10)
|
||||
assert.Equal(t, "老伴", label.LabelName)
|
||||
assert.Equal(t, "lao-ban", label.LabelSlug)
|
||||
assert.Equal(t, 10, label.LabelPriority)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLabel_TableName(t *testing.T) {
|
||||
|
|
@ -172,20 +183,409 @@ func TestLabel_GetSlug(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestFirstOrCreateLabel(t *testing.T) {
|
||||
label := LabelFixtures.Get("flower")
|
||||
result := FirstOrCreateLabel(&label)
|
||||
t.Run("First", func(t *testing.T) {
|
||||
// Find flower
|
||||
label := LabelFixtures.Get("flower")
|
||||
result := FirstOrCreateLabel(&label)
|
||||
|
||||
if result == nil {
|
||||
t.Fatal("result must not be nil")
|
||||
}
|
||||
if !assert.NotNil(t, result) {
|
||||
t.Fatal("result should not be nil")
|
||||
}
|
||||
assert.Equal(t, label.LabelName, result.LabelName, "LabelName should be the same")
|
||||
assert.Equal(t, label.LabelSlug, result.LabelSlug, "LabelSlug should be the same")
|
||||
|
||||
if result.LabelName != label.LabelName {
|
||||
t.Errorf("LabelName should be the same: %s %s", result.LabelName, label.LabelName)
|
||||
}
|
||||
// Find Batch Delete
|
||||
label = *NewLabel("Batch Delete", 10)
|
||||
resultBase := FirstOrCreateLabel(&label)
|
||||
if !assert.NotNil(t, resultBase) {
|
||||
t.Fatal("resultBase should not be nil")
|
||||
}
|
||||
assert.Equal(t, LabelFixtures.Get("batchdelete").LabelUID, resultBase.LabelUID)
|
||||
|
||||
if result.LabelSlug != label.LabelSlug {
|
||||
t.Errorf("LabelName should be the same: %s %s", result.LabelSlug, label.LabelSlug)
|
||||
}
|
||||
// Find Batch Delete with -
|
||||
label = *NewLabel("Batch-Delete", 10)
|
||||
result = FirstOrCreateLabel(&label)
|
||||
if !assert.NotNil(t, result) {
|
||||
t.Fatal("result should not be nil")
|
||||
}
|
||||
assert.Equal(t, LabelFixtures.Get("batchdelete").LabelUID, result.LabelUID)
|
||||
|
||||
// Find Batch Delete lowercase
|
||||
label = *NewLabel("batch delete'", 10)
|
||||
result = FirstOrCreateLabel(&label)
|
||||
if !assert.NotNil(t, result) {
|
||||
t.Fatal("result should not be nil")
|
||||
}
|
||||
assert.Equal(t, LabelFixtures.Get("batchdelete").LabelUID, result.LabelUID)
|
||||
|
||||
// Find Batch Delete with a unicode graphic character
|
||||
label = *NewLabel("BATCH DELETE🢱", 10)
|
||||
result = FirstOrCreateLabel(&label)
|
||||
if !assert.NotNil(t, result) {
|
||||
t.Fatal("result should not be nil")
|
||||
}
|
||||
assert.Equal(t, LabelFixtures.Get("batchdelete").LabelUID, result.LabelUID)
|
||||
})
|
||||
|
||||
t.Run("Homophones", func(t *testing.T) {
|
||||
// Add a homophone
|
||||
label := NewLabel("老板", 10)
|
||||
assert.Equal(t, "老板", label.LabelName)
|
||||
assert.Equal(t, "lao-ban", label.LabelSlug)
|
||||
assert.Equal(t, 10, label.LabelPriority)
|
||||
|
||||
result1 := FirstOrCreateLabel(label)
|
||||
|
||||
if !assert.NotNil(t, result1) {
|
||||
t.Fatal("result should not be nil")
|
||||
}
|
||||
//t.Logf("result = %+v", result)
|
||||
assert.Equal(t, label.LabelName, result1.LabelName, "LabelName should be the same")
|
||||
assert.Equal(t, label.LabelSlug, result1.LabelSlug, "LabelSlug should be the same")
|
||||
assert.Equal(t, "", result1.MaxHomophone)
|
||||
|
||||
// Add another homophone
|
||||
label2 := NewLabel("老伴", 10)
|
||||
assert.Equal(t, "老伴", label2.LabelName)
|
||||
assert.Equal(t, "lao-ban", label2.LabelSlug)
|
||||
assert.Equal(t, 10, label2.LabelPriority)
|
||||
|
||||
result2 := FirstOrCreateLabel(label2)
|
||||
if !assert.NotNil(t, result2) {
|
||||
t.Fatal("result should not be nil")
|
||||
}
|
||||
//t.Logf("result = %+v", result)
|
||||
assert.NotEqual(t, label.LabelName, label2.LabelName, "LabelName should not be the same")
|
||||
assert.NotEqual(t, label.LabelSlug, label2.LabelSlug, "LabelSlug should not be the same")
|
||||
assert.Equal(t, "lao-ban-c-a", result2.LabelSlug)
|
||||
assert.Equal(t, "a", result2.MaxHomophone)
|
||||
|
||||
// Add the homophone in ascii
|
||||
label3 := NewLabel("lao-ban", 10)
|
||||
result3 := FirstOrCreateLabel(label3)
|
||||
if !assert.NotNil(t, result3) {
|
||||
t.Fatal("result should not be nil")
|
||||
}
|
||||
//t.Logf("result = %+v", result)
|
||||
assert.NotEqual(t, label.LabelName, label3.LabelName, "LabelName should be the same")
|
||||
assert.NotEqual(t, label.LabelSlug, label3.LabelSlug, "LabelSlug should not be the same")
|
||||
assert.Equal(t, "lao-ban-c-b", result3.LabelSlug)
|
||||
assert.Equal(t, "b", result3.MaxHomophone)
|
||||
|
||||
assert.NotEqual(t, result1.LabelUID, result2.LabelUID)
|
||||
assert.NotEqual(t, result1.LabelUID, result3.LabelUID)
|
||||
assert.NotEqual(t, result2.LabelUID, result3.LabelUID)
|
||||
|
||||
// Make sure that we find the correct homophone
|
||||
label1a := NewLabel("老板", 10)
|
||||
result1a := FirstOrCreateLabel(label1a)
|
||||
if !assert.NotNil(t, result1a) {
|
||||
t.Fatal("result should not be nil")
|
||||
}
|
||||
assert.Equal(t, result1.LabelUID, result1a.LabelUID)
|
||||
assert.Equal(t, "b", result1a.MaxHomophone)
|
||||
|
||||
label2a := NewLabel("老伴", 10)
|
||||
result2a := FirstOrCreateLabel(label2a)
|
||||
if !assert.NotNil(t, result2a) {
|
||||
t.Fatal("result should not be nil")
|
||||
}
|
||||
assert.Equal(t, result2.LabelUID, result2a.LabelUID)
|
||||
assert.Equal(t, "b", result2a.MaxHomophone)
|
||||
|
||||
label3a := NewLabel("lao-ban", 10)
|
||||
result3a := FirstOrCreateLabel(label3a)
|
||||
if !assert.NotNil(t, result3a) {
|
||||
t.Fatal("result should not be nil")
|
||||
}
|
||||
assert.Equal(t, result3.LabelUID, result3a.LabelUID)
|
||||
assert.Equal(t, "b", result3a.MaxHomophone)
|
||||
|
||||
assert.NoError(t, UnscopedDb().Delete(&result1).Error)
|
||||
assert.NoError(t, UnscopedDb().Delete(&result2).Error)
|
||||
assert.NoError(t, UnscopedDb().Delete(&result3).Error)
|
||||
})
|
||||
t.Run("ExceedHomophones", func(t *testing.T) {
|
||||
label := NewLabel("送钟", 10)
|
||||
result := FirstOrCreateLabel(label)
|
||||
//t.Logf("result = %+v", result)
|
||||
assert.Equal(t, "song-zhong", result.LabelSlug)
|
||||
assert.NotNil(t, result)
|
||||
label = NewLabel("song-zhong", 10)
|
||||
result = FirstOrCreateLabel(label)
|
||||
assert.NotNil(t, result)
|
||||
assert.Equal(t, "song-zhong-c-a", result.LabelSlug)
|
||||
// Force increment to maximum
|
||||
label = NewLabel("song-zhong-c-z", 10)
|
||||
result = FirstOrCreateLabel(label)
|
||||
assert.NotNil(t, result)
|
||||
assert.Equal(t, "song-zhong-c-z", result.LabelSlug)
|
||||
// This one should fail as we have run out of incrementers
|
||||
label = NewLabel("送终", 10)
|
||||
result = FirstOrCreateLabel(label)
|
||||
//t.Logf("result = %+v", result)
|
||||
assert.Nil(t, result)
|
||||
})
|
||||
t.Run("UnicodeEmoji", func(t *testing.T) {
|
||||
// Test emitocons
|
||||
label := NewLabel("😖😕", 10)
|
||||
assert.Equal(t, "😖😕", label.LabelName)
|
||||
assert.Equal(t, "_5cpzrfxqt5mjk", label.LabelSlug)
|
||||
assert.Equal(t, 10, label.LabelPriority)
|
||||
|
||||
result := FirstOrCreateLabel(label)
|
||||
|
||||
if !assert.NotNil(t, result) {
|
||||
t.Fatal("result should not be nil")
|
||||
}
|
||||
//t.Logf("result = %+v", result)
|
||||
assert.Equal(t, "😖😕", result.LabelName, "LabelName should be the same")
|
||||
assert.Equal(t, "_5cpzrfxqt5mjk", result.LabelSlug, "LabelSlug should be the same")
|
||||
assert.Equal(t, 10, result.LabelPriority)
|
||||
|
||||
label = NewLabel("😖😕", 1)
|
||||
result2 := FirstOrCreateLabel(label)
|
||||
if !assert.NotNil(t, result2) {
|
||||
t.Fatal("result should not be nil")
|
||||
}
|
||||
//t.Logf("result = %+v", result)
|
||||
assert.Equal(t, "😖😕", result2.LabelName, "LabelName should be the same")
|
||||
assert.Equal(t, "_5cpzrfxqt5mjk", result2.LabelSlug, "LabelSlug should be the same")
|
||||
assert.Equal(t, result.LabelUID, result2.LabelUID, "LabelUID should be the same")
|
||||
assert.Equal(t, 10, result2.LabelPriority)
|
||||
|
||||
// Test Unicode AND Emoticons
|
||||
label = NewLabel("😖அஆஇ😕", 10)
|
||||
assert.Equal(t, "😖அஆஇ😕", label.LabelName)
|
||||
assert.Equal(t, "aaai", label.LabelSlug)
|
||||
assert.Equal(t, 10, label.LabelPriority)
|
||||
|
||||
result = FirstOrCreateLabel(label)
|
||||
|
||||
if !assert.NotNil(t, result) {
|
||||
t.Fatal("result should not be nil")
|
||||
}
|
||||
//t.Logf("result = %+v", result)
|
||||
assert.Equal(t, "😖அஆஇ😕", result.LabelName, "LabelName should be the same")
|
||||
assert.Equal(t, "aaai", result.LabelSlug, "LabelSlug should be the same")
|
||||
|
||||
label = NewLabel("😖அஆஇ😕", 1)
|
||||
result2 = FirstOrCreateLabel(label)
|
||||
if !assert.NotNil(t, result2) {
|
||||
t.Fatal("result should not be nil")
|
||||
}
|
||||
//t.Logf("result = %+v", result)
|
||||
assert.Equal(t, "😖அஆஇ😕", result2.LabelName, "LabelName should be the same")
|
||||
assert.Equal(t, "aaai", result2.LabelSlug, "LabelSlug should be the same")
|
||||
assert.Equal(t, result.LabelUID, result2.LabelUID, "LabelUID should be the same")
|
||||
assert.Equal(t, 10, result2.LabelPriority)
|
||||
|
||||
// Unicode Only to find with Emoticons
|
||||
label = NewLabel("அஆஇ", 1)
|
||||
result2 = FirstOrCreateLabel(label)
|
||||
if !assert.NotNil(t, result2) {
|
||||
t.Fatal("result should not be nil")
|
||||
}
|
||||
//t.Logf("result = %+v", result)
|
||||
assert.Equal(t, "😖அஆஇ😕", result2.LabelName, "LabelName should be the same")
|
||||
assert.Equal(t, "aaai", result2.LabelSlug, "LabelSlug should be the same")
|
||||
assert.Equal(t, result.LabelUID, result2.LabelUID, "LabelUID should be the same")
|
||||
assert.Equal(t, 10, result2.LabelPriority)
|
||||
|
||||
// Test Unicode Only
|
||||
label = NewLabel("அஆஇண", 10)
|
||||
assert.Equal(t, "அஆஇண", label.LabelName)
|
||||
assert.Equal(t, "aaainn", label.LabelSlug)
|
||||
assert.Equal(t, 10, label.LabelPriority)
|
||||
|
||||
result = FirstOrCreateLabel(label)
|
||||
|
||||
if !assert.NotNil(t, result) {
|
||||
t.Fatal("result should not be nil")
|
||||
}
|
||||
//t.Logf("result = %+v", result)
|
||||
assert.Equal(t, "அஆஇண", result.LabelName, "LabelName should be the same")
|
||||
assert.Equal(t, "aaainn", result.LabelSlug, "LabelSlug should be the same")
|
||||
|
||||
label = NewLabel("அஆஇண", 1)
|
||||
result2 = FirstOrCreateLabel(label)
|
||||
if !assert.NotNil(t, result2) {
|
||||
t.Fatal("result should not be nil")
|
||||
}
|
||||
//t.Logf("result = %+v", result)
|
||||
assert.Equal(t, "அஆஇண", result2.LabelName, "LabelName should be the same")
|
||||
assert.Equal(t, "aaainn", result2.LabelSlug, "LabelSlug should be the same")
|
||||
assert.Equal(t, result.LabelUID, result2.LabelUID, "LabelUID should be the same")
|
||||
assert.Equal(t, 10, result2.LabelPriority)
|
||||
|
||||
})
|
||||
t.Run("Renamed", func(t *testing.T) {
|
||||
// Find cow
|
||||
label := LabelFixtures.Get("cow")
|
||||
result := FirstOrCreateLabel(&label)
|
||||
|
||||
if !assert.NotNil(t, result) {
|
||||
t.Fatal("result should not be nil")
|
||||
}
|
||||
assert.Equal(t, label.LabelName, result.LabelName, "LabelName should be the same")
|
||||
assert.Equal(t, label.LabelSlug, result.LabelSlug, "LabelSlug should be the same")
|
||||
|
||||
label1 := NewLabel("Cow", 5)
|
||||
result1 := FirstOrCreateLabel(label1)
|
||||
require.NotNil(t, result1)
|
||||
|
||||
assert.Equal(t, label.ID, result1.ID)
|
||||
})
|
||||
t.Run("AmpersandVsAnd", func(t *testing.T) {
|
||||
// Add base record
|
||||
label := NewLabel("Fire and Station", 10)
|
||||
assert.Equal(t, "Fire and Station", label.LabelName)
|
||||
assert.Equal(t, "fire-and-station", label.LabelSlug)
|
||||
assert.Equal(t, 10, label.LabelPriority)
|
||||
|
||||
result1 := FirstOrCreateLabel(label)
|
||||
|
||||
if !assert.NotNil(t, result1) {
|
||||
t.Fatal("result should not be nil")
|
||||
}
|
||||
//t.Logf("result = %+v", result)
|
||||
assert.Equal(t, label.LabelName, result1.LabelName, "LabelName should be the same")
|
||||
assert.Equal(t, label.LabelSlug, result1.LabelSlug, "LabelSlug should be the same")
|
||||
assert.Equal(t, "", result1.MaxHomophone)
|
||||
|
||||
// Find Base Record with slug
|
||||
label2 := NewLabel("Fire & Station", 10)
|
||||
assert.Equal(t, "Fire & Station", label2.LabelName)
|
||||
assert.Equal(t, "fire-and-station", label2.LabelSlug)
|
||||
assert.Equal(t, 10, label2.LabelPriority)
|
||||
|
||||
result2 := FirstOrCreateLabel(label2)
|
||||
if !assert.NotNil(t, result2) {
|
||||
t.Fatal("result should not be nil")
|
||||
}
|
||||
//t.Logf("result = %+v", result)
|
||||
assert.NotEqual(t, label.LabelName, label2.LabelName, "LabelName should not be the same")
|
||||
assert.Equal(t, label.LabelSlug, label2.LabelSlug, "LabelSlug should be the same")
|
||||
assert.Equal(t, "fire-and-station", result2.LabelSlug)
|
||||
assert.Equal(t, "", result2.MaxHomophone)
|
||||
|
||||
assert.Equal(t, result1.LabelUID, result2.LabelUID)
|
||||
|
||||
assert.NoError(t, UnscopedDb().Delete(&result1).Error)
|
||||
assert.NoError(t, UnscopedDb().Delete(&result2).Error)
|
||||
})
|
||||
t.Run("AtVsAt", func(t *testing.T) {
|
||||
// Add base record
|
||||
label := NewLabel("老伴 at 伤害", 10)
|
||||
assert.Equal(t, "老伴 at 伤害", label.LabelName)
|
||||
assert.Equal(t, "lao-ban-at-shang-hai", label.LabelSlug)
|
||||
assert.Equal(t, 10, label.LabelPriority)
|
||||
|
||||
result1 := FirstOrCreateLabel(label)
|
||||
|
||||
if !assert.NotNil(t, result1) {
|
||||
t.Fatal("result should not be nil")
|
||||
}
|
||||
//t.Logf("result = %+v", result)
|
||||
assert.Equal(t, label.LabelName, result1.LabelName, "LabelName should be the same")
|
||||
assert.Equal(t, label.LabelSlug, result1.LabelSlug, "LabelSlug should be the same")
|
||||
assert.Equal(t, "", result1.MaxHomophone)
|
||||
|
||||
// Find Base Record with slug
|
||||
label2 := NewLabel("老伴 @ 伤害", 10)
|
||||
assert.Equal(t, "老伴 @ 伤害", label2.LabelName)
|
||||
assert.Equal(t, "lao-ban-at-shang-hai", label2.LabelSlug)
|
||||
assert.Equal(t, 10, label2.LabelPriority)
|
||||
|
||||
result2 := FirstOrCreateLabel(label2)
|
||||
if !assert.NotNil(t, result2) {
|
||||
t.Fatal("result should not be nil")
|
||||
}
|
||||
//t.Logf("result = %+v", result)
|
||||
assert.NotEqual(t, label.LabelName, label2.LabelName, "LabelName should not be the same")
|
||||
assert.Equal(t, label.LabelSlug, label2.LabelSlug, "LabelSlug should be the same")
|
||||
assert.Equal(t, "lao-ban-at-shang-hai", result2.LabelSlug)
|
||||
assert.Equal(t, "", result2.MaxHomophone)
|
||||
|
||||
assert.Equal(t, result1.LabelUID, result2.LabelUID)
|
||||
|
||||
label3 := NewLabel("老伴 @ 上海", 10)
|
||||
assert.Equal(t, "老伴 @ 上海", label3.LabelName)
|
||||
assert.Equal(t, "lao-ban-at-shang-hai", label3.LabelSlug)
|
||||
assert.Equal(t, 10, label3.LabelPriority)
|
||||
|
||||
result3 := FirstOrCreateLabel(label3)
|
||||
if !assert.NotNil(t, result3) {
|
||||
t.Fatal("result should not be nil")
|
||||
}
|
||||
//t.Logf("result = %+v", result)
|
||||
assert.NotEqual(t, label.LabelName, label3.LabelName, "LabelName should not be the same")
|
||||
assert.NotEqual(t, label.LabelSlug, label3.LabelSlug, "LabelSlug should be the same")
|
||||
assert.Equal(t, "lao-ban-at-shang-hai-c-a", result3.LabelSlug)
|
||||
assert.Equal(t, "a", result3.MaxHomophone)
|
||||
|
||||
assert.NotEqual(t, result1.LabelUID, result3.LabelUID)
|
||||
|
||||
assert.NoError(t, UnscopedDb().Delete(&result1).Error)
|
||||
assert.NoError(t, UnscopedDb().Delete(&result2).Error)
|
||||
assert.NoError(t, UnscopedDb().Delete(&result3).Error)
|
||||
})
|
||||
t.Run("RemovedRunes", func(t *testing.T) {
|
||||
// Add base record
|
||||
label := NewLabel("Fire Station", 10)
|
||||
assert.Equal(t, "Fire Station", label.LabelName)
|
||||
assert.Equal(t, "fire-station", label.LabelSlug)
|
||||
assert.Equal(t, 10, label.LabelPriority)
|
||||
|
||||
result1 := FirstOrCreateLabel(label)
|
||||
|
||||
if !assert.NotNil(t, result1) {
|
||||
t.Fatal("result must not be nil")
|
||||
}
|
||||
//t.Logf("result = %+v", result)
|
||||
assert.Equal(t, label.LabelName, result1.LabelName, "LabelName should be the same")
|
||||
assert.Equal(t, label.LabelSlug, result1.LabelSlug, "LabelSlug should be the same")
|
||||
assert.Equal(t, "", result1.MaxHomophone)
|
||||
|
||||
// Find Base Record with slug
|
||||
label2 := NewLabel("fire-station", 10)
|
||||
assert.Equal(t, "Fire-Station", label2.LabelName)
|
||||
assert.Equal(t, "fire-station", label2.LabelSlug)
|
||||
assert.Equal(t, 10, label2.LabelPriority)
|
||||
|
||||
result2 := FirstOrCreateLabel(label2)
|
||||
if !assert.NotNil(t, result2) {
|
||||
t.Fatal("result should not be nil")
|
||||
}
|
||||
//t.Logf("result = %+v", result)
|
||||
assert.NotEqual(t, label.LabelName, label2.LabelName, "LabelName should not be the same")
|
||||
assert.Equal(t, label.LabelSlug, label2.LabelSlug, "LabelSlug should be the same")
|
||||
assert.Equal(t, "fire-station", result2.LabelSlug)
|
||||
assert.Equal(t, "", result2.MaxHomophone)
|
||||
|
||||
// Find Base Record due to character removal
|
||||
label3 := NewLabel("Fire+Station", 10)
|
||||
assert.Equal(t, "Fire+Station", label3.LabelName)
|
||||
assert.Equal(t, "fire-station", label3.LabelSlug)
|
||||
assert.Equal(t, 10, label3.LabelPriority)
|
||||
result3 := FirstOrCreateLabel(label3)
|
||||
if !assert.NotNil(t, result3) {
|
||||
t.Fatal("result should not be nil")
|
||||
}
|
||||
//t.Logf("result = %+v", result)
|
||||
assert.NotEqual(t, label.LabelName, label3.LabelName, "LabelName should be the same")
|
||||
assert.Equal(t, label.LabelSlug, label3.LabelSlug, "LabelSlug should be the same")
|
||||
assert.Equal(t, "fire-station", result3.LabelSlug)
|
||||
assert.Equal(t, "", result3.MaxHomophone)
|
||||
|
||||
assert.Equal(t, result1.LabelUID, result2.LabelUID)
|
||||
assert.Equal(t, result1.LabelUID, result3.LabelUID)
|
||||
assert.Equal(t, result2.LabelUID, result3.LabelUID)
|
||||
|
||||
assert.NoError(t, UnscopedDb().Delete(&result1).Error)
|
||||
assert.NoError(t, UnscopedDb().Delete(&result2).Error)
|
||||
assert.NoError(t, UnscopedDb().Delete(&result3).Error)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLabel_UpdateClassify(t *testing.T) {
|
||||
|
|
@ -388,3 +788,128 @@ func createTestLabel(t *testing.T, prefix string) *Label {
|
|||
|
||||
return label
|
||||
}
|
||||
|
||||
func TestFindLabels(t *testing.T) {
|
||||
t.Run("SuccessSingular", func(t *testing.T) {
|
||||
label := LabelFixtures.Get("flower")
|
||||
labels, err := FindLabels(label.LabelName, txt.Or, false)
|
||||
assert.NoError(t, err)
|
||||
if assert.Len(t, labels, 1) {
|
||||
assert.Equal(t, label.ID, labels[0].ID)
|
||||
}
|
||||
})
|
||||
t.Run("SuccessPlural", func(t *testing.T) {
|
||||
label := LabelFixtures.Get("flower")
|
||||
labels, err := FindLabels(label.LabelName+"s", txt.Or, false)
|
||||
assert.NoError(t, err)
|
||||
if assert.Len(t, labels, 1) {
|
||||
assert.Equal(t, label.ID, labels[0].ID)
|
||||
}
|
||||
})
|
||||
t.Run("SuccessMultiple", func(t *testing.T) {
|
||||
label1 := LabelFixtures.Get("landscape")
|
||||
label2 := LabelFixtures.Get("flower")
|
||||
labels, err := FindLabels(fmt.Sprintf("%s%s%s", label1.LabelName, txt.Or, label2.LabelName), txt.Or, false)
|
||||
assert.NoError(t, err)
|
||||
found1 := false
|
||||
found2 := false
|
||||
if assert.Len(t, labels, 2) {
|
||||
for _, label := range labels {
|
||||
if label.ID == label1.ID {
|
||||
found1 = true
|
||||
} else if label.ID == label2.ID {
|
||||
found2 = true
|
||||
} else {
|
||||
assert.Failf(t, "unable to match", "%+v", label)
|
||||
}
|
||||
}
|
||||
assert.True(t, found1, "Unable to find %+v", label1)
|
||||
assert.True(t, found2, "Unable to find %+v", label2)
|
||||
}
|
||||
})
|
||||
t.Run("SuccessHomophone", func(t *testing.T) {
|
||||
label1 := LabelFixtures.Get("shanghai1")
|
||||
label2 := LabelFixtures.Get("shanghai2")
|
||||
labels, err := FindLabels(label1.LabelName, txt.Or, false)
|
||||
assert.NoError(t, err)
|
||||
found1 := false
|
||||
found2 := false
|
||||
if assert.Len(t, labels, 1) {
|
||||
for _, label := range labels {
|
||||
if label.ID == label1.ID {
|
||||
found1 = true
|
||||
} else if label.ID == label2.ID {
|
||||
found2 = true
|
||||
} else {
|
||||
assert.Failf(t, "unable to match", "%+v", label)
|
||||
}
|
||||
}
|
||||
assert.True(t, found1, "Unable to find %+v", label1)
|
||||
assert.False(t, found2, "Able to find %+v", label2)
|
||||
}
|
||||
})
|
||||
t.Run("SuccessHomophones", func(t *testing.T) {
|
||||
label1 := LabelFixtures.Get("shanghai1")
|
||||
label2 := LabelFixtures.Get("shanghai2")
|
||||
labels, err := FindLabels(fmt.Sprintf("%s%s%s", label1.LabelName, txt.Or, label2.LabelName), txt.Or, false)
|
||||
assert.NoError(t, err)
|
||||
found1 := false
|
||||
found2 := false
|
||||
if assert.Len(t, labels, 2) {
|
||||
for _, label := range labels {
|
||||
if label.ID == label1.ID {
|
||||
found1 = true
|
||||
} else if label.ID == label2.ID {
|
||||
found2 = true
|
||||
} else {
|
||||
assert.Failf(t, "unable to match", "%+v", label)
|
||||
}
|
||||
}
|
||||
assert.True(t, found1, "Unable to find %+v", label1)
|
||||
assert.True(t, found2, "Unable to find %+v", label2)
|
||||
}
|
||||
})
|
||||
t.Run("SuccessHomophoneSlug", func(t *testing.T) {
|
||||
label1 := LabelFixtures.Get("shanghai1")
|
||||
label2 := LabelFixtures.Get("shanghai2")
|
||||
labels, err := FindLabels(label1.LabelSlug, txt.Or, false)
|
||||
assert.NoError(t, err)
|
||||
found1 := false
|
||||
found2 := false
|
||||
if assert.Len(t, labels, 1) {
|
||||
for _, label := range labels {
|
||||
if label.ID == label1.ID {
|
||||
found1 = true
|
||||
} else if label.ID == label2.ID {
|
||||
found2 = true
|
||||
} else {
|
||||
assert.Failf(t, "unable to match", "%+v", label)
|
||||
}
|
||||
}
|
||||
assert.True(t, found1, "Unable to find %+v", label1)
|
||||
assert.False(t, found2, "Able to find %+v", label2)
|
||||
}
|
||||
})
|
||||
t.Run("SuccessHomophoneSlug-A", func(t *testing.T) {
|
||||
label1 := LabelFixtures.Get("shanghai1")
|
||||
label2 := LabelFixtures.Get("shanghai2")
|
||||
labels, err := FindLabels(label2.LabelSlug, txt.Or, false)
|
||||
assert.NoError(t, err)
|
||||
found1 := false
|
||||
found2 := false
|
||||
if assert.Len(t, labels, 1) {
|
||||
for _, label := range labels {
|
||||
if label.ID == label1.ID {
|
||||
found1 = true
|
||||
} else if label.ID == label2.ID {
|
||||
found2 = true
|
||||
} else {
|
||||
assert.Failf(t, "unable to match", "%+v", label)
|
||||
}
|
||||
}
|
||||
assert.False(t, found1, "Able to find %+v", label1)
|
||||
assert.True(t, found2, "Unable to find %+v", label2)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3873,6 +3873,67 @@ var PhotoFixtures = PhotoMap{
|
|||
PhotoStack: 0,
|
||||
PhotoFaces: 0,
|
||||
},
|
||||
"Photo59": { //JPG
|
||||
ID: 1000060,
|
||||
PhotoUID: "ps6sg6byk7wrbk57",
|
||||
TakenAt: time.Date(2024, 11, 12, 9, 7, 18, 0, time.UTC),
|
||||
TakenAtLocal: time.Date(2024, 11, 12, 9, 7, 18, 0, time.UTC),
|
||||
TakenSrc: SrcMeta,
|
||||
PhotoType: "image",
|
||||
TypeSrc: "",
|
||||
PhotoTitle: "Shanghai 2024",
|
||||
TitleSrc: SrcMeta,
|
||||
PhotoCaption: "",
|
||||
CaptionSrc: "",
|
||||
PhotoPath: "2024/11",
|
||||
PhotoName: "2024112_090718_51A301B5",
|
||||
OriginalName: "",
|
||||
PhotoFavorite: false,
|
||||
PhotoPrivate: false,
|
||||
PhotoScan: false,
|
||||
PhotoPanorama: false,
|
||||
TimeZone: "Local",
|
||||
Place: &UnknownPlace,
|
||||
PlaceID: UnknownPlace.ID,
|
||||
PlaceSrc: "",
|
||||
Cell: &UnknownLocation,
|
||||
CellID: UnknownLocation.ID,
|
||||
CellAccuracy: 0,
|
||||
PhotoAltitude: 0,
|
||||
PhotoLat: 31.224361,
|
||||
PhotoLng: 121.469170,
|
||||
PhotoCountry: UnknownPlace.CountryCode(),
|
||||
PhotoYear: 2024,
|
||||
PhotoMonth: 11,
|
||||
PhotoDay: 12,
|
||||
PhotoIso: 0,
|
||||
PhotoExposure: "",
|
||||
PhotoFocalLength: 0,
|
||||
PhotoFNumber: 0,
|
||||
PhotoQuality: 3,
|
||||
PhotoResolution: 0,
|
||||
Camera: &UnknownCamera,
|
||||
CameraID: UnknownCamera.ID,
|
||||
CameraSerial: "",
|
||||
CameraSrc: "",
|
||||
Lens: &UnknownLens,
|
||||
LensID: UnknownLens.ID,
|
||||
Keywords: []Keyword{},
|
||||
Albums: []Album{},
|
||||
Files: []File{},
|
||||
Labels: []PhotoLabel{
|
||||
LabelFixtures.PhotoLabel(1000057, "shanghai1", 38, "image"),
|
||||
LabelFixtures.PhotoLabel(1000057, "shanghai2", 38, "manual"),
|
||||
},
|
||||
CreatedAt: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
UpdatedAt: time.Date(2025, 1, 5, 0, 0, 0, 0, time.UTC),
|
||||
EditedAt: nil,
|
||||
CheckedAt: &checkedTime,
|
||||
DeletedAt: nil,
|
||||
PhotoColor: 14,
|
||||
PhotoStack: 0,
|
||||
PhotoFaces: 0,
|
||||
},
|
||||
}
|
||||
|
||||
// CreatePhotoFixtures inserts known entities into the database for testing.
|
||||
|
|
|
|||
|
|
@ -220,7 +220,7 @@ func TestPhoto_LabelKeywordIndexing(t *testing.T) {
|
|||
|
||||
words := photoKeywordWords(t, photo.ID)
|
||||
assert.Contains(t, words, "flower")
|
||||
assert.Contains(t, words, "cake")
|
||||
assert.Contains(t, words, "kuchen")
|
||||
assert.NotContains(t, words, "cow") // SrcKeyword entries are skipped
|
||||
})
|
||||
t.Run("Optimize", func(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ func TestLabelByUID(t *testing.T) {
|
|||
}
|
||||
|
||||
assert.IsType(t, &entity.Label{}, result)
|
||||
assert.Equal(t, "COW", result.LabelName)
|
||||
assert.Equal(t, "Kuh", result.LabelName)
|
||||
})
|
||||
t.Run("NotFound", func(t *testing.T) {
|
||||
result, err := LabelByUID("111")
|
||||
|
|
|
|||
|
|
@ -61,29 +61,32 @@ func Labels(frm form.SearchLabels) (results []Label, err error) {
|
|||
}
|
||||
|
||||
if frm.Query != "" {
|
||||
var labelIds []uint
|
||||
var labelIDs []uint
|
||||
var categories []entity.Category
|
||||
var label entity.Label
|
||||
var labels []entity.Label
|
||||
|
||||
slugString := txt.Slug(frm.Query)
|
||||
likeString := "%" + frm.Query + "%"
|
||||
|
||||
if result := Db().First(&label, "label_slug = ? OR custom_slug = ?", slugString, slugString); result.Error != nil {
|
||||
if labels, err = entity.FindLabels(frm.Query, txt.Or, false); err != nil {
|
||||
log.Errorf("search: label %s not found with error %s", clean.Log(frm.Query), err)
|
||||
return results, err
|
||||
}
|
||||
if len(labels) == 0 {
|
||||
log.Infof("search: label %s not found", clean.Log(frm.Query))
|
||||
|
||||
s = s.Where("labels.label_name LIKE ?", likeString)
|
||||
} else {
|
||||
labelIds = append(labelIds, label.ID)
|
||||
for _, label := range labels {
|
||||
labelIDs = append(labelIDs, label.ID)
|
||||
|
||||
Db().Where("category_id = ?", label.ID).Find(&categories)
|
||||
Log("find categories", Db().Where("category_id = ?", label.ID).Find(&categories).Error)
|
||||
log.Tracef("search: label %s includes %d categories", txt.LogParamLower(label.LabelName), len(categories))
|
||||
|
||||
for _, category := range categories {
|
||||
labelIds = append(labelIds, category.LabelID)
|
||||
for _, category := range categories {
|
||||
labelIDs = append(labelIDs, category.LabelID)
|
||||
}
|
||||
}
|
||||
|
||||
log.Infof("search: label %s includes %d categories", clean.Log(label.LabelName), len(labelIds))
|
||||
|
||||
s = s.Where("labels.id IN (?)", labelIds)
|
||||
s = s.Where("labels.id IN (?)", labelIDs)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/entity/sortby"
|
||||
|
|
@ -67,6 +68,36 @@ func TestLabels(t *testing.T) {
|
|||
}
|
||||
}
|
||||
})
|
||||
t.Run("SearchForKuh", func(t *testing.T) {
|
||||
query := form.NewLabelSearch("Q:Kuh")
|
||||
query.Count = 1005
|
||||
query.Order = "slug"
|
||||
result, err := Labels(query)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Logf("results: %+v", result)
|
||||
|
||||
assert.LessOrEqual(t, 1, len(result))
|
||||
|
||||
for _, r := range result {
|
||||
assert.IsType(t, Label{}, r)
|
||||
assert.NotEmpty(t, r.ID)
|
||||
assert.NotEmpty(t, r.LabelName)
|
||||
assert.NotEmpty(t, r.LabelSlug)
|
||||
assert.NotEmpty(t, r.CustomSlug)
|
||||
|
||||
if fix, ok := entity.LabelFixtures[r.LabelSlug]; ok {
|
||||
assert.Equal(t, fix.LabelName, r.LabelName)
|
||||
assert.Equal(t, fix.LabelSlug, r.LabelSlug)
|
||||
assert.Equal(t, fix.CustomSlug, r.CustomSlug)
|
||||
} else {
|
||||
assert.Fail(t, "fixture not found by slug")
|
||||
}
|
||||
}
|
||||
})
|
||||
t.Run("SearchForFavorites", func(t *testing.T) {
|
||||
query := form.NewLabelSearch("Favorite:true")
|
||||
query.Count = 15
|
||||
|
|
@ -197,4 +228,112 @@ func TestLabels(t *testing.T) {
|
|||
|
||||
assert.Equal(t, "flower", result[0].LabelSlug)
|
||||
})
|
||||
t.Run("SearchForHomophones", func(t *testing.T) {
|
||||
t.Log("Create Label 老板")
|
||||
label1 := entity.FirstOrCreateLabel(entity.NewLabel("老板", 10))
|
||||
query := form.NewLabelSearch("q:老板")
|
||||
query.Count = 5
|
||||
query.Order = "slug"
|
||||
result, err := Labels(query)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Logf("results for search query q:老板: %+v", result)
|
||||
|
||||
if assert.Len(t, result, 1) {
|
||||
assert.Equal(t, label1.LabelName, result[0].LabelName)
|
||||
}
|
||||
|
||||
t.Log("Create Label 老伴")
|
||||
label2 := entity.FirstOrCreateLabel(entity.NewLabel("老伴", 10))
|
||||
|
||||
assert.NotEqual(t, label1.ID, label2.ID)
|
||||
|
||||
query = form.NewLabelSearch("q:老板")
|
||||
query.Count = 5
|
||||
query.Order = "slug"
|
||||
result, err = Labels(query)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Logf("results for search query q:老板: %+v", result)
|
||||
|
||||
if assert.Len(t, result, 1) {
|
||||
assert.Equal(t, label1.LabelName, result[0].LabelName)
|
||||
}
|
||||
|
||||
query = form.NewLabelSearch("q:老伴")
|
||||
query.Count = 5
|
||||
query.Order = "slug"
|
||||
result, err = Labels(query)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Logf("results for search query q:老伴: %+v", result)
|
||||
|
||||
if assert.Len(t, result, 1) {
|
||||
assert.Equal(t, label2.LabelName, result[0].LabelName)
|
||||
}
|
||||
|
||||
query = form.NewLabelSearch("q:lao-ban")
|
||||
query.Count = 5
|
||||
query.Order = "slug"
|
||||
result, err = Labels(query)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Logf("results for search query q:lao-ban: %+v", result)
|
||||
|
||||
if assert.Len(t, result, 1) {
|
||||
assert.Equal(t, label1.LabelName, result[0].LabelName)
|
||||
}
|
||||
|
||||
t.Log("Rename Label 老板 to 老板renamed")
|
||||
|
||||
updFrm := &form.Label{LabelName: "老板renamed"}
|
||||
require.NoError(t, label1.SaveForm(updFrm))
|
||||
|
||||
query = form.NewLabelSearch("q:老板renamed")
|
||||
query.Count = 5
|
||||
query.Order = "slug"
|
||||
result, err = Labels(query)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Logf("results for search query q:老板renamed: %+v", result)
|
||||
|
||||
if assert.Len(t, result, 1) {
|
||||
assert.Equal(t, label1.LabelName, result[0].LabelName)
|
||||
}
|
||||
|
||||
query = form.NewLabelSearch("q:lao-ban-renamed")
|
||||
query.Count = 5
|
||||
query.Order = "slug"
|
||||
result, err = Labels(query)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Logf("results for search query q:lao-ban-renamed: %+v", result)
|
||||
|
||||
if assert.Len(t, result, 1) {
|
||||
assert.Equal(t, label1.LabelName, result[0].LabelName)
|
||||
}
|
||||
|
||||
label1.Delete()
|
||||
entity.UnscopedDb().Delete(label1)
|
||||
label2.Delete()
|
||||
entity.UnscopedDb().Delete(label2)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -287,16 +287,11 @@ func searchPhotos(frm form.SearchPhotos, sess *entity.Session, resultCols string
|
|||
// Filter by label, label category and keywords.
|
||||
if txt.NotEmpty(frm.Label) {
|
||||
var categories []entity.Category
|
||||
var labels []entity.Label
|
||||
var labelIds []uint
|
||||
|
||||
if labelErr := Db().Where(AnySlug("label_slug", frm.Label, txt.Or)).Or(AnySlug("custom_slug", frm.Label, txt.Or)).Find(&labels).Error; len(labels) == 0 || labelErr != nil {
|
||||
log.Debugf("search: label %s not found", txt.LogParamLower(frm.Label))
|
||||
return PhotoResults{}, 0, nil
|
||||
} else {
|
||||
if labels, labelErr := entity.FindLabels(frm.Label, txt.Or, false); len(labels) != 0 && labelErr == nil {
|
||||
for _, l := range labels {
|
||||
labelIds = append(labelIds, l.ID)
|
||||
|
||||
Log("find categories", Db().Where("category_id = ?", l.ID).Find(&categories).Error)
|
||||
log.Debugf("search: label %s includes %d categories", txt.LogParamLower(l.LabelName), len(categories))
|
||||
|
||||
|
|
@ -307,6 +302,9 @@ func searchPhotos(frm form.SearchPhotos, sess *entity.Session, resultCols string
|
|||
|
||||
s = s.Joins("JOIN photos_labels ON photos_labels.photo_id = files.photo_id AND photos_labels.uncertainty < 100 AND photos_labels.label_id IN (?)", labelIds).
|
||||
Group("photos.id, files.id")
|
||||
} else {
|
||||
log.Debugf("search: label %s not found", txt.LogParamLower(frm.Label))
|
||||
return PhotoResults{}, 0, nil
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -397,10 +395,9 @@ func searchPhotos(frm form.SearchPhotos, sess *entity.Session, resultCols string
|
|||
// Filter by query string.
|
||||
if frm.Query != "" {
|
||||
var categories []entity.Category
|
||||
var labels []entity.Label
|
||||
var labelIds []uint
|
||||
|
||||
if labelsErr := Db().Where(AnySlug("custom_slug", frm.Query, " ")).Find(&labels).Error; len(labels) == 0 || labelsErr != nil {
|
||||
if labels, labelsErr := entity.FindLabels(frm.Query, " ", false); len(labels) == 0 || labelsErr != nil {
|
||||
log.Tracef("search: label %s not found, using fuzzy search", txt.LogParamLower(frm.Query))
|
||||
|
||||
for _, where := range LikeAnyKeyword("k.keyword", frm.Query) {
|
||||
|
|
@ -410,8 +407,7 @@ func searchPhotos(frm form.SearchPhotos, sess *entity.Session, resultCols string
|
|||
for _, l := range labels {
|
||||
labelIds = append(labelIds, l.ID)
|
||||
|
||||
Db().Where("category_id = ?", l.ID).Find(&categories)
|
||||
|
||||
Log("find categories", Db().Where("category_id = ?", l.ID).Find(&categories).Error)
|
||||
log.Tracef("search: label %s includes %d categories", txt.LogParamLower(l.LabelName), len(categories))
|
||||
|
||||
for _, category := range categories {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,9 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
)
|
||||
|
||||
|
|
@ -724,4 +726,96 @@ func TestPhotosQueryLabel(t *testing.T) {
|
|||
}
|
||||
assert.Len(t, photos, 2)
|
||||
})
|
||||
t.Run("Homophones", func(t *testing.T) {
|
||||
var f form.SearchPhotos
|
||||
|
||||
f.Query = `label:"上海"`
|
||||
f.Merged = true
|
||||
|
||||
photos, _, err := Photos(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Len(t, photos, 1)
|
||||
|
||||
f.Query = `label:"伤害"`
|
||||
f.Merged = true
|
||||
|
||||
photos, _, err = Photos(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Len(t, photos, 1)
|
||||
|
||||
f.Query = `label:shang-hai`
|
||||
f.Merged = true
|
||||
|
||||
photos, _, err = Photos(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Len(t, photos, 1)
|
||||
|
||||
// Rename the label
|
||||
label1, err := entity.FindLabel("上海", true)
|
||||
require.NoError(t, err)
|
||||
updFrm := &form.Label{LabelName: "上海renamed"}
|
||||
require.NoError(t, label1.SaveForm(updFrm))
|
||||
|
||||
f.Query = `label:"上海"`
|
||||
f.Merged = true
|
||||
|
||||
photos, _, err = Photos(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Len(t, photos, 0)
|
||||
|
||||
f.Query = `label:上海renamed`
|
||||
f.Merged = true
|
||||
|
||||
photos, _, err = Photos(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Len(t, photos, 1)
|
||||
|
||||
f.Query = `label:shang-hai`
|
||||
f.Merged = true
|
||||
|
||||
photos, _, err = Photos(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Len(t, photos, 1)
|
||||
|
||||
f.Query = `label:shang-hai-renamed`
|
||||
f.Merged = true
|
||||
|
||||
photos, _, err = Photos(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Len(t, photos, 1)
|
||||
|
||||
updFrm = &form.Label{LabelName: "上海"}
|
||||
require.NoError(t, label1.SaveForm(updFrm))
|
||||
|
||||
f.Query = `label:"上海"`
|
||||
f.Merged = true
|
||||
|
||||
photos, _, err = Photos(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Len(t, photos, 1)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
)
|
||||
|
|
@ -260,7 +261,7 @@ func TestPhotosQueryPortrait(t *testing.T) {
|
|||
|
||||
photos, _, err := Photos(f)
|
||||
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, portraits, len(photos))
|
||||
})
|
||||
t.Run("OrSearch", func(t *testing.T) {
|
||||
|
|
@ -283,7 +284,7 @@ func TestPhotosQueryPortrait(t *testing.T) {
|
|||
photos, _, err := Photos(f)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 8, len(photos))
|
||||
assert.Len(t, photos, 9)
|
||||
})
|
||||
t.Run("Square", func(t *testing.T) {
|
||||
var f form.SearchPhotos
|
||||
|
|
|
|||
|
|
@ -209,13 +209,9 @@ func UserPhotosGeo(frm form.SearchPhotosGeo, sess *entity.Session) (results GeoR
|
|||
// Filter by label, label category and keywords.
|
||||
if txt.NotEmpty(frm.Label) {
|
||||
var categories []entity.Category
|
||||
var labels []entity.Label
|
||||
var labelIds []uint
|
||||
|
||||
if labelErr := Db().Where(AnySlug("label_slug", frm.Label, txt.Or)).Or(AnySlug("custom_slug", frm.Label, txt.Or)).Find(&labels).Error; len(labels) == 0 || labelErr != nil {
|
||||
log.Debugf("search: label %s not found", txt.LogParamLower(frm.Label))
|
||||
return GeoResults{}, nil
|
||||
} else {
|
||||
if labels, labelsErr := entity.FindLabels(frm.Label, txt.Or, false); len(labels) != 0 && labelsErr == nil {
|
||||
for _, l := range labels {
|
||||
labelIds = append(labelIds, l.ID)
|
||||
|
||||
|
|
@ -229,6 +225,9 @@ func UserPhotosGeo(frm form.SearchPhotosGeo, sess *entity.Session) (results GeoR
|
|||
|
||||
s = s.Joins("JOIN photos_labels ON photos_labels.photo_id = files.photo_id AND photos_labels.uncertainty < 100 AND photos_labels.label_id IN (?)", labelIds).
|
||||
Group("photos.id, files.id")
|
||||
} else {
|
||||
log.Debugf("search: label %s not found with error %+v", txt.LogParamLower(frm.Label), labelsErr)
|
||||
return GeoResults{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -309,10 +308,9 @@ func UserPhotosGeo(frm form.SearchPhotosGeo, sess *entity.Session) (results GeoR
|
|||
// Filter by label, label category, and keywords.
|
||||
if frm.Query != "" {
|
||||
var categories []entity.Category
|
||||
var labels []entity.Label
|
||||
var labelIds []uint
|
||||
|
||||
if labelsErr := Db().Where(AnySlug("custom_slug", frm.Query, " ")).Find(&labels).Error; len(labels) == 0 || labelsErr != nil {
|
||||
if labels, labelsErr := entity.FindLabels(frm.Query, " ", false); len(labels) == 0 || labelsErr != nil {
|
||||
log.Tracef("search: label %s not found, using fuzzy search", txt.LogParamLower(frm.Query))
|
||||
|
||||
for _, where := range LikeAnyKeyword("k.keyword", frm.Query) {
|
||||
|
|
|
|||
|
|
@ -134,6 +134,6 @@ func TestPhotosGeoFilterTime(t *testing.T) {
|
|||
|
||||
// t.Logf("After: %#v", photos)
|
||||
|
||||
assert.GreaterOrEqual(t, 4, len(photos))
|
||||
assert.Len(t, photos, 5)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package search
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
|
|
@ -1194,4 +1195,234 @@ func TestGeo(t *testing.T) {
|
|||
assert.Empty(t, r.PhotoTitle)
|
||||
}
|
||||
})
|
||||
t.Run("query:label:ladder", func(t *testing.T) {
|
||||
var f form.SearchPhotosGeo
|
||||
|
||||
f.Query = "label:ladder"
|
||||
|
||||
photos, err := PhotosGeo(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Len(t, photos, 4)
|
||||
|
||||
found51 := false
|
||||
for _, r := range photos {
|
||||
assert.IsType(t, GeoResult{}, r)
|
||||
if fmt.Sprintf("%d", entity.PhotoFixtures.Get("photo51 ").ID) == r.ID {
|
||||
found51 = true
|
||||
}
|
||||
}
|
||||
assert.True(t, found51, "unable to find photo51 ")
|
||||
})
|
||||
t.Run("label:ladder\"", func(t *testing.T) {
|
||||
var f form.SearchPhotosGeo
|
||||
|
||||
f.Label = "ladder\""
|
||||
|
||||
photos, err := PhotosGeo(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Len(t, photos, 4)
|
||||
|
||||
found51 := false
|
||||
for _, r := range photos {
|
||||
assert.IsType(t, GeoResult{}, r)
|
||||
if fmt.Sprintf("%d", entity.PhotoFixtures.Get("photo51 ").ID) == r.ID {
|
||||
found51 = true
|
||||
}
|
||||
}
|
||||
assert.True(t, found51, "unable to find Photo51")
|
||||
})
|
||||
t.Run("query: label:上海", func(t *testing.T) {
|
||||
var f form.SearchPhotosGeo
|
||||
|
||||
f.Query = "label:上海"
|
||||
|
||||
photos, err := PhotosGeo(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Len(t, photos, 1)
|
||||
|
||||
for _, r := range photos {
|
||||
assert.IsType(t, GeoResult{}, r)
|
||||
assert.Equal(t, fmt.Sprintf("%d", entity.PhotoFixtures.Get("Photo59").ID), r.ID)
|
||||
}
|
||||
})
|
||||
t.Run("label:上海", func(t *testing.T) {
|
||||
var f form.SearchPhotosGeo
|
||||
|
||||
f.Label = "上海"
|
||||
|
||||
photos, err := PhotosGeo(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Len(t, photos, 1)
|
||||
|
||||
for _, r := range photos {
|
||||
assert.IsType(t, GeoResult{}, r)
|
||||
assert.Equal(t, fmt.Sprintf("%d", entity.PhotoFixtures.Get("Photo59").ID), r.ID)
|
||||
}
|
||||
})
|
||||
t.Run("query: label:伤害", func(t *testing.T) {
|
||||
var f form.SearchPhotosGeo
|
||||
|
||||
f.Query = "label:伤害"
|
||||
|
||||
photos, err := PhotosGeo(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Len(t, photos, 1)
|
||||
|
||||
for _, r := range photos {
|
||||
assert.IsType(t, GeoResult{}, r)
|
||||
assert.Equal(t, fmt.Sprintf("%d", entity.PhotoFixtures.Get("Photo59").ID), r.ID)
|
||||
}
|
||||
})
|
||||
t.Run("label:伤害", func(t *testing.T) {
|
||||
var f form.SearchPhotosGeo
|
||||
|
||||
f.Label = "伤害"
|
||||
|
||||
photos, err := PhotosGeo(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Len(t, photos, 1)
|
||||
|
||||
for _, r := range photos {
|
||||
assert.IsType(t, GeoResult{}, r)
|
||||
assert.Equal(t, fmt.Sprintf("%d", entity.PhotoFixtures.Get("Photo59").ID), r.ID)
|
||||
}
|
||||
})
|
||||
t.Run("query: label:Shang Hai", func(t *testing.T) {
|
||||
var f form.SearchPhotosGeo
|
||||
|
||||
f.Query = "label:Shang Hai"
|
||||
|
||||
photos, err := PhotosGeo(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Len(t, photos, 0)
|
||||
})
|
||||
t.Run("label:Shang Hai", func(t *testing.T) {
|
||||
var f form.SearchPhotosGeo
|
||||
|
||||
f.Label = "Shang Hai" // homophone in latin char set
|
||||
|
||||
photos, err := PhotosGeo(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Len(t, photos, 0)
|
||||
})
|
||||
t.Run("query: label:shang-hai", func(t *testing.T) {
|
||||
var f form.SearchPhotosGeo
|
||||
|
||||
f.Query = "label:shang-hai"
|
||||
|
||||
photos, err := PhotosGeo(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Len(t, photos, 1)
|
||||
for _, r := range photos {
|
||||
assert.IsType(t, GeoResult{}, r)
|
||||
assert.Equal(t, fmt.Sprintf("%d", entity.PhotoFixtures.Get("Photo59").ID), r.ID)
|
||||
}
|
||||
})
|
||||
t.Run("label:shang-hai", func(t *testing.T) {
|
||||
var f form.SearchPhotosGeo
|
||||
|
||||
f.Label = "shang-hai" // homophone in latin char set
|
||||
|
||||
photos, err := PhotosGeo(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Len(t, photos, 1)
|
||||
for _, r := range photos {
|
||||
assert.IsType(t, GeoResult{}, r)
|
||||
assert.Equal(t, fmt.Sprintf("%d", entity.PhotoFixtures.Get("Photo59").ID), r.ID)
|
||||
}
|
||||
})
|
||||
t.Run("query: label:shang-hai or 伤害", func(t *testing.T) {
|
||||
var f form.SearchPhotosGeo
|
||||
|
||||
f.Query = "label:shang-hai|伤害"
|
||||
|
||||
photos, err := PhotosGeo(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Len(t, photos, 1)
|
||||
|
||||
for _, r := range photos {
|
||||
assert.IsType(t, GeoResult{}, r)
|
||||
assert.Equal(t, fmt.Sprintf("%d", entity.PhotoFixtures.Get("Photo59").ID), r.ID)
|
||||
}
|
||||
})
|
||||
t.Run("label:shang-hai or 伤害", func(t *testing.T) {
|
||||
var f form.SearchPhotosGeo
|
||||
|
||||
f.Label = "shang-hai|伤害"
|
||||
|
||||
photos, err := PhotosGeo(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Len(t, photos, 1)
|
||||
|
||||
for _, r := range photos {
|
||||
assert.IsType(t, GeoResult{}, r)
|
||||
assert.Equal(t, fmt.Sprintf("%d", entity.PhotoFixtures.Get("Photo59").ID), r.ID)
|
||||
}
|
||||
})
|
||||
t.Run("query: shang-hai or 伤害", func(t *testing.T) {
|
||||
var f form.SearchPhotosGeo
|
||||
|
||||
f.Query = "shang-hai 伤害"
|
||||
|
||||
photos, err := PhotosGeo(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Len(t, photos, 1)
|
||||
|
||||
for _, r := range photos {
|
||||
assert.IsType(t, GeoResult{}, r)
|
||||
assert.Equal(t, fmt.Sprintf("%d", entity.PhotoFixtures.Get("Photo59").ID), r.ID)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2433,4 +2433,184 @@ func TestPhotos(t *testing.T) {
|
|||
assert.NotEmpty(t, p.PhotoName)
|
||||
}
|
||||
})
|
||||
t.Run("query: label:no-jpeg", func(t *testing.T) {
|
||||
var f form.SearchPhotos
|
||||
|
||||
f.Query = "label:no-jpeg"
|
||||
|
||||
photos, _, err := Photos(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Len(t, photos, 1)
|
||||
|
||||
for _, r := range photos {
|
||||
assert.IsType(t, Photo{}, r)
|
||||
assert.Equal(t, entity.PhotoFixtures.Get("Photo01").ID, r.ID)
|
||||
}
|
||||
})
|
||||
t.Run("label:No Jpeg", func(t *testing.T) {
|
||||
var f form.SearchPhotos
|
||||
|
||||
f.Label = "No Jpeg"
|
||||
|
||||
photos, _, err := Photos(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Len(t, photos, 1)
|
||||
|
||||
for _, r := range photos {
|
||||
assert.IsType(t, Photo{}, r)
|
||||
assert.Equal(t, entity.PhotoFixtures.Get("Photo01").ID, r.ID)
|
||||
}
|
||||
})
|
||||
t.Run("query: label:上海", func(t *testing.T) {
|
||||
var f form.SearchPhotos
|
||||
|
||||
f.Query = "label:上海"
|
||||
|
||||
photos, _, err := Photos(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Len(t, photos, 1)
|
||||
|
||||
for _, r := range photos {
|
||||
assert.IsType(t, Photo{}, r)
|
||||
assert.Equal(t, entity.PhotoFixtures.Get("Photo59").ID, r.ID)
|
||||
}
|
||||
})
|
||||
t.Run("label:上海", func(t *testing.T) {
|
||||
var f form.SearchPhotos
|
||||
|
||||
f.Label = "上海"
|
||||
|
||||
photos, _, err := Photos(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Len(t, photos, 1)
|
||||
|
||||
for _, r := range photos {
|
||||
assert.IsType(t, Photo{}, r)
|
||||
assert.Equal(t, entity.PhotoFixtures.Get("Photo59").ID, r.ID)
|
||||
}
|
||||
})
|
||||
t.Run("query: label:伤害", func(t *testing.T) {
|
||||
var f form.SearchPhotos
|
||||
|
||||
f.Query = "label:伤害"
|
||||
|
||||
photos, _, err := Photos(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Len(t, photos, 1)
|
||||
|
||||
for _, r := range photos {
|
||||
assert.IsType(t, Photo{}, r)
|
||||
assert.Equal(t, entity.PhotoFixtures.Get("Photo59").ID, r.ID)
|
||||
}
|
||||
})
|
||||
t.Run("label:伤害", func(t *testing.T) {
|
||||
var f form.SearchPhotos
|
||||
|
||||
f.Label = "伤害"
|
||||
|
||||
photos, _, err := Photos(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Len(t, photos, 1)
|
||||
|
||||
for _, r := range photos {
|
||||
assert.IsType(t, Photo{}, r)
|
||||
assert.Equal(t, entity.PhotoFixtures.Get("Photo59").ID, r.ID)
|
||||
}
|
||||
})
|
||||
t.Run("query: label:shang-hai", func(t *testing.T) {
|
||||
var f form.SearchPhotos
|
||||
|
||||
f.Query = "label:shang-hai" // homophone in latin char set
|
||||
|
||||
photos, _, err := Photos(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Len(t, photos, 1)
|
||||
|
||||
for _, r := range photos {
|
||||
assert.IsType(t, Photo{}, r)
|
||||
assert.Equal(t, entity.PhotoFixtures.Get("Photo59").ID, r.ID)
|
||||
}
|
||||
})
|
||||
t.Run("label:shang-hai", func(t *testing.T) {
|
||||
var f form.SearchPhotos
|
||||
|
||||
f.Label = "shang-hai-c-a" // homophone in latin char set
|
||||
|
||||
photos, _, err := Photos(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Len(t, photos, 1)
|
||||
|
||||
for _, r := range photos {
|
||||
assert.IsType(t, Photo{}, r)
|
||||
assert.Equal(t, entity.PhotoFixtures.Get("Photo59").ID, r.ID)
|
||||
}
|
||||
})
|
||||
t.Run("query: label:shang-hai or 伤害", func(t *testing.T) {
|
||||
var f form.SearchPhotos
|
||||
|
||||
f.Query = "label:shang-hai|伤害"
|
||||
|
||||
photos, _, err := Photos(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Len(t, photos, 1)
|
||||
|
||||
for _, r := range photos {
|
||||
assert.IsType(t, Photo{}, r)
|
||||
assert.Equal(t, entity.PhotoFixtures.Get("Photo59").ID, r.ID)
|
||||
}
|
||||
})
|
||||
t.Run("label:shang-hai or 伤害", func(t *testing.T) {
|
||||
var f form.SearchPhotos
|
||||
|
||||
f.Label = "shang-hai|伤害"
|
||||
|
||||
photos, _, err := Photos(f)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Len(t, photos, 1)
|
||||
|
||||
for _, r := range photos {
|
||||
assert.IsType(t, Photo{}, r)
|
||||
assert.Equal(t, entity.PhotoFixtures.Get("Photo59").ID, r.ID)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
38
pkg/clean/label.go
Normal file
38
pkg/clean/label.go
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
package clean
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
|
||||
"github.com/gosimple/slug"
|
||||
)
|
||||
|
||||
// LabelName cleans a string so that it can be compared against other LabelNames to ensure
|
||||
// that LabelSlug clashes are only created appropriately
|
||||
func LabelName(s string) string {
|
||||
s = strings.ReplaceAll(s, "-", " ")
|
||||
s = strings.ReplaceAll(s, "@", "at")
|
||||
s = strings.ReplaceAll(s, "&", "and")
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" || s == "-" {
|
||||
return s
|
||||
}
|
||||
if s[0] == txt.SlugEncoded && txt.ContainsAlnumLower(s[1:]) {
|
||||
return txt.Clip(s, txt.ClipSlug)
|
||||
}
|
||||
|
||||
if slug.Make(s) == "" {
|
||||
return s
|
||||
}
|
||||
|
||||
cleanLabel := strings.Map(func(r rune) rune {
|
||||
if unicode.IsDigit(r) || unicode.IsLetter(r) || unicode.IsSpace(r) {
|
||||
return unicode.ToLower(r)
|
||||
}
|
||||
return -1
|
||||
}, s)
|
||||
|
||||
return cleanLabel
|
||||
}
|
||||
48
pkg/clean/label_test.go
Normal file
48
pkg/clean/label_test.go
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
package clean
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestLabelName(t *testing.T) {
|
||||
t.Run("Empty", func(t *testing.T) {
|
||||
assert.Equal(t, "", LabelName(""))
|
||||
})
|
||||
t.Run("Fire Station", func(t *testing.T) {
|
||||
assert.Equal(t, "fire station", LabelName("Fire Station"))
|
||||
assert.Equal(t, "fire station", LabelName("Fire' Station"))
|
||||
assert.Equal(t, "fire station", LabelName("😀Fire Station"))
|
||||
assert.Equal(t, "fire station", LabelName("Fire Station◨"))
|
||||
assert.Equal(t, "fire station", LabelName("Fire-Station"))
|
||||
})
|
||||
t.Run("ABC", func(t *testing.T) {
|
||||
assert.Equal(t, "abc", LabelName("Abc"))
|
||||
assert.Equal(t, "abc", LabelName("abc."))
|
||||
assert.Equal(t, "abc", LabelName(".abc."))
|
||||
assert.Equal(t, "abc", LabelName("😀ABC "))
|
||||
assert.Equal(t, "abc", LabelName("aB◨c"))
|
||||
assert.Equal(t, "abc", LabelName("abc-"))
|
||||
})
|
||||
t.Run("Unicode", func(t *testing.T) {
|
||||
assert.Equal(t, "送钟", LabelName("送钟"))
|
||||
assert.Equal(t, "送终", LabelName("送终"))
|
||||
assert.Equal(t, "送 终", LabelName("送-终"))
|
||||
assert.Equal(t, "送 终", LabelName(" 送 终 "))
|
||||
assert.Equal(t, "送终", LabelName("-送终"))
|
||||
})
|
||||
t.Run("Mixed", func(t *testing.T) {
|
||||
assert.Equal(t, "送钟abc", LabelName("送钟AbC"))
|
||||
assert.Equal(t, "даbc", LabelName("ДаBc"))
|
||||
assert.Equal(t, "так", LabelName("ТАК"))
|
||||
})
|
||||
t.Run("Graphic", func(t *testing.T) {
|
||||
assert.Equal(t, "😀", LabelName("😀"))
|
||||
assert.Equal(t, "◨", LabelName("◨"))
|
||||
assert.Equal(t, "😀◨😀", LabelName("😀◨😀"))
|
||||
assert.Equal(t, "😀◨😀", LabelName(" 😀◨😀 "))
|
||||
assert.Equal(t, "😀 ◨😀", LabelName(" 😀-◨😀 "))
|
||||
assert.Equal(t, "😀◨ 😀", LabelName(" 😀◨ 😀 "))
|
||||
})
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue