Skip to content

Commit f1547c3

Browse files
committed
add export as CSV
1 parent 009169f commit f1547c3

File tree

5 files changed

+202
-11
lines changed

5 files changed

+202
-11
lines changed

src/app/templates/app/profile.html

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
</fieldset>
3737
</form>
3838

39-
<div>
39+
<div class="mb-4">
4040
<legend class="border-bottom mb-4">Import</legend>
4141

4242
<fieldset class="input-group grid-container">
@@ -66,6 +66,17 @@
6666
</fieldset>
6767
</div>
6868

69+
<div>
70+
<legend class="border-bottom mb-4">Export</legend>
71+
72+
<fieldset class="input-group grid-container">
73+
<form class="p-2 grid-item" method="POST" action="{% url 'export' %}">
74+
{% csrf_token %}
75+
<button name="tmdb" class="btn no-fill-btn bg-dark w-100" type="submit">Export as CSV</button>
76+
</form>
77+
</fieldset>
78+
</div>
79+
6980
</div>
7081

7182
{% endblock %}

src/app/tests/test_exports.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
from django.test import TestCase
2+
from django.urls import reverse
3+
from app.models import User, TV, Movie, Season, Episode, Anime, Manga
4+
from datetime import date
5+
import csv
6+
from io import StringIO
7+
8+
9+
class ExportCSVTest(TestCase):
10+
def setUp(self):
11+
self.credentials = {"username": "test", "password": "12345"}
12+
self.user = User.objects.create_superuser(**self.credentials)
13+
self.client.login(**self.credentials)
14+
15+
# Create test data for each model
16+
TV.objects.create(
17+
media_id=1668,
18+
title="Friends",
19+
score=9,
20+
user=self.user,
21+
notes="Nice",
22+
)
23+
24+
Movie.objects.create(
25+
media_id=10494,
26+
title="Perfect Blue",
27+
score=9,
28+
status="Completed",
29+
user=self.user,
30+
notes="Nice",
31+
end_date=date(2023, 6, 1),
32+
)
33+
34+
season = Season.objects.create(
35+
media_id=1668,
36+
title="Friends",
37+
score=9,
38+
status="Completed",
39+
season_number=1,
40+
user=self.user,
41+
notes="Nice",
42+
)
43+
44+
Episode.objects.create(
45+
related_season=season, episode_number=1, watch_date=date(2023, 6, 1)
46+
)
47+
48+
Anime.objects.create(
49+
media_id=1,
50+
title="Cowboy Bebop",
51+
status="Watching",
52+
user=self.user,
53+
progress=2,
54+
start_date=date(2021, 6, 1),
55+
)
56+
57+
Manga.objects.create(
58+
media_id=1,
59+
title="Berserk",
60+
status="Watching",
61+
user=self.user,
62+
progress=2,
63+
start_date=date(2021, 6, 1),
64+
)
65+
66+
def test_export_csv(self):
67+
68+
# Generate the CSV file by accessing the export view
69+
response = self.client.get(reverse("export"))
70+
71+
# Assert that the response is successful (status code 200)
72+
self.assertEqual(response.status_code, 200)
73+
74+
# Assert that the response content type is text/csv
75+
self.assertEqual(response["Content-Type"], "text/csv")
76+
77+
# Read the CSV content from the response
78+
csv_content = response.content.decode("utf-8")
79+
80+
# Create a CSV reader from the CSV content
81+
reader = csv.DictReader(StringIO(csv_content))
82+
83+
# Get all media IDs from the database
84+
db_media_ids = set(
85+
TV.objects.values_list("media_id", flat=True).filter(user=self.user)
86+
)
87+
db_media_ids.update(
88+
Movie.objects.values_list("media_id", flat=True).filter(user=self.user)
89+
)
90+
db_media_ids.update(
91+
Season.objects.values_list("media_id", flat=True).filter(user=self.user)
92+
)
93+
db_media_ids.update(
94+
Episode.objects.values_list("related_season__media_id", flat=True).filter(
95+
related_season__user=self.user
96+
)
97+
)
98+
db_media_ids.update(
99+
Anime.objects.values_list("media_id", flat=True).filter(user=self.user)
100+
)
101+
db_media_ids.update(
102+
Manga.objects.values_list("media_id", flat=True).filter(user=self.user)
103+
)
104+
105+
for row in reader:
106+
media_id = int(row["media_id"])
107+
self.assertIn(media_id, db_media_ids)

src/app/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
name="season_details",
2525
),
2626
path("profile", views.profile, name="profile"),
27+
path("export", views.export, name="export"),
2728
path("login", views.CustomLoginView.as_view(), name="login"),
2829
path("logout", auth_views.LogoutView.as_view(), name="logout"),
2930
path("modal_data", views.modal_data, name="modal_data"),

src/app/utils/exports.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
from app.models import TV, Season, Episode, Movie, Anime, Manga
2+
import csv
3+
import logging
4+
5+
6+
logger = logging.getLogger(__name__)
7+
8+
9+
def export_csv(response, user):
10+
"""Export a CSV file of the user's media."""
11+
12+
fields = [
13+
"media_id",
14+
"media_type",
15+
"title",
16+
"image",
17+
"score",
18+
"progress",
19+
"status",
20+
"start_date",
21+
"end_date",
22+
"notes",
23+
"season_number",
24+
"episode_number",
25+
"watch_date",
26+
]
27+
28+
writer = csv.writer(response, quoting=csv.QUOTE_ALL)
29+
writer.writerow(fields)
30+
31+
export_model_data(writer, fields, TV.objects.filter(user=user), "tv")
32+
export_model_data(writer, fields, Movie.objects.filter(user=user), "movie")
33+
export_model_data(writer, fields, Season.objects.filter(user=user), "season")
34+
export_model_data(writer, fields, Episode.objects.filter(related_season__user=user), "episode")
35+
export_model_data(writer, fields, Anime.objects.filter(user=user), "anime")
36+
export_model_data(writer, fields, Manga.objects.filter(user=user), "manga")
37+
38+
return response
39+
40+
41+
def export_model_data(writer, fields, queryset, media_type):
42+
logger.info(f"Adding {media_type} data to CSV file")
43+
44+
for item in queryset:
45+
# write fields if they exist, otherwise write empty string
46+
row = [getattr(item, field, "") for field in fields]
47+
48+
# replace media_type field with the correct value
49+
row[fields.index("media_type")] = media_type
50+
51+
if media_type == "episode":
52+
row[fields.index("media_id")] = item.related_season.media_id
53+
row[fields.index("title")] = item.related_season.title
54+
55+
writer.writerow(row)

src/app/views.py

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
from django.contrib.auth import update_session_auth_hash
44
from django.contrib.auth.views import LoginView
55
from django.contrib.auth.decorators import login_required
6-
from django.http import JsonResponse
6+
from django.http import JsonResponse, HttpResponse
77
from django.middleware import csrf
88
from crispy_forms.utils import render_crispy_form
99

10-
from app.utils import helpers, search, metadata, form_handlers
10+
from app.utils import helpers, search, metadata, form_handlers, exports
1111
from app.utils.imports import anilist, mal, tmdb
1212
from app.models import TV, Season, Episode, Anime, Manga
1313
from app.forms import (
@@ -66,17 +66,18 @@ def media_list(request, media_type):
6666
media_mapping = helpers.media_type_mapper(media_type)
6767
filter_form = FilterForm(
6868
# fill form with current values if they exist
69-
request.GET or None, sort_choices=media_mapping["sort_choices"]
69+
request.GET or None,
70+
sort_choices=media_mapping["sort_choices"],
7071
)
7172

7273
# if form valid or no form submitted
7374
if filter_form.is_valid() or not request.GET:
74-
7575
if media_type == "tv":
76-
7776
if "status" in filter_params:
7877
# as tv doesn't have a status field, only filter seasons
79-
media_list = Season.objects.filter(**filter_params).order_by(sort_filter)
78+
media_list = Season.objects.filter(**filter_params).order_by(
79+
sort_filter
80+
)
8081

8182
else:
8283
# show both tv and seasons in the list
@@ -89,8 +90,10 @@ def media_list(request, media_type):
8990
key=lambda item: getattr(item, sort_filter, float("-inf")),
9091
)
9192
else:
92-
media_list = media_mapping["model"].objects.filter(**filter_params).order_by(
93-
sort_filter
93+
media_list = (
94+
media_mapping["model"]
95+
.objects.filter(**filter_params)
96+
.order_by(sort_filter)
9497
)
9598

9699
return render(
@@ -297,8 +300,7 @@ def profile(request):
297300
)
298301
else:
299302
title = "Couldn't find a matching MAL ID for: \n"
300-
messages.error(request, title + error)
301-
303+
messages.warning(request, title + error)
302304
else:
303305
messages.error(request, "There was an error with your request")
304306

@@ -314,9 +316,24 @@ def profile(request):
314316
"user_form": user_form,
315317
"password_form": password_form,
316318
}
319+
317320
return render(request, "app/profile.html", context)
318321

319322

323+
def export(request):
324+
# Create the HttpResponse object with the appropriate CSV header.
325+
response = HttpResponse(
326+
content_type="text/csv",
327+
headers={"Content-Disposition": 'attachment; filename="yamtrack.csv"'},
328+
)
329+
330+
response = exports.export_csv(response, request.user)
331+
332+
logger.info(f"User {request.user.username} exported their data")
333+
334+
return response
335+
336+
320337
def modal_data(request):
321338
media_type = request.GET.get("media_type")
322339
media_id = request.GET.get("media_id")

0 commit comments

Comments
 (0)