diff --git a/journal/importers/csv.py b/journal/importers/csv.py index d1cd34af..73372404 100644 --- a/journal/importers/csv.py +++ b/journal/importers/csv.py @@ -333,7 +333,8 @@ class CsvImporter(Task): success = import_function(row) self.progress(success) - def validate_file(self, filename: str) -> bool: + @classmethod + def validate_file(cls, filename: str) -> bool: """Validate that the given file is a valid CSV export ZIP file. Args: diff --git a/users/templates/users/data.html b/users/templates/users/data.html index 30d8e0bf..419a0529 100644 --- a/users/templates/users/data.html +++ b/users/templates/users/data.html @@ -213,6 +213,66 @@ +
+
+ {% trans 'Import marks, reviews and notes from CSV' %} +
+ {% csrf_token %} +
    +
  • {% trans 'Upload a ZIP file containing CSV files exported from NeoDB.' %}
  • +
  • {% trans 'Existing marks and reviews with newer dates will be preserved.' %}
  • +
+
+ +

+ {% trans 'Visibility' %}: +
+ + + +

+ + + {% if csv_import_task %} +
+ {% trans 'Last import started' %}: {{ csv_import_task.created_time }} + {% trans 'Status' %}: {{ csv_import_task.get_state_display }}。 +
+ {{ csv_import_task.message }} + {% if csv_import_task.metadata.failed_items %} + {% trans 'Failed items' %}: +
+ + {% endif %} + {% endif %} +
+
+
+
{% trans 'Export Data' %} diff --git a/users/urls.py b/users/urls.py index 2aa9878e..689f478e 100644 --- a/users/urls.py +++ b/users/urls.py @@ -15,6 +15,7 @@ urlpatterns = [ path("data/import/douban", import_douban, name="import_douban"), path("data/import/letterboxd", import_letterboxd, name="import_letterboxd"), path("data/import/opml", import_opml, name="import_opml"), + path("data/import/csv", import_csv, name="import_csv"), path("data/export/reviews", export_reviews, name="export_reviews"), path("data/export/marks", export_marks, name="export_marks"), path("data/export/csv", export_csv, name="export_csv"), diff --git a/users/views/data.py b/users/views/data.py index c13aec67..f8ae1be0 100644 --- a/users/views/data.py +++ b/users/views/data.py @@ -14,6 +14,7 @@ from django.utils.translation import gettext as _ from common.utils import GenerateDateUUIDMediaFilePath from journal.exporters import CsvExporter, DoufenExporter, NdjsonExporter from journal.importers import ( + CsvImporter, DoubanImporter, GoodreadsImporter, LetterboxdImporter, @@ -98,6 +99,7 @@ def data(request): "import_task": DoubanImporter.latest_task(request.user), "export_task": DoufenExporter.latest_task(request.user), "csv_export_task": CsvExporter.latest_task(request.user), + "csv_import_task": CsvImporter.latest_task(request.user), "ndjson_export_task": NdjsonExporter.latest_task(request.user), "letterboxd_task": LetterboxdImporter.latest_task(request.user), "goodreads_task": GoodreadsImporter.latest_task(request.user), @@ -319,3 +321,29 @@ def import_opml(request): else: messages.add_message(request, messages.ERROR, _("Invalid file.")) return redirect(reverse("users:data")) + + +@login_required +def import_csv(request): + if request.method == "POST": + f = ( + settings.MEDIA_ROOT + + "/" + + GenerateDateUUIDMediaFilePath("x.zip", settings.SYNC_FILE_PATH_ROOT) + ) + os.makedirs(os.path.dirname(f), exist_ok=True) + with open(f, "wb+") as destination: + for chunk in request.FILES["file"].chunks(): + destination.write(chunk) + if not CsvImporter.validate_file(f): + messages.add_message(request, messages.ERROR, _("Invalid file.")) + return redirect(reverse("users:data")) + CsvImporter.create( + request.user, + visibility=int(request.POST.get("visibility", 0)), + file=f, + ).enqueue() + messages.add_message( + request, messages.INFO, _("File is uploaded and will be imported soon.") + ) + return redirect(reverse("users:data"))