Improve label handling (#48)
This commit is contained in:
parent
364a172876
commit
318920fe29
16 changed files with 515 additions and 90 deletions
|
@ -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] "One endpoint to rule them all" -> Array-addable
|
||||
* [x] Labels
|
||||
* [ ] Check if something changed at all before running everything
|
||||
* [ ] Editable via task edit, like assignees
|
||||
* [ ] "One endpoint to rule them all" -> Array-addable
|
||||
* [x] Check if something changed at all before running everything
|
||||
* [x] Editable via task edit, like assignees
|
||||
* [x] "One endpoint to rule them all" -> Array-addable
|
||||
* [ ] Attachments
|
||||
* [ ] 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
|
||||
|
@ -154,6 +154,10 @@ Sorry for some of them being in German, I'll tranlate them at some point.
|
|||
* [ ] 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.
|
||||
|
||||
### Refactor
|
||||
|
||||
* [ ] ListTaskRights, sollte überall gleich funktionieren, gibt ja mittlerweile auch eine Methode um liste von nem Task aus zu kriegen oder so
|
||||
|
||||
### Linters
|
||||
|
||||
* [x] goconst
|
||||
|
|
|
@ -53,4 +53,18 @@ Content-Type: application/json
|
|||
DELETE http://localhost:8080/api/v1/tasks/3565/labels/1
|
||||
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}
|
||||
]
|
||||
}
|
||||
|
||||
###
|
|
@ -135,10 +135,9 @@ Authorization: Bearer {{auth_token}}
|
|||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"assignees": [
|
||||
{
|
||||
"id": 1
|
||||
}
|
||||
"labels": [
|
||||
{"id": 1},
|
||||
{"id": 2}
|
||||
]
|
||||
}
|
||||
|
||||
|
|
98
docs/docs.go
98
docs/docs.go
|
@ -1,6 +1,6 @@
|
|||
// GENERATED BY THE COMMAND ABOVE; DO NOT EDIT
|
||||
// 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
|
||||
|
||||
|
@ -391,7 +391,7 @@ var doc = `{
|
|||
"JWTKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Returns a team by its ID.",
|
||||
"description": "Returns a list by its ID.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
|
@ -399,13 +399,13 @@ var doc = `{
|
|||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"team"
|
||||
"list"
|
||||
],
|
||||
"summary": "Gets one team",
|
||||
"summary": "Gets one list",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Team ID",
|
||||
"description": "List ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
|
@ -413,14 +413,14 @@ var doc = `{
|
|||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The team",
|
||||
"description": "The list",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"$ref": "#/definitions/models.Team"
|
||||
"$ref": "#/definitions/models.List"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "The user does not have access to the team",
|
||||
"description": "The user does not have access to the list",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"$ref": "#/definitions/code.vikunja.io.web.HTTPError"
|
||||
|
@ -2533,7 +2533,7 @@ var doc = `{
|
|||
"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": [
|
||||
"application/json"
|
||||
],
|
||||
|
@ -2712,13 +2712,13 @@ var doc = `{
|
|||
}
|
||||
},
|
||||
"/tasks/{taskID}/assignees/bulk": {
|
||||
"put": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"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": [
|
||||
"application/json"
|
||||
],
|
||||
|
@ -2728,7 +2728,7 @@ var doc = `{
|
|||
"tags": [
|
||||
"assignees"
|
||||
],
|
||||
"summary": "Add new assignees to a task",
|
||||
"summary": "Add multiple new assignees to a task",
|
||||
"parameters": [
|
||||
{
|
||||
"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": {
|
||||
"get": {
|
||||
"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": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
|
@ -41,4 +41,5 @@ This document describes the different errors Vikunja can return.
|
|||
| 7002 | 409 | The user already has access to that list. |
|
||||
| 7003 | 403 | The user does not have access to that list. |
|
||||
| 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. |
|
|
@ -378,7 +378,7 @@
|
|||
"JWTKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Returns a team by its ID.",
|
||||
"description": "Returns a list by its ID.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
|
@ -386,13 +386,13 @@
|
|||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"team"
|
||||
"list"
|
||||
],
|
||||
"summary": "Gets one team",
|
||||
"summary": "Gets one list",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Team ID",
|
||||
"description": "List ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
|
@ -400,14 +400,14 @@
|
|||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The team",
|
||||
"description": "The list",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"$ref": "#/definitions/models.Team"
|
||||
"$ref": "#/definitions/models.List"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "The user does not have access to the team",
|
||||
"description": "The user does not have access to the list",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"$ref": "#/definitions/code.vikunja.io/web.HTTPError"
|
||||
|
@ -2520,7 +2520,7 @@
|
|||
"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": [
|
||||
"application/json"
|
||||
],
|
||||
|
@ -2699,13 +2699,13 @@
|
|||
}
|
||||
},
|
||||
"/tasks/{taskID}/assignees/bulk": {
|
||||
"put": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"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": [
|
||||
"application/json"
|
||||
],
|
||||
|
@ -2715,7 +2715,7 @@
|
|||
"tags": [
|
||||
"assignees"
|
||||
],
|
||||
"summary": "Add new assignees to a task",
|
||||
"summary": "Add multiple new assignees to a task",
|
||||
"parameters": [
|
||||
{
|
||||
"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": {
|
||||
"get": {
|
||||
"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": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
|
@ -146,6 +146,15 @@ definitions:
|
|||
change this value.
|
||||
type: integer
|
||||
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:
|
||||
properties:
|
||||
created:
|
||||
|
@ -923,9 +932,9 @@ paths:
|
|||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Returns a team by its ID.
|
||||
description: Returns a list by its ID.
|
||||
parameters:
|
||||
- description: Team ID
|
||||
- description: List ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
|
@ -934,12 +943,12 @@ paths:
|
|||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: The team
|
||||
description: The list
|
||||
schema:
|
||||
$ref: '#/definitions/models.Team'
|
||||
$ref: '#/definitions/models.List'
|
||||
type: object
|
||||
"403":
|
||||
description: The user does not have access to the team
|
||||
description: The user does not have access to the list
|
||||
schema:
|
||||
$ref: '#/definitions/code.vikunja.io/web.HTTPError'
|
||||
type: object
|
||||
|
@ -950,9 +959,9 @@ paths:
|
|||
type: object
|
||||
security:
|
||||
- JWTKeyAuth: []
|
||||
summary: Gets one team
|
||||
summary: Gets one list
|
||||
tags:
|
||||
- team
|
||||
- list
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
|
@ -2183,7 +2192,9 @@ paths:
|
|||
post:
|
||||
consumes:
|
||||
- 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:
|
||||
- description: Task ID
|
||||
in: path
|
||||
|
@ -2442,12 +2453,13 @@ paths:
|
|||
tags:
|
||||
- assignees
|
||||
/tasks/{taskID}/assignees/bulk:
|
||||
put:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
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.
|
||||
parameters:
|
||||
- description: The array of assignees
|
||||
in: body
|
||||
|
@ -2481,9 +2493,50 @@ paths:
|
|||
type: object
|
||||
security:
|
||||
- JWTKeyAuth: []
|
||||
summary: Add new assignees to a task
|
||||
summary: Add multiple new assignees to a task
|
||||
tags:
|
||||
- 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:
|
||||
get:
|
||||
consumes:
|
||||
|
|
|
@ -951,3 +951,31 @@ func (err ErrLabelDoesNotExist) HTTPError() web.HTTPError {
|
|||
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.",
|
||||
}
|
||||
}
|
||||
|
|
|
@ -48,22 +48,3 @@ type Label struct {
|
|||
func (Label) TableName() string {
|
||||
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"
|
||||
}
|
||||
|
|
|
@ -45,7 +45,12 @@ func (l *Label) ReadAll(search string, a web.Auth, page int) (ls interface{}, er
|
|||
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
|
||||
|
|
|
@ -21,6 +21,25 @@ import (
|
|||
"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
|
||||
// @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.
|
||||
|
@ -35,8 +54,8 @@ import (
|
|||
// @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})
|
||||
func (lt *LabelTask) Delete() (err error) {
|
||||
_, err = x.Delete(&LabelTask{LabelID: lt.LabelID, TaskID: lt.TaskID})
|
||||
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 500 {object} models.Message "Internal error"
|
||||
// @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
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
if exists {
|
||||
return ErrLabelIsAlreadyOnTask{l.LabelID, l.TaskID}
|
||||
return ErrLabelIsAlreadyOnTask{lt.LabelID, lt.TaskID}
|
||||
}
|
||||
|
||||
// Insert it
|
||||
_, err = x.Insert(l)
|
||||
return err
|
||||
_, err = x.Insert(lt)
|
||||
return
|
||||
}
|
||||
|
||||
// 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"
|
||||
// @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) {
|
||||
func (lt *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)
|
||||
task, err := GetListTaskByID(lt.TaskID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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 {
|
||||
TaskID int64
|
||||
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
|
||||
// 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
|
||||
func getLabelsByTaskIDs(opts *LabelByTaskIDsOptions) (ls []*labelWithTaskID, err error) {
|
||||
// Include unused labels. Needed to be able to show a list of all unused labels a user
|
||||
// has access to.
|
||||
var uidOrNil interface{}
|
||||
var requestOrNil interface{}
|
||||
if getUnusedLabels {
|
||||
uidOrNil = u.ID
|
||||
if opts.GetUnusedLabels {
|
||||
uidOrNil = opts.User.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").
|
||||
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+"%").
|
||||
Or(builder.In("label_task.task_id", opts.TaskIDs)).
|
||||
And("labels.title LIKE ?", "%"+opts.Search+"%").
|
||||
GroupBy("labels.id").
|
||||
Limit(getLimitFromPageIndex(page)).
|
||||
Limit(getLimitFromPageIndex(opts.Page)).
|
||||
Find(&labels)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -151,3 +186,120 @@ func getLabelsByTaskIDs(search string, u *User, page int, taskIDs []int64, getUn
|
|||
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -29,12 +29,12 @@ func (lt *LabelTask) CanCreate(a web.Auth) bool {
|
|||
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
|
||||
func (lt *LabelTask) CanDelete(a web.Auth) bool {
|
||||
if !lt.canDoLabelTask(a) {
|
||||
if !canDoLabelTask(lt.TaskID, a) {
|
||||
return false
|
||||
}
|
||||
|
||||
|
@ -48,12 +48,17 @@ func (lt *LabelTask) CanDelete(a web.Auth) bool {
|
|||
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
|
||||
// + 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 {
|
||||
func canDoLabelTask(taskID int64, 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)
|
||||
task, err := getTaskByIDSimple(taskID)
|
||||
if err != nil {
|
||||
log.Log.Error("Error occurred during canDoLabelTask for LabelTask: %v", err)
|
||||
return false
|
||||
|
|
|
@ -232,7 +232,7 @@ type BulkAssignees struct {
|
|||
// @Success 200 {object} models.ListTaskAssginee "The created assingees object."
|
||||
// @Failure 400 {object} code.vikunja.io/web.HTTPError "Invalid assignee object provided."
|
||||
// @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) {
|
||||
task, err := GetListTaskByID(ba.TaskID) // We need to use the full method here because we need all current assignees.
|
||||
if err != nil {
|
||||
|
|
|
@ -111,7 +111,7 @@ func GetTasksByListID(listID int64) (tasks []*ListTask, err error) {
|
|||
}
|
||||
|
||||
// Get all labels for the tasks
|
||||
labels, err := getLabelsByTaskIDs("", &User{}, -1, taskIDs, false)
|
||||
labels, err := getLabelsByTaskIDs(&LabelByTaskIDsOptions{TaskIDs: taskIDs})
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -77,7 +77,7 @@ func (t *ListTask) Create(a web.Auth) (err error) {
|
|||
|
||||
// Update updates a list 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
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
|
@ -104,6 +104,23 @@ func (t *ListTask) Update() (err error) {
|
|||
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
|
||||
// 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.
|
||||
|
|
|
@ -255,7 +255,7 @@ func RegisterRoutes(e *echo.Echo) {
|
|||
return &models.BulkAssignees{}
|
||||
},
|
||||
}
|
||||
a.PUT("/tasks/:listtask/assignees/bulk", bulkAssigneeHandler.CreateWeb)
|
||||
a.POST("/tasks/:listtask/assignees/bulk", bulkAssigneeHandler.CreateWeb)
|
||||
|
||||
labelTaskHandler := &handler.WebHandler{
|
||||
EmptyStruct: func() handler.CObject {
|
||||
|
@ -266,6 +266,13 @@ func RegisterRoutes(e *echo.Echo) {
|
|||
a.DELETE("/tasks/:listtask/labels/:label", labelTaskHandler.DeleteWeb)
|
||||
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{
|
||||
EmptyStruct: func() handler.CObject {
|
||||
return &models.Label{}
|
||||
|
|
Loading…
Reference in a new issue