Merge tags functionality (#1481)

* Add API to merge tags

Add new API endpoint, `tagsMerge(source, destination)` to merge multiple
tags into a single one. The "sources" must be provided as a list of ids
and the destination as a single id. All usages of the source tags
(scenes, markers (primary and additional), images, galleries and
performers) will be updated to the destination tag, all aliases of the
source tags will be updated to the destination, and the name of the
source will be added as alias to the destination as well.

* Add merge tag UI
* Add unit tests
* Update test mocks
* Update internationalisation
* Add changelog entry

Co-authored-by: gitgiggety <gitgiggety@outlook.com>
This commit is contained in:
WithoutPants
2021-06-16 14:33:54 +10:00
committed by GitHub
parent 45f4a5ba81
commit 4fe4da6c01
18 changed files with 468 additions and 14 deletions

View File

@@ -5,6 +5,7 @@ package sqlite_test
import (
"context"
"database/sql"
"errors"
"fmt"
"io/ioutil"
"os"
@@ -342,6 +343,16 @@ func withTxn(f func(r models.Repository) error) error {
return t.WithTxn(context.TODO(), f)
}
func withRollbackTxn(f func(r models.Repository) error) error {
var ret error
withTxn(func(repo models.Repository) error {
ret = f(repo)
return errors.New("fake error for rollback")
})
return ret
}
func testTeardown(databaseFile string) {
err := database.DB.Close()

View File

@@ -536,3 +536,64 @@ func (qb *tagQueryBuilder) GetAliases(tagID int) ([]string, error) {
func (qb *tagQueryBuilder) UpdateAliases(tagID int, aliases []string) error {
return qb.aliasRepository().replace(tagID, aliases)
}
func (qb *tagQueryBuilder) Merge(source []int, destination int) error {
if len(source) == 0 {
return nil
}
inBinding := getInBinding(len(source))
args := []interface{}{destination}
for _, id := range source {
if id == destination {
return errors.New("cannot merge where source == destination")
}
args = append(args, id)
}
tagTables := map[string]string{
scenesTagsTable: sceneIDColumn,
"scene_markers_tags": "scene_marker_id",
galleriesTagsTable: galleryIDColumn,
imagesTagsTable: imageIDColumn,
"performers_tags": "performer_id",
}
tagArgs := append(args, destination)
for table, idColumn := range tagTables {
_, err := qb.tx.Exec(`UPDATE `+table+`
SET tag_id = ?
WHERE tag_id IN `+inBinding+`
AND NOT EXISTS(SELECT 1 FROM `+table+` o WHERE o.`+idColumn+` = `+table+`.`+idColumn+` AND o.tag_id = ?)`,
tagArgs...,
)
if err != nil {
return err
}
}
_, err := qb.tx.Exec("UPDATE "+sceneMarkerTable+" SET primary_tag_id = ? WHERE primary_tag_id IN "+inBinding, args...)
if err != nil {
return err
}
_, err = qb.tx.Exec("INSERT INTO "+tagAliasesTable+" (tag_id, alias) SELECT ?, name FROM "+tagTable+" WHERE id IN "+inBinding, args...)
if err != nil {
return err
}
_, err = qb.tx.Exec("UPDATE "+tagAliasesTable+" SET tag_id = ? WHERE tag_id IN "+inBinding, args...)
if err != nil {
return err
}
for _, id := range source {
err = qb.Destroy(id)
if err != nil {
return err
}
}
return nil
}

View File

@@ -600,6 +600,116 @@ func TestTagUpdateAlias(t *testing.T) {
}
}
func TestTagMerge(t *testing.T) {
assert := assert.New(t)
// merge tests - perform these in a transaction that we'll rollback
if err := withRollbackTxn(func(r models.Repository) error {
qb := r.Tag()
// try merging into same tag
err := qb.Merge([]int{tagIDs[tagIdx1WithScene]}, tagIDs[tagIdx1WithScene])
assert.NotNil(err)
// merge everything into tagIdxWithScene
srcIdxs := []int{
tagIdx1WithScene,
tagIdx2WithScene,
tagIdxWithPrimaryMarker,
tagIdxWithMarker,
tagIdxWithCoverImage,
tagIdxWithImage,
tagIdx1WithImage,
tagIdx2WithImage,
tagIdxWithPerformer,
tagIdx1WithPerformer,
tagIdx2WithPerformer,
tagIdxWithGallery,
tagIdx1WithGallery,
tagIdx2WithGallery,
}
var srcIDs []int
for _, idx := range srcIdxs {
srcIDs = append(srcIDs, tagIDs[idx])
}
destID := tagIDs[tagIdxWithScene]
if err = qb.Merge(srcIDs, destID); err != nil {
return err
}
// ensure other tags are deleted
for _, tagId := range srcIDs {
t, err := qb.Find(tagId)
if err != nil {
return err
}
assert.Nil(t)
}
// ensure aliases are set on the destination
destAliases, err := qb.GetAliases(destID)
if err != nil {
return err
}
for _, tagIdx := range srcIdxs {
assert.Contains(destAliases, getTagStringValue(tagIdx, "Name"))
}
// ensure scene points to new tag
sceneTagIDs, err := r.Scene().GetTagIDs(sceneIDs[sceneIdxWithTwoTags])
if err != nil {
return err
}
assert.Contains(sceneTagIDs, destID)
// ensure marker points to new tag
marker, err := r.SceneMarker().Find(markerIDs[markerIdxWithScene])
if err != nil {
return err
}
assert.Equal(destID, marker.PrimaryTagID)
markerTagIDs, err := r.SceneMarker().GetTagIDs(marker.ID)
if err != nil {
return err
}
assert.Contains(markerTagIDs, destID)
// ensure image points to new tag
imageTagIDs, err := r.Image().GetTagIDs(imageIDs[imageIdxWithTwoTags])
if err != nil {
return err
}
assert.Contains(imageTagIDs, destID)
// ensure gallery points to new tag
galleryTagIDs, err := r.Gallery().GetTagIDs(galleryIDs[galleryIdxWithTwoTags])
if err != nil {
return err
}
assert.Contains(galleryTagIDs, destID)
// ensure performer points to new tag
performerTagIDs, err := r.Gallery().GetTagIDs(performerIDs[performerIdxWithTwoTags])
if err != nil {
return err
}
assert.Contains(performerTagIDs, destID)
return nil
}); err != nil {
t.Error(err.Error())
}
}
// TODO Create
// TODO Update
// TODO Destroy