Fix tag hierarchy validation (#1926)

* update tag hierarchy validation
* refactor MergeHierarchy
* update tag hierarchy error message
* rename tag hierarchy function
* add tag path to error message
* Rename EnsureHierarchy to ValidateHierarchy

Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
This commit is contained in:
7dJx1qP 2021-11-06 18:33:46 -04:00 committed by GitHub
parent 5bb5f6f2ce
commit e961ba4459
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 239 additions and 233 deletions

View File

@ -6,6 +6,7 @@ import (
"strconv"
"time"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/plugin"
"github.com/stashapp/stash/pkg/tag"
@ -43,7 +44,24 @@ func (r *mutationResolver) TagCreate(ctx context.Context, input models.TagCreate
}
}
// Start the transaction and save the t
var parentIDs []int
var childIDs []int
if len(input.ParentIds) > 0 {
parentIDs, err = utils.StringSliceToIntSlice(input.ParentIds)
if err != nil {
return nil, err
}
}
if len(input.ChildIds) > 0 {
childIDs, err = utils.StringSliceToIntSlice(input.ChildIds)
if err != nil {
return nil, err
}
}
// Start the transaction and save the tag
var t *models.Tag
if err := r.withTxn(ctx, func(repo models.Repository) error {
qb := repo.Tag()
@ -75,24 +93,22 @@ func (r *mutationResolver) TagCreate(ctx context.Context, input models.TagCreate
}
}
if input.ParentIds != nil && len(input.ParentIds) > 0 {
ids, err := utils.StringSliceToIntSlice(input.ParentIds)
if err != nil {
return err
}
if err := qb.UpdateParentTags(t.ID, ids); err != nil {
if len(parentIDs) > 0 {
if err := qb.UpdateParentTags(t.ID, parentIDs); err != nil {
return err
}
}
if input.ChildIds != nil && len(input.ChildIds) > 0 {
ids, err := utils.StringSliceToIntSlice(input.ChildIds)
if err != nil {
if len(childIDs) > 0 {
if err := qb.UpdateChildTags(t.ID, childIDs); err != nil {
return err
}
}
if err := qb.UpdateChildTags(t.ID, ids); err != nil {
// FIXME: This should be called before any changes are made, but
// requires a rewrite of ValidateHierarchy.
if len(parentIDs) > 0 || len(childIDs) > 0 {
if err := tag.ValidateHierarchy(t, parentIDs, childIDs, qb); err != nil {
return err
}
}
@ -128,6 +144,23 @@ func (r *mutationResolver) TagUpdate(ctx context.Context, input models.TagUpdate
}
}
var parentIDs []int
var childIDs []int
if translator.hasField("parent_ids") {
parentIDs, err = utils.StringSliceToIntSlice(input.ParentIds)
if err != nil {
return nil, err
}
}
if translator.hasField("child_ids") {
childIDs, err = utils.StringSliceToIntSlice(input.ChildIds)
if err != nil {
return nil, err
}
}
// Start the transaction and save the tag
var t *models.Tag
if err := r.withTxn(ctx, func(repo models.Repository) error {
@ -183,29 +216,6 @@ func (r *mutationResolver) TagUpdate(ctx context.Context, input models.TagUpdate
}
}
var parentIDs []int
var childIDs []int
if translator.hasField("parent_ids") {
parentIDs, err = utils.StringSliceToIntSlice(input.ParentIds)
if err != nil {
return err
}
}
if translator.hasField("child_ids") {
childIDs, err = utils.StringSliceToIntSlice(input.ChildIds)
if err != nil {
return err
}
}
if parentIDs != nil || childIDs != nil {
if err := tag.EnsureUniqueHierarchy(tagID, parentIDs, childIDs, qb); err != nil {
return err
}
}
if parentIDs != nil {
if err := qb.UpdateParentTags(tagID, parentIDs); err != nil {
return err
@ -218,6 +228,15 @@ func (r *mutationResolver) TagUpdate(ctx context.Context, input models.TagUpdate
}
}
// FIXME: This should be called before any changes are made, but
// requires a rewrite of ValidateHierarchy.
if parentIDs != nil || childIDs != nil {
if err := tag.ValidateHierarchy(t, parentIDs, childIDs, qb); err != nil {
logger.Errorf("Error saving tag: %s", err)
return err
}
}
return nil
}); err != nil {
return nil, err
@ -317,6 +336,12 @@ func (r *mutationResolver) TagsMerge(ctx context.Context, input models.TagsMerge
return err
}
err = tag.ValidateHierarchy(t, parents, children, qb)
if err != nil {
logger.Errorf("Error merging tag: %s", err)
return err
}
return nil
}); err != nil {
return nil, err

View File

@ -131,15 +131,15 @@ func (_m *TagReaderWriter) Find(id int) (*models.Tag, error) {
}
// FindAllAncestors provides a mock function with given fields: tagID, excludeIDs
func (_m *TagReaderWriter) FindAllAncestors(tagID int, excludeIDs []int) ([]*models.Tag, error) {
func (_m *TagReaderWriter) FindAllAncestors(tagID int, excludeIDs []int) ([]*models.TagPath, error) {
ret := _m.Called(tagID, excludeIDs)
var r0 []*models.Tag
if rf, ok := ret.Get(0).(func(int, []int) []*models.Tag); ok {
var r0 []*models.TagPath
if rf, ok := ret.Get(0).(func(int, []int) []*models.TagPath); ok {
r0 = rf(tagID, excludeIDs)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*models.Tag)
r0 = ret.Get(0).([]*models.TagPath)
}
}
@ -154,15 +154,15 @@ func (_m *TagReaderWriter) FindAllAncestors(tagID int, excludeIDs []int) ([]*mod
}
// FindAllDescendants provides a mock function with given fields: tagID, excludeIDs
func (_m *TagReaderWriter) FindAllDescendants(tagID int, excludeIDs []int) ([]*models.Tag, error) {
func (_m *TagReaderWriter) FindAllDescendants(tagID int, excludeIDs []int) ([]*models.TagPath, error) {
ret := _m.Called(tagID, excludeIDs)
var r0 []*models.Tag
if rf, ok := ret.Get(0).(func(int, []int) []*models.Tag); ok {
var r0 []*models.TagPath
if rf, ok := ret.Get(0).(func(int, []int) []*models.TagPath); ok {
r0 = rf(tagID, excludeIDs)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*models.Tag)
r0 = ret.Get(0).([]*models.TagPath)
}
}

View File

@ -16,6 +16,11 @@ type TagPartial struct {
UpdatedAt *SQLiteTimestamp `db:"updated_at" json:"updated_at"`
}
type TagPath struct {
Tag
Path string `db:"path" json:"path"`
}
func NewTag(name string) *Tag {
currentTime := time.Now()
return &Tag{
@ -35,6 +40,16 @@ func (t *Tags) New() interface{} {
return &Tag{}
}
type TagPaths []*TagPath
func (t *TagPaths) Append(o interface{}) {
*t = append(*t, o.(*TagPath))
}
func (t *TagPaths) New() interface{} {
return &TagPath{}
}
// Original Tag image from: https://fontawesome.com/icons/tag?style=solid
// Modified to change color and rotate
// Licensed under CC Attribution 4.0: https://fontawesome.com/license

View File

@ -20,8 +20,8 @@ type TagReader interface {
Query(tagFilter *TagFilterType, findFilter *FindFilterType) ([]*Tag, int, error)
GetImage(tagID int) ([]byte, error)
GetAliases(tagID int) ([]string, error)
FindAllAncestors(tagID int, excludeIDs []int) ([]*Tag, error)
FindAllDescendants(tagID int, excludeIDs []int) ([]*Tag, error)
FindAllAncestors(tagID int, excludeIDs []int) ([]*TagPath, error)
FindAllDescendants(tagID int, excludeIDs []int) ([]*TagPath, error)
}
type TagWriter interface {

View File

@ -732,26 +732,21 @@ func (qb *tagQueryBuilder) UpdateChildTags(tagID int, childIDs []int) error {
return nil
}
func (qb *tagQueryBuilder) FindAllAncestors(tagID int, excludeIDs []int) ([]*models.Tag, error) {
// FindAllAncestors returns a slice of TagPath objects, representing all
// ancestors of the tag with the provided id.
func (qb *tagQueryBuilder) FindAllAncestors(tagID int, excludeIDs []int) ([]*models.TagPath, error) {
inBinding := getInBinding(len(excludeIDs) + 1)
query := `WITH RECURSIVE
parents AS (
SELECT t.id AS parent_id, t.id AS child_id FROM tags t WHERE t.id = ?
SELECT t.id AS parent_id, t.id AS child_id, t.name as path FROM tags t WHERE t.id = ?
UNION
SELECT tr.parent_id, tr.child_id FROM tags_relations tr INNER JOIN parents p ON p.parent_id = tr.child_id WHERE tr.parent_id NOT IN` + inBinding + `
),
children AS (
SELECT tr.parent_id, tr.child_id FROM tags_relations tr INNER JOIN parents p ON p.parent_id = tr.parent_id WHERE tr.child_id NOT IN` + inBinding + `
UNION
SELECT tr.parent_id, tr.child_id FROM tags_relations tr INNER JOIN children c ON c.child_id = tr.parent_id WHERE tr.child_id NOT IN` + inBinding + `
SELECT tr.parent_id, tr.child_id, t.name || '->' || p.path as path FROM tags_relations tr INNER JOIN parents p ON p.parent_id = tr.child_id JOIN tags t ON t.id = tr.parent_id WHERE tr.parent_id NOT IN` + inBinding + `
)
SELECT t.* FROM tags t INNER JOIN parents p ON t.id = p.parent_id
UNION
SELECT t.* FROM tags t INNER JOIN children c ON t.id = c.child_id
SELECT t.*, p.path FROM tags t INNER JOIN parents p ON t.id = p.parent_id
`
var ret models.Tags
var ret models.TagPaths
excludeArgs := []interface{}{tagID}
for _, excludeID := range excludeIDs {
excludeArgs = append(excludeArgs, excludeID)
@ -765,26 +760,21 @@ SELECT t.* FROM tags t INNER JOIN children c ON t.id = c.child_id
return ret, nil
}
func (qb *tagQueryBuilder) FindAllDescendants(tagID int, excludeIDs []int) ([]*models.Tag, error) {
// FindAllDescendants returns a slice of TagPath objects, representing all
// descendants of the tag with the provided id.
func (qb *tagQueryBuilder) FindAllDescendants(tagID int, excludeIDs []int) ([]*models.TagPath, error) {
inBinding := getInBinding(len(excludeIDs) + 1)
query := `WITH RECURSIVE
children AS (
SELECT t.id AS parent_id, t.id AS child_id FROM tags t WHERE t.id = ?
SELECT t.id AS parent_id, t.id AS child_id, t.name as path FROM tags t WHERE t.id = ?
UNION
SELECT tr.parent_id, tr.child_id FROM tags_relations tr INNER JOIN children c ON c.child_id = tr.parent_id WHERE tr.child_id NOT IN` + inBinding + `
),
parents AS (
SELECT tr.parent_id, tr.child_id FROM tags_relations tr INNER JOIN children c ON c.child_id = tr.child_id WHERE tr.parent_id NOT IN` + inBinding + `
UNION
SELECT tr.parent_id, tr.child_id FROM tags_relations tr INNER JOIN parents p ON p.parent_id = tr.child_id WHERE tr.parent_id NOT IN` + inBinding + `
SELECT tr.parent_id, tr.child_id, c.path || '->' || t.name as path FROM tags_relations tr INNER JOIN children c ON c.child_id = tr.parent_id JOIN tags t ON t.id = tr.child_id WHERE tr.child_id NOT IN` + inBinding + `
)
SELECT t.* FROM tags t INNER JOIN children c ON t.id = c.child_id
UNION
SELECT t.* FROM tags t INNER JOIN parents p ON t.id = p.parent_id
SELECT t.*, c.path FROM tags t INNER JOIN children c ON t.id = c.child_id
`
var ret models.Tags
var ret models.TagPaths
excludeArgs := []interface{}{tagID}
for _, excludeID := range excludeIDs {
excludeArgs = append(excludeArgs, excludeID)

View File

@ -24,17 +24,15 @@ func (e *NameUsedByAliasError) Error() string {
}
type InvalidTagHierarchyError struct {
Direction string
InvalidTag string
ApplyingTag string
Direction string
CurrentRelation string
InvalidTag string
ApplyingTag string
TagPath string
}
func (e *InvalidTagHierarchyError) Error() string {
if e.InvalidTag == e.ApplyingTag {
return fmt.Sprintf("Cannot apply tag \"%s\" as it already is a %s", e.InvalidTag, e.Direction)
} else {
return fmt.Sprintf("Cannot apply tag \"%s\" as it is linked to \"%s\" which already is a %s", e.ApplyingTag, e.InvalidTag, e.Direction)
}
return fmt.Sprintf("cannot apply tag \"%s\" as a %s of \"%s\" as it is already %s (%s)", e.InvalidTag, e.Direction, e.ApplyingTag, e.CurrentRelation, e.TagPath)
}
// EnsureTagNameUnique returns an error if the tag name provided
@ -78,45 +76,55 @@ func EnsureAliasesUnique(id int, aliases []string, qb models.TagReader) error {
return nil
}
func EnsureUniqueHierarchy(id int, parentIDs, childIDs []int, qb models.TagReader) error {
allAncestors := make(map[int]*models.Tag)
allDescendants := make(map[int]*models.Tag)
excludeIDs := []int{id}
func ValidateHierarchy(tag *models.Tag, parentIDs, childIDs []int, qb models.TagReader) error {
id := tag.ID
allAncestors := make(map[int]*models.TagPath)
allDescendants := make(map[int]*models.TagPath)
validateParent := func(testID, applyingID int) error {
if parentTag, exists := allAncestors[testID]; exists {
applyingTag, err := qb.Find(applyingID)
parentsAncestors, err := qb.FindAllAncestors(id, nil)
if err != nil {
return err
}
if err != nil {
return nil
}
for _, ancestorTag := range parentsAncestors {
allAncestors[ancestorTag.ID] = ancestorTag
}
childsDescendants, err := qb.FindAllDescendants(id, nil)
if err != nil {
return err
}
for _, descendentTag := range childsDescendants {
allDescendants[descendentTag.ID] = descendentTag
}
validateParent := func(testID int) error {
if parentTag, exists := allDescendants[testID]; exists {
return &InvalidTagHierarchyError{
Direction: "parent",
InvalidTag: parentTag.Name,
ApplyingTag: applyingTag.Name,
Direction: "parent",
CurrentRelation: "a descendant",
InvalidTag: parentTag.Name,
ApplyingTag: tag.Name,
TagPath: parentTag.Path,
}
}
return nil
}
validateChild := func(testID, applyingID int) error {
if childTag, exists := allDescendants[testID]; exists {
applyingTag, err := qb.Find(applyingID)
if err != nil {
return nil
}
validateChild := func(testID int) error {
if childTag, exists := allAncestors[testID]; exists {
return &InvalidTagHierarchyError{
Direction: "child",
InvalidTag: childTag.Name,
ApplyingTag: applyingTag.Name,
Direction: "child",
CurrentRelation: "an ancestor",
InvalidTag: childTag.Name,
ApplyingTag: tag.Name,
TagPath: childTag.Path,
}
}
return validateParent(testID, applyingID)
return nil
}
if parentIDs == nil {
@ -142,33 +150,15 @@ func EnsureUniqueHierarchy(id int, parentIDs, childIDs []int, qb models.TagReade
}
for _, parentID := range parentIDs {
parentsAncestors, err := qb.FindAllAncestors(parentID, excludeIDs)
if err != nil {
if err := validateParent(parentID); err != nil {
return err
}
for _, ancestorTag := range parentsAncestors {
if err := validateParent(ancestorTag.ID, parentID); err != nil {
return err
}
allAncestors[ancestorTag.ID] = ancestorTag
}
}
for _, childID := range childIDs {
childsDescendants, err := qb.FindAllDescendants(childID, excludeIDs)
if err != nil {
if err := validateChild(childID); err != nil {
return err
}
for _, descendentTag := range childsDescendants {
if err := validateChild(descendentTag.ID, childID); err != nil {
return err
}
allDescendants[descendentTag.ID] = descendentTag
}
}
return nil
@ -217,10 +207,5 @@ func MergeHierarchy(destination int, sources []int, qb models.TagReader) ([]int,
mergedChildren = addTo(mergedChildren, children)
}
err := EnsureUniqueHierarchy(destination, mergedParents, mergedChildren, qb)
if err != nil {
return nil, nil, err
}
return mergedParents, mergedChildren, nil
}

View File

@ -29,39 +29,50 @@ var testUniqueHierarchyTags = map[int]*models.Tag{
},
}
var testUniqueHierarchyTagPaths = map[int]*models.TagPath{
1: {
Tag: *testUniqueHierarchyTags[1],
},
2: {
Tag: *testUniqueHierarchyTags[2],
},
3: {
Tag: *testUniqueHierarchyTags[3],
},
4: {
Tag: *testUniqueHierarchyTags[4],
},
}
type testUniqueHierarchyCase struct {
id int
parents []*models.Tag
children []*models.Tag
onFindAllAncestors map[int][]*models.Tag
onFindAllDescendants map[int][]*models.Tag
onFindAllAncestors []*models.TagPath
onFindAllDescendants []*models.TagPath
expectedError string
}
var testUniqueHierarchyCases = []testUniqueHierarchyCase{
{
id: 1,
parents: []*models.Tag{},
children: []*models.Tag{},
onFindAllAncestors: map[int][]*models.Tag{
1: {},
},
onFindAllDescendants: map[int][]*models.Tag{
1: {},
},
expectedError: "",
id: 1,
parents: []*models.Tag{},
children: []*models.Tag{},
onFindAllAncestors: []*models.TagPath{},
onFindAllDescendants: []*models.TagPath{},
expectedError: "",
},
{
id: 1,
parents: []*models.Tag{testUniqueHierarchyTags[2]},
children: []*models.Tag{testUniqueHierarchyTags[3]},
onFindAllAncestors: map[int][]*models.Tag{
2: {testUniqueHierarchyTags[2]},
onFindAllAncestors: []*models.TagPath{
testUniqueHierarchyTagPaths[2],
},
onFindAllDescendants: map[int][]*models.Tag{
3: {testUniqueHierarchyTags[3]},
onFindAllDescendants: []*models.TagPath{
testUniqueHierarchyTagPaths[3],
},
expectedError: "",
},
@ -69,11 +80,11 @@ var testUniqueHierarchyCases = []testUniqueHierarchyCase{
id: 2,
parents: []*models.Tag{testUniqueHierarchyTags[3]},
children: make([]*models.Tag, 0),
onFindAllAncestors: map[int][]*models.Tag{
3: {testUniqueHierarchyTags[3]},
onFindAllAncestors: []*models.TagPath{
testUniqueHierarchyTagPaths[3],
},
onFindAllDescendants: map[int][]*models.Tag{
2: {testUniqueHierarchyTags[2]},
onFindAllDescendants: []*models.TagPath{
testUniqueHierarchyTagPaths[2],
},
expectedError: "",
},
@ -84,24 +95,23 @@ var testUniqueHierarchyCases = []testUniqueHierarchyCase{
testUniqueHierarchyTags[4],
},
children: []*models.Tag{},
onFindAllAncestors: map[int][]*models.Tag{
3: {testUniqueHierarchyTags[3], testUniqueHierarchyTags[4]},
4: {testUniqueHierarchyTags[4]},
onFindAllAncestors: []*models.TagPath{
testUniqueHierarchyTagPaths[3], testUniqueHierarchyTagPaths[4],
},
onFindAllDescendants: map[int][]*models.Tag{
2: {testUniqueHierarchyTags[2]},
onFindAllDescendants: []*models.TagPath{
testUniqueHierarchyTagPaths[2],
},
expectedError: "Cannot apply tag \"four\" as it already is a parent",
expectedError: "",
},
{
id: 2,
parents: []*models.Tag{},
children: []*models.Tag{testUniqueHierarchyTags[3]},
onFindAllAncestors: map[int][]*models.Tag{
2: {testUniqueHierarchyTags[2]},
onFindAllAncestors: []*models.TagPath{
testUniqueHierarchyTagPaths[2],
},
onFindAllDescendants: map[int][]*models.Tag{
3: {testUniqueHierarchyTags[3]},
onFindAllDescendants: []*models.TagPath{
testUniqueHierarchyTagPaths[3],
},
expectedError: "",
},
@ -112,50 +122,49 @@ var testUniqueHierarchyCases = []testUniqueHierarchyCase{
testUniqueHierarchyTags[3],
testUniqueHierarchyTags[4],
},
onFindAllAncestors: map[int][]*models.Tag{
2: {testUniqueHierarchyTags[2]},
onFindAllAncestors: []*models.TagPath{
testUniqueHierarchyTagPaths[2],
},
onFindAllDescendants: map[int][]*models.Tag{
3: {testUniqueHierarchyTags[3], testUniqueHierarchyTags[4]},
4: {testUniqueHierarchyTags[4]},
onFindAllDescendants: []*models.TagPath{
testUniqueHierarchyTagPaths[3], testUniqueHierarchyTagPaths[4],
},
expectedError: "Cannot apply tag \"four\" as it already is a child",
expectedError: "",
},
{
id: 1,
parents: []*models.Tag{testUniqueHierarchyTags[2]},
children: []*models.Tag{testUniqueHierarchyTags[3]},
onFindAllAncestors: map[int][]*models.Tag{
2: {testUniqueHierarchyTags[2], testUniqueHierarchyTags[3]},
onFindAllAncestors: []*models.TagPath{
testUniqueHierarchyTagPaths[2], testUniqueHierarchyTagPaths[3],
},
onFindAllDescendants: map[int][]*models.Tag{
3: {testUniqueHierarchyTags[3]},
onFindAllDescendants: []*models.TagPath{
testUniqueHierarchyTagPaths[3],
},
expectedError: "Cannot apply tag \"three\" as it already is a parent",
expectedError: "cannot apply tag \"three\" as a child of \"one\" as it is already an ancestor ()",
},
{
id: 1,
parents: []*models.Tag{testUniqueHierarchyTags[2]},
children: []*models.Tag{testUniqueHierarchyTags[3]},
onFindAllAncestors: map[int][]*models.Tag{
2: {testUniqueHierarchyTags[2]},
onFindAllAncestors: []*models.TagPath{
testUniqueHierarchyTagPaths[2],
},
onFindAllDescendants: map[int][]*models.Tag{
3: {testUniqueHierarchyTags[3], testUniqueHierarchyTags[2]},
onFindAllDescendants: []*models.TagPath{
testUniqueHierarchyTagPaths[3], testUniqueHierarchyTagPaths[2],
},
expectedError: "Cannot apply tag \"three\" as it is linked to \"two\" which already is a parent",
expectedError: "cannot apply tag \"two\" as a parent of \"one\" as it is already a descendant ()",
},
{
id: 1,
parents: []*models.Tag{testUniqueHierarchyTags[3]},
children: []*models.Tag{testUniqueHierarchyTags[3]},
onFindAllAncestors: map[int][]*models.Tag{
3: {testUniqueHierarchyTags[3]},
onFindAllAncestors: []*models.TagPath{
testUniqueHierarchyTagPaths[3],
},
onFindAllDescendants: map[int][]*models.Tag{
3: {testUniqueHierarchyTags[3]},
onFindAllDescendants: []*models.TagPath{
testUniqueHierarchyTagPaths[3],
},
expectedError: "Cannot apply tag \"three\" as it already is a parent",
expectedError: "cannot apply tag \"three\" as a parent of \"one\" as it is already a descendant ()",
},
{
id: 1,
@ -165,54 +174,55 @@ var testUniqueHierarchyCases = []testUniqueHierarchyCase{
children: []*models.Tag{
testUniqueHierarchyTags[3],
},
onFindAllAncestors: map[int][]*models.Tag{
2: {testUniqueHierarchyTags[2]},
onFindAllAncestors: []*models.TagPath{
testUniqueHierarchyTagPaths[2],
},
onFindAllDescendants: map[int][]*models.Tag{
3: {testUniqueHierarchyTags[3], testUniqueHierarchyTags[2]},
onFindAllDescendants: []*models.TagPath{
testUniqueHierarchyTagPaths[3], testUniqueHierarchyTagPaths[2],
},
expectedError: "Cannot apply tag \"three\" as it is linked to \"two\" which already is a parent",
expectedError: "cannot apply tag \"two\" as a parent of \"one\" as it is already a descendant ()",
},
{
id: 1,
parents: []*models.Tag{testUniqueHierarchyTags[2]},
children: []*models.Tag{testUniqueHierarchyTags[2]},
onFindAllAncestors: map[int][]*models.Tag{
2: {testUniqueHierarchyTags[2]},
onFindAllAncestors: []*models.TagPath{
testUniqueHierarchyTagPaths[2],
},
onFindAllDescendants: map[int][]*models.Tag{
2: {testUniqueHierarchyTags[2]},
onFindAllDescendants: []*models.TagPath{
testUniqueHierarchyTagPaths[2],
},
expectedError: "Cannot apply tag \"two\" as it already is a parent",
expectedError: "cannot apply tag \"two\" as a parent of \"one\" as it is already a descendant ()",
},
{
id: 2,
parents: []*models.Tag{testUniqueHierarchyTags[1]},
children: []*models.Tag{testUniqueHierarchyTags[3]},
onFindAllAncestors: map[int][]*models.Tag{
1: {testUniqueHierarchyTags[1]},
onFindAllAncestors: []*models.TagPath{
testUniqueHierarchyTagPaths[1],
},
onFindAllDescendants: map[int][]*models.Tag{
3: {testUniqueHierarchyTags[3], testUniqueHierarchyTags[1]},
onFindAllDescendants: []*models.TagPath{
testUniqueHierarchyTagPaths[3], testUniqueHierarchyTagPaths[1],
},
expectedError: "Cannot apply tag \"three\" as it is linked to \"one\" which already is a parent",
expectedError: "cannot apply tag \"one\" as a parent of \"two\" as it is already a descendant ()",
},
}
func TestEnsureUniqueHierarchy(t *testing.T) {
func TestEnsureHierarchy(t *testing.T) {
for _, tc := range testUniqueHierarchyCases {
testEnsureUniqueHierarchy(t, tc, false, false)
testEnsureUniqueHierarchy(t, tc, true, false)
testEnsureUniqueHierarchy(t, tc, false, true)
testEnsureUniqueHierarchy(t, tc, true, true)
testEnsureHierarchy(t, tc, false, false)
testEnsureHierarchy(t, tc, true, false)
testEnsureHierarchy(t, tc, false, true)
testEnsureHierarchy(t, tc, true, true)
}
}
func testEnsureUniqueHierarchy(t *testing.T, tc testUniqueHierarchyCase, queryParents, queryChildren bool) {
func testEnsureHierarchy(t *testing.T, tc testUniqueHierarchyCase, queryParents, queryChildren bool) {
mockTagReader := &mocks.TagReaderWriter{}
var parentIDs, childIDs []int
find := make(map[int]*models.Tag)
find[tc.id] = testUniqueHierarchyTags[tc.id]
if tc.parents != nil {
parentIDs = make([]int, 0)
for _, parent := range tc.parents {
@ -243,50 +253,25 @@ func testEnsureUniqueHierarchy(t *testing.T, tc testUniqueHierarchyCase, queryPa
mockTagReader.On("FindByParentTagID", tc.id).Return(tc.children, nil).Once()
}
mockTagReader.On("Find", mock.AnythingOfType("int")).Return(func(tagID int) *models.Tag {
for id, tag := range find {
if id == tagID {
return tag
}
}
return nil
}, func(tagID int) error {
return nil
}).Maybe()
mockTagReader.On("FindAllAncestors", mock.AnythingOfType("int"), []int{tc.id}).Return(func(tagID int, excludeIDs []int) []*models.Tag {
for id, tags := range tc.onFindAllAncestors {
if id == tagID {
return tags
}
}
return nil
mockTagReader.On("FindAllAncestors", mock.AnythingOfType("int"), []int(nil)).Return(func(tagID int, excludeIDs []int) []*models.TagPath {
return tc.onFindAllAncestors
}, func(tagID int, excludeIDs []int) error {
for id := range tc.onFindAllAncestors {
if id == tagID {
return nil
}
if tc.onFindAllAncestors != nil {
return nil
}
return fmt.Errorf("undefined ancestors for: %d", tagID)
}).Maybe()
mockTagReader.On("FindAllDescendants", mock.AnythingOfType("int"), []int{tc.id}).Return(func(tagID int, excludeIDs []int) []*models.Tag {
for id, tags := range tc.onFindAllDescendants {
if id == tagID {
return tags
}
}
return nil
mockTagReader.On("FindAllDescendants", mock.AnythingOfType("int"), []int(nil)).Return(func(tagID int, excludeIDs []int) []*models.TagPath {
return tc.onFindAllDescendants
}, func(tagID int, excludeIDs []int) error {
for id := range tc.onFindAllDescendants {
if id == tagID {
return nil
}
if tc.onFindAllDescendants != nil {
return nil
}
return fmt.Errorf("undefined descendants for: %d", tagID)
}).Maybe()
res := EnsureUniqueHierarchy(tc.id, parentIDs, childIDs, mockTagReader)
res := ValidateHierarchy(testUniqueHierarchyTags[tc.id], parentIDs, childIDs, mockTagReader)
assert := assert.New(t)

View File

@ -22,6 +22,8 @@
* Optimised scanning process. ([#1816](https://github.com/stashapp/stash/pull/1816))
### 🐛 Bug fixes
* Fix tag hierarchy not being validated during tag creation. ([#1926](https://github.com/stashapp/stash/pull/1926))
* Fix tag hierarchy validation incorrectly failing for some hierarchies. ([#1926](https://github.com/stashapp/stash/pull/1926))
* Fix exclusion pattern fields losing focus on keypress. ([#1952](https://github.com/stashapp/stash/pull/1952))
* Include stash ids in import/export. ([#1916](https://github.com/stashapp/stash/pull/1916))
* Fix tiny menu items in scrape menu when a stash-box instance has no name. ([#1889](https://github.com/stashapp/stash/pull/1889))

View File

@ -421,6 +421,10 @@ div.dropdown-menu {
text-shadow: none;
}
}
.toast-body {
white-space: pre-wrap;
}
}
.image-input {