Add labels to tasks (#45)
This commit is contained in:
@ -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.",
|
||||
}
|
||||
}
|
||||
|
3
pkg/models/fixtures/label_task.yml
Normal file
3
pkg/models/fixtures/label_task.yml
Normal file
@ -0,0 +1,3 @@
|
||||
- id: 1
|
||||
task_id: 1
|
||||
label_id: 4
|
12
pkg/models/fixtures/labels.yml
Normal file
12
pkg/models/fixtures/labels.yml
Normal 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
|
@ -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
|
@ -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
59
pkg/models/label.go
Normal 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"
|
||||
}
|
87
pkg/models/label_create_update.go
Normal file
87
pkg/models/label_create_update.go
Normal 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
106
pkg/models/label_read.go
Normal 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
|
||||
}
|
83
pkg/models/label_rights.go
Normal file
83
pkg/models/label_rights.go
Normal 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
153
pkg/models/label_task.go
Normal 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
|
||||
}
|
62
pkg/models/label_task_rights.go
Normal file
62
pkg/models/label_task_rights.go
Normal 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)
|
||||
}
|
279
pkg/models/label_task_test.go
Normal file
279
pkg/models/label_task_test.go
Normal 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
452
pkg/models/label_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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{},
|
||||
)
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user