1
0

Favorite tasks (#653)

Fixed namespace tests

Add test for favorite tasks

Fix favorite tasks not being updated

Fix integration tests

Fix lint

Return a pseudo namespace and list for favorites

Make sure users can only see their favorites

Add condition show tasks from the favorites list

Regenerate swagger docs

Add favorite field to task

Co-authored-by: kolaente <k@knt.li>
Reviewed-on: https://kolaente.dev/vikunja/api/pulls/653
This commit is contained in:
konrad
2020-09-05 20:16:02 +00:00
parent ecf09e17a8
commit e5559137dd
17 changed files with 230 additions and 31 deletions

View File

@ -128,7 +128,7 @@ func (b *Bucket) ReadAll(auth web.Auth, search string, page int, perPage int) (r
},
},
}
tasks, _, _, err := getTasksForLists([]*List{{ID: b.ListID}}, opts)
tasks, _, _, err := getTasksForLists([]*List{{ID: b.ListID}}, auth, opts)
if err != nil {
return
}

View File

@ -217,7 +217,7 @@ func getUserTaskIDs(u *user.User) (taskIDs []int64, err error) {
return nil, err
}
tasks, _, _, err := getRawTasksForLists(lists, &taskOptions{
tasks, _, _, err := getRawTasksForLists(lists, u, &taskOptions{
page: -1,
perPage: 0,
})

View File

@ -74,6 +74,16 @@ type ListBackgroundType struct {
// ListBackgroundUpload represents the list upload background type
const ListBackgroundUpload string = "upload"
// FavoritesPseudoList holds all tasks marked as favorites
var FavoritesPseudoList = List{
ID: -1,
Title: "Favorites",
Description: "This list has all tasks marked as favorites.",
NamespaceID: FavoritesPseudoNamespace.ID,
Created: time.Now(),
Updated: time.Now(),
}
// GetListsByNamespaceID gets all lists in a namespace
func GetListsByNamespaceID(nID int64, doer *user.User) (lists []*List, err error) {
if nID == -1 {
@ -165,6 +175,12 @@ func (l *List) ReadAll(a web.Auth, search string, page int, perPage int) (result
// @Failure 500 {object} models.Message "Internal error"
// @Router /lists/{id} [get]
func (l *List) ReadOne() (err error) {
if l.ID == FavoritesPseudoList.ID {
// Already "built" the list in CanRead
return nil
}
// Get list owner
l.Owner, err = user.GetUserByID(l.OwnerID)
if err != nil {

View File

@ -106,7 +106,7 @@ func (ld *ListDuplicate) Create(a web.Auth) (err error) {
log.Debugf("Duplicated all buckets from list %d into %d", ld.ListID, ld.List.ID)
// Get all tasks + all task details
tasks, _, _, err := getTasksForLists([]*List{{ID: ld.ListID}}, &taskOptions{})
tasks, _, _, err := getTasksForLists([]*List{{ID: ld.ListID}}, a, &taskOptions{})
if err != nil {
return err
}

View File

@ -25,6 +25,11 @@ import (
// CanWrite return whether the user can write on that list or not
func (l *List) CanWrite(a web.Auth) (bool, error) {
// The favorite list can't be edited
if l.ID == FavoritesPseudoList.ID {
return false, nil
}
// Get the list and check the right
originalList := &List{ID: l.ID}
err := originalList.GetSimpleByID()
@ -63,6 +68,19 @@ func (l *List) CanWrite(a web.Auth) (bool, error) {
// CanRead checks if a user has read access to a list
func (l *List) CanRead(a web.Auth) (bool, int, error) {
// The favorite list needs a special treatment
if l.ID == FavoritesPseudoList.ID {
owner, err := user.GetFromAuth(a)
if err != nil {
return false, 0, err
}
*l = FavoritesPseudoList
l.Owner = owner
return true, int(RightRead), nil
}
// Check if the user is either owner or can read
if err := l.GetSimpleByID(); err != nil {
return false, 0, err
@ -83,6 +101,10 @@ func (l *List) CanRead(a web.Auth) (bool, int, error) {
// CanUpdate checks if the user can update a list
func (l *List) CanUpdate(a web.Auth) (canUpdate bool, err error) {
// The favorite list can't be edited
if l.ID == FavoritesPseudoList.ID {
return false, nil
}
canUpdate, err = l.CanWrite(a)
// If the list is archived and the user tries to un-archive it, let the request through
if IsErrListIsArchived(err) && !l.IsArchived {
@ -105,6 +127,11 @@ func (l *List) CanCreate(a web.Auth) (bool, error) {
// IsAdmin returns whether the user has admin rights on the list or not
func (l *List) IsAdmin(a web.Auth) (bool, error) {
// The favorite list can't be edited
if l.ID == FavoritesPseudoList.ID {
return false, nil
}
originalList := &List{ID: l.ID}
err := originalList.GetSimpleByID()
if err != nil {

View File

@ -62,6 +62,15 @@ var PseudoNamespace = Namespace{
Updated: time.Now(),
}
// FavoritesPseudoNamespace is a pseudo namespace used to hold favorited lists and tasks
var FavoritesPseudoNamespace = Namespace{
ID: -2,
Title: "Favorites",
Description: "Favorite lists and tasks.",
Created: time.Now(),
Updated: time.Now(),
}
// TableName makes beautiful table names
func (Namespace) TableName() string {
return "namespaces"
@ -79,6 +88,11 @@ func (n *Namespace) GetSimpleByID() (err error) {
return
}
if n.ID == FavoritesPseudoNamespace.ID {
*n = FavoritesPseudoNamespace
return
}
namespaceFromDB := &Namespace{}
exists, err := x.Where("id = ?", n.ID).Get(namespaceFromDB)
if err != nil {
@ -180,8 +194,17 @@ func (n *Namespace) ReadAll(a web.Auth, search string, page int, perPage int) (r
)
}
// Create our pseudo-namespace to hold the shared lists
// Create our pseudo namespace with favorite lists
// We want this one at the beginning, which is why we create it here
pseudoFavoriteNamespace := FavoritesPseudoNamespace
pseudoFavoriteNamespace.Owner = doer
all = append(all, &NamespaceWithLists{
Namespace: pseudoFavoriteNamespace,
Lists: []*List{{}},
})
*all[0].Lists[0] = FavoritesPseudoList // Copying the list to be able to modify it later
// Create our pseudo namespace to hold the shared lists
pseudonamespace := PseudoNamespace
pseudonamespace.Owner = doer
all = append(all, &NamespaceWithLists{
@ -266,6 +289,19 @@ func (n *Namespace) ReadAll(a web.Auth, search string, page int, perPage int) (r
// Remove the pseudonamespace if we don't have any shared lists
if len(individualLists) == 0 {
all = append(all[:1], all[2:]...)
}
// Check if we have any favorites and remove the favorites namespace from the list if not
favoriteCount, err := x.
Join("INNER", "list", "tasks.list_id = list.id").
Join("INNER", "namespaces", "list.namespace_id = namespaces.id").
Where(builder.And(builder.Eq{"is_favorite": true}, builder.In("namespaces.id", namespaceids))).
Count(&Task{})
if err != nil {
return nil, 0, 0, err
}
if favoriteCount == 0 {
all = append(all[:0], all[1:]...)
}

View File

@ -143,8 +143,9 @@ func TestNamespace_ReadAll(t *testing.T) {
namespaces := nn.([]*NamespaceWithLists)
assert.NoError(t, err)
assert.NotNil(t, namespaces)
assert.Len(t, namespaces, 9) // Total of 9 including shared
assert.Equal(t, int64(-1), namespaces[0].ID) // The first one should be the one with the shared namespaces
assert.Len(t, namespaces, 10) // Total of 10 including shared & favorites
assert.Equal(t, int64(-2), namespaces[0].ID) // The first one should be the one with favorites
assert.Equal(t, int64(-1), namespaces[1].ID) // The second one should be the one with the shared namespaces
// Ensure every list and namespace are not archived
for _, namespace := range namespaces {
assert.False(t, namespace.IsArchived)
@ -161,7 +162,8 @@ func TestNamespace_ReadAll(t *testing.T) {
namespaces := nn.([]*NamespaceWithLists)
assert.NoError(t, err)
assert.NotNil(t, namespaces)
assert.Len(t, namespaces, 10) // Total of 10 including shared, one is archived
assert.Equal(t, int64(-1), namespaces[0].ID) // The first one should be the one with the shared namespaces
assert.Len(t, namespaces, 11) // Total of 11 including shared & favorites, one is archived
assert.Equal(t, int64(-2), namespaces[0].ID) // The first one should be the one with favorites
assert.Equal(t, int64(-1), namespaces[1].ID) // The second one should be the one with the shared namespaces
})
}

View File

@ -150,7 +150,7 @@ func (tf *TaskCollection) ReadAll(a web.Auth, search string, page int, perPage i
if err != nil {
return nil, 0, 0, err
}
return getTasksForLists([]*List{list}, taskopts)
return getTasksForLists([]*List{list}, a, taskopts)
}
// If the list ID is not set, we get all tasks for the user.
@ -176,5 +176,5 @@ func (tf *TaskCollection) ReadAll(a web.Auth, search string, page int, perPage i
tf.Lists = []*List{{ID: tf.ListID}}
}
return getTasksForLists(tf.Lists, taskopts)
return getTasksForLists(tf.Lists, a, taskopts)
}

View File

@ -66,6 +66,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
CreatedBy: user1,
ListID: 1,
BucketID: 1,
IsFavorite: true,
Labels: []*Label{
{
ID: 4,
@ -288,6 +289,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
CreatedByID: 6,
CreatedBy: user6,
ListID: 6,
IsFavorite: true,
RelatedTasks: map[RelationKind][]*Task{},
BucketID: 6,
Created: time.Unix(1543626724, 0).In(loc),
@ -484,6 +486,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
Index: 1,
CreatedByID: 1,
ListID: 1,
IsFavorite: true,
Created: time.Unix(1543626724, 0).In(loc),
Updated: time.Unix(1543626724, 0).In(loc),
BucketID: 1,
@ -836,6 +839,18 @@ func TestTaskCollection_ReadAll(t *testing.T) {
},
wantErr: false,
},
{
name: "favorited tasks",
args: defaultArgs,
fields: fields{
ListID: FavoritesPseudoList.ID,
},
want: []*Task{
task1,
task15,
// Task 34 is also a favorite, but on a list user 1 has no access to.
},
},
}
for _, tt := range tests {

View File

@ -84,6 +84,9 @@ 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"`
// A timestamp when this task was created. You cannot change this value.
Created time.Time `xorm:"created not null" json:"created"`
// A timestamp when this task was last updated. You cannot change this value.
@ -165,7 +168,7 @@ func (t *Task) ReadAll(a web.Auth, search string, page int, perPage int) (result
return nil, 0, 0, nil
}
func getRawTasksForLists(lists []*List, opts *taskOptions) (tasks []*Task, resultCount int, totalItems int64, err error) {
func getRawTasksForLists(lists []*List, a web.Auth, opts *taskOptions) (tasks []*Task, resultCount int, totalItems int64, err error) {
// If the user does not have any lists, don't try to get any tasks
if len(lists) == 0 {
@ -179,7 +182,11 @@ func getRawTasksForLists(lists []*List, opts *taskOptions) (tasks []*Task, resul
// Get all list IDs and get the tasks
var listIDs []int64
var hasFavoriteLists bool
for _, l := range lists {
if l.ID == FavoritesPseudoList.ID {
hasFavoriteLists = true
}
listIDs = append(listIDs, l.ID)
}
@ -274,11 +281,34 @@ func getRawTasksForLists(lists []*List, opts *taskOptions) (tasks []*Task, resul
}
}
var listIDCond builder.Cond
var listCond builder.Cond
if len(listIDs) > 0 {
query = query.In("list_id", listIDs)
queryCount = queryCount.In("list_id", listIDs)
listIDCond = builder.In("list_id", listIDs)
listCond = listIDCond
}
if hasFavoriteLists {
// Make sure users can only see their favorites
userLists, _, _, err := getRawListsForUser(&listOptions{
user: &user.User{ID: a.GetID()},
page: -1,
})
if err != nil {
return nil, 0, 0, err
}
userListIDs := make([]int64, len(userLists))
for _, l := range userLists {
userListIDs = append(userListIDs, l.ID)
}
listCond = builder.Or(listIDCond, builder.And(builder.Eq{"is_favorite": true}, builder.In("list_id", userListIDs)))
}
query = query.Where(listCond)
queryCount = queryCount.Where(listCond)
if len(filters) > 0 {
if opts.filterConcat == filterConcatOr {
query = query.Where(builder.Or(filters...))
@ -311,9 +341,9 @@ func getRawTasksForLists(lists []*List, opts *taskOptions) (tasks []*Task, resul
return tasks, len(tasks), totalItems, nil
}
func getTasksForLists(lists []*List, opts *taskOptions) (tasks []*Task, resultCount int, totalItems int64, err error) {
func getTasksForLists(lists []*List, a web.Auth, opts *taskOptions) (tasks []*Task, resultCount int, totalItems int64, err error) {
tasks, resultCount, totalItems, err = getRawTasksForLists(lists, opts)
tasks, resultCount, totalItems, err = getRawTasksForLists(lists, a, opts)
if err != nil {
return nil, 0, 0, err
}
@ -760,6 +790,7 @@ func (t *Task) Update() (err error) {
"bucket_id",
"position",
"repeat_from_current_date",
"is_favorite",
}
// Make sure we have a bucket
@ -868,6 +899,10 @@ func (t *Task) Update() (err error) {
if !t.RepeatFromCurrentDate {
ot.RepeatFromCurrentDate = false
}
// Is Favorite
if !t.IsFavorite {
ot.IsFavorite = false
}
_, err = s.ID(t.ID).
Cols(colsToUpdate...).