1
0

Add labels to tasks (#45)

This commit is contained in:
konrad
2018-12-31 01:18:41 +00:00
committed by Gitea
parent d39007baa0
commit 6b40df50d3
45 changed files with 9101 additions and 57 deletions

View File

@ -474,6 +474,34 @@ func (err ErrBulkTasksNeedAtLeastOne) HTTPError() web.HTTPError {
return web.HTTPError{HTTPCode: http.StatusBadRequest, Code: ErrCodeBulkTasksNeedAtLeastOne, Message: "Need at least one tasks to do bulk editing."}
}
// ErrNoRightToSeeTask represents an error where a user does not have the right to see a task
type ErrNoRightToSeeTask struct {
TaskID int64
UserID int64
}
// IsErrNoRightToSeeTask checks if an error is ErrNoRightToSeeTask.
func IsErrNoRightToSeeTask(err error) bool {
_, ok := err.(ErrNoRightToSeeTask)
return ok
}
func (err ErrNoRightToSeeTask) Error() string {
return fmt.Sprintf("User does not have the right to see the task [TaskID: %v, UserID: %v]", err.TaskID, err.UserID)
}
// ErrCodeNoRightToSeeTask holds the unique world-error code of this error
const ErrCodeNoRightToSeeTask = 4005
// HTTPError holds the http error description
func (err ErrNoRightToSeeTask) HTTPError() web.HTTPError {
return web.HTTPError{
HTTPCode: http.StatusForbidden,
Code: ErrCodeNoRightToSeeTask,
Message: "You don't have the right to see this task.",
}
}
// =================
// Namespace errors
// =================
@ -864,3 +892,62 @@ const ErrCodeUserDoesNotHaveAccessToList = 7003
func (err ErrUserDoesNotHaveAccessToList) HTTPError() web.HTTPError {
return web.HTTPError{HTTPCode: http.StatusForbidden, Code: ErrCodeUserDoesNotHaveAccessToList, Message: "This user does not have access to the list."}
}
// =============
// Label errors
// =============
// ErrLabelIsAlreadyOnTask represents an error where a label is already bound to a task
type ErrLabelIsAlreadyOnTask struct {
LabelID int64
TaskID int64
}
// IsErrLabelIsAlreadyOnTask checks if an error is ErrLabelIsAlreadyOnTask.
func IsErrLabelIsAlreadyOnTask(err error) bool {
_, ok := err.(ErrLabelIsAlreadyOnTask)
return ok
}
func (err ErrLabelIsAlreadyOnTask) Error() string {
return fmt.Sprintf("Label already exists on task [TaskID: %v, LabelID: %v]", err.TaskID, err.LabelID)
}
// ErrCodeLabelIsAlreadyOnTask holds the unique world-error code of this error
const ErrCodeLabelIsAlreadyOnTask = 8001
// HTTPError holds the http error description
func (err ErrLabelIsAlreadyOnTask) HTTPError() web.HTTPError {
return web.HTTPError{
HTTPCode: http.StatusBadRequest,
Code: ErrCodeLabelIsAlreadyOnTask,
Message: "This label already exists on the task.",
}
}
// ErrLabelDoesNotExist represents an error where a label does not exist
type ErrLabelDoesNotExist struct {
LabelID int64
}
// IsErrLabelDoesNotExist checks if an error is ErrLabelDoesNotExist.
func IsErrLabelDoesNotExist(err error) bool {
_, ok := err.(ErrLabelDoesNotExist)
return ok
}
func (err ErrLabelDoesNotExist) Error() string {
return fmt.Sprintf("Label does not exist [LabelID: %v]", err.LabelID)
}
// ErrCodeLabelDoesNotExist holds the unique world-error code of this error
const ErrCodeLabelDoesNotExist = 8002
// HTTPError holds the http error description
func (err ErrLabelDoesNotExist) HTTPError() web.HTTPError {
return web.HTTPError{
HTTPCode: http.StatusNotFound,
Code: ErrCodeLabelDoesNotExist,
Message: "This label does not exist.",
}
}

View File

@ -0,0 +1,3 @@
- id: 1
task_id: 1
label_id: 4

View File

@ -0,0 +1,12 @@
- id: 1
title: 'Label #1'
created_by_id: 1
- id: 2
title: 'Label #2'
created_by_id: 1
- id: 3
title: 'Label #3 - other user'
created_by_id: 2
- id: 4
title: 'Label #4 - visible via other task'
created_by_id: 2

View File

@ -21,4 +21,10 @@
title: Test4
description: Lorem Ipsum
owner_id: 3
namespace_id: 3
namespace_id: 3
-
id: 5
title: Test5
description: Lorem Ipsum
owner_id: 5
namespace_id: 5

View File

@ -84,4 +84,10 @@
created_by_id: 1
list_id: 2
created: 1543626724
updated: 1543626724
- id: 14
text: 'task #14 basic other list'
created_by_id: 5
list_id: 5
created: 1543626724
updated: 1543626724

59
pkg/models/label.go Normal file
View File

@ -0,0 +1,59 @@
// Vikunja is a todo-list application to facilitate your life.
// Copyright 2018 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 General Public License 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package models
import (
"code.vikunja.io/web"
)
// Label represents a label
type Label struct {
ID int64 `xorm:"int(11) autoincr not null unique pk" json:"id" param:"label"`
Title string `xorm:"varchar(250) not null" json:"title" valid:"runelength(3|250)"`
Description string `xorm:"varchar(250)" json:"description" valid:"runelength(0|250)"`
HexColor string `xorm:"varchar(6)" json:"hex_color" valid:"runelength(0|6)"`
CreatedByID int64 `xorm:"int(11) not null" json:"-"`
CreatedBy *User `xorm:"-" json:"created_by"`
Created int64 `xorm:"created" json:"created"`
Updated int64 `xorm:"updated" json:"updated"`
web.CRUDable `xorm:"-" json:"-"`
web.Rights `xorm:"-" json:"-"`
}
// TableName makes a pretty table name
func (Label) TableName() string {
return "labels"
}
// LabelTask represents a relation between a label and a task
type LabelTask struct {
ID int64 `xorm:"int(11) autoincr not null unique pk" json:"id"`
TaskID int64 `xorm:"int(11) INDEX not null" json:"-" param:"listtask"`
LabelID int64 `xorm:"int(11) INDEX not null" json:"label_id" param:"label"`
Created int64 `xorm:"created" json:"created"`
web.CRUDable `xorm:"-" json:"-"`
web.Rights `xorm:"-" json:"-"`
}
// TableName makes a pretty table name
func (LabelTask) TableName() string {
return "label_task"
}

View File

@ -0,0 +1,87 @@
// Vikunja is a todo-list application to facilitate your life.
// Copyright 2018 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 General Public License 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package models
import "code.vikunja.io/web"
// Create creates a new label
// @Summary Create a label
// @Description Creates a new label.
// @tags labels
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param label body models.Label true "The label object"
// @Success 200 {object} models.Label "The created label object."
// @Failure 400 {object} code.vikunja.io/web.HTTPError "Invalid label object provided."
// @Failure 500 {object} models.Message "Internal error"
// @Router /labels [put]
func (l *Label) Create(a web.Auth) (err error) {
u, err := getUserWithError(a)
if err != nil {
return
}
l.CreatedBy = u
l.CreatedByID = u.ID
_, err = x.Insert(l)
return
}
// Update updates a label
// @Summary Update a label
// @Description Update an existing label. The user needs to be the creator of the label to be able to do this.
// @tags labels
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param id path int true "Label ID"
// @Param label body models.Label true "The label object"
// @Success 200 {object} models.Label "The created label object."
// @Failure 400 {object} code.vikunja.io/web.HTTPError "Invalid label object provided."
// @Failure 403 {object} code.vikunja.io/web.HTTPError "Not allowed to update the label."
// @Failure 404 {object} code.vikunja.io/web.HTTPError "Label not found."
// @Failure 500 {object} models.Message "Internal error"
// @Router /labels/{id} [put]
func (l *Label) Update() (err error) {
_, err = x.ID(l.ID).Update(l)
if err != nil {
return
}
err = l.ReadOne()
return
}
// Delete deletes a label
// @Summary Delete a label
// @Description Delete an existing label. The user needs to be the creator of the label to be able to do this.
// @tags labels
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param id path int true "Label ID"
// @Success 200 {object} models.Label "The label was successfully deleted."
// @Failure 403 {object} code.vikunja.io/web.HTTPError "Not allowed to delete the label."
// @Failure 404 {object} code.vikunja.io/web.HTTPError "Label not found."
// @Failure 500 {object} models.Message "Internal error"
// @Router /labels/{id} [delete]
func (l *Label) Delete() (err error) {
_, err = x.ID(l.ID).Delete(&Label{})
return err
}

106
pkg/models/label_read.go Normal file
View File

@ -0,0 +1,106 @@
// Vikunja is a todo-list application to facilitate your life.
// Copyright 2018 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 General Public License 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package models
import (
"code.vikunja.io/web"
"time"
)
// ReadAll gets all labels a user can use
// @Summary Get all labels a user has access to
// @Description Returns all labels which are either created by the user or associated with a task the user has at least read-access to.
// @tags labels
// @Accept json
// @Produce json
// @Param p query int false "The page number. Used for pagination. If not provided, the first page of results is returned."
// @Param s query string false "Search labels by label text."
// @Security ApiKeyAuth
// @Success 200 {array} models.Label "The labels"
// @Failure 500 {object} models.Message "Internal error"
// @Router /labels [get]
func (l *Label) ReadAll(search string, a web.Auth, page int) (ls interface{}, err error) {
u, err := getUserWithError(a)
if err != nil {
return nil, err
}
// Get all tasks
taskIDs, err := getUserTaskIDs(u)
if err != nil {
return nil, err
}
return getLabelsByTaskIDs(search, u, page, taskIDs, true)
}
// ReadOne gets one label
// @Summary Gets one label
// @Description Returns one label by its ID.
// @tags labels
// @Accept json
// @Produce json
// @Param id path int true "Label ID"
// @Security ApiKeyAuth
// @Success 200 {object} models.Label "The label"
// @Failure 403 {object} code.vikunja.io/web.HTTPError "The user does not have access to the label"
// @Failure 404 {object} code.vikunja.io/web.HTTPError "Label not found"
// @Failure 500 {object} models.Message "Internal error"
// @Router /labels/{id} [get]
func (l *Label) ReadOne() (err error) {
label, err := getLabelByIDSimple(l.ID)
if err != nil {
return err
}
*l = *label
user, err := GetUserByID(l.CreatedByID)
if err != nil {
return err
}
l.CreatedBy = &user
return
}
func getLabelByIDSimple(labelID int64) (*Label, error) {
label := Label{}
exists, err := x.ID(labelID).Get(&label)
if err != nil {
return &label, err
}
if !exists {
return &Label{}, ErrLabelDoesNotExist{labelID}
}
return &label, err
}
// Helper method to get all task ids a user has
func getUserTaskIDs(u *User) (taskIDs []int64, err error) {
tasks, err := GetTasksByUser("", u, -1, SortTasksByUnsorted, time.Unix(0, 0), time.Unix(0, 0))
if err != nil {
return nil, err
}
// make a slice of task ids
for _, t := range tasks {
taskIDs = append(taskIDs, t.ID)
}
return
}

View File

@ -0,0 +1,83 @@
// Vikunja is a todo-list application to facilitate your life.
// Copyright 2018 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 General Public License 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package models
import (
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/web"
"github.com/go-xorm/builder"
)
// CanUpdate checks if a user can update a label
func (l *Label) CanUpdate(a web.Auth) bool {
return l.isLabelOwner(a) // Only owners should be allowed to update a label
}
// CanDelete checks if a user can delete a label
func (l *Label) CanDelete(a web.Auth) bool {
return l.isLabelOwner(a) // Only owners should be allowed to delete a label
}
// CanRead checks if a user can read a label
func (l *Label) CanRead(a web.Auth) bool {
return l.hasAccessToLabel(a)
}
// CanCreate checks if the user can create a label
// Currently a dummy.
func (l *Label) CanCreate(a web.Auth) bool {
return true
}
func (l *Label) isLabelOwner(a web.Auth) bool {
u := getUserForRights(a)
lorig, err := getLabelByIDSimple(l.ID)
if err != nil {
log.Log.Errorf("Error occurred during isLabelOwner for Label: %v", err)
return false
}
return lorig.CreatedByID == u.ID
}
// Helper method to check if a user can see a specific label
func (l *Label) hasAccessToLabel(a web.Auth) bool {
u := getUserForRights(a)
// Get all tasks
taskIDs, err := getUserTaskIDs(u)
if err != nil {
log.Log.Errorf("Error occurred during hasAccessToLabel for Label: %v", err)
return false
}
// Get all labels associated with these tasks
var labels []*Label
has, err := x.Table("labels").
Select("labels.*").
Join("LEFT", "label_task", "label_task.label_id = labels.id").
Where("label_task.label_id != null OR labels.created_by_id = ?", u.ID).
Or(builder.In("label_task.task_id", taskIDs)).
And("labels.id = ?", l.ID).
GroupBy("labels.id").
Exist(&labels)
if err != nil {
log.Log.Errorf("Error occurred during hasAccessToLabel for Label: %v", err)
return false
}
return has
}

153
pkg/models/label_task.go Normal file
View File

@ -0,0 +1,153 @@
// Vikunja is a todo-list application to facilitate your life.
// Copyright 2018 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 General Public License 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package models
import (
"code.vikunja.io/web"
"github.com/go-xorm/builder"
)
// Delete deletes a label on a task
// @Summary Remove a label from a task
// @Description Remove a label from a task. The user needs to have write-access to the list to be able do this.
// @tags labels
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param task path int true "Task ID"
// @Param label path int true "Label ID"
// @Success 200 {object} models.Label "The label was successfully removed."
// @Failure 403 {object} code.vikunja.io/web.HTTPError "Not allowed to remove the label."
// @Failure 404 {object} code.vikunja.io/web.HTTPError "Label not found."
// @Failure 500 {object} models.Message "Internal error"
// @Router /tasks/{task}/labels/{label} [delete]
func (l *LabelTask) Delete() (err error) {
_, err = x.Delete(&LabelTask{LabelID: l.LabelID, TaskID: l.TaskID})
return err
}
// Create adds a label to a task
// @Summary Add a label to a task
// @Description Add a label to a task. The user needs to have write-access to the list to be able do this.
// @tags labels
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param task path int true "Task ID"
// @Param label body models.Label true "The label object"
// @Success 200 {object} models.Label "The created label relation object."
// @Failure 400 {object} code.vikunja.io/web.HTTPError "Invalid label object provided."
// @Failure 403 {object} code.vikunja.io/web.HTTPError "Not allowed to add the label."
// @Failure 404 {object} code.vikunja.io/web.HTTPError "The label does not exist."
// @Failure 500 {object} models.Message "Internal error"
// @Router /tasks/{task}/labels [put]
func (l *LabelTask) Create(a web.Auth) (err error) {
// Check if the label is already added
exists, err := x.Exist(&LabelTask{LabelID: l.LabelID, TaskID: l.TaskID})
if err != nil {
return err
}
if exists {
return ErrLabelIsAlreadyOnTask{l.LabelID, l.TaskID}
}
// Insert it
_, err = x.Insert(l)
return err
}
// ReadAll gets all labels on a task
// @Summary Get all labels on a task
// @Description Returns all labels which are assicociated with a given task.
// @tags labels
// @Accept json
// @Produce json
// @Param task path int true "Task ID"
// @Param p query int false "The page number. Used for pagination. If not provided, the first page of results is returned."
// @Param s query string false "Search labels by label text."
// @Security ApiKeyAuth
// @Success 200 {array} models.Label "The labels"
// @Failure 500 {object} models.Message "Internal error"
// @Router /tasks/{task}/labels [get]
func (l *LabelTask) ReadAll(search string, a web.Auth, page int) (labels interface{}, err error) {
u, err := getUserWithError(a)
if err != nil {
return nil, err
}
// Check if the user has the right to see the task
task, err := GetListTaskByID(l.TaskID)
if err != nil {
return nil, err
}
if !task.CanRead(a) {
return nil, ErrNoRightToSeeTask{l.TaskID, u.ID}
}
return getLabelsByTaskIDs(search, u, page, []int64{l.TaskID}, false)
}
type labelWithTaskID struct {
TaskID int64
Label `xorm:"extends"`
}
// Helper function to get all labels for a set of tasks
// Used when getting all labels for one task as well when getting all lables
func getLabelsByTaskIDs(search string, u *User, page int, taskIDs []int64, getUnusedLabels bool) (ls []*labelWithTaskID, err error) {
// Incl unused labels
var uidOrNil interface{}
var requestOrNil interface{}
if getUnusedLabels {
uidOrNil = u.ID
requestOrNil = "label_task.label_id != null OR labels.created_by_id = ?"
}
// Get all labels associated with these labels
var labels []*labelWithTaskID
err = x.Table("labels").
Select("labels.*, label_task.task_id").
Join("LEFT", "label_task", "label_task.label_id = labels.id").
Where(requestOrNil, uidOrNil).
Or(builder.In("label_task.task_id", taskIDs)).
And("labels.title LIKE ?", "%"+search+"%").
GroupBy("labels.id").
Limit(getLimitFromPageIndex(page)).
Find(&labels)
if err != nil {
return nil, err
}
// Get all created by users
var userids []int64
for _, l := range labels {
userids = append(userids, l.CreatedByID)
}
users := make(map[int64]*User)
err = x.In("id", userids).Find(&users)
if err != nil {
return nil, err
}
// Put it all together
for in, l := range labels {
labels[in].CreatedBy = users[l.CreatedByID]
}
return labels, err
}

View File

@ -0,0 +1,62 @@
// Vikunja is a todo-list application to facilitate your life.
// Copyright 2018 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 General Public License 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package models
import (
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/web"
)
// CanCreate checks if a user can add a label to a task
func (lt *LabelTask) CanCreate(a web.Auth) bool {
label, err := getLabelByIDSimple(lt.LabelID)
if err != nil {
log.Log.Errorf("Error during CanCreate for LabelTask: %v", err)
return false
}
return label.hasAccessToLabel(a) && lt.canDoLabelTask(a)
}
// CanDelete checks if a user can delete a label from a task
func (lt *LabelTask) CanDelete(a web.Auth) bool {
if !lt.canDoLabelTask(a) {
return false
}
// We don't care here if the label exists or not. The only relevant thing here is if the relation already exists,
// throw an error.
exists, err := x.Exist(&LabelTask{LabelID: lt.LabelID, TaskID: lt.TaskID})
if err != nil {
log.Log.Errorf("Error during CanDelete for LabelTask: %v", err)
return false
}
return exists
}
// Helper function to check if a user can write to a task
// + is able to see the label
// always the same check for either deleting or adding a label to a task
func (lt *LabelTask) canDoLabelTask(a web.Auth) bool {
// A user can add a label to a task if he can write to the task
task, err := getTaskByIDSimple(lt.TaskID)
if err != nil {
log.Log.Error("Error occurred during canDoLabelTask for LabelTask: %v", err)
return false
}
return task.CanUpdate(a)
}

View File

@ -0,0 +1,279 @@
package models
import (
"reflect"
"runtime"
"testing"
"code.vikunja.io/web"
)
func TestLabelTask_ReadAll(t *testing.T) {
type fields struct {
ID int64
TaskID int64
LabelID int64
Created int64
CRUDable web.CRUDable
Rights web.Rights
}
type args struct {
search string
a web.Auth
page int
}
tests := []struct {
name string
fields fields
args args
wantLabels interface{}
wantErr bool
errType func(error) bool
}{
{
name: "normal",
fields: fields{
TaskID: 1,
},
args: args{
a: &User{ID: 1},
},
wantLabels: []*labelWithTaskID{
{
TaskID: 1,
Label: Label{
ID: 4,
Title: "Label #4 - visible via other task",
CreatedByID: 2,
CreatedBy: &User{
ID: 2,
Username: "user2",
Password: "1234",
Email: "user2@example.com",
},
},
},
},
},
{
name: "no right to see the task",
fields: fields{
TaskID: 14,
},
args: args{
a: &User{ID: 1},
},
wantErr: true,
errType: IsErrNoRightToSeeTask,
},
{
name: "nonexistant task",
fields: fields{
TaskID: 9999,
},
args: args{
a: &User{ID: 1},
},
wantErr: true,
errType: IsErrListTaskDoesNotExist,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := &LabelTask{
ID: tt.fields.ID,
TaskID: tt.fields.TaskID,
LabelID: tt.fields.LabelID,
Created: tt.fields.Created,
CRUDable: tt.fields.CRUDable,
Rights: tt.fields.Rights,
}
gotLabels, err := l.ReadAll(tt.args.search, tt.args.a, tt.args.page)
if (err != nil) != tt.wantErr {
t.Errorf("LabelTask.ReadAll() error = %v, wantErr %v", err, tt.wantErr)
return
}
if (err != nil) && tt.wantErr && !tt.errType(err) {
t.Errorf("LabelTask.ReadAll() Wrong error type! Error = %v, want = %v", err, runtime.FuncForPC(reflect.ValueOf(tt.errType).Pointer()).Name())
}
if !reflect.DeepEqual(gotLabels, tt.wantLabels) {
t.Errorf("LabelTask.ReadAll() = %v, want %v", gotLabels, tt.wantLabels)
}
})
}
}
func TestLabelTask_Create(t *testing.T) {
type fields struct {
ID int64
TaskID int64
LabelID int64
Created int64
CRUDable web.CRUDable
Rights web.Rights
}
type args struct {
a web.Auth
}
tests := []struct {
name string
fields fields
args args
wantErr bool
errType func(error) bool
wantForbidden bool
}{
{
name: "normal",
fields: fields{
TaskID: 1,
LabelID: 1,
},
args: args{
a: &User{ID: 1},
},
},
{
name: "already existing",
fields: fields{
TaskID: 1,
LabelID: 1,
},
args: args{
a: &User{ID: 1},
},
wantErr: true,
errType: IsErrLabelIsAlreadyOnTask,
},
{
name: "nonexisting label",
fields: fields{
TaskID: 1,
LabelID: 9999,
},
args: args{
a: &User{ID: 1},
},
wantForbidden: true,
},
{
name: "nonexisting task",
fields: fields{
TaskID: 9999,
LabelID: 1,
},
args: args{
a: &User{ID: 1},
},
wantForbidden: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := &LabelTask{
ID: tt.fields.ID,
TaskID: tt.fields.TaskID,
LabelID: tt.fields.LabelID,
Created: tt.fields.Created,
CRUDable: tt.fields.CRUDable,
Rights: tt.fields.Rights,
}
if !l.CanCreate(tt.args.a) && !tt.wantForbidden {
t.Errorf("LabelTask.CanCreate() forbidden, want %v", tt.wantForbidden)
}
err := l.Create(tt.args.a)
if (err != nil) != tt.wantErr {
t.Errorf("LabelTask.Create() error = %v, wantErr %v", err, tt.wantErr)
}
if (err != nil) && tt.wantErr && !tt.errType(err) {
t.Errorf("LabelTask.Create() Wrong error type! Error = %v, want = %v", err, runtime.FuncForPC(reflect.ValueOf(tt.errType).Pointer()).Name())
}
})
}
}
func TestLabelTask_Delete(t *testing.T) {
type fields struct {
ID int64
TaskID int64
LabelID int64
Created int64
CRUDable web.CRUDable
Rights web.Rights
}
tests := []struct {
name string
fields fields
wantErr bool
errType func(error) bool
auth web.Auth
wantForbidden bool
}{
{
name: "normal",
fields: fields{
TaskID: 1,
LabelID: 1,
},
auth: &User{ID: 1},
},
{
name: "delete nonexistant",
fields: fields{
TaskID: 1,
LabelID: 1,
},
auth: &User{ID: 1},
wantForbidden: true,
},
{
name: "nonexisting label",
fields: fields{
TaskID: 1,
LabelID: 9999,
},
auth: &User{ID: 1},
wantForbidden: true,
},
{
name: "nonexisting task",
fields: fields{
TaskID: 9999,
LabelID: 1,
},
auth: &User{ID: 1},
wantForbidden: true,
},
{
name: "existing, but forbidden task",
fields: fields{
TaskID: 14,
LabelID: 1,
},
auth: &User{ID: 1},
wantForbidden: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := &LabelTask{
ID: tt.fields.ID,
TaskID: tt.fields.TaskID,
LabelID: tt.fields.LabelID,
Created: tt.fields.Created,
CRUDable: tt.fields.CRUDable,
Rights: tt.fields.Rights,
}
if !l.CanDelete(tt.auth) && !tt.wantForbidden {
t.Errorf("LabelTask.CanDelete() forbidden, want %v", tt.wantForbidden)
}
err := l.Delete()
if (err != nil) != tt.wantErr {
t.Errorf("LabelTask.Delete() error = %v, wantErr %v", err, tt.wantErr)
}
if (err != nil) && tt.wantErr && !tt.errType(err) {
t.Errorf("LabelTask.Delete() Wrong error type! Error = %v, want = %v", err, runtime.FuncForPC(reflect.ValueOf(tt.errType).Pointer()).Name())
}
})
}
}

452
pkg/models/label_test.go Normal file
View File

@ -0,0 +1,452 @@
// Vikunja is a todo-list application to facilitate your life.
// Copyright 2018 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 General Public License 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package models
import (
"reflect"
"runtime"
"testing"
"code.vikunja.io/web"
)
func TestLabel_ReadAll(t *testing.T) {
type fields struct {
ID int64
Title string
Description string
HexColor string
CreatedByID int64
CreatedBy *User
Created int64
Updated int64
CRUDable web.CRUDable
Rights web.Rights
}
type args struct {
search string
a web.Auth
page int
}
user1 := &User{
ID: 1,
Username: "user1",
Password: "1234",
Email: "user1@example.com",
}
tests := []struct {
name string
fields fields
args args
wantLs interface{}
wantErr bool
}{
{
name: "normal",
args: args{
a: &User{ID: 1},
},
wantLs: []*labelWithTaskID{
{
Label: Label{
ID: 1,
Title: "Label #1",
CreatedByID: 1,
CreatedBy: user1,
},
},
{
Label: Label{
ID: 2,
Title: "Label #2",
CreatedByID: 1,
CreatedBy: user1,
},
},
{
TaskID: 1,
Label: Label{
ID: 4,
Title: "Label #4 - visible via other task",
CreatedByID: 2,
CreatedBy: &User{
ID: 2,
Username: "user2",
Password: "1234",
Email: "user2@example.com",
},
},
},
},
},
{
name: "invalid user",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := &Label{
ID: tt.fields.ID,
Title: tt.fields.Title,
Description: tt.fields.Description,
HexColor: tt.fields.HexColor,
CreatedByID: tt.fields.CreatedByID,
CreatedBy: tt.fields.CreatedBy,
Created: tt.fields.Created,
Updated: tt.fields.Updated,
CRUDable: tt.fields.CRUDable,
Rights: tt.fields.Rights,
}
gotLs, err := l.ReadAll(tt.args.search, tt.args.a, tt.args.page)
if (err != nil) != tt.wantErr {
t.Errorf("Label.ReadAll() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(gotLs, tt.wantLs) {
t.Errorf("Label.ReadAll() = %v, want %v", gotLs, tt.wantLs)
}
})
}
}
func TestLabel_ReadOne(t *testing.T) {
type fields struct {
ID int64
Title string
Description string
HexColor string
CreatedByID int64
CreatedBy *User
Created int64
Updated int64
CRUDable web.CRUDable
Rights web.Rights
}
user1 := &User{
ID: 1,
Username: "user1",
Password: "1234",
Email: "user1@example.com",
}
tests := []struct {
name string
fields fields
want *Label
wantErr bool
errType func(error) bool
auth web.Auth
wantForbidden bool
}{
{
name: "Get label #1",
fields: fields{
ID: 1,
},
want: &Label{
ID: 1,
Title: "Label #1",
CreatedByID: 1,
CreatedBy: user1,
},
auth: &User{ID: 1},
},
{
name: "Get nonexistant label",
fields: fields{
ID: 9999,
},
wantErr: true,
errType: IsErrLabelDoesNotExist,
wantForbidden: true,
auth: &User{ID: 1},
},
{
name: "no rights",
fields: fields{
ID: 3,
},
wantForbidden: true,
auth: &User{ID: 1},
},
{
name: "Get label #4 - other user",
fields: fields{
ID: 4,
},
want: &Label{
ID: 4,
Title: "Label #4 - visible via other task",
CreatedByID: 2,
CreatedBy: &User{
ID: 2,
Username: "user2",
Password: "1234",
Email: "user2@example.com",
},
},
auth: &User{ID: 1},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := &Label{
ID: tt.fields.ID,
Title: tt.fields.Title,
Description: tt.fields.Description,
HexColor: tt.fields.HexColor,
CreatedByID: tt.fields.CreatedByID,
CreatedBy: tt.fields.CreatedBy,
Created: tt.fields.Created,
Updated: tt.fields.Updated,
CRUDable: tt.fields.CRUDable,
Rights: tt.fields.Rights,
}
if !l.CanRead(tt.auth) && !tt.wantForbidden {
t.Errorf("Label.CanRead() forbidden, want %v", tt.wantForbidden)
}
err := l.ReadOne()
if (err != nil) != tt.wantErr {
t.Errorf("Label.ReadOne() error = %v, wantErr %v", err, tt.wantErr)
}
if (err != nil) && tt.wantErr && !tt.errType(err) {
t.Errorf("Label.ReadOne() Wrong error type! Error = %v, want = %v", err, runtime.FuncForPC(reflect.ValueOf(tt.errType).Pointer()).Name())
}
if !reflect.DeepEqual(l, tt.want) && !tt.wantErr && !tt.wantForbidden {
t.Errorf("Label.ReadOne() = %v, want %v", l, tt.want)
}
})
}
}
func TestLabel_Create(t *testing.T) {
type fields struct {
ID int64
Title string
Description string
HexColor string
CreatedByID int64
CreatedBy *User
Created int64
Updated int64
CRUDable web.CRUDable
Rights web.Rights
}
type args struct {
a web.Auth
}
tests := []struct {
name string
fields fields
args args
wantErr bool
wantForbidden bool
}{
{
name: "normal",
fields: fields{
Title: "Test #1",
Description: "Lorem Ipsum",
HexColor: "ffccff",
},
args: args{
a: &User{ID: 1},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := &Label{
ID: tt.fields.ID,
Title: tt.fields.Title,
Description: tt.fields.Description,
HexColor: tt.fields.HexColor,
CreatedByID: tt.fields.CreatedByID,
CreatedBy: tt.fields.CreatedBy,
Created: tt.fields.Created,
Updated: tt.fields.Updated,
CRUDable: tt.fields.CRUDable,
Rights: tt.fields.Rights,
}
if !l.CanCreate(tt.args.a) && !tt.wantForbidden {
t.Errorf("Label.CanCreate() forbidden, want %v", tt.wantForbidden)
}
if err := l.Create(tt.args.a); (err != nil) != tt.wantErr {
t.Errorf("Label.Create() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestLabel_Update(t *testing.T) {
type fields struct {
ID int64
Title string
Description string
HexColor string
CreatedByID int64
CreatedBy *User
Created int64
Updated int64
CRUDable web.CRUDable
Rights web.Rights
}
tests := []struct {
name string
fields fields
wantErr bool
auth web.Auth
wantForbidden bool
}{
{
name: "normal",
fields: fields{
ID: 1,
Title: "new and better",
},
auth: &User{ID: 1},
},
{
name: "nonexisting",
fields: fields{
ID: 99999,
Title: "new and better",
},
auth: &User{ID: 1},
wantForbidden: true,
wantErr: true,
},
{
name: "no rights",
fields: fields{
ID: 3,
Title: "new and better",
},
auth: &User{ID: 1},
wantForbidden: true,
},
{
name: "no rights other creator but access",
fields: fields{
ID: 4,
Title: "new and better",
},
auth: &User{ID: 1},
wantForbidden: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := &Label{
ID: tt.fields.ID,
Title: tt.fields.Title,
Description: tt.fields.Description,
HexColor: tt.fields.HexColor,
CreatedByID: tt.fields.CreatedByID,
CreatedBy: tt.fields.CreatedBy,
Created: tt.fields.Created,
Updated: tt.fields.Updated,
CRUDable: tt.fields.CRUDable,
Rights: tt.fields.Rights,
}
if !l.CanUpdate(tt.auth) && !tt.wantForbidden {
t.Errorf("Label.CanUpdate() forbidden, want %v", tt.wantForbidden)
}
if err := l.Update(); (err != nil) != tt.wantErr {
t.Errorf("Label.Update() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestLabel_Delete(t *testing.T) {
type fields struct {
ID int64
Title string
Description string
HexColor string
CreatedByID int64
CreatedBy *User
Created int64
Updated int64
CRUDable web.CRUDable
Rights web.Rights
}
tests := []struct {
name string
fields fields
wantErr bool
auth web.Auth
wantForbidden bool
}{
{
name: "normal",
fields: fields{
ID: 1,
},
auth: &User{ID: 1},
},
{
name: "nonexisting",
fields: fields{
ID: 99999,
},
auth: &User{ID: 1},
wantForbidden: true, // When the label does not exist, it is forbidden. We should fix this, but for everything.
},
{
name: "no rights",
fields: fields{
ID: 3,
},
auth: &User{ID: 1},
wantForbidden: true,
},
{
name: "no rights but visible",
fields: fields{
ID: 4,
},
auth: &User{ID: 1},
wantForbidden: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := &Label{
ID: tt.fields.ID,
Title: tt.fields.Title,
Description: tt.fields.Description,
HexColor: tt.fields.HexColor,
CreatedByID: tt.fields.CreatedByID,
CreatedBy: tt.fields.CreatedBy,
Created: tt.fields.Created,
Updated: tt.fields.Updated,
CRUDable: tt.fields.CRUDable,
Rights: tt.fields.Rights,
}
if !l.CanDelete(tt.auth) && !tt.wantForbidden {
t.Errorf("Label.CanDelete() forbidden, want %v", tt.wantForbidden)
}
if err := l.Delete(); (err != nil) != tt.wantErr {
t.Errorf("Label.Delete() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

View File

@ -23,20 +23,21 @@ import (
// ListTask represents an task in a todolist
type ListTask struct {
ID int64 `xorm:"int(11) autoincr not null unique pk" json:"id" param:"listtask"`
Text string `xorm:"varchar(250)" json:"text" valid:"runelength(3|250)"`
Description string `xorm:"varchar(250)" json:"description" valid:"runelength(0|250)"`
Done bool `xorm:"INDEX" json:"done"`
DueDateUnix int64 `xorm:"int(11) INDEX" json:"dueDate"`
RemindersUnix []int64 `xorm:"JSON TEXT" json:"reminderDates"`
CreatedByID int64 `xorm:"int(11)" json:"-"` // ID of the user who put that task on the list
ListID int64 `xorm:"int(11) INDEX" json:"listID" param:"list"`
RepeatAfter int64 `xorm:"int(11) INDEX" json:"repeatAfter"`
ParentTaskID int64 `xorm:"int(11) INDEX" json:"parentTaskID"`
Priority int64 `xorm:"int(11)" json:"priority"`
StartDateUnix int64 `xorm:"int(11) INDEX" json:"startDate"`
EndDateUnix int64 `xorm:"int(11) INDEX" json:"endDate"`
Assignees []*User `xorm:"-" json:"assignees"`
ID int64 `xorm:"int(11) autoincr not null unique pk" json:"id" param:"listtask"`
Text string `xorm:"varchar(250)" json:"text" valid:"runelength(3|250)"`
Description string `xorm:"varchar(250)" json:"description" valid:"runelength(0|250)"`
Done bool `xorm:"INDEX" json:"done"`
DueDateUnix int64 `xorm:"int(11) INDEX" json:"dueDate"`
RemindersUnix []int64 `xorm:"JSON TEXT" json:"reminderDates"`
CreatedByID int64 `xorm:"int(11)" json:"-"` // ID of the user who put that task on the list
ListID int64 `xorm:"int(11) INDEX" json:"listID" param:"list"`
RepeatAfter int64 `xorm:"int(11) INDEX" json:"repeatAfter"`
ParentTaskID int64 `xorm:"int(11) INDEX" json:"parentTaskID"`
Priority int64 `xorm:"int(11)" json:"priority"`
StartDateUnix int64 `xorm:"int(11) INDEX" json:"startDate"`
EndDateUnix int64 `xorm:"int(11) INDEX" json:"endDate"`
Assignees []*User `xorm:"-" json:"assignees"`
Labels []*Label `xorm:"-" json:"labels"`
Sorting string `xorm:"-" json:"-" param:"sort"` // Parameter to sort by
StartDateSortUnix int64 `xorm:"-" json:"-" param:"startdatefilter"`
@ -61,8 +62,8 @@ func (ListTask) TableName() string {
// ListTaskAssginee represents an assignment of a user to a task
type ListTaskAssginee struct {
ID int64 `xorm:"int(11) autoincr not null unique pk"`
TaskID int64 `xorm:"int(11) not null"`
UserID int64 `xorm:"int(11) not null"`
TaskID int64 `xorm:"int(11) INDEX not null"`
UserID int64 `xorm:"int(11) INDEX not null"`
Created int64 `xorm:"created"`
}
@ -79,37 +80,24 @@ type ListTaskAssigneeWithUser struct {
// GetTasksByListID gets all todotasks for a list
func GetTasksByListID(listID int64) (tasks []*ListTask, err error) {
err = x.Where("list_id = ?", listID).Find(&tasks)
// make a map so we can put in a lot of other stuff more easily
taskMap := make(map[int64]*ListTask, len(tasks))
err = x.Where("list_id = ?", listID).Find(&taskMap)
if err != nil {
return
}
// No need to iterate over users if the list doesn't has tasks
if len(tasks) == 0 {
// No need to iterate over users and stuff if the list doesn't has tasks
if len(taskMap) == 0 {
return
}
// make a map so we can put in subtasks more easily
taskMap := make(map[int64]*ListTask, len(tasks))
// Get all users & task ids and put them into the array
var userIDs []int64
var taskIDs []int64
for _, i := range tasks {
for _, i := range taskMap {
taskIDs = append(taskIDs, i.ID)
found := false
for _, u := range userIDs {
if i.CreatedByID == u {
found = true
break
}
}
if !found {
userIDs = append(userIDs, i.CreatedByID)
}
taskMap[i.ID] = i
userIDs = append(userIDs, i.CreatedByID)
}
// Get all assignees
@ -124,7 +112,18 @@ func GetTasksByListID(listID int64) (tasks []*ListTask, err error) {
}
}
var users []User
// Get all labels for the tasks
labels, err := getLabelsByTaskIDs("", &User{}, -1, taskIDs, false)
if err != nil {
return
}
for _, l := range labels {
if l != nil {
taskMap[l.TaskID].Labels = append(taskMap[l.TaskID].Labels, &l.Label)
}
}
users := make(map[int64]*User)
err = x.In("id", userIDs).Find(&users)
if err != nil {
return
@ -134,12 +133,7 @@ func GetTasksByListID(listID int64) (tasks []*ListTask, err error) {
for _, task := range taskMap {
// Make created by user objects
for _, u := range users {
if task.CreatedByID == u.ID {
taskMap[task.ID].CreatedBy = u
break
}
}
taskMap[task.ID].CreatedBy = *users[task.CreatedByID]
// Reorder all subtasks
if task.ParentTaskID != 0 {
@ -154,7 +148,7 @@ func GetTasksByListID(listID int64) (tasks []*ListTask, err error) {
tasks = append(tasks, t)
}
// Sort the output. In Go, contents on a map are put on that map in no particular order.
// Sort the output. In Go, contents on a map are put on that map in no particular order (saved on heap).
// Because of this, tasks are not sorted anymore in the output, this leads to confiusion.
// To avoid all this, we need to sort the slice afterwards
sort.Slice(tasks, func(i, j int) bool {
@ -174,19 +168,27 @@ func getRawTaskAssigneesForTasks(taskIDs []int64) (taskAssignees []*ListTaskAssi
return
}
// GetListTaskByID returns all tasks a list has
func GetListTaskByID(listTaskID int64) (listTask ListTask, err error) {
if listTaskID < 1 {
return ListTask{}, ErrListTaskDoesNotExist{listTaskID}
func getTaskByIDSimple(taskID int64) (task ListTask, err error) {
if taskID < 1 {
return ListTask{}, ErrListTaskDoesNotExist{taskID}
}
exists, err := x.ID(listTaskID).Get(&listTask)
exists, err := x.ID(taskID).Get(&task)
if err != nil {
return ListTask{}, err
}
if !exists {
return ListTask{}, ErrListTaskDoesNotExist{listTaskID}
return ListTask{}, ErrListTaskDoesNotExist{taskID}
}
return
}
// GetListTaskByID returns all tasks a list has
func GetListTaskByID(listTaskID int64) (listTask ListTask, err error) {
listTask, err = getTaskByIDSimple(listTaskID)
if err != nil {
return
}
u, err := GetUserByID(listTask.CreatedByID)

View File

@ -43,15 +43,19 @@ func (t *ListTask) CanUpdate(a web.Auth) bool {
doer := getUserForRights(a)
// Get the task
lI, err := GetListTaskByID(t.ID)
lI, err := getTaskByIDSimple(t.ID)
if err != nil {
log.Log.Error("Error occurred during CanDelete for ListTask: %s", err)
log.Log.Error("Error occurred during CanUpdate (getTaskByIDSimple) for ListTask: %s", err)
return false
}
// A user can update an task if he has write acces to its list
l := &List{ID: lI.ListID}
l.ReadOne()
err = l.GetSimpleByID()
if err != nil {
log.Log.Error("Error occurred during CanUpdate (ReadOne) for ListTask: %s", err)
return false
}
return l.CanWrite(doer)
}
@ -64,3 +68,10 @@ func (t *ListTask) CanCreate(a web.Auth) bool {
l.ReadOne()
return l.CanWrite(doer)
}
// CanRead determines if a user can read a task
func (t *ListTask) CanRead(a web.Auth) bool {
// A user can read a task if it has access to the list
list := &List{ID: t.ListID}
return list.CanRead(a)
}

View File

@ -69,6 +69,8 @@ func init() {
new(ListUser),
new(NamespaceUser),
new(ListTaskAssginee),
new(Label),
new(LabelTask),
)
tablesWithPointer = append(tables,
@ -83,6 +85,8 @@ func init() {
&ListUser{},
&NamespaceUser{},
&ListTaskAssginee{},
&Label{},
&LabelTask{},
)
}