Task assignees (#44)

This commit is contained in:
konrad 2018-12-29 14:29:50 +00:00 committed by Gitea
parent 37345e6bd3
commit d39007baa0
15 changed files with 226 additions and 57 deletions

View file

@ -99,8 +99,8 @@ Sorry for some of them being in German, I'll tranlate them at some point.
* [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
* [x] Assignees
* [ ] Labels
* [ ] Assignees
* [ ] 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

@ -15,7 +15,7 @@ POST http://localhost:8080/api/v1/register
Content-Type: application/json
{
"username": "user5",
"username": "user",
"password": "1234",
"email": "5@knt.li"
}

View file

@ -1,35 +1,34 @@
# Get all lists
GET http://localhost:8080/api/v1/namespaces/1/lists
GET http://localhost:8080/api/v1/namespaces/1
Authorization: Bearer {{auth_token}}
###
# Get one list
GET http://localhost:8080/api/v1/lists/1163
GET http://localhost:8080/api/v1/lists/1172
Authorization: Bearer {{auth_token}}
###
# Add a new list
PUT http://localhost:8080/api/v1/namespaces/6/lists
PUT http://localhost:8080/api/v1/namespaces/1/lists
Authorization: Bearer {{auth_token}}
Content-Type: application/json
{
"title": "sffffc me only"
"title": "test"
}
###
# Add a new item
PUT http://localhost:8080/api/v1/lists/15
PUT http://localhost:8080/api/v1/lists/1
Authorization: Bearer {{auth_token}}
Content-Type: application/json
{
"text": "this is a subtask 2",
"description": "Schinken",
"parentTaskID": 34
"text": "Task",
"description": "Schinken"
}
###
@ -83,11 +82,11 @@ Authorization: Bearer {{auth_token}}
###
# Give a user access to that list
PUT http://localhost:8080/api/v1/lists/30/users
PUT http://localhost:8080/api/v1/lists/1172/users
Authorization: Bearer {{auth_token}}
Content-Type: application/json
{"user_id":3, "right":1}
{"user_id":1, "right":1}
###
@ -131,11 +130,13 @@ GET http://localhost:8080/api/v1/tasks/caldav
###
# Update a task
POST http://localhost:8080/api/v1/tasks/3491
POST http://localhost:8080/api/v1/tasks/1
Authorization: Bearer {{auth_token}}
Content-Type: application/json
{"startDate":1546804000, "endDate": 1546805000}
{"assignees":[
{"id": 1}
]}
###

View file

@ -1,6 +1,6 @@
# Get all users
GET http://localhost:8080/api/v1/users
GET http://localhost:8080/api/v1/user
Authorization: Bearer {{auth_token}}
######

View file

@ -1,6 +1,6 @@
// GENERATED BY THE COMMAND ABOVE; DO NOT EDIT
// This file was generated by swaggo/swag at
// 2018-12-28 01:18:16.824999107 +0100 CET m=+0.098072896
// 2018-12-29 15:14:06.225275112 +0100 CET m=+0.295589005
package docs
@ -3012,6 +3012,12 @@ var doc = `{
"models.BulkTask": {
"type": "object",
"properties": {
"assignees": {
"type": "array",
"items": {
"$ref": "#/definitions/models.User"
}
},
"created": {
"type": "integer"
},
@ -3116,6 +3122,12 @@ var doc = `{
"models.ListTask": {
"type": "object",
"properties": {
"assignees": {
"type": "array",
"items": {
"$ref": "#/definitions/models.User"
}
},
"created": {
"type": "integer"
},

View file

@ -2998,6 +2998,12 @@
"models.BulkTask": {
"type": "object",
"properties": {
"assignees": {
"type": "array",
"items": {
"$ref": "#/definitions/models.User"
}
},
"created": {
"type": "integer"
},
@ -3102,6 +3108,12 @@
"models.ListTask": {
"type": "object",
"properties": {
"assignees": {
"type": "array",
"items": {
"$ref": "#/definitions/models.User"
}
},
"created": {
"type": "integer"
},

View file

@ -13,6 +13,10 @@ definitions:
type: object
models.BulkTask:
properties:
assignees:
items:
$ref: '#/definitions/models.User'
type: array
created:
type: integer
createdBy:
@ -82,6 +86,10 @@ definitions:
type: object
models.ListTask:
properties:
assignees:
items:
$ref: '#/definitions/models.User'
type: array
created:
type: integer
createdBy:

View file

@ -95,6 +95,11 @@ func (bt *BulkTask) Update() (err error) {
// When a repeating task is marked as done, we update all deadlines and reminders and set it as undone
updateDone(oldtask, &bt.ListTask)
// Update the assignees
if err := oldtask.updateTaskAssignees(bt.Assignees); err != nil {
return err
}
// 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.

View file

@ -69,7 +69,12 @@ func (l *List) CanRead(a web.Auth) bool {
return true
}
return l.checkListTeamRight(user, TeamRightRead)
if l.checkListTeamRight(user, TeamRightRead) {
return true
}
// Users who are able to write should also be able to read
return l.CanWrite(a)
}
// CanDelete checks if the user can delete a list

View file

@ -36,6 +36,7 @@ type ListTask struct {
Priority int64 `xorm:"int(11)" json:"priority"`
StartDateUnix int64 `xorm:"int(11) INDEX" json:"startDate"`
EndDateUnix int64 `xorm:"int(11) INDEX" json:"endDate"`
Assignees []*User `xorm:"-" json:"assignees"`
Sorting string `xorm:"-" json:"-" param:"sort"` // Parameter to sort by
StartDateSortUnix int64 `xorm:"-" json:"-" param:"startdatefilter"`
@ -57,6 +58,25 @@ func (ListTask) TableName() string {
return "tasks"
}
// ListTaskAssginee represents an assignment of a user to a task
type ListTaskAssginee struct {
ID int64 `xorm:"int(11) autoincr not null unique pk"`
TaskID int64 `xorm:"int(11) not null"`
UserID int64 `xorm:"int(11) not null"`
Created int64 `xorm:"created"`
}
// TableName makes a pretty table name
func (ListTaskAssginee) TableName() string {
return "task_assignees"
}
// ListTaskAssigneeWithUser is a helper type to deal with user joins
type ListTaskAssigneeWithUser struct {
TaskID int64
User `xorm:"extends"`
}
// GetTasksByListID gets all todotasks for a list
func GetTasksByListID(listID int64) (tasks []*ListTask, err error) {
err = x.Where("list_id = ?", listID).Find(&tasks)
@ -72,9 +92,11 @@ func GetTasksByListID(listID int64) (tasks []*ListTask, err error) {
// make a map so we can put in subtasks more easily
taskMap := make(map[int64]*ListTask, len(tasks))
// Get all users and put them into the array
// Get all users & task ids and put them into the array
var userIDs []int64
var taskIDs []int64
for _, i := range tasks {
taskIDs = append(taskIDs, i.ID)
found := false
for _, u := range userIDs {
if i.CreatedByID == u {
@ -90,6 +112,18 @@ func GetTasksByListID(listID int64) (tasks []*ListTask, err error) {
taskMap[i.ID] = i
}
// Get all assignees
taskAssignees, err := getRawTaskAssigneesForTasks(taskIDs)
if err != nil {
return
}
// Put the assignees in the task map
for _, a := range taskAssignees {
if a != nil {
taskMap[a.TaskID].Assignees = append(taskMap[a.TaskID].Assignees, &a.User)
}
}
var users []User
err = x.In("id", userIDs).Find(&users)
if err != nil {
@ -130,6 +164,16 @@ func GetTasksByListID(listID int64) (tasks []*ListTask, err error) {
return
}
func getRawTaskAssigneesForTasks(taskIDs []int64) (taskAssignees []*ListTaskAssigneeWithUser, err error) {
taskAssignees = []*ListTaskAssigneeWithUser{nil}
err = x.Table("task_assignees").
Select("task_id, users.*").
In("task_id", taskIDs).
Join("INNER", "users", "task_assignees.user_id = users.id").
Find(&taskAssignees)
return
}
// GetListTaskByID returns all tasks a list has
func GetListTaskByID(listTaskID int64) (listTask ListTask, err error) {
if listTaskID < 1 {
@ -151,6 +195,17 @@ func GetListTaskByID(listTaskID int64) (listTask ListTask, err error) {
}
listTask.CreatedBy = u
// Get assignees
taskAssignees, err := getRawTaskAssigneesForTasks([]int64{listTaskID})
if err != nil {
return
}
for _, u := range taskAssignees {
if u != nil {
listTask.Assignees = append(listTask.Assignees, &u.User)
}
}
return
}

View file

@ -36,21 +36,21 @@ import (
// @Failure 403 {object} code.vikunja.io/web.HTTPError "The user does not have access to the list"
// @Failure 500 {object} models.Message "Internal error"
// @Router /lists/{id} [put]
func (i *ListTask) Create(a web.Auth) (err error) {
func (t *ListTask) Create(a web.Auth) (err error) {
doer, err := getUserWithError(a)
if err != nil {
return err
}
i.ID = 0
t.ID = 0
// Check if we have at least a text
if i.Text == "" {
if t.Text == "" {
return ErrListTaskCannotBeEmpty{}
}
// Check if the list exists
l := &List{ID: i.ListID}
l := &List{ID: t.ListID}
if err = l.GetSimpleByID(); err != nil {
return
}
@ -60,9 +60,14 @@ func (i *ListTask) Create(a web.Auth) (err error) {
return err
}
i.CreatedByID = u.ID
i.CreatedBy = u
if _, err = x.Insert(i); err != nil {
t.CreatedByID = u.ID
t.CreatedBy = u
if _, err = x.Insert(t); err != nil {
return err
}
// Update the assignees
if err := t.updateTaskAssignees(t.Assignees); err != nil {
return err
}
@ -84,29 +89,34 @@ func (i *ListTask) Create(a web.Auth) (err error) {
// @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/{id} [post]
func (i *ListTask) Update() (err error) {
func (t *ListTask) Update() (err error) {
// Check if the task exists
ot, err := GetListTaskByID(i.ID)
ot, err := GetListTaskByID(t.ID)
if err != nil {
return
}
// When a repeating task is marked as done, we update all deadlines and reminders and set it as undone
updateDone(&ot, i)
updateDone(&ot, t)
// Update the assignees
if err := ot.updateTaskAssignees(t.Assignees); err != nil {
return err
}
// 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(&ot, i, mergo.WithOverride); err != nil {
if err := mergo.Merge(&ot, t, 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 i.Done == false {
if t.Done == false {
ot.Done = false
}
_, err = x.ID(i.ID).
_, err = x.ID(t.ID).
Cols("text",
"description",
"done",
@ -118,7 +128,7 @@ func (i *ListTask) Update() (err error) {
"start_date_unix",
"end_date_unix").
Update(ot)
*i = ot
*t = ot
return
}
@ -133,3 +143,73 @@ func updateDone(oldTask *ListTask, newTask *ListTask) {
newTask.Done = false
}
}
// Create a bunch of task assignees
func (t *ListTask) updateTaskAssignees(assignees []*User) (err error) {
// Get old assignees to delete
var found bool
var assigneesToDelete []int64
for _, oldAssignee := range t.Assignees {
found = false
for _, newAssignee := range assignees {
if newAssignee.ID == oldAssignee.ID {
found = true // If a new assignee is already in the list with old assignees
break
}
}
// Put all assignees which are only on the old list to the trash
if !found {
assigneesToDelete = append(assigneesToDelete, oldAssignee.ID)
}
}
// Delete all assignees not passed
if len(assigneesToDelete) > 0 {
_, err = x.In("user_id", assigneesToDelete).
And("task_id = ?", t.ID).
Delete(ListTaskAssginee{})
if err != nil {
return err
}
}
// Get the list to perform later checks
list := List{ID: t.ListID}
err = list.ReadOne()
if err != nil {
return
}
// Loop through our users and add them
AddNewAssignee:
for _, u := range assignees {
// Check if the user is already assigned and assign him only if not
for _, oldAssignee := range t.Assignees {
if oldAssignee.ID == u.ID {
// continue outer loop
continue AddNewAssignee
}
}
// Check if the user exists and has access to the list
newAssignee, err := GetUserByID(u.ID)
if err != nil {
return err
}
if !list.CanRead(&newAssignee) {
return ErrUserDoesNotHaveAccessToList{list.ID, u.ID}
}
_, err = x.Insert(ListTaskAssginee{
TaskID: t.ID,
UserID: u.ID,
})
if err != nil {
return err
}
}
return
}

View file

@ -33,15 +33,20 @@ import (
// @Failure 403 {object} code.vikunja.io/web.HTTPError "The user does not have access to the list"
// @Failure 500 {object} models.Message "Internal error"
// @Router /tasks/{id} [delete]
func (i *ListTask) Delete() (err error) {
func (t *ListTask) Delete() (err error) {
// Check if it exists
_, err = GetListTaskByID(i.ID)
_, err = GetListTaskByID(t.ID)
if err != nil {
return
}
if _, err = x.ID(i.ID).Delete(ListTask{}); err != nil {
if _, err = x.ID(t.ID).Delete(ListTask{}); err != nil {
return err
}
// Delete assignees
if _, err = x.Where("task_id = ?", t.ID).Delete(ListTaskAssginee{}); err != nil {
return err
}

View file

@ -22,11 +22,11 @@ import (
)
// CanDelete checks if the user can delete an task
func (i *ListTask) CanDelete(a web.Auth) bool {
func (t *ListTask) CanDelete(a web.Auth) bool {
doer := getUserForRights(a)
// Get the task
lI, err := GetListTaskByID(i.ID)
lI, err := GetListTaskByID(t.ID)
if err != nil {
log.Log.Error("Error occurred during CanDelete for ListTask: %s", err)
return false
@ -39,11 +39,11 @@ func (i *ListTask) CanDelete(a web.Auth) bool {
}
// CanUpdate determines if a user has the right to update a list task
func (i *ListTask) CanUpdate(a web.Auth) bool {
func (t *ListTask) CanUpdate(a web.Auth) bool {
doer := getUserForRights(a)
// Get the task
lI, err := GetListTaskByID(i.ID)
lI, err := GetListTaskByID(t.ID)
if err != nil {
log.Log.Error("Error occurred during CanDelete for ListTask: %s", err)
return false
@ -56,11 +56,11 @@ func (i *ListTask) CanUpdate(a web.Auth) bool {
}
// CanCreate determines if a user has the right to create a list task
func (i *ListTask) CanCreate(a web.Auth) bool {
func (t *ListTask) CanCreate(a web.Auth) bool {
doer := getUserForRights(a)
// A user can create an task if he has write acces to its list
l := &List{ID: i.ListID}
l := &List{ID: t.ListID}
l.ReadOne()
return l.CanWrite(doer)
}

View file

@ -14,22 +14,6 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
// 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 (

View file

@ -68,6 +68,7 @@ func init() {
new(Namespace),
new(ListUser),
new(NamespaceUser),
new(ListTaskAssginee),
)
tablesWithPointer = append(tables,
@ -81,6 +82,7 @@ func init() {
&Namespace{},
&ListUser{},
&NamespaceUser{},
&ListTaskAssginee{},
)
}