fix(migration): reuse existing labels on re-import
Seed the dedup map at the start of insertFromStructure with the importing user's existing labels, keyed by title + normalized hex color. Previously the map was empty on each run, so importing the same CSV (or any other migration format) twice would create a second copy of every label. Scoped to the user's own labels so imports don't silently link to other users' labels visible via shared projects. Fixes #2742
This commit is contained in:
parent
3c048223c3
commit
fa6e1f8e49
|
|
@ -27,6 +27,7 @@ import (
|
|||
"code.vikunja.io/api/pkg/models"
|
||||
"code.vikunja.io/api/pkg/modules/background/handler"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/api/pkg/utils"
|
||||
)
|
||||
|
||||
// InsertFromStructure takes a fully nested Vikunja data structure and a user and then creates everything for this user
|
||||
|
|
@ -57,7 +58,17 @@ func insertFromStructure(s *xorm.Session, str []*models.ProjectWithTasksAndBucke
|
|||
|
||||
log.Debugf("[creating structure] Creating %d projects", len(str))
|
||||
|
||||
// Seed the dedup map with the user's existing labels so re-imports
|
||||
// reuse them instead of creating duplicates (see issue #2742).
|
||||
labels := make(map[string]*models.Label)
|
||||
existingLabels := []*models.Label{}
|
||||
if err = s.Where("created_by_id = ?", user.ID).Find(&existingLabels); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, l := range existingLabels {
|
||||
labels[l.Title+utils.NormalizeHex(l.HexColor)] = l
|
||||
}
|
||||
|
||||
archivedProjects := []int64{}
|
||||
|
||||
childRelations := make(map[int64][]int64) // old id is the key, slice of old children ids
|
||||
|
|
@ -436,14 +447,15 @@ func createProjectWithEverything(s *xorm.Session, project *models.ProjectWithTas
|
|||
if label == nil {
|
||||
continue
|
||||
}
|
||||
lb, exists = labels[label.Title+label.HexColor]
|
||||
key := label.Title + utils.NormalizeHex(label.HexColor)
|
||||
lb, exists = labels[key]
|
||||
if !exists {
|
||||
err = label.Create(s, user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Debugf("[creating structure] Created new label %d", label.ID)
|
||||
labels[label.Title+label.HexColor] = label
|
||||
labels[key] = label
|
||||
lb = label
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -155,4 +155,59 @@ func TestInsertFromStructure(t *testing.T) {
|
|||
assert.NotEqual(t, 0, testStructure[1].Tasks[0].BucketID) // Should get the default bucket
|
||||
assert.NotEqual(t, 0, testStructure[1].Tasks[6].BucketID) // Should get the default bucket
|
||||
})
|
||||
t.Run("reuses existing labels across imports", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
|
||||
makeStructure := func() []*models.ProjectWithTasksAndBuckets {
|
||||
return []*models.ProjectWithTasksAndBuckets{
|
||||
{
|
||||
Project: models.Project{Title: "Import project"},
|
||||
Tasks: []*models.TaskWithComments{
|
||||
{
|
||||
Task: models.Task{
|
||||
Title: "Task with label",
|
||||
Labels: []*models.Label{
|
||||
{Title: "Mealie", HexColor: "abcdef"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
require.NoError(t, InsertFromStructure(makeStructure(), u))
|
||||
require.NoError(t, InsertFromStructure(makeStructure(), u))
|
||||
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
count, err := s.Where("created_by_id = ? AND title = ?", u.ID, "Mealie").Count(&models.Label{})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(1), count, "second import must reuse the existing 'Mealie' label")
|
||||
})
|
||||
t.Run("does not merge into another user's label", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
|
||||
// Fixture label #3 'Label #3 - other user' is created_by_id: 2.
|
||||
// Importing the same title for user 1 must create a new, user-owned label.
|
||||
structure := []*models.ProjectWithTasksAndBuckets{
|
||||
{
|
||||
Project: models.Project{Title: "Import project"},
|
||||
Tasks: []*models.TaskWithComments{
|
||||
{
|
||||
Task: models.Task{
|
||||
Title: "Task",
|
||||
Labels: []*models.Label{{Title: "Label #3 - other user"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
require.NoError(t, InsertFromStructure(structure, u))
|
||||
|
||||
db.AssertExists(t, "labels", map[string]interface{}{
|
||||
"title": "Label #3 - other user",
|
||||
"created_by_id": u.ID,
|
||||
}, false)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue