
As I mentioned [here](https://kolaente.dev/vikunja/api/pulls/1442#issuecomment-55215), this is mainly a cleanup of @zewaren 's original [PR](https://kolaente.dev/vikunja/api/pulls/1442). It adds support for the `RELATED-TO` property in CalDAV's `VTODO` and the `RELTYPE=PARENT` and `RELTYPE=CHILD` relationships. In other words, it allows for `ParentTask->SubTask` relations to be handled supported through CalDAV. In addition to the included tests, this has been tested by both @zewaren & myself with DAVx5 & Tasks (Android) and it's been working great. Resolves https://kolaente.dev/vikunja/api/issues/1345 Co-authored-by: Miguel A. Arroyo <miguel@codeheads.dev> Co-authored-by: Erwan Martin <public@fzwte.net> Reviewed-on: https://kolaente.dev/vikunja/api/pulls/1634 Reviewed-by: konrad <k@knt.li> Co-authored-by: Miguel Arroyo <mayanez@noreply.kolaente.de> Co-committed-by: Miguel Arroyo <mayanez@noreply.kolaente.de>
667 lines
18 KiB
Go
667 lines
18 KiB
Go
// Vikunja is a to-do list application to facilitate your life.
|
|
// Copyright 2018-present 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 Affero General Public Licensee 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 Affero General Public Licensee for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public Licensee
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
package caldav
|
|
|
|
import (
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"code.vikunja.io/api/pkg/caldav"
|
|
"code.vikunja.io/api/pkg/db"
|
|
"code.vikunja.io/api/pkg/log"
|
|
"code.vikunja.io/api/pkg/models"
|
|
user2 "code.vikunja.io/api/pkg/user"
|
|
"code.vikunja.io/web"
|
|
"github.com/samedi/caldav-go/data"
|
|
"github.com/samedi/caldav-go/errs"
|
|
"xorm.io/xorm"
|
|
)
|
|
|
|
// DavBasePath is the base url path
|
|
const DavBasePath = `/dav/`
|
|
|
|
// ProjectBasePath is the base path for all projects resources
|
|
const ProjectBasePath = DavBasePath + `projects`
|
|
|
|
// VikunjaCaldavProjectStorage represents a project storage
|
|
type VikunjaCaldavProjectStorage struct {
|
|
// Used when handling a project
|
|
project *models.ProjectWithTasksAndBuckets
|
|
// Used when handling a single task, like updating
|
|
task *models.Task
|
|
// The current user
|
|
user *user2.User
|
|
isPrincipal bool
|
|
isEntry bool // Entry level handling should only return a link to the principal url
|
|
}
|
|
|
|
// GetResources returns either all projects, links to the principal, or only one project, depending on the request
|
|
func (vcls *VikunjaCaldavProjectStorage) GetResources(rpath string, _ bool) ([]data.Resource, error) {
|
|
|
|
// It looks like we need to have the same handler for returning both the calendar home set and the user principal
|
|
// Since the client seems to ignore the whatever is being returned in the first request and just makes a second one
|
|
// to the same url but requesting the calendar home instead
|
|
// The problem with this is caldav-go just return whatever ressource it gets and making that the requested path
|
|
// And for us here, there is no easy (I can think of at least one hacky way) to figure out if the client is requesting
|
|
// the home or principal url. Ough.
|
|
|
|
// Ok, maybe the problem is more the client making a request to /dav/ and getting a response which says
|
|
// something like "hey, for /dav/projects, the calendar home is /dav/projects", but the client expects a
|
|
// response to go something like "hey, for /dav/, the calendar home is /dav/projects" since it requested /dav/
|
|
// and not /dav/projects. I'm not sure if thats a bug in the client or in caldav-go.
|
|
|
|
if vcls.isEntry {
|
|
r := data.NewResource(rpath, &VikunjaProjectResourceAdapter{
|
|
isPrincipal: true,
|
|
isCollection: true,
|
|
})
|
|
return []data.Resource{r}, nil
|
|
}
|
|
|
|
// If the request wants the principal url, we'll return that and nothing else
|
|
if vcls.isPrincipal {
|
|
r := data.NewResource(DavBasePath+`/projects/`, &VikunjaProjectResourceAdapter{
|
|
isPrincipal: true,
|
|
isCollection: true,
|
|
})
|
|
return []data.Resource{r}, nil
|
|
}
|
|
|
|
// If vcls.project.ID is != 0, this means the user is doing a PROPFIND request to /projects/:project
|
|
// Which means we need to get only one project
|
|
if vcls.project != nil && vcls.project.ID != 0 {
|
|
rr, err := vcls.getProjectRessource(true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
r := data.NewResource(rpath, &rr)
|
|
r.Name = vcls.project.Title
|
|
return []data.Resource{r}, nil
|
|
}
|
|
|
|
s := db.NewSession()
|
|
defer s.Close()
|
|
|
|
// Otherwise get all projects
|
|
theprojects, _, _, err := vcls.project.ReadAll(s, vcls.user, "", -1, 50)
|
|
if err != nil {
|
|
_ = s.Rollback()
|
|
return nil, err
|
|
}
|
|
if err := s.Commit(); err != nil {
|
|
return nil, err
|
|
}
|
|
projects := theprojects.([]*models.Project)
|
|
|
|
var resources []data.Resource
|
|
for _, l := range projects {
|
|
rr := VikunjaProjectResourceAdapter{
|
|
project: &models.ProjectWithTasksAndBuckets{
|
|
Project: *l,
|
|
},
|
|
isCollection: true,
|
|
}
|
|
r := data.NewResource(ProjectBasePath+"/"+strconv.FormatInt(l.ID, 10), &rr)
|
|
r.Name = l.Title
|
|
resources = append(resources, r)
|
|
}
|
|
|
|
return resources, nil
|
|
}
|
|
|
|
// GetResourcesByList fetches a project of resources from a slice of paths
|
|
func (vcls *VikunjaCaldavProjectStorage) GetResourcesByList(rpaths []string) ([]data.Resource, error) {
|
|
|
|
// Parse the set of resourcepaths into usable uids
|
|
// A path looks like this: /dav/projects/10/a6eb526d5748a5c499da202fe74f36ed1aea2aef.ics
|
|
// So we split the url in parts, take the last one and strip the ".ics" at the end
|
|
var uids []string
|
|
for _, path := range rpaths {
|
|
parts := strings.Split(path, "/")
|
|
uids = append(uids, strings.TrimSuffix(parts[4], ".ics"))
|
|
}
|
|
|
|
s := db.NewSession()
|
|
defer s.Close()
|
|
|
|
// GetTasksByUIDs...
|
|
// Parse these into ressources...
|
|
tasks, err := models.GetTasksByUIDs(s, uids, vcls.user)
|
|
if err != nil {
|
|
_ = s.Rollback()
|
|
return nil, err
|
|
}
|
|
if err := s.Commit(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var resources []data.Resource
|
|
for _, t := range tasks {
|
|
rr := VikunjaProjectResourceAdapter{
|
|
task: t,
|
|
}
|
|
r := data.NewResource(getTaskURL(t), &rr)
|
|
r.Name = t.Title
|
|
resources = append(resources, r)
|
|
}
|
|
|
|
return resources, nil
|
|
}
|
|
|
|
// GetResourcesByFilters fetches a project of resources with a filter
|
|
func (vcls *VikunjaCaldavProjectStorage) GetResourcesByFilters(rpath string, _ *data.ResourceFilter) ([]data.Resource, error) {
|
|
|
|
// If we already have a project saved, that means the user is making a REPORT request to find out if
|
|
// anything changed, in that case we need to return all tasks.
|
|
// That project is coming from a previous "getProjectRessource" in L177
|
|
if vcls.project.Tasks != nil {
|
|
var resources []data.Resource
|
|
for i := range vcls.project.Tasks {
|
|
rr := VikunjaProjectResourceAdapter{
|
|
project: vcls.project,
|
|
task: &vcls.project.Tasks[i].Task,
|
|
isCollection: false,
|
|
}
|
|
r := data.NewResource(getTaskURL(&vcls.project.Tasks[i].Task), &rr)
|
|
r.Name = vcls.project.Tasks[i].Title
|
|
resources = append(resources, r)
|
|
}
|
|
return resources, nil
|
|
}
|
|
|
|
// This is used to get all
|
|
rr, err := vcls.getProjectRessource(false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
r := data.NewResource(rpath, &rr)
|
|
r.Name = vcls.project.Title
|
|
return []data.Resource{r}, nil
|
|
// For now, filtering is disabled.
|
|
// return vcls.GetResources(rpath, false)
|
|
}
|
|
|
|
func getTaskURL(task *models.Task) string {
|
|
return ProjectBasePath + "/" + strconv.FormatInt(task.ProjectID, 10) + `/` + task.UID + `.ics`
|
|
}
|
|
|
|
// GetResource fetches a single resource
|
|
func (vcls *VikunjaCaldavProjectStorage) GetResource(rpath string) (*data.Resource, bool, error) {
|
|
|
|
// If the task is not nil, we need to get the task and not the project
|
|
if vcls.task != nil {
|
|
s := db.NewSession()
|
|
defer s.Close()
|
|
|
|
// save and override the updated unix date to not break any later etag checks
|
|
updated := vcls.task.Updated
|
|
tasks, err := models.GetTasksByUIDs(s, []string{vcls.task.UID}, vcls.user)
|
|
if err != nil {
|
|
_ = s.Rollback()
|
|
if models.IsErrTaskDoesNotExist(err) {
|
|
return nil, false, errs.ResourceNotFoundError
|
|
}
|
|
return nil, false, err
|
|
}
|
|
if err := s.Commit(); err != nil {
|
|
return nil, false, err
|
|
}
|
|
|
|
if len(tasks) < 1 {
|
|
return nil, false, errs.ResourceNotFoundError
|
|
}
|
|
vcls.task = tasks[0]
|
|
|
|
if updated.Unix() > 0 {
|
|
vcls.task.Updated = updated
|
|
}
|
|
|
|
rr := VikunjaProjectResourceAdapter{
|
|
project: vcls.project,
|
|
task: vcls.task,
|
|
}
|
|
r := data.NewResource(rpath, &rr)
|
|
return &r, true, nil
|
|
}
|
|
|
|
// Otherwise get the project with all tasks
|
|
rr, err := vcls.getProjectRessource(true)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
r := data.NewResource(rpath, &rr)
|
|
return &r, true, nil
|
|
}
|
|
|
|
// GetShallowResource gets a ressource without childs
|
|
// Since Vikunja has no children, this is the same as GetResource
|
|
func (vcls *VikunjaCaldavProjectStorage) GetShallowResource(rpath string) (*data.Resource, bool, error) {
|
|
// Since Vikunja has no childs, this just returns the same as GetResource()
|
|
// FIXME: This should just get the project with no tasks whatsoever, nothing else
|
|
return vcls.GetResource(rpath)
|
|
}
|
|
|
|
// CreateResource creates a new resource
|
|
func (vcls *VikunjaCaldavProjectStorage) CreateResource(rpath, content string) (*data.Resource, error) {
|
|
|
|
s := db.NewSession()
|
|
defer s.Close()
|
|
|
|
vTask, err := caldav.ParseTaskFromVTODO(content)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
vTask.ProjectID = vcls.project.ID
|
|
|
|
// Check the rights
|
|
canCreate, err := vTask.CanCreate(s, vcls.user)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !canCreate {
|
|
return nil, errs.ForbiddenError
|
|
}
|
|
|
|
// Create the task
|
|
err = vTask.Create(s, vcls.user)
|
|
if err != nil {
|
|
_ = s.Rollback()
|
|
return nil, err
|
|
}
|
|
|
|
vcls.task.ID = vTask.ID
|
|
err = persistLabels(s, vcls.user, vcls.task, vTask.Labels)
|
|
if err != nil {
|
|
_ = s.Rollback()
|
|
return nil, err
|
|
}
|
|
|
|
vcls.task.ProjectID = vcls.project.ID
|
|
err = persistRelations(s, vcls.user, vcls.task, vTask.RelatedTasks)
|
|
if err != nil {
|
|
_ = s.Rollback()
|
|
return nil, err
|
|
}
|
|
|
|
if err := s.Commit(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Build up the proper response
|
|
rr := VikunjaProjectResourceAdapter{
|
|
project: vcls.project,
|
|
task: vTask,
|
|
}
|
|
r := data.NewResource(rpath, &rr)
|
|
return &r, nil
|
|
}
|
|
|
|
// UpdateResource updates a resource
|
|
func (vcls *VikunjaCaldavProjectStorage) UpdateResource(rpath, content string) (*data.Resource, error) {
|
|
|
|
vTask, err := caldav.ParseTaskFromVTODO(content)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// At this point, we already have the right task in vcls.task, so we can use that ID directly
|
|
vTask.ID = vcls.task.ID
|
|
|
|
// Explicitly set the ProjectID in case the task now belongs to a different project:
|
|
vTask.ProjectID = vcls.project.ID
|
|
vcls.task.ProjectID = vcls.project.ID
|
|
|
|
s := db.NewSession()
|
|
defer s.Close()
|
|
|
|
// Check the rights
|
|
canUpdate, err := vTask.CanUpdate(s, vcls.user)
|
|
if err != nil {
|
|
_ = s.Rollback()
|
|
return nil, err
|
|
}
|
|
if !canUpdate {
|
|
_ = s.Rollback()
|
|
return nil, errs.ForbiddenError
|
|
}
|
|
|
|
// Update the task
|
|
err = vTask.Update(s, vcls.user)
|
|
if err != nil {
|
|
_ = s.Rollback()
|
|
return nil, err
|
|
}
|
|
|
|
err = persistLabels(s, vcls.user, vcls.task, vTask.Labels)
|
|
if err != nil {
|
|
_ = s.Rollback()
|
|
return nil, err
|
|
}
|
|
|
|
err = persistRelations(s, vcls.user, vcls.task, vTask.RelatedTasks)
|
|
if err != nil {
|
|
_ = s.Rollback()
|
|
return nil, err
|
|
}
|
|
|
|
if err := s.Commit(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Build up the proper response
|
|
rr := VikunjaProjectResourceAdapter{
|
|
project: vcls.project,
|
|
task: vTask,
|
|
}
|
|
r := data.NewResource(rpath, &rr)
|
|
return &r, nil
|
|
}
|
|
|
|
// DeleteResource deletes a resource
|
|
func (vcls *VikunjaCaldavProjectStorage) DeleteResource(_ string) error {
|
|
if vcls.task != nil {
|
|
s := db.NewSession()
|
|
defer s.Close()
|
|
|
|
// Check the rights
|
|
canDelete, err := vcls.task.CanDelete(s, vcls.user)
|
|
if err != nil {
|
|
_ = s.Rollback()
|
|
return err
|
|
}
|
|
if !canDelete {
|
|
return errs.ForbiddenError
|
|
}
|
|
|
|
// Delete it
|
|
err = vcls.task.Delete(s, vcls.user)
|
|
if err != nil {
|
|
_ = s.Rollback()
|
|
return err
|
|
}
|
|
|
|
return s.Commit()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func persistLabels(s *xorm.Session, a web.Auth, task *models.Task, labels []*models.Label) (err error) {
|
|
|
|
labelTitles := []string{}
|
|
|
|
for _, label := range labels {
|
|
labelTitles = append(labelTitles, label.Title)
|
|
}
|
|
|
|
u := &user2.User{
|
|
ID: a.GetID(),
|
|
}
|
|
|
|
// Using readall ensures the current user has the permission to see the labels they provided via caldav.
|
|
existingLabels, _, _, err := models.GetLabelsByTaskIDs(s, &models.LabelByTaskIDsOptions{
|
|
Search: labelTitles,
|
|
User: u,
|
|
GetForUser: u.ID,
|
|
GetUnusedLabels: true,
|
|
GroupByLabelIDsOnly: true,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
labelMap := make(map[string]*models.Label)
|
|
for i := range existingLabels {
|
|
labelMap[existingLabels[i].Title] = &existingLabels[i].Label
|
|
}
|
|
|
|
for _, label := range labels {
|
|
if l, has := labelMap[label.Title]; has {
|
|
*label = *l
|
|
continue
|
|
}
|
|
|
|
err = label.Create(s, a)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Create the label <-> task relation
|
|
return task.UpdateTaskLabels(s, a, labels)
|
|
}
|
|
|
|
func removeStaleRelations(s *xorm.Session, a web.Auth, task *models.Task, newRelations map[models.RelationKind][]*models.Task) (err error) {
|
|
|
|
// Get the existing task with details:
|
|
existingTask := &models.Task{ID: task.ID}
|
|
// FIXME: Optimize to get only required attributes (ie. RelatedTasks).
|
|
err = existingTask.ReadOne(s, a)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
for relationKind, relatedTasks := range existingTask.RelatedTasks {
|
|
|
|
for _, relatedTask := range relatedTasks {
|
|
relationInNewList := slices.ContainsFunc(newRelations[relationKind], func(newRelation *models.Task) bool { return newRelation.UID == relatedTask.UID })
|
|
|
|
if !relationInNewList {
|
|
rel := models.TaskRelation{
|
|
TaskID: task.ID,
|
|
OtherTaskID: relatedTask.ID,
|
|
RelationKind: relationKind,
|
|
}
|
|
err = rel.Delete(s, a)
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// Persist new relations provided by the VTODO entry:
|
|
func persistRelations(s *xorm.Session, a web.Auth, task *models.Task, newRelations map[models.RelationKind][]*models.Task) (err error) {
|
|
|
|
err = removeStaleRelations(s, a, task, newRelations)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Ensure the current relations exist:
|
|
for relationType, relatedTasksInVTODO := range newRelations {
|
|
// Persist each relation independently:
|
|
for _, relatedTaskInVTODO := range relatedTasksInVTODO {
|
|
|
|
var relatedTask *models.Task
|
|
createDummy := false
|
|
|
|
// Get the task from the DB:
|
|
relatedTaskInDB, err := models.GetTaskSimpleByUUID(s, relatedTaskInVTODO.UID)
|
|
if err != nil {
|
|
relatedTask = relatedTaskInVTODO
|
|
createDummy = true
|
|
} else {
|
|
relatedTask = relatedTaskInDB
|
|
}
|
|
|
|
// If the related task doesn't exist, create a dummy one now in the same list.
|
|
// It'll probably be populated right after in a following request.
|
|
// In the worst case, this was an error by the client and we are left with
|
|
// this dummy task to clean up.
|
|
if createDummy {
|
|
relatedTask.ProjectID = task.ProjectID
|
|
relatedTask.Title = "DUMMY-UID-" + relatedTask.UID
|
|
err = relatedTask.Create(s, a)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Create the relation:
|
|
rel := models.TaskRelation{
|
|
TaskID: task.ID,
|
|
OtherTaskID: relatedTask.ID,
|
|
RelationKind: relationType,
|
|
}
|
|
err = rel.Create(s, a)
|
|
if err != nil && !models.IsErrRelationAlreadyExists(err) {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
|
|
// VikunjaProjectResourceAdapter holds the actual resource
|
|
type VikunjaProjectResourceAdapter struct {
|
|
project *models.ProjectWithTasksAndBuckets
|
|
projectTasks []*models.TaskWithComments
|
|
task *models.Task
|
|
|
|
isPrincipal bool
|
|
isCollection bool
|
|
}
|
|
|
|
// IsCollection checks if the resoure in the adapter is a collection
|
|
func (vlra *VikunjaProjectResourceAdapter) IsCollection() bool {
|
|
// If the discovery does not work, setting this to true makes it work again.
|
|
return vlra.isCollection
|
|
}
|
|
|
|
// CalculateEtag returns the etag of a resource
|
|
func (vlra *VikunjaProjectResourceAdapter) CalculateEtag() string {
|
|
|
|
// If we're updating a task, the client sends the etag of the project instead of the one from the task.
|
|
// And therefore, updating the task fails since these etags don't match.
|
|
// To fix that, we use this extra field to determine if we're currently updating a task and return the
|
|
// etag of the project instead.
|
|
// if vlra.project != nil {
|
|
// return `"` + strconv.FormatInt(vlra.project.ID, 10) + `-` + strconv.FormatInt(vlra.project.Updated, 10) + `"`
|
|
// }
|
|
|
|
// Return the etag of a task if we have one
|
|
if vlra.task != nil {
|
|
return `"` + strconv.FormatInt(vlra.task.ID, 10) + `-` + strconv.FormatInt(vlra.task.Updated.Unix(), 10) + `"`
|
|
}
|
|
|
|
if vlra.project == nil {
|
|
return ""
|
|
}
|
|
|
|
// This also returns the etag of the project, and not of the task,
|
|
// which becomes problematic because the client uses this etag (= the one from the project) to make
|
|
// Requests to update a task. These do not match and thus updating a task fails.
|
|
return `"` + strconv.FormatInt(vlra.project.ID, 10) + `-` + strconv.FormatInt(vlra.project.Updated.Unix(), 10) + `"`
|
|
}
|
|
|
|
// GetContent returns the content string of a resource (a task in our case)
|
|
func (vlra *VikunjaProjectResourceAdapter) GetContent() string {
|
|
if vlra.project != nil && vlra.project.Tasks != nil {
|
|
return caldav.GetCaldavTodosForTasks(vlra.project, vlra.projectTasks)
|
|
}
|
|
|
|
if vlra.task != nil {
|
|
project := models.ProjectWithTasksAndBuckets{Tasks: []*models.TaskWithComments{{Task: *vlra.task}}}
|
|
return caldav.GetCaldavTodosForTasks(&project, project.Tasks)
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
// GetContentSize is the size of a caldav content
|
|
func (vlra *VikunjaProjectResourceAdapter) GetContentSize() int64 {
|
|
return int64(len(vlra.GetContent()))
|
|
}
|
|
|
|
// GetModTime returns when the resource was last modified
|
|
func (vlra *VikunjaProjectResourceAdapter) GetModTime() time.Time {
|
|
if vlra.task != nil {
|
|
return vlra.task.Updated
|
|
}
|
|
|
|
if vlra.project != nil {
|
|
return vlra.project.Updated
|
|
}
|
|
|
|
return time.Time{}
|
|
}
|
|
|
|
func (vcls *VikunjaCaldavProjectStorage) getProjectRessource(isCollection bool) (rr VikunjaProjectResourceAdapter, err error) {
|
|
s := db.NewSession()
|
|
defer s.Close()
|
|
|
|
if vcls.project == nil {
|
|
return
|
|
}
|
|
|
|
can, _, err := vcls.project.CanRead(s, vcls.user)
|
|
if err != nil {
|
|
_ = s.Rollback()
|
|
return
|
|
}
|
|
if !can {
|
|
_ = s.Rollback()
|
|
log.Errorf("User %v tried to access a caldav resource (Project %v) which they are not allowed to access", vcls.user.Username, vcls.project.ID)
|
|
return rr, models.ErrUserDoesNotHaveAccessToProject{ProjectID: vcls.project.ID}
|
|
}
|
|
err = vcls.project.ReadOne(s, vcls.user)
|
|
if err != nil {
|
|
_ = s.Rollback()
|
|
return
|
|
}
|
|
|
|
projectTasks := vcls.project.Tasks
|
|
if projectTasks == nil {
|
|
tk := models.TaskCollection{
|
|
ProjectID: vcls.project.ID,
|
|
}
|
|
iface, _, _, err := tk.ReadAll(s, vcls.user, "", 1, 1000)
|
|
if err != nil {
|
|
_ = s.Rollback()
|
|
return rr, err
|
|
}
|
|
tasks, ok := iface.([]*models.Task)
|
|
if !ok {
|
|
panic("Tasks returned from TaskCollection.ReadAll are not []*models.Task!")
|
|
}
|
|
|
|
for _, t := range tasks {
|
|
projectTasks = append(projectTasks, &models.TaskWithComments{Task: *t})
|
|
}
|
|
vcls.project.Tasks = projectTasks
|
|
}
|
|
|
|
if err := s.Commit(); err != nil {
|
|
return rr, err
|
|
}
|
|
|
|
rr = VikunjaProjectResourceAdapter{
|
|
project: vcls.project,
|
|
projectTasks: projectTasks,
|
|
isCollection: isCollection,
|
|
}
|
|
|
|
return
|
|
}
|