Compare commits

..

1 Commits

Author SHA1 Message Date
renovate 6236171c95 chore(deps): update alpine docker tag to v3.14 2021-08-20 18:00:36 +00:00
57 changed files with 639 additions and 2703 deletions

View File

@ -662,7 +662,7 @@ steps:
image: docker:git
commands:
- git fetch --tags
- name: docker-arm-unstable
- name: docker-arm-latest
image: plugins/docker:linux-arm
pull: true
settings:
@ -671,7 +671,7 @@ steps:
password:
from_secret: docker_password
repo: vikunja/api
tags: unstable-linux-arm
tags: latest-linux-arm
depends_on: [ fetch-tags ]
when:
ref:
@ -693,7 +693,7 @@ steps:
ref:
- "refs/tags/**"
- name: docker-arm64-unstable
- name: docker-arm64-latest
image: plugins/docker:linux-arm64
pull: true
settings:
@ -702,7 +702,7 @@ steps:
password:
from_secret: docker_password
repo: vikunja/api
tags: unstable-linux-arm64
tags: latest-linux-arm64
depends_on: [ fetch-tags ]
when:
ref:
@ -746,8 +746,7 @@ steps:
image: docker:git
commands:
- git fetch --tags
- name: docker-unstable
- name: docker-latest
image: plugins/docker:linux-amd64
pull: true
settings:
@ -756,7 +755,7 @@ steps:
password:
from_secret: docker_password
repo: vikunja/api
tags: unstable-linux-amd64
tags: latest-linux-amd64
depends_on: [ fetch-tags ]
when:
ref:
@ -793,13 +792,13 @@ depends_on:
- docker-arm-release
steps:
- name: manifest-unstable
- name: manifest-latest
pull: always
image: plugins/manifest
settings:
tags: unstable
tags: latest
ignore_missing: true
spec: docker-manifest-unstable.tmpl
spec: docker-manifest-latest.tmpl
password:
from_secret: docker_password
username:
@ -808,7 +807,7 @@ steps:
ref:
- refs/heads/main
- name: manifest-release
- name: manifest
pull: always
image: plugins/manifest
settings:
@ -823,23 +822,6 @@ steps:
ref:
- "refs/tags/**"
- name: manifest-release-latest
pull: always
image: plugins/manifest
depends_on:
- clone
settings:
tags: latest
ignore_missing: true
spec: docker-manifest.tmpl
password:
from_secret: docker_password
username:
from_secret: docker_username
when:
ref:
- "refs/tags/**"
---
kind: pipeline
type: docker
@ -874,6 +856,6 @@ steps:
- failure
---
kind: signature
hmac: 110b782e9b704b4b3b3d618678383718c92262cf3c214f4fe6705d40cd3da367
hmac: 2cee8cac22aae8b4a25ffa28c47d9defc6ce227e0d262b57677f9adbf8badc3e
...

View File

@ -2,184 +2,11 @@
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres
to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
All releases can be found on https://code.vikunja.io/api/releases.
## [0.18.1] - 2021-09-08
### Fixed
* Docs: Add another third-party tutorial link
* Don't try to export items which do not have a parent
* fix(deps): update golang.org/x/sys commit hash to 6f6e228 (#970)
* fix(deps): update golang.org/x/sys commit hash to c212e73 (#971)
* Fix exporting tasks from archived lists
* Fix lint
* Fix tasks not exported
* Fix tmp export file created in the wrong path
## [0.18.0] - 2021-09-05
### Added
* Add default list setting (#875)
* Add menu link to Vikunja Cloud in docs
* Add more logging and better error messages for openid authentication + clarify docs
* Add more logging for test data api endpoint
* Add searching for tasks by index
* Add setting for first day of the week
* Add support of Unix socket (#912)
* Add truncate parameter to test fixtures setup
* Notify the user after three failed login attempts
* Reorder tasks, lists and kanban buckets (#923)
* Send a notification on failed TOTP
* Task mentions (#926)
* Try to get more information about the user when authenticating with openid
* User account deletion (#937)
* User Data Export and import (#967)
### Changed
* Allow running migration 20210711173657 multiple times to fix issues when it didn't completely run through previously
* Better logging for errors while importing a bunch of tasks
* Change task title to TEXT instead of varchar(250) to allow for longer task titles
* Disable the user account after 10 failed password attempts
* Docs: Add a note about default password
* Docs: Add another youtube tutorial
* Docs: Add ios to the list of not working caldav clients
* Docs: Add k8s-at-home Helm Chart for Vikunja
* Docs: Add other installation resources
* Docs: Add translation docs
* Docs: Fix rewrite rules in apache example configs
* Docs: Translation now happening at crowdin
* Docs: Update translation guidelines
* Don't fail when removing the last bucket in migration from other services
* Don't notify the user who created the team
* Don't use the mariadb root user in docker-compose examples
* Ensure case insensitive search on postgres (#927)
* Increase test timeout
* Only filter out failing openid providers if multiple are configured and one of them failed
* Only send an email about failed totp after three failed attempts
* Rearrange setting frontend url in config
* Refactor user email confirmation + password reset handling (#919)
* Rename and sign drone config
* Replace jwt-go with github.com/golang-jwt/jwt
* Reset failed totp attempts when logging in successfully
* Save user tokens as text and not varchar
* Save user tokens as varchar(450) and not text to fix mysql indexing issues
* Set todoist migration redirect url to the frontend url by default
* Show config full paths and env variables with config options
* Switch the :latest docker image tag to contain the latest release instead of the latest unstable
* Tune test db server settings to speed up tests (#939)
### Fixed
* Fix authentication callback
* Fix duplicating empty lists
* Fix error handling when deleting an attachment file
* Fix error when searching for a namespace returned no results
* Fix error when searching for a namespace with subscribers
* Fix goimports
* Fix importing archived projects and done items from todoist
* Fix jwt middleware
* Fix lint
* Fix mapping task priorities from Vikunja to calDAV
* Fix moving the done bucket around
* Fix old references to master in docs
* Fix panic on invalid smtp config
* Fix parsing openid config when using a json config file
* Fix saving pointer values to memory keyvalue
* Fix saving reminders of repeating tasks
* Fix setting a saved filter as favorite
* Fix setting task favorite status of related tasks
* Fix setting up keyvalue storage in tests
* Fix swagger docs for create requests
* Fix task relations not getting properly cleaned up when deleting them
* Fix tests & lint
* Make sure a bucket exists or use the default bucket when importing tasks
* Make sure all associated entities of a task are deleted when the task is deleted
* Make sure list / task favorites are set per user, not per entity (#915)
* Make sure the configured frontend url always has a / at the end
* Refactor & fix storing struct-values in redis keyvalue
* Todoist migration: don't panic if no reminder was found for task
### Dependency updates
* fix(deps): update golang.org/x/sys commit hash to 63515b4 (#959)
* fix(deps): update golang.org/x/sys commit hash to 97244b9 (#965)
* fix(deps): update golang.org/x/sys commit hash to f475640 (#962)
* fix(deps): update golang.org/x/sys commit hash to f4d4317 (#961)
* fix(deps): update module github.com/lib/pq to v1.10.3 (#963)
* Update alpine Docker tag to v3.13 (#884)
* Update alpine Docker tag to v3.14 (#889)
* Update golang.org/x/crypto commit hash to 0a44fdf (#944)
* Update golang.org/x/crypto commit hash to 0ba0e8f (#943)
* Update golang.org/x/crypto commit hash to 32db794 (#949)
* Update golang.org/x/crypto commit hash to 5ff15b2 (#891)
* Update golang.org/x/crypto commit hash to a769d52 (#916)
* Update golang.org/x/image commit hash to 775e3b0 (#880)
* Update golang.org/x/image commit hash to a66eb64 (#900)
* Update golang.org/x/image commit hash to e6eecd4 (#893)
* Update golang.org/x/net commit hash to 37e1c6af
* Update golang.org/x/oauth2 commit hash to 14747e6 (#894)
* Update golang.org/x/oauth2 commit hash to 2bc19b1 (#955)
* Update golang.org/x/oauth2 commit hash to 6f1e639 (#931)
* Update golang.org/x/oauth2 commit hash to 7df4dd6 (#952)
* Update golang.org/x/oauth2 commit hash to a41e5a7 (#902)
* Update golang.org/x/oauth2 commit hash to a8dc77f (#896)
* Update golang.org/x/oauth2 commit hash to bce0382 (#895)
* Update golang.org/x/oauth2 commit hash to d040287 (#888)
* Update golang.org/x/oauth2 commit hash to f6687ab (#862)
* Update golang.org/x/oauth2 commit hash to faf39c7 (#935)
* Update golang.org/x/sys commit hash to 00dd8d7 (#953)
* Update golang.org/x/sys commit hash to 15123e1 (#946)
* Update golang.org/x/sys commit hash to 1e6c022 (#947)
* Update golang.org/x/sys commit hash to 30e4713 (#945)
* Update golang.org/x/sys commit hash to 41cdb87 (#956)
* Update golang.org/x/sys commit hash to 7d9622a (#948)
* Update golang.org/x/sys commit hash to bfb29a6 (#951)
* Update golang.org/x/sys commit hash to d867a43 (#934)
* Update golang.org/x/sys commit hash to e5e7981 (#933)
* Update golang.org/x/sys commit hash to f52c844 (#954)
* Update golang.org/x/term commit hash to 6886f2d (#887)
* Update module getsentry/sentry-go to v0.11.0 (#869)
* Update module github.com/coreos/go-oidc to v3 (#885)
* Update module github.com/gabriel-vasile/mimetype to v1.3.1 (#904)
* Update module github.com/golang-jwt/jwt to v3.2.2 (#928)
* Update module github.com/golang-jwt/jwt to v4 (#930)
* Update module github.com/go-redis/redis/v8 to v8.11.0 (#903)
* Update module github.com/go-redis/redis/v8 to v8.11.1 (#925)
* Update module github.com/go-redis/redis/v8 to v8.11.2 (#932)
* Update module github.com/go-redis/redis/v8 to v8.11.3 (#942)
* Update module github.com/iancoleman/strcase to v0.2.0 (#918)
* Update module github.com/labstack/echo/v4 to v4.4.0 (#917)
* Update module github.com/labstack/echo/v4 to v4.5.0 (#929)
* Update module github.com/mattn/go-sqlite3 to v1.14.8 (#921)
* Update module github.com/spf13/cobra to v1.2.0 (#905)
* Update module github.com/spf13/cobra to v1.2.1 (#906)
* Update module github.com/spf13/viper to v1.8.0 (#890)
* Update module github.com/spf13/viper to v1.8.1 (#899)
* Update module github.com/swaggo/swag to v1.7.1 (#936)
* Update module github.com/yuin/goldmark to v1.3.8 (#892)
* Update module github.com/yuin/goldmark to v1.3.9 (#901)
* Update module github.com/yuin/goldmark to v1.4.0 (#908)
* Update module go-redis/redis/v8 to v8.10.0 (#882)
* Update module go-redis/redis/v8 to v8.7.1 (#807)
* Update module go-testfixtures/testfixtures/v3 to v3.6.1 (#868)
* Update module lib/pq to v1.10.2 (#865)
* Update module prometheus/client_golang to v1.11.0 (#879)
* Update module yuin/goldmark to v1.3.6 (#863)
* Update module yuin/goldmark to v1.3.7 (#867)
* Update monachus/hugo Docker tag to v0.75.1 (#940)
## [0.17.1] - 2021-06-09
### Fixed
* Fix parsing openid config when using a json config file
## [0.17.0] - 2021-05-14
### Added
@ -619,7 +446,7 @@ All releases can be found on https://code.vikunja.io/api/releases.
## [0.14.0] - 2020-07-01
### Added
### Added
* Add ability to run the docker container with configurable user and group ids
* Add better errors if the sqlite db file is not writable
@ -833,7 +660,7 @@ All releases can be found on https://code.vikunja.io/api/releases.
## [0.12] - 2020-04-04
#### Added
#### Added
* Add support for archiving lists and namespaces (#152)
* Colors for lists and namespaces (#155)
@ -857,7 +684,7 @@ All releases can be found on https://code.vikunja.io/api/releases.
## [0.11] - 2020-03-01
### Added
### Added
* Add config options for cors handling (#124)
* Add config options for task attachments (#125)
@ -925,7 +752,7 @@ All releases can be found on https://code.vikunja.io/api/releases.
## [0.9] - 2019-11-24
### Added
### Added
* Task Attachments (#104)
* Task Relations (#103)
@ -938,8 +765,7 @@ All releases can be found on https://code.vikunja.io/api/releases.
### Fixed
* Fix default logging settings (#107)
* Fixed a bug where adding assignees or reminders via an update would re-create them and not respect already inserted
ones, leaving a lot of garbage
* Fixed a bug where adding assignees or reminders via an update would re-create them and not respect already inserted ones, leaving a lot of garbage
* Fixed a bug where deleting an attachment would cause a nil panic
* Fixed building docs theme
* Fixed error when setting max file size on 32-Bit systems
@ -952,7 +778,7 @@ All releases can be found on https://code.vikunja.io/api/releases.
* Fixed removing reminders
* Small link share fixes (#96)
### Changed
### Changed
* Improve pagination (#105)
* Moved `teams_{namespace|list}_*` to `{namespace|list}_teams_*` for better consistency (#101)
@ -1073,8 +899,7 @@ All releases can be found on https://code.vikunja.io/api/releases.
* Updated libraries
* Updated drone to version 1
* Releases are now signed with our pgp key (more info about this
on [the download page](https://vikunja.io/en/download/)).
* Releases are now signed with our pgp key (more info about this on [the download page](https://vikunja.io/en/download/)).
## [0.5] - 2018-12-02
@ -1104,7 +929,7 @@ All releases can be found on https://code.vikunja.io/api/releases.
## [0.3] - 2018-11-02
### Added
### Added
* Password reset
* Email verification when registering

View File

@ -23,7 +23,7 @@ RUN if [ -n "${VIKUNJA_VERSION}" ]; then git checkout "${VIKUNJA_VERSION}"; fi \
# The actual image
# Note: I wanted to use the scratch image here, but unfortunatly the go-sqlite bindings require cgo and
# because of this, the container would not start when I compiled the image without cgo.
FROM alpine:3.12
FROM alpine:3.14
LABEL maintainer="maintainers@vikunja.io"
WORKDIR /app/vikunja/

View File

@ -2,7 +2,7 @@
[![Build Status](https://drone.kolaente.de/api/badges/vikunja/api/status.svg)](https://drone.kolaente.de/vikunja/api)
[![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](LICENSE)
[![Download](https://img.shields.io/badge/download-v0.18.1-brightgreen.svg)](https://dl.vikunja.io)
[![Download](https://img.shields.io/badge/download-v0.17.0-brightgreen.svg)](https://dl.vikunja.io)
[![Docker Pulls](https://img.shields.io/docker/pulls/vikunja/api.svg)](https://hub.docker.com/r/vikunja/api/)
[![Swagger Docs](https://img.shields.io/badge/swagger-docs-brightgreen.svg)](https://try.vikunja.io/api/v1/docs)
[![Go Report Card](https://goreportcard.com/badge/kolaente.dev/vikunja/api)](https://goreportcard.com/report/kolaente.dev/vikunja/api)

View File

@ -1,17 +1,17 @@
image: vikunja/api:unstable
image: vikunja/api:latest
manifests:
-
image: vikunja/api:unstable-linux-amd64
image: vikunja/api:latest-linux-amd64
platform:
architecture: amd64
os: linux
-
image: vikunja/api:unstable-linux-arm64
image: vikunja/api:latest-linux-arm64
platform:
architecture: arm64
os: linux
-
image: vikunja/api:unstable-linux-arm
image: vikunja/api:latest-linux-arm
platform:
architecture: arm
os: linux

View File

@ -31,10 +31,10 @@ menu:
url: https://vikunja.io/en/
weight: 10
- name: Features
url: https://vikunja.io/features
url: https://vikunja.io/en/features
weight: 20
- name: Download
url: https://vikunja.io/download
url: https://vikunja.io/en/download
weight: 30
- name: Docs
url: https://vikunja.io/docs

View File

@ -14,17 +14,7 @@ It is possible to migrate data from other to-do services to Vikunja.
To make this easier, we have put together a few helpers which are documented on this page.
In general, each migrator implements a migrator interface which is then called from a client.
The interface makes it possible to use helper methods which handle http and focus only on the implementation of the migrator itself.
There are two ways of migrating data from another service:
1. Through the auth-based flow where the user gives you access to their data at the third-party service through an
oauth flow. You can then call the service's api on behalf of your user to get all the data.
The Todoist, Trello and Microsoft To-Do Migrators use this pattern.
2. A file migration where the user uploads a file obtained from some third-party service. In your migrator, you need
to parse the file and create the lists, tasks etc.
The Vikunja File Import uses this pattern.
To differentiate the two, there are two different interfaces you must implement.
The interface makes it possible to use helper methods which handle http an focus only on the implementation of the migrator itself.
{{< table_of_contents >}}
@ -33,16 +23,13 @@ To differentiate the two, there are two different interfaces you must implement.
All migrator implementations live in their own package in `pkg/modules/migration/<name-of-the-service>`.
When creating a new migrator, you should place all related code inside that module.
## Migrator Interface
## Migrator interface
The migrator interface is defined as follows:
```go
// Migrator is the basic migrator interface which is shared among all migrators
type Migrator interface {
// Name holds the name of the migration.
// This is used to show the name to users and to keep track of users who already migrated.
Name() string
// Migrate is the interface used to migrate a user's tasks from another platform to vikunja.
// The user object is the user who's tasks will be migrated.
Migrate(user *models.User) error
@ -50,20 +37,9 @@ type Migrator interface {
// The use case for this are Oauth flows, where the server token should remain hidden and not
// known to the frontend.
AuthURL() string
}
```
## File Migrator Interface
```go
// FileMigrator handles importing Vikunja data from a file. The implementation of it determines the format.
type FileMigrator interface {
// Name holds the name of the migration.
// This is used to show the name to users and to keep track of users who already migrated.
Name() string
// Migrate is the interface used to migrate a user's tasks, list and other things from a file to vikunja.
// The user object is the user who's tasks will be migrated.
Migrate(user *user.User, file io.ReaderAt, size int64) error
}
```
@ -78,26 +54,15 @@ authUrl, Status and Migrate methods.
```go
// This is an example for the Wunderlist migrator
if config.MigrationWunderlistEnable.GetBool() {
wunderlistMigrationHandler := &migrationHandler.MigrationWeb{
wunderlistMigrationHandler := &migrationHandler.MigrationWeb{
MigrationStruct: func() migration.Migrator {
return &wunderlist.Migration{}
},
}
wunderlistMigrationHandler.RegisterRoutes(m)
wunderlistMigrationHandler.RegisterRoutes(m)
}
```
And for the file migrator:
```go
vikunjaFileMigrationHandler := &migrationHandler.FileMigratorWeb{
MigrationStruct: func() migration.FileMigrator {
return &vikunja_file.FileMigrator{}
},
}
vikunjaFileMigrationHandler.RegisterRoutes(m)
```
You should also document the routes with [swagger annotations]({{< ref "swagger-docs.md" >}}).
## Insertion helper method
@ -105,8 +70,7 @@ You should also document the routes with [swagger annotations]({{< ref "swagger-
There is a method available in the `migration` package which takes a fully nested Vikunja structure and creates it with all relations.
This means you start by adding a namespace, then add lists inside of that namespace, then tasks in the lists and so on.
The root structure must be present as `[]*models.NamespaceWithListsAndTasks`. It allows to represent all of Vikunja's
hierachie as a single data structure.
The root structure must be present as `[]*models.NamespaceWithLists`.
Then call the method like so:
@ -121,16 +85,14 @@ err = migration.InsertFromStructure(fullVikunjaHierachie, user)
## Configuration
If your migrator is an oauth-based one, you should add at least an option to enable or disable it.
You should add at least an option to enable or disable the migration.
Chances are, you'll need some more options for things like client ID and secret
(if the other service uses oAuth as an authentication flow).
The easiest way to implement an on/off switch is to check whether your migration service is enabled or not when
registering the routes, and then simply don't registering the routes in case it is disabled.
File based migrators can always be enabled.
registering the routes, and then simply don't registering the routes in the case it is disabled.
### Making the migrator public in `/info`
You should make your migrator available in the `/info` endpoint so that frontends can display options to enable them or not.
To do this, add an entry to the `AvailableMigrators` field in `pkg/routes/api/v1/info.go`.
To do this, add an entry to `pkg/routes/api/v1/info.go`.

View File

@ -25,52 +25,29 @@ Genauer definiert:
* “falsch” anstatt “nicht korrekt/inkorrekt”
* “Wende dich an …” anstatt “kontaktiere …”
* In derselben Zeit übersetzen (sonst wird aus dem englischen “is“ das deutsche “war”)
* Richtige Anführungszeichen verwenden. Also `„“` statt `''` oder `'` oder ` oder ´
* Richtige Anführungszeichen verwenden. Also „“ statt '' oder ' oder ` oder ´
* `„` für beginnende Anführungszeichen, `“` für schließende Anführungszeichen
Es gelten Artikel und Worttrennungen aus dem [Duden](https://duden.de).
## Formulierungen
## Gendern
* `Account` statt `Konto`.
* `TOTP` immer als ein Wort und Groß.
* `CalDAV` immer so.
* `löschen` oder `entfernen` je nach Kontext. Wenn etwas *gelöscht* wird, existiert das gelöschte Objekt und danach
nicht mehr und hat evtl. andere Objekte mitgelöscht (z.B. eine Aufgabe). Wird etwas *entfernt*, bezieht sich das
meistens auf die Beziehung zu einem anderen Objekt. Das entfernte Objekt existiert danach immernoch, z.B. beim
Entfernen eine:r Nutzer:in aus einem Team.
* Analog zu `löschen` oder `entfernen` gilt ähnliches für `hinzufügen` oder `erstellen`. Eine Aufgabe wird *erstellt*,
aber ein:e Nutzer:in nur zu einem Team *hinzugefügt*.
* `Anmeldename` anstatt `Benutzer:innenname`
Wo möglich, sollte eine geschlechtsneutrale Anrede verwendet werden.
Falls diese sehr umständlich würden (siehe oben „Amtsdeutsch-Umschreibungen“), soll mit *Doppelpunkt* gegendert werden.
Beispiel: „Benutzer:in“
## Formulierungen in Modals und Buttons
Es sollten die gleichen Formulierungen auf Buttons und Modals verwendet werden.
Beispiel: Wenn der Button mit `löschen` beschriftet ist, sollte im Modal die Frage
lauten `Willst du das wirklich löschen?` und nicht `Willst du das wirklich entfernen?`. Gleiches gilt für
Erfolgs/Fehlermeldungen nach der Aktion.
## Gendern
Wo möglich, sollte eine geschlechtsneutrale Anrede verwendet werden. Falls diese sehr umständlich würden (siehe oben
„Amtsdeutsch-Umschreibungen“), soll mit *Doppelpunkt* gegendert werden.
Beispiel: „Benutzer:in“
Beispiel: Wenn der Button mit `löschen` beschriftet ist, sollte im Modal die Frage lauten `Willst du das wirklich löschen?` und nicht `Willst du das wirklich entfernen?`.
Gleiches gilt für Erfolgs/Fehlermeldungen nach der Aktion.
## Trennungen
* E-Mail-Adresse (siehe Duden)
## Wörter und Ausdrücke
| Englisches Original | Verwendung in deutscher Übersetzung |
| ------------------- | -------------------- |
| Bucket | Spalte |
| Namespace | Namespace |
| Link Share | Linkfreigabe |
| Username | Anmeldename |
## Weiterführende Links
* http://docs.translatehouse.org/projects/localization-guide/en/latest/guide/translation_guidelines_german.html

View File

@ -48,7 +48,7 @@ Strings in other languages will be synced through weblate and should not be adde
## Requesting a new language
If you want to start translating Vikunja in a language not yet available in Vikunja, please request the language through the crowdin interface.
If you have issues with this or need a discussion before doing so, please [contact us](https://vikunja.io/contact/) or [start a discussion in the forum](https://community.vikunja.io).
If you want to start translating Vikunja in a language not yet available in Vikunja, please request the language through the weblate interface.
If you have issues with this or need a discussion before doing so, pleace [contact us](https://vikunja.io/contact/) or [start a discussion in the forum](https://community.vikunja.io).
Once at least 50% of all translation strings are translated and approved, they will be added and distributed with the Vikunja frontend for users to select and use Vikunja with them.

View File

@ -203,9 +203,9 @@ services:
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
environment:
MYSQL_ROOT_PASSWORD: supersecret
MYSQL_USER: vikunja
MYSQL_PASSWORD: secret
MYSQL_DATABASE: vikunja
MYSQL_USER: vikunja
MYSQL_PASSWORD: secret
MYSQL_DATABASE: vikunja
volumes:
- ./db:/var/lib/mysql
restart: unless-stopped
@ -260,10 +260,10 @@ services:
image: mariadb:10
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
environment:
MYSQL_ROOT_PASSWORD: supersecret
MYSQL_USER: vikunja
MYSQL_PASSWORD: secret
MYSQL_DATABASE: vikunja
MYSQL_ROOT_PASSWORD: supersecret
MYSQL_USER: vikunja
MYSQL_PASSWORD: secret
MYSQL_DATABASE: vikunja
volumes:
- ./db:/var/lib/mysql
restart: unless-stopped

View File

@ -44,10 +44,3 @@ This document provides an overview and instructions for the different methods.
A third-party Helm Chart is available from the k8s-at-home project [here](https://github.com/k8s-at-home/charts/tree/master/charts/stable/vikunja).
## Other installation resources
* [Docker Compose is MUCH Easier Than you Think - Let's Install Vikunja](https://www.youtube.com/watch?v=fGlz2PkXjuo) (Youtube)
* [Setup Vikunja using Docker Compose - Homelab Wiki](https://thehomelab.wiki/books/docker/page/setup-vikunja-using-docker-compose)
* [A Closer look at Vikunja - Email Notifications - Enable or Disable Registrations - Allow Attachments](https://www.youtube.com/watch?v=47wj9pRT6Gw) (Youtube)
* [Install Vikunja in Docker for self-hosted Task Tracking](https://smarthomepursuits.com/install-vikunja-in-docker-for-self-hosted-task-tracking/)

4
go.mod
View File

@ -44,7 +44,7 @@ require (
github.com/labstack/echo/v4 v4.5.0
github.com/labstack/gommon v0.3.0
github.com/laurent22/ical-go v0.1.1-0.20181107184520-7e5d6ade8eef
github.com/lib/pq v1.10.3
github.com/lib/pq v1.10.2
github.com/magefile/mage v1.11.0
github.com/mattn/go-isatty v0.0.13 // indirect
github.com/mattn/go-sqlite3 v1.14.8
@ -66,7 +66,7 @@ require (
golang.org/x/net v0.0.0-20210614182718-04defd469f4e // indirect
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
golang.org/x/sys v0.0.0-20210909193231-528a39cd75f3
golang.org/x/sys v0.0.0-20210820121016-41cdb8703e55
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/d4l3k/messagediff.v1 v1.2.1

20
go.sum
View File

@ -462,8 +462,6 @@ github.com/lib/pq v1.7.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.10.3 h1:v9QZf2Sn6AmjXtQeFpdoq/eaNtYP6IN+7lcrygsIAtg=
github.com/lib/pq v1.10.3/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lithammer/shortuuid/v3 v3.0.4 h1:uj4xhotfY92Y1Oa6n6HUiFn87CdoEHYUlTy0+IgbLrs=
github.com/lithammer/shortuuid/v3 v3.0.4/go.mod h1:RviRjexKqIzx/7r1peoAITm6m7gnif/h+0zmolKJjzw=
github.com/magefile/mage v1.11.0 h1:C/55Ywp9BpgVVclD3lRnSYCwXTYxmSppIgLeDYlNuls=
@ -949,24 +947,6 @@ golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c h1:Lyn7+CqXIiC+LOR9aHD6jDK+h
golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210820121016-41cdb8703e55 h1:rw6UNGRMfarCepjI8qOepea/SXwIBVfTKjztZ5gBbq4=
golang.org/x/sys v0.0.0-20210820121016-41cdb8703e55/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf h1:2ucpDCmfkl8Bd/FsLtiD653Wf96cW37s+iGx93zsu4k=
golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e h1:XMgFehsDnnLGtjvjOfqWSUzt0alpTR1RSEuznObga2c=
golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210902050250-f475640dd07b h1:S7hKs0Flbq0bbc9xgYt4stIEG1zNDFqyrPwAX2Wj/sE=
golang.org/x/sys v0.0.0-20210902050250-f475640dd07b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210903071746-97244b99971b h1:3Dq0eVHn0uaQJmPO+/aYPI/fRMqdrVDbu7MQcku54gg=
golang.org/x/sys v0.0.0-20210903071746-97244b99971b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34 h1:GkvMjFtXUmahfDtashnc1mnrCtuBVcwse5QV2lUk/tI=
golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210908143011-c212e7322662 h1:2+M7sCYQcvlpag1ug05BCZa5B9jbazrHdgsOdwqlfE8=
golang.org/x/sys v0.0.0-20210908143011-c212e7322662/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210908160347-a851e7ddeee0 h1:6xxeVXiyYpF8WCTnKKCbjnEdsrwjZYY8TOuk7xP0chg=
golang.org/x/sys v0.0.0-20210908160347-a851e7ddeee0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365 h1:6wSTsvPddg9gc/mVEEyk9oOAoxn+bT4Z9q1zx+4RwA4=
golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210909193231-528a39cd75f3 h1:3Ad41xy2WCESpufXwgs7NpDSu+vjxqLt2UFqUV+20bI=
golang.org/x/sys v0.0.0-20210909193231-528a39cd75f3/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b h1:9zKuko04nR4gjZ4+DNjHqRlAJqbJETHwiNKDqTfOjfE=
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=

View File

@ -26,7 +26,7 @@ import (
"github.com/laurent22/ical-go"
)
func GetCaldavTodosForTasks(list *models.ListWithTasksAndBuckets, listTasks []*models.TaskWithComments) string {
func GetCaldavTodosForTasks(list *models.List, listTasks []*models.Task) string {
// Make caldav todos from Vikunja todos
var caldavtodos []*Todo

View File

@ -22,8 +22,6 @@ import (
"strconv"
"time"
"xorm.io/xorm"
"code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/log"
@ -79,18 +77,7 @@ func Create(f io.Reader, realname string, realsize uint64, a web.Auth) (file *Fi
// CreateWithMime creates a new file from an FileHeader and sets its mime type
func CreateWithMime(f io.Reader, realname string, realsize uint64, a web.Auth, mime string) (file *File, err error) {
s := db.NewSession()
defer s.Close()
file, err = CreateWithMimeAndSession(s, f, realname, realsize, a, mime)
if err != nil {
_ = s.Rollback()
return
}
return
}
func CreateWithMimeAndSession(s *xorm.Session, f io.Reader, realname string, realsize uint64, a web.Auth, mime string) (file *File, err error) {
// Get and parse the configured file size
var maxSize datasize.ByteSize
err = maxSize.UnmarshalText([]byte(config.FilesMaxSize.GetString()))
@ -109,13 +96,21 @@ func CreateWithMimeAndSession(s *xorm.Session, f io.Reader, realname string, rea
Mime: mime,
}
s := db.NewSession()
defer s.Close()
_, err = s.Insert(file)
if err != nil {
_ = s.Rollback()
return
}
// Save the file to storage with its new ID as path
err = file.Save(f)
if err != nil {
_ = s.Rollback()
return
}
return
}

View File

@ -97,7 +97,6 @@ func FullInit() {
user.RegisterTokenCleanupCron()
user.RegisterDeletionNotificationCron()
models.RegisterUserDeletionCron()
models.RegisterOldExportCleanupCron()
// Start processing events
go func() {

View File

@ -1,43 +0,0 @@
// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-2021 Vikunja and contributors. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public Licensee as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public Licensee for more details.
//
// You should have received a copy of the GNU Affero General Public Licensee
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package migration
import (
"src.techknowlogick.com/xormigrate"
"xorm.io/xorm"
)
type users20210829194722 struct {
ExportFileID int64 `xorm:"bigint null" json:"-"`
}
func (users20210829194722) TableName() string {
return "users"
}
func init() {
migrations = append(migrations, &xormigrate.Migration{
ID: "20210829194722",
Description: "Add data export file id to users",
Migrate: func(tx *xorm.Engine) error {
return tx.Sync2(users20210829194722{})
},
Rollback: func(tx *xorm.Engine) error {
return nil
},
})
}

View File

@ -21,16 +21,6 @@ import (
"code.vikunja.io/web"
)
// DataExportRequestEvent represents a DataExportRequestEvent event
type DataExportRequestEvent struct {
User *user.User
}
// Name defines the name for DataExportRequestEvent
func (t *DataExportRequestEvent) Name() string {
return "user.export.request"
}
/////////////////
// Task Events //
/////////////////
@ -267,13 +257,3 @@ type TeamDeletedEvent struct {
func (t *TeamDeletedEvent) Name() string {
return "team.deleted"
}
// UserDataExportRequestedEvent represents a UserDataExportRequestedEvent event
type UserDataExportRequestedEvent struct {
User *user.User
}
// Name defines the name for UserDataExportRequestedEvent
func (t *UserDataExportRequestedEvent) Name() string {
return "user.export.requested"
}

View File

@ -1,366 +0,0 @@
// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-2021 Vikunja and contributors. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public Licensee as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public Licensee for more details.
//
// You should have received a copy of the GNU Affero General Public Licensee
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package models
import (
"archive/zip"
"encoding/json"
"fmt"
"io"
"os"
"strconv"
"time"
"code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/cron"
"code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/files"
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/notifications"
"code.vikunja.io/api/pkg/user"
"code.vikunja.io/api/pkg/utils"
"code.vikunja.io/api/pkg/version"
"xorm.io/xorm"
)
func ExportUserData(s *xorm.Session, u *user.User) (err error) {
exportDir := config.FilesBasePath.GetString() + "/user-export-tmp/"
err = os.MkdirAll(exportDir, 0700)
if err != nil {
return err
}
tmpFilename := exportDir + strconv.FormatInt(u.ID, 10) + "_" + time.Now().Format("2006-01-02_15-03-05") + ".zip"
// Open zip
dumpFile, err := os.Create(tmpFilename)
if err != nil {
return fmt.Errorf("error opening dump file: %s", err)
}
defer dumpFile.Close()
dumpWriter := zip.NewWriter(dumpFile)
defer dumpWriter.Close()
// Get the data
err = exportListsAndTasks(s, u, dumpWriter)
if err != nil {
return err
}
// Task attachment files
err = exportTaskAttachments(s, u, dumpWriter)
if err != nil {
return err
}
// Saved filters
err = exportSavedFilters(s, u, dumpWriter)
if err != nil {
return err
}
// Background files
err = exportListBackgrounds(s, u, dumpWriter)
if err != nil {
return err
}
// Vikunja Version
err = utils.WriteBytesToZip("VERSION", []byte(version.Version), dumpWriter)
if err != nil {
return err
}
// If we reuse the same file again, saving it as a file in Vikunja will save it as a file with 0 bytes in size.
// Closing and reopening does work.
dumpWriter.Close()
dumpFile.Close()
exported, err := os.Open(tmpFilename)
if err != nil {
return err
}
stat, err := exported.Stat()
if err != nil {
return err
}
exportFile, err := files.CreateWithMimeAndSession(s, exported, tmpFilename, uint64(stat.Size()), u, "application/zip")
if err != nil {
return err
}
// Save the file id with the user
u.ExportFileID = exportFile.ID
_, err = s.Cols("export_file_id").Update(u)
if err != nil {
return
}
// Remove the old file
err = os.Remove(exported.Name())
if err != nil {
return err
}
// Send a notification
return notifications.Notify(u, &DataExportReadyNotification{
User: u,
})
}
func exportListsAndTasks(s *xorm.Session, u *user.User, wr *zip.Writer) (err error) {
namspaces, _, _, err := (&Namespace{IsArchived: true}).ReadAll(s, u, "", -1, 0)
if err != nil {
return err
}
namespaceIDs := []int64{}
namespaces := []*NamespaceWithListsAndTasks{}
listMap := make(map[int64]*ListWithTasksAndBuckets)
listIDs := []int64{}
for _, n := range namspaces.([]*NamespaceWithLists) {
if n.ID < 1 {
// Don't include filters
continue
}
nn := &NamespaceWithListsAndTasks{
Namespace: n.Namespace,
Lists: []*ListWithTasksAndBuckets{},
}
for _, l := range n.Lists {
ll := &ListWithTasksAndBuckets{
List: *l,
BackgroundFileID: l.BackgroundFileID,
Tasks: []*TaskWithComments{},
}
nn.Lists = append(nn.Lists, ll)
listMap[l.ID] = ll
listIDs = append(listIDs, l.ID)
}
namespaceIDs = append(namespaceIDs, n.ID)
namespaces = append(namespaces, nn)
}
if len(namespaceIDs) == 0 {
return nil
}
// Get all lists
lists, err := getListsForNamespaces(s, namespaceIDs, true)
if err != nil {
return err
}
tasks, _, _, err := getTasksForLists(s, lists, u, &taskOptions{
page: 0,
perPage: -1,
})
if err != nil {
return err
}
taskMap := make(map[int64]*TaskWithComments, len(tasks))
for _, t := range tasks {
taskMap[t.ID] = &TaskWithComments{
Task: *t,
}
if _, exists := listMap[t.ListID]; !exists {
log.Debugf("[User Data Export] List %d does not exist for task %d, omitting", t.ListID, t.ID)
continue
}
listMap[t.ListID].Tasks = append(listMap[t.ListID].Tasks, taskMap[t.ID])
}
comments := []*TaskComment{}
err = s.
Join("LEFT", "tasks", "tasks.id = task_comments.task_id").
In("tasks.list_id", listIDs).
Find(&comments)
if err != nil {
return
}
for _, c := range comments {
if _, exists := taskMap[c.TaskID]; !exists {
log.Debugf("[User Data Export] Task %d does not exist for comment %d, omitting", c.TaskID, c.ID)
continue
}
taskMap[c.TaskID].Comments = append(taskMap[c.TaskID].Comments, c)
}
buckets := []*Bucket{}
err = s.In("list_id", listIDs).Find(&buckets)
if err != nil {
return
}
for _, b := range buckets {
if _, exists := listMap[b.ListID]; !exists {
log.Debugf("[User Data Export] List %d does not exist for bucket %d, omitting", b.ListID, b.ID)
continue
}
listMap[b.ListID].Buckets = append(listMap[b.ListID].Buckets, b)
}
data, err := json.Marshal(namespaces)
if err != nil {
return err
}
return utils.WriteBytesToZip("data.json", data, wr)
}
func exportTaskAttachments(s *xorm.Session, u *user.User, wr *zip.Writer) (err error) {
lists, _, _, err := getRawListsForUser(
s,
&listOptions{
user: u,
page: -1,
},
)
if err != nil {
return err
}
tasks, _, _, err := getRawTasksForLists(s, lists, u, &taskOptions{page: -1})
if err != nil {
return err
}
taskIDs := []int64{}
for _, t := range tasks {
taskIDs = append(taskIDs, t.ID)
}
tas, err := getTaskAttachmentsByTaskIDs(s, taskIDs)
if err != nil {
return err
}
fs := make(map[int64]io.ReadCloser)
for _, ta := range tas {
if err := ta.File.LoadFileByID(); err != nil {
return err
}
fs[ta.FileID] = ta.File.File
}
return utils.WriteFilesToZip(fs, wr)
}
func exportSavedFilters(s *xorm.Session, u *user.User, wr *zip.Writer) (err error) {
filters, err := getSavedFiltersForUser(s, u)
if err != nil {
return err
}
data, err := json.Marshal(filters)
if err != nil {
return err
}
return utils.WriteBytesToZip("filters.json", data, wr)
}
func exportListBackgrounds(s *xorm.Session, u *user.User, wr *zip.Writer) (err error) {
lists, _, _, err := getRawListsForUser(
s,
&listOptions{
user: u,
page: -1,
},
)
if err != nil {
return err
}
fs := make(map[int64]io.ReadCloser)
for _, l := range lists {
if l.BackgroundFileID == 0 {
continue
}
bgFile := &files.File{
ID: l.BackgroundFileID,
}
err = bgFile.LoadFileByID()
if err != nil {
return
}
fs[l.BackgroundFileID] = bgFile.File
}
return utils.WriteFilesToZip(fs, wr)
}
func RegisterOldExportCleanupCron() {
const logPrefix = "[User Export Cleanup Cron] "
err := cron.Schedule("0 * * * *", func() {
s := db.NewSession()
defer s.Close()
users := []*user.User{}
err := s.Where("export_file_id IS NOT NULL AND export_file_id != ?", 0).Find(&users)
if err != nil {
log.Errorf(logPrefix+"Could not get users with export files: %s", err)
return
}
fileIDs := []int64{}
for _, u := range users {
fileIDs = append(fileIDs, u.ExportFileID)
}
fs := []*files.File{}
err = s.Where("created < ?", time.Now().Add(-time.Hour*24*7)).In("id", fileIDs).Find(&fs)
if err != nil {
log.Errorf(logPrefix+"Could not get users with export files: %s", err)
return
}
if len(fs) == 0 {
return
}
log.Debugf(logPrefix+"Removing %d old user data exports...", len(fs))
for _, f := range fs {
err = f.Delete()
if err != nil {
log.Errorf(logPrefix+"Could not remove user export file %d: %s", f.ID, err)
return
}
}
_, err = s.In("export_file_id", fileIDs).Cols("export_file_id").Update(&user.User{})
if err != nil {
log.Errorf(logPrefix+"Could not update user export file state: %s", err)
return
}
log.Debugf(logPrefix+"Removed %d old user data exports...", len(fs))
})
if err != nil {
log.Fatalf("Could not old export cleanup cron: %s", err)
}
}

View File

@ -51,6 +51,12 @@ type List struct {
// The user who created this list.
Owner *user.User `xorm:"-" json:"owner" valid:"-"`
// An array of tasks which belong to the list.
// Deprecated: you should use the dedicated task list endpoint because it has support for pagination and filtering
Tasks []*Task `xorm:"-" json:"-"`
// Only used for migration.
Buckets []*Bucket `xorm:"-" json:"-"`
// Whether or not a list is archived.
IsArchived bool `xorm:"not null default false" json:"is_archived" query:"is_archived"`
@ -79,15 +85,6 @@ type List struct {
web.Rights `xorm:"-" json:"-"`
}
type ListWithTasksAndBuckets struct {
List
// An array of tasks which belong to the list.
Tasks []*TaskWithComments `xorm:"-" json:"tasks"`
// Only used for migration.
Buckets []*Bucket `xorm:"-" json:"buckets"`
BackgroundFileID int64 `xorm:"null" json:"background_file_id"`
}
// TableName returns a better name for the lists table
func (l *List) TableName() string {
return "lists"

View File

@ -50,7 +50,6 @@ func RegisterListeners() {
events.RegisterListener((&TaskCommentUpdatedEvent{}).Name(), &HandleTaskCommentEditMentions{})
events.RegisterListener((&TaskCreatedEvent{}).Name(), &HandleTaskCreateMentions{})
events.RegisterListener((&TaskUpdatedEvent{}).Name(), &HandleTaskUpdatedMentions{})
events.RegisterListener((&UserDataExportRequestedEvent{}).Name(), &HandleUserDataExport{})
}
//////
@ -563,41 +562,3 @@ func (s *SendTeamMemberAddedNotification) Handle(msg *message.Message) (err erro
Team: event.Team,
})
}
// HandleUserDataExport represents a listener
type HandleUserDataExport struct {
}
// Name defines the name for the HandleUserDataExport listener
func (s *HandleUserDataExport) Name() string {
return "handle.user.data.export"
}
// Handle is executed when the event HandleUserDataExport listens on is fired
func (s *HandleUserDataExport) Handle(msg *message.Message) (err error) {
event := &UserDataExportRequestedEvent{}
err = json.Unmarshal(msg.Payload, event)
if err != nil {
return err
}
log.Debugf("Starting to export user data for user %d...", event.User.ID)
sess := db.NewSession()
defer sess.Close()
err = sess.Begin()
if err != nil {
return
}
err = ExportUserData(sess, event.User)
if err != nil {
_ = sess.Rollback()
return
}
log.Debugf("Done exporting user data for user %d...", event.User.ID)
err = sess.Commit()
return err
}

View File

@ -187,11 +187,6 @@ type NamespaceWithLists struct {
Lists []*List `xorm:"-" json:"lists"`
}
type NamespaceWithListsAndTasks struct {
Namespace
Lists []*ListWithTasksAndBuckets `xorm:"-" json:"lists"`
}
func makeNamespaceSlice(namespaces map[int64]*NamespaceWithLists, userMap map[int64]*user.User, subscriptions map[int64]*Subscription) []*NamespaceWithLists {
all := make([]*NamespaceWithLists, 0, len(namespaces))
for _, n := range namespaces {

View File

@ -302,29 +302,3 @@ func (n *UserMentionedInTaskNotification) ToDB() interface{} {
func (n *UserMentionedInTaskNotification) Name() string {
return "task.mentioned"
}
// DataExportReadyNotification represents a DataExportReadyNotification notification
type DataExportReadyNotification struct {
User *user.User `json:"user"`
}
// ToMail returns the mail notification for DataExportReadyNotification
func (n *DataExportReadyNotification) ToMail() *notifications.Mail {
return notifications.NewMail().
Subject("Your Vikunja Data Export is ready").
Greeting("Hi "+n.User.GetName()+",").
Line("Your Vikunja Data Export is ready for you to download. Click the button below to download it:").
Action("Download", config.ServiceFrontendurl.GetString()+"user/export/download").
Line("The download will be available for the next 7 days.").
Line("Have a nice day!")
}
// ToDB returns the DataExportReadyNotification notification in a format which can be saved in the db
func (n *DataExportReadyNotification) ToDB() interface{} {
return nil
}
// Name returns the name of the notification
func (n *DataExportReadyNotification) Name() string {
return "data.export.ready"
}

View File

@ -129,11 +129,6 @@ type Task struct {
web.Rights `xorm:"-" json:"-"`
}
type TaskWithComments struct {
Task
Comments []*TaskComment `xorm:"-" json:"comments"`
}
// TableName returns the table name for listtasks
func (Task) TableName() string {
return "tasks"

View File

@ -21,15 +21,19 @@ import (
"fmt"
"io"
"os"
"strconv"
"code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/files"
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/utils"
"code.vikunja.io/api/pkg/version"
"github.com/spf13/viper"
)
// Change to deflate to gain better compression
// see http://golang.org/pkg/archive/zip/#pkg-constants
const compressionUsed = zip.Deflate
// Dump creates a zip file with all vikunja files at filename
func Dump(filename string) error {
dumpFile, err := os.Create(filename)
@ -51,7 +55,7 @@ func Dump(filename string) error {
// Version
log.Info("Start dumping version file...")
err = utils.WriteBytesToZip("VERSION", []byte(version.Version), dumpWriter)
err = writeBytesToZip("VERSION", []byte(version.Version), dumpWriter)
if err != nil {
return fmt.Errorf("error saving version: %s", err)
}
@ -64,7 +68,7 @@ func Dump(filename string) error {
return fmt.Errorf("error saving database data: %s", err)
}
for t, d := range data {
err = utils.WriteBytesToZip("database/"+t+".json", d, dumpWriter)
err = writeBytesToZip("database/"+t+".json", d, dumpWriter)
if err != nil {
return fmt.Errorf("error writing database table %s: %s", t, err)
}
@ -77,12 +81,21 @@ func Dump(filename string) error {
if err != nil {
return fmt.Errorf("error saving file: %s", err)
}
err = utils.WriteFilesToZip(allFiles, dumpWriter)
if err != nil {
return err
for fid, file := range allFiles {
header := &zip.FileHeader{
Name: "files/" + strconv.FormatInt(fid, 10),
Method: compressionUsed,
}
w, err := dumpWriter.CreateHeader(header)
if err != nil {
return err
}
_, err = io.Copy(w, file)
if err != nil {
return fmt.Errorf("error writing file %d: %s", fid, err)
}
_ = file.Close()
}
log.Infof("Dumped files")
log.Info("Done creating dump")
@ -110,7 +123,7 @@ func writeFileToZip(filename string, writer *zip.Writer) error {
}
header.Name = info.Name()
header.Method = utils.CompressionUsed
header.Method = compressionUsed
w, err := writer.CreateHeader(header)
if err != nil {
@ -119,3 +132,16 @@ func writeFileToZip(filename string, writer *zip.Writer) error {
_, err = io.Copy(w, fileToZip)
return err
}
func writeBytesToZip(filename string, data []byte, writer *zip.Writer) (err error) {
header := &zip.FileHeader{
Name: filename,
Method: compressionUsed,
}
w, err := writer.CreateHeader(header)
if err != nil {
return err
}
_, err = w.Write(data)
return
}

View File

@ -33,7 +33,6 @@ import (
"code.vikunja.io/api/pkg/initialize"
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/migration"
"src.techknowlogick.com/xormigrate"
)
@ -195,7 +194,6 @@ func Restore(filename string) error {
///////
// Done
log.Infof("Done restoring dump.")
log.Infof("Restart Vikunja to make sure the new configuration file is applied.")
return nil
}

View File

@ -31,7 +31,7 @@ import (
// InsertFromStructure takes a fully nested Vikunja data structure and a user and then creates everything for this user
// (Namespaces, tasks, etc. Even attachments and relations.)
func InsertFromStructure(str []*models.NamespaceWithListsAndTasks, user *user.User) (err error) {
func InsertFromStructure(str []*models.NamespaceWithLists, user *user.User) (err error) {
s := db.NewSession()
defer s.Close()
@ -45,7 +45,7 @@ func InsertFromStructure(str []*models.NamespaceWithListsAndTasks, user *user.Us
return s.Commit()
}
func insertFromStructure(s *xorm.Session, str []*models.NamespaceWithListsAndTasks, user *user.User) (err error) {
func insertFromStructure(s *xorm.Session, str []*models.NamespaceWithLists, user *user.User) (err error) {
log.Debugf("[creating structure] Creating %d namespaces", len(str))
@ -129,7 +129,7 @@ func insertFromStructure(s *xorm.Session, str []*models.NamespaceWithListsAndTas
// Create all tasks
for _, t := range tasks {
setBucketOrDefault(&t.Task)
setBucketOrDefault(t)
t.ListID = l.ID
err = t.Create(s, user)
@ -221,15 +221,6 @@ func insertFromStructure(s *xorm.Session, str []*models.NamespaceWithListsAndTas
}
log.Debugf("[creating structure] Associated task %d with label %d", t.ID, lb.ID)
}
for _, comment := range t.Comments {
comment.TaskID = t.ID
err = comment.Create(s, user)
if err != nil {
return
}
log.Debugf("[creating structure] Created new comment %d", comment.ID)
}
}
// All tasks brought their own bucket with them, therefore the newly created default bucket is just extra space

View File

@ -32,95 +32,79 @@ func TestInsertFromStructure(t *testing.T) {
}
t.Run("normal", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
testStructure := []*models.NamespaceWithListsAndTasks{
testStructure := []*models.NamespaceWithLists{
{
Namespace: models.Namespace{
Title: "Test1",
Description: "Lorem Ipsum",
},
Lists: []*models.ListWithTasksAndBuckets{
Lists: []*models.List{
{
List: models.List{
Title: "Testlist1",
Description: "Something",
},
Title: "Testlist1",
Description: "Something",
Buckets: []*models.Bucket{
{
ID: 1234,
Title: "Test Bucket",
},
},
Tasks: []*models.TaskWithComments{
Tasks: []*models.Task{
{
Task: models.Task{
Title: "Task1",
Description: "Lorem",
},
Title: "Task1",
Description: "Lorem",
},
{
Task: models.Task{
Title: "Task with related tasks",
RelatedTasks: map[models.RelationKind][]*models.Task{
models.RelationKindSubtask: {
{
Title: "Related to task with related task",
Description: "As subtask",
},
Title: "Task with related tasks",
RelatedTasks: map[models.RelationKind][]*models.Task{
models.RelationKindSubtask: {
{
Title: "Related to task with related task",
Description: "As subtask",
},
},
},
},
{
Task: models.Task{
Title: "Task with attachments",
Attachments: []*models.TaskAttachment{
{
File: &files.File{
Name: "testfile",
Size: 4,
FileContent: []byte{1, 2, 3, 4},
},
Title: "Task with attachments",
Attachments: []*models.TaskAttachment{
{
File: &files.File{
Name: "testfile",
Size: 4,
FileContent: []byte{1, 2, 3, 4},
},
},
},
},
{
Task: models.Task{
Title: "Task with labels",
Labels: []*models.Label{
{
Title: "Label1",
HexColor: "ff00ff",
},
{
Title: "Label2",
HexColor: "ff00ff",
},
Title: "Task with labels",
Labels: []*models.Label{
{
Title: "Label1",
HexColor: "ff00ff",
},
{
Title: "Label2",
HexColor: "ff00ff",
},
},
},
{
Task: models.Task{
Title: "Task with same label",
Labels: []*models.Label{
{
Title: "Label1",
HexColor: "ff00ff",
},
Title: "Task with same label",
Labels: []*models.Label{
{
Title: "Label1",
HexColor: "ff00ff",
},
},
},
{
Task: models.Task{
Title: "Task in a bucket",
BucketID: 1234,
},
Title: "Task in a bucket",
BucketID: 1234,
},
{
Task: models.Task{
Title: "Task in a nonexisting bucket",
BucketID: 1111,
},
Title: "Task in a nonexisting bucket",
BucketID: 1111,
},
},
},

View File

@ -1,40 +0,0 @@
// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-2021 Vikunja and contributors. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public Licensee as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public Licensee for more details.
//
// You should have received a copy of the GNU Affero General Public Licensee
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package handler
import (
"net/http"
"code.vikunja.io/api/pkg/modules/migration"
user2 "code.vikunja.io/api/pkg/user"
"code.vikunja.io/web/handler"
"github.com/labstack/echo/v4"
)
func status(ms migration.MigratorName, c echo.Context) error {
user, err := user2.GetCurrentUser(c)
if err != nil {
return handler.HandleHTTPError(err, c)
}
status, err := migration.GetMigrationStatus(ms, user)
if err != nil {
return handler.HandleHTTPError(err, c)
}
return c.JSON(http.StatusOK, status)
}

View File

@ -84,5 +84,15 @@ func (mw *MigrationWeb) Migrate(c echo.Context) error {
func (mw *MigrationWeb) Status(c echo.Context) error {
ms := mw.MigrationStruct()
return status(ms, c)
user, err := user2.GetCurrentUser(c)
if err != nil {
return handler.HandleHTTPError(err, c)
}
status, err := migration.GetMigrationStatus(ms, user)
if err != nil {
return handler.HandleHTTPError(err, c)
}
return c.JSON(http.StatusOK, status)
}

View File

@ -1,79 +0,0 @@
// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-2021 Vikunja and contributors. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public Licensee as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public Licensee for more details.
//
// You should have received a copy of the GNU Affero General Public Licensee
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package handler
import (
"net/http"
"code.vikunja.io/api/pkg/models"
"code.vikunja.io/api/pkg/modules/migration"
user2 "code.vikunja.io/api/pkg/user"
"code.vikunja.io/web/handler"
"github.com/labstack/echo/v4"
)
type FileMigratorWeb struct {
MigrationStruct func() migration.FileMigrator
}
// RegisterRoutes registers all routes for migration
func (fw *FileMigratorWeb) RegisterRoutes(g *echo.Group) {
ms := fw.MigrationStruct()
g.GET("/"+ms.Name()+"/status", fw.Status)
g.PUT("/"+ms.Name()+"/migrate", fw.Migrate)
}
// Migrate calls the migration method
func (fw *FileMigratorWeb) Migrate(c echo.Context) error {
ms := fw.MigrationStruct()
// Get the user from context
user, err := user2.GetCurrentUser(c)
if err != nil {
return handler.HandleHTTPError(err, c)
}
file, err := c.FormFile("import")
if err != nil {
return err
}
src, err := file.Open()
if err != nil {
return err
}
defer src.Close()
// Do the migration
err = ms.Migrate(user, src, file.Size)
if err != nil {
return handler.HandleHTTPError(err, c)
}
err = migration.SetMigrationStatus(ms, user)
if err != nil {
return handler.HandleHTTPError(err, c)
}
return c.JSON(http.StatusOK, models.Message{Message: "Everything was migrated successfully."})
}
// Status returns whether or not a user has already done this migration
func (fw *FileMigratorWeb) Status(c echo.Context) error {
ms := fw.MigrationStruct()
return status(ms, c)
}

View File

@ -243,15 +243,15 @@ func getMicrosoftTodoData(token string) (microsoftTodoData []*list, err error) {
return
}
func convertMicrosoftTodoData(todoData []*list) (vikunjsStructure []*models.NamespaceWithListsAndTasks, err error) {
func convertMicrosoftTodoData(todoData []*list) (vikunjsStructure []*models.NamespaceWithLists, err error) {
// One namespace with all lists
vikunjsStructure = []*models.NamespaceWithListsAndTasks{
vikunjsStructure = []*models.NamespaceWithLists{
{
Namespace: models.Namespace{
Title: "Migrated from Microsoft Todo",
},
Lists: []*models.ListWithTasksAndBuckets{},
Lists: []*models.List{},
},
}
@ -262,10 +262,8 @@ func convertMicrosoftTodoData(todoData []*list) (vikunjsStructure []*models.Name
log.Debugf("[Microsoft Todo Migration] Converting list %s", l.ID)
// Lists only with title
list := &models.ListWithTasksAndBuckets{
List: models.List{
Title: l.DisplayName,
},
list := &models.List{
Title: l.DisplayName,
}
log.Debugf("[Microsoft Todo Migration] Converting %d tasks", len(l.Tasks))
@ -342,7 +340,7 @@ func convertMicrosoftTodoData(todoData []*list) (vikunjsStructure []*models.Name
}
}
list.Tasks = append(list.Tasks, &models.TaskWithComments{Task: *task})
list.Tasks = append(list.Tasks, task)
log.Debugf("[Microsoft Todo Migration] Done converted %d tasks", len(l.Tasks))
}

View File

@ -102,79 +102,57 @@ func TestConverting(t *testing.T) {
},
}
expectedHierachie := []*models.NamespaceWithListsAndTasks{
expectedHierachie := []*models.NamespaceWithLists{
{
Namespace: models.Namespace{
Title: "Migrated from Microsoft Todo",
},
Lists: []*models.ListWithTasksAndBuckets{
Lists: []*models.List{
{
List: models.List{
Title: "List 1",
},
Tasks: []*models.TaskWithComments{
Title: "List 1",
Tasks: []*models.Task{
{
Task: models.Task{
Title: "Task 1",
Description: "This is a description",
Title: "Task 1",
Description: "This is a description",
},
{
Title: "Task 2",
Done: true,
DoneAt: testtimeTime,
},
{
Title: "Task 3",
Priority: 1,
},
{
Title: "Task 4",
Priority: 3,
},
{
Title: "Task 5",
Reminders: []time.Time{
testtimeTime,
},
},
{
Task: models.Task{
Title: "Task 2",
Done: true,
DoneAt: testtimeTime,
},
Title: "Task 6",
DueDate: testtimeTime,
},
{
Task: models.Task{
Title: "Task 3",
Priority: 1,
},
},
{
Task: models.Task{
Title: "Task 4",
Priority: 3,
},
},
{
Task: models.Task{
Title: "Task 5",
Reminders: []time.Time{
testtimeTime,
},
},
},
{
Task: models.Task{
Title: "Task 6",
DueDate: testtimeTime,
},
},
{
Task: models.Task{
Title: "Task 7",
DueDate: testtimeTime,
RepeatAfter: 60 * 60 * 24 * 7, // The amount of seconds in a week
},
Title: "Task 7",
DueDate: testtimeTime,
RepeatAfter: 60 * 60 * 24 * 7, // The amount of seconds in a week
},
},
},
{
List: models.List{
Title: "List 2",
},
Tasks: []*models.TaskWithComments{
Title: "List 2",
Tasks: []*models.Task{
{
Task: models.Task{
Title: "Task 1",
},
Title: "Task 1",
},
{
Task: models.Task{
Title: "Task 2",
},
Title: "Task 2",
},
},
},

View File

@ -37,7 +37,7 @@ func (s *Status) TableName() string {
}
// SetMigrationStatus sets the migration status for a user
func SetMigrationStatus(m MigratorName, u *user.User) (err error) {
func SetMigrationStatus(m Migrator, u *user.User) (err error) {
s := db.NewSession()
defer s.Close()
@ -50,7 +50,7 @@ func SetMigrationStatus(m MigratorName, u *user.User) (err error) {
}
// GetMigrationStatus returns the migration status for a migration and a user
func GetMigrationStatus(m MigratorName, u *user.User) (status *Status, err error) {
func GetMigrationStatus(m Migrator, u *user.User) (status *Status, err error) {
s := db.NewSession()
defer s.Close()

View File

@ -17,20 +17,11 @@
package migration
import (
"io"
"code.vikunja.io/api/pkg/user"
)
type MigratorName interface {
// Name holds the name of the migration.
// This is used to show the name to users and to keep track of users who already migrated.
Name() string
}
// Migrator is the basic migrator interface which is shared among all migrators
type Migrator interface {
MigratorName
// Migrate is the interface used to migrate a user's tasks from another platform to vikunja.
// The user object is the user who's tasks will be migrated.
Migrate(user *user.User) error
@ -38,12 +29,7 @@ type Migrator interface {
// The use case for this are Oauth flows, where the server token should remain hidden and not
// known to the frontend.
AuthURL() string
}
// FileMigrator handles importing Vikunja data from a file. The implementation of it determines the format.
type FileMigrator interface {
MigratorName
// Migrate is the interface used to migrate a user's tasks, list and other things from a file to vikunja.
// The user object is the user who's tasks will be migrated.
Migrate(user *user.User, file io.ReaderAt, size int64) error
// Title holds the name of the migration.
// This is used to show the name to users and to keep track of users who already migrated.
Name() string
}

View File

@ -252,30 +252,28 @@ func parseDate(dateString string) (date time.Time, err error) {
return date, err
}
func convertTodoistToVikunja(sync *sync, doneItems map[int64]*doneItem) (fullVikunjaHierachie []*models.NamespaceWithListsAndTasks, err error) {
func convertTodoistToVikunja(sync *sync, doneItems map[int64]*doneItem) (fullVikunjaHierachie []*models.NamespaceWithLists, err error) {
newNamespace := &models.NamespaceWithListsAndTasks{
newNamespace := &models.NamespaceWithLists{
Namespace: models.Namespace{
Title: "Migrated from todoist",
},
}
// A map for all vikunja lists with the project id they're coming from as key
lists := make(map[int64]*models.ListWithTasksAndBuckets, len(sync.Projects))
lists := make(map[int64]*models.List, len(sync.Projects))
// A map for all vikunja tasks with the todoist task id as key to find them easily and add more data
tasks := make(map[int64]*models.TaskWithComments, len(sync.Items))
tasks := make(map[int64]*models.Task, len(sync.Items))
// A map for all vikunja labels with the todoist id as key to find them easier
labels := make(map[int64]*models.Label, len(sync.Labels))
for _, p := range sync.Projects {
list := &models.ListWithTasksAndBuckets{
List: models.List{
Title: p.Name,
HexColor: todoistColors[p.Color],
IsArchived: p.IsArchived == 1,
},
list := &models.List{
Title: p.Name,
HexColor: todoistColors[p.Color],
IsArchived: p.IsArchived == 1,
}
lists[p.ID] = list
@ -307,13 +305,11 @@ func convertTodoistToVikunja(sync *sync, doneItems map[int64]*doneItem) (fullVik
}
for _, i := range sync.Items {
task := &models.TaskWithComments{
Task: models.Task{
Title: i.Content,
Created: i.DateAdded.In(config.GetTimeZone()),
Done: i.Checked == 1,
BucketID: i.SectionID,
},
task := &models.Task{
Title: i.Content,
Created: i.DateAdded.In(config.GetTimeZone()),
Done: i.Checked == 1,
BucketID: i.SectionID,
}
// Only try to parse the task done at date if the task is actually done
@ -369,7 +365,7 @@ func convertTodoistToVikunja(sync *sync, doneItems map[int64]*doneItem) (fullVik
tasks[i.ParentID].RelatedTasks = make(models.RelatedTaskMap)
}
tasks[i.ParentID].RelatedTasks[models.RelationKindSubtask] = append(tasks[i.ParentID].RelatedTasks[models.RelationKindSubtask], &tasks[i.ID].Task)
tasks[i.ParentID].RelatedTasks[models.RelationKindSubtask] = append(tasks[i.ParentID].RelatedTasks[models.RelationKindSubtask], tasks[i.ID])
// Remove the task from the top level structure, otherwise it is added twice
outer:
@ -453,7 +449,7 @@ func convertTodoistToVikunja(sync *sync, doneItems map[int64]*doneItem) (fullVik
tasks[r.ItemID].Reminders = append(tasks[r.ItemID].Reminders, date.In(config.GetTimeZone()))
}
return []*models.NamespaceWithListsAndTasks{
return []*models.NamespaceWithLists{
newNamespace,
}, err
}

View File

@ -375,259 +375,211 @@ func TestConvertTodoistToVikunja(t *testing.T) {
},
}
expectedHierachie := []*models.NamespaceWithListsAndTasks{
expectedHierachie := []*models.NamespaceWithLists{
{
Namespace: models.Namespace{
Title: "Migrated from todoist",
},
Lists: []*models.ListWithTasksAndBuckets{
Lists: []*models.List{
{
List: models.List{
Title: "Project1",
Description: "Lorem Ipsum dolor sit amet\nLorem Ipsum dolor sit amet 2\nLorem Ipsum dolor sit amet 3",
HexColor: todoistColors[30],
},
Title: "Project1",
Description: "Lorem Ipsum dolor sit amet\nLorem Ipsum dolor sit amet 2\nLorem Ipsum dolor sit amet 3",
HexColor: todoistColors[30],
Buckets: []*models.Bucket{
{
ID: 1234,
Title: "Some Bucket",
},
},
Tasks: []*models.TaskWithComments{
Tasks: []*models.Task{
{
Task: models.Task{
Title: "Task400000000",
Description: "Lorem Ipsum dolor sit amet",
Done: false,
Created: time1,
Reminders: []time.Time{
time.Date(2020, time.June, 15, 0, 0, 0, 0, time.UTC).In(config.GetTimeZone()),
time.Date(2020, time.June, 16, 7, 0, 0, 0, time.UTC).In(config.GetTimeZone()),
},
Title: "Task400000000",
Description: "Lorem Ipsum dolor sit amet",
Done: false,
Created: time1,
Reminders: []time.Time{
time.Date(2020, time.June, 15, 0, 0, 0, 0, time.UTC).In(config.GetTimeZone()),
time.Date(2020, time.June, 16, 7, 0, 0, 0, time.UTC).In(config.GetTimeZone()),
},
},
{
Task: models.Task{
Title: "Task400000001",
Description: "Lorem Ipsum dolor sit amet",
Done: false,
Created: time1,
Title: "Task400000001",
Description: "Lorem Ipsum dolor sit amet",
Done: false,
Created: time1,
},
{
Title: "Task400000002",
Done: false,
Created: time1,
Reminders: []time.Time{
time.Date(2020, time.July, 15, 7, 0, 0, 0, time.UTC).In(config.GetTimeZone()),
},
},
{
Task: models.Task{
Title: "Task400000002",
Done: false,
Created: time1,
Reminders: []time.Time{
time.Date(2020, time.July, 15, 7, 0, 0, 0, time.UTC).In(config.GetTimeZone()),
},
Title: "Task400000003",
Description: "Lorem Ipsum dolor sit amet",
Done: true,
DueDate: dueTime,
Created: time1,
DoneAt: time3,
Labels: vikunjaLabels,
Reminders: []time.Time{
time.Date(2020, time.June, 15, 7, 0, 0, 0, time.UTC).In(config.GetTimeZone()),
},
},
{
Task: models.Task{
Title: "Task400000003",
Description: "Lorem Ipsum dolor sit amet",
Done: true,
DueDate: dueTime,
Created: time1,
DoneAt: time3,
Labels: vikunjaLabels,
Reminders: []time.Time{
time.Date(2020, time.June, 15, 7, 0, 0, 0, time.UTC).In(config.GetTimeZone()),
},
Title: "Task400000004",
Done: false,
Created: time1,
Labels: vikunjaLabels,
},
{
Title: "Task400000005",
Done: true,
DueDate: dueTime,
Created: time1,
DoneAt: time3,
Reminders: []time.Time{
time.Date(2020, time.June, 15, 7, 0, 0, 0, time.UTC).In(config.GetTimeZone()),
},
},
{
Task: models.Task{
Title: "Task400000004",
Done: false,
Created: time1,
Labels: vikunjaLabels,
},
},
{
Task: models.Task{
Title: "Task400000005",
Done: true,
DueDate: dueTime,
Created: time1,
DoneAt: time3,
Reminders: []time.Time{
time.Date(2020, time.June, 15, 7, 0, 0, 0, time.UTC).In(config.GetTimeZone()),
},
},
},
{
Task: models.Task{
Title: "Task400000006",
Done: true,
DueDate: dueTime,
Created: time1,
DoneAt: time3,
RelatedTasks: map[models.RelationKind][]*models.Task{
models.RelationKindSubtask: {
{
Title: "Task with parent",
Done: false,
Priority: 2,
Created: time1,
DoneAt: nilTime,
},
},
},
},
},
{
Task: models.Task{
Title: "Task400000106",
Done: true,
DueDate: dueTimeWithTime,
Created: time1,
DoneAt: time3,
Labels: vikunjaLabels,
},
},
{
Task: models.Task{
Title: "Task400000107",
Done: true,
Created: time1,
DoneAt: time3,
},
},
{
Task: models.Task{
Title: "Task400000108",
Done: true,
Created: time1,
DoneAt: time3,
},
},
{
Task: models.Task{
Title: "Task400000109",
Done: true,
Created: time1,
DoneAt: time3,
BucketID: 1234,
},
},
},
},
{
List: models.List{
Title: "Project2",
Description: "Lorem Ipsum dolor sit amet 4\nLorem Ipsum dolor sit amet 5",
HexColor: todoistColors[37],
},
Tasks: []*models.TaskWithComments{
{
Task: models.Task{
Title: "Task400000007",
Done: false,
DueDate: dueTime,
Created: time1,
},
},
{
Task: models.Task{
Title: "Task400000008",
Done: false,
DueDate: dueTime,
Created: time1,
},
},
{
Task: models.Task{
Title: "Task400000009",
Done: false,
Created: time1,
Reminders: []time.Time{
time.Date(2020, time.June, 15, 7, 0, 0, 0, time.UTC).In(config.GetTimeZone()),
},
},
},
{
Task: models.Task{
Title: "Task400000010",
Description: "Lorem Ipsum dolor sit amet",
Done: true,
Created: time1,
DoneAt: time3,
},
},
{
Task: models.Task{
Title: "Task400000101",
Description: "Lorem Ipsum dolor sit amet",
Done: false,
Created: time1,
Attachments: []*models.TaskAttachment{
Title: "Task400000006",
Done: true,
DueDate: dueTime,
Created: time1,
DoneAt: time3,
RelatedTasks: map[models.RelationKind][]*models.Task{
models.RelationKindSubtask: {
{
File: &files.File{
Name: "file.md",
Mime: "text/plain",
Size: 12345,
Created: time1,
FileContent: exampleFile,
},
Created: time1,
Title: "Task with parent",
Done: false,
Priority: 2,
Created: time1,
DoneAt: nilTime,
},
},
},
},
{
Task: models.Task{
Title: "Task400000102",
Done: false,
DueDate: dueTime,
Created: time1,
Labels: vikunjaLabels,
},
Title: "Task400000106",
Done: true,
DueDate: dueTimeWithTime,
Created: time1,
DoneAt: time3,
Labels: vikunjaLabels,
},
{
Task: models.Task{
Title: "Task400000103",
Done: false,
Created: time1,
Labels: vikunjaLabels,
},
Title: "Task400000107",
Done: true,
Created: time1,
DoneAt: time3,
},
{
Task: models.Task{
Title: "Task400000104",
Done: false,
Created: time1,
Labels: vikunjaLabels,
},
Title: "Task400000108",
Done: true,
Created: time1,
DoneAt: time3,
},
{
Task: models.Task{
Title: "Task400000105",
Done: false,
DueDate: dueTime,
Created: time1,
Labels: vikunjaLabels,
},
Title: "Task400000109",
Done: true,
Created: time1,
DoneAt: time3,
BucketID: 1234,
},
},
},
{
List: models.List{
Title: "Project3 - Archived",
HexColor: todoistColors[37],
IsArchived: true,
},
Tasks: []*models.TaskWithComments{
Title: "Project2",
Description: "Lorem Ipsum dolor sit amet 4\nLorem Ipsum dolor sit amet 5",
HexColor: todoistColors[37],
Tasks: []*models.Task{
{
Task: models.Task{
Title: "Task400000111",
Done: true,
Created: time1,
DoneAt: time3,
Title: "Task400000007",
Done: false,
DueDate: dueTime,
Created: time1,
},
{
Title: "Task400000008",
Done: false,
DueDate: dueTime,
Created: time1,
},
{
Title: "Task400000009",
Done: false,
Created: time1,
Reminders: []time.Time{
time.Date(2020, time.June, 15, 7, 0, 0, 0, time.UTC).In(config.GetTimeZone()),
},
},
{
Title: "Task400000010",
Description: "Lorem Ipsum dolor sit amet",
Done: true,
Created: time1,
DoneAt: time3,
},
{
Title: "Task400000101",
Description: "Lorem Ipsum dolor sit amet",
Done: false,
Created: time1,
Attachments: []*models.TaskAttachment{
{
File: &files.File{
Name: "file.md",
Mime: "text/plain",
Size: 12345,
Created: time1,
FileContent: exampleFile,
},
Created: time1,
},
},
},
{
Title: "Task400000102",
Done: false,
DueDate: dueTime,
Created: time1,
Labels: vikunjaLabels,
},
{
Title: "Task400000103",
Done: false,
Created: time1,
Labels: vikunjaLabels,
},
{
Title: "Task400000104",
Done: false,
Created: time1,
Labels: vikunjaLabels,
},
{
Title: "Task400000105",
Done: false,
DueDate: dueTime,
Created: time1,
Labels: vikunjaLabels,
},
},
},
{
Title: "Project3 - Archived",
HexColor: todoistColors[37],
IsArchived: true,
Tasks: []*models.Task{
{
Title: "Task400000111",
Done: true,
Created: time1,
DoneAt: time3,
},
},
},
},

View File

@ -144,16 +144,16 @@ func getTrelloData(token string) (trelloData []*trello.Board, err error) {
// Converts all previously obtained data from trello into the vikunja format.
// `trelloData` should contain all boards with their lists and cards respectively.
func convertTrelloDataToVikunja(trelloData []*trello.Board) (fullVikunjaHierachie []*models.NamespaceWithListsAndTasks, err error) {
func convertTrelloDataToVikunja(trelloData []*trello.Board) (fullVikunjaHierachie []*models.NamespaceWithLists, err error) {
log.Debugf("[Trello Migration] ")
fullVikunjaHierachie = []*models.NamespaceWithListsAndTasks{
fullVikunjaHierachie = []*models.NamespaceWithLists{
{
Namespace: models.Namespace{
Title: "Imported from Trello",
},
Lists: []*models.ListWithTasksAndBuckets{},
Lists: []*models.List{},
},
}
@ -162,12 +162,10 @@ func convertTrelloDataToVikunja(trelloData []*trello.Board) (fullVikunjaHierachi
log.Debugf("[Trello Migration] Converting %d boards to vikunja lists", len(trelloData))
for _, board := range trelloData {
list := &models.ListWithTasksAndBuckets{
List: models.List{
Title: board.Name,
Description: board.Desc,
IsArchived: board.Closed,
},
list := &models.List{
Title: board.Name,
Description: board.Desc,
IsArchived: board.Closed,
}
// Background
@ -271,7 +269,7 @@ func convertTrelloDataToVikunja(trelloData []*trello.Board) (fullVikunjaHierachi
log.Debugf("[Trello Migration] Downloaded card attachment %s", attachment.ID)
}
list.Tasks = append(list.Tasks, &models.TaskWithComments{Task: *task})
list.Tasks = append(list.Tasks, task)
}
list.Buckets = append(list.Buckets, bucket)

View File

@ -187,18 +187,16 @@ func TestConvertTrelloToVikunja(t *testing.T) {
}
trelloData[0].Prefs.BackgroundImage = "https://vikunja.io/testimage.jpg" // Using an image which we are hosting, so it'll still be up
expectedHierachie := []*models.NamespaceWithListsAndTasks{
expectedHierachie := []*models.NamespaceWithLists{
{
Namespace: models.Namespace{
Title: "Imported from Trello",
},
Lists: []*models.ListWithTasksAndBuckets{
Lists: []*models.List{
{
List: models.List{
Title: "TestBoard",
Description: "This is a description",
BackgroundInformation: bytes.NewBuffer(exampleFile),
},
Title: "TestBoard",
Description: "This is a description",
BackgroundInformation: bytes.NewBuffer(exampleFile),
Buckets: []*models.Bucket{
{
ID: 1,
@ -209,40 +207,37 @@ func TestConvertTrelloToVikunja(t *testing.T) {
Title: "Test List 2",
},
},
Tasks: []*models.TaskWithComments{
Tasks: []*models.Task{
{
Task: models.Task{
Title: "Test Card 1",
Description: "Card Description",
BucketID: 1,
KanbanPosition: 123,
DueDate: time1,
Labels: []*models.Label{
{
Title: "Label 1",
HexColor: trelloColorMap["green"],
},
{
Title: "Label 2",
HexColor: trelloColorMap["orange"],
},
Title: "Test Card 1",
Description: "Card Description",
BucketID: 1,
KanbanPosition: 123,
DueDate: time1,
Labels: []*models.Label{
{
Title: "Label 1",
HexColor: trelloColorMap["green"],
},
Attachments: []*models.TaskAttachment{
{
File: &files.File{
Name: "Testimage.jpg",
Mime: "image/jpg",
Size: uint64(len(exampleFile)),
FileContent: exampleFile,
},
{
Title: "Label 2",
HexColor: trelloColorMap["orange"],
},
},
Attachments: []*models.TaskAttachment{
{
File: &files.File{
Name: "Testimage.jpg",
Mime: "image/jpg",
Size: uint64(len(exampleFile)),
FileContent: exampleFile,
},
},
},
},
{
Task: models.Task{
Title: "Test Card 2",
Description: `
Title: "Test Card 2",
Description: `
## Checklist 1
@ -253,105 +248,84 @@ func TestConvertTrelloToVikunja(t *testing.T) {
* [ ] Pending Task
* [ ] Another Pending Task`,
BucketID: 1,
KanbanPosition: 124,
},
BucketID: 1,
KanbanPosition: 124,
},
{
Task: models.Task{
Title: "Test Card 3",
BucketID: 1,
KanbanPosition: 126,
},
Title: "Test Card 3",
BucketID: 1,
KanbanPosition: 126,
},
{
Task: models.Task{
Title: "Test Card 4",
BucketID: 1,
KanbanPosition: 127,
Labels: []*models.Label{
{
Title: "Label 2",
HexColor: trelloColorMap["orange"],
},
Title: "Test Card 4",
BucketID: 1,
KanbanPosition: 127,
Labels: []*models.Label{
{
Title: "Label 2",
HexColor: trelloColorMap["orange"],
},
},
},
{
Task: models.Task{
Title: "Test Card 5",
BucketID: 2,
KanbanPosition: 111,
Labels: []*models.Label{
{
Title: "Label 3",
HexColor: trelloColorMap["blue"],
},
Title: "Test Card 5",
BucketID: 2,
KanbanPosition: 111,
Labels: []*models.Label{
{
Title: "Label 3",
HexColor: trelloColorMap["blue"],
},
},
},
{
Task: models.Task{
Title: "Test Card 6",
BucketID: 2,
KanbanPosition: 222,
DueDate: time1,
},
Title: "Test Card 6",
BucketID: 2,
KanbanPosition: 222,
DueDate: time1,
},
{
Task: models.Task{
Title: "Test Card 7",
BucketID: 2,
KanbanPosition: 333,
},
Title: "Test Card 7",
BucketID: 2,
KanbanPosition: 333,
},
{
Task: models.Task{
Title: "Test Card 8",
BucketID: 2,
KanbanPosition: 444,
},
Title: "Test Card 8",
BucketID: 2,
KanbanPosition: 444,
},
},
},
{
List: models.List{
Title: "TestBoard 2",
},
Title: "TestBoard 2",
Buckets: []*models.Bucket{
{
ID: 3,
Title: "Test List 4",
},
},
Tasks: []*models.TaskWithComments{
Tasks: []*models.Task{
{
Task: models.Task{
Title: "Test Card 634",
BucketID: 3,
KanbanPosition: 123,
},
Title: "Test Card 634",
BucketID: 3,
KanbanPosition: 123,
},
},
},
{
List: models.List{
Title: "TestBoard Archived",
IsArchived: true,
},
Title: "TestBoard Archived",
IsArchived: true,
Buckets: []*models.Bucket{
{
ID: 4,
Title: "Test List 5",
},
},
Tasks: []*models.TaskWithComments{
Tasks: []*models.Task{
{
Task: models.Task{
Title: "Test Card 63423",
BucketID: 4,
KanbanPosition: 123,
},
Title: "Test Card 63423",
BucketID: 4,
KanbanPosition: 123,
},
},
},

View File

@ -1,44 +0,0 @@
// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-2021 Vikunja and contributors. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public Licensee as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public Licensee for more details.
//
// You should have received a copy of the GNU Affero General Public Licensee
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package vikunjafile
import (
"os"
"testing"
"code.vikunja.io/api/pkg/events"
"code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/files"
"code.vikunja.io/api/pkg/models"
"code.vikunja.io/api/pkg/user"
)
// TestMain is the main test function used to bootstrap the test env
func TestMain(m *testing.M) {
// Set default config
config.InitDefaultConfig()
// We need to set the root path even if we're not using the config, otherwise fixtures are not loaded correctly
config.ServiceRootpath.Set(os.Getenv("VIKUNJA_SERVICE_ROOTPATH"))
// Some tests use the file engine, so we'll need to initialize that
files.InitTests()
user.InitTests()
models.SetupTests()
events.Fake()
os.Exit(m.Run())
}

View File

@ -1,204 +0,0 @@
// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-2021 Vikunja and contributors. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public Licensee as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public Licensee for more details.
//
// You should have received a copy of the GNU Affero General Public Licensee
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package vikunjafile
import (
"archive/zip"
"bytes"
"encoding/json"
"fmt"
"io"
"strconv"
"strings"
"code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/models"
"code.vikunja.io/api/pkg/modules/migration"
"code.vikunja.io/api/pkg/user"
)
const logPrefix = "[Vikunja File Import] "
type FileMigrator struct {
}
// Name is used to get the name of the vikunja-file migration - we're using the docs here to annotate the status route.
// @Summary Get migration status
// @Description Returns if the current user already did the migation or not. This is useful to show a confirmation message in the frontend if the user is trying to do the same migration again.
// @tags migration
// @Produce json
// @Security JWTKeyAuth
// @Success 200 {object} migration.Status "The migration status"
// @Failure 500 {object} models.Message "Internal server error"
// @Router /migration/vikunja-file/status [get]
func (v *FileMigrator) Name() string {
return "vikunja-file"
}
// Migrate takes a vikunja file export, parses it and imports everything in it into Vikunja.
// @Summary Import all lists, tasks etc. from a Vikunja data export
// @Description Imports all projects, tasks, notes, reminders, subtasks and files from a Vikunjda data export into Vikunja.
// @tags migration
// @Accept json
// @Produce json
// @Security JWTKeyAuth
// @Param import formData string true "The Vikunja export zip file."
// @Success 200 {object} models.Message "A message telling you everything was migrated successfully."
// @Failure 500 {object} models.Message "Internal server error"
// @Router /migration/vikunja-file/migrate [post]
func (v *FileMigrator) Migrate(user *user.User, file io.ReaderAt, size int64) error {
r, err := zip.NewReader(file, size)
if err != nil {
return fmt.Errorf("could not open import file: %s", err)
}
log.Debugf(logPrefix+"Importing a zip file containing %d files", len(r.File))
var dataFile *zip.File
var filterFile *zip.File
storedFiles := make(map[int64]*zip.File)
for _, f := range r.File {
if strings.HasPrefix(f.Name, "files/") {
fname := strings.ReplaceAll(f.Name, "files/", "")
id, err := strconv.ParseInt(fname, 10, 64)
if err != nil {
return fmt.Errorf("could not convert file id: %s", err)
}
storedFiles[id] = f
log.Debugf(logPrefix + "Found a blob file")
continue
}
if f.Name == "data.json" {
dataFile = f
log.Debugf(logPrefix + "Found a data file")
continue
}
if f.Name == "filters.json" {
filterFile = f
log.Debugf(logPrefix + "Found a filter file")
}
}
if dataFile == nil {
return fmt.Errorf("no data file provided")
}
log.Debugf(logPrefix + "")
//////
// Import the bulk of Vikunja data
df, err := dataFile.Open()
if err != nil {
return fmt.Errorf("could not open data file: %s", err)
}
defer df.Close()
var bufData bytes.Buffer
if _, err := bufData.ReadFrom(df); err != nil {
return fmt.Errorf("could not read data file: %s", err)
}
namespaces := []*models.NamespaceWithListsAndTasks{}
if err := json.Unmarshal(bufData.Bytes(), &namespaces); err != nil {
return fmt.Errorf("could not read data: %s", err)
}
for _, n := range namespaces {
for _, l := range n.Lists {
if b, exists := storedFiles[l.BackgroundFileID]; exists {
bf, err := b.Open()
if err != nil {
return fmt.Errorf("could not open list background file %d for reading: %s", l.BackgroundFileID, err)
}
var buf bytes.Buffer
if _, err := buf.ReadFrom(bf); err != nil {
return fmt.Errorf("could not read list background file %d: %s", l.BackgroundFileID, err)
}
l.BackgroundInformation = &buf
}
for _, t := range l.Tasks {
for _, label := range t.Labels {
label.ID = 0
}
for _, comment := range t.Comments {
comment.ID = 0
}
for _, attachment := range t.Attachments {
af, err := storedFiles[attachment.File.ID].Open()
if err != nil {
return fmt.Errorf("could not open attachment %d for reading: %s", attachment.ID, err)
}
var buf bytes.Buffer
if _, err := buf.ReadFrom(af); err != nil {
return fmt.Errorf("could not read attachment %d: %s", attachment.ID, err)
}
attachment.ID = 0
attachment.File.ID = 0
attachment.File.FileContent = buf.Bytes()
}
}
}
}
err = migration.InsertFromStructure(namespaces, user)
if err != nil {
return fmt.Errorf("could not insert data: %s", err)
}
if filterFile == nil {
log.Debugf(logPrefix + "No filter file found")
return nil
}
///////
// Import filters
ff, err := filterFile.Open()
if err != nil {
return fmt.Errorf("could not open filters file: %s", err)
}
defer ff.Close()
var bufFilter bytes.Buffer
if _, err := bufFilter.ReadFrom(ff); err != nil {
return fmt.Errorf("could not read filters file: %s", err)
}
filters := []*models.SavedFilter{}
if err := json.Unmarshal(bufFilter.Bytes(), &filters); err != nil {
return fmt.Errorf("could not read filter data: %s", err)
}
log.Debugf(logPrefix+"Importing %d saved filters", len(filters))
s := db.NewSession()
defer s.Close()
for _, f := range filters {
f.ID = 0
err = f.Create(s, user)
if err != nil {
_ = s.Rollback()
return err
}
}
return s.Commit()
}

View File

@ -1,79 +0,0 @@
// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-2021 Vikunja and contributors. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public Licensee as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public Licensee for more details.
//
// You should have received a copy of the GNU Affero General Public Licensee
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package vikunjafile
import (
"os"
"testing"
"code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/user"
"github.com/stretchr/testify/assert"
)
func TestVikunjaFileMigrator_Migrate(t *testing.T) {
db.LoadAndAssertFixtures(t)
m := &FileMigrator{}
u := &user.User{ID: 1}
f, err := os.Open(config.ServiceRootpath.GetString() + "/pkg/modules/migration/vikunja-file/export.zip")
if err != nil {
t.Fatalf("Could not open file: %s", err)
}
defer f.Close()
s, err := f.Stat()
if err != nil {
t.Fatalf("Could not stat file: %s", err)
}
err = m.Migrate(u, f, s.Size())
assert.NoError(t, err)
db.AssertExists(t, "namespaces", map[string]interface{}{
"title": "test",
"owner_id": u.ID,
}, false)
db.AssertExists(t, "lists", map[string]interface{}{
"title": "Test list",
"owner_id": u.ID,
}, false)
db.AssertExists(t, "lists", map[string]interface{}{
"title": "A list with a background",
"owner_id": u.ID,
}, false)
db.AssertExists(t, "tasks", map[string]interface{}{
"title": "Some other task",
"created_by_id": u.ID,
}, false)
db.AssertExists(t, "task_comments", map[string]interface{}{
"comment": "This is a comment",
"author_id": u.ID,
}, false)
db.AssertExists(t, "files", map[string]interface{}{
"name": "cristiano-mozzillo-v3d5uBB26yA-unsplash.jpg",
"created_by_id": u.ID,
}, false)
db.AssertExists(t, "labels", map[string]interface{}{
"title": "test",
"created_by_id": u.ID,
}, false)
db.AssertExists(t, "buckets", map[string]interface{}{
"title": "Test Bucket",
"created_by_id": u.ID,
}, false)
}

View File

@ -142,13 +142,11 @@ type wunderlistContents struct {
subtasks []*subtask
}
func convertListForFolder(listID int, list *list, content *wunderlistContents) (*models.ListWithTasksAndBuckets, error) {
func convertListForFolder(listID int, list *list, content *wunderlistContents) (*models.List, error) {
l := &models.ListWithTasksAndBuckets{
List: models.List{
Title: list.Title,
Created: list.CreatedAt,
},
l := &models.List{
Title: list.Title,
Created: list.CreatedAt,
}
// Find all tasks belonging to this list and put them in
@ -235,13 +233,13 @@ func convertListForFolder(listID int, list *list, content *wunderlistContents) (
}
}
l.Tasks = append(l.Tasks, &models.TaskWithComments{Task: *newTask})
l.Tasks = append(l.Tasks, newTask)
}
}
return l, nil
}
func convertWunderlistToVikunja(content *wunderlistContents) (fullVikunjaHierachie []*models.NamespaceWithListsAndTasks, err error) {
func convertWunderlistToVikunja(content *wunderlistContents) (fullVikunjaHierachie []*models.NamespaceWithLists, err error) {
// Make a map from the list with the key being list id for easier handling
listMap := make(map[int]*list, len(content.lists))
@ -251,7 +249,7 @@ func convertWunderlistToVikunja(content *wunderlistContents) (fullVikunjaHierach
// First, we look through all folders and create namespaces for them.
for _, folder := range content.folders {
namespace := &models.NamespaceWithListsAndTasks{
namespace := &models.NamespaceWithLists{
Namespace: models.Namespace{
Title: folder.Title,
Created: folder.CreatedAt,
@ -278,7 +276,7 @@ func convertWunderlistToVikunja(content *wunderlistContents) (fullVikunjaHierach
// At the end, loop over all lists which don't belong to a namespace and put them in a default namespace
if len(listMap) > 0 {
newNamespace := &models.NamespaceWithListsAndTasks{
newNamespace := &models.NamespaceWithLists{
Namespace: models.Namespace{
Title: "Migrated from wunderlist",
},

View File

@ -194,100 +194,49 @@ func TestWunderlistParsing(t *testing.T) {
},
}
expectedHierachie := []*models.NamespaceWithListsAndTasks{
expectedHierachie := []*models.NamespaceWithLists{
{
Namespace: models.Namespace{
Title: "Lorem Ipsum",
Created: time1,
Updated: time2,
},
Lists: []*models.ListWithTasksAndBuckets{
Lists: []*models.List{
{
List: models.List{
Created: time1,
Title: "Lorem1",
},
Tasks: []*models.TaskWithComments{
Created: time1,
Title: "Lorem1",
Tasks: []*models.Task{
{
Task: models.Task{
Title: "Ipsum1",
DueDate: time.Unix(1378339200, 0).In(config.GetTimeZone()),
Created: time1,
Description: "Lorem Ipsum dolor sit amet",
Attachments: []*models.TaskAttachment{
Title: "Ipsum1",
DueDate: time.Unix(1378339200, 0).In(config.GetTimeZone()),
Created: time1,
Description: "Lorem Ipsum dolor sit amet",
Attachments: []*models.TaskAttachment{
{
File: &files.File{
Name: "file.md",
Mime: "text/plain",
Size: 12345,
Created: time2,
FileContent: exampleFile,
},
Created: time2,
},
},
Reminders: []time.Time{time4},
},
{
Title: "Ipsum2",
DueDate: time.Unix(1378339200, 0).In(config.GetTimeZone()),
Created: time1,
Description: "Lorem Ipsum dolor sit amet",
RelatedTasks: map[models.RelationKind][]*models.Task{
models.RelationKindSubtask: {
{
File: &files.File{
Name: "file.md",
Mime: "text/plain",
Size: 12345,
Created: time2,
FileContent: exampleFile,
},
Created: time2,
Title: "LoremSub1",
},
},
Reminders: []time.Time{time4},
},
},
{
Task: models.Task{
Title: "Ipsum2",
DueDate: time.Unix(1378339200, 0).In(config.GetTimeZone()),
Created: time1,
Description: "Lorem Ipsum dolor sit amet",
RelatedTasks: map[models.RelationKind][]*models.Task{
models.RelationKindSubtask: {
{
Title: "LoremSub1",
},
{
Title: "LoremSub2",
},
},
},
},
},
},
},
{
List: models.List{
Created: time1,
Title: "Lorem2",
},
Tasks: []*models.TaskWithComments{
{
Task: models.Task{
Title: "Ipsum3",
Done: true,
DoneAt: time1,
DueDate: time.Unix(1378339200, 0).In(config.GetTimeZone()),
Created: time1,
Description: "Lorem Ipsum dolor sit amet",
Attachments: []*models.TaskAttachment{
{
File: &files.File{
Name: "file2.md",
Mime: "text/plain",
Size: 12345,
Created: time3,
FileContent: exampleFile,
},
Created: time3,
},
},
},
},
{
Task: models.Task{
Title: "Ipsum4",
DueDate: time.Unix(1378339200, 0).In(config.GetTimeZone()),
Created: time1,
Reminders: []time.Time{time3},
RelatedTasks: map[models.RelationKind][]*models.Task{
models.RelationKindSubtask: {
{
Title: "LoremSub3",
},
Title: "LoremSub2",
},
},
},
@ -295,68 +244,91 @@ func TestWunderlistParsing(t *testing.T) {
},
},
{
List: models.List{
Created: time1,
Title: "Lorem3",
},
Tasks: []*models.TaskWithComments{
Created: time1,
Title: "Lorem2",
Tasks: []*models.Task{
{
Task: models.Task{
Title: "Ipsum5",
DueDate: time.Unix(1378339200, 0).In(config.GetTimeZone()),
Created: time1,
Title: "Ipsum3",
Done: true,
DoneAt: time1,
DueDate: time.Unix(1378339200, 0).In(config.GetTimeZone()),
Created: time1,
Description: "Lorem Ipsum dolor sit amet",
Attachments: []*models.TaskAttachment{
{
File: &files.File{
Name: "file2.md",
Mime: "text/plain",
Size: 12345,
Created: time3,
FileContent: exampleFile,
},
Created: time3,
},
},
},
{
Task: models.Task{
Title: "Ipsum6",
DueDate: time.Unix(1378339200, 0).In(config.GetTimeZone()),
Created: time1,
Done: true,
DoneAt: time1,
},
},
{
Task: models.Task{
Title: "Ipsum7",
DueDate: time.Unix(1378339200, 0).In(config.GetTimeZone()),
Created: time1,
Done: true,
DoneAt: time1,
},
},
{
Task: models.Task{
Title: "Ipsum8",
DueDate: time.Unix(1378339200, 0).In(config.GetTimeZone()),
Created: time1,
Title: "Ipsum4",
DueDate: time.Unix(1378339200, 0).In(config.GetTimeZone()),
Created: time1,
Reminders: []time.Time{time3},
RelatedTasks: map[models.RelationKind][]*models.Task{
models.RelationKindSubtask: {
{
Title: "LoremSub3",
},
},
},
},
},
},
{
List: models.List{
Created: time1,
Title: "Lorem4",
},
Tasks: []*models.TaskWithComments{
Created: time1,
Title: "Lorem3",
Tasks: []*models.Task{
{
Task: models.Task{
Title: "Ipsum9",
DueDate: time.Unix(1378339200, 0).In(config.GetTimeZone()),
Created: time1,
Done: true,
DoneAt: time1,
},
Title: "Ipsum5",
DueDate: time.Unix(1378339200, 0).In(config.GetTimeZone()),
Created: time1,
},
{
Task: models.Task{
Title: "Ipsum10",
DueDate: time.Unix(1378339200, 0).In(config.GetTimeZone()),
Created: time1,
Done: true,
DoneAt: time1,
},
Title: "Ipsum6",
DueDate: time.Unix(1378339200, 0).In(config.GetTimeZone()),
Created: time1,
Done: true,
DoneAt: time1,
},
{
Title: "Ipsum7",
DueDate: time.Unix(1378339200, 0).In(config.GetTimeZone()),
Created: time1,
Done: true,
DoneAt: time1,
},
{
Title: "Ipsum8",
DueDate: time.Unix(1378339200, 0).In(config.GetTimeZone()),
Created: time1,
},
},
},
{
Created: time1,
Title: "Lorem4",
Tasks: []*models.Task{
{
Title: "Ipsum9",
DueDate: time.Unix(1378339200, 0).In(config.GetTimeZone()),
Created: time1,
Done: true,
DoneAt: time1,
},
{
Title: "Ipsum10",
DueDate: time.Unix(1378339200, 0).In(config.GetTimeZone()),
Created: time1,
Done: true,
DoneAt: time1,
},
},
},
@ -366,12 +338,10 @@ func TestWunderlistParsing(t *testing.T) {
Namespace: models.Namespace{
Title: "Migrated from wunderlist",
},
Lists: []*models.ListWithTasksAndBuckets{
Lists: []*models.List{
{
List: models.List{
Created: time4,
Title: "List without a namespace",
},
Created: time4,
Title: "List without a namespace",
},
},
},

View File

@ -19,8 +19,6 @@ package v1
import (
"net/http"
vikunja_file "code.vikunja.io/api/pkg/modules/migration/vikunja-file"
microsofttodo "code.vikunja.io/api/pkg/modules/migration/microsoft-todo"
"code.vikunja.io/api/pkg/modules/migration/trello"
@ -93,9 +91,6 @@ func Info(c echo.Context) error {
CaldavEnabled: config.ServiceEnableCaldav.GetBool(),
EmailRemindersEnabled: config.ServiceEnableEmailReminders.GetBool(),
UserDeletionEnabled: config.ServiceEnableUserDeletion.GetBool(),
AvailableMigrators: []string{
(&vikunja_file.FileMigrator{}).Name(),
},
Legal: legalInfo{
ImprintURL: config.LegalImprintURL.GetString(),
PrivacyPolicyURL: config.LegalPrivacyURL.GetString(),

View File

@ -27,7 +27,7 @@ import (
"github.com/labstack/echo/v4"
)
type UserPasswordConfirmation struct {
type UserDeletionRequest struct {
Password string `json:"password" valid:"required"`
}
@ -41,13 +41,13 @@ type UserDeletionRequestConfirm struct {
// @tags user
// @Accept json
// @Produce json
// @Param credentials body v1.UserPasswordConfirmation true "The user password."
// @Param credentials body v1.UserDeletionRequest true "The user password."
// @Success 200 {object} models.Message
// @Failure 412 {object} web.HTTPError "Bad password provided."
// @Failure 500 {object} models.Message "Internal error"
// @Router /user/deletion/request [post]
func UserRequestDeletion(c echo.Context) error {
var deletionRequest UserPasswordConfirmation
var deletionRequest UserDeletionRequest
if err := c.Bind(&deletionRequest); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "No password provided.")
}
@ -149,13 +149,13 @@ func UserConfirmDeletion(c echo.Context) error {
// @tags user
// @Accept json
// @Produce json
// @Param credentials body v1.UserPasswordConfirmation true "The user password to confirm."
// @Param credentials body v1.UserDeletionRequest true "The user password to confirm."
// @Success 200 {object} models.Message
// @Failure 412 {object} web.HTTPError "Bad password provided."
// @Failure 500 {object} models.Message "Internal error"
// @Router /user/deletion/cancel [post]
func UserCancelDeletion(c echo.Context) error {
var deletionRequest UserPasswordConfirmation
var deletionRequest UserDeletionRequest
if err := c.Bind(&deletionRequest); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "No password provided.")
}

View File

@ -1,136 +0,0 @@
// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-2021 Vikunja and contributors. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public Licensee as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public Licensee for more details.
//
// You should have received a copy of the GNU Affero General Public Licensee
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package v1
import (
"net/http"
"code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/events"
"code.vikunja.io/api/pkg/files"
"code.vikunja.io/api/pkg/models"
"code.vikunja.io/api/pkg/user"
"code.vikunja.io/web/handler"
"github.com/labstack/echo/v4"
"xorm.io/xorm"
)
func checkExportRequest(c echo.Context) (s *xorm.Session, u *user.User, err error) {
var pass UserPasswordConfirmation
if err := c.Bind(&pass); err != nil {
return nil, nil, echo.NewHTTPError(http.StatusBadRequest, "No password provided.")
}
err = c.Validate(pass)
if err != nil {
return nil, nil, echo.NewHTTPError(http.StatusBadRequest, err)
}
s = db.NewSession()
defer s.Close()
err = s.Begin()
if err != nil {
return nil, nil, handler.HandleHTTPError(err, c)
}
u, err = user.GetCurrentUserFromDB(s, c)
if err != nil {
_ = s.Rollback()
return nil, nil, handler.HandleHTTPError(err, c)
}
err = user.CheckUserPassword(u, pass.Password)
if err != nil {
_ = s.Rollback()
return nil, nil, handler.HandleHTTPError(err, c)
}
return
}
// RequestUserDataExport is the handler to request a user data export
// @Summary Request a user data export.
// @tags user
// @Accept json
// @Produce json
// @Security JWTKeyAuth
// @Param password body v1.UserPasswordConfirmation true "User password to confirm the data export request."
// @Success 200 {object} models.Message
// @Failure 400 {object} web.HTTPError "Something's invalid."
// @Failure 500 {object} models.Message "Internal server error."
// @Router /user/export/request [post]
func RequestUserDataExport(c echo.Context) error {
s, u, err := checkExportRequest(c)
if err != nil {
return err
}
err = events.Dispatch(&models.UserDataExportRequestedEvent{
User: u,
})
if err != nil {
_ = s.Rollback()
return handler.HandleHTTPError(err, c)
}
err = s.Commit()
if err != nil {
_ = s.Rollback()
return handler.HandleHTTPError(err, c)
}
return c.JSON(http.StatusOK, models.Message{Message: "Successfully requested data export. We will send you an email when it's ready."})
}
// DownloadUserDataExport is the handler to download a created user data export
// @Summary Download a user data export.
// @tags user
// @Accept json
// @Produce json
// @Security JWTKeyAuth
// @Param password body v1.UserPasswordConfirmation true "User password to confirm the download."
// @Success 200 {object} models.Message
// @Failure 400 {object} web.HTTPError "Something's invalid."
// @Failure 500 {object} models.Message "Internal server error."
// @Router /user/export/download [post]
func DownloadUserDataExport(c echo.Context) error {
s, u, err := checkExportRequest(c)
if err != nil {
return err
}
err = s.Commit()
if err != nil {
_ = s.Rollback()
return handler.HandleHTTPError(err, c)
}
// Download
exportFile := &files.File{ID: u.ExportFileID}
err = exportFile.LoadFileMetaByID()
if err != nil {
return handler.HandleHTTPError(err, c)
}
err = exportFile.LoadFileByID()
if err != nil {
return handler.HandleHTTPError(err, c)
}
http.ServeContent(c.Response(), c.Request(), exportFile.Name, exportFile.Created, exportFile.File)
return nil
}

View File

@ -56,7 +56,7 @@ func ListHandler(c echo.Context) error {
}
storage := &VikunjaCaldavListStorage{
list: &models.ListWithTasksAndBuckets{List: models.List{ID: listID}},
list: &models.List{ID: listID},
user: u,
}
@ -102,7 +102,7 @@ func TaskHandler(c echo.Context) error {
taskUID := strings.TrimSuffix(c.Param("task"), ".ics")
storage := &VikunjaCaldavListStorage{
list: &models.ListWithTasksAndBuckets{List: models.List{ID: listID}},
list: &models.List{ID: listID},
task: &models.Task{UID: taskUID},
user: u,
}

View File

@ -39,7 +39,7 @@ const ListBasePath = DavBasePath + `lists`
// VikunjaCaldavListStorage represents a list storage
type VikunjaCaldavListStorage struct {
// Used when handling a list
list *models.ListWithTasksAndBuckets
list *models.List
// Used when handling a single task, like updating
task *models.Task
// The current user
@ -109,9 +109,7 @@ func (vcls *VikunjaCaldavListStorage) GetResources(rpath string, withChildren bo
var resources []data.Resource
for _, l := range lists {
rr := VikunjaListResourceAdapter{
list: &models.ListWithTasksAndBuckets{
List: *l,
},
list: l,
isCollection: true,
}
r := data.NewResource(ListBasePath+"/"+strconv.FormatInt(l.ID, 10), &rr)
@ -174,10 +172,10 @@ func (vcls *VikunjaCaldavListStorage) GetResourcesByFilters(rpath string, filter
for _, t := range vcls.list.Tasks {
rr := VikunjaListResourceAdapter{
list: vcls.list,
task: &t.Task,
task: t,
isCollection: false,
}
r := data.NewResource(getTaskURL(&t.Task), &rr)
r := data.NewResource(getTaskURL(t), &rr)
r.Name = t.Title
resources = append(resources, r)
}
@ -370,8 +368,8 @@ func (vcls *VikunjaCaldavListStorage) DeleteResource(rpath string) error {
// VikunjaListResourceAdapter holds the actual resource
type VikunjaListResourceAdapter struct {
list *models.ListWithTasksAndBuckets
listTasks []*models.TaskWithComments
list *models.List
listTasks []*models.Task
task *models.Task
isPrincipal bool
@ -417,7 +415,7 @@ func (vlra *VikunjaListResourceAdapter) GetContent() string {
}
if vlra.task != nil {
list := models.ListWithTasksAndBuckets{Tasks: []*models.TaskWithComments{{Task: *vlra.task}}}
list := models.List{Tasks: []*models.Task{vlra.task}}
return caldav.GetCaldavTodosForTasks(&list, list.Tasks)
}
@ -481,10 +479,8 @@ func (vcls *VikunjaCaldavListStorage) getListRessource(isCollection bool) (rr Vi
panic("Tasks returned from TaskCollection.ReadAll are not []*models.Task!")
}
for _, t := range tasks {
listTasks = append(listTasks, &models.TaskWithComments{Task: *t})
}
vcls.list.Tasks = listTasks
listTasks = tasks
vcls.list.Tasks = tasks
}
if err := s.Commit(); err != nil {

View File

@ -52,8 +52,6 @@ import (
"strings"
"time"
vikunja_file "code.vikunja.io/api/pkg/modules/migration/vikunja-file"
"code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/log"
@ -305,8 +303,6 @@ func registerAPIRoutes(a *echo.Group) {
u.POST("/settings/avatar", apiv1.ChangeUserAvatarProvider)
u.PUT("/settings/avatar/upload", apiv1.UploadAvatar)
u.POST("/settings/general", apiv1.UpdateGeneralUserSettings)
u.POST("/export/request", apiv1.RequestUserDataExport)
u.POST("/export/download", apiv1.DownloadUserDataExport)
if config.ServiceEnableTotp.GetBool() {
u.GET("/settings/totp", apiv1.UserTOTP)
@ -567,35 +563,7 @@ func registerAPIRoutes(a *echo.Group) {
// Migrations
m := a.Group("/migration")
registerMigrations(m)
// List Backgrounds
if config.BackgroundsEnabled.GetBool() {
a.GET("/lists/:list/background", backgroundHandler.GetListBackground)
a.DELETE("/lists/:list/background", backgroundHandler.RemoveListBackground)
if config.BackgroundsUploadEnabled.GetBool() {
uploadBackgroundProvider := &backgroundHandler.BackgroundProvider{
Provider: func() background.Provider {
return &upload.Provider{}
},
}
a.PUT("/lists/:list/backgrounds/upload", uploadBackgroundProvider.UploadBackground)
}
if config.BackgroundsUnsplashEnabled.GetBool() {
unsplashBackgroundProvider := &backgroundHandler.BackgroundProvider{
Provider: func() background.Provider {
return &unsplash.Provider{}
},
}
a.GET("/backgrounds/unsplash/search", unsplashBackgroundProvider.SearchBackgrounds)
a.POST("/lists/:list/backgrounds/unsplash", unsplashBackgroundProvider.SetBackground)
a.GET("/backgrounds/unsplash/images/:image/thumb", unsplash.ProxyUnsplashThumb)
a.GET("/backgrounds/unsplash/images/:image", unsplash.ProxyUnsplashImage)
}
}
}
func registerMigrations(m *echo.Group) {
// Wunderlist
if config.MigrationWunderlistEnable.GetBool() {
wunderlistMigrationHandler := &migrationHandler.MigrationWeb{
@ -636,12 +604,30 @@ func registerMigrations(m *echo.Group) {
microsoftTodoMigrationHandler.RegisterRoutes(m)
}
vikunjaFileMigrationHandler := &migrationHandler.FileMigratorWeb{
MigrationStruct: func() migration.FileMigrator {
return &vikunja_file.FileMigrator{}
},
// List Backgrounds
if config.BackgroundsEnabled.GetBool() {
a.GET("/lists/:list/background", backgroundHandler.GetListBackground)
a.DELETE("/lists/:list/background", backgroundHandler.RemoveListBackground)
if config.BackgroundsUploadEnabled.GetBool() {
uploadBackgroundProvider := &backgroundHandler.BackgroundProvider{
Provider: func() background.Provider {
return &upload.Provider{}
},
}
a.PUT("/lists/:list/backgrounds/upload", uploadBackgroundProvider.UploadBackground)
}
if config.BackgroundsUnsplashEnabled.GetBool() {
unsplashBackgroundProvider := &backgroundHandler.BackgroundProvider{
Provider: func() background.Provider {
return &unsplash.Provider{}
},
}
a.GET("/backgrounds/unsplash/search", unsplashBackgroundProvider.SearchBackgrounds)
a.POST("/lists/:list/backgrounds/unsplash", unsplashBackgroundProvider.SetBackground)
a.GET("/backgrounds/unsplash/images/:image/thumb", unsplash.ProxyUnsplashThumb)
a.GET("/backgrounds/unsplash/images/:image", unsplash.ProxyUnsplashImage)
}
}
vikunjaFileMigrationHandler.RegisterRoutes(m)
}
func registerCalDavRoutes(c *echo.Group) {

View File

@ -2921,80 +2921,6 @@ var doc = `{
}
}
},
"/migration/vikunja-file/migrate": {
"post": {
"security": [
{
"JWTKeyAuth": []
}
],
"description": "Imports all projects, tasks, notes, reminders, subtasks and files from a Vikunjda data export into Vikunja.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"migration"
],
"summary": "Import all lists, tasks etc. from a Vikunja data export",
"parameters": [
{
"type": "string",
"description": "The Vikunja export zip file.",
"name": "import",
"in": "formData",
"required": true
}
],
"responses": {
"200": {
"description": "A message telling you everything was migrated successfully.",
"schema": {
"$ref": "#/definitions/models.Message"
}
},
"500": {
"description": "Internal server error",
"schema": {
"$ref": "#/definitions/models.Message"
}
}
}
}
},
"/migration/vikunja-file/status": {
"get": {
"security": [
{
"JWTKeyAuth": []
}
],
"description": "Returns if the current user already did the migation or not. This is useful to show a confirmation message in the frontend if the user is trying to do the same migration again.",
"produces": [
"application/json"
],
"tags": [
"migration"
],
"summary": "Get migration status",
"responses": {
"200": {
"description": "The migration status",
"schema": {
"$ref": "#/definitions/migration.Status"
}
},
"500": {
"description": "Internal server error",
"schema": {
"$ref": "#/definitions/models.Message"
}
}
}
}
},
"/migration/wunderlist/auth": {
"get": {
"security": [
@ -6409,7 +6335,7 @@ var doc = `{
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/v1.UserPasswordConfirmation"
"$ref": "#/definitions/v1.UserDeletionRequest"
}
}
],
@ -6501,7 +6427,7 @@ var doc = `{
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/v1.UserPasswordConfirmation"
"$ref": "#/definitions/v1.UserDeletionRequest"
}
}
],
@ -6527,106 +6453,6 @@ var doc = `{
}
}
},
"/user/export/download": {
"post": {
"security": [
{
"JWTKeyAuth": []
}
],
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"user"
],
"summary": "Download a user data export.",
"parameters": [
{
"description": "User password to confirm the download.",
"name": "password",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/v1.UserPasswordConfirmation"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.Message"
}
},
"400": {
"description": "Something's invalid.",
"schema": {
"$ref": "#/definitions/web.HTTPError"
}
},
"500": {
"description": "Internal server error.",
"schema": {
"$ref": "#/definitions/models.Message"
}
}
}
}
},
"/user/export/request": {
"post": {
"security": [
{
"JWTKeyAuth": []
}
],
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"user"
],
"summary": "Request a user data export.",
"parameters": [
{
"description": "User password to confirm the data export request.",
"name": "password",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/v1.UserPasswordConfirmation"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.Message"
}
},
"400": {
"description": "Something's invalid.",
"schema": {
"$ref": "#/definitions/web.HTTPError"
}
},
"500": {
"description": "Internal server error.",
"schema": {
"$ref": "#/definitions/models.Message"
}
}
}
}
},
"/user/password": {
"post": {
"security": [
@ -8868,6 +8694,14 @@ var doc = `{
}
}
},
"v1.UserDeletionRequest": {
"type": "object",
"properties": {
"password": {
"type": "string"
}
}
},
"v1.UserDeletionRequestConfirm": {
"type": "object",
"properties": {
@ -8887,14 +8721,6 @@ var doc = `{
}
}
},
"v1.UserPasswordConfirmation": {
"type": "object",
"properties": {
"password": {
"type": "string"
}
}
},
"v1.UserSettings": {
"type": "object",
"properties": {

View File

@ -2904,80 +2904,6 @@
}
}
},
"/migration/vikunja-file/migrate": {
"post": {
"security": [
{
"JWTKeyAuth": []
}
],
"description": "Imports all projects, tasks, notes, reminders, subtasks and files from a Vikunjda data export into Vikunja.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"migration"
],
"summary": "Import all lists, tasks etc. from a Vikunja data export",
"parameters": [
{
"type": "string",
"description": "The Vikunja export zip file.",
"name": "import",
"in": "formData",
"required": true
}
],
"responses": {
"200": {
"description": "A message telling you everything was migrated successfully.",
"schema": {
"$ref": "#/definitions/models.Message"
}
},
"500": {
"description": "Internal server error",
"schema": {
"$ref": "#/definitions/models.Message"
}
}
}
}
},
"/migration/vikunja-file/status": {
"get": {
"security": [
{
"JWTKeyAuth": []
}
],
"description": "Returns if the current user already did the migation or not. This is useful to show a confirmation message in the frontend if the user is trying to do the same migration again.",
"produces": [
"application/json"
],
"tags": [
"migration"
],
"summary": "Get migration status",
"responses": {
"200": {
"description": "The migration status",
"schema": {
"$ref": "#/definitions/migration.Status"
}
},
"500": {
"description": "Internal server error",
"schema": {
"$ref": "#/definitions/models.Message"
}
}
}
}
},
"/migration/wunderlist/auth": {
"get": {
"security": [
@ -6392,7 +6318,7 @@
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/v1.UserPasswordConfirmation"
"$ref": "#/definitions/v1.UserDeletionRequest"
}
}
],
@ -6484,7 +6410,7 @@
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/v1.UserPasswordConfirmation"
"$ref": "#/definitions/v1.UserDeletionRequest"
}
}
],
@ -6510,106 +6436,6 @@
}
}
},
"/user/export/download": {
"post": {
"security": [
{
"JWTKeyAuth": []
}
],
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"user"
],
"summary": "Download a user data export.",
"parameters": [
{
"description": "User password to confirm the download.",
"name": "password",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/v1.UserPasswordConfirmation"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.Message"
}
},
"400": {
"description": "Something's invalid.",
"schema": {
"$ref": "#/definitions/web.HTTPError"
}
},
"500": {
"description": "Internal server error.",
"schema": {
"$ref": "#/definitions/models.Message"
}
}
}
}
},
"/user/export/request": {
"post": {
"security": [
{
"JWTKeyAuth": []
}
],
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"user"
],
"summary": "Request a user data export.",
"parameters": [
{
"description": "User password to confirm the data export request.",
"name": "password",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/v1.UserPasswordConfirmation"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.Message"
}
},
"400": {
"description": "Something's invalid.",
"schema": {
"$ref": "#/definitions/web.HTTPError"
}
},
"500": {
"description": "Internal server error.",
"schema": {
"$ref": "#/definitions/models.Message"
}
}
}
}
},
"/user/password": {
"post": {
"security": [
@ -8851,6 +8677,14 @@
}
}
},
"v1.UserDeletionRequest": {
"type": "object",
"properties": {
"password": {
"type": "string"
}
}
},
"v1.UserDeletionRequestConfirm": {
"type": "object",
"properties": {
@ -8870,14 +8704,6 @@
}
}
},
"v1.UserPasswordConfirmation": {
"type": "object",
"properties": {
"password": {
"type": "string"
}
}
},
"v1.UserSettings": {
"type": "object",
"properties": {

View File

@ -1185,6 +1185,11 @@ definitions:
email), `upload`, `initials`, `default`.
type: string
type: object
v1.UserDeletionRequest:
properties:
password:
type: string
type: object
v1.UserDeletionRequestConfirm:
properties:
token:
@ -1197,11 +1202,6 @@ definitions:
old_password:
type: string
type: object
v1.UserPasswordConfirmation:
properties:
password:
type: string
type: object
v1.UserSettings:
properties:
default_list_id:
@ -3287,55 +3287,6 @@ paths:
summary: Get migration status
tags:
- migration
/migration/vikunja-file/migrate:
post:
consumes:
- application/json
description: Imports all projects, tasks, notes, reminders, subtasks and files
from a Vikunjda data export into Vikunja.
parameters:
- description: The Vikunja export zip file.
in: formData
name: import
required: true
type: string
produces:
- application/json
responses:
"200":
description: A message telling you everything was migrated successfully.
schema:
$ref: '#/definitions/models.Message'
"500":
description: Internal server error
schema:
$ref: '#/definitions/models.Message'
security:
- JWTKeyAuth: []
summary: Import all lists, tasks etc. from a Vikunja data export
tags:
- migration
/migration/vikunja-file/status:
get:
description: Returns if the current user already did the migation or not. This
is useful to show a confirmation message in the frontend if the user is trying
to do the same migration again.
produces:
- application/json
responses:
"200":
description: The migration status
schema:
$ref: '#/definitions/migration.Status'
"500":
description: Internal server error
schema:
$ref: '#/definitions/models.Message'
security:
- JWTKeyAuth: []
summary: Get migration status
tags:
- migration
/migration/wunderlist/auth:
get:
description: Returns the auth url where the user needs to get its auth code.
@ -5603,7 +5554,7 @@ paths:
name: credentials
required: true
schema:
$ref: '#/definitions/v1.UserPasswordConfirmation'
$ref: '#/definitions/v1.UserDeletionRequest'
produces:
- application/json
responses:
@ -5664,7 +5615,7 @@ paths:
name: credentials
required: true
schema:
$ref: '#/definitions/v1.UserPasswordConfirmation'
$ref: '#/definitions/v1.UserDeletionRequest'
produces:
- application/json
responses:
@ -5683,68 +5634,6 @@ paths:
summary: Request the deletion of the user
tags:
- user
/user/export/download:
post:
consumes:
- application/json
parameters:
- description: User password to confirm the download.
in: body
name: password
required: true
schema:
$ref: '#/definitions/v1.UserPasswordConfirmation'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/models.Message'
"400":
description: Something's invalid.
schema:
$ref: '#/definitions/web.HTTPError'
"500":
description: Internal server error.
schema:
$ref: '#/definitions/models.Message'
security:
- JWTKeyAuth: []
summary: Download a user data export.
tags:
- user
/user/export/request:
post:
consumes:
- application/json
parameters:
- description: User password to confirm the data export request.
in: body
name: password
required: true
schema:
$ref: '#/definitions/v1.UserPasswordConfirmation'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/models.Message'
"400":
description: Something's invalid.
schema:
$ref: '#/definitions/web.HTTPError'
"500":
description: Internal server error.
schema:
$ref: '#/definitions/models.Message'
security:
- JWTKeyAuth: []
summary: Request a user data export.
tags:
- user
/user/password:
post:
consumes:

View File

@ -21,7 +21,7 @@ type CreatedEvent struct {
User *User
}
// Name defines the name for CreatedEvent
// TopicName defines the name for CreatedEvent
func (t *CreatedEvent) Name() string {
return "user.created"
}

View File

@ -98,8 +98,6 @@ type User struct {
DeletionScheduledAt time.Time `xorm:"datetime null" json:"-"`
DeletionLastReminderSent time.Time `xorm:"datetime null" json:"-"`
ExportFileID int64 `xorm:"bigint null" json:"-"`
// A timestamp when this task was created. You cannot change this value.
Created time.Time `xorm:"created not null" json:"created"`
// A timestamp when this task was last updated. You cannot change this value.

View File

@ -1,63 +0,0 @@
// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-2021 Vikunja and contributors. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public Licensee as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public Licensee for more details.
//
// You should have received a copy of the GNU Affero General Public Licensee
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package utils
import (
"archive/zip"
"fmt"
"io"
"strconv"
)
// Change to deflate to gain better compression
// see http://golang.org/pkg/archive/zip/#pkg-constants
const CompressionUsed = zip.Deflate
func WriteBytesToZip(filename string, data []byte, writer *zip.Writer) (err error) {
header := &zip.FileHeader{
Name: filename,
Method: CompressionUsed,
}
w, err := writer.CreateHeader(header)
if err != nil {
return err
}
_, err = w.Write(data)
return
}
// WriteFilesToZip writes a bunch of files from the db to a zip file. It exprects a map with the file id
// as key and its content as io.ReadCloser.
func WriteFilesToZip(files map[int64]io.ReadCloser, wr *zip.Writer) (err error) {
for fid, file := range files {
header := &zip.FileHeader{
Name: "files/" + strconv.FormatInt(fid, 10),
Method: CompressionUsed,
}
w, err := wr.CreateHeader(header)
if err != nil {
return err
}
_, err = io.Copy(w, file)
if err != nil {
return fmt.Errorf("error writing file %d: %s", fid, err)
}
_ = file.Close()
}
return nil
}