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:
parent
373e3f3d60
commit
d0c77ad1c1
16 changed files with 409 additions and 53 deletions
21
pkg/db/fixtures/favorites.yml
Normal file
21
pkg/db/fixtures/favorites.yml
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
112
pkg/migration/20210709211508.go
Normal file
112
pkg/migration/20210709211508.go
Normal 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
109
pkg/models/favorites.go
Normal 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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -60,6 +60,7 @@ func GetTables() []interface{} {
|
|||
&UnsplashPhoto{},
|
||||
&SavedFilter{},
|
||||
&Subscription{},
|
||||
&Favorite{},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -60,6 +60,7 @@ func SetupTests() {
|
|||
"buckets",
|
||||
"saved_filters",
|
||||
"subscriptions",
|
||||
"favorites",
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in a new issue