Make sure list / task favorites are set per user, not per entity (#915)

Co-authored-by: kolaente <k@knt.li>
Reviewed-on: https://kolaente.dev/vikunja/api/pulls/915
Co-authored-by: konrad <konrad@kola-entertainments.de>
Co-committed-by: konrad <konrad@kola-entertainments.de>
This commit is contained in:
konrad 2021-07-10 10:21:54 +00:00
parent 373e3f3d60
commit d0c77ad1c1
16 changed files with 409 additions and 53 deletions

View file

@ -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

View file

@ -207,6 +207,5 @@
identifier: test23 identifier: test23
owner_id: 12 owner_id: 12
namespace_id: 17 namespace_id: 17
is_favorite: true
updated: 2018-12-02 15:13:12 updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12 created: 2018-12-01 15:13:12

View file

@ -8,7 +8,6 @@
created: 2018-12-01 01:12:04 created: 2018-12-01 01:12:04
updated: 2018-12-01 01:12:04 updated: 2018-12-01 01:12:04
bucket_id: 1 bucket_id: 1
is_favorite: true
- id: 2 - id: 2
title: 'task #2 done' title: 'task #2 done'
done: true done: true
@ -141,7 +140,6 @@
list_id: 6 list_id: 6
index: 1 index: 1
bucket_id: 6 bucket_id: 6
is_favorite: true
created: 2018-12-01 01:12:04 created: 2018-12-01 01:12:04
updated: 2018-12-01 01:12:04 updated: 2018-12-01 01:12:04
- id: 16 - id: 16
@ -317,7 +315,6 @@
list_id: 20 list_id: 20
index: 20 index: 20
bucket_id: 5 bucket_id: 5
is_favorite: true
created: 2018-12-01 01:12:04 created: 2018-12-01 01:12:04
updated: 2018-12-01 01:12:04 updated: 2018-12-01 01:12:04
- id: 35 - id: 35

View file

@ -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 <https://www.gnu.org/licenses/>.
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
},
})
}

109
pkg/models/favorites.go Normal file
View file

@ -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 <https://www.gnu.org/licenses/>.
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
}

View file

@ -209,7 +209,7 @@ func (b *Bucket) ReadAll(s *xorm.Session, auth web.Auth, search string, page int
taskMap[t.ID] = t taskMap[t.ID] = t
} }
err = addMoreInfoToTasks(s, taskMap) err = addMoreInfoToTasks(s, taskMap, auth)
if err != nil { if err != nil {
return nil, 0, 0, err return nil, 0, 0, err
} }

View file

@ -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 // 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"` BackgroundInformation interface{} `xorm:"-" json:"background_information"`
// True if a list is a favorite. Favorite lists show up in a separate namespace. // 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:"default false" json:"is_favorite"` 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. // 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. // 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 // get more list details
err = addListDetails(s, lists) err = addListDetails(s, lists, doer)
return lists, err 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 return nil, 0, 0, err
} }
lists := []*List{list} lists := []*List{list}
err = addListDetails(s, lists) err = addListDetails(s, lists, a)
return lists, 0, 0, err 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 // Add more list details
err = addListDetails(s, lists) err = addListDetails(s, lists, a)
return lists, resultCount, totalItems, err 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) l.Subscription, err = GetSubscription(s, SubscriptionEntityList, l.ID, a)
return 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 // 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 { if len(lists) == 0 {
return return
} }
@ -441,7 +446,9 @@ func addListDetails(s *xorm.Session, lists []*List) (err error) {
} }
var fileIDs []int64 var fileIDs []int64
var listIDs []int64
for _, l := range lists { for _, l := range lists {
listIDs = append(listIDs, l.ID)
if o, exists := owners[l.OwnerID]; exists { if o, exists := owners[l.OwnerID]; exists {
l.Owner = o l.Owner = o
} }
@ -451,6 +458,15 @@ func addListDetails(s *xorm.Session, lists []*List) (err error) {
fileIDs = append(fileIDs, l.BackgroundFileID) 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 { if len(fileIDs) == 0 {
return return
} }
@ -536,6 +552,14 @@ func CreateOrUpdateList(s *xorm.Session, list *List, auth web.Auth) (err error)
if list.ID == 0 { if list.ID == 0 {
_, err = s.Insert(list) _, err = s.Insert(list)
if err != nil {
return
}
if list.IsFavorite {
if err := addToFavorites(s, list.ID, auth, FavoriteKindList); err != nil {
return err
}
}
} else { } else {
// We need to specify the cols we want to update here to be able to un-archive lists // We need to specify the cols we want to update here to be able to un-archive lists
colsToUpdate := []string{ colsToUpdate := []string{
@ -543,17 +567,35 @@ func CreateOrUpdateList(s *xorm.Session, list *List, auth web.Auth) (err error)
"is_archived", "is_archived",
"identifier", "identifier",
"hex_color", "hex_color",
"is_favorite",
"background_file_id", "background_file_id",
} }
if list.Description != "" { if list.Description != "" {
colsToUpdate = append(colsToUpdate, "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. _, err = s.
ID(list.ID). ID(list.ID).
Cols(colsToUpdate...). Cols(colsToUpdate...).
Update(list) Update(list)
if err != nil {
return err
}
} }
if err != nil { if err != nil {
@ -568,7 +610,6 @@ func CreateOrUpdateList(s *xorm.Session, list *List, auth web.Auth) (err error)
*list = *l *list = *l
err = list.ReadOne(s, auth) err = list.ReadOne(s, auth)
return return
} }
// Update implements the update method of CRUDable // 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 return err
} }
f.IsFavorite = l.IsFavorite
f.Title = l.Title f.Title = l.Title
f.Description = l.Description f.Description = l.Description
err = f.Update(s, a) err = f.Update(s, a)

View file

@ -60,6 +60,7 @@ func GetTables() []interface{} {
&UnsplashPhoto{}, &UnsplashPhoto{},
&SavedFilter{}, &SavedFilter{},
&Subscription{}, &Subscription{},
&Favorite{},
} }
} }

View file

@ -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 // Check if we have any favorites or favorited lists and remove the favorites namespace from the list if not
var favoriteCount int64 cond := builder.
favoriteCount, err = s. Select("tasks.id").
From("tasks").
Join("INNER", "lists", "tasks.list_id = lists.id"). Join("INNER", "lists", "tasks.list_id = lists.id").
Join("INNER", "namespaces", "lists.namespace_id = namespaces.id"). Join("INNER", "namespaces", "lists.namespace_id = namespaces.id").
Where(builder.And(builder.Eq{"tasks.is_favorite": true}, builder.In("namespaces.id", namespaceIDs))). Where(builder.In("namespaces.id", namespaceIDs))
Count(&Task{})
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 { if err != nil {
return return
} }
@ -538,6 +547,13 @@ func (n *Namespace) ReadAll(s *xorm.Session, a web.Auth, search string, page int
lists = append(lists, savedFiltersNamespace.Lists...) lists = append(lists, savedFiltersNamespace.Lists...)
} }
/////////////////
// Add list details (favorite state, among other things
err = addListDetails(s, lists, a)
if err != nil {
return
}
///////////////// /////////////////
// Favorite lists // Favorite lists
@ -553,11 +569,6 @@ func (n *Namespace) ReadAll(s *xorm.Session, a web.Auth, search string, page int
////////////////////// //////////////////////
// Put it all together // Put it all together
err = addListDetails(s, lists)
if err != nil {
return
}
for _, list := range lists { for _, list := range lists {
if list.NamespaceID == SharedListsPseudoNamespace.ID || list.NamespaceID == SavedFiltersPseudoNamespace.ID { if list.NamespaceID == SharedListsPseudoNamespace.ID || list.NamespaceID == SavedFiltersPseudoNamespace.ID {
// Shared lists and filtered lists are already in the namespace // Shared lists and filtered lists are already in the namespace

View file

@ -59,8 +59,7 @@ type Task struct {
// The time when the task is due. // The time when the task is due.
DueDate time.Time `xorm:"DATETIME INDEX null 'due_date'" json:"due_date"` 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. // An array of datetimes when the user wants to be reminded of the task.
Reminders []time.Time `xorm:"-" json:"reminder_dates"` 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
// The list this task belongs to. // The list this task belongs to.
ListID int64 `xorm:"bigint INDEX not null" json:"list_id" param:"list"` 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. // 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 // All attachments this task has
Attachments []*TaskAttachment `xorm:"-" json:"attachments"` Attachments []*TaskAttachment `xorm:"-" json:"attachments"`
// True if a task is a favorite task. Favorite tasks show up in a separate "Important" list // 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:"default false" json:"is_favorite"` 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. // 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. // Will only returned when retreiving one task.
@ -120,7 +119,8 @@ type Task struct {
Position float64 `xorm:"double null" json:"position"` Position float64 `xorm:"double null" json:"position"`
// The user who initially created the task. // 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.CRUDable `xorm:"-" json:"-"`
web.Rights `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 // Get all list IDs and get the tasks
var listIDs []int64 var listIDs []int64
var hasFavoriteLists bool var hasFavoritesList bool
for _, l := range lists { for _, l := range lists {
if l.ID == FavoritesPseudoList.ID { if l.ID == FavoritesPseudoList.ID {
hasFavoriteLists = true hasFavoritesList = true
continue
} }
listIDs = append(listIDs, l.ID) listIDs = append(listIDs, l.ID)
} }
@ -375,7 +376,7 @@ func getRawTasksForLists(s *xorm.Session, lists []*List, a web.Auth, opts *taskO
listCond = listIDCond listCond = listIDCond
} }
if hasFavoriteLists { if hasFavoritesList {
// Make sure users can only see their favorites // Make sure users can only see their favorites
userLists, _, _, err := getRawListsForUser( userLists, _, _, err := getRawListsForUser(
s, s,
@ -393,7 +394,17 @@ func getRawTasksForLists(s *xorm.Session, lists []*List, a web.Auth, opts *taskO
userListIDs = append(userListIDs, l.ID) 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 { if len(reminderFilters) > 0 {
@ -473,7 +484,7 @@ func getTasksForLists(s *xorm.Session, lists []*List, a web.Auth, opts *taskOpti
taskMap[t.ID] = t taskMap[t.ID] = t
} }
err = addMoreInfoToTasks(s, taskMap) err = addMoreInfoToTasks(s, taskMap, a)
if err != nil { if err != nil {
return nil, 0, 0, err 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 // 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{} tasks = []*Task{}
err = s.In("uid", uids).Find(&tasks) err = s.In("uid", uids).Find(&tasks)
if err != nil { if err != nil {
@ -533,7 +544,7 @@ func GetTasksByUIDs(s *xorm.Session, uids []string) (tasks []*Task, err error) {
taskMap[t.ID] = t taskMap[t.ID] = t
} }
err = addMoreInfoToTasks(s, taskMap) err = addMoreInfoToTasks(s, taskMap, a)
return return
} }
@ -611,7 +622,7 @@ func getTaskReminderMap(s *xorm.Session, taskIDs []int64) (taskReminders map[int
return 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{} relatedTasks := []*TaskRelation{}
err = s.In("task_id", taskIDs).Find(&relatedTasks) err = s.In("task_id", taskIDs).Find(&relatedTasks)
if err != nil { if err != nil {
@ -634,10 +645,16 @@ func addRelatedTasksToTasks(s *xorm.Session, taskIDs []int64, taskMap map[int64]
return 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. // 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 // Go through all task relations and put them into the task objects
for _, rt := range relatedTasks { 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]) 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 // 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 // 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 // No need to iterate over users and stuff if the list doesn't have tasks
if len(taskMap) == 0 { if len(taskMap) == 0 {
@ -688,6 +705,11 @@ func addMoreInfoToTasks(s *xorm.Session, taskMap map[int64]*Task) (err error) {
return err return err
} }
taskFavorites, err := getFavorites(s, taskIDs, a, FavoriteKindTask)
if err != nil {
return err
}
// Get all identifiers // Get all identifiers
lists, err := GetListsByIDs(s, listIDs) lists, err := GetListsByIDs(s, listIDs)
if err != nil { 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 // Build the task identifier from the list identifier and task index
task.setIdentifier(lists[task.ListID]) task.setIdentifier(lists[task.ListID])
task.IsFavorite = taskFavorites[task.ID]
} }
// Get all related tasks // Get all related tasks
err = addRelatedTasksToTasks(s, taskIDs, taskMap) err = addRelatedTasksToTasks(s, taskIDs, taskMap, a)
return return
} }
@ -874,6 +898,12 @@ func createTask(s *xorm.Session, t *Task, a web.Auth, updateAssignees bool) (err
t.setIdentifier(l) t.setIdentifier(l)
if t.IsFavorite {
if err := addToFavorites(s, t.ID, createdBy, FavoriteKindTask); err != nil {
return err
}
}
err = events.Dispatch(&TaskCreatedEvent{ err = events.Dispatch(&TaskCreatedEvent{
Task: t, Task: t,
Doer: createdBy, Doer: createdBy,
@ -950,7 +980,6 @@ func (t *Task) Update(s *xorm.Session, a web.Auth) (err error) {
"list_id", "list_id",
"bucket_id", "bucket_id",
"position", "position",
"is_favorite",
"repeat_mode", "repeat_mode",
} }
@ -974,6 +1003,22 @@ func (t *Task) Update(s *xorm.Session, a web.Auth) (err error) {
colsToUpdate = append(colsToUpdate, "index") 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 // Update the labels
// //
// Maybe FIXME: // Maybe FIXME:
@ -1322,7 +1367,7 @@ func (t *Task) ReadOne(s *xorm.Session, a web.Auth) (err error) {
return return
} }
err = addMoreInfoToTasks(s, taskMap) err = addMoreInfoToTasks(s, taskMap, a)
if err != nil { if err != nil {
return return
} }

View file

@ -718,14 +718,12 @@ func TestTask_ReadOne(t *testing.T) {
assert.True(t, IsErrTaskDoesNotExist(err)) assert.True(t, IsErrTaskDoesNotExist(err))
}) })
t.Run("with subscription", func(t *testing.T) { t.Run("with subscription", func(t *testing.T) {
u = &user.User{ID: 6}
db.LoadAndAssertFixtures(t) db.LoadAndAssertFixtures(t)
s := db.NewSession() s := db.NewSession()
defer s.Close() defer s.Close()
task := &Task{ID: 22} task := &Task{ID: 22}
err := task.ReadOne(s, u) err := task.ReadOne(s, &user.User{ID: 6})
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, task.Subscription) assert.NotNil(t, task.Subscription)
}) })
@ -742,4 +740,24 @@ func TestTask_ReadOne(t *testing.T) {
assert.NotNil(t, task.CreatedBy) assert.NotNil(t, task.CreatedBy)
assert.Equal(t, int64(-2), task.CreatedBy.ID) 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)
})
} }

View file

@ -60,6 +60,7 @@ func SetupTests() {
"buckets", "buckets",
"saved_filters", "saved_filters",
"subscriptions", "subscriptions",
"favorites",
) )
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)

View file

@ -139,7 +139,7 @@ func (vcls *VikunjaCaldavListStorage) GetResourcesByList(rpaths []string) ([]dat
// GetTasksByUIDs... // GetTasksByUIDs...
// Parse these into ressources... // Parse these into ressources...
tasks, err := models.GetTasksByUIDs(s, uids) tasks, err := models.GetTasksByUIDs(s, uids, vcls.user)
if err != nil { if err != nil {
_ = s.Rollback() _ = s.Rollback()
return nil, err return nil, err

View file

@ -7322,7 +7322,7 @@ var doc = `{
"type": "integer" "type": "integer"
}, },
"is_favorite": { "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" "type": "boolean"
}, },
"labels": { "labels": {
@ -7563,7 +7563,7 @@ var doc = `{
"type": "boolean" "type": "boolean"
}, },
"is_favorite": { "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" "type": "boolean"
}, },
"namespace_id": { "namespace_id": {
@ -7898,7 +7898,7 @@ var doc = `{
"type": "integer" "type": "integer"
}, },
"is_favorite": { "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" "type": "boolean"
}, },
"labels": { "labels": {

View file

@ -7305,7 +7305,7 @@
"type": "integer" "type": "integer"
}, },
"is_favorite": { "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" "type": "boolean"
}, },
"labels": { "labels": {
@ -7546,7 +7546,7 @@
"type": "boolean" "type": "boolean"
}, },
"is_favorite": { "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" "type": "boolean"
}, },
"namespace_id": { "namespace_id": {
@ -7881,7 +7881,7 @@
"type": "integer" "type": "integer"
}, },
"is_favorite": { "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" "type": "boolean"
}, },
"labels": { "labels": {

View file

@ -183,7 +183,8 @@ definitions:
type: integer type: integer
is_favorite: is_favorite:
description: True if a task is a favorite task. Favorite tasks show up in 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 type: boolean
labels: labels:
description: An array of labels which are associated with this task. description: An array of labels which are associated with this task.
@ -395,7 +396,7 @@ definitions:
type: boolean type: boolean
is_favorite: is_favorite:
description: True if a list is a favorite. Favorite lists show up in a separate 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 type: boolean
namespace_id: namespace_id:
type: integer type: integer
@ -665,7 +666,8 @@ definitions:
type: integer type: integer
is_favorite: is_favorite:
description: True if a task is a favorite task. Favorite tasks show up in 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 type: boolean
labels: labels:
description: An array of labels which are associated with this task. description: An array of labels which are associated with this task.