diff --git a/frontend/cypress/e2e/project/project-view-kanban.spec.ts b/frontend/cypress/e2e/project/project-view-kanban.spec.ts index 55e3150db..f338ea075 100644 --- a/frontend/cypress/e2e/project/project-view-kanban.spec.ts +++ b/frontend/cypress/e2e/project/project-view-kanban.spec.ts @@ -35,17 +35,17 @@ function createSingleTaskInBucket(count = 1, attrs = {}) { } function createTaskWithBuckets(buckets, count = 1) { - const data = TaskFactory.create(10, { - project_id: 1, - }) - TaskBucketFactory.truncate() - data.forEach(t => TaskBucketFactory.create(count, { - task_id: t.id, - bucket_id: buckets[0].id, - project_view_id: buckets[0].project_view_id, - }, false)) + const data = TaskFactory.create(count, { + project_id: 1, + }) + TaskBucketFactory.truncate() + data.forEach(t => TaskBucketFactory.create(1, { + task_id: t.id, + bucket_id: buckets[0].id, + project_view_id: buckets[0].project_view_id, + }, false)) - return data + return data } describe('Project View Kanban', () => { diff --git a/pkg/migration/20250624092830.go b/pkg/migration/20250624092830.go new file mode 100644 index 000000000..e17384e29 --- /dev/null +++ b/pkg/migration/20250624092830.go @@ -0,0 +1,44 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-present Vikunja and contributors. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package migration + +import ( + "src.techknowlogick.com/xormigrate" + "xorm.io/xorm" + "xorm.io/xorm/schemas" +) + +func init() { + migrations = append(migrations, &xormigrate.Migration{ + ID: "20250624092830", + Description: "add unique index for task buckets", + Migrate: func(tx *xorm.Engine) error { + var query string + switch tx.Dialect().URI().DBType { + case schemas.MYSQL: + query = "CREATE UNIQUE INDEX UQE_task_buckets_task_project_view ON task_buckets (task_id, project_view_id)" + default: + query = "CREATE UNIQUE INDEX IF NOT EXISTS UQE_task_buckets_task_project_view ON task_buckets (task_id, project_view_id)" + } + _, err := tx.Exec(query) + return err + }, + Rollback: func(tx *xorm.Engine) error { + return nil + }, + }) +} diff --git a/pkg/models/kanban_task_bucket.go b/pkg/models/kanban_task_bucket.go index e8f49574a..0da154993 100644 --- a/pkg/models/kanban_task_bucket.go +++ b/pkg/models/kanban_task_bucket.go @@ -25,13 +25,20 @@ import ( "xorm.io/xorm" ) +// TaskBucket represents the relation between a task and a kanban bucket. +// A task can only appear once per project view which is ensured by a +// unique index on the combination of task_id and project_view_id. type TaskBucket struct { - BucketID int64 `xorm:"bigint not null index" json:"bucket_id" param:"bucket"` - Bucket *Bucket `xorm:"-" json:"bucket"` - TaskID int64 `xorm:"bigint not null index" json:"task_id"` - ProjectViewID int64 `xorm:"bigint not null index" json:"project_view_id" param:"view"` - ProjectID int64 `xorm:"-" json:"-" param:"project"` - Task *Task `xorm:"-" json:"task"` + BucketID int64 `xorm:"bigint not null index" json:"bucket_id" param:"bucket"` + Bucket *Bucket `xorm:"-" json:"bucket"` + // The task which belongs to the bucket. Together with ProjectViewID + // this field is part of a unique index to prevent duplicates. + TaskID int64 `xorm:"bigint not null index unique(task_view)" json:"task_id"` + // The view this bucket belongs to. Combined with TaskID this forms a + // unique index. + ProjectViewID int64 `xorm:"bigint not null index unique(task_view)" json:"project_view_id" param:"view"` + ProjectID int64 `xorm:"-" json:"-" param:"project"` + Task *Task `xorm:"-" json:"task"` web.Rights `xorm:"-" json:"-"` web.CRUDable `xorm:"-" json:"-"`