Improve label handling (#48)

This commit is contained in:
konrad 2019-01-09 23:08:12 +00:00 committed by Gitea
parent 364a172876
commit 318920fe29
16 changed files with 515 additions and 90 deletions

View file

@ -120,9 +120,9 @@ Sorry for some of them being in German, I'll tranlate them at some point.
* [x] Wegen Performance auf eigene endpoints umziehen, wie labels * [x] Wegen Performance auf eigene endpoints umziehen, wie labels
* [x] "One endpoint to rule them all" -> Array-addable * [x] "One endpoint to rule them all" -> Array-addable
* [x] Labels * [x] Labels
* [ ] Check if something changed at all before running everything * [x] Check if something changed at all before running everything
* [ ] Editable via task edit, like assignees * [x] Editable via task edit, like assignees
* [ ] "One endpoint to rule them all" -> Array-addable * [x] "One endpoint to rule them all" -> Array-addable
* [ ] Attachments * [ ] Attachments
* [ ] Task-Templates innerhalb namespaces und Listen (-> Mehrere, die auswählbar sind) * [ ] Task-Templates innerhalb namespaces und Listen (-> Mehrere, die auswählbar sind)
* [ ] Ein Task muss von mehreren Assignees abgehakt werden bis er als done markiert wird * [ ] Ein Task muss von mehreren Assignees abgehakt werden bis er als done markiert wird
@ -154,6 +154,10 @@ Sorry for some of them being in German, I'll tranlate them at some point.
* [ ] Rights methods should return errors * [ ] Rights methods should return errors
* [ ] Re-check all `{List|Namespace}{User|Team}` if really all parameters need to be exposed via json or are overwritten via param anyway. * [ ] Re-check all `{List|Namespace}{User|Team}` if really all parameters need to be exposed via json or are overwritten via param anyway.
### Refactor
* [ ] ListTaskRights, sollte überall gleich funktionieren, gibt ja mittlerweile auch eine Methode um liste von nem Task aus zu kriegen oder so
### Linters ### Linters
* [x] goconst * [x] goconst

View file

@ -54,3 +54,17 @@ DELETE http://localhost:8080/api/v1/tasks/3565/labels/1
Authorization: Bearer {{auth_token}} Authorization: Bearer {{auth_token}}
### ###
# Add a new label to a task
POST http://localhost:8080/api/v1/tasks/3565/labels/bulk
Authorization: Bearer {{auth_token}}
Content-Type: application/json
{
"labels": [
{"id": 1},
{"id": 2},
{"id": 3}
]
}
###

View file

@ -135,10 +135,9 @@ Authorization: Bearer {{auth_token}}
Content-Type: application/json Content-Type: application/json
{ {
"assignees": [ "labels": [
{ {"id": 1},
"id": 1 {"id": 2}
}
] ]
} }

View file

@ -1,6 +1,6 @@
// GENERATED BY THE COMMAND ABOVE; DO NOT EDIT // GENERATED BY THE COMMAND ABOVE; DO NOT EDIT
// This file was generated by swaggo/swag at // This file was generated by swaggo/swag at
// 2019-01-07 23:16:50.581590248 +0100 CET m=+0.121922055 // 2019-01-10 00:01:27.123040428 +0100 CET m=+0.110268080
package docs package docs
@ -391,7 +391,7 @@ var doc = `{
"JWTKeyAuth": [] "JWTKeyAuth": []
} }
], ],
"description": "Returns a team by its ID.", "description": "Returns a list by its ID.",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
@ -399,13 +399,13 @@ var doc = `{
"application/json" "application/json"
], ],
"tags": [ "tags": [
"team" "list"
], ],
"summary": "Gets one team", "summary": "Gets one list",
"parameters": [ "parameters": [
{ {
"type": "integer", "type": "integer",
"description": "Team ID", "description": "List ID",
"name": "id", "name": "id",
"in": "path", "in": "path",
"required": true "required": true
@ -413,14 +413,14 @@ var doc = `{
], ],
"responses": { "responses": {
"200": { "200": {
"description": "The team", "description": "The list",
"schema": { "schema": {
"type": "object", "type": "object",
"$ref": "#/definitions/models.Team" "$ref": "#/definitions/models.List"
} }
}, },
"403": { "403": {
"description": "The user does not have access to the team", "description": "The user does not have access to the list",
"schema": { "schema": {
"type": "object", "type": "object",
"$ref": "#/definitions/code.vikunja.io.web.HTTPError" "$ref": "#/definitions/code.vikunja.io.web.HTTPError"
@ -2533,7 +2533,7 @@ var doc = `{
"JWTKeyAuth": [] "JWTKeyAuth": []
} }
], ],
"description": "Updates a task. This includes marking it as done.", "description": "Updates a task. This includes marking it as done. Assignees you pass will be updated, see their individual endpoints for more details on how this is done. To update labels, see the description of the endpoint.",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
@ -2712,13 +2712,13 @@ var doc = `{
} }
}, },
"/tasks/{taskID}/assignees/bulk": { "/tasks/{taskID}/assignees/bulk": {
"put": { "post": {
"security": [ "security": [
{ {
"JWTKeyAuth": [] "JWTKeyAuth": []
} }
], ],
"description": "Adds new assignees to a task. The assignee needs to have access to the list, the doer must be able to edit this task. Every user not in the list will be unassigned from the task, pass an empty array to unassign everyone.", "description": "Adds multiple new assignees to a task. The assignee needs to have access to the list, the doer must be able to edit this task. Every user not in the list will be unassigned from the task, pass an empty array to unassign everyone.",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
@ -2728,7 +2728,7 @@ var doc = `{
"tags": [ "tags": [
"assignees" "assignees"
], ],
"summary": "Add new assignees to a task", "summary": "Add multiple new assignees to a task",
"parameters": [ "parameters": [
{ {
"description": "The array of assignees", "description": "The array of assignees",
@ -2832,6 +2832,68 @@ var doc = `{
} }
} }
}, },
"/tasks/{taskID}/labels/bulk": {
"post": {
"security": [
{
"JWTKeyAuth": []
}
],
"description": "Adds multiple new labels to a task.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"labels"
],
"summary": "Add multiple new labels to a task",
"parameters": [
{
"description": "The array of labels",
"name": "label",
"in": "body",
"required": true,
"schema": {
"type": "object",
"$ref": "#/definitions/models.LabelTaskBulk"
}
},
{
"type": "integer",
"description": "Task ID",
"name": "taskID",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "The updated labels object.",
"schema": {
"type": "object",
"$ref": "#/definitions/models.LabelTaskBulk"
}
},
"400": {
"description": "Invalid label object provided.",
"schema": {
"type": "object",
"$ref": "#/definitions/code.vikunja.io.web.HTTPError"
}
},
"500": {
"description": "Internal error",
"schema": {
"type": "object",
"$ref": "#/definitions/models.Message"
}
}
}
}
},
"/tasks/{task}/labels": { "/tasks/{task}/labels": {
"get": { "get": {
"security": [ "security": [
@ -3858,6 +3920,18 @@ var doc = `{
} }
} }
}, },
"models.LabelTaskBulk": {
"type": "object",
"properties": {
"labels": {
"description": "All labels you want to update at once. Works exactly like you would update labels while updateing a list.",
"type": "array",
"items": {
"$ref": "#/definitions/models.Label"
}
}
}
},
"models.List": { "models.List": {
"type": "object", "type": "object",
"properties": { "properties": {

View file

@ -42,3 +42,4 @@ This document describes the different errors Vikunja can return.
| 7003 | 403 | The user does not have access to that list. | | 7003 | 403 | The user does not have access to that list. |
| 8001 | 403 | This label already exists on that task. | | 8001 | 403 | This label already exists on that task. |
| 8002 | 404 | The label does not exist. | | 8002 | 404 | The label does not exist. |
| 8003 | 403 | The user does not have access to this label. |

View file

@ -378,7 +378,7 @@
"JWTKeyAuth": [] "JWTKeyAuth": []
} }
], ],
"description": "Returns a team by its ID.", "description": "Returns a list by its ID.",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
@ -386,13 +386,13 @@
"application/json" "application/json"
], ],
"tags": [ "tags": [
"team" "list"
], ],
"summary": "Gets one team", "summary": "Gets one list",
"parameters": [ "parameters": [
{ {
"type": "integer", "type": "integer",
"description": "Team ID", "description": "List ID",
"name": "id", "name": "id",
"in": "path", "in": "path",
"required": true "required": true
@ -400,14 +400,14 @@
], ],
"responses": { "responses": {
"200": { "200": {
"description": "The team", "description": "The list",
"schema": { "schema": {
"type": "object", "type": "object",
"$ref": "#/definitions/models.Team" "$ref": "#/definitions/models.List"
} }
}, },
"403": { "403": {
"description": "The user does not have access to the team", "description": "The user does not have access to the list",
"schema": { "schema": {
"type": "object", "type": "object",
"$ref": "#/definitions/code.vikunja.io/web.HTTPError" "$ref": "#/definitions/code.vikunja.io/web.HTTPError"
@ -2520,7 +2520,7 @@
"JWTKeyAuth": [] "JWTKeyAuth": []
} }
], ],
"description": "Updates a task. This includes marking it as done.", "description": "Updates a task. This includes marking it as done. Assignees you pass will be updated, see their individual endpoints for more details on how this is done. To update labels, see the description of the endpoint.",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
@ -2699,13 +2699,13 @@
} }
}, },
"/tasks/{taskID}/assignees/bulk": { "/tasks/{taskID}/assignees/bulk": {
"put": { "post": {
"security": [ "security": [
{ {
"JWTKeyAuth": [] "JWTKeyAuth": []
} }
], ],
"description": "Adds new assignees to a task. The assignee needs to have access to the list, the doer must be able to edit this task. Every user not in the list will be unassigned from the task, pass an empty array to unassign everyone.", "description": "Adds multiple new assignees to a task. The assignee needs to have access to the list, the doer must be able to edit this task. Every user not in the list will be unassigned from the task, pass an empty array to unassign everyone.",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
@ -2715,7 +2715,7 @@
"tags": [ "tags": [
"assignees" "assignees"
], ],
"summary": "Add new assignees to a task", "summary": "Add multiple new assignees to a task",
"parameters": [ "parameters": [
{ {
"description": "The array of assignees", "description": "The array of assignees",
@ -2819,6 +2819,68 @@
} }
} }
}, },
"/tasks/{taskID}/labels/bulk": {
"post": {
"security": [
{
"JWTKeyAuth": []
}
],
"description": "Adds multiple new labels to a task.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"labels"
],
"summary": "Add multiple new labels to a task",
"parameters": [
{
"description": "The array of labels",
"name": "label",
"in": "body",
"required": true,
"schema": {
"type": "object",
"$ref": "#/definitions/models.LabelTaskBulk"
}
},
{
"type": "integer",
"description": "Task ID",
"name": "taskID",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "The updated labels object.",
"schema": {
"type": "object",
"$ref": "#/definitions/models.LabelTaskBulk"
}
},
"400": {
"description": "Invalid label object provided.",
"schema": {
"type": "object",
"$ref": "#/definitions/code.vikunja.io/web.HTTPError"
}
},
"500": {
"description": "Internal error",
"schema": {
"type": "object",
"$ref": "#/definitions/models.Message"
}
}
}
}
},
"/tasks/{task}/labels": { "/tasks/{task}/labels": {
"get": { "get": {
"security": [ "security": [
@ -3844,6 +3906,18 @@
} }
} }
}, },
"models.LabelTaskBulk": {
"type": "object",
"properties": {
"labels": {
"description": "All labels you want to update at once. Works exactly like you would update labels while updateing a list.",
"type": "array",
"items": {
"$ref": "#/definitions/models.Label"
}
}
}
},
"models.List": { "models.List": {
"type": "object", "type": "object",
"properties": { "properties": {

View file

@ -146,6 +146,15 @@ definitions:
change this value. change this value.
type: integer type: integer
type: object type: object
models.LabelTaskBulk:
properties:
labels:
description: All labels you want to update at once. Works exactly like you
would update labels while updateing a list.
items:
$ref: '#/definitions/models.Label'
type: array
type: object
models.List: models.List:
properties: properties:
created: created:
@ -923,9 +932,9 @@ paths:
get: get:
consumes: consumes:
- application/json - application/json
description: Returns a team by its ID. description: Returns a list by its ID.
parameters: parameters:
- description: Team ID - description: List ID
in: path in: path
name: id name: id
required: true required: true
@ -934,12 +943,12 @@ paths:
- application/json - application/json
responses: responses:
"200": "200":
description: The team description: The list
schema: schema:
$ref: '#/definitions/models.Team' $ref: '#/definitions/models.List'
type: object type: object
"403": "403":
description: The user does not have access to the team description: The user does not have access to the list
schema: schema:
$ref: '#/definitions/code.vikunja.io/web.HTTPError' $ref: '#/definitions/code.vikunja.io/web.HTTPError'
type: object type: object
@ -950,9 +959,9 @@ paths:
type: object type: object
security: security:
- JWTKeyAuth: [] - JWTKeyAuth: []
summary: Gets one team summary: Gets one list
tags: tags:
- team - list
post: post:
consumes: consumes:
- application/json - application/json
@ -2183,7 +2192,9 @@ paths:
post: post:
consumes: consumes:
- application/json - application/json
description: Updates a task. This includes marking it as done. description: Updates a task. This includes marking it as done. Assignees you
pass will be updated, see their individual endpoints for more details on how
this is done. To update labels, see the description of the endpoint.
parameters: parameters:
- description: Task ID - description: Task ID
in: path in: path
@ -2442,12 +2453,13 @@ paths:
tags: tags:
- assignees - assignees
/tasks/{taskID}/assignees/bulk: /tasks/{taskID}/assignees/bulk:
put: post:
consumes: consumes:
- application/json - application/json
description: Adds new assignees to a task. The assignee needs to have access description: Adds multiple new assignees to a task. The assignee needs to have
to the list, the doer must be able to edit this task. Every user not in the access to the list, the doer must be able to edit this task. Every user not
list will be unassigned from the task, pass an empty array to unassign everyone. in the list will be unassigned from the task, pass an empty array to unassign
everyone.
parameters: parameters:
- description: The array of assignees - description: The array of assignees
in: body in: body
@ -2481,9 +2493,50 @@ paths:
type: object type: object
security: security:
- JWTKeyAuth: [] - JWTKeyAuth: []
summary: Add new assignees to a task summary: Add multiple new assignees to a task
tags: tags:
- assignees - assignees
/tasks/{taskID}/labels/bulk:
post:
consumes:
- application/json
description: Adds multiple new labels to a task.
parameters:
- description: The array of labels
in: body
name: label
required: true
schema:
$ref: '#/definitions/models.LabelTaskBulk'
type: object
- description: Task ID
in: path
name: taskID
required: true
type: integer
produces:
- application/json
responses:
"200":
description: The updated labels object.
schema:
$ref: '#/definitions/models.LabelTaskBulk'
type: object
"400":
description: Invalid label object provided.
schema:
$ref: '#/definitions/code.vikunja.io/web.HTTPError'
type: object
"500":
description: Internal error
schema:
$ref: '#/definitions/models.Message'
type: object
security:
- JWTKeyAuth: []
summary: Add multiple new labels to a task
tags:
- labels
/tasks/all: /tasks/all:
get: get:
consumes: consumes:

View file

@ -951,3 +951,31 @@ func (err ErrLabelDoesNotExist) HTTPError() web.HTTPError {
Message: "This label does not exist.", Message: "This label does not exist.",
} }
} }
// ErrUserHasNoAccessToLabel represents an error where a user does not have the right to see a label
type ErrUserHasNoAccessToLabel struct {
LabelID int64
UserID int64
}
// IsErrUserHasNoAccessToLabel checks if an error is ErrUserHasNoAccessToLabel.
func IsErrUserHasNoAccessToLabel(err error) bool {
_, ok := err.(ErrUserHasNoAccessToLabel)
return ok
}
func (err ErrUserHasNoAccessToLabel) Error() string {
return fmt.Sprintf("The user does not have access to this label [LabelID: %v, UserID: %v]", err.LabelID, err.UserID)
}
// ErrCodeUserHasNoAccessToLabel holds the unique world-error code of this error
const ErrCodeUserHasNoAccessToLabel = 8003
// HTTPError holds the http error description
func (err ErrUserHasNoAccessToLabel) HTTPError() web.HTTPError {
return web.HTTPError{
HTTPCode: http.StatusForbidden,
Code: ErrCodeUserHasNoAccessToLabel,
Message: "You don't have access to this label.",
}
}

View file

@ -48,22 +48,3 @@ type Label struct {
func (Label) TableName() string { func (Label) TableName() string {
return "labels" return "labels"
} }
// LabelTask represents a relation between a label and a task
type LabelTask struct {
// The unique, numeric id of this label.
ID int64 `xorm:"int(11) autoincr not null unique pk" json:"id"`
TaskID int64 `xorm:"int(11) INDEX not null" json:"-" param:"listtask"`
// The label id you want to associate with a task.
LabelID int64 `xorm:"int(11) INDEX not null" json:"label_id" param:"label"`
// A unix timestamp when this task was created. You cannot change this value.
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

@ -45,7 +45,12 @@ func (l *Label) ReadAll(search string, a web.Auth, page int) (ls interface{}, er
return nil, err return nil, err
} }
return getLabelsByTaskIDs(search, u, page, taskIDs, true) return getLabelsByTaskIDs(&LabelByTaskIDsOptions{
Search: search,
User: u,
TaskIDs: taskIDs,
GetUnusedLabels: true,
})
} }
// ReadOne gets one label // ReadOne gets one label

View file

@ -21,6 +21,25 @@ import (
"github.com/go-xorm/builder" "github.com/go-xorm/builder"
) )
// LabelTask represents a relation between a label and a task
type LabelTask struct {
// The unique, numeric id of this label.
ID int64 `xorm:"int(11) autoincr not null unique pk" json:"id"`
TaskID int64 `xorm:"int(11) INDEX not null" json:"-" param:"listtask"`
// The label id you want to associate with a task.
LabelID int64 `xorm:"int(11) INDEX not null" json:"label_id" param:"label"`
// A unix timestamp when this task was created. You cannot change this value.
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"
}
// Delete deletes a label on a task // Delete deletes a label on a task
// @Summary Remove a label from 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. // @Description Remove a label from a task. The user needs to have write-access to the list to be able do this.
@ -35,8 +54,8 @@ import (
// @Failure 404 {object} code.vikunja.io/web.HTTPError "Label not found." // @Failure 404 {object} code.vikunja.io/web.HTTPError "Label not found."
// @Failure 500 {object} models.Message "Internal error" // @Failure 500 {object} models.Message "Internal error"
// @Router /tasks/{task}/labels/{label} [delete] // @Router /tasks/{task}/labels/{label} [delete]
func (l *LabelTask) Delete() (err error) { func (lt *LabelTask) Delete() (err error) {
_, err = x.Delete(&LabelTask{LabelID: l.LabelID, TaskID: l.TaskID}) _, err = x.Delete(&LabelTask{LabelID: lt.LabelID, TaskID: lt.TaskID})
return err return err
} }
@ -55,19 +74,19 @@ func (l *LabelTask) Delete() (err error) {
// @Failure 404 {object} code.vikunja.io/web.HTTPError "The label does not exist." // @Failure 404 {object} code.vikunja.io/web.HTTPError "The label does not exist."
// @Failure 500 {object} models.Message "Internal error" // @Failure 500 {object} models.Message "Internal error"
// @Router /tasks/{task}/labels [put] // @Router /tasks/{task}/labels [put]
func (l *LabelTask) Create(a web.Auth) (err error) { func (lt *LabelTask) Create(a web.Auth) (err error) {
// Check if the label is already added // Check if the label is already added
exists, err := x.Exist(&LabelTask{LabelID: l.LabelID, TaskID: l.TaskID}) exists, err := x.Exist(&LabelTask{LabelID: lt.LabelID, TaskID: lt.TaskID})
if err != nil { if err != nil {
return err return err
} }
if exists { if exists {
return ErrLabelIsAlreadyOnTask{l.LabelID, l.TaskID} return ErrLabelIsAlreadyOnTask{lt.LabelID, lt.TaskID}
} }
// Insert it // Insert it
_, err = x.Insert(l) _, err = x.Insert(lt)
return err return
} }
// ReadAll gets all labels on a task // ReadAll gets all labels on a task
@ -83,38 +102,54 @@ func (l *LabelTask) Create(a web.Auth) (err error) {
// @Success 200 {array} models.Label "The labels" // @Success 200 {array} models.Label "The labels"
// @Failure 500 {object} models.Message "Internal error" // @Failure 500 {object} models.Message "Internal error"
// @Router /tasks/{task}/labels [get] // @Router /tasks/{task}/labels [get]
func (l *LabelTask) ReadAll(search string, a web.Auth, page int) (labels interface{}, err error) { func (lt *LabelTask) ReadAll(search string, a web.Auth, page int) (labels interface{}, err error) {
u, err := getUserWithError(a) u, err := getUserWithError(a)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Check if the user has the right to see the task // Check if the user has the right to see the task
task, err := GetListTaskByID(l.TaskID) task, err := GetListTaskByID(lt.TaskID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if !task.CanRead(a) { if !task.CanRead(a) {
return nil, ErrNoRightToSeeTask{l.TaskID, u.ID} return nil, ErrNoRightToSeeTask{lt.TaskID, u.ID}
} }
return getLabelsByTaskIDs(search, u, page, []int64{l.TaskID}, false) return getLabelsByTaskIDs(&LabelByTaskIDsOptions{
User: u,
Search: search,
Page: page,
TaskIDs: []int64{lt.TaskID},
})
} }
// Helper struct, contains the label + its task ID
type labelWithTaskID struct { type labelWithTaskID struct {
TaskID int64 TaskID int64
Label `xorm:"extends"` Label `xorm:"extends"`
} }
// LabelByTaskIDsOptions is a struct to not clutter the function with too many optional parameters.
type LabelByTaskIDsOptions struct {
User *User
Search string
Page int
TaskIDs []int64
GetUnusedLabels bool
}
// Helper function to get all labels for a set of tasks // 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 // 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) { func getLabelsByTaskIDs(opts *LabelByTaskIDsOptions) (ls []*labelWithTaskID, err error) {
// Incl unused labels // Include unused labels. Needed to be able to show a list of all unused labels a user
// has access to.
var uidOrNil interface{} var uidOrNil interface{}
var requestOrNil interface{} var requestOrNil interface{}
if getUnusedLabels { if opts.GetUnusedLabels {
uidOrNil = u.ID uidOrNil = opts.User.ID
requestOrNil = "label_task.label_id != null OR labels.created_by_id = ?" requestOrNil = "label_task.label_id != null OR labels.created_by_id = ?"
} }
@ -124,10 +159,10 @@ func getLabelsByTaskIDs(search string, u *User, page int, taskIDs []int64, getUn
Select("labels.*, label_task.task_id"). Select("labels.*, label_task.task_id").
Join("LEFT", "label_task", "label_task.label_id = labels.id"). Join("LEFT", "label_task", "label_task.label_id = labels.id").
Where(requestOrNil, uidOrNil). Where(requestOrNil, uidOrNil).
Or(builder.In("label_task.task_id", taskIDs)). Or(builder.In("label_task.task_id", opts.TaskIDs)).
And("labels.title LIKE ?", "%"+search+"%"). And("labels.title LIKE ?", "%"+opts.Search+"%").
GroupBy("labels.id"). GroupBy("labels.id").
Limit(getLimitFromPageIndex(page)). Limit(getLimitFromPageIndex(opts.Page)).
Find(&labels) Find(&labels)
if err != nil { if err != nil {
return nil, err return nil, err
@ -151,3 +186,120 @@ func getLabelsByTaskIDs(search string, u *User, page int, taskIDs []int64, getUn
return labels, err return labels, err
} }
// Create or update a bunch of task labels
func (t *ListTask) updateTaskLabels(creator web.Auth, labels []*Label) (err error) {
// If we don't have any new labels, delete everything right away. Saves us some hassle.
if len(labels) == 0 && len(t.Labels) > 0 {
_, err = x.Where("task_id = ?", t.ID).
Delete(LabelTask{})
return err
}
// If we didn't change anything (from 0 to zero) don't do anything.
if len(labels) == 0 && len(t.Labels) == 0 {
return nil
}
// Make a hashmap of the new labels for easier comparison
newLabels := make(map[int64]*Label, len(labels))
var allLabelIDs []int64
for _, newLabel := range labels {
newLabels[newLabel.ID] = newLabel
allLabelIDs = append(allLabelIDs, newLabel.ID)
}
// Get old labels to delete
var found bool
var labelsToDelete []int64
oldLabels := make(map[int64]*Label, len(t.Labels))
allLabels := t.Labels
t.Labels = []*Label{} // We re-empty our labels struct here because we want it to be fully empty so we can put in all the actual labels.
for _, oldLabel := range allLabels {
found = false
if newLabels[oldLabel.ID] != nil {
found = true // If a new label is already in the list with old labels
}
// Put all labels which are only on the old list to the trash
if !found {
labelsToDelete = append(labelsToDelete, oldLabel.ID)
} else {
t.Labels = append(t.Labels, oldLabel)
}
// Put it in a list with all old labels, just using the loop here
oldLabels[oldLabel.ID] = oldLabel
}
// Delete all labels not passed
if len(labelsToDelete) > 0 {
_, err = x.In("label_id", labelsToDelete).
And("task_id = ?", t.ID).
Delete(LabelTask{})
if err != nil {
return err
}
}
// Loop through our labels and add them
for _, l := range labels {
// Check if the label is already added on the task and only add it if not
if oldLabels[l.ID] != nil {
// continue outer loop
continue
}
// Add the new label
label, err := getLabelByIDSimple(l.ID)
if err != nil {
return err
}
// Check if the user has the rights to see the label he is about to add
if !label.hasAccessToLabel(creator) {
user, _ := creator.(*User)
return ErrUserHasNoAccessToLabel{LabelID: l.ID, UserID: user.ID}
}
// Insert it
_, err = x.Insert(&LabelTask{LabelID: l.ID, TaskID: t.ID})
if err != nil {
return err
}
t.Labels = append(t.Labels, label)
}
return
}
// LabelTaskBulk is a helper struct to update a bunch of labels at once
type LabelTaskBulk struct {
// All labels you want to update at once. Works exactly like you would update labels while updateing a list.
Labels []*Label `json:"labels"`
TaskID int64 `json:"-" param:"listtask"`
web.CRUDable `json:"-"`
web.Rights `json:"-"`
}
// Create updates a bunch of labels on a task at once
// @Summary Add multiple new labels to a task
// @Description Adds multiple new labels to a task.
// @tags labels
// @Accept json
// @Produce json
// @Security JWTKeyAuth
// @Param label body models.LabelTaskBulk true "The array of labels"
// @Param taskID path int true "Task ID"
// @Success 200 {object} models.LabelTaskBulk "The updated labels object."
// @Failure 400 {object} code.vikunja.io/web.HTTPError "Invalid label object provided."
// @Failure 500 {object} models.Message "Internal error"
// @Router /tasks/{taskID}/labels/bulk [post]
func (ltb *LabelTaskBulk) Create(a web.Auth) (err error) {
task, err := GetListTaskByID(ltb.TaskID)
if err != nil {
return
}
return task.updateTaskLabels(a, task.Labels)
}

View file

@ -29,12 +29,12 @@ func (lt *LabelTask) CanCreate(a web.Auth) bool {
return false return false
} }
return label.hasAccessToLabel(a) && lt.canDoLabelTask(a) return label.hasAccessToLabel(a) && canDoLabelTask(lt.TaskID, a)
} }
// CanDelete checks if a user can delete a label from a task // CanDelete checks if a user can delete a label from a task
func (lt *LabelTask) CanDelete(a web.Auth) bool { func (lt *LabelTask) CanDelete(a web.Auth) bool {
if !lt.canDoLabelTask(a) { if !canDoLabelTask(lt.TaskID, a) {
return false return false
} }
@ -48,12 +48,17 @@ func (lt *LabelTask) CanDelete(a web.Auth) bool {
return exists return exists
} }
// CanCreate determines if a user can update a labeltask
func (ltb *LabelTaskBulk) CanCreate(a web.Auth) bool {
return canDoLabelTask(ltb.TaskID, a)
}
// Helper function to check if a user can write to a task // Helper function to check if a user can write to a task
// + is able to see the label // + is able to see the label
// always the same check for either deleting or adding a label to a task // always the same check for either deleting or adding a label to a task
func (lt *LabelTask) canDoLabelTask(a web.Auth) bool { func canDoLabelTask(taskID int64, a web.Auth) bool {
// A user can add a label to a task if he can write to the task // A user can add a label to a task if he can write to the task
task, err := getTaskByIDSimple(lt.TaskID) task, err := getTaskByIDSimple(taskID)
if err != nil { if err != nil {
log.Log.Error("Error occurred during canDoLabelTask for LabelTask: %v", err) log.Log.Error("Error occurred during canDoLabelTask for LabelTask: %v", err)
return false return false

View file

@ -232,7 +232,7 @@ type BulkAssignees struct {
// @Success 200 {object} models.ListTaskAssginee "The created assingees object." // @Success 200 {object} models.ListTaskAssginee "The created assingees object."
// @Failure 400 {object} code.vikunja.io/web.HTTPError "Invalid assignee object provided." // @Failure 400 {object} code.vikunja.io/web.HTTPError "Invalid assignee object provided."
// @Failure 500 {object} models.Message "Internal error" // @Failure 500 {object} models.Message "Internal error"
// @Router /tasks/{taskID}/assignees/bulk [put] // @Router /tasks/{taskID}/assignees/bulk [post]
func (ba *BulkAssignees) Create(a web.Auth) (err error) { func (ba *BulkAssignees) Create(a web.Auth) (err error) {
task, err := GetListTaskByID(ba.TaskID) // We need to use the full method here because we need all current assignees. task, err := GetListTaskByID(ba.TaskID) // We need to use the full method here because we need all current assignees.
if err != nil { if err != nil {

View file

@ -111,7 +111,7 @@ func GetTasksByListID(listID int64) (tasks []*ListTask, err error) {
} }
// Get all labels for the tasks // Get all labels for the tasks
labels, err := getLabelsByTaskIDs("", &User{}, -1, taskIDs, false) labels, err := getLabelsByTaskIDs(&LabelByTaskIDsOptions{TaskIDs: taskIDs})
if err != nil { if err != nil {
return return
} }
@ -196,6 +196,17 @@ func GetListTaskByID(listTaskID int64) (listTask ListTask, err error) {
} }
} }
// Get task labels
taskLabels, err := getLabelsByTaskIDs(&LabelByTaskIDsOptions{
TaskIDs: []int64{listTaskID},
})
if err != nil {
return
}
for _, label := range taskLabels {
listTask.Labels = append(listTask.Labels, &label.Label)
}
return return
} }

View file

@ -77,7 +77,7 @@ func (t *ListTask) Create(a web.Auth) (err error) {
// Update updates a list task // Update updates a list task
// @Summary Update a task // @Summary Update a task
// @Description Updates a task. This includes marking it as done. // @Description Updates a task. This includes marking it as done. Assignees you pass will be updated, see their individual endpoints for more details on how this is done. To update labels, see the description of the endpoint.
// @tags task // @tags task
// @Accept json // @Accept json
// @Produce json // @Produce json
@ -104,6 +104,23 @@ func (t *ListTask) Update() (err error) {
return err return err
} }
// Update the labels
//
// Maybe FIXME:
// I've disabled this for now, because it requires significant changes in the way we do updates (using the
// Update() function. We need a user object in updateTaskLabels to check if the user has the right to see
// the label it is currently adding. To do this, we'll need to update the webhandler to let it pass the current
// user object (like it's already the case with the create method). However when we change it, that'll break
// a lot of existing code which we'll then need to refactor.
// This is why.
//
//if err := ot.updateTaskLabels(t.Labels); err != nil {
// return err
//}
// set the labels to ot.Labels because our updateTaskLabels function puts the full label objects in it pretty nicely
// We also set this here to prevent it being overwritten later on.
//t.Labels = ot.Labels
// For whatever reason, xorm dont detect if done is updated, so we need to update this every time by hand // For whatever reason, xorm dont detect if done is updated, so we need to update this every time by hand
// Which is why we merge the actual task struct with the one we got from the // Which is why we merge the actual task struct with the one we got from the
// The user struct overrides values in the actual one. // The user struct overrides values in the actual one.

View file

@ -255,7 +255,7 @@ func RegisterRoutes(e *echo.Echo) {
return &models.BulkAssignees{} return &models.BulkAssignees{}
}, },
} }
a.PUT("/tasks/:listtask/assignees/bulk", bulkAssigneeHandler.CreateWeb) a.POST("/tasks/:listtask/assignees/bulk", bulkAssigneeHandler.CreateWeb)
labelTaskHandler := &handler.WebHandler{ labelTaskHandler := &handler.WebHandler{
EmptyStruct: func() handler.CObject { EmptyStruct: func() handler.CObject {
@ -266,6 +266,13 @@ func RegisterRoutes(e *echo.Echo) {
a.DELETE("/tasks/:listtask/labels/:label", labelTaskHandler.DeleteWeb) a.DELETE("/tasks/:listtask/labels/:label", labelTaskHandler.DeleteWeb)
a.GET("/tasks/:listtask/labels", labelTaskHandler.ReadAllWeb) a.GET("/tasks/:listtask/labels", labelTaskHandler.ReadAllWeb)
bulkLabelTaskHandler := &handler.WebHandler{
EmptyStruct: func() handler.CObject {
return &models.LabelTaskBulk{}
},
}
a.POST("/tasks/:listtask/labels/bulk", bulkLabelTaskHandler.CreateWeb)
labelHandler := &handler.WebHandler{ labelHandler := &handler.WebHandler{
EmptyStruct: func() handler.CObject { EmptyStruct: func() handler.CObject {
return &models.Label{} return &models.Label{}