feat(caldav): Add support for subtasks (i.e. RELATED-TO
property) in CalDAV (#1634)
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
As I mentioned [here](vikunja/api#1442 (comment)), this is mainly a cleanup of @zewaren 's original [PR](vikunja/api#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 vikunja/api#1345 Co-authored-by: Miguel A. Arroyo <miguel@codeheads.dev> Co-authored-by: Erwan Martin <public@fzwte.net> Reviewed-on: vikunja/api#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:
parent
6169c2e12e
commit
225d65268d
@ -38,21 +38,21 @@ type Todo struct {
|
||||
UID string
|
||||
|
||||
// Optional
|
||||
Summary string
|
||||
Description string
|
||||
Completed time.Time
|
||||
Organizer *user.User
|
||||
Priority int64 // 0-9, 1 is highest
|
||||
RelatedToUID string
|
||||
Color string
|
||||
Categories []string
|
||||
Start time.Time
|
||||
End time.Time
|
||||
DueDate time.Time
|
||||
Duration time.Duration
|
||||
RepeatAfter int64
|
||||
RepeatMode models.TaskRepeatMode
|
||||
Alarms []Alarm
|
||||
Summary string
|
||||
Description string
|
||||
Completed time.Time
|
||||
Organizer *user.User
|
||||
Priority int64 // 0-9, 1 is highest
|
||||
Relations []Relation
|
||||
Color string
|
||||
Categories []string
|
||||
Start time.Time
|
||||
End time.Time
|
||||
DueDate time.Time
|
||||
Duration time.Duration
|
||||
RepeatAfter int64
|
||||
RepeatMode models.TaskRepeatMode
|
||||
Alarms []Alarm
|
||||
|
||||
Created time.Time
|
||||
Updated time.Time // last-mod
|
||||
@ -66,6 +66,11 @@ type Alarm struct {
|
||||
Description string
|
||||
}
|
||||
|
||||
type Relation struct {
|
||||
Type models.RelationKind
|
||||
UID string
|
||||
}
|
||||
|
||||
// Config is the caldav calendar config
|
||||
type Config struct {
|
||||
Name string
|
||||
@ -147,11 +152,6 @@ STATUS:COMPLETED`
|
||||
ORGANIZER;CN=:` + t.Organizer.Username
|
||||
}
|
||||
|
||||
if t.RelatedToUID != "" {
|
||||
caldavtodos += `
|
||||
RELATED-TO:` + t.RelatedToUID
|
||||
}
|
||||
|
||||
if t.DueDate.Unix() > 0 {
|
||||
caldavtodos += `
|
||||
DUE:` + makeCalDavTimeFromTimeStamp(t.DueDate)
|
||||
@ -185,6 +185,7 @@ CATEGORIES:` + strings.Join(t.Categories, ",")
|
||||
caldavtodos += `
|
||||
LAST-MODIFIED:` + makeCalDavTimeFromTimeStamp(t.Updated)
|
||||
caldavtodos += ParseAlarms(t.Alarms, t.Summary)
|
||||
caldavtodos += ParseRelations(t.Relations)
|
||||
caldavtodos += `
|
||||
END:VTODO`
|
||||
}
|
||||
@ -222,6 +223,47 @@ END:VALARM`
|
||||
return caldavalarms
|
||||
}
|
||||
|
||||
func ParseRelations(relations []Relation) (caldavrelatedtos string) {
|
||||
|
||||
for _, r := range relations {
|
||||
switch r.Type {
|
||||
case models.RelationKindParenttask:
|
||||
caldavrelatedtos += `
|
||||
RELATED-TO;RELTYPE=PARENT:`
|
||||
case models.RelationKindSubtask:
|
||||
caldavrelatedtos += `
|
||||
RELATED-TO;RELTYPE=CHILD:`
|
||||
case models.RelationKindUnknown:
|
||||
continue
|
||||
case models.RelationKindRelated:
|
||||
continue
|
||||
case models.RelationKindDuplicateOf:
|
||||
continue
|
||||
case models.RelationKindDuplicates:
|
||||
continue
|
||||
case models.RelationKindBlocking:
|
||||
continue
|
||||
case models.RelationKindBlocked:
|
||||
continue
|
||||
case models.RelationKindPreceeds:
|
||||
continue
|
||||
case models.RelationKindFollows:
|
||||
continue
|
||||
case models.RelationKindCopiedFrom:
|
||||
continue
|
||||
case models.RelationKindCopiedTo:
|
||||
continue
|
||||
default:
|
||||
caldavrelatedtos += `
|
||||
RELATED-TO:`
|
||||
}
|
||||
|
||||
caldavrelatedtos += r.UID
|
||||
}
|
||||
|
||||
return caldavrelatedtos
|
||||
}
|
||||
|
||||
func makeCalDavTimeFromTimeStamp(ts time.Time) (caldavtime string) {
|
||||
return ts.In(time.UTC).Format(DateFormat) + "Z"
|
||||
}
|
||||
|
@ -326,6 +326,49 @@ ACTION:DISPLAY
|
||||
DESCRIPTION:Todo #1
|
||||
END:VALARM
|
||||
END:VTODO
|
||||
END:VCALENDAR`,
|
||||
},
|
||||
{
|
||||
name: "with related-to",
|
||||
args: args{
|
||||
config: &Config{
|
||||
Name: "test",
|
||||
ProdID: "RandomProdID which is not random",
|
||||
},
|
||||
todos: []*Todo{
|
||||
{
|
||||
Summary: "Todo #1",
|
||||
Description: "Lorem Ipsum",
|
||||
UID: "randommduid",
|
||||
Relations: []Relation{
|
||||
{
|
||||
Type: models.RelationKindParenttask,
|
||||
UID: "parentuid",
|
||||
},
|
||||
{
|
||||
Type: models.RelationKindSubtask,
|
||||
UID: "subtaskuid",
|
||||
},
|
||||
},
|
||||
Timestamp: time.Unix(1543626724, 0).In(config.GetTimeZone()),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantCaldavtasks: `BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
METHOD:PUBLISH
|
||||
X-PUBLISHED-TTL:PT4H
|
||||
X-WR-CALNAME:test
|
||||
PRODID:-//RandomProdID which is not random//EN
|
||||
BEGIN:VTODO
|
||||
UID:randommduid
|
||||
DTSTAMP:20181201T011204Z
|
||||
SUMMARY:Todo #1
|
||||
DESCRIPTION:Lorem Ipsum
|
||||
LAST-MODIFIED:00010101T000000Z
|
||||
RELATED-TO;RELTYPE=PARENT:parentuid
|
||||
RELATED-TO;RELTYPE=CHILD:subtaskuid
|
||||
END:VTODO
|
||||
END:VCALENDAR`,
|
||||
},
|
||||
}
|
||||
|
@ -23,7 +23,6 @@ import (
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
"code.vikunja.io/api/pkg/utils"
|
||||
@ -51,6 +50,16 @@ func GetCaldavTodosForTasks(project *models.ProjectWithTasksAndBuckets, projectT
|
||||
})
|
||||
}
|
||||
|
||||
var relations []Relation
|
||||
for reltype, tasks := range t.RelatedTasks {
|
||||
for _, r := range tasks {
|
||||
relations = append(relations, Relation{
|
||||
Type: reltype,
|
||||
UID: r.UID,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
caldavtodos = append(caldavtodos, &Todo{
|
||||
Timestamp: t.Updated,
|
||||
UID: t.UID,
|
||||
@ -69,6 +78,7 @@ func GetCaldavTodosForTasks(project *models.ProjectWithTasksAndBuckets, projectT
|
||||
RepeatAfter: t.RepeatAfter,
|
||||
RepeatMode: t.RepeatMode,
|
||||
Alarms: alarms,
|
||||
Relations: relations,
|
||||
})
|
||||
}
|
||||
|
||||
@ -91,11 +101,11 @@ func ParseTaskFromVTODO(content string) (vTask *models.Task, err error) {
|
||||
}
|
||||
// We put the vTodo details in a map to be able to handle them more easily
|
||||
task := make(map[string]ics.IANAProperty)
|
||||
var relation ics.IANAProperty
|
||||
var relations []ics.IANAProperty
|
||||
for _, c := range vTodo.UnknownPropertiesIANAProperties() {
|
||||
task[c.IANAToken] = c
|
||||
if strings.HasPrefix(c.IANAToken, "RELATED-TO") {
|
||||
relation = c
|
||||
relations = append(relations, c)
|
||||
}
|
||||
}
|
||||
|
||||
@ -139,17 +149,33 @@ func ParseTaskFromVTODO(content string) (vTask *models.Task, err error) {
|
||||
DoneAt: caldavTimeToTimestamp(task["COMPLETED"]),
|
||||
}
|
||||
|
||||
if relation.Value != "" {
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
for _, c := range relations {
|
||||
var relTypeStr string
|
||||
if _, ok := c.ICalParameters["RELTYPE"]; ok {
|
||||
if len(c.ICalParameters["RELTYPE"]) != 1 {
|
||||
continue
|
||||
}
|
||||
|
||||
subtask, err := models.GetTaskSimpleByUUID(s, relation.Value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
relTypeStr = c.ICalParameters["RELTYPE"][0]
|
||||
}
|
||||
|
||||
vTask.RelatedTasks = make(map[models.RelationKind][]*models.Task)
|
||||
vTask.RelatedTasks[models.RelationKindSubtask] = []*models.Task{subtask}
|
||||
var relationKind models.RelationKind
|
||||
switch relTypeStr {
|
||||
case "PARENT":
|
||||
relationKind = models.RelationKindParenttask
|
||||
case "CHILD":
|
||||
relationKind = models.RelationKindSubtask
|
||||
default:
|
||||
relationKind = models.RelationKindParenttask
|
||||
}
|
||||
|
||||
if vTask.RelatedTasks == nil {
|
||||
vTask.RelatedTasks = make(map[models.RelationKind][]*models.Task)
|
||||
}
|
||||
|
||||
vTask.RelatedTasks[relationKind] = append(vTask.RelatedTasks[relationKind], &models.Task{
|
||||
UID: c.Value,
|
||||
})
|
||||
}
|
||||
|
||||
if task["STATUS"].Value == "COMPLETED" {
|
||||
|
@ -219,6 +219,70 @@ END:VCALENDAR`,
|
||||
Updated: time.Unix(1543626724, 0).In(config.GetTimeZone()),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "With parent",
|
||||
args: args{content: `BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
METHOD:PUBLISH
|
||||
X-PUBLISHED-TTL:PT4H
|
||||
X-WR-CALNAME:test
|
||||
PRODID:-//RandomProdID which is not random//EN
|
||||
BEGIN:VTODO
|
||||
UID:randomuid
|
||||
DTSTAMP:20181201T011204
|
||||
SUMMARY:SubTask #1
|
||||
DESCRIPTION:Lorem Ipsum
|
||||
LAST-MODIFIED:00010101T000000
|
||||
RELATED-TO;RELTYPE=PARENT:randomuid_parent
|
||||
END:VTODO
|
||||
END:VCALENDAR`,
|
||||
},
|
||||
wantVTask: &models.Task{
|
||||
Title: "SubTask #1",
|
||||
UID: "randomuid",
|
||||
Description: "Lorem Ipsum",
|
||||
Updated: time.Unix(1543626724, 0).In(config.GetTimeZone()),
|
||||
RelatedTasks: map[models.RelationKind][]*models.Task{
|
||||
models.RelationKindParenttask: {
|
||||
{
|
||||
UID: "randomuid_parent",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "With subtask",
|
||||
args: args{content: `BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
METHOD:PUBLISH
|
||||
X-PUBLISHED-TTL:PT4H
|
||||
X-WR-CALNAME:test
|
||||
PRODID:-//RandomProdID which is not random//EN
|
||||
BEGIN:VTODO
|
||||
UID:randomuid
|
||||
DTSTAMP:20181201T011204
|
||||
SUMMARY:Parent
|
||||
DESCRIPTION:Lorem Ipsum
|
||||
LAST-MODIFIED:00010101T000000
|
||||
RELATED-TO;RELTYPE=CHILD:randomuid_child
|
||||
END:VTODO
|
||||
END:VCALENDAR`,
|
||||
},
|
||||
wantVTask: &models.Task{
|
||||
Title: "Parent",
|
||||
UID: "randomuid",
|
||||
Description: "Lorem Ipsum",
|
||||
Updated: time.Unix(1543626724, 0).In(config.GetTimeZone()),
|
||||
RelatedTasks: map[models.RelationKind][]*models.Task{
|
||||
models.RelationKindSubtask: {
|
||||
{
|
||||
UID: "randomuid_child",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "example task from tasks.org app",
|
||||
args: args{content: `BEGIN:VCALENDAR
|
||||
@ -392,6 +456,124 @@ ACTION:DISPLAY
|
||||
DESCRIPTION:Task 1
|
||||
END:VALARM
|
||||
END:VTODO
|
||||
END:VCALENDAR`,
|
||||
},
|
||||
{
|
||||
name: "Format Task with Related Tasks as CalDAV",
|
||||
args: args{
|
||||
list: &models.ProjectWithTasksAndBuckets{
|
||||
Project: models.Project{
|
||||
Title: "List title",
|
||||
},
|
||||
},
|
||||
tasks: []*models.TaskWithComments{
|
||||
{
|
||||
Task: models.Task{
|
||||
Title: "Parent Task",
|
||||
UID: "randomuid_parent",
|
||||
Description: "A parent task",
|
||||
Priority: 3,
|
||||
Created: time.Unix(1543626721, 0).In(config.GetTimeZone()),
|
||||
Updated: time.Unix(1543626725, 0).In(config.GetTimeZone()),
|
||||
RelatedTasks: map[models.RelationKind][]*models.Task{
|
||||
models.RelationKindSubtask: {
|
||||
{
|
||||
Title: "Subtask 1",
|
||||
UID: "randomuid_child_1",
|
||||
Description: "The first child task",
|
||||
Created: time.Unix(1543626724, 0).In(config.GetTimeZone()),
|
||||
Updated: time.Unix(1543626724, 0).In(config.GetTimeZone()),
|
||||
},
|
||||
{
|
||||
Title: "Subtask 2",
|
||||
UID: "randomuid_child_2",
|
||||
Description: "The second child task",
|
||||
Created: time.Unix(1543626724, 0).In(config.GetTimeZone()),
|
||||
Updated: time.Unix(1543626724, 0).In(config.GetTimeZone()),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Task: models.Task{
|
||||
Title: "Subtask 1",
|
||||
UID: "randomuid_child_1",
|
||||
Description: "The first child task",
|
||||
Created: time.Unix(1543626724, 0).In(config.GetTimeZone()),
|
||||
Updated: time.Unix(1543626724, 0).In(config.GetTimeZone()),
|
||||
RelatedTasks: map[models.RelationKind][]*models.Task{
|
||||
models.RelationKindParenttask: {
|
||||
{
|
||||
Title: "Parent task",
|
||||
UID: "randomuid_parent",
|
||||
Description: "A parent task",
|
||||
Priority: 3,
|
||||
Created: time.Unix(1543626721, 0).In(config.GetTimeZone()),
|
||||
Updated: time.Unix(1543626725, 0).In(config.GetTimeZone()),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Task: models.Task{
|
||||
Title: "Subtask 2",
|
||||
UID: "randomuid_child_2",
|
||||
Description: "The second child task",
|
||||
Created: time.Unix(1543626724, 0).In(config.GetTimeZone()),
|
||||
Updated: time.Unix(1543626724, 0).In(config.GetTimeZone()),
|
||||
RelatedTasks: map[models.RelationKind][]*models.Task{
|
||||
models.RelationKindParenttask: {
|
||||
{
|
||||
Title: "Parent task",
|
||||
UID: "randomuid_parent",
|
||||
Description: "A parent task",
|
||||
Priority: 3,
|
||||
Created: time.Unix(1543626721, 0).In(config.GetTimeZone()),
|
||||
Updated: time.Unix(1543626725, 0).In(config.GetTimeZone()),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantCaldav: `BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
METHOD:PUBLISH
|
||||
X-PUBLISHED-TTL:PT4H
|
||||
X-WR-CALNAME:List title
|
||||
PRODID:-//Vikunja Todo App//EN
|
||||
BEGIN:VTODO
|
||||
UID:randomuid_parent
|
||||
DTSTAMP:20181201T011205Z
|
||||
SUMMARY:Parent Task
|
||||
DESCRIPTION:A parent task
|
||||
CREATED:20181201T011201Z
|
||||
PRIORITY:3
|
||||
LAST-MODIFIED:20181201T011205Z
|
||||
RELATED-TO;RELTYPE=CHILD:randomuid_child_1
|
||||
RELATED-TO;RELTYPE=CHILD:randomuid_child_2
|
||||
END:VTODO
|
||||
BEGIN:VTODO
|
||||
UID:randomuid_child_1
|
||||
DTSTAMP:20181201T011204Z
|
||||
SUMMARY:Subtask 1
|
||||
DESCRIPTION:The first child task
|
||||
CREATED:20181201T011204Z
|
||||
LAST-MODIFIED:20181201T011204Z
|
||||
RELATED-TO;RELTYPE=PARENT:randomuid_parent
|
||||
END:VTODO
|
||||
BEGIN:VTODO
|
||||
UID:randomuid_child_2
|
||||
DTSTAMP:20181201T011204Z
|
||||
SUMMARY:Subtask 2
|
||||
DESCRIPTION:The second child task
|
||||
CREATED:20181201T011204Z
|
||||
LAST-MODIFIED:20181201T011204Z
|
||||
RELATED-TO;RELTYPE=PARENT:randomuid_parent
|
||||
END:VTODO
|
||||
END:VCALENDAR`,
|
||||
},
|
||||
}
|
||||
|
@ -236,3 +236,9 @@
|
||||
created_by_id: 15
|
||||
created: 2020-04-18 21:13:52
|
||||
updated: 2020-04-18 21:13:52
|
||||
- id: 39
|
||||
title: testbucket38
|
||||
project_id: 38
|
||||
created_by_id: 15
|
||||
created: 2020-04-18 21:13:52
|
||||
updated: 2020-04-18 21:13:52
|
@ -327,3 +327,12 @@
|
||||
position: 1
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
-
|
||||
id: 38
|
||||
title: Project 38 for Caldav tests
|
||||
description: Lorem Ipsum
|
||||
identifier: test38
|
||||
owner_id: 15
|
||||
position: 2
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
@ -34,3 +34,39 @@
|
||||
relation_kind: 'related'
|
||||
created_by_id: 1
|
||||
created: 2018-12-01 15:13:12
|
||||
- id: 7
|
||||
task_id: 41
|
||||
other_task_id: 43
|
||||
relation_kind: 'subtask'
|
||||
created_by_id: 15
|
||||
created: 2018-12-01 15:13:12
|
||||
- id: 8
|
||||
task_id: 43
|
||||
other_task_id: 41
|
||||
relation_kind: 'parenttask'
|
||||
created_by_id: 15
|
||||
created: 2018-12-01 15:13:12
|
||||
- id: 9
|
||||
task_id: 41
|
||||
other_task_id: 44
|
||||
relation_kind: 'subtask'
|
||||
created_by_id: 15
|
||||
created: 2018-12-01 15:13:12
|
||||
- id: 10
|
||||
task_id: 44
|
||||
other_task_id: 41
|
||||
relation_kind: 'parenttask'
|
||||
created_by_id: 15
|
||||
created: 2018-12-01 15:13:12
|
||||
- id: 11
|
||||
task_id: 45
|
||||
other_task_id: 46
|
||||
relation_kind: 'subtask'
|
||||
created_by_id: 15
|
||||
created: 2018-12-01 15:13:12
|
||||
- id: 12
|
||||
task_id: 46
|
||||
other_task_id: 45
|
||||
relation_kind: 'parenttask'
|
||||
created_by_id: 15
|
||||
created: 2018-12-01 15:13:12
|
@ -374,5 +374,89 @@
|
||||
due_date: 2023-03-01 15:00:00
|
||||
created: 2018-12-01 01:12:04
|
||||
updated: 2018-12-01 01:12:04
|
||||
bucket_id: 1
|
||||
bucket_id: 38
|
||||
position: 39
|
||||
- id: 41
|
||||
uid: 'uid-caldav-test-parent-task'
|
||||
title: 'Parent task for Caldav Test'
|
||||
description: 'Description Caldav Test'
|
||||
priority: 3
|
||||
done: false
|
||||
created_by_id: 15
|
||||
project_id: 36
|
||||
index: 40
|
||||
due_date: 2023-03-01 15:00:00
|
||||
created: 2018-12-01 01:12:04
|
||||
updated: 2018-12-01 01:12:04
|
||||
bucket_id: 38
|
||||
position: 40
|
||||
- id: 42
|
||||
uid: 'uid-caldav-test-parent-task-2'
|
||||
title: 'Parent task for Caldav Test 2'
|
||||
description: 'Description Caldav Test 2'
|
||||
priority: 3
|
||||
done: false
|
||||
created_by_id: 15
|
||||
project_id: 36
|
||||
index: 41
|
||||
due_date: 2023-03-01 15:00:00
|
||||
created: 2018-12-01 01:12:04
|
||||
updated: 2018-12-01 01:12:04
|
||||
bucket_id: 38
|
||||
position: 41
|
||||
- id: 43
|
||||
uid: 'uid-caldav-test-child-task'
|
||||
title: 'Child task for Caldav Test'
|
||||
description: 'Description Caldav Test'
|
||||
priority: 3
|
||||
done: false
|
||||
created_by_id: 15
|
||||
project_id: 36
|
||||
index: 42
|
||||
due_date: 2023-03-01 15:00:00
|
||||
created: 2018-12-01 01:12:04
|
||||
updated: 2018-12-01 01:12:04
|
||||
bucket_id: 38
|
||||
position: 42
|
||||
- id: 44
|
||||
uid: 'uid-caldav-test-child-task-2'
|
||||
title: 'Child task for Caldav Test '
|
||||
description: 'Description Caldav Test'
|
||||
priority: 3
|
||||
done: false
|
||||
created_by_id: 15
|
||||
project_id: 38
|
||||
index: 43
|
||||
due_date: 2023-03-01 15:00:00
|
||||
created: 2018-12-01 01:12:04
|
||||
updated: 2018-12-01 01:12:04
|
||||
bucket_id: 38
|
||||
position: 43
|
||||
- id: 45
|
||||
uid: 'uid-caldav-test-parent-task-another-list'
|
||||
title: 'Parent task for Caldav Test'
|
||||
description: 'Description Caldav Test'
|
||||
priority: 3
|
||||
done: false
|
||||
created_by_id: 15
|
||||
project_id: 36
|
||||
index: 44
|
||||
due_date: 2023-03-01 15:00:00
|
||||
created: 2018-12-01 01:12:04
|
||||
updated: 2018-12-01 01:12:04
|
||||
bucket_id: 38
|
||||
position: 44
|
||||
- id: 46
|
||||
uid: 'uid-caldav-test-child-task-another-list'
|
||||
title: 'Child task for Caldav Test '
|
||||
description: 'Description Caldav Test'
|
||||
priority: 3
|
||||
done: false
|
||||
created_by_id: 15
|
||||
project_id: 38
|
||||
index: 45
|
||||
due_date: 2023-03-01 15:00:00
|
||||
created: 2018-12-01 01:12:04
|
||||
updated: 2018-12-01 01:12:04
|
||||
bucket_id: 38
|
||||
position: 45
|
@ -100,3 +100,15 @@
|
||||
right: 0
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
- id: 18
|
||||
user_id: 15
|
||||
project_id: 36
|
||||
right: 0
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
||||
- id: 19
|
||||
user_id: 15
|
||||
project_id: 38
|
||||
right: 0
|
||||
updated: 2018-12-02 15:13:12
|
||||
created: 2018-12-01 15:13:12
|
@ -24,7 +24,20 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const vtodo = `BEGIN:VCALENDAR
|
||||
func TestCaldav(t *testing.T) {
|
||||
t.Run("Delivers VTODO for project", func(t *testing.T) {
|
||||
e, _ := setupTestEnv()
|
||||
rec, err := newCaldavTestRequestWithUser(t, e, http.MethodGet, caldav.ProjectHandler, &testuser15, ``, nil, map[string]string{"project": "36"})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), "BEGIN:VCALENDAR")
|
||||
assert.Contains(t, rec.Body.String(), "PRODID:-//Vikunja Todo App//EN")
|
||||
assert.Contains(t, rec.Body.String(), "X-WR-CALNAME:Project 36 for Caldav tests")
|
||||
assert.Contains(t, rec.Body.String(), "BEGIN:VTODO")
|
||||
assert.Contains(t, rec.Body.String(), "END:VTODO")
|
||||
assert.Contains(t, rec.Body.String(), "END:VCALENDAR")
|
||||
})
|
||||
t.Run("Import VTODO", func(t *testing.T) {
|
||||
const vtodo = `BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
METHOD:PUBLISH
|
||||
X-PUBLISHED-TTL:PT4H
|
||||
@ -44,24 +57,14 @@ END:VALARM
|
||||
END:VTODO
|
||||
END:VCALENDAR`
|
||||
|
||||
func TestCaldav(t *testing.T) {
|
||||
t.Run("Delivers VTODO for project", func(t *testing.T) {
|
||||
rec, err := newCaldavTestRequestWithUser(t, http.MethodGet, caldav.ProjectHandler, &testuser15, ``, nil, map[string]string{"project": "36"})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), "BEGIN:VCALENDAR")
|
||||
assert.Contains(t, rec.Body.String(), "PRODID:-//Vikunja Todo App//EN")
|
||||
assert.Contains(t, rec.Body.String(), "X-WR-CALNAME:Project 36 for Caldav tests")
|
||||
assert.Contains(t, rec.Body.String(), "BEGIN:VTODO")
|
||||
assert.Contains(t, rec.Body.String(), "END:VTODO")
|
||||
assert.Contains(t, rec.Body.String(), "END:VCALENDAR")
|
||||
})
|
||||
t.Run("Import VTODO", func(t *testing.T) {
|
||||
rec, err := newCaldavTestRequestWithUser(t, http.MethodPut, caldav.TaskHandler, &testuser15, vtodo, nil, map[string]string{"project": "36", "task": "uid"})
|
||||
e, _ := setupTestEnv()
|
||||
rec, err := newCaldavTestRequestWithUser(t, e, http.MethodPut, caldav.TaskHandler, &testuser15, vtodo, nil, map[string]string{"project": "36", "task": "uid"})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 201, rec.Result().StatusCode)
|
||||
})
|
||||
t.Run("Export VTODO", func(t *testing.T) {
|
||||
rec, err := newCaldavTestRequestWithUser(t, http.MethodGet, caldav.TaskHandler, &testuser15, ``, nil, map[string]string{"project": "36", "task": "uid-caldav-test"})
|
||||
e, _ := setupTestEnv()
|
||||
rec, err := newCaldavTestRequestWithUser(t, e, http.MethodGet, caldav.TaskHandler, &testuser15, ``, nil, map[string]string{"project": "36", "task": "uid-caldav-test"})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), "BEGIN:VCALENDAR")
|
||||
assert.Contains(t, rec.Body.String(), "SUMMARY:Title Caldav Test")
|
||||
@ -75,3 +78,241 @@ func TestCaldav(t *testing.T) {
|
||||
assert.Contains(t, rec.Body.String(), "END:VALARM")
|
||||
})
|
||||
}
|
||||
|
||||
func TestCaldavSubtasks(t *testing.T) {
|
||||
const vtodoHeader = `BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
METHOD:PUBLISH
|
||||
X-PUBLISHED-TTL:PT4H
|
||||
X-WR-CALNAME:Project 36 for Caldav tests
|
||||
PRODID:-//Vikunja Todo App//EN
|
||||
`
|
||||
const vtodoFooter = `
|
||||
END:VCALENDAR`
|
||||
|
||||
t.Run("Import Task & Subtask", func(t *testing.T) {
|
||||
|
||||
const vtodoParentTaskStub = `BEGIN:VTODO
|
||||
UID:uid_parent_import
|
||||
DTSTAMP:20230301T073337Z
|
||||
SUMMARY:Caldav parent task
|
||||
CREATED:20230301T073337Z
|
||||
LAST-MODIFIED:20230301T073337Z
|
||||
END:VTODO`
|
||||
|
||||
const vtodoChildTaskStub = `BEGIN:VTODO
|
||||
UID:uid_child_import
|
||||
DTSTAMP:20230301T073337Z
|
||||
SUMMARY:Caldav child task
|
||||
CREATED:20230301T073337Z
|
||||
LAST-MODIFIED:20230301T073337Z
|
||||
RELATED-TO;RELTYPE=PARENT:uid_parent_import
|
||||
END:VTODO`
|
||||
|
||||
const vtodoGrandChildTaskStub = `
|
||||
BEGIN:VTODO
|
||||
UID:uid_grand_child_import
|
||||
DTSTAMP:20230301T073337Z
|
||||
SUMMARY:Caldav grand child task
|
||||
CREATED:20230301T073337Z
|
||||
LAST-MODIFIED:20230301T073337Z
|
||||
RELATED-TO;RELTYPE=PARENT:uid_child_import
|
||||
END:VTODO`
|
||||
|
||||
e, _ := setupTestEnv()
|
||||
|
||||
const parentVTODO = vtodoHeader + vtodoParentTaskStub + vtodoFooter
|
||||
rec, err := newCaldavTestRequestWithUser(t, e, http.MethodPut, caldav.TaskHandler, &testuser15, parentVTODO, nil, map[string]string{"project": "36", "task": "uid_parent_import"})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 201, rec.Result().StatusCode)
|
||||
|
||||
const childVTODO = vtodoHeader + vtodoChildTaskStub + vtodoFooter
|
||||
rec, err = newCaldavTestRequestWithUser(t, e, http.MethodPut, caldav.TaskHandler, &testuser15, childVTODO, nil, map[string]string{"project": "36", "task": "uid_child_import"})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 201, rec.Result().StatusCode)
|
||||
|
||||
const grandChildVTODO = vtodoHeader + vtodoGrandChildTaskStub + vtodoFooter
|
||||
rec, err = newCaldavTestRequestWithUser(t, e, http.MethodPut, caldav.TaskHandler, &testuser15, grandChildVTODO, nil, map[string]string{"project": "36", "task": "uid_grand_child_import"})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 201, rec.Result().StatusCode)
|
||||
|
||||
rec, err = newCaldavTestRequestWithUser(t, e, http.MethodGet, caldav.ProjectHandler, &testuser15, ``, nil, map[string]string{"project": "36"})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 200, rec.Result().StatusCode)
|
||||
|
||||
assert.Contains(t, rec.Body.String(), "UID:uid_parent_import")
|
||||
assert.Contains(t, rec.Body.String(), "RELATED-TO;RELTYPE=CHILD:uid_child_import")
|
||||
assert.Contains(t, rec.Body.String(), "UID:uid_child_import")
|
||||
assert.Contains(t, rec.Body.String(), "RELATED-TO;RELTYPE=PARENT:uid_parent_import")
|
||||
assert.Contains(t, rec.Body.String(), "RELATED-TO;RELTYPE=CHILD:uid_grand_child_import")
|
||||
assert.Contains(t, rec.Body.String(), "UID:uid_grand_child_import")
|
||||
assert.Contains(t, rec.Body.String(), "RELATED-TO;RELTYPE=PARENT:uid_child_import")
|
||||
})
|
||||
|
||||
t.Run("Import Task & Subtask (Reverse - Subtask first)", func(t *testing.T) {
|
||||
e, _ := setupTestEnv()
|
||||
|
||||
const vtodoGrandChildTaskStub = `
|
||||
BEGIN:VTODO
|
||||
UID:uid_grand_child_import
|
||||
DTSTAMP:20230301T073337Z
|
||||
SUMMARY:Caldav grand child task
|
||||
CREATED:20230301T073337Z
|
||||
LAST-MODIFIED:20230301T073337Z
|
||||
RELATED-TO;RELTYPE=PARENT:uid_child_import
|
||||
END:VTODO`
|
||||
|
||||
const grandChildVTODO = vtodoHeader + vtodoGrandChildTaskStub + vtodoFooter
|
||||
rec, err := newCaldavTestRequestWithUser(t, e, http.MethodPut, caldav.TaskHandler, &testuser15, grandChildVTODO, nil, map[string]string{"project": "36", "task": "uid_grand_child_import"})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 201, rec.Result().StatusCode)
|
||||
|
||||
const vtodoChildTaskStub = `BEGIN:VTODO
|
||||
UID:uid_child_import
|
||||
DTSTAMP:20230301T073337Z
|
||||
SUMMARY:Caldav child task
|
||||
CREATED:20230301T073337Z
|
||||
LAST-MODIFIED:20230301T073337Z
|
||||
RELATED-TO;RELTYPE=PARENT:uid_parent_import
|
||||
RELATED-TO;RELTYPE=CHILD:uid_grand_child_import
|
||||
END:VTODO`
|
||||
|
||||
const childVTODO = vtodoHeader + vtodoChildTaskStub + vtodoFooter
|
||||
rec, err = newCaldavTestRequestWithUser(t, e, http.MethodPut, caldav.TaskHandler, &testuser15, childVTODO, nil, map[string]string{"project": "36", "task": "uid_child_import"})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 201, rec.Result().StatusCode)
|
||||
|
||||
const vtodoParentTaskStub = `BEGIN:VTODO
|
||||
UID:uid_parent_import
|
||||
DTSTAMP:20230301T073337Z
|
||||
SUMMARY:Caldav parent task
|
||||
CREATED:20230301T073337Z
|
||||
LAST-MODIFIED:20230301T073337Z
|
||||
RELATED-TO;RELTYPE=CHILD:uid_child_import
|
||||
END:VTODO`
|
||||
|
||||
const parentVTODO = vtodoHeader + vtodoParentTaskStub + vtodoFooter
|
||||
rec, err = newCaldavTestRequestWithUser(t, e, http.MethodPut, caldav.TaskHandler, &testuser15, parentVTODO, nil, map[string]string{"project": "36", "task": "uid_parent_import"})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 201, rec.Result().StatusCode)
|
||||
|
||||
rec, err = newCaldavTestRequestWithUser(t, e, http.MethodGet, caldav.ProjectHandler, &testuser15, ``, nil, map[string]string{"project": "36"})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 200, rec.Result().StatusCode)
|
||||
|
||||
assert.Contains(t, rec.Body.String(), "UID:uid_parent_import")
|
||||
assert.Contains(t, rec.Body.String(), "RELATED-TO;RELTYPE=CHILD:uid_child_import")
|
||||
assert.Contains(t, rec.Body.String(), "UID:uid_child_import")
|
||||
assert.Contains(t, rec.Body.String(), "RELATED-TO;RELTYPE=PARENT:uid_parent_import")
|
||||
assert.Contains(t, rec.Body.String(), "RELATED-TO;RELTYPE=CHILD:uid_grand_child_import")
|
||||
assert.Contains(t, rec.Body.String(), "UID:uid_grand_child_import")
|
||||
assert.Contains(t, rec.Body.String(), "RELATED-TO;RELTYPE=PARENT:uid_child_import")
|
||||
})
|
||||
|
||||
t.Run("Delete Subtask", func(t *testing.T) {
|
||||
e, _ := setupTestEnv()
|
||||
|
||||
rec, err := newCaldavTestRequestWithUser(t, e, http.MethodDelete, caldav.TaskHandler, &testuser15, ``, nil, map[string]string{"project": "36", "task": "uid-caldav-test-child-task"})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 204, rec.Result().StatusCode)
|
||||
|
||||
rec, err = newCaldavTestRequestWithUser(t, e, http.MethodDelete, caldav.TaskHandler, &testuser15, ``, nil, map[string]string{"project": "36", "task": "uid-caldav-test-child-task-2"})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 204, rec.Result().StatusCode)
|
||||
|
||||
rec, err = newCaldavTestRequestWithUser(t, e, http.MethodGet, caldav.TaskHandler, &testuser15, ``, nil, map[string]string{"project": "36", "task": "uid-caldav-test-parent-task"})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 200, rec.Result().StatusCode)
|
||||
|
||||
assert.NotContains(t, rec.Body.String(), "RELATED-TO;RELTYPE=CHILD:uid-caldav-test-child-task")
|
||||
assert.NotContains(t, rec.Body.String(), "RELATED-TO;RELTYPE=CHILD:uid-caldav-test-child-task-2")
|
||||
})
|
||||
|
||||
t.Run("Delete Parent Task", func(t *testing.T) {
|
||||
e, _ := setupTestEnv()
|
||||
|
||||
rec, err := newCaldavTestRequestWithUser(t, e, http.MethodDelete, caldav.TaskHandler, &testuser15, ``, nil, map[string]string{"project": "36", "task": "uid-caldav-test-parent-task"})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 204, rec.Result().StatusCode)
|
||||
|
||||
rec, err = newCaldavTestRequestWithUser(t, e, http.MethodGet, caldav.TaskHandler, &testuser15, ``, nil, map[string]string{"project": "36", "task": "uid-caldav-test-child-task"})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 200, rec.Result().StatusCode)
|
||||
|
||||
assert.NotContains(t, rec.Body.String(), "RELATED-TO;RELTYPE=PARENT:uid-caldav-test-parent-task")
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func TestCaldavSubtasksDifferentLists(t *testing.T) {
|
||||
t.Run("Import Parent Task & Child Task Different Lists", func(t *testing.T) {
|
||||
const vtodoParentTask = `BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
METHOD:PUBLISH
|
||||
X-PUBLISHED-TTL:PT4H
|
||||
X-WR-CALNAME:Project 36 for Caldav tests
|
||||
PRODID:-//Vikunja Todo App//EN
|
||||
BEGIN:VTODO
|
||||
UID:uid_parent_import
|
||||
DTSTAMP:20230301T073337Z
|
||||
SUMMARY:Caldav parent task
|
||||
CREATED:20230301T073337Z
|
||||
LAST-MODIFIED:20230301T073337Z
|
||||
END:VTODO
|
||||
END:VCALENDAR`
|
||||
|
||||
const vtodoChildTask = `BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
METHOD:PUBLISH
|
||||
X-PUBLISHED-TTL:PT4H
|
||||
X-WR-CALNAME:Project 38 for Caldav tests
|
||||
PRODID:-//Vikunja Todo App//EN
|
||||
BEGIN:VTODO
|
||||
UID:uid_child_import
|
||||
DTSTAMP:20230301T073337Z
|
||||
SUMMARY:Caldav child task
|
||||
CREATED:20230301T073337Z
|
||||
LAST-MODIFIED:20230301T073337Z
|
||||
RELATED-TO;RELTYPE=PARENT:uid_parent_import
|
||||
END:VTODO
|
||||
END:VCALENDAR`
|
||||
|
||||
e, _ := setupTestEnv()
|
||||
|
||||
rec, err := newCaldavTestRequestWithUser(t, e, http.MethodPut, caldav.TaskHandler, &testuser15, vtodoParentTask, nil, map[string]string{"project": "36", "task": "uid_parent_import"})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, rec.Result().StatusCode, 201)
|
||||
|
||||
rec, err = newCaldavTestRequestWithUser(t, e, http.MethodPut, caldav.TaskHandler, &testuser15, vtodoChildTask, nil, map[string]string{"project": "38", "task": "uid_child_import"})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, rec.Result().StatusCode, 201)
|
||||
|
||||
rec, err = newCaldavTestRequestWithUser(t, e, http.MethodGet, caldav.TaskHandler, &testuser15, ``, nil, map[string]string{"project": "36", "task": "uid_parent_import"})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, rec.Result().StatusCode, 200)
|
||||
assert.Contains(t, rec.Body.String(), "UID:uid_parent_import")
|
||||
assert.Contains(t, rec.Body.String(), "RELATED-TO;RELTYPE=CHILD:uid_child_import")
|
||||
|
||||
rec, err = newCaldavTestRequestWithUser(t, e, http.MethodGet, caldav.TaskHandler, &testuser15, ``, nil, map[string]string{"project": "38", "task": "uid_child_import"})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, rec.Result().StatusCode, 200)
|
||||
assert.Contains(t, rec.Body.String(), "UID:uid_child_import")
|
||||
assert.Contains(t, rec.Body.String(), "RELATED-TO;RELTYPE=PARENT:uid_parent_import")
|
||||
})
|
||||
|
||||
t.Run("Check relationships across lists", func(t *testing.T) {
|
||||
e, _ := setupTestEnv()
|
||||
|
||||
rec, err := newCaldavTestRequestWithUser(t, e, http.MethodGet, caldav.TaskHandler, &testuser15, ``, nil, map[string]string{"project": "36", "task": "uid-caldav-test-parent-task-another-list"})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, rec.Result().StatusCode, 200)
|
||||
assert.Contains(t, rec.Body.String(), "UID:uid-caldav-test-parent-task-another-list")
|
||||
assert.Contains(t, rec.Body.String(), "RELATED-TO;RELTYPE=CHILD:uid-caldav-test-child-task-another-list")
|
||||
|
||||
rec, err = newCaldavTestRequestWithUser(t, e, http.MethodGet, caldav.TaskHandler, &testuser15, ``, nil, map[string]string{"project": "38", "task": "uid-caldav-test-child-task-another-list"})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, rec.Result().StatusCode, 200)
|
||||
assert.Contains(t, rec.Body.String(), "UID:uid-caldav-test-child-task-another-list")
|
||||
assert.Contains(t, rec.Body.String(), "RELATED-TO;RELTYPE=PARENT:uid-caldav-test-parent-task-another-list")
|
||||
})
|
||||
}
|
||||
|
@ -79,23 +79,36 @@ func setupTestEnv() (e *echo.Echo, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func bootstrapTestRequest(t *testing.T, method string, payload string, queryParam url.Values) (c echo.Context, rec *httptest.ResponseRecorder) {
|
||||
// Setup
|
||||
e, err := setupTestEnv()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Do the actual request
|
||||
func createRequest(e *echo.Echo, method string, payload string, queryParam url.Values, urlParams map[string]string) (c echo.Context, rec *httptest.ResponseRecorder) {
|
||||
req := httptest.NewRequest(method, "/", strings.NewReader(payload))
|
||||
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
|
||||
req.URL.RawQuery = queryParam.Encode()
|
||||
rec = httptest.NewRecorder()
|
||||
|
||||
c = e.NewContext(req, rec)
|
||||
var paramNames []string
|
||||
var paramValues []string
|
||||
for name, value := range urlParams {
|
||||
paramNames = append(paramNames, name)
|
||||
paramValues = append(paramValues, value)
|
||||
}
|
||||
c.SetParamNames(paramNames...)
|
||||
c.SetParamValues(paramValues...)
|
||||
return
|
||||
}
|
||||
|
||||
func bootstrapTestRequest(t *testing.T, method string, payload string, queryParam url.Values, urlParams map[string]string) (c echo.Context, rec *httptest.ResponseRecorder) {
|
||||
// Setup
|
||||
e, err := setupTestEnv()
|
||||
assert.NoError(t, err)
|
||||
|
||||
c, rec = createRequest(e, method, payload, queryParam, urlParams)
|
||||
return
|
||||
}
|
||||
|
||||
func newTestRequest(t *testing.T, method string, handler func(ctx echo.Context) error, payload string, queryParams url.Values, urlParams map[string]string) (rec *httptest.ResponseRecorder, err error) {
|
||||
rec, c := testRequestSetup(t, method, payload, queryParams, urlParams)
|
||||
var c echo.Context
|
||||
c, rec = bootstrapTestRequest(t, method, payload, queryParams, urlParams)
|
||||
err = handler(c)
|
||||
return
|
||||
}
|
||||
@ -124,36 +137,25 @@ func addLinkShareTokenToContext(t *testing.T, share *models.LinkSharing, c echo.
|
||||
c.Set("user", tken)
|
||||
}
|
||||
|
||||
func testRequestSetup(t *testing.T, method string, payload string, queryParams url.Values, urlParams map[string]string) (rec *httptest.ResponseRecorder, c echo.Context) {
|
||||
c, rec = bootstrapTestRequest(t, method, payload, queryParams)
|
||||
|
||||
var paramNames []string
|
||||
var paramValues []string
|
||||
for name, value := range urlParams {
|
||||
paramNames = append(paramNames, name)
|
||||
paramValues = append(paramValues, value)
|
||||
}
|
||||
c.SetParamNames(paramNames...)
|
||||
c.SetParamValues(paramValues...)
|
||||
return
|
||||
}
|
||||
|
||||
func newTestRequestWithUser(t *testing.T, method string, handler echo.HandlerFunc, user *user.User, payload string, queryParams url.Values, urlParams map[string]string) (rec *httptest.ResponseRecorder, err error) {
|
||||
rec, c := testRequestSetup(t, method, payload, queryParams, urlParams)
|
||||
var c echo.Context
|
||||
c, rec = bootstrapTestRequest(t, method, payload, queryParams, urlParams)
|
||||
addUserTokenToContext(t, user, c)
|
||||
err = handler(c)
|
||||
return
|
||||
}
|
||||
|
||||
func newTestRequestWithLinkShare(t *testing.T, method string, handler echo.HandlerFunc, share *models.LinkSharing, payload string, queryParams url.Values, urlParams map[string]string) (rec *httptest.ResponseRecorder, err error) {
|
||||
rec, c := testRequestSetup(t, method, payload, queryParams, urlParams)
|
||||
var c echo.Context
|
||||
c, rec = bootstrapTestRequest(t, method, payload, queryParams, urlParams)
|
||||
addLinkShareTokenToContext(t, share, c)
|
||||
err = handler(c)
|
||||
return
|
||||
}
|
||||
|
||||
func newCaldavTestRequestWithUser(t *testing.T, method string, handler echo.HandlerFunc, user *user.User, payload string, queryParams url.Values, urlParams map[string]string) (rec *httptest.ResponseRecorder, err error) {
|
||||
rec, c := testRequestSetup(t, method, payload, queryParams, urlParams)
|
||||
func newCaldavTestRequestWithUser(t *testing.T, e *echo.Echo, method string, handler echo.HandlerFunc, user *user.User, payload string, queryParams url.Values, urlParams map[string]string) (rec *httptest.ResponseRecorder, err error) {
|
||||
var c echo.Context
|
||||
c, rec = createRequest(e, method, payload, queryParams, urlParams)
|
||||
c.Request().Header.Set(echo.HeaderContentType, echo.MIMETextPlain)
|
||||
|
||||
result, _ := caldav.BasicAuth(user.Username, "1234", c)
|
||||
|
@ -147,7 +147,7 @@ func TestBucket_Delete(t *testing.T) {
|
||||
tasks := []*Task{}
|
||||
err = s.Where("bucket_id = ?", 1).Find(&tasks)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, tasks, 16)
|
||||
assert.Len(t, tasks, 15)
|
||||
db.AssertMissing(t, "buckets", map[string]interface{}{
|
||||
"id": 2,
|
||||
"project_id": 1,
|
||||
|
@ -373,7 +373,14 @@ func (bt *BulkTask) GetTasksByIDs(s *xorm.Session) (err error) {
|
||||
}
|
||||
|
||||
func GetTaskSimpleByUUID(s *xorm.Session, uid string) (task *Task, err error) {
|
||||
_, err = s.In("uid", uid).Get(task)
|
||||
var has bool
|
||||
task = &Task{}
|
||||
|
||||
has, err = s.In("uid", uid).Get(task)
|
||||
if !has || err != nil {
|
||||
return &Task{}, ErrTaskDoesNotExist{}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
484
pkg/routes/caldav/listStorageProvider_test.go
Normal file
484
pkg/routes/caldav/listStorageProvider_test.go
Normal file
@ -0,0 +1,484 @@
|
||||
// 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
|
||||
|
||||
// This file tests logic related to handling tasks in CALDAV format
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
"code.vikunja.io/api/pkg/files"
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
|
||||
"github.com/samedi/caldav-go/data"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// Check logic related to creating sub-tasks
|
||||
func TestSubTask_Create(t *testing.T) {
|
||||
u := &user.User{
|
||||
ID: 15,
|
||||
Username: "user15",
|
||||
Email: "user15@example.com",
|
||||
}
|
||||
|
||||
config.InitDefaultConfig()
|
||||
files.InitTests()
|
||||
user.InitTests()
|
||||
models.SetupTests()
|
||||
|
||||
//
|
||||
// Create a subtask
|
||||
//
|
||||
t.Run("create", func(t *testing.T) {
|
||||
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
const taskUID = "uid_child1"
|
||||
const taskContent = `BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
METHOD:PUBLISH
|
||||
X-PUBLISHED-TTL:PT4H
|
||||
X-WR-CALNAME:Project 36 for Caldav tests
|
||||
PRODID:-//Vikunja Todo App//EN
|
||||
BEGIN:VTODO
|
||||
UID:uid_child1
|
||||
DTSTAMP:20230301T073337Z
|
||||
SUMMARY:Caldav child task 1
|
||||
CREATED:20230301T073337Z
|
||||
LAST-MODIFIED:20230301T073337Z
|
||||
RELATED-TO;RELTYPE=PARENT:uid-caldav-test-parent-task
|
||||
END:VTODO
|
||||
END:VCALENDAR`
|
||||
|
||||
storage := &VikunjaCaldavProjectStorage{
|
||||
project: &models.ProjectWithTasksAndBuckets{Project: models.Project{ID: 36}},
|
||||
task: &models.Task{UID: taskUID},
|
||||
user: u,
|
||||
}
|
||||
|
||||
// Create the subtask:
|
||||
taskResource, err := storage.CreateResource(taskUID, taskContent)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Check that the result CALDAV contains the relation:
|
||||
content, _ := taskResource.GetContentData()
|
||||
assert.Contains(t, content, "UID:"+taskUID)
|
||||
assert.Contains(t, content, "RELATED-TO;RELTYPE=PARENT:uid-caldav-test-parent-task")
|
||||
|
||||
// Get the task from the DB:
|
||||
tasks, err := models.GetTasksByUIDs(s, []string{taskUID}, u)
|
||||
assert.NoError(t, err)
|
||||
task := tasks[0]
|
||||
|
||||
// Check that the parent-child relationship is present:
|
||||
assert.Len(t, task.RelatedTasks[models.RelationKindParenttask], 1)
|
||||
parentTask := task.RelatedTasks[models.RelationKindParenttask][0]
|
||||
assert.Equal(t, "uid-caldav-test-parent-task", parentTask.UID)
|
||||
})
|
||||
|
||||
//
|
||||
// Create a subtask on a subtask, i.e. create a grand-child
|
||||
//
|
||||
t.Run("create grandchild on child task", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
const taskUIDChild = "uid_child1"
|
||||
const taskContentChild = `BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
METHOD:PUBLISH
|
||||
X-PUBLISHED-TTL:PT4H
|
||||
X-WR-CALNAME:Project 36 for Caldav tests
|
||||
PRODID:-//Vikunja Todo App//EN
|
||||
BEGIN:VTODO
|
||||
UID:uid_child1
|
||||
DTSTAMP:20230301T073337Z
|
||||
SUMMARY:Caldav child task 1
|
||||
CREATED:20230301T073337Z
|
||||
LAST-MODIFIED:20230301T073337Z
|
||||
RELATED-TO;RELTYPE=PARENT:uid-caldav-test-parent-task
|
||||
END:VTODO
|
||||
END:VCALENDAR`
|
||||
|
||||
storage := &VikunjaCaldavProjectStorage{
|
||||
project: &models.ProjectWithTasksAndBuckets{Project: models.Project{ID: 36}},
|
||||
task: &models.Task{UID: taskUIDChild},
|
||||
user: u,
|
||||
}
|
||||
|
||||
// Create the subtask:
|
||||
_, err := storage.CreateResource(taskUIDChild, taskContentChild)
|
||||
assert.NoError(t, err)
|
||||
|
||||
const taskUID = "uid_grand_child1"
|
||||
const taskContent = `BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
METHOD:PUBLISH
|
||||
X-PUBLISHED-TTL:PT4H
|
||||
X-WR-CALNAME:Project 36 for Caldav tests
|
||||
PRODID:-//Vikunja Todo App//EN
|
||||
BEGIN:VTODO
|
||||
UID:uid_grand_child1
|
||||
DTSTAMP:20230301T073337Z
|
||||
SUMMARY:Caldav grand child task 1
|
||||
CREATED:20230301T073337Z
|
||||
LAST-MODIFIED:20230301T073337Z
|
||||
RELATED-TO;RELTYPE=PARENT:uid_child1
|
||||