export csv

This commit is contained in:
Your Name 2025-01-27 02:44:37 -05:00 committed by Henri Dickson
parent 6ab2c63961
commit 409ba0a6fd
7 changed files with 279 additions and 4 deletions

View file

@ -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
View 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()

View 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",),
),
]

View 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,
),
),
]

View file

@ -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>

View file

@ -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",

View file

@ -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":