Add bulk edit for tasks (#42)

This commit is contained in:
konrad 2018-12-28 21:49:46 +00:00 committed by Gitea
parent b050132f4f
commit 3814b8a504
16 changed files with 862 additions and 20 deletions

View file

@ -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

View file

@ -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}}
###
@ -137,4 +137,16 @@ 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"
}
###

View file

@ -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": {

View file

@ -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. |

View file

@ -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": {

View file

@ -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
View file

@ -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
View file

@ -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=

View 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
}

View 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)
}
})
}
}

View file

@ -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
// =================

View file

@ -60,4 +60,28 @@
created: 1543626724
updated: 1543626724
start_date_unix: 1544600000
end_date_unix: 1544700000
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

View file

@ -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)
}*/
}
}
})
}

View file

@ -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
}

View file

@ -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
}
}

View file

@ -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{}