mirror of
https://github.com/go-vikunja/vikunja.git
synced 2026-05-04 02:47:03 +00:00
Add a new CSV migration module that allows users to import tasks from any CSV file with custom column mapping and parsing options. Backend changes: - New CSV migrator module with detection, preview, and import endpoints - Auto-detection of delimiter, quote character, and date format - Suggested column mappings based on column name patterns - Transactional import using InsertFromStructure Frontend changes: - New CSV migration UI with two-step flow (upload -> mapping -> import) - Column mapping selectors for all task attributes - Live preview showing first 5 tasks with current mapping - Parsing option controls for delimiter and date format The CSV migrator creates a parent "Imported from CSV" project with child projects based on the project column if provided, or a default "Tasks" project for tasks without a specified project.
207 lines
6.1 KiB
Go
207 lines
6.1 KiB
Go
// Vikunja is a to-do list application to facilitate your life.
|
|
// Copyright 2018-present Vikunja and contributors. All rights reserved.
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
package csv
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
|
|
"code.vikunja.io/api/pkg/models"
|
|
"code.vikunja.io/api/pkg/modules/migration"
|
|
user2 "code.vikunja.io/api/pkg/user"
|
|
"github.com/labstack/echo/v5"
|
|
)
|
|
|
|
// MigratorWeb handles CSV migration HTTP routes
|
|
type MigratorWeb struct{}
|
|
|
|
// RegisterRoutes registers all CSV migration routes
|
|
func (c *MigratorWeb) RegisterRoutes(g *echo.Group) {
|
|
g.GET("/csv/status", c.Status)
|
|
g.PUT("/csv/detect", c.Detect)
|
|
g.PUT("/csv/preview", c.Preview)
|
|
g.PUT("/csv/migrate", c.Migrate)
|
|
}
|
|
|
|
// Status returns the migration status
|
|
// @Summary Get CSV migration status
|
|
// @Description Returns if the current user already did the CSV migration or not.
|
|
// @tags migration
|
|
// @Produce json
|
|
// @Security JWTKeyAuth
|
|
// @Success 200 {object} migration.Status "The migration status"
|
|
// @Failure 500 {object} models.Message "Internal server error"
|
|
// @Router /migration/csv/status [get]
|
|
func (c *MigratorWeb) Status(ctx *echo.Context) error {
|
|
u, err := user2.GetCurrentUser(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
m := &Migrator{}
|
|
s, err := migration.GetMigrationStatus(m, u)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return ctx.JSON(http.StatusOK, s)
|
|
}
|
|
|
|
// Detect analyzes a CSV file and returns detection results
|
|
// @Summary Detect CSV structure
|
|
// @Description Analyzes a CSV file and returns auto-detected columns, delimiter, quote character, and date format with suggested column mappings.
|
|
// @tags migration
|
|
// @Accept multipart/form-data
|
|
// @Produce json
|
|
// @Security JWTKeyAuth
|
|
// @Param import formData file true "The CSV file to analyze"
|
|
// @Success 200 {object} DetectionResult "Detection results with suggested mappings"
|
|
// @Failure 400 {object} models.Message "Invalid CSV file"
|
|
// @Failure 500 {object} models.Message "Internal server error"
|
|
// @Router /migration/csv/detect [put]
|
|
func (c *MigratorWeb) Detect(ctx *echo.Context) error {
|
|
_, err := user2.GetCurrentUser(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
file, err := ctx.FormFile("import")
|
|
if err != nil {
|
|
return echo.NewHTTPError(http.StatusBadRequest, "No file provided")
|
|
}
|
|
|
|
src, err := file.Open()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer src.Close()
|
|
|
|
result, err := DetectCSVStructure(src, file.Size)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return ctx.JSON(http.StatusOK, result)
|
|
}
|
|
|
|
// Preview generates a preview of the import
|
|
// @Summary Preview CSV import
|
|
// @Description Generates a preview of the first 5 tasks that would be imported with the given configuration.
|
|
// @tags migration
|
|
// @Accept multipart/form-data
|
|
// @Produce json
|
|
// @Security JWTKeyAuth
|
|
// @Param import formData file true "The CSV file to preview"
|
|
// @Param config formData string true "The import configuration JSON"
|
|
// @Success 200 {object} PreviewResult "Preview of tasks to import"
|
|
// @Failure 400 {object} models.Message "Invalid CSV file or configuration"
|
|
// @Failure 500 {object} models.Message "Internal server error"
|
|
// @Router /migration/csv/preview [put]
|
|
func (c *MigratorWeb) Preview(ctx *echo.Context) error {
|
|
_, err := user2.GetCurrentUser(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
file, err := ctx.FormFile("import")
|
|
if err != nil {
|
|
return echo.NewHTTPError(http.StatusBadRequest, "No file provided")
|
|
}
|
|
|
|
configStr := ctx.FormValue("config")
|
|
if configStr == "" {
|
|
return echo.NewHTTPError(http.StatusBadRequest, "No configuration provided")
|
|
}
|
|
|
|
var config ImportConfig
|
|
if err := json.Unmarshal([]byte(configStr), &config); err != nil {
|
|
return echo.NewHTTPError(http.StatusBadRequest, "Invalid configuration: "+err.Error())
|
|
}
|
|
|
|
src, err := file.Open()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer src.Close()
|
|
|
|
result, err := PreviewImport(src, file.Size, &config)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return ctx.JSON(http.StatusOK, result)
|
|
}
|
|
|
|
// Migrate imports the CSV file
|
|
// @Summary Import CSV file
|
|
// @Description Imports tasks from a CSV file into Vikunja with the provided configuration.
|
|
// @tags migration
|
|
// @Accept multipart/form-data
|
|
// @Produce json
|
|
// @Security JWTKeyAuth
|
|
// @Param import formData file true "The CSV file to import"
|
|
// @Param config formData string true "The import configuration JSON"
|
|
// @Success 200 {object} models.Message "A message telling you everything was migrated successfully."
|
|
// @Failure 400 {object} models.Message "Invalid CSV file or configuration"
|
|
// @Failure 500 {object} models.Message "Internal server error"
|
|
// @Router /migration/csv/migrate [put]
|
|
func (c *MigratorWeb) Migrate(ctx *echo.Context) error {
|
|
u, err := user2.GetCurrentUser(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
file, err := ctx.FormFile("import")
|
|
if err != nil {
|
|
return echo.NewHTTPError(http.StatusBadRequest, "No file provided")
|
|
}
|
|
|
|
configStr := ctx.FormValue("config")
|
|
if configStr == "" {
|
|
return echo.NewHTTPError(http.StatusBadRequest, "No configuration provided")
|
|
}
|
|
|
|
var config ImportConfig
|
|
if err := json.Unmarshal([]byte(configStr), &config); err != nil {
|
|
return echo.NewHTTPError(http.StatusBadRequest, "Invalid configuration: "+err.Error())
|
|
}
|
|
|
|
src, err := file.Open()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer src.Close()
|
|
|
|
m := &Migrator{}
|
|
status, err := migration.StartMigration(m, u)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = MigrateWithConfig(u, src, file.Size, &config)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = migration.FinishMigration(status)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return ctx.JSON(http.StatusOK, models.Message{Message: "Everything was migrated successfully."})
|
|
}
|