1
0

Task Comments (#138)

Add swagger docs

Add integration tests

Add tests

Add task comment test fixtures

Add config option to enable/disable task comments

Add custom error if a task comment does not exist

Fix lint

Add getting author when getting a single comment

Fix getting comments/comments author

Add rights check to ReadAll

+ actually get the comment author

Add migration and table definitions

Add routes

Add ReadOne method

Add basic crud rights

Signed-off-by: kolaente <k@knt.li>

Implement basic crudable functions for task comments

Signed-off-by: kolaente <k@knt.li>

Start adding task comments

Signed-off-by: kolaente <k@knt.li>
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: https://kolaente.dev/vikunja/api/pulls/138
This commit is contained in:
konrad
2020-02-19 21:57:56 +00:00
parent 5901cf64b4
commit 1f039c4cda
16 changed files with 859 additions and 2 deletions

View File

@ -276,7 +276,7 @@ const ErrCodeTaskDoesNotExist = 4002
// HTTPError holds the http error description
func (err ErrTaskDoesNotExist) HTTPError() web.HTTPError {
return web.HTTPError{HTTPCode: http.StatusNotFound, Code: ErrCodeTaskDoesNotExist, Message: "This list task does not exist"}
return web.HTTPError{HTTPCode: http.StatusNotFound, Code: ErrCodeTaskDoesNotExist, Message: "This task does not exist"}
}
// ErrBulkTasksMustBeInSameList represents a "ErrBulkTasksMustBeInSameList" kind of error.
@ -602,6 +602,34 @@ func (err ErrInvalidSortOrder) HTTPError() web.HTTPError {
}
}
// ErrTaskCommentDoesNotExist represents an error where a task comment does not exist
type ErrTaskCommentDoesNotExist struct {
ID int64
TaskID int64
}
// IsErrTaskCommentDoesNotExist checks if an error is ErrTaskCommentDoesNotExist.
func IsErrTaskCommentDoesNotExist(err error) bool {
_, ok := err.(ErrTaskCommentDoesNotExist)
return ok
}
func (err ErrTaskCommentDoesNotExist) Error() string {
return fmt.Sprintf("Task comment does not exist [ID: %d, TaskID: %d]", err.ID, err.TaskID)
}
// ErrCodeTaskCommentDoesNotExist holds the unique world-error code of this error
const ErrCodeTaskCommentDoesNotExist = 4015
// HTTPError holds the http error description
func (err ErrTaskCommentDoesNotExist) HTTPError() web.HTTPError {
return web.HTTPError{
HTTPCode: http.StatusNotFound,
Code: ErrCodeTaskCommentDoesNotExist,
Message: "This task comment does not exist",
}
}
// =================
// Namespace errors
// =================

View File

@ -50,6 +50,7 @@ func GetTables() []interface{} {
&LinkSharing{},
&TaskRelation{},
&TaskAttachment{},
&TaskComment{},
}
}

View File

@ -0,0 +1,44 @@
// Copyright 2020 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 <https://www.gnu.org/licenses/>.
package models
import "code.vikunja.io/web"
// CanRead checks if a user can read a comment
func (tc *TaskComment) CanRead(a web.Auth) (bool, error) {
t := Task{ID: tc.TaskID}
return t.CanRead(a)
}
// CanDelete checks if a user can delete a comment
func (tc *TaskComment) CanDelete(a web.Auth) (bool, error) {
t := Task{ID: tc.TaskID}
return t.CanWrite(a)
}
// CanUpdate checks if a user can update a comment
func (tc *TaskComment) CanUpdate(a web.Auth) (bool, error) {
t := Task{ID: tc.TaskID}
return t.CanWrite(a)
}
// CanCreate checks if a user can create a new comment
func (tc *TaskComment) CanCreate(a web.Auth) (bool, error) {
t := Task{ID: tc.TaskID}
return t.CanWrite(a)
}

209
pkg/models/task_comments.go Normal file
View File

@ -0,0 +1,209 @@
// Copyright 2020 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 <https://www.gnu.org/licenses/>.
package models
import (
"code.vikunja.io/api/pkg/timeutil"
"code.vikunja.io/api/pkg/user"
"code.vikunja.io/web"
)
// TaskComment represents a task comment
type TaskComment struct {
ID int64 `xorm:"autoincr pk unique not null" json:"id" param:"commentid"`
Comment string `xorm:"text not null" json:"comment"`
AuthorID int64 `xorm:"not null" json:"-"`
Author *user.User `xorm:"-" json:"author"`
TaskID int64 `xorm:"not null" json:"-" param:"task"`
Created timeutil.TimeStamp `xorm:"created" json:"created"`
Updated timeutil.TimeStamp `xorm:"updated" json:"updated"`
web.CRUDable `xorm:"-" json:"-"`
web.Rights `xorm:"-" json:"-"`
}
// TableName holds the table name for the task comments table
func (tc *TaskComment) TableName() string {
return "task_comments"
}
// Create creates a new task comment
// @Summary Create a new task comment
// @Description Create a new task comment. The user doing this need to have at least write access to the task this comment should belong to.
// @tags task
// @Accept json
// @Produce json
// @Security JWTKeyAuth
// @Param relation body models.TaskComment true "The task comment object"
// @Param taskID path int true "Task ID"
// @Success 200 {object} models.TaskComment "The created task comment object."
// @Failure 400 {object} code.vikunja.io/web.HTTPError "Invalid task comment object provided."
// @Failure 500 {object} models.Message "Internal error"
// @Router /tasks/{taskID}/comments [put]
func (tc *TaskComment) Create(a web.Auth) (err error) {
// Check if the task exists
_, err = GetTaskSimple(&Task{ID: tc.TaskID})
if err != nil {
return err
}
tc.AuthorID = a.GetID()
_, err = x.Insert(tc)
return
}
// Delete removes a task comment
// @Summary Remove a task comment
// @Description Remove a task comment. The user doing this need to have at least write access to the task this comment belongs to.
// @tags task
// @Accept json
// @Produce json
// @Security JWTKeyAuth
// @Param taskID path int true "Task ID"
// @Param commentID path int true "Comment ID"
// @Success 200 {object} models.Message "The task comment was successfully deleted."
// @Failure 400 {object} code.vikunja.io/web.HTTPError "Invalid task comment object provided."
// @Failure 404 {object} code.vikunja.io/web.HTTPError "The task comment was not found."
// @Failure 500 {object} models.Message "Internal error"
// @Router /tasks/{taskID}/comments/{commentID} [delete]
func (tc *TaskComment) Delete() error {
deleted, err := x.ID(tc.ID).NoAutoCondition().Delete(tc)
if deleted == 0 {
return ErrTaskCommentDoesNotExist{ID: tc.ID}
}
return err
}
// Update updates a task text by its ID
// @Summary Update an existing task comment
// @Description Update an existing task comment. The user doing this need to have at least write access to the task this comment belongs to.
// @tags task
// @Accept json
// @Produce json
// @Security JWTKeyAuth
// @Param taskID path int true "Task ID"
// @Param commentID path int true "Comment ID"
// @Success 200 {object} models.TaskComment "The updated task comment object."
// @Failure 400 {object} code.vikunja.io/web.HTTPError "Invalid task comment object provided."
// @Failure 404 {object} code.vikunja.io/web.HTTPError "The task comment was not found."
// @Failure 500 {object} models.Message "Internal error"
// @Router /tasks/{taskID}/comments/{commentID} [post]
func (tc *TaskComment) Update() error {
updated, err := x.ID(tc.ID).Cols("comment").Update(tc)
if updated == 0 {
return ErrTaskCommentDoesNotExist{ID: tc.ID}
}
return err
}
// ReadOne handles getting a single comment
// @Summary Remove a task comment
// @Description Remove a task comment. The user doing this need to have at least read access to the task this comment belongs to.
// @tags task
// @Accept json
// @Produce json
// @Security JWTKeyAuth
// @Param taskID path int true "Task ID"
// @Param commentID path int true "Comment ID"
// @Success 200 {object} models.TaskComment "The task comment object."
// @Failure 400 {object} code.vikunja.io/web.HTTPError "Invalid task comment object provided."
// @Failure 404 {object} code.vikunja.io/web.HTTPError "The task comment was not found."
// @Failure 500 {object} models.Message "Internal error"
// @Router /tasks/{taskID}/comments/{commentID} [get]
func (tc *TaskComment) ReadOne() (err error) {
exists, err := x.Get(tc)
if err != nil {
return
}
if !exists {
return ErrTaskCommentDoesNotExist{
ID: tc.ID,
TaskID: tc.TaskID,
}
}
// Get the author
author := &user.User{}
_, err = x.
Where("id = ?", tc.AuthorID).
Get(author)
tc.Author = author
return
}
// ReadAll returns all comments for a task
// @Summary Get all task comments
// @Description Get all task comments. The user doing this need to have at least read access to the task.
// @tags task
// @Accept json
// @Produce json
// @Security JWTKeyAuth
// @Param taskID path int true "Task ID"
// @Success 200 {array} models.TaskComment "The array with all task comments"
// @Failure 500 {object} models.Message "Internal error"
// @Router /tasks/{taskID}/comments [get]
func (tc *TaskComment) ReadAll(auth web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, numberOfTotalItems int64, err error) {
// Check if the user has access to the task
canRead, err := tc.CanRead(auth)
if err != nil {
return nil, 0, 0, err
}
if !canRead {
return nil, 0, 0, ErrGenericForbidden{}
}
// Because we can't extend the type in general, we need to do this here.
// Not a good solution, but saves performance.
type TaskCommentWithAuthor struct {
TaskComment
AuthorFromDB *user.User `xorm:"extends" json:"-"`
}
comments := []*TaskComment{}
err = x.
Where("task_id = ? AND comment like ?", tc.TaskID, "%"+search+"%").
Join("LEFT", "users", "users.id = task_comments.author_id").
Limit(getLimitFromPageIndex(page, perPage)).
Find(&comments)
if err != nil {
return
}
// Get all authors
authors := make(map[int64]*user.User)
err = x.
Select("users.*").
Table("task_comments").
Where("task_id = ? AND comment like ?", tc.TaskID, "%"+search+"%").
Join("INNER", "users", "users.id = task_comments.author_id").
Find(&authors)
if err != nil {
return
}
for _, comment := range comments {
comment.Author = authors[comment.AuthorID]
}
numberOfTotalItems, err = x.
Where("task_id = ? AND comment like ?", tc.TaskID, "%"+search+"%").
Count(&TaskCommentWithAuthor{})
return comments, len(comments), numberOfTotalItems, err
}

View File

@ -0,0 +1,126 @@
// Vikunja is a todo-list application to facilitate your life.
// Copyright 2018-2020 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/db"
"code.vikunja.io/api/pkg/user"
"github.com/stretchr/testify/assert"
"testing"
)
func TestTaskComment_Create(t *testing.T) {
u := &user.User{ID: 1}
t.Run("normal", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
tc := &TaskComment{
Comment: "test",
TaskID: 1,
}
err := tc.Create(u)
assert.NoError(t, err)
})
t.Run("nonexisting task", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
tc := &TaskComment{
Comment: "test",
TaskID: 99999,
}
err := tc.Create(u)
assert.Error(t, err)
assert.True(t, IsErrTaskDoesNotExist(err))
})
}
func TestTaskComment_Delete(t *testing.T) {
t.Run("normal", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
tc := &TaskComment{ID: 1}
err := tc.Delete()
assert.NoError(t, err)
})
t.Run("nonexisting comment", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
tc := &TaskComment{ID: 9999}
err := tc.Delete()
assert.Error(t, err)
assert.True(t, IsErrTaskCommentDoesNotExist(err))
})
}
func TestTaskComment_Update(t *testing.T) {
t.Run("normal", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
tc := &TaskComment{
ID: 1,
Comment: "testing",
}
err := tc.Update()
assert.NoError(t, err)
})
t.Run("nonexisting comment", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
tc := &TaskComment{
ID: 9999,
}
err := tc.Update()
assert.Error(t, err)
assert.True(t, IsErrTaskCommentDoesNotExist(err))
})
}
func TestTaskComment_ReadOne(t *testing.T) {
t.Run("normal", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
tc := &TaskComment{ID: 1}
err := tc.ReadOne()
assert.NoError(t, err)
assert.Equal(t, "Lorem Ipsum Dolor Sit Amet", tc.Comment)
assert.NotEmpty(t, tc.Author.ID)
})
t.Run("nonexisting", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
tc := &TaskComment{ID: 9999}
err := tc.ReadOne()
assert.Error(t, err)
assert.True(t, IsErrTaskCommentDoesNotExist(err))
})
}
func TestTaskComment_ReadAll(t *testing.T) {
t.Run("normal", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
tc := &TaskComment{TaskID: 1}
u := &user.User{ID: 1}
result, resultCount, total, err := tc.ReadAll(u, "", 0, -1)
resultComment := result.([]*TaskComment)
assert.NoError(t, err)
assert.Equal(t, 1, resultCount)
assert.Equal(t, int64(1), total)
assert.Equal(t, int64(1), resultComment[0].ID)
assert.Equal(t, "Lorem Ipsum Dolor Sit Amet", resultComment[0].Comment)
assert.NotEmpty(t, resultComment[0].Author.ID)
})
t.Run("no access to task", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
tc := &TaskComment{TaskID: 14}
u := &user.User{ID: 1}
_, _, _, err := tc.ReadAll(u, "", 0, -1)
assert.Error(t, err)
assert.True(t, IsErrGenericForbidden(err))
})
}

View File

@ -64,7 +64,7 @@ func (t *Task) canDoTask(a web.Auth) (bool, error) {
return false, err
}
// A user can do a task if he has write acces to its list
// A user can do a task if it has write acces to its list
l := &List{ID: lI.ListID}
return l.CanWrite(a)
}

View File

@ -46,6 +46,7 @@ func SetupTests() {
"namespaces",
"task_assignees",
"task_attachments",
"task_comments",
"task_relations",
"task_reminders",
"tasks",