Tag aliases (#1412)

* Add Tag Update/UpdateFull
* Tag alias implementation
* Refactor tag page
* Add aliases in UI
* Include tag aliases in q filter
* Include aliases in tag select
* Add aliases to auto-tagger
* Use aliases in scraper
* Add tag aliases for filename parser
This commit is contained in:
WithoutPants
2021-05-26 14:36:05 +10:00
committed by GitHub
parent 9b57fbbf50
commit c70faa2a53
48 changed files with 1303 additions and 315 deletions

View File

@@ -479,3 +479,28 @@ func (m *countCriterionHandlerBuilder) handler(criterion *models.IntCriterionInp
}
}
}
// handler for StringCriterion for string list fields
type stringListCriterionHandlerBuilder struct {
// table joining primary and foreign objects
joinTable string
// string field on the join table
stringColumn string
addJoinTable func(f *filterBuilder)
}
func (m *stringListCriterionHandlerBuilder) handler(criterion *models.StringCriterionInput) criterionHandlerFunc {
return func(f *filterBuilder) {
if criterion != nil && len(criterion.Value) > 0 {
var args []interface{}
for _, tagID := range criterion.Value {
args = append(args, tagID)
}
m.addJoinTable(f)
stringCriterionHandler(criterion, m.joinTable+"."+m.stringColumn)(f)
}
}
}

View File

@@ -343,6 +343,45 @@ func (r *imageRepository) replace(id int, image []byte) error {
return err
}
type stringRepository struct {
repository
stringColumn string
}
func (r *stringRepository) get(id int) ([]string, error) {
query := fmt.Sprintf("SELECT %s from %s WHERE %s = ?", r.stringColumn, r.tableName, r.idColumn)
var ret []string
err := r.queryFunc(query, []interface{}{id}, func(rows *sqlx.Rows) error {
var out string
if err := rows.Scan(&out); err != nil {
return err
}
ret = append(ret, out)
return nil
})
return ret, err
}
func (r *stringRepository) insert(id int, s string) (sql.Result, error) {
stmt := fmt.Sprintf("INSERT INTO %s (%s, %s) VALUES (?, ?)", r.tableName, r.idColumn, r.stringColumn)
return r.tx.Exec(stmt, id, s)
}
func (r *stringRepository) replace(id int, newStrings []string) error {
if err := r.destroy([]int{id}); err != nil {
return err
}
for _, s := range newStrings {
if _, err := r.insert(id, s); err != nil {
return err
}
}
return nil
}
type stashIDRepository struct {
repository
}

View File

@@ -852,6 +852,12 @@ func createTags(tqb models.TagReaderWriter, n int, o int) error {
return fmt.Errorf("Error creating tag %v+: %s", tag, err.Error())
}
// add alias
alias := getTagStringValue(i, "Alias")
if err := tqb.UpdateAliases(created.ID, []string{alias}); err != nil {
return fmt.Errorf("error setting tag alias: %s", err.Error())
}
tagIDs = append(tagIDs, created.ID)
tagNames = append(tagNames, created.Name)
}

View File

@@ -11,6 +11,8 @@ import (
const tagTable = "tags"
const tagIDColumn = "tag_id"
const tagAliasesTable = "tag_aliases"
const tagAliasColumn = "alias"
type tagQueryBuilder struct {
repository
@@ -35,7 +37,16 @@ func (qb *tagQueryBuilder) Create(newObject models.Tag) (*models.Tag, error) {
return &ret, nil
}
func (qb *tagQueryBuilder) Update(updatedObject models.Tag) (*models.Tag, error) {
func (qb *tagQueryBuilder) Update(updatedObject models.TagPartial) (*models.Tag, error) {
const partial = true
if err := qb.update(updatedObject.ID, updatedObject, partial); err != nil {
return nil, err
}
return qb.Find(updatedObject.ID)
}
func (qb *tagQueryBuilder) UpdateFull(updatedObject models.Tag) (*models.Tag, error) {
const partial = false
if err := qb.update(updatedObject.ID, updatedObject, partial); err != nil {
return nil, err
@@ -197,13 +208,19 @@ func (qb *tagQueryBuilder) QueryForAutoTag(words []string) ([]*models.Tag, error
// TODO - Query needs to be changed to support queries of this type, and
// this method should be removed
query := selectAll(tagTable)
query += " LEFT JOIN tag_aliases ON tag_aliases.tag_id = tags.id"
var whereClauses []string
var args []interface{}
for _, w := range words {
whereClauses = append(whereClauses, "name like ?")
args = append(args, "%"+w+"%")
ww := "%" + w + "%"
whereClauses = append(whereClauses, "tags.name like ?")
args = append(args, ww)
// include aliases
whereClauses = append(whereClauses, "tag_aliases.alias like ?")
args = append(args, ww)
}
where := strings.Join(whereClauses, " OR ")
@@ -262,6 +279,9 @@ func (qb *tagQueryBuilder) makeFilter(tagFilter *models.TagFilterType) *filterBu
// }
// }
query.handleCriterionFunc(stringCriterionHandler(tagFilter.Name, tagTable+".name"))
query.handleCriterionFunc(tagAliasCriterionHandler(qb, tagFilter.Aliases))
query.handleCriterionFunc(tagIsMissingCriterionHandler(qb, tagFilter.IsMissing))
query.handleCriterionFunc(tagSceneCountCriterionHandler(qb, tagFilter.SceneCount))
query.handleCriterionFunc(tagImageCountCriterionHandler(qb, tagFilter.ImageCount))
@@ -297,7 +317,8 @@ func (qb *tagQueryBuilder) Query(tagFilter *models.TagFilterType, findFilter *mo
// Disabling querying/sorting on marker count for now.
if q := findFilter.Q; q != nil && *q != "" {
searchColumns := []string{"tags.name"}
query.join(tagAliasesTable, "", "tag_aliases.tag_id = tags.id")
searchColumns := []string{"tags.name", "tag_aliases.alias"}
clause, thisArgs := getSearchBinding(searchColumns, *q, false)
query.addWhere(clause)
query.addArg(thisArgs...)
@@ -328,6 +349,18 @@ func (qb *tagQueryBuilder) Query(tagFilter *models.TagFilterType, findFilter *mo
return tags, countResult, nil
}
func tagAliasCriterionHandler(qb *tagQueryBuilder, alias *models.StringCriterionInput) criterionHandlerFunc {
h := stringListCriterionHandlerBuilder{
joinTable: tagAliasesTable,
stringColumn: tagAliasColumn,
addJoinTable: func(f *filterBuilder) {
qb.aliasRepository().join(f, "", "tags.id")
},
}
return h.handler(alias)
}
func tagIsMissingCriterionHandler(qb *tagQueryBuilder, isMissing *string) criterionHandlerFunc {
return func(f *filterBuilder) {
if isMissing != nil && *isMissing != "" {
@@ -484,3 +517,22 @@ func (qb *tagQueryBuilder) UpdateImage(tagID int, image []byte) error {
func (qb *tagQueryBuilder) DestroyImage(tagID int) error {
return qb.imageRepository().destroy([]int{tagID})
}
func (qb *tagQueryBuilder) aliasRepository() *stringRepository {
return &stringRepository{
repository: repository{
tx: qb.tx,
tableName: tagAliasesTable,
idColumn: tagIDColumn,
},
stringColumn: tagAliasColumn,
}
}
func (qb *tagQueryBuilder) GetAliases(tagID int) ([]string, error) {
return qb.aliasRepository().get(tagID)
}
func (qb *tagQueryBuilder) UpdateAliases(tagID int, aliases []string) error {
return qb.aliasRepository().replace(tagID, aliases)
}

View File

@@ -83,8 +83,20 @@ func TestTagQueryForAutoTag(t *testing.T) {
}
assert.Len(t, tags, 2)
assert.Equal(t, strings.ToLower(tagNames[tagIdxWithScene]), strings.ToLower(tags[0].Name))
assert.Equal(t, strings.ToLower(tagNames[tagIdxWithScene]), strings.ToLower(tags[1].Name))
lcName := tagNames[tagIdxWithScene]
assert.Equal(t, strings.ToLower(lcName), strings.ToLower(tags[0].Name))
assert.Equal(t, strings.ToLower(lcName), strings.ToLower(tags[1].Name))
// find by alias
name = getTagStringValue(tagIdxWithScene, "Alias")
tags, err = tqb.QueryForAutoTag([]string{name})
if err != nil {
t.Errorf("Error finding tags: %s", err.Error())
}
assert.Len(t, tags, 1)
assert.Equal(t, tagIDs[tagIdxWithScene], tags[0].ID)
return nil
})
@@ -137,6 +149,100 @@ func TestTagFindByNames(t *testing.T) {
})
}
func TestTagQueryName(t *testing.T) {
const tagIdx = 1
tagName := getSceneStringValue(tagIdx, "Name")
nameCriterion := &models.StringCriterionInput{
Value: tagName,
Modifier: models.CriterionModifierEquals,
}
tagFilter := &models.TagFilterType{
Name: nameCriterion,
}
verifyFn := func(tag *models.Tag, r models.Repository) {
verifyString(t, tag.Name, *nameCriterion)
}
verifyTagQuery(t, tagFilter, nil, verifyFn)
nameCriterion.Modifier = models.CriterionModifierNotEquals
verifyTagQuery(t, tagFilter, nil, verifyFn)
nameCriterion.Modifier = models.CriterionModifierMatchesRegex
nameCriterion.Value = "tag_.*1_Name"
verifyTagQuery(t, tagFilter, nil, verifyFn)
nameCriterion.Modifier = models.CriterionModifierNotMatchesRegex
verifyTagQuery(t, tagFilter, nil, verifyFn)
}
func TestTagQueryAlias(t *testing.T) {
const tagIdx = 1
tagName := getSceneStringValue(tagIdx, "Alias")
aliasCriterion := &models.StringCriterionInput{
Value: tagName,
Modifier: models.CriterionModifierEquals,
}
tagFilter := &models.TagFilterType{
Aliases: aliasCriterion,
}
verifyFn := func(tag *models.Tag, r models.Repository) {
aliases, err := r.Tag().GetAliases(tag.ID)
if err != nil {
t.Errorf("Error querying tags: %s", err.Error())
}
var alias string
if len(aliases) > 0 {
alias = aliases[0]
}
verifyString(t, alias, *aliasCriterion)
}
verifyTagQuery(t, tagFilter, nil, verifyFn)
aliasCriterion.Modifier = models.CriterionModifierNotEquals
verifyTagQuery(t, tagFilter, nil, verifyFn)
aliasCriterion.Modifier = models.CriterionModifierMatchesRegex
aliasCriterion.Value = "tag_.*1_Alias"
verifyTagQuery(t, tagFilter, nil, verifyFn)
aliasCriterion.Modifier = models.CriterionModifierNotMatchesRegex
verifyTagQuery(t, tagFilter, nil, verifyFn)
}
func verifyTagQuery(t *testing.T, tagFilter *models.TagFilterType, findFilter *models.FindFilterType, verifyFn func(t *models.Tag, r models.Repository)) {
withTxn(func(r models.Repository) error {
sqb := r.Tag()
tags := queryTags(t, sqb, tagFilter, findFilter)
for _, tag := range tags {
verifyFn(tag, r)
}
return nil
})
}
func queryTags(t *testing.T, qb models.TagReader, tagFilter *models.TagFilterType, findFilter *models.FindFilterType) []*models.Tag {
t.Helper()
tags, _, err := qb.Query(tagFilter, findFilter)
if err != nil {
t.Errorf("Error querying tags: %s", err.Error())
}
return tags
}
func TestTagQueryIsMissingImage(t *testing.T) {
withTxn(func(r models.Repository) error {
qb := r.Tag()
@@ -461,6 +567,39 @@ func TestTagDestroyTagImage(t *testing.T) {
}
}
func TestTagUpdateAlias(t *testing.T) {
if err := withTxn(func(r models.Repository) error {
qb := r.Tag()
// create tag to test against
const name = "TestTagUpdateAlias"
tag := models.Tag{
Name: name,
}
created, err := qb.Create(tag)
if err != nil {
return fmt.Errorf("Error creating tag: %s", err.Error())
}
aliases := []string{"alias1", "alias2"}
err = qb.UpdateAliases(created.ID, aliases)
if err != nil {
return fmt.Errorf("Error updating tag aliases: %s", err.Error())
}
// ensure aliases set
storedAliases, err := qb.GetAliases(created.ID)
if err != nil {
return fmt.Errorf("Error getting aliases: %s", err.Error())
}
assert.Equal(t, aliases, storedAliases)
return nil
}); err != nil {
t.Error(err.Error())
}
}
// TODO Create
// TODO Update
// TODO Destroy