forked from vikunja/vikunja
Compare commits
1 Commits
main
...
renovate/a
Author | SHA1 | Date |
---|---|---|
renovate | 6236171c95 |
40
.drone.yml
40
.drone.yml
|
@ -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
|
||||
|
||||
...
|
||||
|
|
183
CHANGELOG.md
183
CHANGELOG.md
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
|
|
@ -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/
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -87,17 +63,6 @@ if config.MigrationWunderlistEnable.GetBool() {
|
|||
}
|
||||
```
|
||||
|
||||
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`.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
4
go.mod
|
@ -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
20
go.sum
|
@ -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=
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -97,7 +97,6 @@ func FullInit() {
|
|||
user.RegisterTokenCleanupCron()
|
||||
user.RegisterDeletionNotificationCron()
|
||||
models.RegisterUserDeletionCron()
|
||||
models.RegisterOldExportCleanupCron()
|
||||
|
||||
// Start processing events
|
||||
go func() {
|
||||
|
|
|
@ -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
|
||||
},
|
||||
})
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -32,33 +32,28 @@ 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",
|
||||
},
|
||||
Buckets: []*models.Bucket{
|
||||
{
|
||||
ID: 1234,
|
||||
Title: "Test Bucket",
|
||||
},
|
||||
},
|
||||
Tasks: []*models.TaskWithComments{
|
||||
Tasks: []*models.Task{
|
||||
{
|
||||
Task: models.Task{
|
||||
Title: "Task1",
|
||||
Description: "Lorem",
|
||||
},
|
||||
},
|
||||
{
|
||||
Task: models.Task{
|
||||
Title: "Task with related tasks",
|
||||
RelatedTasks: map[models.RelationKind][]*models.Task{
|
||||
models.RelationKindSubtask: {
|
||||
|
@ -69,9 +64,7 @@ func TestInsertFromStructure(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Task: models.Task{
|
||||
Title: "Task with attachments",
|
||||
Attachments: []*models.TaskAttachment{
|
||||
{
|
||||
|
@ -83,9 +76,7 @@ func TestInsertFromStructure(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Task: models.Task{
|
||||
Title: "Task with labels",
|
||||
Labels: []*models.Label{
|
||||
{
|
||||
|
@ -98,9 +89,7 @@ func TestInsertFromStructure(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Task: models.Task{
|
||||
Title: "Task with same label",
|
||||
Labels: []*models.Label{
|
||||
{
|
||||
|
@ -109,15 +98,11 @@ func TestInsertFromStructure(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Task: models.Task{
|
||||
Title: "Task in a bucket",
|
||||
BucketID: 1234,
|
||||
},
|
||||
},
|
||||
{
|
||||
Task: models.Task{
|
||||
Title: "Task in a nonexisting bucket",
|
||||
BucketID: 1111,
|
||||
},
|
||||
|
@ -125,7 +110,6 @@ func TestInsertFromStructure(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
err := InsertFromStructure(testStructure, u)
|
||||
assert.NoError(t, err)
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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{
|
||||
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))
|
||||
}
|
||||
|
||||
|
|
|
@ -102,84 +102,62 @@ 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{
|
||||
Tasks: []*models.Task{
|
||||
{
|
||||
Task: models.Task{
|
||||
Title: "Task 1",
|
||||
Description: "This is a description",
|
||||
},
|
||||
},
|
||||
{
|
||||
Task: models.Task{
|
||||
Title: "Task 2",
|
||||
Done: true,
|
||||
DoneAt: 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
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
List: models.List{
|
||||
Title: "List 2",
|
||||
},
|
||||
Tasks: []*models.TaskWithComments{
|
||||
Tasks: []*models.Task{
|
||||
{
|
||||
Task: models.Task{
|
||||
Title: "Task 1",
|
||||
},
|
||||
},
|
||||
{
|
||||
Task: models.Task{
|
||||
Title: "Task 2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
hierachie, err := convertMicrosoftTodoData(microsoftTodoData)
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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{
|
||||
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{
|
||||
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
|
||||
}
|
||||
|
|
|
@ -375,27 +375,24 @@ 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],
|
||||
},
|
||||
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,
|
||||
|
@ -405,17 +402,13 @@ func TestConvertTodoistToVikunja(t *testing.T) {
|
|||
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,
|
||||
},
|
||||
},
|
||||
{
|
||||
Task: models.Task{
|
||||
Title: "Task400000002",
|
||||
Done: false,
|
||||
Created: time1,
|
||||
|
@ -423,9 +416,7 @@ func TestConvertTodoistToVikunja(t *testing.T) {
|
|||
time.Date(2020, time.July, 15, 7, 0, 0, 0, time.UTC).In(config.GetTimeZone()),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Task: models.Task{
|
||||
Title: "Task400000003",
|
||||
Description: "Lorem Ipsum dolor sit amet",
|
||||
Done: true,
|
||||
|
@ -437,17 +428,13 @@ func TestConvertTodoistToVikunja(t *testing.T) {
|
|||
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,
|
||||
|
@ -457,9 +444,7 @@ func TestConvertTodoistToVikunja(t *testing.T) {
|
|||
time.Date(2020, time.June, 15, 7, 0, 0, 0, time.UTC).In(config.GetTimeZone()),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Task: models.Task{
|
||||
Title: "Task400000006",
|
||||
Done: true,
|
||||
DueDate: dueTime,
|
||||
|
@ -477,9 +462,7 @@ func TestConvertTodoistToVikunja(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Task: models.Task{
|
||||
Title: "Task400000106",
|
||||
Done: true,
|
||||
DueDate: dueTimeWithTime,
|
||||
|
@ -487,25 +470,19 @@ func TestConvertTodoistToVikunja(t *testing.T) {
|
|||
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,
|
||||
|
@ -514,32 +491,24 @@ func TestConvertTodoistToVikunja(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
List: models.List{
|
||||
Title: "Project2",
|
||||
Description: "Lorem Ipsum dolor sit amet 4\nLorem Ipsum dolor sit amet 5",
|
||||
HexColor: todoistColors[37],
|
||||
},
|
||||
Tasks: []*models.TaskWithComments{
|
||||
Tasks: []*models.Task{
|
||||
{
|
||||
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,
|
||||
|
@ -547,18 +516,14 @@ func TestConvertTodoistToVikunja(t *testing.T) {
|
|||
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,
|
||||
|
@ -576,34 +541,26 @@ func TestConvertTodoistToVikunja(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Task: models.Task{
|
||||
Title: "Task400000102",
|
||||
Done: false,
|
||||
DueDate: dueTime,
|
||||
Created: time1,
|
||||
Labels: vikunjaLabels,
|
||||
},
|
||||
},
|
||||
{
|
||||
Task: models.Task{
|
||||
Title: "Task400000103",
|
||||
Done: false,
|
||||
Created: time1,
|
||||
Labels: vikunjaLabels,
|
||||
},
|
||||
},
|
||||
{
|
||||
Task: models.Task{
|
||||
Title: "Task400000104",
|
||||
Done: false,
|
||||
Created: time1,
|
||||
Labels: vikunjaLabels,
|
||||
},
|
||||
},
|
||||
{
|
||||
Task: models.Task{
|
||||
Title: "Task400000105",
|
||||
Done: false,
|
||||
DueDate: dueTime,
|
||||
|
@ -612,16 +569,12 @@ func TestConvertTodoistToVikunja(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
List: models.List{
|
||||
Title: "Project3 - Archived",
|
||||
HexColor: todoistColors[37],
|
||||
IsArchived: true,
|
||||
},
|
||||
Tasks: []*models.TaskWithComments{
|
||||
Tasks: []*models.Task{
|
||||
{
|
||||
Task: models.Task{
|
||||
Title: "Task400000111",
|
||||
Done: true,
|
||||
Created: time1,
|
||||
|
@ -631,7 +584,6 @@ func TestConvertTodoistToVikunja(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
doneItems := make(map[int64]*doneItem)
|
||||
|
|
|
@ -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{
|
||||
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)
|
||||
|
|
|
@ -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),
|
||||
},
|
||||
Buckets: []*models.Bucket{
|
||||
{
|
||||
ID: 1,
|
||||
|
@ -209,9 +207,8 @@ 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,
|
||||
|
@ -238,9 +235,7 @@ func TestConvertTrelloToVikunja(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Task: models.Task{
|
||||
Title: "Test Card 2",
|
||||
Description: `
|
||||
|
||||
|
@ -256,16 +251,12 @@ func TestConvertTrelloToVikunja(t *testing.T) {
|
|||
BucketID: 1,
|
||||
KanbanPosition: 124,
|
||||
},
|
||||
},
|
||||
{
|
||||
Task: models.Task{
|
||||
Title: "Test Card 3",
|
||||
BucketID: 1,
|
||||
KanbanPosition: 126,
|
||||
},
|
||||
},
|
||||
{
|
||||
Task: models.Task{
|
||||
Title: "Test Card 4",
|
||||
BucketID: 1,
|
||||
KanbanPosition: 127,
|
||||
|
@ -276,9 +267,7 @@ func TestConvertTrelloToVikunja(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Task: models.Task{
|
||||
Title: "Test Card 5",
|
||||
BucketID: 2,
|
||||
KanbanPosition: 111,
|
||||
|
@ -289,65 +278,51 @@ func TestConvertTrelloToVikunja(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Task: models.Task{
|
||||
Title: "Test Card 6",
|
||||
BucketID: 2,
|
||||
KanbanPosition: 222,
|
||||
DueDate: time1,
|
||||
},
|
||||
},
|
||||
{
|
||||
Task: models.Task{
|
||||
Title: "Test Card 7",
|
||||
BucketID: 2,
|
||||
KanbanPosition: 333,
|
||||
},
|
||||
},
|
||||
{
|
||||
Task: models.Task{
|
||||
Title: "Test Card 8",
|
||||
BucketID: 2,
|
||||
KanbanPosition: 444,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
List: models.List{
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
List: models.List{
|
||||
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,
|
||||
|
@ -356,7 +331,6 @@ func TestConvertTrelloToVikunja(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
hierachie, err := convertTrelloDataToVikunja(trelloData)
|
||||
|
|
Binary file not shown.
|
@ -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())
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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{
|
||||
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",
|
||||
},
|
||||
|
|
|
@ -194,22 +194,19 @@ 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{
|
||||
Tasks: []*models.Task{
|
||||
{
|
||||
Task: models.Task{
|
||||
Title: "Ipsum1",
|
||||
DueDate: time.Unix(1378339200, 0).In(config.GetTimeZone()),
|
||||
Created: time1,
|
||||
|
@ -228,9 +225,7 @@ func TestWunderlistParsing(t *testing.T) {
|
|||
},
|
||||
Reminders: []time.Time{time4},
|
||||
},
|
||||
},
|
||||
{
|
||||
Task: models.Task{
|
||||
Title: "Ipsum2",
|
||||
DueDate: time.Unix(1378339200, 0).In(config.GetTimeZone()),
|
||||
Created: time1,
|
||||
|
@ -248,15 +243,11 @@ func TestWunderlistParsing(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
List: models.List{
|
||||
Created: time1,
|
||||
Title: "Lorem2",
|
||||
},
|
||||
Tasks: []*models.TaskWithComments{
|
||||
Tasks: []*models.Task{
|
||||
{
|
||||
Task: models.Task{
|
||||
Title: "Ipsum3",
|
||||
Done: true,
|
||||
DoneAt: time1,
|
||||
|
@ -276,9 +267,7 @@ func TestWunderlistParsing(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Task: models.Task{
|
||||
Title: "Ipsum4",
|
||||
DueDate: time.Unix(1378339200, 0).In(config.GetTimeZone()),
|
||||
Created: time1,
|
||||
|
@ -293,64 +282,48 @@ func TestWunderlistParsing(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
List: models.List{
|
||||
Created: time1,
|
||||
Title: "Lorem3",
|
||||
},
|
||||
Tasks: []*models.TaskWithComments{
|
||||
Tasks: []*models.Task{
|
||||
{
|
||||
Task: models.Task{
|
||||
Title: "Ipsum5",
|
||||
DueDate: time.Unix(1378339200, 0).In(config.GetTimeZone()),
|
||||
Created: time1,
|
||||
},
|
||||
},
|
||||
{
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
List: models.List{
|
||||
Created: time1,
|
||||
Title: "Lorem4",
|
||||
},
|
||||
Tasks: []*models.TaskWithComments{
|
||||
Tasks: []*models.Task{
|
||||
{
|
||||
Task: models.Task{
|
||||
Title: "Ipsum9",
|
||||
DueDate: time.Unix(1378339200, 0).In(config.GetTimeZone()),
|
||||
Created: time1,
|
||||
Done: true,
|
||||
DoneAt: time1,
|
||||
},
|
||||
},
|
||||
{
|
||||
Task: models.Task{
|
||||
Title: "Ipsum10",
|
||||
DueDate: time.Unix(1378339200, 0).In(config.GetTimeZone()),
|
||||
Created: time1,
|
||||
|
@ -361,20 +334,17 @@ 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",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
hierachie, err := convertWunderlistToVikunja(fixtures)
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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.")
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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{}
|
||||
},
|
||||
}
|
||||
vikunjaFileMigrationHandler.RegisterRoutes(m)
|
||||
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 registerCalDavRoutes(c *echo.Group) {
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue