Add bulk edit for tasks (#42)
This commit is contained in:
parent
b050132f4f
commit
3814b8a504
16 changed files with 862 additions and 20 deletions
|
@ -98,9 +98,9 @@ Sorry for some of them being in German, I'll tranlate them at some point.
|
|||
* [x] Start/Enddatum für Tasks
|
||||
* [x] Timeline/Calendar view -> Dazu tasks die in einem Bestimmten Bereich due sind, macht dann das Frontend
|
||||
* [x] Tasks innerhalb eines definierbarem Bereich, sollte aber trotzdem der server machen, so à la "Gib mir alles für diesen Monat"
|
||||
* [x] Bulk-edit -> Transactions
|
||||
* [ ] Labels
|
||||
* [ ] Assignees
|
||||
* [ ] Bulk-edit -> Transactions
|
||||
* [ ] 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
|
||||
|
|
|
@ -5,7 +5,7 @@ Authorization: Bearer {{auth_token}}
|
|||
###
|
||||
|
||||
# Get one list
|
||||
GET http://localhost:8080/api/v1/lists/15
|
||||
GET http://localhost:8080/api/v1/lists/1163
|
||||
Authorization: Bearer {{auth_token}}
|
||||
|
||||
###
|
||||
|
@ -138,3 +138,15 @@ Content-Type: application/json
|
|||
{"startDate":1546804000, "endDate": 1546805000}
|
||||
|
||||
###
|
||||
|
||||
# Bulk update multiple tasks at once
|
||||
POST http://localhost:8080/api/v1/tasks/bulk
|
||||
Authorization: Bearer {{auth_token}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"task_ids": [3518,3519,3521],
|
||||
"text":"bulkupdated"
|
||||
}
|
||||
|
||||
###
|
||||
|
|
130
docs/docs.go
130
docs/docs.go
|
@ -1,6 +1,6 @@
|
|||
// GENERATED BY THE COMMAND ABOVE; DO NOT EDIT
|
||||
// This file was generated by swaggo/swag at
|
||||
// 2018-12-25 21:44:18.815676554 +0100 CET m=+0.161606284
|
||||
// 2018-12-28 01:18:16.824999107 +0100 CET m=+0.098072896
|
||||
|
||||
package docs
|
||||
|
||||
|
@ -2133,6 +2133,68 @@ var doc = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"/tasks/bulk": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Updates a bunch of tasks at once. This includes marking them as done. Note: although you could supply another ID, it will be ignored. Use task_ids instead.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"task"
|
||||
],
|
||||
"summary": "Update a bunch of tasks at once",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "The task object. Looks like a normal task, the only difference is it uses an array of list_ids to update.",
|
||||
"name": "task",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"$ref": "#/definitions/models.BulkTask"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The updated task object.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"$ref": "#/definitions/models.ListTask"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid task object provided.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"$ref": "#/definitions/code.vikunja.io.web.HTTPError"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "The user does not have access to the task (aka its list)",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"$ref": "#/definitions/code.vikunja.io.web.HTTPError"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal error",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"$ref": "#/definitions/models.Message"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/tasks/caldav": {
|
||||
"get": {
|
||||
"security": [
|
||||
|
@ -2947,6 +3009,72 @@ var doc = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"models.BulkTask": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"created": {
|
||||
"type": "integer"
|
||||
},
|
||||
"createdBy": {
|
||||
"type": "object",
|
||||
"$ref": "#/definitions/models.User"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"done": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"dueDate": {
|
||||
"type": "integer"
|
||||
},
|
||||
"endDate": {
|
||||
"type": "integer"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"listID": {
|
||||
"type": "integer"
|
||||
},
|
||||
"parentTaskID": {
|
||||
"type": "integer"
|
||||
},
|
||||
"priority": {
|
||||
"type": "integer"
|
||||
},
|
||||
"reminderDates": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"repeatAfter": {
|
||||
"type": "integer"
|
||||
},
|
||||
"startDate": {
|
||||
"type": "integer"
|
||||
},
|
||||
"subtasks": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/models.ListTask"
|
||||
}
|
||||
},
|
||||
"task_ids": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
"updated": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.EmailConfirm": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
|
@ -21,6 +21,8 @@ This document describes the different errors Vikunja can return.
|
|||
| 3005 | 400 | The list title cannot be empty. |
|
||||
| 4001 | 400 | The list task text cannot be empty. |
|
||||
| 4002 | 404 | The list task does not exist. |
|
||||
| 4003 | 403 | All bulk editing tasks must belong to the same list. |
|
||||
| 4004 | 403 | Need at least one task when bulk editing tasks. |
|
||||
| 5001 | 404 | The namspace does not exist. |
|
||||
| 5003 | 403 | The user does not have access to the specified namespace. |
|
||||
| 5006 | 400 | The namespace name cannot be empty. |
|
||||
|
|
|
@ -2120,6 +2120,68 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/tasks/bulk": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Updates a bunch of tasks at once. This includes marking them as done. Note: although you could supply another ID, it will be ignored. Use task_ids instead.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"task"
|
||||
],
|
||||
"summary": "Update a bunch of tasks at once",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "The task object. Looks like a normal task, the only difference is it uses an array of list_ids to update.",
|
||||
"name": "task",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"$ref": "#/definitions/models.BulkTask"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The updated task object.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"$ref": "#/definitions/models.ListTask"
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid task object provided.",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"$ref": "#/definitions/code.vikunja.io/web.HTTPError"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "The user does not have access to the task (aka its list)",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"$ref": "#/definitions/code.vikunja.io/web.HTTPError"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal error",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"$ref": "#/definitions/models.Message"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/tasks/caldav": {
|
||||
"get": {
|
||||
"security": [
|
||||
|
@ -2933,6 +2995,72 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"models.BulkTask": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"created": {
|
||||
"type": "integer"
|
||||
},
|
||||
"createdBy": {
|
||||
"type": "object",
|
||||
"$ref": "#/definitions/models.User"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"done": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"dueDate": {
|
||||
"type": "integer"
|
||||
},
|
||||
"endDate": {
|
||||
"type": "integer"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"listID": {
|
||||
"type": "integer"
|
||||
},
|
||||
"parentTaskID": {
|
||||
"type": "integer"
|
||||
},
|
||||
"priority": {
|
||||
"type": "integer"
|
||||
},
|
||||
"reminderDates": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"repeatAfter": {
|
||||
"type": "integer"
|
||||
},
|
||||
"startDate": {
|
||||
"type": "integer"
|
||||
},
|
||||
"subtasks": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/models.ListTask"
|
||||
}
|
||||
},
|
||||
"task_ids": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"text": {
|
||||
"type": "string"
|
||||
},
|
||||
"updated": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.EmailConfirm": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
|
@ -11,6 +11,50 @@ definitions:
|
|||
username:
|
||||
type: string
|
||||
type: object
|
||||
models.BulkTask:
|
||||
properties:
|
||||
created:
|
||||
type: integer
|
||||
createdBy:
|
||||
$ref: '#/definitions/models.User'
|
||||
type: object
|
||||
description:
|
||||
type: string
|
||||
done:
|
||||
type: boolean
|
||||
dueDate:
|
||||
type: integer
|
||||
endDate:
|
||||
type: integer
|
||||
id:
|
||||
type: integer
|
||||
listID:
|
||||
type: integer
|
||||
parentTaskID:
|
||||
type: integer
|
||||
priority:
|
||||
type: integer
|
||||
reminderDates:
|
||||
items:
|
||||
type: integer
|
||||
type: array
|
||||
repeatAfter:
|
||||
type: integer
|
||||
startDate:
|
||||
type: integer
|
||||
subtasks:
|
||||
items:
|
||||
$ref: '#/definitions/models.ListTask'
|
||||
type: array
|
||||
task_ids:
|
||||
items:
|
||||
type: integer
|
||||
type: array
|
||||
text:
|
||||
type: string
|
||||
updated:
|
||||
type: integer
|
||||
type: object
|
||||
models.EmailConfirm:
|
||||
properties:
|
||||
token:
|
||||
|
@ -1822,6 +1866,50 @@ paths:
|
|||
summary: Get tasks sorted and within a date range
|
||||
tags:
|
||||
- task
|
||||
/tasks/bulk:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: 'Updates a bunch of tasks at once. This includes marking them as
|
||||
done. Note: although you could supply another ID, it will be ignored. Use
|
||||
task_ids instead.'
|
||||
parameters:
|
||||
- description: The task object. Looks like a normal task, the only difference
|
||||
is it uses an array of list_ids to update.
|
||||
in: body
|
||||
name: task
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/models.BulkTask'
|
||||
type: object
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: The updated task object.
|
||||
schema:
|
||||
$ref: '#/definitions/models.ListTask'
|
||||
type: object
|
||||
"400":
|
||||
description: Invalid task object provided.
|
||||
schema:
|
||||
$ref: '#/definitions/code.vikunja.io/web.HTTPError'
|
||||
type: object
|
||||
"403":
|
||||
description: The user does not have access to the task (aka its list)
|
||||
schema:
|
||||
$ref: '#/definitions/code.vikunja.io/web.HTTPError'
|
||||
type: object
|
||||
"500":
|
||||
description: Internal error
|
||||
schema:
|
||||
$ref: '#/definitions/models.Message'
|
||||
type: object
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: Update a bunch of tasks at once
|
||||
tags:
|
||||
- task
|
||||
/tasks/caldav:
|
||||
get:
|
||||
description: Returns a calDAV-parsable format with all tasks as calendar events.
|
||||
|
|
1
go.mod
1
go.mod
|
@ -23,6 +23,7 @@ require (
|
|||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc
|
||||
github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf
|
||||
github.com/client9/misspell v0.3.4
|
||||
github.com/cweill/gotests v1.5.2 // indirect
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||
github.com/fzipp/gocyclo v0.0.0-20150627053110-6acd4345c835
|
||||
github.com/garyburd/redigo v1.6.0 // indirect
|
||||
|
|
1
go.sum
1
go.sum
|
@ -18,6 +18,7 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24
|
|||
github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cweill/gotests v1.5.2 h1:kKqmKmS2wCV3tuLnfpbiuN8OlkosQZTpCfiqmiuNAsA=
|
||||
github.com/cweill/gotests v1.5.2/go.mod h1:XZYOJkGVkCRoymaIzmp9Wyi3rUgfA3oOnkuljYrjFV8=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/denisenkom/go-mssqldb v0.0.0-20181014144952-4e0d7dc8888f h1:WH0w/R4Yoey+04HhFxqZ6VX6I0d7RMyw5aXQ9UTvQPs=
|
||||
|
|
133
pkg/models/bulk_list_task.go
Normal file
133
pkg/models/bulk_list_task.go
Normal file
|
@ -0,0 +1,133 @@
|
|||
// Vikunja is a todo-list application to facilitate your life.
|
||||
// Copyright 2018 Vikunja and contributors. All rights reserved.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/web"
|
||||
"github.com/imdario/mergo"
|
||||
)
|
||||
|
||||
// BulkTask is the definition of a bulk update task
|
||||
type BulkTask struct {
|
||||
IDs []int64 `json:"task_ids"`
|
||||
Tasks []*ListTask `json:"-"`
|
||||
ListTask
|
||||
}
|
||||
|
||||
func (bt *BulkTask) checkIfTasksAreOnTheSameList() (err error) {
|
||||
// Get the tasks
|
||||
err = bt.GetTasksByIDs()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(bt.Tasks) == 0 {
|
||||
return ErrBulkTasksNeedAtLeastOne{}
|
||||
}
|
||||
|
||||
// Check if all tasks are in the same list
|
||||
var firstListID = bt.Tasks[0].ListID
|
||||
for _, t := range bt.Tasks {
|
||||
if t.ListID != firstListID {
|
||||
return ErrBulkTasksMustBeInSameList{firstListID, t.ListID}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CanUpdate checks if a user is allowed to update a task
|
||||
func (bt *BulkTask) CanUpdate(a web.Auth) bool {
|
||||
|
||||
err := bt.checkIfTasksAreOnTheSameList()
|
||||
if err != nil {
|
||||
log.Log.Error("Error occurred during CanUpdate for BulkTask: %s", err)
|
||||
return false
|
||||
}
|
||||
|
||||
doer := getUserForRights(a)
|
||||
// A user can update an task if he has write acces to its list
|
||||
l := &List{ID: bt.Tasks[0].ListID}
|
||||
l.ReadOne()
|
||||
return l.CanWrite(doer)
|
||||
}
|
||||
|
||||
// Update updates a bunch of tasks at once
|
||||
// @Summary Update a bunch of tasks at once
|
||||
// @Description Updates a bunch of tasks at once. This includes marking them as done. Note: although you could supply another ID, it will be ignored. Use task_ids instead.
|
||||
// @tags task
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param task body models.BulkTask true "The task object. Looks like a normal task, the only difference is it uses an array of list_ids to update."
|
||||
// @Success 200 {object} models.ListTask "The updated task object."
|
||||
// @Failure 400 {object} code.vikunja.io/web.HTTPError "Invalid task object provided."
|
||||
// @Failure 403 {object} code.vikunja.io/web.HTTPError "The user does not have access to the task (aka its list)"
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /tasks/bulk [post]
|
||||
func (bt *BulkTask) Update() (err error) {
|
||||
|
||||
sess := x.NewSession()
|
||||
defer sess.Close()
|
||||
|
||||
err = sess.Begin()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, oldtask := range bt.Tasks {
|
||||
|
||||
// When a repeating task is marked as done, we update all deadlines and reminders and set it as undone
|
||||
updateDone(oldtask, &bt.ListTask)
|
||||
|
||||
// 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.
|
||||
if err := mergo.Merge(oldtask, &bt.ListTask, mergo.WithOverride); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// And because a false is considered to be a null value, we need to explicitly check that case here.
|
||||
if bt.ListTask.Done == false {
|
||||
oldtask.Done = false
|
||||
}
|
||||
|
||||
_, err = sess.ID(oldtask.ID).
|
||||
Cols("text",
|
||||
"description",
|
||||
"done",
|
||||
"due_date_unix",
|
||||
"reminders_unix",
|
||||
"repeat_after",
|
||||
"parent_task_id",
|
||||
"priority",
|
||||
"start_date_unix",
|
||||
"end_date_unix").
|
||||
Update(oldtask)
|
||||
if err != nil {
|
||||
return sess.Rollback()
|
||||
}
|
||||
}
|
||||
|
||||
err = sess.Commit()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
69
pkg/models/bulk_list_task_test.go
Normal file
69
pkg/models/bulk_list_task_test.go
Normal file
|
@ -0,0 +1,69 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBulkTask_Update(t *testing.T) {
|
||||
type fields struct {
|
||||
IDs []int64
|
||||
Tasks []*ListTask
|
||||
ListTask ListTask
|
||||
User *User
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
wantErr bool
|
||||
wantForbidden bool
|
||||
}{
|
||||
{
|
||||
name: "Test normal update",
|
||||
fields: fields{
|
||||
IDs: []int64{10, 11, 12},
|
||||
ListTask: ListTask{
|
||||
Text: "bulkupdated",
|
||||
},
|
||||
User: &User{ID: 1},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Test with one task on different list",
|
||||
fields: fields{
|
||||
IDs: []int64{10, 11, 12, 13},
|
||||
ListTask: ListTask{
|
||||
Text: "bulkupdated",
|
||||
},
|
||||
User: &User{ID: 1},
|
||||
},
|
||||
wantForbidden: true,
|
||||
},
|
||||
{
|
||||
name: "Test without any tasks",
|
||||
fields: fields{
|
||||
IDs: []int64{},
|
||||
ListTask: ListTask{
|
||||
Text: "bulkupdated",
|
||||
},
|
||||
User: &User{ID: 1},
|
||||
},
|
||||
wantForbidden: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
bt := &BulkTask{
|
||||
IDs: tt.fields.IDs,
|
||||
Tasks: tt.fields.Tasks,
|
||||
ListTask: tt.fields.ListTask,
|
||||
}
|
||||
allowed := bt.CanUpdate(tt.fields.User)
|
||||
if !allowed != tt.wantForbidden {
|
||||
t.Errorf("BulkTask.Update() want forbidden, got %v, want %v", allowed, tt.wantForbidden)
|
||||
}
|
||||
if err := bt.Update(); (err != nil) != tt.wantErr {
|
||||
t.Errorf("BulkTask.Update() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -429,6 +429,51 @@ func (err ErrListTaskDoesNotExist) HTTPError() web.HTTPError {
|
|||
return web.HTTPError{HTTPCode: http.StatusNotFound, Code: ErrCodeListTaskDoesNotExist, Message: "This list task does not exist"}
|
||||
}
|
||||
|
||||
// ErrBulkTasksMustBeInSameList represents a "ErrBulkTasksMustBeInSameList" kind of error.
|
||||
type ErrBulkTasksMustBeInSameList struct {
|
||||
ShouldBeID int64
|
||||
IsID int64
|
||||
}
|
||||
|
||||
// IsErrBulkTasksMustBeInSameList checks if an error is a ErrBulkTasksMustBeInSameList.
|
||||
func IsErrBulkTasksMustBeInSameList(err error) bool {
|
||||
_, ok := err.(ErrBulkTasksMustBeInSameList)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrBulkTasksMustBeInSameList) Error() string {
|
||||
return fmt.Sprintf("All bulk editing tasks must be in the same list. [Should be: %d, is: %d]", err.ShouldBeID, err.IsID)
|
||||
}
|
||||
|
||||
// ErrCodeBulkTasksMustBeInSameList holds the unique world-error code of this error
|
||||
const ErrCodeBulkTasksMustBeInSameList = 4003
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrBulkTasksMustBeInSameList) HTTPError() web.HTTPError {
|
||||
return web.HTTPError{HTTPCode: http.StatusBadRequest, Code: ErrCodeBulkTasksMustBeInSameList, Message: "All tasks must be in the same list."}
|
||||
}
|
||||
|
||||
// ErrBulkTasksNeedAtLeastOne represents a "ErrBulkTasksNeedAtLeastOne" kind of error.
|
||||
type ErrBulkTasksNeedAtLeastOne struct{}
|
||||
|
||||
// IsErrBulkTasksNeedAtLeastOne checks if an error is a ErrBulkTasksNeedAtLeastOne.
|
||||
func IsErrBulkTasksNeedAtLeastOne(err error) bool {
|
||||
_, ok := err.(ErrBulkTasksNeedAtLeastOne)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrBulkTasksNeedAtLeastOne) Error() string {
|
||||
return fmt.Sprintf("Need at least one task when bulk editing tasks")
|
||||
}
|
||||
|
||||
// ErrCodeBulkTasksNeedAtLeastOne holds the unique world-error code of this error
|
||||
const ErrCodeBulkTasksNeedAtLeastOne = 4004
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrBulkTasksNeedAtLeastOne) HTTPError() web.HTTPError {
|
||||
return web.HTTPError{HTTPCode: http.StatusBadRequest, Code: ErrCodeBulkTasksNeedAtLeastOne, Message: "Need at least one tasks to do bulk editing."}
|
||||
}
|
||||
|
||||
// =================
|
||||
// Namespace errors
|
||||
// =================
|
||||
|
|
|
@ -61,3 +61,27 @@
|
|||
updated: 1543626724
|
||||
start_date_unix: 1544600000
|
||||
end_date_unix: 1544700000
|
||||
- id: 10
|
||||
text: 'task #10 basic'
|
||||
created_by_id: 1
|
||||
list_id: 1
|
||||
created: 1543626724
|
||||
updated: 1543626724
|
||||
- id: 11
|
||||
text: 'task #11 basic'
|
||||
created_by_id: 1
|
||||
list_id: 1
|
||||
created: 1543626724
|
||||
updated: 1543626724
|
||||
- id: 12
|
||||
text: 'task #12 basic'
|
||||
created_by_id: 1
|
||||
list_id: 1
|
||||
created: 1543626724
|
||||
updated: 1543626724
|
||||
- id: 13
|
||||
text: 'task #13 basic other list'
|
||||
created_by_id: 1
|
||||
list_id: 2
|
||||
created: 1543626724
|
||||
updated: 1543626724
|
|
@ -7,6 +7,8 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"reflect"
|
||||
"sort"
|
||||
"testing"
|
||||
|
@ -97,6 +99,30 @@ func sortTasksForTesting(by SortBy) (tasks []*ListTask) {
|
|||
StartDateUnix: 1544600000,
|
||||
EndDateUnix: 1544700000,
|
||||
},
|
||||
{
|
||||
ID: 10,
|
||||
Text: "task #10 basic",
|
||||
CreatedByID: 1,
|
||||
ListID: 1,
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
},
|
||||
{
|
||||
ID: 11,
|
||||
Text: "task #11 basic",
|
||||
CreatedByID: 1,
|
||||
ListID: 1,
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
},
|
||||
{
|
||||
ID: 12,
|
||||
Text: "task #12 basic",
|
||||
CreatedByID: 1,
|
||||
ListID: 1,
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
},
|
||||
}
|
||||
|
||||
switch by {
|
||||
|
@ -122,6 +148,7 @@ func sortTasksForTesting(by SortBy) (tasks []*ListTask) {
|
|||
}
|
||||
|
||||
func TestListTask_ReadAll(t *testing.T) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
type fields struct {
|
||||
ID int64
|
||||
Text string
|
||||
|
@ -254,6 +281,30 @@ func TestListTask_ReadAll(t *testing.T) {
|
|||
StartDateUnix: 1544600000,
|
||||
EndDateUnix: 1544700000,
|
||||
},
|
||||
{
|
||||
ID: 10,
|
||||
Text: "task #10 basic",
|
||||
CreatedByID: 1,
|
||||
ListID: 1,
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
},
|
||||
{
|
||||
ID: 11,
|
||||
Text: "task #11 basic",
|
||||
CreatedByID: 1,
|
||||
ListID: 1,
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
},
|
||||
{
|
||||
ID: 12,
|
||||
Text: "task #12 basic",
|
||||
CreatedByID: 1,
|
||||
ListID: 1,
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
},
|
||||
{
|
||||
ID: 4,
|
||||
Text: "task #4 low prio",
|
||||
|
@ -311,7 +362,113 @@ func TestListTask_ReadAll(t *testing.T) {
|
|||
a: &User{ID: 1},
|
||||
page: 0,
|
||||
},
|
||||
want: sortTasksForTesting(SortTasksByDueDateAsc),
|
||||
want: []*ListTask{
|
||||
{
|
||||
ID: 1,
|
||||
Text: "task #1",
|
||||
CreatedByID: 1,
|
||||
ListID: 1,
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
},
|
||||
{
|
||||
ID: 2,
|
||||
Text: "task #2 done",
|
||||
Done: true,
|
||||
CreatedByID: 1,
|
||||
ListID: 1,
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
},
|
||||
{
|
||||
ID: 3,
|
||||
Text: "task #3 high prio",
|
||||
CreatedByID: 1,
|
||||
ListID: 1,
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
Priority: 100,
|
||||
},
|
||||
{
|
||||
ID: 4,
|
||||
Text: "task #4 low prio",
|
||||
CreatedByID: 1,
|
||||
ListID: 1,
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
Priority: 1,
|
||||
},
|
||||
{
|
||||
ID: 7,
|
||||
Text: "task #7 with start date",
|
||||
CreatedByID: 1,
|
||||
ListID: 1,
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
StartDateUnix: 1544600000,
|
||||
},
|
||||
{
|
||||
ID: 8,
|
||||
Text: "task #8 with end date",
|
||||
CreatedByID: 1,
|
||||
ListID: 1,
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
EndDateUnix: 1544700000,
|
||||
},
|
||||
{
|
||||
ID: 9,
|
||||
Text: "task #9 with start and end date",
|
||||
CreatedByID: 1,
|
||||
ListID: 1,
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
StartDateUnix: 1544600000,
|
||||
EndDateUnix: 1544700000,
|
||||
},
|
||||
{
|
||||
ID: 10,
|
||||
Text: "task #10 basic",
|
||||
CreatedByID: 1,
|
||||
ListID: 1,
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
},
|
||||
{
|
||||
ID: 11,
|
||||
Text: "task #11 basic",
|
||||
CreatedByID: 1,
|
||||
ListID: 1,
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
},
|
||||
{
|
||||
ID: 12,
|
||||
Text: "task #12 basic",
|
||||
CreatedByID: 1,
|
||||
ListID: 1,
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
},
|
||||
{
|
||||
ID: 6,
|
||||
Text: "task #6 lower due date",
|
||||
CreatedByID: 1,
|
||||
ListID: 1,
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
DueDateUnix: 1543616724,
|
||||
},
|
||||
{
|
||||
ID: 5,
|
||||
Text: "task #5 higher due date",
|
||||
CreatedByID: 1,
|
||||
ListID: 1,
|
||||
Created: 1543626724,
|
||||
Updated: 1543626724,
|
||||
DueDateUnix: 1543636724,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
|
@ -460,20 +617,20 @@ func TestListTask_ReadAll(t *testing.T) {
|
|||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("ListTask.ReadAll() = %v, want %v", got, tt.want)
|
||||
/*fmt.Println("Got:")
|
||||
fmt.Println("Got:")
|
||||
gotslice := got.([]*ListTask)
|
||||
for _, g := range gotslice {
|
||||
fmt.Println(g.Priority, g.Text)
|
||||
fmt.Println(g.Text)
|
||||
//fmt.Println(g.StartDateUnix)
|
||||
//fmt.Println(g.EndDateUnix)
|
||||
}
|
||||
fmt.Println("Want:")
|
||||
wantslice := tt.want.([]*ListTask)
|
||||
for _, w := range wantslice {
|
||||
fmt.Println(w.Priority, w.Text)
|
||||
fmt.Println(w.Text)
|
||||
//fmt.Println(w.StartDateUnix)
|
||||
//fmt.Println(w.EndDateUnix)
|
||||
}*/
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -153,3 +153,46 @@ func GetListTaskByID(listTaskID int64) (listTask ListTask, err error) {
|
|||
|
||||
return
|
||||
}
|
||||
|
||||
// GetTasksByIDs returns all tasks for a list of ids
|
||||
func (bt *BulkTask) GetTasksByIDs() (err error) {
|
||||
for _, id := range bt.IDs {
|
||||
if id < 1 {
|
||||
return ErrListTaskDoesNotExist{id}
|
||||
}
|
||||
}
|
||||
|
||||
err = x.In("id", bt.IDs).Find(&bt.Tasks)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// We use a map, to avoid looping over two slices at once
|
||||
var usermapids = make(map[int64]bool) // Bool ist just something, doesn't acutually matter
|
||||
for _, list := range bt.Tasks {
|
||||
usermapids[list.CreatedByID] = true
|
||||
}
|
||||
|
||||
// Make a slice from the map
|
||||
var userids []int64
|
||||
for uid := range usermapids {
|
||||
userids = append(userids, uid)
|
||||
}
|
||||
|
||||
// Get all users for the tasks
|
||||
var users []*User
|
||||
err = x.In("id", userids).Find(&users)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for in, task := range bt.Tasks {
|
||||
for _, u := range users {
|
||||
if task.CreatedByID == u.ID {
|
||||
bt.Tasks[in].CreatedBy = *u
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
|
|
@ -91,16 +91,8 @@ func (i *ListTask) Update() (err error) {
|
|||
return
|
||||
}
|
||||
|
||||
// When a repeating task is marked, as done, we update all deadlines and reminders and set it as undone
|
||||
if !ot.Done && i.Done && ot.RepeatAfter > 0 {
|
||||
ot.DueDateUnix = ot.DueDateUnix + ot.RepeatAfter
|
||||
|
||||
for in, r := range ot.RemindersUnix {
|
||||
ot.RemindersUnix[in] = r + ot.RepeatAfter
|
||||
}
|
||||
|
||||
i.Done = false
|
||||
}
|
||||
// When a repeating task is marked as done, we update all deadlines and reminders and set it as undone
|
||||
updateDone(&ot, i)
|
||||
|
||||
// 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
|
||||
|
@ -129,3 +121,15 @@ func (i *ListTask) Update() (err error) {
|
|||
*i = ot
|
||||
return
|
||||
}
|
||||
|
||||
func updateDone(oldTask *ListTask, newTask *ListTask) {
|
||||
if !oldTask.Done && newTask.Done && oldTask.RepeatAfter > 0 {
|
||||
oldTask.DueDateUnix = oldTask.DueDateUnix + oldTask.RepeatAfter // assuming we'll save the old task (merged)
|
||||
|
||||
for in, r := range oldTask.RemindersUnix {
|
||||
oldTask.RemindersUnix[in] = r + oldTask.RepeatAfter
|
||||
}
|
||||
|
||||
newTask.Done = false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -222,6 +222,13 @@ func RegisterRoutes(e *echo.Echo) {
|
|||
a.DELETE("/tasks/:listtask", taskHandler.DeleteWeb)
|
||||
a.POST("/tasks/:listtask", taskHandler.UpdateWeb)
|
||||
|
||||
bulkTaskHandler := &handler.WebHandler{
|
||||
EmptyStruct: func() handler.CObject {
|
||||
return &models.BulkTask{}
|
||||
},
|
||||
}
|
||||
a.POST("/tasks/bulk", bulkTaskHandler.UpdateWeb)
|
||||
|
||||
listTeamHandler := &handler.WebHandler{
|
||||
EmptyStruct: func() handler.CObject {
|
||||
return &models.TeamList{}
|
||||
|
|
Loading…
Reference in a new issue