feat: add setting for default bucket

Reviewed-on: vikunja/api#1602
This commit is contained in:
konrad 2023-09-03 15:13:52 +00:00
commit b99b323c4c
8 changed files with 182 additions and 80 deletions

View File

@ -18,7 +18,6 @@
title: testbucket3
project_id: 1
created_by_id: 1
is_done_bucket: 1
position: 3
created: 2020-04-18 21:13:52
updated: 2020-04-18 21:13:52
@ -26,7 +25,6 @@
title: testbucket4 - other project
project_id: 2
created_by_id: 1
is_done_bucket: 1
created: 2020-04-18 21:13:52
updated: 2020-04-18 21:13:52
# The following are not or only partly owned by user 1
@ -40,6 +38,7 @@
title: testbucket6
project_id: 6
created_by_id: 1
position: 1
created: 2020-04-18 21:13:52
updated: 2020-04-18 21:13:52
- id: 7
@ -137,6 +136,7 @@
title: testbucket22
project_id: 6
created_by_id: 1
position: 2
created: 2020-04-18 21:13:52
updated: 2020-04-18 21:13:52
- id: 23

View File

@ -5,6 +5,7 @@
identifier: test1
owner_id: 1
position: 3
done_bucket_id: 3
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
-
@ -14,6 +15,7 @@
identifier: test2
owner_id: 3
position: 2
done_bucket_id: 4
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
-
@ -50,6 +52,7 @@
identifier: test6
owner_id: 6
position: 6
default_bucket_id: 22
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
-

View File

@ -0,0 +1,118 @@
// 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 Licensee 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 Licensee for more details.
//
// You should have received a copy of the GNU Affero General Public Licensee
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package migration
import (
"src.techknowlogick.com/xormigrate"
"xorm.io/xorm"
"xorm.io/xorm/schemas"
)
type projects20230903143017 struct {
ID int64 `xorm:"bigint autoincr not null unique pk" json:"id" param:"project"`
DefaultBucketID int64 `xorm:"bigint INDEX null" json:"default_bucket_id"`
DoneBucketID int64 `xorm:"bigint INDEX null" json:"done_bucket_id"`
}
func (projects20230903143017) TableName() string {
return "projects"
}
type bucket20230903143017 struct {
ID int64 `xorm:"bigint autoincr not null unique pk" json:"id" param:"bucket"`
IsDoneBucket bool `xorm:"BOOL" json:"is_done_bucket"`
ProjectID int64 `xorm:"bigint not null" json:"project_id" param:"project"`
}
func (bucket20230903143017) TableName() string {
return "buckets"
}
const dropIsDoneBucketColSqlite20230903143017 = `
create table buckets_dg_tmp
(
id INTEGER not null
primary key autoincrement,
title TEXT not null,
project_id INTEGER not null,
"limit" INTEGER default 0,
position REAL,
created DATETIME not null,
updated DATETIME not null,
created_by_id INTEGER not null
);
insert into buckets_dg_tmp(id, title, project_id, "limit", position, created, updated, created_by_id)
select id,
title,
project_id,
"limit",
position,
created,
updated,
created_by_id
from buckets;
drop table buckets;
alter table buckets_dg_tmp
rename to buckets;
create unique index UQE_buckets_id
on buckets (id);
`
func init() {
migrations = append(migrations, &xormigrate.Migration{
ID: "20230903143017",
Description: "Move done bucket state to project + add default bucket setting",
Migrate: func(tx *xorm.Engine) (err error) {
err = tx.Sync2(projects20230903143017{})
if err != nil {
return
}
doneBuckets := []*bucket20230903143017{}
err = tx.Where("is_done_bucket = true").
Find(&doneBuckets)
if err != nil {
return
}
for _, bucket := range doneBuckets {
_, err = tx.Where("id = ?", bucket.ProjectID).
Cols("done_bucket_id").
Update(&projects20230903143017{
DoneBucketID: bucket.ID,
})
if err != nil {
return
}
}
if tx.Dialect().URI().DBType == schemas.SQLITE {
_, err = tx.Exec(dropIsDoneBucketColSqlite20230903143017)
return err
}
return dropTableColum(tx, "buckets", "is_done_bucket")
},
Rollback: func(tx *xorm.Engine) error {
return nil
},
})
}

View File

@ -38,8 +38,6 @@ type Bucket struct {
// How many tasks can be at the same time on this board max
Limit int64 `xorm:"default 0" json:"limit" minimum:"0" valid:"range(0|9223372036854775807)"`
// If this bucket is the "done bucket". All tasks moved into this bucket will automatically marked as done. All tasks marked as done from elsewhere will be moved into this bucket.
IsDoneBucket bool `xorm:"BOOL" json:"is_done_bucket"`
// The number of tasks currently in this bucket
Count int64 `xorm:"-" json:"count"`
@ -80,28 +78,21 @@ func getBucketByID(s *xorm.Session, id int64) (b *Bucket, err error) {
return
}
func getDefaultBucket(s *xorm.Session, projectID int64) (bucket *Bucket, err error) {
bucket = &Bucket{}
func getDefaultBucketID(s *xorm.Session, project *Project) (bucketID int64, err error) {
if project.DefaultBucketID != 0 {
return project.DefaultBucketID, nil
}
bucket := &Bucket{}
_, err = s.
Where("project_id = ?", projectID).
Where("project_id = ?", project.ID).
OrderBy("position asc").
Get(bucket)
return
}
func getDoneBucketForProject(s *xorm.Session, projectID int64) (bucket *Bucket, err error) {
bucket = &Bucket{}
exists, err := s.
Where("project_id = ? and is_done_bucket = ?", projectID, true).
Get(bucket)
if err != nil {
return nil, err
}
if !exists {
bucket = nil
return 0, err
}
return
return bucket.ID, nil
}
// ReadAll returns all buckets with their tasks for a certain project
@ -287,29 +278,11 @@ func (b *Bucket) Create(s *xorm.Session, a web.Auth) (err error) {
// @Failure 500 {object} models.Message "Internal error"
// @Router /projects/{projectID}/buckets/{bucketID} [post]
func (b *Bucket) Update(s *xorm.Session, _ web.Auth) (err error) {
doneBucket, err := getDoneBucketForProject(s, b.ProjectID)
if err != nil {
return err
}
if doneBucket != nil && doneBucket.IsDoneBucket && b.IsDoneBucket && doneBucket.ID != b.ID {
// When the current bucket will be the new done bucket, the old one should not be the done bucket anymore
doneBucket.IsDoneBucket = false
_, err = s.
Where("id = ?", doneBucket.ID).
Cols("is_done_bucket").
Update(doneBucket)
if err != nil {
return
}
}
_, err = s.
Where("id = ?", b.ID).
Cols(
"title",
"limit",
"is_done_bucket",
"position",
).
Update(b)
@ -350,15 +323,19 @@ func (b *Bucket) Delete(s *xorm.Session, _ web.Auth) (err error) {
}
// Get the default bucket
defaultBucket, err := getDefaultBucket(s, b.ProjectID)
p, err := GetProjectSimpleByID(s, b.ProjectID)
if err != nil {
return
}
defaultBucketID, err := getDefaultBucketID(s, p)
if err != nil {
return err
}
// Remove all associations of tasks to that bucket
_, err = s.
Where("bucket_id = ?", b.ID).
Cols("bucket_id").
Update(&Task{BucketID: defaultBucket.ID})
Update(&Task{BucketID: defaultBucketID})
return
}

View File

@ -217,28 +217,4 @@ func TestBucket_Update(t *testing.T) {
testAndAssertBucketUpdate(t, b, s)
})
t.Run("old done bucket should be unset", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
b := &Bucket{
ID: 1,
ProjectID: 1,
IsDoneBucket: true,
}
err := b.Update(s, &user.User{ID: 1})
assert.NoError(t, err)
db.AssertExists(t, "buckets", map[string]interface{}{
"id": 1,
"project_id": 1,
"is_done_bucket": true,
}, false)
db.AssertExists(t, "buckets", map[string]interface{}{
"id": 3,
"project_id": 1,
"is_done_bucket": false,
}, false)
})
}

View File

@ -51,6 +51,11 @@ type Project struct {
ParentProjectID int64 `xorm:"bigint INDEX null" json:"parent_project_id"`
ParentProject *Project `xorm:"-" json:"-"`
// The ID of the bucket where new tasks without a bucket are added to. By default, this is the leftmost bucket in a project.
DefaultBucketID int64 `xorm:"bigint INDEX null" json:"default_bucket_id"`
// If tasks are moved to the done bucket, they are marked as done. If they are marked as done individually, they are moved into the done bucket.
DoneBucketID int64 `xorm:"bigint INDEX null" json:"done_bucket_id"`
// The user who created this project.
Owner *user.User `xorm:"-" json:"owner" valid:"-"`
@ -778,6 +783,8 @@ func UpdateProject(s *xorm.Session, project *Project, auth web.Auth, updateProje
"hex_color",
"parent_project_id",
"position",
"done_bucket_id",
"default_bucket_id",
}
if project.Description != "" {
colsToUpdate = append(colsToUpdate, "description")

View File

@ -629,17 +629,18 @@ func checkBucketLimit(s *xorm.Session, t *Task, bucket *Bucket) (err error) {
}
// Contains all the task logic to figure out what bucket to use for this task.
func setTaskBucket(s *xorm.Session, task *Task, originalTask *Task, doCheckBucketLimit bool) (targetBucket *Bucket, err error) {
// Make sure we have a bucket
var bucket *Bucket
if task.Done && originalTask != nil && !originalTask.Done {
bucket, err := getDoneBucketForProject(s, task.ProjectID)
func setTaskBucket(s *xorm.Session, task *Task, originalTask *Task, doCheckBucketLimit bool, project *Project) (targetBucket *Bucket, err error) {
if project == nil {
project, err = GetProjectSimpleByID(s, task.ProjectID)
if err != nil {
return nil, err
}
if bucket != nil {
task.BucketID = bucket.ID
}
}
var bucket *Bucket
if task.Done && originalTask != nil && !originalTask.Done {
task.BucketID = project.DoneBucketID
}
if task.BucketID == 0 && originalTask != nil && originalTask.BucketID != 0 {
@ -648,11 +649,10 @@ func setTaskBucket(s *xorm.Session, task *Task, originalTask *Task, doCheckBucke
// Either no bucket was provided or the task was moved between projects
if task.BucketID == 0 || (originalTask != nil && task.ProjectID != 0 && originalTask.ProjectID != task.ProjectID) {
bucket, err = getDefaultBucket(s, task.ProjectID)
task.BucketID, err = getDefaultBucketID(s, project)
if err != nil {
return
}
task.BucketID = bucket.ID
}
if bucket == nil {
@ -676,7 +676,7 @@ func setTaskBucket(s *xorm.Session, task *Task, originalTask *Task, doCheckBucke
}
}
if bucket.IsDoneBucket && originalTask != nil && !originalTask.Done {
if bucket.ID == project.DoneBucketID && originalTask != nil && !originalTask.Done {
task.Done = true
}
@ -732,7 +732,7 @@ func createTask(s *xorm.Session, t *Task, a web.Auth, updateAssignees bool) (err
}
// Check if the project exists
l, err := GetProjectSimpleByID(s, t.ProjectID)
p, err := GetProjectSimpleByID(s, t.ProjectID)
if err != nil {
return err
}
@ -749,7 +749,7 @@ func createTask(s *xorm.Session, t *Task, a web.Auth, updateAssignees bool) (err
}
// Get the default bucket and move the task there
_, err = setTaskBucket(s, t, nil, true)
_, err = setTaskBucket(s, t, nil, true, nil)
if err != nil {
return
}
@ -781,7 +781,7 @@ func createTask(s *xorm.Session, t *Task, a web.Auth, updateAssignees bool) (err
return err
}
t.setIdentifier(l)
t.setIdentifier(p)
if t.IsFavorite {
if err := addToFavorites(s, t.ID, createdBy, FavoriteKindTask); err != nil {
@ -838,14 +838,18 @@ func (t *Task) Update(s *xorm.Session, a web.Auth) (err error) {
// Old task has the stored reminders
ot.Reminders = reminders
targetBucket, err := setTaskBucket(s, t, &ot, t.BucketID != 0 && t.BucketID != ot.BucketID)
targetBucket, err := setTaskBucket(s, t, &ot, t.BucketID != 0 && t.BucketID != ot.BucketID, nil)
if err != nil {
return err
}
// If the task was moved into the done bucket and the task has a repeating cycle we should not update
// the bucket.
if targetBucket.IsDoneBucket && t.RepeatAfter > 0 {
project, err := GetProjectSimpleByID(s, t.ProjectID)
if err != nil {
return err
}
if targetBucket.ID == project.DoneBucketID && t.RepeatAfter > 0 {
t.Done = true // This will trigger the correct re-scheduling of the task (happening in updateDone later)
t.BucketID = ot.BucketID
}

View File

@ -170,6 +170,23 @@ func TestTask_Create(t *testing.T) {
assert.Error(t, err)
assert.True(t, IsErrBucketLimitExceeded(err))
})
t.Run("default bucket different", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
task := &Task{
Title: "Lorem",
Description: "Lorem Ipsum Dolor",
ProjectID: 6,
}
err := task.Create(s, usr)
assert.NoError(t, err)
db.AssertExists(t, "tasks", map[string]interface{}{
"id": task.ID,
"bucket_id": 22, // default bucket of project 6 but with a position of 2
}, false)
})
}
func TestTask_Update(t *testing.T) {