export csv
This commit is contained in:
parent
6ab2c63961
commit
409ba0a6fd
7 changed files with 279 additions and 4 deletions
|
@ -1,3 +1,4 @@
|
||||||
|
from .csv import CsvExporter
|
||||||
from .doufen import DoufenExporter
|
from .doufen import DoufenExporter
|
||||||
|
|
||||||
__all__ = ["DoufenExporter"]
|
__all__ = ["DoufenExporter", "CsvExporter"]
|
||||||
|
|
170
journal/exporters/csv.py
Normal file
170
journal/exporters/csv.py
Normal file
|
@ -0,0 +1,170 @@
|
||||||
|
import csv
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
from catalog.common.models import Item
|
||||||
|
from catalog.models import ItemCategory
|
||||||
|
from common.utils import GenerateDateUUIDMediaFilePath
|
||||||
|
from journal.models import Note, Review, ShelfMember, q_item_in_category
|
||||||
|
from users.models import Task
|
||||||
|
|
||||||
|
#
|
||||||
|
|
||||||
|
_mark_heading = [
|
||||||
|
"title",
|
||||||
|
"info",
|
||||||
|
"links",
|
||||||
|
"timestamp",
|
||||||
|
"status",
|
||||||
|
"rating",
|
||||||
|
"comment",
|
||||||
|
"tags",
|
||||||
|
]
|
||||||
|
|
||||||
|
_review_heading = [
|
||||||
|
"title",
|
||||||
|
"info",
|
||||||
|
"links",
|
||||||
|
"timestamp",
|
||||||
|
"title",
|
||||||
|
"content",
|
||||||
|
]
|
||||||
|
|
||||||
|
_note_heading = [
|
||||||
|
"title",
|
||||||
|
"info",
|
||||||
|
"links",
|
||||||
|
"timestamp",
|
||||||
|
"progress",
|
||||||
|
"title",
|
||||||
|
"content",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class CsvExporter(Task):
|
||||||
|
class Meta:
|
||||||
|
app_label = "journal" # workaround bug in TypedModel
|
||||||
|
|
||||||
|
TaskQueue = "export"
|
||||||
|
DefaultMetadata = {
|
||||||
|
"file": None,
|
||||||
|
"total": 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def filename(self) -> str:
|
||||||
|
d = self.created_time.strftime("%Y%m%d%H%M%S")
|
||||||
|
return f"neodb_{self.user.username}_{d}_csv"
|
||||||
|
|
||||||
|
def get_item_info(self, item: Item) -> str:
|
||||||
|
s = []
|
||||||
|
for a in [
|
||||||
|
"imdb",
|
||||||
|
"isbn",
|
||||||
|
"year",
|
||||||
|
"pub_year",
|
||||||
|
"season_number",
|
||||||
|
"episode_number",
|
||||||
|
]:
|
||||||
|
if hasattr(item, a) and getattr(item, a):
|
||||||
|
s.append(f"{a}:{getattr(item, a)}")
|
||||||
|
for a in ["author", "artist", "director", "host"]:
|
||||||
|
if hasattr(item, a) and getattr(item, a):
|
||||||
|
s.append(f"{a}:{'/'.join(getattr(item, a))}")
|
||||||
|
return " ".join(s)
|
||||||
|
|
||||||
|
def get_item_links(self, item: Item) -> str:
|
||||||
|
links = [item.absolute_url]
|
||||||
|
for ext in item.external_resources.all():
|
||||||
|
links.append(ext.url)
|
||||||
|
return " ".join(links)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
user = self.user
|
||||||
|
temp_dir = tempfile.mkdtemp()
|
||||||
|
temp_folder_path = os.path.join(temp_dir, self.filename)
|
||||||
|
os.makedirs(temp_folder_path)
|
||||||
|
for category in [
|
||||||
|
ItemCategory.Movie,
|
||||||
|
ItemCategory.TV,
|
||||||
|
ItemCategory.Music,
|
||||||
|
ItemCategory.Book,
|
||||||
|
ItemCategory.Game,
|
||||||
|
ItemCategory.Podcast,
|
||||||
|
ItemCategory.Performance,
|
||||||
|
]:
|
||||||
|
q = q_item_in_category(category)
|
||||||
|
csv_file_path = os.path.join(temp_folder_path, f"{category}")
|
||||||
|
marks = (
|
||||||
|
ShelfMember.objects.filter(owner=user.identity)
|
||||||
|
.filter(q)
|
||||||
|
.order_by("created_time")
|
||||||
|
)
|
||||||
|
with open(csv_file_path + "_mark.csv", "w") as csvfile:
|
||||||
|
writer = csv.writer(csvfile)
|
||||||
|
writer.writerow(_mark_heading)
|
||||||
|
for mark in marks:
|
||||||
|
item = mark.item
|
||||||
|
line = [
|
||||||
|
item.display_title,
|
||||||
|
self.get_item_info(item),
|
||||||
|
self.get_item_links(item),
|
||||||
|
mark.created_time.isoformat(),
|
||||||
|
mark.shelf_type,
|
||||||
|
mark.rating_grade,
|
||||||
|
mark.comment_text,
|
||||||
|
" ".join(mark.tags),
|
||||||
|
]
|
||||||
|
writer.writerow(line)
|
||||||
|
reviews = (
|
||||||
|
Review.objects.filter(owner=user.identity)
|
||||||
|
.filter(q)
|
||||||
|
.order_by("created_time")
|
||||||
|
)
|
||||||
|
with open(csv_file_path + "_review.csv", "w") as csvfile:
|
||||||
|
writer = csv.writer(csvfile)
|
||||||
|
writer.writerow(_review_heading)
|
||||||
|
for review in reviews:
|
||||||
|
item = review.item
|
||||||
|
line = [
|
||||||
|
item.display_title,
|
||||||
|
self.get_item_info(item),
|
||||||
|
self.get_item_links(item),
|
||||||
|
review.created_time.isoformat(),
|
||||||
|
review.title,
|
||||||
|
review.body,
|
||||||
|
]
|
||||||
|
writer.writerow(line)
|
||||||
|
with open(csv_file_path + "_note.csv", "w") as csvfile:
|
||||||
|
writer = csv.writer(csvfile)
|
||||||
|
writer.writerow(_note_heading)
|
||||||
|
notes = (
|
||||||
|
Note.objects.filter(owner=user.identity)
|
||||||
|
.filter(q)
|
||||||
|
.order_by("created_time")
|
||||||
|
)
|
||||||
|
for note in notes:
|
||||||
|
item = note.item
|
||||||
|
line = [
|
||||||
|
item.display_title,
|
||||||
|
self.get_item_info(item),
|
||||||
|
self.get_item_links(item),
|
||||||
|
note.created_time.isoformat(),
|
||||||
|
note.progress_display,
|
||||||
|
note.title,
|
||||||
|
note.content,
|
||||||
|
]
|
||||||
|
writer.writerow(line)
|
||||||
|
|
||||||
|
filename = GenerateDateUUIDMediaFilePath(
|
||||||
|
"f.zip", settings.MEDIA_ROOT + "/" + settings.EXPORT_FILE_PATH_ROOT
|
||||||
|
)
|
||||||
|
if not os.path.exists(os.path.dirname(filename)):
|
||||||
|
os.makedirs(os.path.dirname(filename))
|
||||||
|
shutil.make_archive(filename[:-4], "zip", temp_folder_path)
|
||||||
|
self.metadata["file"] = filename
|
||||||
|
self.message = "Export complete."
|
||||||
|
self.save()
|
23
journal/migrations/0005_csvexporter.py
Normal file
23
journal/migrations/0005_csvexporter.py
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
# Generated by Django 4.2.17 on 2025-01-27 07:42
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("users", "0007_alter_task_type"),
|
||||||
|
("journal", "0004_tasks"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="CsvExporter",
|
||||||
|
fields=[],
|
||||||
|
options={
|
||||||
|
"proxy": True,
|
||||||
|
"indexes": [],
|
||||||
|
"constraints": [],
|
||||||
|
},
|
||||||
|
bases=("users.task",),
|
||||||
|
),
|
||||||
|
]
|
27
users/migrations/0007_alter_task_type.py
Normal file
27
users/migrations/0007_alter_task_type.py
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
# Generated by Django 4.2.17 on 2025-01-27 07:42
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("users", "0006_alter_task_type"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="task",
|
||||||
|
name="type",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("journal.csvexporter", "csv exporter"),
|
||||||
|
("journal.doubanimporter", "douban importer"),
|
||||||
|
("journal.doufenexporter", "doufen exporter"),
|
||||||
|
("journal.goodreadsimporter", "goodreads importer"),
|
||||||
|
("journal.letterboxdimporter", "letterboxd importer"),
|
||||||
|
],
|
||||||
|
db_index=True,
|
||||||
|
max_length=255,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -219,7 +219,8 @@
|
||||||
method="post"
|
method="post"
|
||||||
enctype="multipart/form-data">
|
enctype="multipart/form-data">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="submit" value="{% trans 'Export marks and reviews' %}" />
|
<input type="submit"
|
||||||
|
value="{% trans 'Export marks and reviews in XLSX (Doufen format)' %}" />
|
||||||
{% if export_task %}
|
{% if export_task %}
|
||||||
<br>
|
<br>
|
||||||
{% trans 'Last export' %}: {{ export_task.created_time }}
|
{% trans 'Last export' %}: {{ export_task.created_time }}
|
||||||
|
@ -231,6 +232,24 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</form>
|
</form>
|
||||||
|
<hr>
|
||||||
|
<form action="{% url 'users:export_csv' %}"
|
||||||
|
method="post"
|
||||||
|
enctype="multipart/form-data">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="submit"
|
||||||
|
value="{% trans 'Export marks, reviews and notes in CSV' %}" />
|
||||||
|
{% if csv_export_task %}
|
||||||
|
<br>
|
||||||
|
{% trans 'Last export' %}: {{ csv_export_task.created_time }}
|
||||||
|
{% trans 'Status' %}: {{ csv_export_task.get_state_display }}
|
||||||
|
<br>
|
||||||
|
{{ csv_export_task.message }}
|
||||||
|
{% if csv_export_task.metadata.file %}
|
||||||
|
<a href="{% url 'users:export_csv' %}" download>{% trans 'Download' %}</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</form>
|
||||||
</details>
|
</details>
|
||||||
</article>
|
</article>
|
||||||
<article>
|
<article>
|
||||||
|
|
|
@ -17,6 +17,7 @@ urlpatterns = [
|
||||||
path("data/import/opml", import_opml, name="import_opml"),
|
path("data/import/opml", import_opml, name="import_opml"),
|
||||||
path("data/export/reviews", export_reviews, name="export_reviews"),
|
path("data/export/reviews", export_reviews, name="export_reviews"),
|
||||||
path("data/export/marks", export_marks, name="export_marks"),
|
path("data/export/marks", export_marks, name="export_marks"),
|
||||||
|
path("data/export/csv", export_csv, name="export_csv"),
|
||||||
path("data/sync_mastodon", sync_mastodon, name="sync_mastodon"),
|
path("data/sync_mastodon", sync_mastodon, name="sync_mastodon"),
|
||||||
path(
|
path(
|
||||||
"data/sync_mastodon_preference",
|
"data/sync_mastodon_preference",
|
||||||
|
|
|
@ -8,11 +8,12 @@ from django.db.models import Min
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.shortcuts import redirect, render
|
from django.shortcuts import redirect, render
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import translation
|
from django.utils import timezone, translation
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from common.utils import GenerateDateUUIDMediaFilePath
|
from common.utils import GenerateDateUUIDMediaFilePath
|
||||||
from journal.exporters import DoufenExporter
|
from journal.exporters import DoufenExporter
|
||||||
|
from journal.exporters.csv import CsvExporter
|
||||||
from journal.importers import (
|
from journal.importers import (
|
||||||
DoubanImporter,
|
DoubanImporter,
|
||||||
GoodreadsImporter,
|
GoodreadsImporter,
|
||||||
|
@ -97,6 +98,7 @@ def data(request):
|
||||||
"allow_any_site": settings.MASTODON_ALLOW_ANY_SITE,
|
"allow_any_site": settings.MASTODON_ALLOW_ANY_SITE,
|
||||||
"import_task": DoubanImporter.latest_task(request.user),
|
"import_task": DoubanImporter.latest_task(request.user),
|
||||||
"export_task": DoufenExporter.latest_task(request.user),
|
"export_task": DoufenExporter.latest_task(request.user),
|
||||||
|
"csv_export_task": CsvExporter.latest_task(request.user),
|
||||||
"letterboxd_task": LetterboxdImporter.latest_task(request.user),
|
"letterboxd_task": LetterboxdImporter.latest_task(request.user),
|
||||||
"goodreads_task": GoodreadsImporter.latest_task(request.user),
|
"goodreads_task": GoodreadsImporter.latest_task(request.user),
|
||||||
"years": years,
|
"years": years,
|
||||||
|
@ -149,6 +151,38 @@ def export_marks(request):
|
||||||
return redirect(reverse("users:data"))
|
return redirect(reverse("users:data"))
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def export_csv(request):
|
||||||
|
if request.method == "POST":
|
||||||
|
task = CsvExporter.latest_task(request.user)
|
||||||
|
if (
|
||||||
|
task
|
||||||
|
and task.state not in [Task.States.complete, Task.States.failed]
|
||||||
|
and task.created_time > (timezone.now() - datetime.timedelta(hours=1))
|
||||||
|
):
|
||||||
|
messages.add_message(
|
||||||
|
request, messages.INFO, _("Recent export still in progress.")
|
||||||
|
)
|
||||||
|
return redirect(reverse("users:data"))
|
||||||
|
CsvExporter.create(request.user).enqueue()
|
||||||
|
messages.add_message(request, messages.INFO, _("Generating exports."))
|
||||||
|
return redirect(reverse("users:data"))
|
||||||
|
else:
|
||||||
|
task = CsvExporter.latest_task(request.user)
|
||||||
|
if not task or task.state != Task.States.complete:
|
||||||
|
messages.add_message(
|
||||||
|
request, messages.ERROR, _("Export file not available.")
|
||||||
|
)
|
||||||
|
return redirect(reverse("users:data"))
|
||||||
|
response = HttpResponse()
|
||||||
|
response["X-Accel-Redirect"] = (
|
||||||
|
settings.MEDIA_URL + task.metadata["file"][len(settings.MEDIA_ROOT) :]
|
||||||
|
)
|
||||||
|
response["Content-Type"] = "application/zip"
|
||||||
|
response["Content-Disposition"] = f'attachment; filename="{task.filename}.zip"'
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def sync_mastodon(request):
|
def sync_mastodon(request):
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
|
|
Loading…
Add table
Reference in a new issue