Return iso dates for everything date related from the api (#130)

Remove traces of unix timestamp

Revert renaming reminder table column

Fix staticcheck

Remove unused table call

Add migration for renaming reminders table

Fix issues with using TimeStamp

Fix lint

Updated all created / updated fields to use TimeStamps

Add comments

Convert all created / updated fields to datetime

Add time util package

Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/api#130
This commit is contained in:
konrad 2020-02-08 12:48:49 +00:00
parent 1f99238019
commit db2d868eed
44 changed files with 573 additions and 450 deletions

View File

@ -26,6 +26,8 @@ service:
enableregistration: true enableregistration: true
# Whether to enable task attachments or not # Whether to enable task attachments or not
enabletaskattachments: true enabletaskattachments: true
# The time zone all timestamps are in
timezone: GMT
database: database:
# Database type to use. Supported types are mysql and sqlite. # Database type to use. Supported types are mysql and sqlite.

View File

@ -69,6 +69,8 @@ service:
enableregistration: true enableregistration: true
# Whether to enable task attachments or not # Whether to enable task attachments or not
enabletaskattachments: true enabletaskattachments: true
# The time zone all timestamps are in
timezone: GMT
database: database:
# Database type to use. Supported types are mysql and sqlite. # Database type to use. Supported types are mysql and sqlite.

View File

@ -17,6 +17,7 @@
package caldav package caldav
import ( import (
"code.vikunja.io/api/pkg/timeutil"
"code.vikunja.io/api/pkg/user" "code.vikunja.io/api/pkg/user"
"code.vikunja.io/api/pkg/utils" "code.vikunja.io/api/pkg/utils"
"fmt" "fmt"
@ -34,37 +35,37 @@ type Event struct {
UID string UID string
Alarms []Alarm Alarms []Alarm
TimestampUnix int64 Timestamp timeutil.TimeStamp
StartUnix int64 Start timeutil.TimeStamp
EndUnix int64 End timeutil.TimeStamp
} }
// Todo holds a single VTODO // Todo holds a single VTODO
type Todo struct { type Todo struct {
// Required // Required
TimestampUnix int64 Timestamp timeutil.TimeStamp
UID string UID string
// Optional // Optional
Summary string Summary string
Description string Description string
CompletedUnix int64 Completed timeutil.TimeStamp
Organizer *user.User Organizer *user.User
Priority int64 // 0-9, 1 is highest Priority int64 // 0-9, 1 is highest
RelatedToUID string RelatedToUID string
StartUnix int64 Start timeutil.TimeStamp
EndUnix int64 End timeutil.TimeStamp
DueDateUnix int64 DueDate timeutil.TimeStamp
Duration time.Duration Duration time.Duration
CreatedUnix int64 Created timeutil.TimeStamp
UpdatedUnix int64 // last-mod Updated timeutil.TimeStamp // last-mod
} }
// Alarm holds infos about an alarm from a caldav event // Alarm holds infos about an alarm from a caldav event
type Alarm struct { type Alarm struct {
TimeUnix int64 Time timeutil.TimeStamp
Description string Description string
} }
@ -86,7 +87,7 @@ PRODID:-//` + config.ProdID + `//EN`
for _, e := range events { for _, e := range events {
if e.UID == "" { if e.UID == "" {
e.UID = makeCalDavTimeFromUnixTime(e.TimestampUnix) + utils.Sha256(e.Summary) e.UID = makeCalDavTimeFromTimeStamp(e.Timestamp) + utils.Sha256(e.Summary)
} }
caldavevents += ` caldavevents += `
@ -94,9 +95,9 @@ BEGIN:VEVENT
UID:` + e.UID + ` UID:` + e.UID + `
SUMMARY:` + e.Summary + ` SUMMARY:` + e.Summary + `
DESCRIPTION:` + e.Description + ` DESCRIPTION:` + e.Description + `
DTSTAMP:` + makeCalDavTimeFromUnixTime(e.TimestampUnix) + ` DTSTAMP:` + makeCalDavTimeFromTimeStamp(e.Timestamp) + `
DTSTART:` + makeCalDavTimeFromUnixTime(e.StartUnix) + ` DTSTART:` + makeCalDavTimeFromTimeStamp(e.Start) + `
DTEND:` + makeCalDavTimeFromUnixTime(e.EndUnix) DTEND:` + makeCalDavTimeFromTimeStamp(e.End)
for _, a := range e.Alarms { for _, a := range e.Alarms {
if a.Description == "" { if a.Description == "" {
@ -105,7 +106,7 @@ DTEND:` + makeCalDavTimeFromUnixTime(e.EndUnix)
caldavevents += ` caldavevents += `
BEGIN:VALARM BEGIN:VALARM
TRIGGER:` + calcAlarmDateFromReminder(e.StartUnix, a.TimeUnix) + ` TRIGGER:` + calcAlarmDateFromReminder(e.Start, a.Time) + `
ACTION:DISPLAY ACTION:DISPLAY
DESCRIPTION:` + a.Description + ` DESCRIPTION:` + a.Description + `
END:VALARM` END:VALARM`
@ -131,30 +132,30 @@ PRODID:-//` + config.ProdID + `//EN`
for _, t := range todos { for _, t := range todos {
if t.UID == "" { if t.UID == "" {
t.UID = makeCalDavTimeFromUnixTime(t.TimestampUnix) + utils.Sha256(t.Summary) t.UID = makeCalDavTimeFromTimeStamp(t.Timestamp) + utils.Sha256(t.Summary)
} }
caldavtodos += ` caldavtodos += `
BEGIN:VTODO BEGIN:VTODO
UID:` + t.UID + ` UID:` + t.UID + `
DTSTAMP:` + makeCalDavTimeFromUnixTime(t.TimestampUnix) + ` DTSTAMP:` + makeCalDavTimeFromTimeStamp(t.Timestamp) + `
SUMMARY:` + t.Summary SUMMARY:` + t.Summary
if t.StartUnix != 0 { if t.Start != 0 {
caldavtodos += ` caldavtodos += `
DTSTART: ` + makeCalDavTimeFromUnixTime(t.StartUnix) DTSTART: ` + makeCalDavTimeFromTimeStamp(t.Start)
} }
if t.EndUnix != 0 { if t.End != 0 {
caldavtodos += ` caldavtodos += `
DTEND: ` + makeCalDavTimeFromUnixTime(t.EndUnix) DTEND: ` + makeCalDavTimeFromTimeStamp(t.End)
} }
if t.Description != "" { if t.Description != "" {
caldavtodos += ` caldavtodos += `
DESCRIPTION:` + t.Description DESCRIPTION:` + t.Description
} }
if t.CompletedUnix != 0 { if t.Completed != 0 {
caldavtodos += ` caldavtodos += `
COMPLETED: ` + makeCalDavTimeFromUnixTime(t.CompletedUnix) COMPLETED: ` + makeCalDavTimeFromTimeStamp(t.Completed)
} }
if t.Organizer != nil { if t.Organizer != nil {
caldavtodos += ` caldavtodos += `
@ -166,14 +167,14 @@ ORGANIZER;CN=:` + t.Organizer.Username
RELATED-TO:` + t.RelatedToUID RELATED-TO:` + t.RelatedToUID
} }
if t.DueDateUnix != 0 { if t.DueDate != 0 {
caldavtodos += ` caldavtodos += `
DUE:` + makeCalDavTimeFromUnixTime(t.DueDateUnix) DUE:` + makeCalDavTimeFromTimeStamp(t.DueDate)
} }
if t.CreatedUnix != 0 { if t.Created != 0 {
caldavtodos += ` caldavtodos += `
CREATED:` + makeCalDavTimeFromUnixTime(t.CreatedUnix) CREATED:` + makeCalDavTimeFromTimeStamp(t.Created)
} }
if t.Duration != 0 { if t.Duration != 0 {
@ -187,7 +188,7 @@ PRIORITY:` + strconv.Itoa(int(t.Priority))
} }
caldavtodos += ` caldavtodos += `
LAST-MODIFIED:` + makeCalDavTimeFromUnixTime(t.UpdatedUnix) LAST-MODIFIED:` + makeCalDavTimeFromTimeStamp(t.Updated)
caldavtodos += ` caldavtodos += `
END:VTODO` END:VTODO`
@ -199,13 +200,12 @@ END:VCALENDAR` // Need a line break
return return
} }
func makeCalDavTimeFromUnixTime(unixtime int64) (caldavtime string) { func makeCalDavTimeFromTimeStamp(ts timeutil.TimeStamp) (caldavtime string) {
tz, _ := time.LoadLocation("UTC") tz, _ := time.LoadLocation("UTC")
tm := time.Unix(unixtime, 0).In(tz) return ts.ToTime().In(tz).Format(DateFormat)
return tm.Format(DateFormat)
} }
func calcAlarmDateFromReminder(eventStartUnix, reminderUnix int64) (alarmTime string) { func calcAlarmDateFromReminder(eventStartUnix, reminderUnix timeutil.TimeStamp) (alarmTime string) {
if eventStartUnix > reminderUnix { if eventStartUnix > reminderUnix {
alarmTime += `-` alarmTime += `-`
} }

View File

@ -37,26 +37,26 @@ func TestParseEvents(t *testing.T) {
}, },
events: []*Event{ events: []*Event{
{ {
Summary: "Event #1", Summary: "Event #1",
Description: "Lorem Ipsum", Description: "Lorem Ipsum",
UID: "randommduid", UID: "randommduid",
TimestampUnix: 1543626724, Timestamp: 1543626724,
StartUnix: 1543626724, Start: 1543626724,
EndUnix: 1543627824, End: 1543627824,
}, },
{ {
Summary: "Event #2", Summary: "Event #2",
UID: "randommduidd", UID: "randommduidd",
TimestampUnix: 1543726724, Timestamp: 1543726724,
StartUnix: 1543726724, Start: 1543726724,
EndUnix: 1543738724, End: 1543738724,
}, },
{ {
Summary: "Event #3 with empty uid", Summary: "Event #3 with empty uid",
UID: "20181202T0600242aaef4a81d770c1e775e26bc5abebc87f1d3d7bffaa83", UID: "20181202T0600242aaef4a81d770c1e775e26bc5abebc87f1d3d7bffaa83",
TimestampUnix: 1543726824, Timestamp: 1543726824,
StartUnix: 1543726824, Start: 1543726824,
EndUnix: 1543727000, End: 1543727000,
}, },
}, },
}, },
@ -101,47 +101,47 @@ END:VCALENDAR`,
}, },
events: []*Event{ events: []*Event{
{ {
Summary: "Event #1", Summary: "Event #1",
Description: "Lorem Ipsum", Description: "Lorem Ipsum",
UID: "randommduid", UID: "randommduid",
TimestampUnix: 1543626724, Timestamp: 1543626724,
StartUnix: 1543626724, Start: 1543626724,
EndUnix: 1543627824, End: 1543627824,
Alarms: []Alarm{ Alarms: []Alarm{
{TimeUnix: 1543626524}, {Time: 1543626524},
{TimeUnix: 1543626224}, {Time: 1543626224},
{TimeUnix: 1543626024}, {Time: 1543626024},
}, },
}, },
{ {
Summary: "Event #2", Summary: "Event #2",
UID: "randommduidd", UID: "randommduidd",
TimestampUnix: 1543726724, Timestamp: 1543726724,
StartUnix: 1543726724, Start: 1543726724,
EndUnix: 1543738724, End: 1543738724,
Alarms: []Alarm{ Alarms: []Alarm{
{TimeUnix: 1543626524}, {Time: 1543626524},
{TimeUnix: 1543626224}, {Time: 1543626224},
{TimeUnix: 1543626024}, {Time: 1543626024},
}, },
}, },
{ {
Summary: "Event #3 with empty uid", Summary: "Event #3 with empty uid",
TimestampUnix: 1543726824, Timestamp: 1543726824,
StartUnix: 1543726824, Start: 1543726824,
EndUnix: 1543727000, End: 1543727000,
Alarms: []Alarm{ Alarms: []Alarm{
{TimeUnix: 1543626524}, {Time: 1543626524},
{TimeUnix: 1543626224}, {Time: 1543626224},
{TimeUnix: 1543626024}, {Time: 1543626024},
{TimeUnix: 1543826824}, {Time: 1543826824},
}, },
}, },
{ {
Summary: "Event #4 without any", Summary: "Event #4 without any",
TimestampUnix: 1543726824, Timestamp: 1543726824,
StartUnix: 1543726824, Start: 1543726824,
EndUnix: 1543727000, End: 1543727000,
}, },
}, },
}, },

View File

@ -44,6 +44,7 @@ const (
ServiceEnableLinkSharing Key = `service.enablelinksharing` ServiceEnableLinkSharing Key = `service.enablelinksharing`
ServiceEnableRegistration Key = `service.enableregistration` ServiceEnableRegistration Key = `service.enableregistration`
ServiceEnableTaskAttachments Key = `service.enabletaskattachments` ServiceEnableTaskAttachments Key = `service.enabletaskattachments`
ServiceTimeZone Key = `service.timezone`
DatabaseType Key = `database.type` DatabaseType Key = `database.type`
DatabaseHost Key = `database.host` DatabaseHost Key = `database.host`
@ -168,6 +169,7 @@ func InitDefaultConfig() {
ServiceEnableLinkSharing.setDefault(true) ServiceEnableLinkSharing.setDefault(true)
ServiceEnableRegistration.setDefault(true) ServiceEnableRegistration.setDefault(true)
ServiceEnableTaskAttachments.setDefault(true) ServiceEnableTaskAttachments.setDefault(true)
ServiceTimeZone.setDefault("GMT")
// Database // Database
DatabaseType.setDefault("sqlite") DatabaseType.setDefault("sqlite")

View File

@ -18,6 +18,7 @@ package files
import ( import (
"code.vikunja.io/api/pkg/config" "code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/timeutil"
"code.vikunja.io/web" "code.vikunja.io/web"
"github.com/c2h5oh/datasize" "github.com/c2h5oh/datasize"
"github.com/spf13/afero" "github.com/spf13/afero"
@ -35,8 +36,8 @@ type File struct {
Created time.Time `xorm:"-" json:"created"` Created time.Time `xorm:"-" json:"created"`
CreatedUnix int64 `xorm:"created" json:"-"` CreatedUnix timeutil.TimeStamp `xorm:"created" json:"-"`
CreatedByID int64 `xorm:"int(11) not null" json:"-"` CreatedByID int64 `xorm:"int(11) not null" json:"-"`
File afero.File `xorm:"-" json:"-"` File afero.File `xorm:"-" json:"-"`
// This ReadCloser is only used for migration purposes. Use with care! // This ReadCloser is only used for migration purposes. Use with care!
@ -65,7 +66,7 @@ func (f *File) LoadFileMetaByID() (err error) {
if !exists { if !exists {
return ErrFileDoesNotExist{FileID: f.ID} return ErrFileDoesNotExist{FileID: f.ID}
} }
f.Created = time.Unix(f.CreatedUnix, 0) f.Created = f.CreatedUnix.ToTime()
return return
} }

View File

@ -95,33 +95,33 @@ func TestTaskCollection(t *testing.T) {
t.Run("by priority", func(t *testing.T) { t.Run("by priority", func(t *testing.T) {
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}}, urlParams) rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}}, urlParams)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `{"id":33,"text":"task #33 with percent done","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":4,"text":"task #4 low prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":1,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"identifier":"test1-4","index":4,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":3,"text":"task #3 high prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":100,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}}]`) assert.Contains(t, rec.Body.String(), `{"id":33,"text":"task #33 with percent done","description":"","done":false,"doneAt":null,"dueDate":null,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":0,"startDate":null,"endDate":null,"assignees":null,"labels":null,"hexColor":"","percentDone":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":null,"updated":null}},{"id":4,"text":"task #4 low prio","description":"","done":false,"doneAt":null,"dueDate":null,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":1,"startDate":null,"endDate":null,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"identifier":"test1-4","index":4,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":null,"updated":null}},{"id":3,"text":"task #3 high prio","description":"","done":false,"doneAt":null,"dueDate":null,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":100,"startDate":null,"endDate":null,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":null,"updated":null}}]`)
}) })
t.Run("by priority desc", func(t *testing.T) { t.Run("by priority desc", func(t *testing.T) {
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}, "order_by": []string{"desc"}}, urlParams) rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}, "order_by": []string{"desc"}}, urlParams)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `[{"id":3,"text":"task #3 high prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":100,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":4,"text":"task #4 low prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":1,`) assert.Contains(t, rec.Body.String(), `[{"id":3,"text":"task #3 high prio","description":"","done":false,"doneAt":null,"dueDate":null,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":100,"startDate":null,"endDate":null,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":null,"updated":null}},{"id":4,"text":"task #4 low prio","description":"","done":false,"doneAt":null,"dueDate":null,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":1`)
}) })
t.Run("by priority asc", func(t *testing.T) { t.Run("by priority asc", func(t *testing.T) {
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}, "order_by": []string{"asc"}}, urlParams) rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}, "order_by": []string{"asc"}}, urlParams)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `{"id":33,"text":"task #33 with percent done","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":4,"text":"task #4 low prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":1,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"identifier":"test1-4","index":4,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":3,"text":"task #3 high prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":100,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}}]`) assert.Contains(t, rec.Body.String(), `{"id":33,"text":"task #33 with percent done","description":"","done":false,"doneAt":null,"dueDate":null,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":0,"startDate":null,"endDate":null,"assignees":null,"labels":null,"hexColor":"","percentDone":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":null,"updated":null}},{"id":4,"text":"task #4 low prio","description":"","done":false,"doneAt":null,"dueDate":null,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":1,"startDate":null,"endDate":null,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"identifier":"test1-4","index":4,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":null,"updated":null}},{"id":3,"text":"task #3 high prio","description":"","done":false,"doneAt":null,"dueDate":null,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":100,"startDate":null,"endDate":null,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":null,"updated":null}}]`)
}) })
// should equal duedate asc // should equal duedate asc
t.Run("by duedate", func(t *testing.T) { t.Run("by duedate", func(t *testing.T) {
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date_unix"}}, urlParams) rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date_unix"}}, urlParams)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `{"id":6,"text":"task #6 lower due date","description":"","done":false,"doneAt":0,"dueDate":1543616724,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":5,"text":"task #5 higher due date","description":"","done":false,"doneAt":0,"dueDate":1543636724,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}}]`) assert.Contains(t, rec.Body.String(), `{"id":6,"text":"task #6 lower due date","description":"","done":false,"doneAt":null,"dueDate":"2018-11-30T22:25:24Z","reminderDates":null,"listID":1,"repeatAfter":0,"priority":0,"startDate":null,"endDate":null,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":null,"updated":null}},{"id":5,"text":"task #5 higher due date","description":"","done":false,"doneAt":null,"dueDate":"2018-12-01T03:58:44Z","reminderDates":null,"listID":1,"repeatAfter":0,"priority":0,"startDate":null,"endDate":null,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":null,"updated":null}}]`)
}) })
t.Run("by duedate desc", func(t *testing.T) { t.Run("by duedate desc", func(t *testing.T) {
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date_unix"}, "order_by": []string{"desc"}}, urlParams) rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date_unix"}, "order_by": []string{"desc"}}, urlParams)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `[{"id":5,"text":"task #5 higher due date","description":"","done":false,"doneAt":0,"dueDate":1543636724,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":6,"text":"task #6 lower due date`) assert.Contains(t, rec.Body.String(), `[{"id":5,"text":"task #5 higher due date","description":"","done":false,"doneAt":null,"dueDate":"2018-12-01T03:58:44Z","reminderDates":null,"listID":1,"repeatAfter":0,"priority":0,"startDate":null,"endDate":null,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":null,"updated":null}},{"id":6,"text":"task #6 lower due date`)
}) })
t.Run("by duedate asc", func(t *testing.T) { t.Run("by duedate asc", func(t *testing.T) {
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date_unix"}, "order_by": []string{"asc"}}, urlParams) rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date_unix"}, "order_by": []string{"asc"}}, urlParams)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `{"id":6,"text":"task #6 lower due date","description":"","done":false,"doneAt":0,"dueDate":1543616724,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":5,"text":"task #5 higher due date","description":"","done":false,"doneAt":0,"dueDate":1543636724,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}}]`) assert.Contains(t, rec.Body.String(), `{"id":6,"text":"task #6 lower due date","description":"","done":false,"doneAt":null,"dueDate":"2018-11-30T22:25:24Z","reminderDates":null,"listID":1,"repeatAfter":0,"priority":0,"startDate":null,"endDate":null,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":null,"updated":null}},{"id":5,"text":"task #5 higher due date","description":"","done":false,"doneAt":null,"dueDate":"2018-12-01T03:58:44Z","reminderDates":null,"listID":1,"repeatAfter":0,"priority":0,"startDate":null,"endDate":null,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":null,"updated":null}}]`)
}) })
t.Run("invalid sort parameter", func(t *testing.T) { t.Run("invalid sort parameter", func(t *testing.T) {
_, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"loremipsum"}}, urlParams) _, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"loremipsum"}}, urlParams)
@ -249,33 +249,33 @@ func TestTaskCollection(t *testing.T) {
t.Run("by priority", func(t *testing.T) { t.Run("by priority", func(t *testing.T) {
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}}, nil) rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}}, nil)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `{"id":33,"text":"task #33 with percent done","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":4,"text":"task #4 low prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":1,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"identifier":"test1-4","index":4,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":3,"text":"task #3 high prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":100,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}}]`) assert.Contains(t, rec.Body.String(), `{"id":33,"text":"task #33 with percent done","description":"","done":false,"doneAt":null,"dueDate":null,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":0,"startDate":null,"endDate":null,"assignees":null,"labels":null,"hexColor":"","percentDone":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":null,"updated":null}},{"id":4,"text":"task #4 low prio","description":"","done":false,"doneAt":null,"dueDate":null,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":1,"startDate":null,"endDate":null,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"identifier":"test1-4","index":4,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":null,"updated":null}},{"id":3,"text":"task #3 high prio","description":"","done":false,"doneAt":null,"dueDate":null,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":100,"startDate":null,"endDate":null,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":null,"updated":null}}]`)
}) })
t.Run("by priority desc", func(t *testing.T) { t.Run("by priority desc", func(t *testing.T) {
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}, "order_by": []string{"desc"}}, nil) rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}, "order_by": []string{"desc"}}, nil)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `[{"id":3,"text":"task #3 high prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":100,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":4,"text":"task #4 low prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":1,`) assert.Contains(t, rec.Body.String(), `[{"id":3,"text":"task #3 high prio","description":"","done":false,"doneAt":null,"dueDate":null,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":100,"startDate":null,"endDate":null,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":null,"updated":null}},{"id":4,"text":"task #4 low prio","description":"","done":false,"doneAt":null,"dueDate":null,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":1`)
}) })
t.Run("by priority asc", func(t *testing.T) { t.Run("by priority asc", func(t *testing.T) {
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}, "order_by": []string{"asc"}}, nil) rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"priority"}, "order_by": []string{"asc"}}, nil)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `{"id":33,"text":"task #33 with percent done","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":4,"text":"task #4 low prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":1,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"identifier":"test1-4","index":4,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":3,"text":"task #3 high prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":100,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}}]`) assert.Contains(t, rec.Body.String(), `{"id":33,"text":"task #33 with percent done","description":"","done":false,"doneAt":null,"dueDate":null,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":0,"startDate":null,"endDate":null,"assignees":null,"labels":null,"hexColor":"","percentDone":0.5,"identifier":"test1-17","index":17,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":null,"updated":null}},{"id":4,"text":"task #4 low prio","description":"","done":false,"doneAt":null,"dueDate":null,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":1,"startDate":null,"endDate":null,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"identifier":"test1-4","index":4,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":null,"updated":null}},{"id":3,"text":"task #3 high prio","description":"","done":false,"doneAt":null,"dueDate":null,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":100,"startDate":null,"endDate":null,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"identifier":"test1-3","index":3,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":null,"updated":null}}]`)
}) })
// should equal duedate asc // should equal duedate asc
t.Run("by duedate", func(t *testing.T) { t.Run("by duedate", func(t *testing.T) {
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date_unix"}}, nil) rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date_unix"}}, nil)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `{"id":6,"text":"task #6 lower due date","description":"","done":false,"doneAt":0,"dueDate":1543616724,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":5,"text":"task #5 higher due date","description":"","done":false,"doneAt":0,"dueDate":1543636724,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}}]`) assert.Contains(t, rec.Body.String(), `{"id":6,"text":"task #6 lower due date","description":"","done":false,"doneAt":null,"dueDate":"2018-11-30T22:25:24Z","reminderDates":null,"listID":1,"repeatAfter":0,"priority":0,"startDate":null,"endDate":null,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":null,"updated":null}},{"id":5,"text":"task #5 higher due date","description":"","done":false,"doneAt":null,"dueDate":"2018-12-01T03:58:44Z","reminderDates":null,"listID":1,"repeatAfter":0,"priority":0,"startDate":null,"endDate":null,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":null,"updated":null}}]`)
}) })
t.Run("by duedate desc", func(t *testing.T) { t.Run("by duedate desc", func(t *testing.T) {
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date_unix"}, "order_by": []string{"desc"}}, nil) rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date_unix"}, "order_by": []string{"desc"}}, nil)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `[{"id":5,"text":"task #5 higher due date","description":"","done":false,"doneAt":0,"dueDate":1543636724,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":6,"text":"task #6 lower due date"`) assert.Contains(t, rec.Body.String(), `[{"id":5,"text":"task #5 higher due date","description":"","done":false,"doneAt":null,"dueDate":"2018-12-01T03:58:44Z","reminderDates":null,"listID":1,"repeatAfter":0,"priority":0,"startDate":null,"endDate":null,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":null,"updated":null}},{"id":6,"text":"task #6 lower due date`)
}) })
t.Run("by duedate asc", func(t *testing.T) { t.Run("by duedate asc", func(t *testing.T) {
rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date_unix"}, "order_by": []string{"asc"}}, nil) rec, err := testHandler.testReadAllWithUser(url.Values{"sort_by": []string{"due_date_unix"}, "order_by": []string{"asc"}}, nil)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `{"id":6,"text":"task #6 lower due date","description":"","done":false,"doneAt":0,"dueDate":1543616724,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":5,"text":"task #5 higher due date","description":"","done":false,"doneAt":0,"dueDate":1543636724,"reminderDates":null,"listID":1,"repeatAfter":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}}]`) assert.Contains(t, rec.Body.String(), `{"id":6,"text":"task #6 lower due date","description":"","done":false,"doneAt":null,"dueDate":"2018-11-30T22:25:24Z","reminderDates":null,"listID":1,"repeatAfter":0,"priority":0,"startDate":null,"endDate":null,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"identifier":"test1-6","index":6,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":null,"updated":null}},{"id":5,"text":"task #5 higher due date","description":"","done":false,"doneAt":null,"dueDate":"2018-12-01T03:58:44Z","reminderDates":null,"listID":1,"repeatAfter":0,"priority":0,"startDate":null,"endDate":null,"assignees":null,"labels":null,"hexColor":"","percentDone":0,"identifier":"test1-5","index":5,"related_tasks":{},"attachments":null,"created":"2018-12-01T01:12:04Z","updated":"2018-12-01T01:12:04Z","createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":null,"updated":null}}]`)
}) })
t.Run("invalid parameter", func(t *testing.T) { t.Run("invalid parameter", func(t *testing.T) {
// Invalid parameter should not sort at all // Invalid parameter should not sort at all

View File

@ -67,21 +67,21 @@ func TestTask(t *testing.T) {
assert.NotContains(t, rec.Body.String(), `"done":true`) assert.NotContains(t, rec.Body.String(), `"done":true`)
}) })
t.Run("Due date", func(t *testing.T) { t.Run("Due date", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"dueDate": 123456}`) rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"dueDate": "2020-02-10T10:00:00Z"}`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"dueDate":123456`) assert.Contains(t, rec.Body.String(), `"dueDate":"2020-02-10T10:00:00Z"`)
assert.NotContains(t, rec.Body.String(), `"dueDate":0`) assert.NotContains(t, rec.Body.String(), `"dueDate":0`)
}) })
t.Run("Due date unset", func(t *testing.T) { t.Run("Due date unset", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "5"}, `{"dueDate": 0}`) rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "5"}, `{"dueDate": null}`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"dueDate":0`) assert.Contains(t, rec.Body.String(), `"dueDate":null`)
assert.NotContains(t, rec.Body.String(), `"dueDate":1543636724`) assert.NotContains(t, rec.Body.String(), `"dueDate":"2020-02-10T10:00:00Z"`)
}) })
t.Run("Reminders", func(t *testing.T) { t.Run("Reminders", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"reminderDates": [1555508227,1555511000]}`) rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"reminderDates": ["2020-02-10T10:00:00Z","2020-02-11T10:00:00Z"]}`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"reminderDates":[1555508227,1555511000]`) assert.Contains(t, rec.Body.String(), `"reminderDates":["2020-02-10T10:00:00Z","2020-02-11T10:00:00Z"]`)
assert.NotContains(t, rec.Body.String(), `"reminderDates": null`) assert.NotContains(t, rec.Body.String(), `"reminderDates": null`)
}) })
t.Run("Reminders unset to empty array", func(t *testing.T) { t.Run("Reminders unset to empty array", func(t *testing.T) {
@ -145,28 +145,28 @@ func TestTask(t *testing.T) {
assert.NotContains(t, rec.Body.String(), `"priority":100`) assert.NotContains(t, rec.Body.String(), `"priority":100`)
}) })
t.Run("Start date", func(t *testing.T) { t.Run("Start date", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"startDate":1234567}`) rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"startDate":"2020-02-10T10:00:00Z"}`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"startDate":1234567`) assert.Contains(t, rec.Body.String(), `"startDate":"2020-02-10T10:00:00Z"`)
assert.NotContains(t, rec.Body.String(), `"startDate":0`) assert.NotContains(t, rec.Body.String(), `"startDate":0`)
}) })
t.Run("Start date unset", func(t *testing.T) { t.Run("Start date unset", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "7"}, `{"startDate":0}`) rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "7"}, `{"startDate":null}`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"startDate":0`) assert.Contains(t, rec.Body.String(), `"startDate":null`)
assert.NotContains(t, rec.Body.String(), `"startDate":1544600000`) assert.NotContains(t, rec.Body.String(), `"startDate":"2020-02-10T10:00:00Z"`)
}) })
t.Run("End date", func(t *testing.T) { t.Run("End date", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"endDate":123456}`) rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"endDate":"2020-02-10T12:00:00Z"}`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"endDate":123456`) assert.Contains(t, rec.Body.String(), `"endDate":"2020-02-10T12:00:00Z"`)
assert.NotContains(t, rec.Body.String(), `"endDate":0`) assert.NotContains(t, rec.Body.String(), `"endDate":""`)
}) })
t.Run("End date unset", func(t *testing.T) { t.Run("End date unset", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "8"}, `{"endDate":0}`) rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "8"}, `{"endDate":null}`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"endDate":0`) assert.Contains(t, rec.Body.String(), `"endDate":null`)
assert.NotContains(t, rec.Body.String(), `"endDate":1544700000`) assert.NotContains(t, rec.Body.String(), `"endDate":"2020-02-10T10:00:00Z"`)
}) })
t.Run("Color", func(t *testing.T) { t.Run("Color", func(t *testing.T) {
rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"hexColor":"f0f0f0"}`) rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"hexColor":"f0f0f0"}`)

View File

@ -17,6 +17,7 @@
package models package models
import ( import (
"code.vikunja.io/api/pkg/timeutil"
"code.vikunja.io/api/pkg/user" "code.vikunja.io/api/pkg/user"
"code.vikunja.io/web" "code.vikunja.io/web"
"time" "time"
@ -37,10 +38,10 @@ type Label struct {
// The user who created this label // The user who created this label
CreatedBy *user.User `xorm:"-" json:"created_by"` CreatedBy *user.User `xorm:"-" json:"created_by"`
// A unix timestamp when this label was created. You cannot change this value. // A timestamp when this label was created. You cannot change this value.
Created int64 `xorm:"created not null" json:"created"` Created timeutil.TimeStamp `xorm:"created not null" json:"created"`
// A unix timestamp when this label was last updated. You cannot change this value. // A timestamp when this label was last updated. You cannot change this value.
Updated int64 `xorm:"updated not null" json:"updated"` Updated timeutil.TimeStamp `xorm:"updated not null" json:"updated"`
web.CRUDable `xorm:"-" json:"-"` web.CRUDable `xorm:"-" json:"-"`
web.Rights `xorm:"-" json:"-"` web.Rights `xorm:"-" json:"-"`

View File

@ -17,6 +17,7 @@
package models package models
import ( import (
"code.vikunja.io/api/pkg/timeutil"
"code.vikunja.io/api/pkg/user" "code.vikunja.io/api/pkg/user"
"code.vikunja.io/web" "code.vikunja.io/web"
"github.com/go-xorm/builder" "github.com/go-xorm/builder"
@ -29,8 +30,8 @@ type LabelTask struct {
TaskID int64 `xorm:"int(11) INDEX not null" json:"-" param:"listtask"` TaskID int64 `xorm:"int(11) INDEX not null" json:"-" param:"listtask"`
// The label id you want to associate with a task. // The label id you want to associate with a task.
LabelID int64 `xorm:"int(11) INDEX not null" json:"label_id" param:"label"` LabelID int64 `xorm:"int(11) INDEX not null" json:"label_id" param:"label"`
// A unix timestamp when this task was created. You cannot change this value. // A timestamp when this task was created. You cannot change this value.
Created int64 `xorm:"created not null" json:"created"` Created timeutil.TimeStamp `xorm:"created not null" json:"created"`
web.CRUDable `xorm:"-" json:"-"` web.CRUDable `xorm:"-" json:"-"`
web.Rights `xorm:"-" json:"-"` web.Rights `xorm:"-" json:"-"`

View File

@ -2,6 +2,7 @@ package models
import ( import (
"code.vikunja.io/api/pkg/db" "code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/timeutil"
"code.vikunja.io/api/pkg/user" "code.vikunja.io/api/pkg/user"
"gopkg.in/d4l3k/messagediff.v1" "gopkg.in/d4l3k/messagediff.v1"
"reflect" "reflect"
@ -16,7 +17,7 @@ func TestLabelTask_ReadAll(t *testing.T) {
ID int64 ID int64
TaskID int64 TaskID int64
LabelID int64 LabelID int64
Created int64 Created timeutil.TimeStamp
CRUDable web.CRUDable CRUDable web.CRUDable
Rights web.Rights Rights web.Rights
} }
@ -113,7 +114,7 @@ func TestLabelTask_Create(t *testing.T) {
ID int64 ID int64
TaskID int64 TaskID int64
LabelID int64 LabelID int64
Created int64 Created timeutil.TimeStamp
CRUDable web.CRUDable CRUDable web.CRUDable
Rights web.Rights Rights web.Rights
} }
@ -207,7 +208,7 @@ func TestLabelTask_Delete(t *testing.T) {
ID int64 ID int64
TaskID int64 TaskID int64
LabelID int64 LabelID int64
Created int64 Created timeutil.TimeStamp
CRUDable web.CRUDable CRUDable web.CRUDable
Rights web.Rights Rights web.Rights
} }

View File

@ -17,6 +17,7 @@
package models package models
import ( import (
"code.vikunja.io/api/pkg/timeutil"
"code.vikunja.io/api/pkg/user" "code.vikunja.io/api/pkg/user"
"gopkg.in/d4l3k/messagediff.v1" "gopkg.in/d4l3k/messagediff.v1"
"reflect" "reflect"
@ -34,8 +35,8 @@ func TestLabel_ReadAll(t *testing.T) {
HexColor string HexColor string
CreatedByID int64 CreatedByID int64
CreatedBy *user.User CreatedBy *user.User
Created int64 Created timeutil.TimeStamp
Updated int64 Updated timeutil.TimeStamp
CRUDable web.CRUDable CRUDable web.CRUDable
Rights web.Rights Rights web.Rights
} }
@ -138,8 +139,8 @@ func TestLabel_ReadOne(t *testing.T) {
HexColor string HexColor string
CreatedByID int64 CreatedByID int64
CreatedBy *user.User CreatedBy *user.User
Created int64 Created timeutil.TimeStamp
Updated int64 Updated timeutil.TimeStamp
CRUDable web.CRUDable CRUDable web.CRUDable
Rights web.Rights Rights web.Rights
} }
@ -250,8 +251,8 @@ func TestLabel_Create(t *testing.T) {
HexColor string HexColor string
CreatedByID int64 CreatedByID int64
CreatedBy *user.User CreatedBy *user.User
Created int64 Created timeutil.TimeStamp
Updated int64 Updated timeutil.TimeStamp
CRUDable web.CRUDable CRUDable web.CRUDable
Rights web.Rights Rights web.Rights
} }
@ -310,8 +311,8 @@ func TestLabel_Update(t *testing.T) {
HexColor string HexColor string
CreatedByID int64 CreatedByID int64
CreatedBy *user.User CreatedBy *user.User
Created int64 Created timeutil.TimeStamp
Updated int64 Updated timeutil.TimeStamp
CRUDable web.CRUDable CRUDable web.CRUDable
Rights web.Rights Rights web.Rights
} }
@ -392,8 +393,8 @@ func TestLabel_Delete(t *testing.T) {
HexColor string HexColor string
CreatedByID int64 CreatedByID int64
CreatedBy *user.User CreatedBy *user.User
Created int64 Created timeutil.TimeStamp
Updated int64 Updated timeutil.TimeStamp
CRUDable web.CRUDable CRUDable web.CRUDable
Rights web.Rights Rights web.Rights
} }

View File

@ -18,6 +18,7 @@
package models package models
import ( import (
"code.vikunja.io/api/pkg/timeutil"
"code.vikunja.io/api/pkg/user" "code.vikunja.io/api/pkg/user"
"code.vikunja.io/api/pkg/utils" "code.vikunja.io/api/pkg/utils"
"code.vikunja.io/web" "code.vikunja.io/web"
@ -52,10 +53,10 @@ type LinkSharing struct {
SharedBy *user.User `xorm:"-" json:"shared_by"` SharedBy *user.User `xorm:"-" json:"shared_by"`
SharedByID int64 `xorm:"int(11) INDEX not null" json:"-"` SharedByID int64 `xorm:"int(11) INDEX not null" json:"-"`
// A unix timestamp when this list was shared. You cannot change this value. // A timestamp when this list was shared. You cannot change this value.
Created int64 `xorm:"created not null" json:"created"` Created timeutil.TimeStamp `xorm:"created not null" json:"created"`
// A unix timestamp when this share was last updated. You cannot change this value. // A timestamp when this share was last updated. You cannot change this value.
Updated int64 `xorm:"updated not null" json:"updated"` Updated timeutil.TimeStamp `xorm:"updated not null" json:"updated"`
web.CRUDable `xorm:"-" json:"-"` web.CRUDable `xorm:"-" json:"-"`
web.Rights `xorm:"-" json:"-"` web.Rights `xorm:"-" json:"-"`

View File

@ -18,6 +18,7 @@ package models
import ( import (
"code.vikunja.io/api/pkg/metrics" "code.vikunja.io/api/pkg/metrics"
"code.vikunja.io/api/pkg/timeutil"
"code.vikunja.io/api/pkg/user" "code.vikunja.io/api/pkg/user"
"code.vikunja.io/web" "code.vikunja.io/web"
) )
@ -42,10 +43,10 @@ type List struct {
// Deprecated: you should use the dedicated task list endpoint because it has support for pagination and filtering // Deprecated: you should use the dedicated task list endpoint because it has support for pagination and filtering
Tasks []*Task `xorm:"-" json:"-"` Tasks []*Task `xorm:"-" json:"-"`
// A unix timestamp when this list was created. You cannot change this value. // A timestamp when this list was created. You cannot change this value.
Created int64 `xorm:"created not null" json:"created"` Created timeutil.TimeStamp `xorm:"created not null" json:"created"`
// A unix timestamp when this list was last updated. You cannot change this value. // A timestamp when this list was last updated. You cannot change this value.
Updated int64 `xorm:"updated not null" json:"updated"` Updated timeutil.TimeStamp `xorm:"updated not null" json:"updated"`
web.CRUDable `xorm:"-" json:"-"` web.CRUDable `xorm:"-" json:"-"`
web.Rights `xorm:"-" json:"-"` web.Rights `xorm:"-" json:"-"`

View File

@ -16,7 +16,10 @@
package models package models
import "code.vikunja.io/web" import (
"code.vikunja.io/api/pkg/timeutil"
"code.vikunja.io/web"
)
// TeamList defines the relation between a team and a list // TeamList defines the relation between a team and a list
type TeamList struct { type TeamList struct {
@ -29,10 +32,10 @@ type TeamList struct {
// The right this team has. 0 = Read only, 1 = Read & Write, 2 = Admin. See the docs for more details. // The right this team has. 0 = Read only, 1 = Read & Write, 2 = Admin. See the docs for more details.
Right Right `xorm:"int(11) INDEX not null default 0" json:"right" valid:"length(0|2)" maximum:"2" default:"0"` Right Right `xorm:"int(11) INDEX not null default 0" json:"right" valid:"length(0|2)" maximum:"2" default:"0"`
// A unix timestamp when this relation was created. You cannot change this value. // A timestamp when this relation was created. You cannot change this value.
Created int64 `xorm:"created not null" json:"created"` Created timeutil.TimeStamp `xorm:"created not null" json:"created"`
// A unix timestamp when this relation was last updated. You cannot change this value. // A timestamp when this relation was last updated. You cannot change this value.
Updated int64 `xorm:"updated not null" json:"updated"` Updated timeutil.TimeStamp `xorm:"updated not null" json:"updated"`
web.CRUDable `xorm:"-" json:"-"` web.CRUDable `xorm:"-" json:"-"`
web.Rights `xorm:"-" json:"-"` web.Rights `xorm:"-" json:"-"`

View File

@ -18,6 +18,7 @@ package models
import ( import (
"code.vikunja.io/api/pkg/db" "code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/timeutil"
"code.vikunja.io/api/pkg/user" "code.vikunja.io/api/pkg/user"
"code.vikunja.io/web" "code.vikunja.io/web"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -120,8 +121,8 @@ func TestTeamList_Update(t *testing.T) {
TeamID int64 TeamID int64
ListID int64 ListID int64
Right Right Right Right
Created int64 Created timeutil.TimeStamp
Updated int64 Updated timeutil.TimeStamp
CRUDable web.CRUDable CRUDable web.CRUDable
Rights web.Rights Rights web.Rights
} }

View File

@ -17,6 +17,7 @@
package models package models
import ( import (
"code.vikunja.io/api/pkg/timeutil"
"code.vikunja.io/api/pkg/user" "code.vikunja.io/api/pkg/user"
"code.vikunja.io/web" "code.vikunja.io/web"
) )
@ -34,10 +35,10 @@ type ListUser struct {
// The right this user has. 0 = Read only, 1 = Read & Write, 2 = Admin. See the docs for more details. // The right this user has. 0 = Read only, 1 = Read & Write, 2 = Admin. See the docs for more details.
Right Right `xorm:"int(11) INDEX not null default 0" json:"right" valid:"length(0|2)" maximum:"2" default:"0"` Right Right `xorm:"int(11) INDEX not null default 0" json:"right" valid:"length(0|2)" maximum:"2" default:"0"`
// A unix timestamp when this relation was created. You cannot change this value. // A timestamp when this relation was created. You cannot change this value.
Created int64 `xorm:"created not null" json:"created"` Created timeutil.TimeStamp `xorm:"created not null" json:"created"`
// A unix timestamp when this relation was last updated. You cannot change this value. // A timestamp when this relation was last updated. You cannot change this value.
Updated int64 `xorm:"updated not null" json:"updated"` Updated timeutil.TimeStamp `xorm:"updated not null" json:"updated"`
web.CRUDable `xorm:"-" json:"-"` web.CRUDable `xorm:"-" json:"-"`
web.Rights `xorm:"-" json:"-"` web.Rights `xorm:"-" json:"-"`

View File

@ -18,6 +18,7 @@ package models
import ( import (
"code.vikunja.io/api/pkg/db" "code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/timeutil"
"code.vikunja.io/api/pkg/user" "code.vikunja.io/api/pkg/user"
"testing" "testing"
@ -30,8 +31,8 @@ func TestListUser_CanDoSomething(t *testing.T) {
UserID int64 UserID int64
ListID int64 ListID int64
Right Right Right Right
Created int64 Created timeutil.TimeStamp
Updated int64 Updated timeutil.TimeStamp
CRUDable web.CRUDable CRUDable web.CRUDable
Rights web.Rights Rights web.Rights
} }

View File

@ -18,6 +18,7 @@ package models
import ( import (
"code.vikunja.io/api/pkg/db" "code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/timeutil"
"code.vikunja.io/api/pkg/user" "code.vikunja.io/api/pkg/user"
"gopkg.in/d4l3k/messagediff.v1" "gopkg.in/d4l3k/messagediff.v1"
"reflect" "reflect"
@ -34,8 +35,8 @@ func TestListUser_Create(t *testing.T) {
Username string Username string
ListID int64 ListID int64
Right Right Right Right
Created int64 Created timeutil.TimeStamp
Updated int64 Updated timeutil.TimeStamp
CRUDable web.CRUDable CRUDable web.CRUDable
Rights web.Rights Rights web.Rights
} }
@ -135,8 +136,8 @@ func TestListUser_ReadAll(t *testing.T) {
UserID int64 UserID int64
ListID int64 ListID int64
Right Right Right Right
Created int64 Created timeutil.TimeStamp
Updated int64 Updated timeutil.TimeStamp
CRUDable web.CRUDable CRUDable web.CRUDable
Rights web.Rights Rights web.Rights
} }
@ -229,8 +230,8 @@ func TestListUser_Update(t *testing.T) {
Username string Username string
ListID int64 ListID int64
Right Right Right Right
Created int64 Created timeutil.TimeStamp
Updated int64 Updated timeutil.TimeStamp
CRUDable web.CRUDable CRUDable web.CRUDable
Rights web.Rights Rights web.Rights
} }
@ -306,8 +307,8 @@ func TestListUser_Delete(t *testing.T) {
Username string Username string
ListID int64 ListID int64
Right Right Right Right
Created int64 Created timeutil.TimeStamp
Updated int64 Updated timeutil.TimeStamp
CRUDable web.CRUDable CRUDable web.CRUDable
Rights web.Rights Rights web.Rights
} }

View File

@ -18,6 +18,7 @@ package models
import ( import (
"code.vikunja.io/api/pkg/metrics" "code.vikunja.io/api/pkg/metrics"
"code.vikunja.io/api/pkg/timeutil"
"code.vikunja.io/api/pkg/user" "code.vikunja.io/api/pkg/user"
"code.vikunja.io/web" "code.vikunja.io/web"
"github.com/imdario/mergo" "github.com/imdario/mergo"
@ -37,10 +38,10 @@ type Namespace struct {
// The user who owns this namespace // The user who owns this namespace
Owner *user.User `xorm:"-" json:"owner" valid:"-"` Owner *user.User `xorm:"-" json:"owner" valid:"-"`
// A unix timestamp when this namespace was created. You cannot change this value. // A timestamp when this namespace was created. You cannot change this value.
Created int64 `xorm:"created not null" json:"created"` Created timeutil.TimeStamp `xorm:"created not null" json:"created"`
// A unix timestamp when this namespace was last updated. You cannot change this value. // A timestamp when this namespace was last updated. You cannot change this value.
Updated int64 `xorm:"updated not null" json:"updated"` Updated timeutil.TimeStamp `xorm:"updated not null" json:"updated"`
web.CRUDable `xorm:"-" json:"-"` web.CRUDable `xorm:"-" json:"-"`
web.Rights `xorm:"-" json:"-"` web.Rights `xorm:"-" json:"-"`
@ -51,8 +52,8 @@ var PseudoNamespace = Namespace{
ID: -1, ID: -1,
Name: "Shared Lists", Name: "Shared Lists",
Description: "Lists of other users shared with you via teams or directly.", Description: "Lists of other users shared with you via teams or directly.",
Created: time.Now().Unix(), Created: timeutil.FromTime(time.Now()),
Updated: time.Now().Unix(), Updated: timeutil.FromTime(time.Now()),
} }
// TableName makes beautiful table names // TableName makes beautiful table names

View File

@ -16,7 +16,10 @@
package models package models
import "code.vikunja.io/web" import (
"code.vikunja.io/api/pkg/timeutil"
"code.vikunja.io/web"
)
// TeamNamespace defines the relationship between a Team and a Namespace // TeamNamespace defines the relationship between a Team and a Namespace
type TeamNamespace struct { type TeamNamespace struct {
@ -29,10 +32,10 @@ type TeamNamespace struct {
// The right this team has. 0 = Read only, 1 = Read & Write, 2 = Admin. See the docs for more details. // The right this team has. 0 = Read only, 1 = Read & Write, 2 = Admin. See the docs for more details.
Right Right `xorm:"int(11) INDEX not null default 0" json:"right" valid:"length(0|2)" maximum:"2" default:"0"` Right Right `xorm:"int(11) INDEX not null default 0" json:"right" valid:"length(0|2)" maximum:"2" default:"0"`
// A unix timestamp when this relation was created. You cannot change this value. // A timestamp when this relation was created. You cannot change this value.
Created int64 `xorm:"created not null" json:"created"` Created timeutil.TimeStamp `xorm:"created not null" json:"created"`
// A unix timestamp when this relation was last updated. You cannot change this value. // A timestamp when this relation was last updated. You cannot change this value.
Updated int64 `xorm:"updated not null" json:"updated"` Updated timeutil.TimeStamp `xorm:"updated not null" json:"updated"`
web.CRUDable `xorm:"-" json:"-"` web.CRUDable `xorm:"-" json:"-"`
web.Rights `xorm:"-" json:"-"` web.Rights `xorm:"-" json:"-"`

View File

@ -18,6 +18,7 @@ package models
import ( import (
"code.vikunja.io/api/pkg/db" "code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/timeutil"
"code.vikunja.io/api/pkg/user" "code.vikunja.io/api/pkg/user"
"testing" "testing"
@ -30,8 +31,8 @@ func TestTeamNamespace_CanDoSomething(t *testing.T) {
TeamID int64 TeamID int64
NamespaceID int64 NamespaceID int64
Right Right Right Right
Created int64 Created timeutil.TimeStamp
Updated int64 Updated timeutil.TimeStamp
CRUDable web.CRUDable CRUDable web.CRUDable
Rights web.Rights Rights web.Rights
} }

View File

@ -18,6 +18,7 @@ package models
import ( import (
"code.vikunja.io/api/pkg/db" "code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/timeutil"
"code.vikunja.io/api/pkg/user" "code.vikunja.io/api/pkg/user"
"code.vikunja.io/web" "code.vikunja.io/web"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -112,8 +113,8 @@ func TestTeamNamespace_Update(t *testing.T) {
TeamID int64 TeamID int64
NamespaceID int64 NamespaceID int64
Right Right Right Right
Created int64 Created timeutil.TimeStamp
Updated int64 Updated timeutil.TimeStamp
CRUDable web.CRUDable CRUDable web.CRUDable
Rights web.Rights Rights web.Rights
} }

View File

@ -17,6 +17,7 @@
package models package models
import ( import (
"code.vikunja.io/api/pkg/timeutil"
user2 "code.vikunja.io/api/pkg/user" user2 "code.vikunja.io/api/pkg/user"
"code.vikunja.io/web" "code.vikunja.io/web"
) )
@ -33,10 +34,10 @@ type NamespaceUser struct {
// The right this user has. 0 = Read only, 1 = Read & Write, 2 = Admin. See the docs for more details. // The right this user has. 0 = Read only, 1 = Read & Write, 2 = Admin. See the docs for more details.
Right Right `xorm:"int(11) INDEX not null default 0" json:"right" valid:"length(0|2)" maximum:"2" default:"0"` Right Right `xorm:"int(11) INDEX not null default 0" json:"right" valid:"length(0|2)" maximum:"2" default:"0"`
// A unix timestamp when this relation was created. You cannot change this value. // A timestamp when this relation was created. You cannot change this value.
Created int64 `xorm:"created not null" json:"created"` Created timeutil.TimeStamp `xorm:"created not null" json:"created"`
// A unix timestamp when this relation was last updated. You cannot change this value. // A timestamp when this relation was last updated. You cannot change this value.
Updated int64 `xorm:"updated not null" json:"updated"` Updated timeutil.TimeStamp `xorm:"updated not null" json:"updated"`
web.CRUDable `xorm:"-" json:"-"` web.CRUDable `xorm:"-" json:"-"`
web.Rights `xorm:"-" json:"-"` web.Rights `xorm:"-" json:"-"`

View File

@ -18,6 +18,7 @@ package models
import ( import (
"code.vikunja.io/api/pkg/db" "code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/timeutil"
"code.vikunja.io/api/pkg/user" "code.vikunja.io/api/pkg/user"
"testing" "testing"
@ -30,8 +31,8 @@ func TestNamespaceUser_CanDoSomething(t *testing.T) {
UserID int64 UserID int64
NamespaceID int64 NamespaceID int64
Right Right Right Right
Created int64 Created timeutil.TimeStamp
Updated int64 Updated timeutil.TimeStamp
CRUDable web.CRUDable CRUDable web.CRUDable
Rights web.Rights Rights web.Rights
} }

View File

@ -18,6 +18,7 @@ package models
import ( import (
"code.vikunja.io/api/pkg/db" "code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/timeutil"
"code.vikunja.io/api/pkg/user" "code.vikunja.io/api/pkg/user"
"code.vikunja.io/web" "code.vikunja.io/web"
"gopkg.in/d4l3k/messagediff.v1" "gopkg.in/d4l3k/messagediff.v1"
@ -32,8 +33,8 @@ func TestNamespaceUser_Create(t *testing.T) {
Username string Username string
NamespaceID int64 NamespaceID int64
Right Right Right Right
Created int64 Created timeutil.TimeStamp
Updated int64 Updated timeutil.TimeStamp
CRUDable web.CRUDable CRUDable web.CRUDable
Rights web.Rights Rights web.Rights
} }
@ -132,8 +133,8 @@ func TestNamespaceUser_ReadAll(t *testing.T) {
UserID int64 UserID int64
NamespaceID int64 NamespaceID int64
Right Right Right Right
Created int64 Created timeutil.TimeStamp
Updated int64 Updated timeutil.TimeStamp
CRUDable web.CRUDable CRUDable web.CRUDable
Rights web.Rights Rights web.Rights
} }
@ -227,8 +228,8 @@ func TestNamespaceUser_Update(t *testing.T) {
Username string Username string
NamespaceID int64 NamespaceID int64
Right Right Right Right
Created int64 Created timeutil.TimeStamp
Updated int64 Updated timeutil.TimeStamp
CRUDable web.CRUDable CRUDable web.CRUDable
Rights web.Rights Rights web.Rights
} }
@ -304,8 +305,8 @@ func TestNamespaceUser_Delete(t *testing.T) {
Username string Username string
NamespaceID int64 NamespaceID int64
Right Right Right Right
Created int64 Created timeutil.TimeStamp
Updated int64 Updated timeutil.TimeStamp
CRUDable web.CRUDable CRUDable web.CRUDable
Rights web.Rights Rights web.Rights
} }

View File

@ -17,16 +17,17 @@
package models package models
import ( import (
"code.vikunja.io/api/pkg/timeutil"
"code.vikunja.io/api/pkg/user" "code.vikunja.io/api/pkg/user"
"code.vikunja.io/web" "code.vikunja.io/web"
) )
// TaskAssginee represents an assignment of a user to a task // TaskAssginee represents an assignment of a user to a task
type TaskAssginee struct { type TaskAssginee struct {
ID int64 `xorm:"int(11) autoincr not null unique pk" json:"-"` ID int64 `xorm:"int(11) autoincr not null unique pk" json:"-"`
TaskID int64 `xorm:"int(11) INDEX not null" json:"-" param:"listtask"` TaskID int64 `xorm:"int(11) INDEX not null" json:"-" param:"listtask"`
UserID int64 `xorm:"int(11) INDEX not null" json:"user_id" param:"user"` UserID int64 `xorm:"int(11) INDEX not null" json:"user_id" param:"user"`
Created int64 `xorm:"created not null"` Created timeutil.TimeStamp `xorm:"created not null"`
web.CRUDable `xorm:"-" json:"-"` web.CRUDable `xorm:"-" json:"-"`
web.Rights `xorm:"-" json:"-"` web.Rights `xorm:"-" json:"-"`

View File

@ -18,10 +18,10 @@ package models
import ( import (
"code.vikunja.io/api/pkg/files" "code.vikunja.io/api/pkg/files"
"code.vikunja.io/api/pkg/timeutil"
"code.vikunja.io/api/pkg/user" "code.vikunja.io/api/pkg/user"
"code.vikunja.io/web" "code.vikunja.io/web"
"io" "io"
"time"
) )
// TaskAttachment is the definition of a task attachment // TaskAttachment is the definition of a task attachment
@ -35,7 +35,7 @@ type TaskAttachment struct {
File *files.File `xorm:"-" json:"file"` File *files.File `xorm:"-" json:"file"`
Created int64 `xorm:"created" json:"created"` Created timeutil.TimeStamp `xorm:"created" json:"created"`
web.CRUDable `xorm:"-" json:"-"` web.CRUDable `xorm:"-" json:"-"`
web.Rights `xorm:"-" json:"-"` web.Rights `xorm:"-" json:"-"`
@ -145,7 +145,7 @@ func (ta *TaskAttachment) ReadAll(a web.Auth, search string, page int, perPage i
continue continue
} }
r.File = fs[r.FileID] r.File = fs[r.FileID]
r.File.Created = time.Unix(r.File.CreatedUnix, 0) r.File.Created = r.File.CreatedUnix.ToTime()
r.CreatedBy = us[r.CreatedByID] r.CreatedBy = us[r.CreatedByID]
} }

View File

@ -53,8 +53,8 @@ type TaskCollection struct {
// @Param s query string false "Search tasks by task text." // @Param s query string false "Search tasks by task text."
// @Param sort_by query string false "The sorting parameter. You can pass this multiple times to get the tasks ordered by multiple different parametes, along with `order_by`. Possible values to sort by are `id`, `text`, `description`, `done`, `done_at_unix`, `due_date_unix`, `created_by_id`, `list_id`, `repeat_after`, `priority`, `start_date_unix`, `end_date_unix`, `hex_color`, `percent_done`, `uid`, `created`, `updated`. Default is `id`." // @Param sort_by query string false "The sorting parameter. You can pass this multiple times to get the tasks ordered by multiple different parametes, along with `order_by`. Possible values to sort by are `id`, `text`, `description`, `done`, `done_at_unix`, `due_date_unix`, `created_by_id`, `list_id`, `repeat_after`, `priority`, `start_date_unix`, `end_date_unix`, `hex_color`, `percent_done`, `uid`, `created`, `updated`. Default is `id`."
// @Param order_by query string false "The ordering parameter. Possible values to order by are `asc` or `desc`. Default is `asc`." // @Param order_by query string false "The ordering parameter. Possible values to order by are `asc` or `desc`. Default is `asc`."
// @Param startdate query int false "The start date parameter to filter by. Expects a unix timestamp. If no end date, but a start date is specified, the end date is set to the current time." // @Param startdate query int false "The start date parameter to filter by. Expects a timestamp. If no end date, but a start date is specified, the end date is set to the current time."
// @Param enddate query int false "The end date parameter to filter by. Expects a unix timestamp. If no start date, but an end date is specified, the start date is set to the current time." // @Param enddate query int false "The end date parameter to filter by. Expects a timestamp. If no start date, but an end date is specified, the start date is set to the current time."
// @Security JWTKeyAuth // @Security JWTKeyAuth
// @Success 200 {array} models.Task "The tasks" // @Success 200 {array} models.Task "The tasks"
// @Failure 500 {object} models.Message "Internal error" // @Failure 500 {object} models.Message "Internal error"

View File

@ -17,6 +17,7 @@
package models package models
import ( import (
"code.vikunja.io/api/pkg/timeutil"
"fmt" "fmt"
"reflect" "reflect"
"sort" "sort"
@ -117,6 +118,13 @@ func mustMakeComparator(fieldName string) taskComparator {
return reflect.ValueOf(task).Elem().FieldByIndex(field.Index).Interface() return reflect.ValueOf(task).Elem().FieldByIndex(field.Index).Interface()
} }
// Special case for handling TimeStamp types
if field.Type.Name() == "TimeStamp" {
return func(lhs, rhs *Task) int64 {
return int64(extractProp(lhs).(timeutil.TimeStamp)) - int64(extractProp(rhs).(timeutil.TimeStamp))
}
}
switch field.Type.Kind() { switch field.Type.Kind() {
case reflect.Int64: case reflect.Int64:
return func(lhs, rhs *Task) int64 { return func(lhs, rhs *Task) int64 {
@ -165,14 +173,14 @@ var propertyComparators = map[sortProperty]taskComparator{
taskPropertyText: mustMakeComparator("Text"), taskPropertyText: mustMakeComparator("Text"),
taskPropertyDescription: mustMakeComparator("Description"), taskPropertyDescription: mustMakeComparator("Description"),
taskPropertyDone: mustMakeComparator("Done"), taskPropertyDone: mustMakeComparator("Done"),
taskPropertyDoneAtUnix: mustMakeComparator("DoneAtUnix"), taskPropertyDoneAtUnix: mustMakeComparator("DoneAt"),
taskPropertyDueDateUnix: mustMakeComparator("DueDateUnix"), taskPropertyDueDateUnix: mustMakeComparator("DueDate"),
taskPropertyCreatedByID: mustMakeComparator("CreatedByID"), taskPropertyCreatedByID: mustMakeComparator("CreatedByID"),
taskPropertyListID: mustMakeComparator("ListID"), taskPropertyListID: mustMakeComparator("ListID"),
taskPropertyRepeatAfter: mustMakeComparator("RepeatAfter"), taskPropertyRepeatAfter: mustMakeComparator("RepeatAfter"),
taskPropertyPriority: mustMakeComparator("Priority"), taskPropertyPriority: mustMakeComparator("Priority"),
taskPropertyStartDateUnix: mustMakeComparator("StartDateUnix"), taskPropertyStartDateUnix: mustMakeComparator("StartDate"),
taskPropertyEndDateUnix: mustMakeComparator("EndDateUnix"), taskPropertyEndDateUnix: mustMakeComparator("EndDate"),
taskPropertyHexColor: mustMakeComparator("HexColor"), taskPropertyHexColor: mustMakeComparator("HexColor"),
taskPropertyPercentDone: mustMakeComparator("PercentDone"), taskPropertyPercentDone: mustMakeComparator("PercentDone"),
taskPropertyUID: mustMakeComparator("UID"), taskPropertyUID: mustMakeComparator("UID"),

View File

@ -99,29 +99,29 @@ var (
Text: "aaa", Text: "aaa",
Description: "Lorem Ipsum", Description: "Lorem Ipsum",
Done: true, Done: true,
DoneAtUnix: 1543626000, DoneAt: 1543626000,
ListID: 1, ListID: 1,
UID: "JywtBPCESImlyKugvaZWrxmXAFAWXFISMeXYImEh", UID: "JywtBPCESImlyKugvaZWrxmXAFAWXFISMeXYImEh",
Created: 1543626724, Created: 1543626724,
Updated: 1543626724, Updated: 1543626724,
} }
task2 = &Task{ task2 = &Task{
ID: 2, ID: 2,
Text: "bbb", Text: "bbb",
Description: "Arem Ipsum", Description: "Arem Ipsum",
Done: true, Done: true,
DoneAtUnix: 1543626724, DoneAt: 1543626724,
CreatedByID: 1, CreatedByID: 1,
ListID: 2, ListID: 2,
PercentDone: 0.3, PercentDone: 0.3,
StartDateUnix: 1543626724, StartDate: 1543626724,
Created: 1553626724, Created: 1553626724,
Updated: 1553626724, Updated: 1553626724,
} }
task3 = &Task{ task3 = &Task{
ID: 3, ID: 3,
Text: "ccc", Text: "ccc",
DueDateUnix: 1583626724, DueDate: 1583626724,
Priority: 100, Priority: 100,
ListID: 3, ListID: 3,
HexColor: "000000", HexColor: "000000",
@ -129,49 +129,49 @@ var (
Updated: 1555555555, Updated: 1555555555,
} }
task4 = &Task{ task4 = &Task{
ID: 4, ID: 4,
Text: "ddd", Text: "ddd",
Priority: 1, Priority: 1,
StartDateUnix: 1643626724, StartDate: 1643626724,
ListID: 1, ListID: 1,
} }
task5 = &Task{ task5 = &Task{
ID: 5, ID: 5,
Text: "eef", Text: "eef",
Priority: 50, Priority: 50,
UID: "shggzCHQWLhGNMNsOGOCOjcVkInOYjTAnORqTkdL", UID: "shggzCHQWLhGNMNsOGOCOjcVkInOYjTAnORqTkdL",
DueDateUnix: 1543636724, DueDate: 1543636724,
Updated: 1565555555, Updated: 1565555555,
} }
task6 = &Task{ task6 = &Task{
ID: 6, ID: 6,
Text: "eef", Text: "eef",
DueDateUnix: 1543616724, DueDate: 1543616724,
RepeatAfter: 6400, RepeatAfter: 6400,
CreatedByID: 2, CreatedByID: 2,
HexColor: "ffffff", HexColor: "ffffff",
} }
task7 = &Task{ task7 = &Task{
ID: 7, ID: 7,
Text: "mmmn", Text: "mmmn",
Description: "Zoremis", Description: "Zoremis",
StartDateUnix: 1544600000, StartDate: 1544600000,
EndDateUnix: 1584600000, EndDate: 1584600000,
UID: "tyzCZuLMSKhwclJOsDyDcUdyVAPBDOPHNTBOLTcW", UID: "tyzCZuLMSKhwclJOsDyDcUdyVAPBDOPHNTBOLTcW",
} }
task8 = &Task{ task8 = &Task{
ID: 8, ID: 8,
Text: "b123", Text: "b123",
EndDateUnix: 1544700000, EndDate: 1544700000,
} }
task9 = &Task{ task9 = &Task{
ID: 9, ID: 9,
Done: true, Done: true,
DoneAtUnix: 1573626724, DoneAt: 1573626724,
Text: "a123", Text: "a123",
RepeatAfter: 86000, RepeatAfter: 86000,
StartDateUnix: 1544600000, StartDate: 1544600000,
EndDateUnix: 1544700000, EndDate: 1544700000,
} }
task10 = &Task{ task10 = &Task{
ID: 10, ID: 10,

View File

@ -19,6 +19,7 @@ package models
import ( import (
"code.vikunja.io/api/pkg/db" "code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/files" "code.vikunja.io/api/pkg/files"
"code.vikunja.io/api/pkg/timeutil"
"code.vikunja.io/api/pkg/user" "code.vikunja.io/api/pkg/user"
"code.vikunja.io/web" "code.vikunja.io/web"
"gopkg.in/d4l3k/messagediff.v1" "gopkg.in/d4l3k/messagediff.v1"
@ -167,7 +168,7 @@ func TestTaskCollection_ReadAll(t *testing.T) {
RelatedTasks: map[RelationKind][]*Task{}, RelatedTasks: map[RelationKind][]*Task{},
Created: 1543626724, Created: 1543626724,
Updated: 1543626724, Updated: 1543626724,
DueDateUnix: 1543636724, DueDate: 1543636724,
} }
task6 := &Task{ task6 := &Task{
ID: 6, ID: 6,
@ -180,20 +181,20 @@ func TestTaskCollection_ReadAll(t *testing.T) {
RelatedTasks: map[RelationKind][]*Task{}, RelatedTasks: map[RelationKind][]*Task{},
Created: 1543626724, Created: 1543626724,
Updated: 1543626724, Updated: 1543626724,
DueDateUnix: 1543616724, DueDate: 1543616724,
} }
task7 := &Task{ task7 := &Task{
ID: 7, ID: 7,
Text: "task #7 with start date", Text: "task #7 with start date",
Identifier: "test1-7", Identifier: "test1-7",
Index: 7, Index: 7,
CreatedByID: 1, CreatedByID: 1,
CreatedBy: user1, CreatedBy: user1,
ListID: 1, ListID: 1,
RelatedTasks: map[RelationKind][]*Task{}, RelatedTasks: map[RelationKind][]*Task{},
Created: 1543626724, Created: 1543626724,
Updated: 1543626724, Updated: 1543626724,
StartDateUnix: 1544600000, StartDate: 1544600000,
} }
task8 := &Task{ task8 := &Task{
ID: 8, ID: 8,
@ -206,21 +207,21 @@ func TestTaskCollection_ReadAll(t *testing.T) {
RelatedTasks: map[RelationKind][]*Task{}, RelatedTasks: map[RelationKind][]*Task{},
Created: 1543626724, Created: 1543626724,
Updated: 1543626724, Updated: 1543626724,
EndDateUnix: 1544700000, EndDate: 1544700000,
} }
task9 := &Task{ task9 := &Task{
ID: 9, ID: 9,
Text: "task #9 with start and end date", Text: "task #9 with start and end date",
Identifier: "test1-9", Identifier: "test1-9",
Index: 9, Index: 9,
CreatedByID: 1, CreatedByID: 1,
CreatedBy: user1, CreatedBy: user1,
ListID: 1, ListID: 1,
RelatedTasks: map[RelationKind][]*Task{}, RelatedTasks: map[RelationKind][]*Task{},
Created: 1543626724, Created: 1543626724,
Updated: 1543626724, Updated: 1543626724,
StartDateUnix: 1544600000, StartDate: 1544600000,
EndDateUnix: 1544700000, EndDate: 1544700000,
} }
task10 := &Task{ task10 := &Task{
ID: 10, ID: 10,
@ -403,17 +404,17 @@ func TestTaskCollection_ReadAll(t *testing.T) {
Updated: 1543626724, Updated: 1543626724,
} }
task27 := &Task{ task27 := &Task{
ID: 27, ID: 27,
Text: "task #27 with reminders", Text: "task #27 with reminders",
Identifier: "test1-12", Identifier: "test1-12",
Index: 12, Index: 12,
CreatedByID: 1, CreatedByID: 1,
CreatedBy: user1, CreatedBy: user1,
RemindersUnix: []int64{1543626724, 1543626824}, Reminders: []timeutil.TimeStamp{1543626724, 1543626824},
ListID: 1, ListID: 1,
RelatedTasks: map[RelationKind][]*Task{}, RelatedTasks: map[RelationKind][]*Task{},
Created: 1543626724, Created: 1543626724,
Updated: 1543626724, Updated: 1543626724,
} }
task28 := &Task{ task28 := &Task{
ID: 28, ID: 28,

View File

@ -18,6 +18,7 @@
package models package models
import ( import (
"code.vikunja.io/api/pkg/timeutil"
"code.vikunja.io/api/pkg/user" "code.vikunja.io/api/pkg/user"
"code.vikunja.io/web" "code.vikunja.io/web"
) )
@ -86,8 +87,8 @@ type TaskRelation struct {
// The user who created this relation // The user who created this relation
CreatedBy *user.User `xorm:"-" json:"created_by"` CreatedBy *user.User `xorm:"-" json:"created_by"`
// A unix timestamp when this label was created. You cannot change this value. // A timestamp when this label was created. You cannot change this value.
Created int64 `xorm:"created not null" json:"created"` Created timeutil.TimeStamp `xorm:"created not null" json:"created"`
web.CRUDable `xorm:"-" json:"-"` web.CRUDable `xorm:"-" json:"-"`
web.Rights `xorm:"-" json:"-"` web.Rights `xorm:"-" json:"-"`

View File

@ -19,6 +19,7 @@ package models
import ( import (
"code.vikunja.io/api/pkg/files" "code.vikunja.io/api/pkg/files"
"code.vikunja.io/api/pkg/metrics" "code.vikunja.io/api/pkg/metrics"
"code.vikunja.io/api/pkg/timeutil"
"code.vikunja.io/api/pkg/user" "code.vikunja.io/api/pkg/user"
"code.vikunja.io/api/pkg/utils" "code.vikunja.io/api/pkg/utils"
"code.vikunja.io/web" "code.vikunja.io/web"
@ -38,13 +39,13 @@ type Task struct {
Description string `xorm:"longtext null" json:"description"` Description string `xorm:"longtext null" json:"description"`
// Whether a task is done or not. // Whether a task is done or not.
Done bool `xorm:"INDEX null" json:"done"` Done bool `xorm:"INDEX null" json:"done"`
// The unix timestamp when a task was marked as done. // The time when a task was marked as done.
DoneAtUnix int64 `xorm:"INDEX null" json:"doneAt"` DoneAt timeutil.TimeStamp `xorm:"INDEX null 'done_at_unix'" json:"doneAt"`
// A unix timestamp when the task is due. // The time when the task is due.
DueDateUnix int64 `xorm:"int(11) INDEX null" json:"dueDate"` DueDate timeutil.TimeStamp `xorm:"int(11) INDEX null 'due_date_unix'" json:"dueDate"`
// An array of unix timestamps when the user wants to be reminded of the task. // An array of datetimes when the user wants to be reminded of the task.
RemindersUnix []int64 `xorm:"-" json:"reminderDates"` Reminders []timeutil.TimeStamp `xorm:"-" json:"reminderDates"`
CreatedByID int64 `xorm:"int(11) not null" json:"-"` // ID of the user who put that task on the list CreatedByID int64 `xorm:"int(11) not null" json:"-"` // ID of the user who put that task on the list
// The list this task belongs to. // The list this task belongs to.
ListID int64 `xorm:"int(11) INDEX not null" json:"listID" param:"list"` ListID int64 `xorm:"int(11) INDEX not null" json:"listID" param:"list"`
// An amount in seconds this task repeats itself. If this is set, when marking the task as done, it will mark itself as "undone" and then increase all remindes and the due date by its amount. // An amount in seconds this task repeats itself. If this is set, when marking the task as done, it will mark itself as "undone" and then increase all remindes and the due date by its amount.
@ -52,9 +53,9 @@ type Task struct {
// The task priority. Can be anything you want, it is possible to sort by this later. // The task priority. Can be anything you want, it is possible to sort by this later.
Priority int64 `xorm:"int(11) null" json:"priority"` Priority int64 `xorm:"int(11) null" json:"priority"`
// When this task starts. // When this task starts.
StartDateUnix int64 `xorm:"int(11) INDEX null" json:"startDate" query:"-"` StartDate timeutil.TimeStamp `xorm:"int(11) INDEX null 'start_date_unix'" json:"startDate" query:"-"`
// When this task ends. // When this task ends.
EndDateUnix int64 `xorm:"int(11) INDEX null" json:"endDate" query:"-"` EndDate timeutil.TimeStamp `xorm:"int(11) INDEX null 'end_date_unix'" json:"endDate" query:"-"`
// An array of users who are assigned to this task // An array of users who are assigned to this task
Assignees []*user.User `xorm:"-" json:"assignees"` Assignees []*user.User `xorm:"-" json:"assignees"`
// An array of labels which are associated with this task. // An array of labels which are associated with this task.
@ -72,20 +73,16 @@ type Task struct {
// The UID is currently not used for anything other than caldav, which is why we don't expose it over json // The UID is currently not used for anything other than caldav, which is why we don't expose it over json
UID string `xorm:"varchar(250) null" json:"-"` UID string `xorm:"varchar(250) null" json:"-"`
Sorting string `xorm:"-" json:"-" query:"sort"` // Parameter to sort by
StartDateSortUnix int64 `xorm:"-" json:"-" query:"startdate"`
EndDateSortUnix int64 `xorm:"-" json:"-" query:"enddate"`
// All related tasks, grouped by their relation kind // All related tasks, grouped by their relation kind
RelatedTasks RelatedTaskMap `xorm:"-" json:"related_tasks"` RelatedTasks RelatedTaskMap `xorm:"-" json:"related_tasks"`
// All attachments this task has // All attachments this task has
Attachments []*TaskAttachment `xorm:"-" json:"attachments"` Attachments []*TaskAttachment `xorm:"-" json:"attachments"`
// A unix timestamp when this task was created. You cannot change this value. // A timestamp when this task was created. You cannot change this value.
Created int64 `xorm:"created not null" json:"created"` Created timeutil.TimeStamp `xorm:"created not null" json:"created"`
// A unix timestamp when this task was last updated. You cannot change this value. // A timestamp when this task was last updated. You cannot change this value.
Updated int64 `xorm:"updated not null" json:"updated"` Updated timeutil.TimeStamp `xorm:"updated not null" json:"updated"`
// The user who initially created the task. // The user who initially created the task.
CreatedBy *user.User `xorm:"-" json:"createdBy" valid:"-"` CreatedBy *user.User `xorm:"-" json:"createdBy" valid:"-"`
@ -101,10 +98,10 @@ func (Task) TableName() string {
// TaskReminder holds a reminder on a task // TaskReminder holds a reminder on a task
type TaskReminder struct { type TaskReminder struct {
ID int64 `xorm:"int(11) autoincr not null unique pk"` ID int64 `xorm:"int(11) autoincr not null unique pk"`
TaskID int64 `xorm:"int(11) not null INDEX"` TaskID int64 `xorm:"int(11) not null INDEX"`
ReminderUnix int64 `xorm:"int(11) not null INDEX"` Reminder timeutil.TimeStamp `xorm:"int(11) not null INDEX 'reminder_unix'"`
Created int64 `xorm:"created not null"` Created timeutil.TimeStamp `xorm:"created not null"`
} }
// TableName returns a pretty table name // TableName returns a pretty table name
@ -131,8 +128,8 @@ type taskOptions struct {
// @Param per_page query int false "The maximum number of items per page. Note this parameter is limited by the configured maximum of items per page." // @Param per_page query int false "The maximum number of items per page. Note this parameter is limited by the configured maximum of items per page."
// @Param s query string false "Search tasks by task text." // @Param s query string false "Search tasks by task text."
// @Param sort query string false "The sorting parameter. Possible values to sort by are priority, prioritydesc, priorityasc, duedate, duedatedesc, duedateasc." // @Param sort query string false "The sorting parameter. Possible values to sort by are priority, prioritydesc, priorityasc, duedate, duedatedesc, duedateasc."
// @Param startdate query int false "The start date parameter to filter by. Expects a unix timestamp. If no end date, but a start date is specified, the end date is set to the current time." // @Param startdate query int false "The start date parameter to filter by. Expects a timestamp. If no end date, but a start date is specified, the end date is set to the current time."
// @Param enddate query int false "The end date parameter to filter by. Expects a unix timestamp. If no start date, but an end date is specified, the start date is set to the current time." // @Param enddate query int false "The end date parameter to filter by. Expects a timestamp. If no start date, but an end date is specified, the start date is set to the current time."
// @Security JWTKeyAuth // @Security JWTKeyAuth
// @Success 200 {array} models.Task "The tasks" // @Success 200 {array} models.Task "The tasks"
// @Failure 500 {object} models.Message "Internal error" // @Failure 500 {object} models.Message "Internal error"
@ -292,7 +289,7 @@ func GetTasksByUIDs(uids []string) (tasks []*Task, err error) {
func getRemindersForTasks(taskIDs []int64) (reminders []*TaskReminder, err error) { func getRemindersForTasks(taskIDs []int64) (reminders []*TaskReminder, err error) {
reminders = []*TaskReminder{} reminders = []*TaskReminder{}
err = x.Table("task_reminders").In("task_id", taskIDs).Find(&reminders) err = x.In("task_id", taskIDs).Find(&reminders)
return return
} }
@ -390,9 +387,9 @@ func addMoreInfoToTasks(taskMap map[int64]*Task) (tasks []*Task, err error) {
return return
} }
taskRemindersUnix := make(map[int64][]int64) taskRemindersUnix := make(map[int64][]timeutil.TimeStamp)
for _, r := range reminders { for _, r := range reminders {
taskRemindersUnix[r.TaskID] = append(taskRemindersUnix[r.TaskID], r.ReminderUnix) taskRemindersUnix[r.TaskID] = append(taskRemindersUnix[r.TaskID], r.Reminder)
} }
// Get all identifiers // Get all identifiers
@ -409,7 +406,7 @@ func addMoreInfoToTasks(taskMap map[int64]*Task) (tasks []*Task, err error) {
task.CreatedBy = users[task.CreatedByID] task.CreatedBy = users[task.CreatedByID]
// Add the reminders // Add the reminders
task.RemindersUnix = taskRemindersUnix[task.ID] task.Reminders = taskRemindersUnix[task.ID]
// Prepare the subtasks // Prepare the subtasks
task.RelatedTasks = make(RelatedTaskMap) task.RelatedTasks = make(RelatedTaskMap)
@ -518,7 +515,7 @@ func (t *Task) Create(a web.Auth) (err error) {
} }
// Update the reminders // Update the reminders
if err := t.updateReminders(t.RemindersUnix); err != nil { if err := t.updateReminders(t.Reminders); err != nil {
return err return err
} }
@ -558,7 +555,7 @@ func (t *Task) Update() (err error) {
} }
// Update the reminders // Update the reminders
if err := ot.updateReminders(t.RemindersUnix); err != nil { if err := ot.updateReminders(t.Reminders); err != nil {
return err return err
} }
@ -603,20 +600,20 @@ func (t *Task) Update() (err error) {
ot.Description = "" ot.Description = ""
} }
// Due date // Due date
if t.DueDateUnix == 0 { if t.DueDate == 0 {
ot.DueDateUnix = 0 ot.DueDate = 0
} }
// Repeat after // Repeat after
if t.RepeatAfter == 0 { if t.RepeatAfter == 0 {
ot.RepeatAfter = 0 ot.RepeatAfter = 0
} }
// Start date // Start date
if t.StartDateUnix == 0 { if t.StartDate == 0 {
ot.StartDateUnix = 0 ot.StartDate = 0
} }
// End date // End date
if t.EndDateUnix == 0 { if t.EndDate == 0 {
ot.EndDateUnix = 0 ot.EndDate = 0
} }
// Color // Color
if t.HexColor == "" { if t.HexColor == "" {
@ -653,10 +650,10 @@ func (t *Task) Update() (err error) {
// with updated values into the db) // with updated values into the db)
func updateDone(oldTask *Task, newTask *Task) { func updateDone(oldTask *Task, newTask *Task) {
if !oldTask.Done && newTask.Done && oldTask.RepeatAfter > 0 { if !oldTask.Done && newTask.Done && oldTask.RepeatAfter > 0 {
oldTask.DueDateUnix = oldTask.DueDateUnix + oldTask.RepeatAfter // assuming we'll save the old task (merged) oldTask.DueDate = timeutil.FromTime(oldTask.DueDate.ToTime().Add(time.Duration(oldTask.RepeatAfter) * time.Second)) // assuming we'll save the old task (merged)
for in, r := range oldTask.RemindersUnix { for in, r := range oldTask.Reminders {
oldTask.RemindersUnix[in] = r + oldTask.RepeatAfter oldTask.Reminders[in] = timeutil.FromTime(r.ToTime().Add(time.Duration(oldTask.RepeatAfter) * time.Second))
} }
newTask.Done = false newTask.Done = false
@ -664,17 +661,17 @@ func updateDone(oldTask *Task, newTask *Task) {
// Update the "done at" timestamp // Update the "done at" timestamp
if !oldTask.Done && newTask.Done { if !oldTask.Done && newTask.Done {
oldTask.DoneAtUnix = time.Now().Unix() oldTask.DoneAt = timeutil.FromTime(time.Now())
} }
// When unmarking a task as done, reset the timestamp // When unmarking a task as done, reset the timestamp
if oldTask.Done && !newTask.Done { if oldTask.Done && !newTask.Done {
oldTask.DoneAtUnix = 0 oldTask.DoneAt = 0
} }
} }
// Creates or deletes all necessary remindes without unneded db operations. // Creates or deletes all necessary remindes without unneded db operations.
// The parameter is a slice with unix dates which holds the new reminders. // The parameter is a slice with unix dates which holds the new reminders.
func (t *Task) updateReminders(reminders []int64) (err error) { func (t *Task) updateReminders(reminders []timeutil.TimeStamp) (err error) {
// Load the current reminders // Load the current reminders
taskReminders, err := getRemindersForTasks([]int64{t.ID}) taskReminders, err := getRemindersForTasks([]int64{t.ID})
@ -682,35 +679,35 @@ func (t *Task) updateReminders(reminders []int64) (err error) {
return err return err
} }
t.RemindersUnix = make([]int64, 0, len(taskReminders)) t.Reminders = make([]timeutil.TimeStamp, 0, len(taskReminders))
for _, reminder := range taskReminders { for _, reminder := range taskReminders {
t.RemindersUnix = append(t.RemindersUnix, reminder.ReminderUnix) t.Reminders = append(t.Reminders, reminder.Reminder)
} }
// If we're removing everything, delete all reminders right away // If we're removing everything, delete all reminders right away
if len(reminders) == 0 && len(t.RemindersUnix) > 0 { if len(reminders) == 0 && len(t.Reminders) > 0 {
_, err = x.Where("task_id = ?", t.ID). _, err = x.Where("task_id = ?", t.ID).
Delete(TaskReminder{}) Delete(TaskReminder{})
t.RemindersUnix = nil t.Reminders = nil
return err return err
} }
// If we didn't change anything (from 0 to zero) don't do anything. // If we didn't change anything (from 0 to zero) don't do anything.
if len(reminders) == 0 && len(t.RemindersUnix) == 0 { if len(reminders) == 0 && len(t.Reminders) == 0 {
return nil return nil
} }
// Make a hashmap of the new reminders for easier comparison // Make a hashmap of the new reminders for easier comparison
newReminders := make(map[int64]*TaskReminder, len(reminders)) newReminders := make(map[timeutil.TimeStamp]*TaskReminder, len(reminders))
for _, newReminder := range reminders { for _, newReminder := range reminders {
newReminders[newReminder] = &TaskReminder{ReminderUnix: newReminder} newReminders[newReminder] = &TaskReminder{Reminder: newReminder}
} }
// Get old reminders to delete // Get old reminders to delete
var found bool var found bool
var remindersToDelete []int64 var remindersToDelete []timeutil.TimeStamp
oldReminders := make(map[int64]*TaskReminder, len(t.RemindersUnix)) oldReminders := make(map[timeutil.TimeStamp]*TaskReminder, len(t.Reminders))
for _, oldReminder := range t.RemindersUnix { for _, oldReminder := range t.Reminders {
found = false found = false
// If a new reminder is already in the list with old reminders // If a new reminder is already in the list with old reminders
if newReminders[oldReminder] != nil { if newReminders[oldReminder] != nil {
@ -722,7 +719,7 @@ func (t *Task) updateReminders(reminders []int64) (err error) {
remindersToDelete = append(remindersToDelete, oldReminder) remindersToDelete = append(remindersToDelete, oldReminder)
} }
oldReminders[oldReminder] = &TaskReminder{ReminderUnix: oldReminder} oldReminders[oldReminder] = &TaskReminder{Reminder: oldReminder}
} }
// Delete all reminders not passed // Delete all reminders not passed
@ -744,15 +741,15 @@ func (t *Task) updateReminders(reminders []int64) (err error) {
} }
// Add the new reminder // Add the new reminder
_, err = x.Insert(TaskReminder{TaskID: t.ID, ReminderUnix: r}) _, err = x.Insert(TaskReminder{TaskID: t.ID, Reminder: r})
if err != nil { if err != nil {
return err return err
} }
} }
t.RemindersUnix = reminders t.Reminders = reminders
if len(reminders) == 0 { if len(reminders) == 0 {
t.RemindersUnix = nil t.Reminders = nil
} }
err = updateListLastUpdated(&List{ID: t.ListID}) err = updateListLastUpdated(&List{ID: t.ListID})

View File

@ -18,6 +18,7 @@ package models
import ( import (
"code.vikunja.io/api/pkg/db" "code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/timeutil"
"code.vikunja.io/api/pkg/user" "code.vikunja.io/api/pkg/user"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"testing" "testing"
@ -127,14 +128,14 @@ func TestUpdateDone(t *testing.T) {
oldTask := &Task{Done: false} oldTask := &Task{Done: false}
newTask := &Task{Done: true} newTask := &Task{Done: true}
updateDone(oldTask, newTask) updateDone(oldTask, newTask)
assert.NotEqual(t, int64(0), oldTask.DoneAtUnix) assert.NotEqual(t, timeutil.TimeStamp(0), oldTask.DoneAt)
}) })
t.Run("unmarking a task as done", func(t *testing.T) { t.Run("unmarking a task as done", func(t *testing.T) {
db.LoadAndAssertFixtures(t) db.LoadAndAssertFixtures(t)
oldTask := &Task{Done: true} oldTask := &Task{Done: true}
newTask := &Task{Done: false} newTask := &Task{Done: false}
updateDone(oldTask, newTask) updateDone(oldTask, newTask)
assert.Equal(t, int64(0), oldTask.DoneAtUnix) assert.Equal(t, timeutil.TimeStamp(0), oldTask.DoneAt)
}) })
} }

View File

@ -18,6 +18,7 @@ package models
import ( import (
"code.vikunja.io/api/pkg/metrics" "code.vikunja.io/api/pkg/metrics"
"code.vikunja.io/api/pkg/timeutil"
"code.vikunja.io/api/pkg/user" "code.vikunja.io/api/pkg/user"
"code.vikunja.io/web" "code.vikunja.io/web"
"github.com/go-xorm/builder" "github.com/go-xorm/builder"
@ -38,10 +39,10 @@ type Team struct {
// An array of all members in this team. // An array of all members in this team.
Members []*TeamUser `xorm:"-" json:"members"` Members []*TeamUser `xorm:"-" json:"members"`
// A unix timestamp when this relation was created. You cannot change this value. // A timestamp when this relation was created. You cannot change this value.
Created int64 `xorm:"created" json:"created"` Created timeutil.TimeStamp `xorm:"created" json:"created"`
// A unix timestamp when this relation was last updated. You cannot change this value. // A timestamp when this relation was last updated. You cannot change this value.
Updated int64 `xorm:"updated" json:"updated"` Updated timeutil.TimeStamp `xorm:"updated" json:"updated"`
web.CRUDable `xorm:"-" json:"-"` web.CRUDable `xorm:"-" json:"-"`
web.Rights `xorm:"-" json:"-"` web.Rights `xorm:"-" json:"-"`
@ -69,8 +70,8 @@ type TeamMember struct {
// Whether or not the member is an admin of the team. See the docs for more about what a team admin can do // Whether or not the member is an admin of the team. See the docs for more about what a team admin can do
Admin bool `xorm:"tinyint(1) INDEX null" json:"admin"` Admin bool `xorm:"tinyint(1) INDEX null" json:"admin"`
// A unix timestamp when this relation was created. You cannot change this value. // A timestamp when this relation was created. You cannot change this value.
Created int64 `xorm:"created not null" json:"created"` Created timeutil.TimeStamp `xorm:"created not null" json:"created"`
web.CRUDable `xorm:"-" json:"-"` web.CRUDable `xorm:"-" json:"-"`
web.Rights `xorm:"-" json:"-"` web.Rights `xorm:"-" json:"-"`

View File

@ -18,6 +18,7 @@ package models
import ( import (
"code.vikunja.io/api/pkg/db" "code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/timeutil"
"code.vikunja.io/api/pkg/user" "code.vikunja.io/api/pkg/user"
"testing" "testing"
@ -32,8 +33,8 @@ func TestTeam_CanDoSomething(t *testing.T) {
CreatedByID int64 CreatedByID int64
CreatedBy *user.User CreatedBy *user.User
Members []*TeamUser Members []*TeamUser
Created int64 Created timeutil.TimeStamp
Updated int64 Updated timeutil.TimeStamp
CRUDable web.CRUDable CRUDable web.CRUDable
Rights web.Rights Rights web.Rights
} }

View File

@ -17,15 +17,16 @@
package migration package migration
import ( import (
"code.vikunja.io/api/pkg/timeutil"
"code.vikunja.io/api/pkg/user" "code.vikunja.io/api/pkg/user"
) )
// Status represents this migration status // Status represents this migration status
type Status struct { type Status struct {
ID int64 `xorm:"int(11) autoincr not null unique pk" json:"id"` ID int64 `xorm:"int(11) autoincr not null unique pk" json:"id"`
UserID int64 `xorm:"int(11) not null" json:"-"` UserID int64 `xorm:"int(11) not null" json:"-"`
MigratorName string `xorm:"varchar(255)" json:"migrator_name"` MigratorName string `xorm:"varchar(255)" json:"migrator_name"`
CreatedUnix int64 `xorm:"created not null" json:"time_unix"` Created timeutil.TimeStamp `xorm:"created not null 'created_unix'" json:"time_unix"`
} }
// TableName holds the table name for the migration status table // TableName holds the table name for the migration status table

View File

@ -23,6 +23,7 @@ import (
"code.vikunja.io/api/pkg/log" "code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/models" "code.vikunja.io/api/pkg/models"
"code.vikunja.io/api/pkg/modules/migration" "code.vikunja.io/api/pkg/modules/migration"
"code.vikunja.io/api/pkg/timeutil"
"code.vikunja.io/api/pkg/user" "code.vikunja.io/api/pkg/user"
"code.vikunja.io/api/pkg/utils" "code.vikunja.io/api/pkg/utils"
"encoding/json" "encoding/json"
@ -144,7 +145,7 @@ func convertListForFolder(listID int, list *list, content *wunderlistContents) (
l := &models.List{ l := &models.List{
Title: list.Title, Title: list.Title,
Created: list.CreatedAt.Unix(), Created: timeutil.FromTime(list.CreatedAt),
} }
// Find all tasks belonging to this list and put them in // Find all tasks belonging to this list and put them in
@ -152,13 +153,13 @@ func convertListForFolder(listID int, list *list, content *wunderlistContents) (
if t.ListID == listID { if t.ListID == listID {
newTask := &models.Task{ newTask := &models.Task{
Text: t.Title, Text: t.Title,
Created: t.CreatedAt.Unix(), Created: timeutil.FromTime(t.CreatedAt),
Done: t.Completed, Done: t.Completed,
} }
// Set Done At // Set Done At
if newTask.Done { if newTask.Done {
newTask.DoneAtUnix = t.CompletedAt.Unix() newTask.DoneAt = timeutil.FromTime(t.CompletedAt)
} }
// Parse the due date // Parse the due date
@ -167,7 +168,7 @@ func convertListForFolder(listID int, list *list, content *wunderlistContents) (
if err != nil { if err != nil {
return nil, err return nil, err
} }
newTask.DueDateUnix = dueDate.Unix() newTask.DueDate = timeutil.FromTime(dueDate)
} }
// Find related notes // Find related notes
@ -198,13 +199,13 @@ func convertListForFolder(listID int, list *list, content *wunderlistContents) (
Mime: f.ContentType, Mime: f.ContentType,
Size: uint64(f.FileSize), Size: uint64(f.FileSize),
Created: f.CreatedAt, Created: f.CreatedAt,
CreatedUnix: f.CreatedAt.Unix(), CreatedUnix: timeutil.FromTime(f.CreatedAt),
// We directly pass the file contents here to have a way to link the attachment to the file later. // We directly pass the file contents here to have a way to link the attachment to the file later.
// Because we don't have an ID for our task at this point of the migration, we cannot just throw all // Because we don't have an ID for our task at this point of the migration, we cannot just throw all
// attachments in a slice and do the work of downloading and properly storing them later. // attachments in a slice and do the work of downloading and properly storing them later.
FileContent: buf.Bytes(), FileContent: buf.Bytes(),
}, },
Created: f.CreatedAt.Unix(), Created: timeutil.FromTime(f.CreatedAt),
}) })
} }
} }
@ -224,7 +225,7 @@ func convertListForFolder(listID int, list *list, content *wunderlistContents) (
// Reminders // Reminders
for _, r := range content.reminders { for _, r := range content.reminders {
if r.TaskID == t.ID { if r.TaskID == t.ID {
newTask.RemindersUnix = append(newTask.RemindersUnix, r.Date.Unix()) newTask.Reminders = append(newTask.Reminders, timeutil.FromTime(r.Date))
} }
} }
@ -247,8 +248,8 @@ func convertWunderlistToVikunja(content *wunderlistContents) (fullVikunjaHierach
namespace := &models.NamespaceWithLists{ namespace := &models.NamespaceWithLists{
Namespace: models.Namespace{ Namespace: models.Namespace{
Name: folder.Title, Name: folder.Title,
Created: folder.CreatedAt.Unix(), Created: timeutil.FromTime(folder.CreatedAt),
Updated: folder.UpdatedAt.Unix(), Updated: timeutil.FromTime(folder.UpdatedAt),
}, },
} }

View File

@ -20,6 +20,7 @@ import (
"code.vikunja.io/api/pkg/config" "code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/files" "code.vikunja.io/api/pkg/files"
"code.vikunja.io/api/pkg/models" "code.vikunja.io/api/pkg/models"
"code.vikunja.io/api/pkg/timeutil"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"gopkg.in/d4l3k/messagediff.v1" "gopkg.in/d4l3k/messagediff.v1"
"io/ioutil" "io/ioutil"
@ -192,18 +193,18 @@ func TestWunderlistParsing(t *testing.T) {
{ {
Namespace: models.Namespace{ Namespace: models.Namespace{
Name: "Lorem Ipsum", Name: "Lorem Ipsum",
Created: time1.Unix(), Created: timeutil.FromTime(time1),
Updated: time2.Unix(), Updated: timeutil.FromTime(time2),
}, },
Lists: []*models.List{ Lists: []*models.List{
{ {
Created: time1.Unix(), Created: timeutil.FromTime(time1),
Title: "Lorem1", Title: "Lorem1",
Tasks: []*models.Task{ Tasks: []*models.Task{
{ {
Text: "Ipsum1", Text: "Ipsum1",
DueDateUnix: 1378339200, DueDate: 1378339200,
Created: time1.Unix(), Created: timeutil.FromTime(time1),
Description: "Lorem Ipsum dolor sit amet", Description: "Lorem Ipsum dolor sit amet",
Attachments: []*models.TaskAttachment{ Attachments: []*models.TaskAttachment{
{ {
@ -212,18 +213,18 @@ func TestWunderlistParsing(t *testing.T) {
Mime: "text/plain", Mime: "text/plain",
Size: 12345, Size: 12345,
Created: time2, Created: time2,
CreatedUnix: time2.Unix(), CreatedUnix: timeutil.FromTime(time2),
FileContent: exampleFile, FileContent: exampleFile,
}, },
Created: time2.Unix(), Created: timeutil.FromTime(time2),
}, },
}, },
RemindersUnix: []int64{time4.Unix()}, Reminders: []timeutil.TimeStamp{timeutil.FromTime(time4)},
}, },
{ {
Text: "Ipsum2", Text: "Ipsum2",
DueDateUnix: 1378339200, DueDate: 1378339200,
Created: time1.Unix(), Created: timeutil.FromTime(time1),
Description: "Lorem Ipsum dolor sit amet", Description: "Lorem Ipsum dolor sit amet",
RelatedTasks: map[models.RelationKind][]*models.Task{ RelatedTasks: map[models.RelationKind][]*models.Task{
models.RelationKindSubtask: { models.RelationKindSubtask: {
@ -239,15 +240,15 @@ func TestWunderlistParsing(t *testing.T) {
}, },
}, },
{ {
Created: time1.Unix(), Created: timeutil.FromTime(time1),
Title: "Lorem2", Title: "Lorem2",
Tasks: []*models.Task{ Tasks: []*models.Task{
{ {
Text: "Ipsum3", Text: "Ipsum3",
Done: true, Done: true,
DoneAtUnix: time1.Unix(), DoneAt: timeutil.FromTime(time1),
DueDateUnix: 1378339200, DueDate: 1378339200,
Created: time1.Unix(), Created: timeutil.FromTime(time1),
Description: "Lorem Ipsum dolor sit amet", Description: "Lorem Ipsum dolor sit amet",
Attachments: []*models.TaskAttachment{ Attachments: []*models.TaskAttachment{
{ {
@ -256,18 +257,18 @@ func TestWunderlistParsing(t *testing.T) {
Mime: "text/plain", Mime: "text/plain",
Size: 12345, Size: 12345,
Created: time3, Created: time3,
CreatedUnix: time3.Unix(), CreatedUnix: timeutil.FromTime(time3),
FileContent: exampleFile, FileContent: exampleFile,
}, },
Created: time3.Unix(), Created: timeutil.FromTime(time3),
}, },
}, },
}, },
{ {
Text: "Ipsum4", Text: "Ipsum4",
DueDateUnix: 1378339200, DueDate: 1378339200,
Created: time1.Unix(), Created: timeutil.FromTime(time1),
RemindersUnix: []int64{time3.Unix()}, Reminders: []timeutil.TimeStamp{timeutil.FromTime(time3)},
RelatedTasks: map[models.RelationKind][]*models.Task{ RelatedTasks: map[models.RelationKind][]*models.Task{
models.RelationKindSubtask: { models.RelationKindSubtask: {
{ {
@ -279,52 +280,52 @@ func TestWunderlistParsing(t *testing.T) {
}, },
}, },
{ {
Created: time1.Unix(), Created: timeutil.FromTime(time1),
Title: "Lorem3", Title: "Lorem3",
Tasks: []*models.Task{ Tasks: []*models.Task{
{ {
Text: "Ipsum5", Text: "Ipsum5",
DueDateUnix: 1378339200, DueDate: 1378339200,
Created: time1.Unix(), Created: timeutil.FromTime(time1),
}, },
{ {
Text: "Ipsum6", Text: "Ipsum6",
DueDateUnix: 1378339200, DueDate: 1378339200,
Created: time1.Unix(), Created: timeutil.FromTime(time1),
Done: true, Done: true,
DoneAtUnix: time1.Unix(), DoneAt: timeutil.FromTime(time1),
}, },
{ {
Text: "Ipsum7", Text: "Ipsum7",
DueDateUnix: 1378339200, DueDate: 1378339200,
Created: time1.Unix(), Created: timeutil.FromTime(time1),
Done: true, Done: true,
DoneAtUnix: time1.Unix(), DoneAt: timeutil.FromTime(time1),
}, },
{ {
Text: "Ipsum8", Text: "Ipsum8",
DueDateUnix: 1378339200, DueDate: 1378339200,
Created: time1.Unix(), Created: timeutil.FromTime(time1),
}, },
}, },
}, },
{ {
Created: time1.Unix(), Created: timeutil.FromTime(time1),
Title: "Lorem4", Title: "Lorem4",
Tasks: []*models.Task{ Tasks: []*models.Task{
{ {
Text: "Ipsum9", Text: "Ipsum9",
DueDateUnix: 1378339200, DueDate: 1378339200,
Created: time1.Unix(), Created: timeutil.FromTime(time1),
Done: true, Done: true,
DoneAtUnix: time1.Unix(), DoneAt: timeutil.FromTime(time1),
}, },
{ {
Text: "Ipsum10", Text: "Ipsum10",
DueDateUnix: 1378339200, DueDate: 1378339200,
Created: time1.Unix(), Created: timeutil.FromTime(time1),
Done: true, Done: true,
DoneAtUnix: time1.Unix(), DoneAt: timeutil.FromTime(time1),
}, },
}, },
}, },
@ -336,7 +337,7 @@ func TestWunderlistParsing(t *testing.T) {
}, },
Lists: []*models.List{ Lists: []*models.List{
{ {
Created: time4.Unix(), Created: timeutil.FromTime(time4),
Title: "List without a namespace", Title: "List without a namespace",
}, },
}, },

View File

@ -342,12 +342,12 @@ func (vlra *VikunjaListResourceAdapter) CalculateEtag() string {
// Return the etag of a task if we have one // Return the etag of a task if we have one
if vlra.task != nil { if vlra.task != nil {
return `"` + strconv.FormatInt(vlra.task.ID, 10) + `-` + strconv.FormatInt(vlra.task.Updated, 10) + `"` return `"` + strconv.FormatInt(vlra.task.ID, 10) + `-` + strconv.FormatInt(vlra.task.Updated.ToTime().Unix(), 10) + `"`
} }
// This also returns the etag of the list, and not of the task, // This also returns the etag of the list, and not of the task,
// which becomes problematic because the client uses this etag (= the one from the list) to make // which becomes problematic because the client uses this etag (= the one from the list) to make
// Requests to update a task. These do not match and thus updating a task fails. // Requests to update a task. These do not match and thus updating a task fails.
return `"` + strconv.FormatInt(vlra.list.ID, 10) + `-` + strconv.FormatInt(vlra.list.Updated, 10) + `"` return `"` + strconv.FormatInt(vlra.list.ID, 10) + `-` + strconv.FormatInt(vlra.list.Updated.ToTime().Unix(), 10) + `"`
} }
// GetContent returns the content string of a resource (a task in our case) // GetContent returns the content string of a resource (a task in our case)
@ -372,11 +372,11 @@ func (vlra *VikunjaListResourceAdapter) GetContentSize() int64 {
// GetModTime returns when the resource was last modified // GetModTime returns when the resource was last modified
func (vlra *VikunjaListResourceAdapter) GetModTime() time.Time { func (vlra *VikunjaListResourceAdapter) GetModTime() time.Time {
if vlra.task != nil { if vlra.task != nil {
return time.Unix(vlra.task.Updated, 0) return time.Unix(vlra.task.Updated.ToTime().Unix(), 0)
} }
if vlra.list != nil { if vlra.list != nil {
return time.Unix(vlra.list.Updated, 0) return time.Unix(vlra.list.Updated.ToTime().Unix(), 0)
} }
return time.Time{} return time.Time{}

View File

@ -20,6 +20,7 @@ import (
"code.vikunja.io/api/pkg/caldav" "code.vikunja.io/api/pkg/caldav"
"code.vikunja.io/api/pkg/log" "code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/models" "code.vikunja.io/api/pkg/models"
"code.vikunja.io/api/pkg/timeutil"
"github.com/laurent22/ical-go" "github.com/laurent22/ical-go"
"strconv" "strconv"
"time" "time"
@ -31,23 +32,22 @@ func getCaldavTodosForTasks(list *models.List) string {
var caldavtodos []*caldav.Todo var caldavtodos []*caldav.Todo
for _, t := range list.Tasks { for _, t := range list.Tasks {
durationString := t.EndDateUnix - t.StartDateUnix duration := t.EndDate.ToTime().Sub(t.StartDate.ToTime())
duration, _ := time.ParseDuration(strconv.FormatInt(durationString, 10) + `s`)
caldavtodos = append(caldavtodos, &caldav.Todo{ caldavtodos = append(caldavtodos, &caldav.Todo{
TimestampUnix: t.Updated, Timestamp: t.Updated,
UID: t.UID, UID: t.UID,
Summary: t.Text, Summary: t.Text,
Description: t.Description, Description: t.Description,
CompletedUnix: t.DoneAtUnix, Completed: t.DoneAt,
// Organizer: &t.CreatedBy, // Disabled until we figure out how this works // Organizer: &t.CreatedBy, // Disabled until we figure out how this works
Priority: t.Priority, Priority: t.Priority,
StartUnix: t.StartDateUnix, Start: t.StartDate,
EndUnix: t.EndDateUnix, End: t.EndDate,
CreatedUnix: t.Created, Created: t.Created,
UpdatedUnix: t.Updated, Updated: t.Updated,
DueDateUnix: t.DueDateUnix, DueDate: t.DueDate,
Duration: duration, Duration: duration,
}) })
} }
@ -90,36 +90,36 @@ func parseTaskFromVTODO(content string) (vTask *models.Task, err error) {
duration, _ := time.ParseDuration(task["DURATION"]) duration, _ := time.ParseDuration(task["DURATION"])
vTask = &models.Task{ vTask = &models.Task{
UID: task["UID"], UID: task["UID"],
Text: task["SUMMARY"], Text: task["SUMMARY"],
Description: task["DESCRIPTION"], Description: task["DESCRIPTION"],
Priority: priority, Priority: priority,
DueDateUnix: caldavTimeToUnixTimestamp(task["DUE"]), DueDate: caldavTimeToTimestamp(task["DUE"]),
Updated: caldavTimeToUnixTimestamp(task["DTSTAMP"]), Updated: caldavTimeToTimestamp(task["DTSTAMP"]),
StartDateUnix: caldavTimeToUnixTimestamp(task["DTSTART"]), StartDate: caldavTimeToTimestamp(task["DTSTART"]),
DoneAtUnix: caldavTimeToUnixTimestamp(task["COMPLETED"]), DoneAt: caldavTimeToTimestamp(task["COMPLETED"]),
} }
if task["STATUS"] == "COMPLETED" { if task["STATUS"] == "COMPLETED" {
vTask.Done = true vTask.Done = true
} }
if duration > 0 && vTask.StartDateUnix > 0 { if duration > 0 && vTask.StartDate > 0 {
vTask.EndDateUnix = vTask.StartDateUnix + int64(duration.Seconds()) vTask.EndDate = timeutil.FromTime(vTask.StartDate.ToTime().Add(duration))
} }
return return
} }
func caldavTimeToUnixTimestamp(tstring string) int64 { func caldavTimeToTimestamp(tstring string) timeutil.TimeStamp {
if tstring == "" { if tstring == "" {
return 0 return 0
} }
t, err := time.Parse(caldav.DateFormat, tstring) t, err := time.Parse(caldav.DateFormat, tstring)
if err != nil { if err != nil {
log.Warningf("Error while parsing caldav time %s to unix time: %s", tstring, err) log.Warningf("Error while parsing caldav time %s to TimeStamp: %s", tstring, err)
return 0 return 0
} }
return t.Unix() return timeutil.FromTime(t)
} }

79
pkg/timeutil/time.go Normal file
View File

@ -0,0 +1,79 @@
// 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 timeutil
import (
"code.vikunja.io/api/pkg/config"
"encoding/json"
"time"
)
// TimeStamp is a type which holds a unix timestamp, but becomes a RFC3339 date when parsed to json.
// This allows us to save the time as a unix timestamp into the database while returning it as an iso
// date to the api client.
type TimeStamp int64
// ToTime returns a time.Time from a TimeStamp
func (ts *TimeStamp) ToTime() time.Time {
return time.Unix(int64(*ts), 0)
}
// FromTime converts a time.Time to a TimeStamp
func FromTime(t time.Time) TimeStamp {
return TimeStamp(t.Unix())
}
// MarshalJSON converts a TimeStamp to a json string
func (ts *TimeStamp) MarshalJSON() ([]byte, error) {
if int64(*ts) == 0 {
return []byte("null"), nil
}
loc, err := time.LoadLocation(config.ServiceTimeZone.GetString())
if err != nil {
return nil, err
}
s := `"` + ts.ToTime().In(loc).Format(time.RFC3339) + `"`
return []byte(s), nil
}
// UnmarshalJSON converts an iso date string from json to a TimeStamp
func (ts *TimeStamp) UnmarshalJSON(data []byte) error {
var s string
err := json.Unmarshal(data, &s)
if err != nil {
return err
}
if s == "" {
*ts = FromTime(time.Unix(0, 0))
return nil
}
t, err := time.Parse(time.RFC3339, s)
if err != nil {
return err
}
loc, err := time.LoadLocation(config.ServiceTimeZone.GetString())
if err != nil {
return err
}
*ts = TimeStamp(t.In(loc).Unix())
return nil
}

View File

@ -21,6 +21,7 @@ import (
"code.vikunja.io/api/pkg/config" "code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/mail" "code.vikunja.io/api/pkg/mail"
"code.vikunja.io/api/pkg/metrics" "code.vikunja.io/api/pkg/metrics"
"code.vikunja.io/api/pkg/timeutil"
"code.vikunja.io/api/pkg/utils" "code.vikunja.io/api/pkg/utils"
"code.vikunja.io/web" "code.vikunja.io/web"
"fmt" "fmt"
@ -54,10 +55,10 @@ type User struct {
PasswordResetToken string `xorm:"varchar(450) null" json:"-"` PasswordResetToken string `xorm:"varchar(450) null" json:"-"`
EmailConfirmToken string `xorm:"varchar(450) null" json:"-"` EmailConfirmToken string `xorm:"varchar(450) null" json:"-"`
// A unix timestamp when this task was created. You cannot change this value. // A timestamp when this task was created. You cannot change this value.
Created int64 `xorm:"created not null" json:"created"` Created timeutil.TimeStamp `xorm:"created not null" json:"created"`
// A unix timestamp when this task was last updated. You cannot change this value. // A timestamp when this task was last updated. You cannot change this value.
Updated int64 `xorm:"updated not null" json:"updated"` Updated timeutil.TimeStamp `xorm:"updated not null" json:"updated"`
web.Auth `xorm:"-" json:"-"` web.Auth `xorm:"-" json:"-"`
} }