diff --git a/Featurecreep.md b/Featurecreep.md index 035be8bd588..8981304ef69 100644 --- a/Featurecreep.md +++ b/Featurecreep.md @@ -211,6 +211,7 @@ Sorry for some of them being in German, I'll tranlate them at some point. * [x] Editable via task edit, like assignees * [x] "One endpoint to rule them all" -> Array-addable * [x] Description should be longtext +* [x] Percent done - For now just a float, may later depend on how many sub tasks are done or so * [ ] Attachments * [ ] Related tasks -> settable with a "kind" of relation like blocked, subtask, or just related or so * [ ] Should be possible to relate tasks which are not in the same list @@ -221,7 +222,6 @@ Sorry for some of them being in German, I'll tranlate them at some point. * Relation Kinds (for now): Subtask/Parent Task (from subtask, no extra relation), Related, later also Blocked/Blocking * Should also be possible to create a new task with relations directly * For everything else dedicated endpoints to manage relations -* [ ] Percent done - For now just a float, may later depend on how many sub tasks are done or so * [ ] Move tasks between lists * [ ] "Status" field for things like "New", "In Progress", "Done", etc (customizable statuses) diff --git a/pkg/integrations/task_test.go b/pkg/integrations/task_test.go index b2100cd5f41..b034b8c376e 100644 --- a/pkg/integrations/task_test.go +++ b/pkg/integrations/task_test.go @@ -80,33 +80,33 @@ func TestTask(t *testing.T) { t.Run("by priority", func(t *testing.T) { rec, err := testHandler.testReadAllWithUser(url.Values{"sort": []string{"priority"}}, nil) assert.NoError(t, err) - assert.Contains(t, rec.Body.String(), `[{"id":3,"text":"task #3 high prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":100,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":4,"text":"task #4 low prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":1`) + assert.Contains(t, rec.Body.String(), `[{"id":3,"text":"task #3 high prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":100,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":4,"text":"task #4 low prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":1`) }) t.Run("by priority desc", func(t *testing.T) { rec, err := testHandler.testReadAllWithUser(url.Values{"sort": []string{"prioritydesc"}}, nil) assert.NoError(t, err) - assert.Contains(t, rec.Body.String(), `[{"id":3,"text":"task #3 high prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":100,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":4,"text":"task #4 low prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":1`) + assert.Contains(t, rec.Body.String(), `[{"id":3,"text":"task #3 high prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":100,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":4,"text":"task #4 low prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":1`) }) t.Run("by priority asc", func(t *testing.T) { rec, err := testHandler.testReadAllWithUser(url.Values{"sort": []string{"priorityasc"}}, nil) assert.NoError(t, err) - assert.Contains(t, rec.Body.String(), `{"id":32,"text":"task #32","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":3,"repeatAfter":0,"parentTaskID":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":4,"text":"task #4 low prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":1,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":3,"text":"task #3 high prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":100,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}}]`) + assert.Contains(t, rec.Body.String(), `{"id":33,"text":"task #33 with percent done","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0.5,"subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":4,"text":"task #4 low prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":1,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":3,"text":"task #3 high prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":100,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}}]`) }) // should equal duedate desc t.Run("by duedate", func(t *testing.T) { rec, err := testHandler.testReadAllWithUser(url.Values{"sort": []string{"duedate"}}, nil) assert.NoError(t, err) - assert.Contains(t, rec.Body.String(), `[{"id":5,"text":"task #5 higher due date","description":"","done":false,"doneAt":0,"dueDate":1543636724,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":6,"text":"task #6 lower due date"`) + assert.Contains(t, rec.Body.String(), `[{"id":5,"text":"task #5 higher due date","description":"","done":false,"doneAt":0,"dueDate":1543636724,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":6,"text":"task #6 lower due date"`) }) t.Run("by duedate desc", func(t *testing.T) { rec, err := testHandler.testReadAllWithUser(url.Values{"sort": []string{"duedatedesc"}}, nil) assert.NoError(t, err) - assert.Contains(t, rec.Body.String(), `[{"id":5,"text":"task #5 higher due date","description":"","done":false,"doneAt":0,"dueDate":1543636724,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":6,"text":"task #6 lower due date"`) + assert.Contains(t, rec.Body.String(), `[{"id":5,"text":"task #5 higher due date","description":"","done":false,"doneAt":0,"dueDate":1543636724,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":6,"text":"task #6 lower due date"`) }) t.Run("by duedate asc", func(t *testing.T) { rec, err := testHandler.testReadAllWithUser(url.Values{"sort": []string{"duedateasc"}}, nil) assert.NoError(t, err) - assert.Contains(t, rec.Body.String(), `{"id":6,"text":"task #6 lower due date","description":"","done":false,"doneAt":0,"dueDate":1543616724,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":5,"text":"task #5 higher due date","description":"","done":false,"doneAt":0,"dueDate":1543636724,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}}]`) + assert.Contains(t, rec.Body.String(), `{"id":6,"text":"task #6 lower due date","description":"","done":false,"doneAt":0,"dueDate":1543616724,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":5,"text":"task #5 higher due date","description":"","done":false,"doneAt":0,"dueDate":1543636724,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}}]`) }) t.Run("invalid parameter", func(t *testing.T) { // Invalid parameter should not sort at all @@ -328,6 +328,18 @@ func TestTask(t *testing.T) { assert.Contains(t, rec.Body.String(), `"hexColor":""`) assert.NotContains(t, rec.Body.String(), `"hexColor":"f0f0f0"`) }) + t.Run("Percent Done", func(t *testing.T) { + rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"percentDone":0.1}`) + assert.NoError(t, err) + assert.Contains(t, rec.Body.String(), `"percentDone":0.1`) + assert.NotContains(t, rec.Body.String(), `"percentDone":0,`) + }) + t.Run("Percent Done unset", func(t *testing.T) { + rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "33"}, `{"percentDone":0}`) + assert.NoError(t, err) + assert.Contains(t, rec.Body.String(), `"percentDone":0,`) + assert.NotContains(t, rec.Body.String(), `"percentDone":0.1`) + }) }) t.Run("Nonexisting", func(t *testing.T) { diff --git a/pkg/migration/20190920185205.go b/pkg/migration/20190920185205.go new file mode 100644 index 00000000000..7629282b4ca --- /dev/null +++ b/pkg/migration/20190920185205.go @@ -0,0 +1,44 @@ +// Copyright 2019 Vikunja and contriubtors. All rights reserved. +// +// This file is part of Vikunja. +// +// Vikunja 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. +// +// Vikunja 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 Vikunja. If not, see . + +package migration + +import ( + "github.com/go-xorm/xorm" + "src.techknowlogick.com/xormigrate" +) + +type task20190920185205 struct { + PercentDone float64 `xorm:"DOUBLE null" json:"percentDone"` +} + +func (task20190920185205) TableName() string { + return "tasks" +} + +func init() { + migrations = append(migrations, &xormigrate.Migration{ + ID: "20190920185205", + Description: "Add task percent done", + Migrate: func(tx *xorm.Engine) error { + return tx.Sync2(task20190920185205{}) + }, + Rollback: func(tx *xorm.Engine) error { + return dropTableColum(tx, "tasks", "percent_done") + }, + }) +} diff --git a/pkg/models/fixtures/tasks.yml b/pkg/models/fixtures/tasks.yml index 285565ae29e..73007088dc3 100644 --- a/pkg/models/fixtures/tasks.yml +++ b/pkg/models/fixtures/tasks.yml @@ -204,4 +204,11 @@ list_id: 3 created: 1543626724 updated: 1543626724 +- id: 33 + text: 'task #33 with percent done' + created_by_id: 1 + list_id: 1 + percent_done: 0.5 + created: 1543626724 + updated: 1543626724 diff --git a/pkg/models/task_readall_test.go b/pkg/models/task_readall_test.go index b5646c77d86..de8651721e8 100644 --- a/pkg/models/task_readall_test.go +++ b/pkg/models/task_readall_test.go @@ -336,6 +336,16 @@ func sortTasksForTesting(by SortBy) (tasks []*Task) { Created: 1543626724, Updated: 1543626724, }, + { + ID: 33, + Text: "task #33 with percent done", + CreatedByID: 1, + CreatedBy: user1, + ListID: 1, + PercentDone: 0.5, + Created: 1543626724, + Updated: 1543626724, + }, } switch by { diff --git a/pkg/models/tasks.go b/pkg/models/tasks.go index be77ded57a3..ade9767937e 100644 --- a/pkg/models/tasks.go +++ b/pkg/models/tasks.go @@ -60,6 +60,8 @@ type Task struct { Labels []*Label `xorm:"-" json:"labels"` // The task color in hex HexColor string `xorm:"varchar(6) null" json:"hexColor" valid:"runelength(0|6)" maxLength:"6"` + // Determines how far a task is left from being done + PercentDone float64 `xorm:"DOUBLE null" json:"percentDone"` // The UID is currently not used for anything other than caldav, which is why we don't expose it over json UID string `xorm:"varchar(250) null" json:"-"` @@ -632,6 +634,10 @@ func (t *Task) Update() (err error) { if t.HexColor == "" { ot.HexColor = "" } + // Percent DOnw + if t.PercentDone == 0 { + ot.PercentDone = 0 + } _, err = x.ID(t.ID). Cols("text", @@ -644,7 +650,8 @@ func (t *Task) Update() (err error) { "start_date_unix", "end_date_unix", "hex_color", - "done_at_unix"). + "done_at_unix", + "percent_done"). Update(ot) *t = ot if err != nil { diff --git a/pkg/swagger/docs.go b/pkg/swagger/docs.go index 8e89b147f87..11c553cb4c5 100644 --- a/pkg/swagger/docs.go +++ b/pkg/swagger/docs.go @@ -1,6 +1,6 @@ // GENERATED BY THE COMMAND ABOVE; DO NOT EDIT // This file was generated by swaggo/swag at -// 2019-09-20 17:41:39.321673846 +0200 CEST m=+0.137010842 +// 2019-09-20 19:04:01.162535903 +0200 CEST m=+0.097033344 package swagger @@ -412,7 +412,7 @@ var doc = `{ "JWTKeyAuth": [] } ], - "description": "Returns a list by its ID.", + "description": "Returns a team by its ID.", "consumes": [ "application/json" ], @@ -420,13 +420,13 @@ var doc = `{ "application/json" ], "tags": [ - "list" + "team" ], - "summary": "Gets one list", + "summary": "Gets one team", "parameters": [ { "type": "integer", - "description": "List ID", + "description": "Team ID", "name": "id", "in": "path", "required": true @@ -434,14 +434,14 @@ var doc = `{ ], "responses": { "200": { - "description": "The list", + "description": "The team", "schema": { "type": "object", - "$ref": "#/definitions/models.List" + "$ref": "#/definitions/models.Team" } }, "403": { - "description": "The user does not have access to the list", + "description": "The user does not have access to the team", "schema": { "type": "object", "$ref": "#/definitions/code.vikunja.io.web.HTTPError" @@ -4147,6 +4147,10 @@ var doc = `{ "description": "If the task is a subtask, this is the id of its parent.", "type": "integer" }, + "percentDone": { + "description": "Determines how far a task is left from being done", + "type": "number" + }, "priority": { "description": "The task priority. Can be anything you want, it is possible to sort by this later.", "type": "integer" @@ -4558,6 +4562,10 @@ var doc = `{ "description": "If the task is a subtask, this is the id of its parent.", "type": "integer" }, + "percentDone": { + "description": "Determines how far a task is left from being done", + "type": "number" + }, "priority": { "description": "The task priority. Can be anything you want, it is possible to sort by this later.", "type": "integer" diff --git a/pkg/swagger/swagger.json b/pkg/swagger/swagger.json index 8cd7351083a..ab8495da1dc 100644 --- a/pkg/swagger/swagger.json +++ b/pkg/swagger/swagger.json @@ -399,7 +399,7 @@ "JWTKeyAuth": [] } ], - "description": "Returns a list by its ID.", + "description": "Returns a team by its ID.", "consumes": [ "application/json" ], @@ -407,13 +407,13 @@ "application/json" ], "tags": [ - "list" + "team" ], - "summary": "Gets one list", + "summary": "Gets one team", "parameters": [ { "type": "integer", - "description": "List ID", + "description": "Team ID", "name": "id", "in": "path", "required": true @@ -421,14 +421,14 @@ ], "responses": { "200": { - "description": "The list", + "description": "The team", "schema": { "type": "object", - "$ref": "#/definitions/models.List" + "$ref": "#/definitions/models.Team" } }, "403": { - "description": "The user does not have access to the list", + "description": "The user does not have access to the team", "schema": { "type": "object", "$ref": "#/definitions/code.vikunja.io/web.HTTPError" @@ -4133,6 +4133,10 @@ "description": "If the task is a subtask, this is the id of its parent.", "type": "integer" }, + "percentDone": { + "description": "Determines how far a task is left from being done", + "type": "number" + }, "priority": { "description": "The task priority. Can be anything you want, it is possible to sort by this later.", "type": "integer" @@ -4544,6 +4548,10 @@ "description": "If the task is a subtask, this is the id of its parent.", "type": "integer" }, + "percentDone": { + "description": "Determines how far a task is left from being done", + "type": "number" + }, "priority": { "description": "The task priority. Can be anything you want, it is possible to sort by this later.", "type": "integer" diff --git a/pkg/swagger/swagger.yaml b/pkg/swagger/swagger.yaml index 475bcdc10bb..25fa9235d92 100644 --- a/pkg/swagger/swagger.yaml +++ b/pkg/swagger/swagger.yaml @@ -77,6 +77,9 @@ definitions: parentTaskID: description: If the task is a subtask, this is the id of its parent. type: integer + percentDone: + description: Determines how far a task is left from being done + type: number priority: description: The task priority. Can be anything you want, it is possible to sort by this later. @@ -405,6 +408,9 @@ definitions: parentTaskID: description: If the task is a subtask, this is the id of its parent. type: integer + percentDone: + description: Determines how far a task is left from being done + type: number priority: description: The task priority. Can be anything you want, it is possible to sort by this later. @@ -1004,9 +1010,9 @@ paths: get: consumes: - application/json - description: Returns a list by its ID. + description: Returns a team by its ID. parameters: - - description: List ID + - description: Team ID in: path name: id required: true @@ -1015,12 +1021,12 @@ paths: - application/json responses: "200": - description: The list + description: The team schema: - $ref: '#/definitions/models.List' + $ref: '#/definitions/models.Team' type: object "403": - description: The user does not have access to the list + description: The user does not have access to the team schema: $ref: '#/definitions/code.vikunja.io/web.HTTPError' type: object @@ -1031,9 +1037,9 @@ paths: type: object security: - JWTKeyAuth: [] - summary: Gets one list + summary: Gets one team tags: - - list + - team post: consumes: - application/json