feat(caldav): Add support for subtasks (i.e. RELATED-TO
property) in CalDAV (#1634)
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>
This commit is contained in:
@ -17,6 +17,7 @@
|
||||
package caldav
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@ -292,6 +293,13 @@ func (vcls *VikunjaCaldavProjectStorage) CreateResource(rpath, content string) (
|
||||
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
|
||||
}
|
||||
@ -316,6 +324,10 @@ func (vcls *VikunjaCaldavProjectStorage) UpdateResource(rpath, content string) (
|
||||
// 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()
|
||||
|
||||
@ -343,6 +355,12 @@ func (vcls *VikunjaCaldavProjectStorage) UpdateResource(rpath, content string) (
|
||||
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
|
||||
}
|
||||
@ -430,6 +448,91 @@ func persistLabels(s *xorm.Session, a web.Auth, task *models.Task, labels []*mod
|
||||
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
|
||||
|
Reference in New Issue
Block a user