Merge branch 'bookwyrm-social:main' into url-names
This commit is contained in:
commit
5a2bf64864
135 changed files with 15863 additions and 3417 deletions
|
@ -28,6 +28,7 @@ from .admin.user_admin import UserAdmin, UserAdminList
|
|||
# user preferences
|
||||
from .preferences.change_password import ChangePassword
|
||||
from .preferences.edit_user import EditUser
|
||||
from .preferences.export import Export, export_user_book_data
|
||||
from .preferences.delete_user import DeleteUser
|
||||
from .preferences.block import Block, unblock
|
||||
|
||||
|
@ -39,7 +40,12 @@ from .books.books import (
|
|||
resolve_book,
|
||||
)
|
||||
from .books.books import update_book_from_remote
|
||||
from .books.edit_book import EditBook, ConfirmEditBook
|
||||
from .books.edit_book import (
|
||||
EditBook,
|
||||
ConfirmEditBook,
|
||||
CreateBook,
|
||||
create_book_from_data,
|
||||
)
|
||||
from .books.editions import Editions, switch_edition
|
||||
from .books.links import BookFileLinks, AddFileLink, delete_link
|
||||
|
||||
|
@ -47,7 +53,8 @@ from .books.links import BookFileLinks, AddFileLink, delete_link
|
|||
from .landing.about import about, privacy, conduct
|
||||
from .landing.landing import Home, Landing
|
||||
from .landing.login import Login, Logout
|
||||
from .landing.register import Register, ConfirmEmail, ConfirmEmailCode, resend_link
|
||||
from .landing.register import Register
|
||||
from .landing.register import ConfirmEmail, ConfirmEmailCode, ResendConfirmEmail
|
||||
from .landing.password import PasswordResetRequest, PasswordReset
|
||||
|
||||
# shelves
|
||||
|
|
|
@ -25,14 +25,23 @@ class Federation(View):
|
|||
|
||||
def get(self, request, status="federated"):
|
||||
"""list of servers"""
|
||||
servers = models.FederatedServer.objects.filter(status=status)
|
||||
|
||||
filters = {}
|
||||
if software := request.GET.get("application_type"):
|
||||
filters["application_type"] = software
|
||||
|
||||
servers = models.FederatedServer.objects.filter(status=status, **filters)
|
||||
|
||||
sort = request.GET.get("sort")
|
||||
sort_fields = ["created_date", "application_type", "server_name"]
|
||||
# pylint: disable=consider-using-f-string
|
||||
if not sort in sort_fields + ["-{:s}".format(f) for f in sort_fields]:
|
||||
sort_fields = [
|
||||
"created_date",
|
||||
"updated_date",
|
||||
"application_type",
|
||||
"server_name",
|
||||
]
|
||||
if not sort in sort_fields + [f"-{f}" for f in sort_fields]:
|
||||
sort = "-created_date"
|
||||
servers = servers.order_by(sort)
|
||||
servers = servers.order_by(sort, "-created_date")
|
||||
|
||||
paginated = Paginator(servers, PAGE_LENGTH)
|
||||
page = paginated.get_page(request.GET.get("page"))
|
||||
|
@ -49,6 +58,9 @@ class Federation(View):
|
|||
page.number, on_each_side=2, on_ends=1
|
||||
),
|
||||
"sort": sort,
|
||||
"software_options": models.FederatedServer.objects.values_list(
|
||||
"application_type", flat=True
|
||||
).distinct(),
|
||||
"form": forms.ServerForm(),
|
||||
}
|
||||
return TemplateResponse(request, "settings/federation/instance_list.html", data)
|
||||
|
|
|
@ -96,6 +96,7 @@ class ManageInviteRequests(View):
|
|||
"created_date",
|
||||
"invite__times_used",
|
||||
"invite__invitees__created_date",
|
||||
"answer",
|
||||
]
|
||||
# pylint: disable=consider-using-f-string
|
||||
if not sort in sort_fields + ["-{:s}".format(f) for f in sort_fields]:
|
||||
|
@ -143,6 +144,7 @@ class ManageInviteRequests(View):
|
|||
invite_request = get_object_or_404(
|
||||
models.InviteRequest, id=request.POST.get("invite-request")
|
||||
)
|
||||
|
||||
# only create a new invite if one doesn't exist already (resending)
|
||||
if not invite_request.invite:
|
||||
invite_request.invite = models.SiteInvite.objects.create(
|
||||
|
@ -170,10 +172,7 @@ class InviteRequest(View):
|
|||
received = True
|
||||
form.save()
|
||||
|
||||
data = {
|
||||
"request_form": form,
|
||||
"request_received": received,
|
||||
}
|
||||
data = {"request_form": form, "request_received": received}
|
||||
return TemplateResponse(request, "landing/landing.html", data)
|
||||
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
""" moderation via flagged posts and users """
|
||||
from django.contrib.auth.decorators import login_required, permission_required
|
||||
from django.core.paginator import Paginator
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.template.response import TemplateResponse
|
||||
|
@ -7,6 +8,7 @@ from django.utils.decorators import method_decorator
|
|||
from django.views import View
|
||||
|
||||
from bookwyrm import forms, models
|
||||
from bookwyrm.settings import PAGE_LENGTH
|
||||
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
|
@ -34,10 +36,17 @@ class ReportsAdmin(View):
|
|||
if username:
|
||||
filters["user__username__icontains"] = username
|
||||
filters["resolved"] = resolved
|
||||
|
||||
reports = models.Report.objects.filter(**filters)
|
||||
paginated = Paginator(reports, PAGE_LENGTH)
|
||||
page = paginated.get_page(request.GET.get("page"))
|
||||
data = {
|
||||
"resolved": resolved,
|
||||
"server": server,
|
||||
"reports": models.Report.objects.filter(**filters),
|
||||
"reports": page,
|
||||
"page_range": paginated.get_elided_page_range(
|
||||
page.number, on_each_side=2, on_ends=1
|
||||
),
|
||||
}
|
||||
return TemplateResponse(request, "settings/reports/reports.html", data)
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
""" the good people stuff! the authors! """
|
||||
from django.contrib.auth.decorators import login_required, permission_required
|
||||
from django.core.paginator import Paginator
|
||||
from django.db.models import Avg, Q
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.template.response import TemplateResponse
|
||||
from django.utils.decorators import method_decorator
|
||||
|
@ -31,9 +30,8 @@ class Author(View):
|
|||
return redirect_local_path
|
||||
|
||||
books = (
|
||||
models.Work.objects.filter(Q(authors=author) | Q(editions__authors=author))
|
||||
.annotate(Avg("editions__review__rating"))
|
||||
.order_by("editions__review__rating__avg")
|
||||
models.Work.objects.filter(editions__authors=author)
|
||||
.order_by("created_date")
|
||||
.distinct()
|
||||
)
|
||||
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
""" the good stuff! the books! """
|
||||
from re import sub
|
||||
from dateutil.parser import parse as dateparse
|
||||
from re import sub, findall
|
||||
from django.contrib.auth.decorators import login_required, permission_required
|
||||
from django.contrib.postgres.search import SearchRank, SearchVector
|
||||
from django.db import transaction
|
||||
from django.http import HttpResponseBadRequest
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.template.response import TemplateResponse
|
||||
from django.utils.datastructures import MultiValueDictKeyError
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.http import require_POST
|
||||
from django.views import View
|
||||
|
||||
from bookwyrm import book_search, forms, models
|
||||
|
@ -30,105 +29,27 @@ from .books import set_cover_from_url
|
|||
class EditBook(View):
|
||||
"""edit a book"""
|
||||
|
||||
def get(self, request, book_id=None):
|
||||
def get(self, request, book_id):
|
||||
"""info about a book"""
|
||||
book = None
|
||||
if book_id:
|
||||
book = get_edition(book_id)
|
||||
if not book.description:
|
||||
book.description = book.parent_work.description
|
||||
book = get_edition(book_id)
|
||||
if not book.description:
|
||||
book.description = book.parent_work.description
|
||||
data = {"book": book, "form": forms.EditionForm(instance=book)}
|
||||
return TemplateResponse(request, "book/edit/edit_book.html", data)
|
||||
|
||||
# pylint: disable=too-many-locals
|
||||
def post(self, request, book_id=None):
|
||||
def post(self, request, book_id):
|
||||
"""edit a book cool"""
|
||||
# returns None if no match is found
|
||||
book = models.Edition.objects.filter(id=book_id).first()
|
||||
book = get_object_or_404(models.Edition, id=book_id)
|
||||
form = forms.EditionForm(request.POST, request.FILES, instance=book)
|
||||
|
||||
data = {"book": book, "form": form}
|
||||
if not form.is_valid():
|
||||
return TemplateResponse(request, "book/edit/edit_book.html", data)
|
||||
|
||||
# filter out empty author fields
|
||||
add_author = [author for author in request.POST.getlist("add_author") if author]
|
||||
if add_author:
|
||||
data["add_author"] = add_author
|
||||
data["author_matches"] = []
|
||||
data["isni_matches"] = []
|
||||
|
||||
for author in add_author:
|
||||
if not author:
|
||||
continue
|
||||
# check for existing authors
|
||||
vector = SearchVector("name", weight="A") + SearchVector(
|
||||
"aliases", weight="B"
|
||||
)
|
||||
|
||||
author_matches = (
|
||||
models.Author.objects.annotate(search=vector)
|
||||
.annotate(rank=SearchRank(vector, author))
|
||||
.filter(rank__gt=0.4)
|
||||
.order_by("-rank")[:5]
|
||||
)
|
||||
|
||||
isni_authors = find_authors_by_name(
|
||||
author, description=True
|
||||
) # find matches from ISNI API
|
||||
|
||||
# dedupe isni authors we already have in the DB
|
||||
exists = [
|
||||
i
|
||||
for i in isni_authors
|
||||
for a in author_matches
|
||||
if sub(r"\D", "", str(i.isni)) == sub(r"\D", "", str(a.isni))
|
||||
]
|
||||
|
||||
# pylint: disable=cell-var-from-loop
|
||||
matches = list(filter(lambda x: x not in exists, isni_authors))
|
||||
# combine existing and isni authors
|
||||
matches.extend(author_matches)
|
||||
|
||||
data["author_matches"].append(
|
||||
{
|
||||
"name": author.strip(),
|
||||
"matches": matches,
|
||||
"existing_isnis": exists,
|
||||
}
|
||||
)
|
||||
|
||||
# we're creating a new book
|
||||
if not book:
|
||||
# check if this is an edition of an existing work
|
||||
author_text = book.author_text if book else add_author
|
||||
data["book_matches"] = book_search.search(
|
||||
f'{form.cleaned_data.get("title")} {author_text}',
|
||||
min_confidence=0.5,
|
||||
)[:5]
|
||||
data = add_authors(request, data)
|
||||
|
||||
# either of the above cases requires additional confirmation
|
||||
if add_author or not book:
|
||||
# creting a book or adding an author to a book needs another step
|
||||
data["confirm_mode"] = True
|
||||
# this isn't preserved because it isn't part of the form obj
|
||||
data["remove_authors"] = request.POST.getlist("remove_authors")
|
||||
data["cover_url"] = request.POST.get("cover-url")
|
||||
|
||||
# make sure the dates are passed in as datetime, they're currently a string
|
||||
# QueryDicts are immutable, we need to copy
|
||||
formcopy = data["form"].data.copy()
|
||||
try:
|
||||
formcopy["first_published_date"] = dateparse(
|
||||
formcopy["first_published_date"]
|
||||
)
|
||||
except (MultiValueDictKeyError, ValueError):
|
||||
pass
|
||||
try:
|
||||
formcopy["published_date"] = dateparse(formcopy["published_date"])
|
||||
except (MultiValueDictKeyError, ValueError):
|
||||
pass
|
||||
data["form"].data = formcopy
|
||||
if data.get("add_author"):
|
||||
return TemplateResponse(request, "book/edit/edit_book.html", data)
|
||||
|
||||
remove_authors = request.POST.getlist("remove_authors")
|
||||
|
@ -136,15 +57,156 @@ class EditBook(View):
|
|||
book.authors.remove(author_id)
|
||||
|
||||
book = form.save(commit=False)
|
||||
|
||||
url = request.POST.get("cover-url")
|
||||
if url:
|
||||
image = set_cover_from_url(url)
|
||||
if image:
|
||||
book.cover.save(*image, save=False)
|
||||
|
||||
book.save()
|
||||
return redirect(f"/book/{book.id}")
|
||||
|
||||
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
@method_decorator(
|
||||
permission_required("bookwyrm.edit_book", raise_exception=True), name="dispatch"
|
||||
)
|
||||
class CreateBook(View):
|
||||
"""brand new book"""
|
||||
|
||||
def get(self, request):
|
||||
"""info about a book"""
|
||||
data = {"form": forms.EditionForm()}
|
||||
return TemplateResponse(request, "book/edit/edit_book.html", data)
|
||||
|
||||
# pylint: disable=too-many-locals
|
||||
def post(self, request):
|
||||
"""create a new book"""
|
||||
# returns None if no match is found
|
||||
form = forms.EditionForm(request.POST, request.FILES)
|
||||
data = {"form": form}
|
||||
|
||||
# collect data provided by the work or import item
|
||||
parent_work_id = request.POST.get("parent_work")
|
||||
authors = None
|
||||
if request.POST.get("authors"):
|
||||
author_ids = findall(r"\d+", request.POST["authors"])
|
||||
authors = models.Author.objects.filter(id__in=author_ids)
|
||||
|
||||
# fake book in case we need to keep editing
|
||||
if parent_work_id:
|
||||
data["book"] = {
|
||||
"parent_work": {"id": parent_work_id},
|
||||
"authors": authors,
|
||||
}
|
||||
|
||||
if not form.is_valid():
|
||||
return TemplateResponse(request, "book/edit/edit_book.html", data)
|
||||
|
||||
data = add_authors(request, data)
|
||||
|
||||
# check if this is an edition of an existing work
|
||||
author_text = ", ".join(data.get("add_author", []))
|
||||
data["book_matches"] = book_search.search(
|
||||
f'{form.cleaned_data.get("title")} {author_text}',
|
||||
min_confidence=0.1,
|
||||
)[:5]
|
||||
|
||||
# go to confirm mode
|
||||
if not parent_work_id or data.get("add_author"):
|
||||
return TemplateResponse(request, "book/edit/edit_book.html", data)
|
||||
|
||||
with transaction.atomic():
|
||||
book = form.save()
|
||||
parent_work = get_object_or_404(models.Work, id=parent_work_id)
|
||||
book.parent_work = parent_work
|
||||
|
||||
if authors:
|
||||
book.authors.add(*authors)
|
||||
|
||||
url = request.POST.get("cover-url")
|
||||
if url:
|
||||
image = set_cover_from_url(url)
|
||||
if image:
|
||||
book.cover.save(*image, save=False)
|
||||
|
||||
book.save()
|
||||
return redirect(f"/book/{book.id}")
|
||||
|
||||
|
||||
def add_authors(request, data):
|
||||
"""helper for adding authors"""
|
||||
add_author = [author for author in request.POST.getlist("add_author") if author]
|
||||
if not add_author:
|
||||
return data
|
||||
|
||||
data["add_author"] = add_author
|
||||
data["author_matches"] = []
|
||||
data["isni_matches"] = []
|
||||
|
||||
# creting a book or adding an author to a book needs another step
|
||||
data["confirm_mode"] = True
|
||||
# this isn't preserved because it isn't part of the form obj
|
||||
data["remove_authors"] = request.POST.getlist("remove_authors")
|
||||
data["cover_url"] = request.POST.get("cover-url")
|
||||
|
||||
for author in add_author:
|
||||
# filter out empty author fields
|
||||
if not author:
|
||||
continue
|
||||
# check for existing authors
|
||||
vector = SearchVector("name", weight="A") + SearchVector("aliases", weight="B")
|
||||
|
||||
author_matches = (
|
||||
models.Author.objects.annotate(search=vector)
|
||||
.annotate(rank=SearchRank(vector, author))
|
||||
.filter(rank__gt=0.4)
|
||||
.order_by("-rank")[:5]
|
||||
)
|
||||
|
||||
isni_authors = find_authors_by_name(
|
||||
author, description=True
|
||||
) # find matches from ISNI API
|
||||
|
||||
# dedupe isni authors we already have in the DB
|
||||
exists = [
|
||||
i
|
||||
for i in isni_authors
|
||||
for a in author_matches
|
||||
if sub(r"\D", "", str(i.isni)) == sub(r"\D", "", str(a.isni))
|
||||
]
|
||||
|
||||
# pylint: disable=cell-var-from-loop
|
||||
matches = list(filter(lambda x: x not in exists, isni_authors))
|
||||
# combine existing and isni authors
|
||||
matches.extend(author_matches)
|
||||
|
||||
data["author_matches"].append(
|
||||
{
|
||||
"name": author.strip(),
|
||||
"matches": matches,
|
||||
"existing_isnis": exists,
|
||||
}
|
||||
)
|
||||
return data
|
||||
|
||||
|
||||
@require_POST
|
||||
@permission_required("bookwyrm.edit_book", raise_exception=True)
|
||||
def create_book_from_data(request):
|
||||
"""create a book with starter data"""
|
||||
author_ids = findall(r"\d+", request.POST.get("authors"))
|
||||
book = {
|
||||
"parent_work": {"id": request.POST.get("parent_work")},
|
||||
"authors": models.Author.objects.filter(id__in=author_ids).all(),
|
||||
"subjects": request.POST.getlist("subjects"),
|
||||
}
|
||||
|
||||
data = {"book": book, "form": forms.EditionForm(request.POST)}
|
||||
return TemplateResponse(request, "book/edit/edit_book.html", data)
|
||||
|
||||
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
@method_decorator(
|
||||
permission_required("bookwyrm.edit_book", raise_exception=True), name="dispatch"
|
||||
|
@ -168,6 +230,13 @@ class ConfirmEditBook(View):
|
|||
# save book
|
||||
book = form.save()
|
||||
|
||||
# add known authors
|
||||
authors = None
|
||||
if request.POST.get("authors"):
|
||||
author_ids = findall(r"\d+", request.POST["authors"])
|
||||
authors = models.Author.objects.filter(id__in=author_ids)
|
||||
book.authors.add(*authors)
|
||||
|
||||
# get or create author as needed
|
||||
for i in range(int(request.POST.get("author-match-count", 0))):
|
||||
match = request.POST.get(f"author_match-{i}")
|
||||
|
@ -201,7 +270,7 @@ class ConfirmEditBook(View):
|
|||
book.authors.add(author)
|
||||
|
||||
# create work, if needed
|
||||
if not book_id:
|
||||
if not book.parent_work:
|
||||
work_match = request.POST.get("parent_work")
|
||||
if work_match and work_match != "0":
|
||||
work = get_object_or_404(models.Work, id=work_match)
|
||||
|
|
|
@ -11,7 +11,7 @@ from django.template.response import TemplateResponse
|
|||
from django.views import View
|
||||
from django.views.decorators.http import require_POST
|
||||
|
||||
from bookwyrm import models
|
||||
from bookwyrm import forms, models
|
||||
from bookwyrm.activitypub import ActivitypubResponse
|
||||
from bookwyrm.settings import PAGE_LENGTH
|
||||
from bookwyrm.views.helpers import is_api_request
|
||||
|
@ -65,6 +65,7 @@ class Editions(View):
|
|||
page.number, on_each_side=2, on_ends=1
|
||||
),
|
||||
"work": work,
|
||||
"work_form": forms.EditionFromWorkForm(instance=work),
|
||||
"languages": languages,
|
||||
"formats": set(
|
||||
e.physical_format.lower() for e in editions if e.physical_format
|
||||
|
|
|
@ -2,12 +2,12 @@
|
|||
import urllib.parse
|
||||
import re
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.db import IntegrityError
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.template.response import TemplateResponse
|
||||
from django.views.decorators.http import require_POST
|
||||
|
||||
from bookwyrm import models
|
||||
from bookwyrm.models.relationship import clear_cache
|
||||
from .helpers import (
|
||||
get_user_from_username,
|
||||
handle_remote_webfinger,
|
||||
|
@ -22,17 +22,17 @@ def follow(request):
|
|||
"""follow another user, here or abroad"""
|
||||
username = request.POST["user"]
|
||||
to_follow = get_user_from_username(request.user, username)
|
||||
clear_cache(request.user, to_follow)
|
||||
|
||||
try:
|
||||
models.UserFollowRequest.objects.create(
|
||||
user_subject=request.user,
|
||||
user_object=to_follow,
|
||||
)
|
||||
except IntegrityError:
|
||||
pass
|
||||
follow_request, created = models.UserFollowRequest.objects.get_or_create(
|
||||
user_subject=request.user,
|
||||
user_object=to_follow,
|
||||
)
|
||||
|
||||
if request.GET.get("next"):
|
||||
return redirect(request.GET.get("next", "/"))
|
||||
if not created:
|
||||
# this request probably failed to connect with the remote
|
||||
# that means we should save to trigger a re-broadcast
|
||||
follow_request.save()
|
||||
|
||||
return redirect(to_follow.local_path)
|
||||
|
||||
|
@ -49,14 +49,14 @@ def unfollow(request):
|
|||
user_subject=request.user, user_object=to_unfollow
|
||||
).delete()
|
||||
except models.UserFollows.DoesNotExist:
|
||||
pass
|
||||
clear_cache(request.user, to_unfollow)
|
||||
|
||||
try:
|
||||
models.UserFollowRequest.objects.get(
|
||||
user_subject=request.user, user_object=to_unfollow
|
||||
).delete()
|
||||
except models.UserFollowRequest.DoesNotExist:
|
||||
pass
|
||||
clear_cache(request.user, to_unfollow)
|
||||
|
||||
# this is handled with ajax so it shouldn't really matter
|
||||
return redirect(request.headers.get("Referer", "/"))
|
||||
|
|
|
@ -5,7 +5,6 @@ from django.shortcuts import get_object_or_404, redirect
|
|||
from django.template.response import TemplateResponse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views import View
|
||||
from django.views.decorators.http import require_POST
|
||||
from django.views.decorators.debug import sensitive_variables, sensitive_post_parameters
|
||||
|
||||
from bookwyrm import emailing, forms, models
|
||||
|
@ -129,12 +128,22 @@ class ConfirmEmail(View):
|
|||
return ConfirmEmailCode().get(request, code)
|
||||
|
||||
|
||||
@require_POST
|
||||
def resend_link(request):
|
||||
"""resend confirmation link"""
|
||||
email = request.POST.get("email")
|
||||
user = get_object_or_404(models.User, email=email)
|
||||
emailing.email_confirmation_email(user)
|
||||
return TemplateResponse(
|
||||
request, "confirm_email/confirm_email.html", {"valid": True}
|
||||
)
|
||||
class ResendConfirmEmail(View):
|
||||
"""you probably didn't get the email because celery is slow but you can try this"""
|
||||
|
||||
def get(self, request, error=False):
|
||||
"""resend link landing page"""
|
||||
return TemplateResponse(request, "confirm_email/resend.html", {"error": error})
|
||||
|
||||
def post(self, request):
|
||||
"""resend confirmation link"""
|
||||
email = request.POST.get("email")
|
||||
try:
|
||||
user = models.User.objects.get(email=email)
|
||||
except models.User.DoesNotExist:
|
||||
return self.get(request, error=True)
|
||||
|
||||
emailing.email_confirmation_email(user)
|
||||
return TemplateResponse(
|
||||
request, "confirm_email/confirm_email.html", {"valid": True}
|
||||
)
|
||||
|
|
97
bookwyrm/views/preferences/export.py
Normal file
97
bookwyrm/views/preferences/export.py
Normal file
|
@ -0,0 +1,97 @@
|
|||
""" Let users export their book data """
|
||||
import csv
|
||||
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.db.models import Q
|
||||
from django.http import StreamingHttpResponse
|
||||
from django.template.response import TemplateResponse
|
||||
from django.views import View
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.http import require_GET
|
||||
|
||||
from bookwyrm import models
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
class Export(View):
|
||||
"""Let users export data"""
|
||||
|
||||
def get(self, request):
|
||||
"""Request csv file"""
|
||||
return TemplateResponse(request, "preferences/export.html")
|
||||
|
||||
|
||||
@login_required
|
||||
@require_GET
|
||||
def export_user_book_data(request):
|
||||
"""Streaming the csv file of a user's book data"""
|
||||
data = (
|
||||
models.Edition.viewer_aware_objects(request.user)
|
||||
.filter(
|
||||
Q(shelves__user=request.user)
|
||||
| Q(readthrough__user=request.user)
|
||||
| Q(review__user=request.user)
|
||||
| Q(comment__user=request.user)
|
||||
| Q(quotation__user=request.user)
|
||||
)
|
||||
.distinct()
|
||||
)
|
||||
|
||||
generator = csv_row_generator(data, request.user)
|
||||
|
||||
pseudo_buffer = Echo()
|
||||
writer = csv.writer(pseudo_buffer)
|
||||
# for testing, if you want to see the results in the browser:
|
||||
# from django.http import JsonResponse
|
||||
# return JsonResponse(list(generator), safe=False)
|
||||
return StreamingHttpResponse(
|
||||
(writer.writerow(row) for row in generator),
|
||||
content_type="text/csv",
|
||||
headers={"Content-Disposition": 'attachment; filename="bookwyrm-export.csv"'},
|
||||
)
|
||||
|
||||
|
||||
def csv_row_generator(books, user):
|
||||
"""generate a csv entry for the user's book"""
|
||||
deduplication_fields = [
|
||||
f.name
|
||||
for f in models.Edition._meta.get_fields() # pylint: disable=protected-access
|
||||
if getattr(f, "deduplication_field", False)
|
||||
]
|
||||
fields = (
|
||||
["title", "author_text"]
|
||||
+ deduplication_fields
|
||||
+ ["rating", "review_name", "review_cw", "review_content"]
|
||||
)
|
||||
yield fields
|
||||
for book in books:
|
||||
# I think this is more efficient than doing a subquery in the view? but idk
|
||||
review_rating = (
|
||||
models.Review.objects.filter(user=user, book=book, rating__isnull=False)
|
||||
.order_by("-published_date")
|
||||
.first()
|
||||
)
|
||||
|
||||
book.rating = review_rating.rating if review_rating else None
|
||||
|
||||
review = (
|
||||
models.Review.objects.filter(user=user, book=book, content__isnull=False)
|
||||
.order_by("-published_date")
|
||||
.first()
|
||||
)
|
||||
if review:
|
||||
book.review_name = review.name
|
||||
book.review_cw = review.content_warning
|
||||
book.review_content = review.raw_content
|
||||
yield [getattr(book, field, "") or "" for field in fields]
|
||||
|
||||
|
||||
class Echo:
|
||||
"""An object that implements just the write method of the file-like
|
||||
interface. (https://docs.djangoproject.com/en/3.2/howto/outputting-csv/)
|
||||
"""
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
def write(self, value):
|
||||
"""Write the value by returning it, instead of storing in a buffer."""
|
||||
return value
|
Loading…
Add table
Add a link
Reference in a new issue