diff --git a/Featurecreep.md b/Featurecreep.md index 4d2da85ec..72c29eb47 100644 --- a/Featurecreep.md +++ b/Featurecreep.md @@ -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 diff --git a/REST-Tests/labels.http b/REST-Tests/labels.http index 0689769df..0c5c5fcdb 100644 --- a/REST-Tests/labels.http +++ b/REST-Tests/labels.http @@ -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} + ] +} + ### \ No newline at end of file diff --git a/REST-Tests/lists.http b/REST-Tests/lists.http index 8ff1d8a42..0257046ef 100644 --- a/REST-Tests/lists.http +++ b/REST-Tests/lists.http @@ -135,10 +135,9 @@ Authorization: Bearer {{auth_token}} Content-Type: application/json { - "assignees": [ - { - "id": 1 - } + "labels": [ + {"id": 1}, + {"id": 2} ] } diff --git a/docs/docs.go b/docs/docs.go index 2518b5384..b9c1d4c4b 100644 --- a/docs/docs.go +++ b/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": { diff --git a/docs/errors.md b/docs/errors.md index fbcecab7d..5ee8d3626 100644 --- a/docs/errors.md +++ b/docs/errors.md @@ -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. | \ No newline at end of file +| 8002 | 404 | The label does not exist. | +| 8003 | 403 | The user does not have access to this label. | \ No newline at end of file diff --git a/docs/swagger/swagger.json b/docs/swagger/swagger.json index a6bd0d173..19e64d96c 100644 --- a/docs/swagger/swagger.json +++ b/docs/swagger/swagger.json @@ -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": { diff --git a/docs/swagger/swagger.yaml b/docs/swagger/swagger.yaml index 91db35028..e8c79d662 100644 --- a/docs/swagger/swagger.yaml +++ b/docs/swagger/swagger.yaml @@ -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: diff --git a/pkg/models/error.go b/pkg/models/error.go index 4f0b59bc4..124191dba 100644 --- a/pkg/models/error.go +++ b/pkg/models/error.go @@ -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.", + } +} diff --git a/pkg/models/label.go b/pkg/models/label.go index 87bc74d8c..b23bd7330 100644 --- a/pkg/models/label.go +++ b/pkg/models/label.go @@ -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" -} diff --git a/pkg/models/label_read.go b/pkg/models/label_read.go index e6d2a8428..5f67379ab 100644 --- a/pkg/models/label_read.go +++ b/pkg/models/label_read.go @@ -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 diff --git a/pkg/models/label_task.go b/pkg/models/label_task.go index e17f81aca..2bc3b6774 100644 --- a/pkg/models/label_task.go +++ b/pkg/models/label_task.go @@ -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) +} diff --git a/pkg/models/label_task_rights.go b/pkg/models/label_task_rights.go index 2d0e4eb15..034253832 100644 --- a/pkg/models/label_task_rights.go +++ b/pkg/models/label_task_rights.go @@ -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 diff --git a/pkg/models/list_task_assignees.go b/pkg/models/list_task_assignees.go index 0d81891cc..835f597f8 100644 --- a/pkg/models/list_task_assignees.go +++ b/pkg/models/list_task_assignees.go @@ -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 { diff --git a/pkg/models/list_tasks.go b/pkg/models/list_tasks.go index 85a486a1d..0bf26ecdf 100644 --- a/pkg/models/list_tasks.go +++ b/pkg/models/list_tasks.go @@ -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 } diff --git a/pkg/models/list_tasks_create_update.go b/pkg/models/list_tasks_create_update.go index c1259cd66..9d63553d8 100644 --- a/pkg/models/list_tasks_create_update.go +++ b/pkg/models/list_tasks_create_update.go @@ -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. diff --git a/pkg/routes/routes.go b/pkg/routes/routes.go index 53d8b7435..e57ff1614 100644 --- a/pkg/routes/routes.go +++ b/pkg/routes/routes.go @@ -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{}