From d0c77ad1c1ef30ee039be33eff3ba1baaf209529 Mon Sep 17 00:00:00 2001 From: konrad Date: Sat, 10 Jul 2021 10:21:54 +0000 Subject: [PATCH] Make sure list / task favorites are set per user, not per entity (#915) Co-authored-by: kolaente Reviewed-on: https://kolaente.dev/vikunja/api/pulls/915 Co-authored-by: konrad Co-committed-by: konrad --- pkg/db/fixtures/favorites.yml | 21 +++++ pkg/db/fixtures/lists.yml | 1 - pkg/db/fixtures/tasks.yml | 3 - pkg/migration/20210709211508.go | 112 +++++++++++++++++++++++ pkg/models/favorites.go | 109 ++++++++++++++++++++++ pkg/models/kanban.go | 2 +- pkg/models/list.go | 58 ++++++++++-- pkg/models/models.go | 1 + pkg/models/namespace.go | 29 ++++-- pkg/models/tasks.go | 79 ++++++++++++---- pkg/models/tasks_test.go | 24 ++++- pkg/models/unit_tests.go | 1 + pkg/routes/caldav/listStorageProvider.go | 2 +- pkg/swagger/docs.go | 6 +- pkg/swagger/swagger.json | 6 +- pkg/swagger/swagger.yaml | 8 +- 16 files changed, 409 insertions(+), 53 deletions(-) create mode 100644 pkg/db/fixtures/favorites.yml create mode 100644 pkg/migration/20210709211508.go create mode 100644 pkg/models/favorites.go diff --git a/pkg/db/fixtures/favorites.yml b/pkg/db/fixtures/favorites.yml new file mode 100644 index 00000000..ec2dd632 --- /dev/null +++ b/pkg/db/fixtures/favorites.yml @@ -0,0 +1,21 @@ +- entity_id: 1 + user_id: 1 + kind: 1 +- entity_id: 15 + user_id: 6 # owner + kind: 1 +- entity_id: 15 + user_id: 1 + kind: 1 +- entity_id: 34 + user_id: 13 # owner + kind: 1 +- entity_id: 34 + user_id: 1 + kind: 1 +- entity_id: 23 + user_id: 12 # owner + kind: 2 +- entity_id: 23 + user_id: 1 + kind: 2 diff --git a/pkg/db/fixtures/lists.yml b/pkg/db/fixtures/lists.yml index 7cbfadc8..45336f95 100644 --- a/pkg/db/fixtures/lists.yml +++ b/pkg/db/fixtures/lists.yml @@ -207,6 +207,5 @@ identifier: test23 owner_id: 12 namespace_id: 17 - is_favorite: true updated: 2018-12-02 15:13:12 created: 2018-12-01 15:13:12 diff --git a/pkg/db/fixtures/tasks.yml b/pkg/db/fixtures/tasks.yml index b1203561..4d944fd1 100644 --- a/pkg/db/fixtures/tasks.yml +++ b/pkg/db/fixtures/tasks.yml @@ -8,7 +8,6 @@ created: 2018-12-01 01:12:04 updated: 2018-12-01 01:12:04 bucket_id: 1 - is_favorite: true - id: 2 title: 'task #2 done' done: true @@ -141,7 +140,6 @@ list_id: 6 index: 1 bucket_id: 6 - is_favorite: true created: 2018-12-01 01:12:04 updated: 2018-12-01 01:12:04 - id: 16 @@ -317,7 +315,6 @@ list_id: 20 index: 20 bucket_id: 5 - is_favorite: true created: 2018-12-01 01:12:04 updated: 2018-12-01 01:12:04 - id: 35 diff --git a/pkg/migration/20210709211508.go b/pkg/migration/20210709211508.go new file mode 100644 index 00000000..81546a53 --- /dev/null +++ b/pkg/migration/20210709211508.go @@ -0,0 +1,112 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-2021 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 . + +package migration + +import ( + "src.techknowlogick.com/xormigrate" + "xorm.io/xorm" +) + +type favorites20210709211508 struct { + EntityID int64 `xorm:"bigint not null pk"` + UserID int64 `xorm:"bigint not null pk"` + Kind int `xorm:"int not null pk"` +} + +func (favorites20210709211508) TableName() string { + return "favorites" +} + +type task20210709211508 struct { + ID int64 `xorm:"bigint autoincr not null unique pk" json:"id" param:"listtask"` + IsFavorite bool `xorm:"default false" json:"is_favorite"` + CreatedByID int64 `xorm:"bigint not null" json:"-"` // ID of the user who put that task on the list +} + +func (task20210709211508) TableName() string { + return "tasks" +} + +type list20210709211508 struct { + ID int64 `xorm:"bigint autoincr not null unique pk" json:"id" param:"listtask"` + IsFavorite bool `xorm:"default false" json:"is_favorite"` + OwnerID int64 `xorm:"bigint not null" json:"-"` // ID of the user who put that task on the list +} + +func (list20210709211508) TableName() string { + return "lists" +} + +func init() { + migrations = append(migrations, &xormigrate.Migration{ + ID: "20210709211508", + Description: "Move favorites to new table", + Migrate: func(tx *xorm.Engine) error { + err := tx.Sync2(favorites20210709211508{}) + if err != nil { + return err + } + + // Migrate all existing favorites + tasks := []*task20210709211508{} + err = tx.Where("is_favorite = ?", true).Find(&tasks) + if err != nil { + return err + } + + for _, task := range tasks { + fav := &favorites20210709211508{ + EntityID: task.ID, + UserID: task.CreatedByID, + Kind: 1, + } + _, err = tx.Insert(fav) + if err != nil { + return err + } + } + + lists := []*list20210709211508{} + err = tx.Where("is_favorite = ?", true).Find(&lists) + if err != nil { + return err + } + + for _, list := range lists { + fav := &favorites20210709211508{ + EntityID: list.ID, + UserID: list.OwnerID, + Kind: 2, + } + _, err = tx.Insert(fav) + if err != nil { + return err + } + } + + err = dropTableColum(tx, "tasks", "is_favorite") + if err != nil { + return err + } + + return dropTableColum(tx, "lists", "is_favorite") + }, + Rollback: func(tx *xorm.Engine) error { + return nil + }, + }) +} diff --git a/pkg/models/favorites.go b/pkg/models/favorites.go new file mode 100644 index 00000000..35f74ac1 --- /dev/null +++ b/pkg/models/favorites.go @@ -0,0 +1,109 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-2021 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 . + +package models + +import ( + "code.vikunja.io/api/pkg/user" + "code.vikunja.io/web" + "xorm.io/builder" + "xorm.io/xorm" +) + +// FavoriteKind represents the kind of entities that can be marked as favorite +type FavoriteKind int + +const ( + FavoriteKindUnknown FavoriteKind = iota + FavoriteKindTask + FavoriteKindList +) + +// Favorite represents an entity which is a favorite to someone +type Favorite struct { + EntityID int64 `xorm:"bigint not null pk"` + UserID int64 `xorm:"bigint not null pk"` + Kind FavoriteKind `xorm:"int not null pk"` +} + +// TableName is the table name +func (t *Favorite) TableName() string { + return "favorites" +} + +func addToFavorites(s *xorm.Session, entityID int64, a web.Auth, kind FavoriteKind) error { + u, err := user.GetFromAuth(a) + if err != nil { + // Only error GetFromAuth is if it's a link share and we want to ignore that + return nil + } + + fav := &Favorite{ + EntityID: entityID, + UserID: u.ID, + Kind: kind, + } + + _, err = s.Insert(fav) + return err +} + +func removeFromFavorite(s *xorm.Session, entityID int64, a web.Auth, kind FavoriteKind) error { + u, err := user.GetFromAuth(a) + if err != nil { + // Only error GetFromAuth is if it's a link share and we want to ignore that + return nil + } + + _, err = s. + Where("entity_id = ? AND user_id = ? AND kind = ?", entityID, u.ID, kind). + Delete(&Favorite{}) + return err +} + +func isFavorite(s *xorm.Session, entityID int64, a web.Auth, kind FavoriteKind) (is bool, err error) { + u, err := user.GetFromAuth(a) + if err != nil { + // Only error GetFromAuth is if it's a link share and we want to ignore that + return false, nil + } + + return s. + Where("entity_id = ? AND user_id = ? AND kind = ?", entityID, u.ID, kind). + Exist(&Favorite{}) +} + +func getFavorites(s *xorm.Session, entityIDs []int64, a web.Auth, kind FavoriteKind) (favorites map[int64]bool, err error) { + favorites = make(map[int64]bool) + u, err := user.GetFromAuth(a) + if err != nil { + // Only error GetFromAuth is if it's a link share and we want to ignore that + return favorites, nil + } + + favs := []*Favorite{} + err = s.Where(builder.And( + builder.Eq{"user_id": u.ID}, + builder.Eq{"kind": kind}, + builder.In("entity_id", entityIDs), + )). + Find(&favs) + + for _, fav := range favs { + favorites[fav.EntityID] = true + } + return +} diff --git a/pkg/models/kanban.go b/pkg/models/kanban.go index 825b69f7..6c001637 100644 --- a/pkg/models/kanban.go +++ b/pkg/models/kanban.go @@ -209,7 +209,7 @@ func (b *Bucket) ReadAll(s *xorm.Session, auth web.Auth, search string, page int taskMap[t.ID] = t } - err = addMoreInfoToTasks(s, taskMap) + err = addMoreInfoToTasks(s, taskMap, auth) if err != nil { return nil, 0, 0, err } diff --git a/pkg/models/list.go b/pkg/models/list.go index 45119149..2148e84b 100644 --- a/pkg/models/list.go +++ b/pkg/models/list.go @@ -64,8 +64,8 @@ type List struct { // Holds extra information about the background set since some background providers require attribution or similar. If not null, the background can be accessed at /lists/{listID}/background BackgroundInformation interface{} `xorm:"-" json:"background_information"` - // True if a list is a favorite. Favorite lists show up in a separate namespace. - IsFavorite bool `xorm:"default false" json:"is_favorite"` + // True if a list is a favorite. Favorite lists show up in a separate namespace. This value depends on the user making the call to the api. + IsFavorite bool `xorm:"-" json:"is_favorite"` // The subscription status for the user reading this list. You can only read this property, use the subscription endpoints to modify it. // Will only returned when retreiving one list. @@ -155,7 +155,7 @@ func GetListsByNamespaceID(s *xorm.Session, nID int64, doer *user.User) (lists [ } // get more list details - err = addListDetails(s, lists) + err = addListDetails(s, lists, doer) return lists, err } @@ -183,7 +183,7 @@ func (l *List) ReadAll(s *xorm.Session, a web.Auth, search string, page int, per return nil, 0, 0, err } lists := []*List{list} - err = addListDetails(s, lists) + err = addListDetails(s, lists, a) return lists, 0, 0, err } @@ -201,7 +201,7 @@ func (l *List) ReadAll(s *xorm.Session, a web.Auth, search string, page int, per } // Add more list details - err = addListDetails(s, lists) + err = addListDetails(s, lists, a) return lists, resultCount, totalItems, err } @@ -266,6 +266,11 @@ func (l *List) ReadOne(s *xorm.Session, a web.Auth) (err error) { } } + l.IsFavorite, err = isFavorite(s, l.ID, a, FavoriteKindList) + if err != nil { + return + } + l.Subscription, err = GetSubscription(s, SubscriptionEntityList, l.ID, a) return } @@ -421,7 +426,7 @@ func getRawListsForUser(s *xorm.Session, opts *listOptions) (lists []*List, resu } // addListDetails adds owner user objects and list tasks to all lists in the slice -func addListDetails(s *xorm.Session, lists []*List) (err error) { +func addListDetails(s *xorm.Session, lists []*List, a web.Auth) (err error) { if len(lists) == 0 { return } @@ -441,7 +446,9 @@ func addListDetails(s *xorm.Session, lists []*List) (err error) { } var fileIDs []int64 + var listIDs []int64 for _, l := range lists { + listIDs = append(listIDs, l.ID) if o, exists := owners[l.OwnerID]; exists { l.Owner = o } @@ -451,6 +458,15 @@ func addListDetails(s *xorm.Session, lists []*List) (err error) { fileIDs = append(fileIDs, l.BackgroundFileID) } + favs, err := getFavorites(s, listIDs, a, FavoriteKindList) + if err != nil { + return err + } + + for _, list := range lists { + list.IsFavorite = favs[list.ID] + } + if len(fileIDs) == 0 { return } @@ -536,6 +552,14 @@ func CreateOrUpdateList(s *xorm.Session, list *List, auth web.Auth) (err error) if list.ID == 0 { _, err = s.Insert(list) + if err != nil { + return + } + if list.IsFavorite { + if err := addToFavorites(s, list.ID, auth, FavoriteKindList); err != nil { + return err + } + } } else { // We need to specify the cols we want to update here to be able to un-archive lists colsToUpdate := []string{ @@ -543,17 +567,35 @@ func CreateOrUpdateList(s *xorm.Session, list *List, auth web.Auth) (err error) "is_archived", "identifier", "hex_color", - "is_favorite", "background_file_id", } if list.Description != "" { colsToUpdate = append(colsToUpdate, "description") } + wasFavorite, err := isFavorite(s, list.ID, auth, FavoriteKindList) + if err != nil { + return err + } + if list.IsFavorite && !wasFavorite { + if err := addToFavorites(s, list.ID, auth, FavoriteKindList); err != nil { + return err + } + } + + if !list.IsFavorite && wasFavorite { + if err := removeFromFavorite(s, list.ID, auth, FavoriteKindList); err != nil { + return err + } + } + _, err = s. ID(list.ID). Cols(colsToUpdate...). Update(list) + if err != nil { + return err + } } if err != nil { @@ -568,7 +610,6 @@ func CreateOrUpdateList(s *xorm.Session, list *List, auth web.Auth) (err error) *list = *l err = list.ReadOne(s, auth) return - } // Update implements the update method of CRUDable @@ -593,7 +634,6 @@ func (l *List) Update(s *xorm.Session, a web.Auth) (err error) { return err } - f.IsFavorite = l.IsFavorite f.Title = l.Title f.Description = l.Description err = f.Update(s, a) diff --git a/pkg/models/models.go b/pkg/models/models.go index 86974462..6f5e9639 100644 --- a/pkg/models/models.go +++ b/pkg/models/models.go @@ -60,6 +60,7 @@ func GetTables() []interface{} { &UnsplashPhoto{}, &SavedFilter{}, &Subscription{}, + &Favorite{}, } } diff --git a/pkg/models/namespace.go b/pkg/models/namespace.go index 7c4753dc..3858ee3a 100644 --- a/pkg/models/namespace.go +++ b/pkg/models/namespace.go @@ -392,12 +392,21 @@ func getFavoriteLists(s *xorm.Session, lists []*List, namespaceIDs []int64, doer } // Check if we have any favorites or favorited lists and remove the favorites namespace from the list if not - var favoriteCount int64 - favoriteCount, err = s. + cond := builder. + Select("tasks.id"). + From("tasks"). Join("INNER", "lists", "tasks.list_id = lists.id"). Join("INNER", "namespaces", "lists.namespace_id = namespaces.id"). - Where(builder.And(builder.Eq{"tasks.is_favorite": true}, builder.In("namespaces.id", namespaceIDs))). - Count(&Task{}) + Where(builder.In("namespaces.id", namespaceIDs)) + + var favoriteCount int64 + favoriteCount, err = s. + Where(builder.And( + builder.Eq{"user_id": doer.ID}, + builder.Eq{"kind": FavoriteKindTask}, + builder.In("entity_id", cond), + )). + Count(&Favorite{}) if err != nil { return } @@ -538,6 +547,13 @@ func (n *Namespace) ReadAll(s *xorm.Session, a web.Auth, search string, page int lists = append(lists, savedFiltersNamespace.Lists...) } + ///////////////// + // Add list details (favorite state, among other things + err = addListDetails(s, lists, a) + if err != nil { + return + } + ///////////////// // Favorite lists @@ -553,11 +569,6 @@ func (n *Namespace) ReadAll(s *xorm.Session, a web.Auth, search string, page int ////////////////////// // Put it all together - err = addListDetails(s, lists) - if err != nil { - return - } - for _, list := range lists { if list.NamespaceID == SharedListsPseudoNamespace.ID || list.NamespaceID == SavedFiltersPseudoNamespace.ID { // Shared lists and filtered lists are already in the namespace diff --git a/pkg/models/tasks.go b/pkg/models/tasks.go index b016b982..ae3df859 100644 --- a/pkg/models/tasks.go +++ b/pkg/models/tasks.go @@ -59,8 +59,7 @@ type Task struct { // The time when the task is due. DueDate time.Time `xorm:"DATETIME INDEX null 'due_date'" json:"due_date"` // An array of datetimes when the user wants to be reminded of the task. - Reminders []time.Time `xorm:"-" json:"reminder_dates"` - CreatedByID int64 `xorm:"bigint not null" json:"-"` // ID of the user who put that task on the list + Reminders []time.Time `xorm:"-" json:"reminder_dates"` // The list this task belongs to. ListID int64 `xorm:"bigint INDEX not null" json:"list_id" param:"list"` // An amount in seconds this task repeats itself. If this is set, when marking the task as done, it will mark itself as "undone" and then increase all remindes and the due date by its amount. @@ -96,8 +95,8 @@ type Task struct { // All attachments this task has Attachments []*TaskAttachment `xorm:"-" json:"attachments"` - // True if a task is a favorite task. Favorite tasks show up in a separate "Important" list - IsFavorite bool `xorm:"default false" json:"is_favorite"` + // True if a task is a favorite task. Favorite tasks show up in a separate "Important" list. This value depends on the user making the call to the api. + IsFavorite bool `xorm:"-" json:"is_favorite"` // The subscription status for the user reading this task. You can only read this property, use the subscription endpoints to modify it. // Will only returned when retreiving one task. @@ -120,7 +119,8 @@ type Task struct { Position float64 `xorm:"double null" json:"position"` // The user who initially created the task. - CreatedBy *user.User `xorm:"-" json:"created_by" valid:"-"` + CreatedBy *user.User `xorm:"-" json:"created_by" valid:"-"` + CreatedByID int64 `xorm:"bigint not null" json:"-"` // ID of the user who put that task on the list web.CRUDable `xorm:"-" json:"-"` web.Rights `xorm:"-" json:"-"` @@ -252,10 +252,11 @@ func getRawTasksForLists(s *xorm.Session, lists []*List, a web.Auth, opts *taskO // Get all list IDs and get the tasks var listIDs []int64 - var hasFavoriteLists bool + var hasFavoritesList bool for _, l := range lists { if l.ID == FavoritesPseudoList.ID { - hasFavoriteLists = true + hasFavoritesList = true + continue } listIDs = append(listIDs, l.ID) } @@ -375,7 +376,7 @@ func getRawTasksForLists(s *xorm.Session, lists []*List, a web.Auth, opts *taskO listCond = listIDCond } - if hasFavoriteLists { + if hasFavoritesList { // Make sure users can only see their favorites userLists, _, _, err := getRawListsForUser( s, @@ -393,7 +394,17 @@ func getRawTasksForLists(s *xorm.Session, lists []*List, a web.Auth, opts *taskO userListIDs = append(userListIDs, l.ID) } - listCond = builder.Or(listIDCond, builder.And(builder.Eq{"is_favorite": true}, builder.In("list_id", userListIDs))) + // All favorite tasks for that user + favCond := builder. + Select("entity_id"). + From("favorites"). + Where( + builder.And( + builder.Eq{"user_id": a.GetID()}, + builder.Eq{"kind": FavoriteKindTask}, + )) + + listCond = builder.And(listCond, builder.And(builder.In("id", favCond), builder.In("list_id", userListIDs))) } if len(reminderFilters) > 0 { @@ -473,7 +484,7 @@ func getTasksForLists(s *xorm.Session, lists []*List, a web.Auth, opts *taskOpti taskMap[t.ID] = t } - err = addMoreInfoToTasks(s, taskMap) + err = addMoreInfoToTasks(s, taskMap, a) if err != nil { return nil, 0, 0, err } @@ -521,7 +532,7 @@ func (bt *BulkTask) GetTasksByIDs(s *xorm.Session) (err error) { } // GetTasksByUIDs gets all tasks from a bunch of uids -func GetTasksByUIDs(s *xorm.Session, uids []string) (tasks []*Task, err error) { +func GetTasksByUIDs(s *xorm.Session, uids []string, a web.Auth) (tasks []*Task, err error) { tasks = []*Task{} err = s.In("uid", uids).Find(&tasks) if err != nil { @@ -533,7 +544,7 @@ func GetTasksByUIDs(s *xorm.Session, uids []string) (tasks []*Task, err error) { taskMap[t.ID] = t } - err = addMoreInfoToTasks(s, taskMap) + err = addMoreInfoToTasks(s, taskMap, a) return } @@ -611,7 +622,7 @@ func getTaskReminderMap(s *xorm.Session, taskIDs []int64) (taskReminders map[int return } -func addRelatedTasksToTasks(s *xorm.Session, taskIDs []int64, taskMap map[int64]*Task) (err error) { +func addRelatedTasksToTasks(s *xorm.Session, taskIDs []int64, taskMap map[int64]*Task, a web.Auth) (err error) { relatedTasks := []*TaskRelation{} err = s.In("task_id", taskIDs).Find(&relatedTasks) if err != nil { @@ -634,10 +645,16 @@ func addRelatedTasksToTasks(s *xorm.Session, taskIDs []int64, taskMap map[int64] return } + taskFavorites, err := getFavorites(s, relatedTaskIDs, a, FavoriteKindTask) + if err != nil { + return err + } + // NOTE: while it certainly be possible to run this function on fullRelatedTasks again, we don't do this for performance reasons. // Go through all task relations and put them into the task objects for _, rt := range relatedTasks { + fullRelatedTasks[rt.OtherTaskID].IsFavorite = taskFavorites[rt.OtherTaskID] taskMap[rt.TaskID].RelatedTasks[rt.RelationKind] = append(taskMap[rt.TaskID].RelatedTasks[rt.RelationKind], fullRelatedTasks[rt.OtherTaskID]) } @@ -646,7 +663,7 @@ func addRelatedTasksToTasks(s *xorm.Session, taskIDs []int64, taskMap map[int64] // This function takes a map with pointers and returns a slice with pointers to tasks // It adds more stuff like assignees/labels/etc to a bunch of tasks -func addMoreInfoToTasks(s *xorm.Session, taskMap map[int64]*Task) (err error) { +func addMoreInfoToTasks(s *xorm.Session, taskMap map[int64]*Task, a web.Auth) (err error) { // No need to iterate over users and stuff if the list doesn't have tasks if len(taskMap) == 0 { @@ -688,6 +705,11 @@ func addMoreInfoToTasks(s *xorm.Session, taskMap map[int64]*Task) (err error) { return err } + taskFavorites, err := getFavorites(s, taskIDs, a, FavoriteKindTask) + if err != nil { + return err + } + // Get all identifiers lists, err := GetListsByIDs(s, listIDs) if err != nil { @@ -708,10 +730,12 @@ func addMoreInfoToTasks(s *xorm.Session, taskMap map[int64]*Task) (err error) { // Build the task identifier from the list identifier and task index task.setIdentifier(lists[task.ListID]) + + task.IsFavorite = taskFavorites[task.ID] } // Get all related tasks - err = addRelatedTasksToTasks(s, taskIDs, taskMap) + err = addRelatedTasksToTasks(s, taskIDs, taskMap, a) return } @@ -874,6 +898,12 @@ func createTask(s *xorm.Session, t *Task, a web.Auth, updateAssignees bool) (err t.setIdentifier(l) + if t.IsFavorite { + if err := addToFavorites(s, t.ID, createdBy, FavoriteKindTask); err != nil { + return err + } + } + err = events.Dispatch(&TaskCreatedEvent{ Task: t, Doer: createdBy, @@ -950,7 +980,6 @@ func (t *Task) Update(s *xorm.Session, a web.Auth) (err error) { "list_id", "bucket_id", "position", - "is_favorite", "repeat_mode", } @@ -974,6 +1003,22 @@ func (t *Task) Update(s *xorm.Session, a web.Auth) (err error) { colsToUpdate = append(colsToUpdate, "index") } + wasFavorite, err := isFavorite(s, t.ID, a, FavoriteKindTask) + if err != nil { + return + } + if t.IsFavorite && !wasFavorite { + if err := addToFavorites(s, t.ID, a, FavoriteKindTask); err != nil { + return err + } + } + + if !t.IsFavorite && wasFavorite { + if err := removeFromFavorite(s, t.ID, a, FavoriteKindTask); err != nil { + return err + } + } + // Update the labels // // Maybe FIXME: @@ -1322,7 +1367,7 @@ func (t *Task) ReadOne(s *xorm.Session, a web.Auth) (err error) { return } - err = addMoreInfoToTasks(s, taskMap) + err = addMoreInfoToTasks(s, taskMap, a) if err != nil { return } diff --git a/pkg/models/tasks_test.go b/pkg/models/tasks_test.go index 1d6ad6ac..371a98fd 100644 --- a/pkg/models/tasks_test.go +++ b/pkg/models/tasks_test.go @@ -718,14 +718,12 @@ func TestTask_ReadOne(t *testing.T) { assert.True(t, IsErrTaskDoesNotExist(err)) }) t.Run("with subscription", func(t *testing.T) { - u = &user.User{ID: 6} - db.LoadAndAssertFixtures(t) s := db.NewSession() defer s.Close() task := &Task{ID: 22} - err := task.ReadOne(s, u) + err := task.ReadOne(s, &user.User{ID: 6}) assert.NoError(t, err) assert.NotNil(t, task.Subscription) }) @@ -742,4 +740,24 @@ func TestTask_ReadOne(t *testing.T) { assert.NotNil(t, task.CreatedBy) assert.Equal(t, int64(-2), task.CreatedBy.ID) }) + t.Run("favorite", func(t *testing.T) { + db.LoadAndAssertFixtures(t) + s := db.NewSession() + defer s.Close() + + task := &Task{ID: 1} + err := task.ReadOne(s, u) + assert.NoError(t, err) + assert.True(t, task.IsFavorite) + }) + t.Run("favorite for a different user", func(t *testing.T) { + db.LoadAndAssertFixtures(t) + s := db.NewSession() + defer s.Close() + + task := &Task{ID: 1} + err := task.ReadOne(s, &user.User{ID: 2}) + assert.NoError(t, err) + assert.False(t, task.IsFavorite) + }) } diff --git a/pkg/models/unit_tests.go b/pkg/models/unit_tests.go index 51c034aa..d3a0e7b2 100644 --- a/pkg/models/unit_tests.go +++ b/pkg/models/unit_tests.go @@ -60,6 +60,7 @@ func SetupTests() { "buckets", "saved_filters", "subscriptions", + "favorites", ) if err != nil { log.Fatal(err) diff --git a/pkg/routes/caldav/listStorageProvider.go b/pkg/routes/caldav/listStorageProvider.go index 64efaf53..1171aa47 100644 --- a/pkg/routes/caldav/listStorageProvider.go +++ b/pkg/routes/caldav/listStorageProvider.go @@ -139,7 +139,7 @@ func (vcls *VikunjaCaldavListStorage) GetResourcesByList(rpaths []string) ([]dat // GetTasksByUIDs... // Parse these into ressources... - tasks, err := models.GetTasksByUIDs(s, uids) + tasks, err := models.GetTasksByUIDs(s, uids, vcls.user) if err != nil { _ = s.Rollback() return nil, err diff --git a/pkg/swagger/docs.go b/pkg/swagger/docs.go index 027fe625..1743ccd2 100644 --- a/pkg/swagger/docs.go +++ b/pkg/swagger/docs.go @@ -7322,7 +7322,7 @@ var doc = `{ "type": "integer" }, "is_favorite": { - "description": "True if a task is a favorite task. Favorite tasks show up in a separate \"Important\" list", + "description": "True if a task is a favorite task. Favorite tasks show up in a separate \"Important\" list. This value depends on the user making the call to the api.", "type": "boolean" }, "labels": { @@ -7563,7 +7563,7 @@ var doc = `{ "type": "boolean" }, "is_favorite": { - "description": "True if a list is a favorite. Favorite lists show up in a separate namespace.", + "description": "True if a list is a favorite. Favorite lists show up in a separate namespace. This value depends on the user making the call to the api.", "type": "boolean" }, "namespace_id": { @@ -7898,7 +7898,7 @@ var doc = `{ "type": "integer" }, "is_favorite": { - "description": "True if a task is a favorite task. Favorite tasks show up in a separate \"Important\" list", + "description": "True if a task is a favorite task. Favorite tasks show up in a separate \"Important\" list. This value depends on the user making the call to the api.", "type": "boolean" }, "labels": { diff --git a/pkg/swagger/swagger.json b/pkg/swagger/swagger.json index 11712ea6..6514dc4b 100644 --- a/pkg/swagger/swagger.json +++ b/pkg/swagger/swagger.json @@ -7305,7 +7305,7 @@ "type": "integer" }, "is_favorite": { - "description": "True if a task is a favorite task. Favorite tasks show up in a separate \"Important\" list", + "description": "True if a task is a favorite task. Favorite tasks show up in a separate \"Important\" list. This value depends on the user making the call to the api.", "type": "boolean" }, "labels": { @@ -7546,7 +7546,7 @@ "type": "boolean" }, "is_favorite": { - "description": "True if a list is a favorite. Favorite lists show up in a separate namespace.", + "description": "True if a list is a favorite. Favorite lists show up in a separate namespace. This value depends on the user making the call to the api.", "type": "boolean" }, "namespace_id": { @@ -7881,7 +7881,7 @@ "type": "integer" }, "is_favorite": { - "description": "True if a task is a favorite task. Favorite tasks show up in a separate \"Important\" list", + "description": "True if a task is a favorite task. Favorite tasks show up in a separate \"Important\" list. This value depends on the user making the call to the api.", "type": "boolean" }, "labels": { diff --git a/pkg/swagger/swagger.yaml b/pkg/swagger/swagger.yaml index f2652ba1..ec7e0c9a 100644 --- a/pkg/swagger/swagger.yaml +++ b/pkg/swagger/swagger.yaml @@ -183,7 +183,8 @@ definitions: type: integer is_favorite: description: True if a task is a favorite task. Favorite tasks show up in - a separate "Important" list + a separate "Important" list. This value depends on the user making the call + to the api. type: boolean labels: description: An array of labels which are associated with this task. @@ -395,7 +396,7 @@ definitions: type: boolean is_favorite: description: True if a list is a favorite. Favorite lists show up in a separate - namespace. + namespace. This value depends on the user making the call to the api. type: boolean namespace_id: type: integer @@ -665,7 +666,8 @@ definitions: type: integer is_favorite: description: True if a task is a favorite task. Favorite tasks show up in - a separate "Important" list + a separate "Important" list. This value depends on the user making the call + to the api. type: boolean labels: description: An array of labels which are associated with this task.