Merge branch 'main' into csv
This commit is contained in:
commit
e5b260e3ee
400 changed files with 39801 additions and 9079 deletions
|
@ -5,6 +5,7 @@ from .admin.announcements import EditAnnouncement, delete_announcement
|
|||
from .admin.automod import AutoMod, automod_delete, run_automod
|
||||
from .admin.automod import schedule_automod_task, unschedule_automod_task
|
||||
from .admin.celery_status import CeleryStatus, celery_ping
|
||||
from .admin.schedule import ScheduledTasks
|
||||
from .admin.dashboard import Dashboard
|
||||
from .admin.federation import Federation, FederatedServer
|
||||
from .admin.federation import AddFederatedServer, ImportServerBlocklist
|
||||
|
@ -16,6 +17,10 @@ from .admin.imports import (
|
|||
disable_imports,
|
||||
enable_imports,
|
||||
set_import_size_limit,
|
||||
set_user_import_completed,
|
||||
set_user_import_limit,
|
||||
enable_user_exports,
|
||||
disable_user_exports,
|
||||
)
|
||||
from .admin.ip_blocklist import IPBlocklist
|
||||
from .admin.invite import ManageInvites, Invite, InviteRequest
|
||||
|
@ -36,7 +41,7 @@ from .admin.user_admin import UserAdmin, UserAdminList, ActivateUserAdmin
|
|||
# user preferences
|
||||
from .preferences.change_password import ChangePassword
|
||||
from .preferences.edit_user import EditUser
|
||||
from .preferences.export import Export
|
||||
from .preferences.export import Export, ExportUser, ExportArchive
|
||||
from .preferences.move_user import MoveUser, AliasUser, remove_alias, unmove
|
||||
from .preferences.delete_user import DeleteUser, DeactivateUser, ReactivateUser
|
||||
from .preferences.block import Block, unblock
|
||||
|
@ -81,7 +86,7 @@ from .shelf.shelf_actions import create_shelf, delete_shelf
|
|||
from .shelf.shelf_actions import shelve, unshelve
|
||||
|
||||
# csv import
|
||||
from .imports.import_data import Import
|
||||
from .imports.import_data import Import, UserImport
|
||||
from .imports.import_status import ImportStatus, retry_item, stop_import
|
||||
from .imports.troubleshoot import ImportTroubleshoot
|
||||
from .imports.manually_review import (
|
||||
|
@ -113,6 +118,7 @@ from .feed import DirectMessage, Feed, Replies, Status
|
|||
from .follow import (
|
||||
follow,
|
||||
unfollow,
|
||||
remove_follow,
|
||||
ostatus_follow_request,
|
||||
ostatus_follow_success,
|
||||
remote_follow,
|
||||
|
|
|
@ -6,7 +6,7 @@ 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_celery_beat.models import PeriodicTask
|
||||
from django_celery_beat.models import PeriodicTask, IntervalSchedule
|
||||
|
||||
from bookwyrm import forms, models
|
||||
|
||||
|
@ -54,7 +54,7 @@ def schedule_automod_task(request):
|
|||
return TemplateResponse(request, "settings/automod/rules.html", data)
|
||||
|
||||
with transaction.atomic():
|
||||
schedule = form.save(request)
|
||||
schedule, _ = IntervalSchedule.objects.get_or_create(**form.cleaned_data)
|
||||
PeriodicTask.objects.get_or_create(
|
||||
interval=schedule,
|
||||
name="automod-task",
|
||||
|
|
|
@ -6,16 +6,18 @@ from dateutil.parser import parse
|
|||
from packaging import version
|
||||
|
||||
from django.contrib.auth.decorators import login_required, permission_required
|
||||
from django.db import transaction
|
||||
from django.db.models import Q
|
||||
from django.shortcuts import redirect
|
||||
from django.template.response import TemplateResponse
|
||||
from django.utils import timezone
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views import View
|
||||
from django_celery_beat.models import PeriodicTask, IntervalSchedule
|
||||
|
||||
from csp.decorators import csp_update
|
||||
|
||||
from bookwyrm import models, settings
|
||||
from bookwyrm.connectors.abstract_connector import get_data
|
||||
from bookwyrm import forms, models, settings
|
||||
from bookwyrm.utils import regex
|
||||
|
||||
|
||||
|
@ -59,21 +61,36 @@ class Dashboard(View):
|
|||
== site._meta.get_field("privacy_policy").get_default()
|
||||
)
|
||||
|
||||
# check version
|
||||
if site.available_version and version.parse(
|
||||
site.available_version
|
||||
) > version.parse(settings.VERSION):
|
||||
data["current_version"] = settings.VERSION
|
||||
data["available_version"] = site.available_version
|
||||
|
||||
try:
|
||||
release = get_data(settings.RELEASE_API, timeout=3)
|
||||
available_version = release.get("tag_name", None)
|
||||
if available_version and version.parse(available_version) > version.parse(
|
||||
settings.VERSION
|
||||
):
|
||||
data["current_version"] = settings.VERSION
|
||||
data["available_version"] = available_version
|
||||
except: # pylint: disable= bare-except
|
||||
pass
|
||||
if not PeriodicTask.objects.filter(name="check-for-updates").exists():
|
||||
data["schedule_form"] = forms.IntervalScheduleForm(
|
||||
{"every": 1, "period": "days"}
|
||||
)
|
||||
|
||||
return TemplateResponse(request, "settings/dashboard/dashboard.html", data)
|
||||
|
||||
def post(self, request):
|
||||
"""Create a schedule task to check for updates"""
|
||||
schedule_form = forms.IntervalScheduleForm(request.POST)
|
||||
if not schedule_form.is_valid():
|
||||
raise schedule_form.ValidationError(schedule_form.errors)
|
||||
|
||||
with transaction.atomic():
|
||||
schedule, _ = IntervalSchedule.objects.get_or_create(
|
||||
**schedule_form.cleaned_data
|
||||
)
|
||||
PeriodicTask.objects.get_or_create(
|
||||
interval=schedule,
|
||||
name="check-for-updates",
|
||||
task="bookwyrm.models.site.check_for_updates_task",
|
||||
)
|
||||
return redirect("settings-dashboard")
|
||||
|
||||
|
||||
def get_charts_and_stats(request):
|
||||
"""Defines the dashboard charts"""
|
||||
|
|
|
@ -9,7 +9,7 @@ from django.views.decorators.http import require_POST
|
|||
|
||||
from bookwyrm import models
|
||||
from bookwyrm.views.helpers import redirect_to_referer
|
||||
from bookwyrm.settings import PAGE_LENGTH
|
||||
from bookwyrm.settings import PAGE_LENGTH, USE_AZURE
|
||||
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
|
@ -40,9 +40,17 @@ class ImportList(View):
|
|||
paginated = Paginator(imports, PAGE_LENGTH)
|
||||
page = paginated.get_page(request.GET.get("page"))
|
||||
|
||||
user_imports = models.BookwyrmImportJob.objects.filter(
|
||||
complete=complete
|
||||
).order_by("created_date")
|
||||
|
||||
user_paginated = Paginator(user_imports, PAGE_LENGTH)
|
||||
user_page = user_paginated.get_page(request.GET.get("page"))
|
||||
|
||||
site_settings = models.SiteSettings.objects.get()
|
||||
data = {
|
||||
"imports": page,
|
||||
"user_imports": user_page,
|
||||
"page_range": paginated.get_elided_page_range(
|
||||
page.number, on_each_side=2, on_ends=1
|
||||
),
|
||||
|
@ -50,6 +58,8 @@ class ImportList(View):
|
|||
"sort": sort,
|
||||
"import_size_limit": site_settings.import_size_limit,
|
||||
"import_limit_reset": site_settings.import_limit_reset,
|
||||
"user_import_time_limit": site_settings.user_import_time_limit,
|
||||
"use_azure": USE_AZURE,
|
||||
}
|
||||
return TemplateResponse(request, "settings/imports/imports.html", data)
|
||||
|
||||
|
@ -95,3 +105,47 @@ def set_import_size_limit(request):
|
|||
site.import_limit_reset = import_limit_reset
|
||||
site.save(update_fields=["import_size_limit", "import_limit_reset"])
|
||||
return redirect("settings-imports")
|
||||
|
||||
|
||||
@require_POST
|
||||
@login_required
|
||||
@permission_required("bookwyrm.moderate_user", raise_exception=True)
|
||||
# pylint: disable=unused-argument
|
||||
def set_user_import_completed(request, import_id):
|
||||
"""Mark a user import as complete"""
|
||||
import_job = get_object_or_404(models.BookwyrmImportJob, id=import_id)
|
||||
import_job.stop_job()
|
||||
return redirect("settings-imports")
|
||||
|
||||
|
||||
@require_POST
|
||||
@permission_required("bookwyrm.edit_instance_settings", raise_exception=True)
|
||||
# pylint: disable=unused-argument
|
||||
def set_user_import_limit(request):
|
||||
"""Limit how ofter users can import and export their account"""
|
||||
site = models.SiteSettings.objects.get()
|
||||
site.user_import_time_limit = int(request.POST.get("limit"))
|
||||
site.save(update_fields=["user_import_time_limit"])
|
||||
return redirect("settings-imports")
|
||||
|
||||
|
||||
@require_POST
|
||||
@permission_required("bookwyrm.edit_instance_settings", raise_exception=True)
|
||||
# pylint: disable=unused-argument
|
||||
def enable_user_exports(request):
|
||||
"""Allow users to export account data"""
|
||||
site = models.SiteSettings.objects.get()
|
||||
site.user_exports_enabled = True
|
||||
site.save(update_fields=["user_exports_enabled"])
|
||||
return redirect("settings-imports")
|
||||
|
||||
|
||||
@require_POST
|
||||
@permission_required("bookwyrm.edit_instance_settings", raise_exception=True)
|
||||
# pylint: disable=unused-argument
|
||||
def disable_user_exports(request):
|
||||
"""Don't allow users to export account data"""
|
||||
site = models.SiteSettings.objects.get()
|
||||
site.user_exports_enabled = False
|
||||
site.save(update_fields=["user_exports_enabled"])
|
||||
return redirect("settings-imports")
|
||||
|
|
31
bookwyrm/views/admin/schedule.py
Normal file
31
bookwyrm/views/admin/schedule.py
Normal file
|
@ -0,0 +1,31 @@
|
|||
""" Scheduled celery tasks """
|
||||
from django.contrib.auth.decorators import login_required, permission_required
|
||||
from django.shortcuts import redirect
|
||||
from django.template.response import TemplateResponse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views import View
|
||||
from django_celery_beat.models import PeriodicTask, IntervalSchedule
|
||||
|
||||
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
@method_decorator(
|
||||
permission_required("bookwyrm.edit_instance_settings", raise_exception=True),
|
||||
name="dispatch",
|
||||
)
|
||||
# pylint: disable=no-self-use
|
||||
class ScheduledTasks(View):
|
||||
"""Manage automated flagging"""
|
||||
|
||||
def get(self, request):
|
||||
"""view schedules"""
|
||||
data = {}
|
||||
data["tasks"] = PeriodicTask.objects.all()
|
||||
data["schedules"] = IntervalSchedule.objects.all()
|
||||
return TemplateResponse(request, "settings/schedules.html", data)
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def post(self, request, task_id):
|
||||
"""un-schedule a task"""
|
||||
task = PeriodicTask.objects.get(id=task_id)
|
||||
task.delete()
|
||||
return redirect("settings-schedules")
|
|
@ -225,4 +225,4 @@ def get_goal_status(user, year):
|
|||
if goal.privacy != "public":
|
||||
return None
|
||||
|
||||
return dict(**goal.progress, **{"goal": goal.goal})
|
||||
return {**goal.progress, **{"goal": goal.goal}}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
""" the good people stuff! the authors! """
|
||||
|
||||
from django.contrib.auth.decorators import login_required, permission_required
|
||||
from django.core.paginator import Paginator
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
|
@ -11,7 +12,11 @@ from bookwyrm import forms, models
|
|||
from bookwyrm.activitypub import ActivitypubResponse
|
||||
from bookwyrm.connectors import connector_manager
|
||||
from bookwyrm.settings import PAGE_LENGTH
|
||||
from bookwyrm.views.helpers import is_api_request, maybe_redirect_local_path
|
||||
from bookwyrm.views.helpers import (
|
||||
is_api_request,
|
||||
get_mergeable_object_or_404,
|
||||
maybe_redirect_local_path,
|
||||
)
|
||||
|
||||
|
||||
# pylint: disable= no-self-use
|
||||
|
@ -21,7 +26,7 @@ class Author(View):
|
|||
# pylint: disable=unused-argument
|
||||
def get(self, request, author_id, slug=None):
|
||||
"""landing page for an author"""
|
||||
author = get_object_or_404(models.Author, id=author_id)
|
||||
author = get_mergeable_object_or_404(models.Author, id=author_id)
|
||||
|
||||
if is_api_request(request):
|
||||
return ActivitypubResponse(author.to_activity())
|
||||
|
@ -56,13 +61,13 @@ class EditAuthor(View):
|
|||
|
||||
def get(self, request, author_id):
|
||||
"""info about a book"""
|
||||
author = get_object_or_404(models.Author, id=author_id)
|
||||
author = get_mergeable_object_or_404(models.Author, id=author_id)
|
||||
data = {"author": author, "form": forms.AuthorForm(instance=author)}
|
||||
return TemplateResponse(request, "author/edit_author.html", data)
|
||||
|
||||
def post(self, request, author_id):
|
||||
"""edit a author cool"""
|
||||
author = get_object_or_404(models.Author, id=author_id)
|
||||
author = get_mergeable_object_or_404(models.Author, id=author_id)
|
||||
|
||||
form = forms.AuthorForm(request.POST, request.FILES, instance=author)
|
||||
if not form.is_valid():
|
||||
|
@ -82,7 +87,7 @@ def update_author_from_remote(request, author_id, connector_identifier):
|
|||
connector = connector_manager.load_connector(
|
||||
get_object_or_404(models.Connector, identifier=connector_identifier)
|
||||
)
|
||||
author = get_object_or_404(models.Author, id=author_id)
|
||||
author = get_mergeable_object_or_404(models.Author, id=author_id)
|
||||
|
||||
connector.update_author_from_remote(author)
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
""" the good stuff! the books! """
|
||||
|
||||
from uuid import uuid4
|
||||
|
||||
from django.contrib.auth.decorators import login_required, permission_required
|
||||
|
@ -15,7 +16,11 @@ from bookwyrm.activitypub import ActivitypubResponse
|
|||
from bookwyrm.connectors import connector_manager, ConnectorException
|
||||
from bookwyrm.connectors.abstract_connector import get_image
|
||||
from bookwyrm.settings import PAGE_LENGTH
|
||||
from bookwyrm.views.helpers import is_api_request, maybe_redirect_local_path
|
||||
from bookwyrm.views.helpers import (
|
||||
is_api_request,
|
||||
maybe_redirect_local_path,
|
||||
get_mergeable_object_or_404,
|
||||
)
|
||||
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
|
@ -40,7 +45,11 @@ class Book(View):
|
|||
# table, so they never have clashing IDs
|
||||
book = (
|
||||
models.Edition.viewer_aware_objects(request.user)
|
||||
.filter(Q(id=book_id) | Q(parent_work__id=book_id))
|
||||
.filter(
|
||||
Q(id=book_id)
|
||||
| Q(parent_work__id=book_id)
|
||||
| Q(absorbed__deleted_id=book_id)
|
||||
)
|
||||
.order_by("-edition_rank")
|
||||
.select_related("parent_work")
|
||||
.prefetch_related("authors", "file_links")
|
||||
|
@ -82,11 +91,13 @@ class Book(View):
|
|||
"book": book,
|
||||
"statuses": paginated.get_page(request.GET.get("page")),
|
||||
"review_count": reviews.count(),
|
||||
"ratings": reviews.filter(
|
||||
Q(content__isnull=True) | Q(content="")
|
||||
).select_related("user")
|
||||
if not user_statuses
|
||||
else None,
|
||||
"ratings": (
|
||||
reviews.filter(Q(content__isnull=True) | Q(content="")).select_related(
|
||||
"user"
|
||||
)
|
||||
if not user_statuses
|
||||
else None
|
||||
),
|
||||
"rating": reviews.aggregate(Avg("rating"))["rating__avg"],
|
||||
"lists": lists,
|
||||
"update_error": kwargs.get("update_error", False),
|
||||
|
@ -130,7 +141,7 @@ class Book(View):
|
|||
@require_POST
|
||||
def upload_cover(request, book_id):
|
||||
"""upload a new cover"""
|
||||
book = get_object_or_404(models.Edition, id=book_id)
|
||||
book = get_mergeable_object_or_404(models.Edition, id=book_id)
|
||||
book.last_edited_by = request.user
|
||||
|
||||
url = request.POST.get("cover-url")
|
||||
|
@ -168,7 +179,7 @@ def set_cover_from_url(url):
|
|||
@permission_required("bookwyrm.edit_book", raise_exception=True)
|
||||
def add_description(request, book_id):
|
||||
"""upload a new cover"""
|
||||
book = get_object_or_404(models.Edition, id=book_id)
|
||||
book = get_mergeable_object_or_404(models.Edition, id=book_id)
|
||||
|
||||
description = request.POST.get("description")
|
||||
|
||||
|
@ -199,7 +210,9 @@ def update_book_from_remote(request, book_id, connector_identifier):
|
|||
connector = connector_manager.load_connector(
|
||||
get_object_or_404(models.Connector, identifier=connector_identifier)
|
||||
)
|
||||
book = get_object_or_404(models.Book.objects.select_subclasses(), id=book_id)
|
||||
book = get_mergeable_object_or_404(
|
||||
models.Book.objects.select_subclasses(), id=book_id
|
||||
)
|
||||
|
||||
try:
|
||||
connector.update_book_from_remote(book)
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
""" the good stuff! the books! """
|
||||
|
||||
from re import sub, findall
|
||||
from django.contrib.auth.decorators import login_required, permission_required
|
||||
from django.contrib.postgres.search import SearchRank, SearchVector
|
||||
|
@ -18,9 +19,10 @@ from bookwyrm.utils.isni import (
|
|||
build_author_from_isni,
|
||||
augment_author_metadata,
|
||||
)
|
||||
from bookwyrm.views.helpers import get_edition
|
||||
from bookwyrm.views.helpers import get_edition, get_mergeable_object_or_404
|
||||
from .books import set_cover_from_url
|
||||
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
@method_decorator(
|
||||
|
@ -42,7 +44,7 @@ class EditBook(View):
|
|||
|
||||
def post(self, request, book_id):
|
||||
"""edit a book cool"""
|
||||
book = get_object_or_404(models.Edition, id=book_id)
|
||||
book = get_mergeable_object_or_404(models.Edition, id=book_id)
|
||||
|
||||
form = forms.EditionForm(request.POST, request.FILES, instance=book)
|
||||
|
||||
|
@ -130,7 +132,7 @@ class CreateBook(View):
|
|||
|
||||
with transaction.atomic():
|
||||
book = form.save(request)
|
||||
parent_work = get_object_or_404(models.Work, id=parent_work_id)
|
||||
parent_work = get_mergeable_object_or_404(models.Work, id=parent_work_id)
|
||||
book.parent_work = parent_work
|
||||
|
||||
if authors:
|
||||
|
@ -295,7 +297,7 @@ class ConfirmEditBook(View):
|
|||
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)
|
||||
work = get_mergeable_object_or_404(models.Work, id=work_match)
|
||||
else:
|
||||
work = models.Work.objects.create(title=form.cleaned_data["title"])
|
||||
work.authors.set(book.authors.all())
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
""" the good stuff! the books! """
|
||||
|
||||
from functools import reduce
|
||||
import operator
|
||||
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.cache import cache as django_cache
|
||||
from django.core.paginator import Paginator
|
||||
from django.db import transaction
|
||||
from django.db.models import Q
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.shortcuts import redirect
|
||||
from django.template.response import TemplateResponse
|
||||
from django.views import View
|
||||
from django.views.decorators.http import require_POST
|
||||
|
@ -14,7 +16,7 @@ from django.views.decorators.http import require_POST
|
|||
from bookwyrm import forms, models
|
||||
from bookwyrm.activitypub import ActivitypubResponse
|
||||
from bookwyrm.settings import PAGE_LENGTH
|
||||
from bookwyrm.views.helpers import is_api_request
|
||||
from bookwyrm.views.helpers import is_api_request, get_mergeable_object_or_404
|
||||
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
|
@ -23,7 +25,7 @@ class Editions(View):
|
|||
|
||||
def get(self, request, book_id):
|
||||
"""list of editions of a book"""
|
||||
work = get_object_or_404(models.Work, id=book_id)
|
||||
work = get_mergeable_object_or_404(models.Work, id=book_id)
|
||||
|
||||
if is_api_request(request):
|
||||
return ActivitypubResponse(work.to_edition_list(**request.GET))
|
||||
|
@ -82,7 +84,7 @@ class Editions(View):
|
|||
def switch_edition(request):
|
||||
"""switch your copy of a book to a different edition"""
|
||||
edition_id = request.POST.get("edition")
|
||||
new_edition = get_object_or_404(models.Edition, id=edition_id)
|
||||
new_edition = get_mergeable_object_or_404(models.Edition, id=edition_id)
|
||||
shelfbooks = models.ShelfBook.objects.filter(
|
||||
book__parent_work=new_edition.parent_work, shelf__user=request.user
|
||||
)
|
||||
|
@ -93,6 +95,7 @@ def switch_edition(request):
|
|||
user=shelfbook.user,
|
||||
shelf=shelfbook.shelf,
|
||||
book=new_edition,
|
||||
shelved_date=shelfbook.shelved_date,
|
||||
)
|
||||
shelfbook.delete()
|
||||
|
||||
|
@ -103,4 +106,20 @@ def switch_edition(request):
|
|||
readthrough.book = new_edition
|
||||
readthrough.save()
|
||||
|
||||
django_cache.delete_many(
|
||||
[
|
||||
f"active_shelf-{request.user.id}-{book_id}"
|
||||
for book_id in new_edition.parent_work.editions.values_list("id", flat=True)
|
||||
]
|
||||
)
|
||||
|
||||
reviews = models.Review.objects.filter(
|
||||
book__parent_work=new_edition.parent_work, user=request.user
|
||||
)
|
||||
for review in reviews.all():
|
||||
# because ratings are a subclass of reviews,
|
||||
# this will pick up both ratings and reviews
|
||||
review.book = new_edition
|
||||
review.save()
|
||||
|
||||
return redirect(f"/book/{new_edition.id}")
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
""" the good stuff! the books! """
|
||||
|
||||
from django.contrib.auth.decorators import login_required, permission_required
|
||||
from django.db import transaction
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
|
@ -8,6 +9,7 @@ from django.utils.decorators import method_decorator
|
|||
from django.views.decorators.http import require_POST
|
||||
|
||||
from bookwyrm import forms, models
|
||||
from bookwyrm.views.helpers import get_mergeable_object_or_404
|
||||
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
|
@ -20,7 +22,7 @@ class BookFileLinks(View):
|
|||
|
||||
def get(self, request, book_id):
|
||||
"""view links"""
|
||||
book = get_object_or_404(models.Edition, id=book_id)
|
||||
book = get_mergeable_object_or_404(models.Edition, id=book_id)
|
||||
annotated_links = get_annotated_links(book)
|
||||
|
||||
data = {"book": book, "links": annotated_links}
|
||||
|
@ -36,7 +38,7 @@ class BookFileLinks(View):
|
|||
|
||||
# this form shouldn't ever really get here, since it's just a dropdown
|
||||
# get the data again rather than redirecting
|
||||
book = get_object_or_404(models.Edition, id=book_id)
|
||||
book = get_mergeable_object_or_404(models.Edition, id=book_id)
|
||||
annotated_links = get_annotated_links(book, form=form)
|
||||
|
||||
data = {"book": book, "links": annotated_links}
|
||||
|
@ -75,7 +77,7 @@ class AddFileLink(View):
|
|||
|
||||
def get(self, request, book_id):
|
||||
"""Create link form"""
|
||||
book = get_object_or_404(models.Edition, id=book_id)
|
||||
book = get_mergeable_object_or_404(models.Edition, id=book_id)
|
||||
data = {
|
||||
"file_link_form": forms.FileLinkForm(),
|
||||
"book": book,
|
||||
|
@ -85,7 +87,9 @@ class AddFileLink(View):
|
|||
@transaction.atomic
|
||||
def post(self, request, book_id, link_id=None):
|
||||
"""Add a link to a copy of the book you can read"""
|
||||
book = get_object_or_404(models.Book.objects.select_subclasses(), id=book_id)
|
||||
book = get_mergeable_object_or_404(
|
||||
models.Book.objects.select_subclasses(), id=book_id
|
||||
)
|
||||
link = get_object_or_404(models.FileLink, id=link_id) if link_id else None
|
||||
form = forms.FileLinkForm(request.POST, instance=link)
|
||||
if not form.is_valid():
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
""" books belonging to the same series """
|
||||
|
||||
from sys import float_info
|
||||
from django.views import View
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.template.response import TemplateResponse
|
||||
|
||||
from bookwyrm.views.helpers import is_api_request
|
||||
from bookwyrm.views.helpers import is_api_request, get_mergeable_object_or_404
|
||||
from bookwyrm import models
|
||||
|
||||
|
||||
|
@ -27,7 +27,7 @@ class BookSeriesBy(View):
|
|||
if is_api_request(request):
|
||||
pass
|
||||
|
||||
author = get_object_or_404(models.Author, id=author_id)
|
||||
author = get_mergeable_object_or_404(models.Author, id=author_id)
|
||||
|
||||
results = models.Edition.objects.filter(authors=author, series=series_name)
|
||||
|
||||
|
@ -56,9 +56,11 @@ class BookSeriesBy(View):
|
|||
sorted(numbered_books, key=sort_by_series)
|
||||
+ sorted(
|
||||
dated_books,
|
||||
key=lambda book: book.first_published_date
|
||||
if book.first_published_date
|
||||
else book.published_date,
|
||||
key=lambda book: (
|
||||
book.first_published_date
|
||||
if book.first_published_date
|
||||
else book.published_date
|
||||
),
|
||||
)
|
||||
+ sorted(
|
||||
unsortable_books,
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
""" non-interactive pages """
|
||||
from datetime import date
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.paginator import Paginator
|
||||
from django.db.models import Q
|
||||
|
@ -52,6 +53,19 @@ class Feed(View):
|
|||
|
||||
suggestions = suggested_users.get_suggestions(request.user)
|
||||
|
||||
cutoff = (
|
||||
date(get_annual_summary_year(), 12, 31)
|
||||
if get_annual_summary_year()
|
||||
else None
|
||||
)
|
||||
readthroughs = (
|
||||
models.ReadThrough.objects.filter(
|
||||
user=request.user, finish_date__lte=cutoff
|
||||
)
|
||||
if get_annual_summary_year()
|
||||
else []
|
||||
)
|
||||
|
||||
data = {
|
||||
**feed_page_data(request.user),
|
||||
**{
|
||||
|
@ -66,6 +80,7 @@ class Feed(View):
|
|||
"path": f"/{tab['key']}",
|
||||
"annual_summary_year": get_annual_summary_year(),
|
||||
"has_tour": True,
|
||||
"has_summary_read_throughs": len(readthroughs),
|
||||
},
|
||||
}
|
||||
return TemplateResponse(request, "feed/feed.html", data)
|
||||
|
@ -185,19 +200,15 @@ class Status(View):
|
|||
params=[status.id, visible_thread, visible_thread],
|
||||
)
|
||||
|
||||
preview = None
|
||||
if hasattr(status, "book"):
|
||||
preview = status.book.preview_image
|
||||
elif status.mention_books.exists():
|
||||
preview = status.mention_books.first().preview_image
|
||||
|
||||
data = {
|
||||
**feed_page_data(request.user),
|
||||
**{
|
||||
"status": status,
|
||||
"children": children,
|
||||
"ancestors": ancestors,
|
||||
"preview": preview,
|
||||
"title": status.page_title,
|
||||
"description": status.page_description,
|
||||
"page_image": status.page_image,
|
||||
},
|
||||
}
|
||||
return TemplateResponse(request, "feed/status.html", data)
|
||||
|
|
|
@ -69,6 +69,33 @@ def unfollow(request):
|
|||
return redirect("/")
|
||||
|
||||
|
||||
@login_required
|
||||
@require_POST
|
||||
def remove_follow(request, user_id):
|
||||
"""remove a previously approved follower without blocking them"""
|
||||
|
||||
to_remove = get_object_or_404(models.User, id=user_id)
|
||||
|
||||
try:
|
||||
models.UserFollows.objects.get(
|
||||
user_subject=to_remove, user_object=request.user
|
||||
).reject()
|
||||
except models.UserFollows.DoesNotExist:
|
||||
clear_cache(to_remove, request.user)
|
||||
|
||||
try:
|
||||
models.UserFollowRequest.objects.get(
|
||||
user_subject=to_remove, user_object=request.user
|
||||
).reject()
|
||||
except models.UserFollowRequest.DoesNotExist:
|
||||
clear_cache(to_remove, request.user)
|
||||
|
||||
if is_api_request(request):
|
||||
return HttpResponse()
|
||||
|
||||
return redirect(f"{request.user.local_path}/followers")
|
||||
|
||||
|
||||
@login_required
|
||||
@require_POST
|
||||
def accept_follow_request(request):
|
||||
|
@ -100,7 +127,7 @@ def delete_follow_request(request):
|
|||
)
|
||||
follow_request.raise_not_deletable(request.user)
|
||||
|
||||
follow_request.delete()
|
||||
follow_request.reject()
|
||||
return redirect(f"/user/{request.user.localname}")
|
||||
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
""" Helping new users figure out the lay of the land """
|
||||
|
||||
import re
|
||||
|
||||
from django.contrib.auth.decorators import login_required
|
||||
|
@ -13,6 +14,7 @@ from django.views import View
|
|||
from bookwyrm import book_search, forms, models
|
||||
from bookwyrm.settings import INSTANCE_ACTOR_USERNAME
|
||||
from bookwyrm.suggested_users import suggested_users
|
||||
from bookwyrm.views.helpers import get_mergeable_object_or_404
|
||||
from .preferences.edit_user import save_user_form
|
||||
|
||||
|
||||
|
@ -80,8 +82,8 @@ class GetStartedBooks(View):
|
|||
for k, v in request.POST.items()
|
||||
if re.match(r"\d+", k) and re.match(r"\d+", v)
|
||||
]
|
||||
for (book_id, shelf_id) in shelve_actions:
|
||||
book = get_object_or_404(models.Edition, id=book_id)
|
||||
for book_id, shelf_id in shelve_actions:
|
||||
book = get_mergeable_object_or_404(models.Edition, id=book_id)
|
||||
shelf = get_object_or_404(models.Shelf, id=shelf_id)
|
||||
|
||||
models.ShelfBook.objects.create(book=book, shelf=shelf, user=request.user)
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
""" helper functions used in various views """
|
||||
|
||||
import re
|
||||
from datetime import datetime, timedelta
|
||||
import dateutil.parser
|
||||
|
@ -8,7 +9,7 @@ from dateutil.parser import ParserError
|
|||
from requests import HTTPError
|
||||
from django.db.models import Q
|
||||
from django.conf import settings as django_settings
|
||||
from django.shortcuts import redirect
|
||||
from django.shortcuts import redirect, _get_queryset
|
||||
from django.http import Http404
|
||||
from django.utils import translation
|
||||
|
||||
|
@ -60,7 +61,7 @@ def is_bookwyrm_request(request):
|
|||
return True
|
||||
|
||||
|
||||
def handle_remote_webfinger(query, unknown_only=False):
|
||||
def handle_remote_webfinger(query, unknown_only=False, refresh=False):
|
||||
"""webfingerin' other servers"""
|
||||
user = None
|
||||
|
||||
|
@ -75,6 +76,11 @@ def handle_remote_webfinger(query, unknown_only=False):
|
|||
return None
|
||||
|
||||
try:
|
||||
|
||||
if refresh:
|
||||
# Always fetch the remote info - don't even bother checking the DB
|
||||
raise models.User.DoesNotExist("remote_only is set to True")
|
||||
|
||||
user = models.User.objects.get(username__iexact=query)
|
||||
|
||||
if unknown_only:
|
||||
|
@ -92,7 +98,7 @@ def handle_remote_webfinger(query, unknown_only=False):
|
|||
if link.get("rel") == "self":
|
||||
try:
|
||||
user = activitypub.resolve_remote_id(
|
||||
link["href"], model=models.User
|
||||
link["href"], model=models.User, refresh=refresh
|
||||
)
|
||||
except (KeyError, activitypub.ActivitySerializerError):
|
||||
return None
|
||||
|
@ -225,10 +231,26 @@ def maybe_redirect_local_path(request, model):
|
|||
def redirect_to_referer(request, *args, **kwargs):
|
||||
"""Redirect to the referrer, if it's in our domain, with get params"""
|
||||
# make sure the refer is part of this instance
|
||||
validated = validate_url_domain(request.META.get("HTTP_REFERER"))
|
||||
validated = validate_url_domain(request.headers.get("referer", ""))
|
||||
|
||||
if validated:
|
||||
return redirect(validated)
|
||||
|
||||
# if not, use the args passed you'd normally pass to redirect()
|
||||
return redirect(*args or "/", **kwargs)
|
||||
|
||||
|
||||
# pylint: disable=redefined-builtin,invalid-name
|
||||
def get_mergeable_object_or_404(klass, id):
|
||||
"""variant of get_object_or_404 that also redirects if id has been merged
|
||||
into another object"""
|
||||
queryset = _get_queryset(klass)
|
||||
try:
|
||||
return queryset.get(pk=id)
|
||||
except queryset.model.DoesNotExist:
|
||||
try:
|
||||
return queryset.get(absorbed__deleted_id=id)
|
||||
except queryset.model.DoesNotExist:
|
||||
pass
|
||||
|
||||
raise Http404(f"No {queryset.model} with ID {id} exists")
|
||||
|
|
|
@ -15,13 +15,14 @@ from django.views import View
|
|||
|
||||
from bookwyrm import forms, models
|
||||
from bookwyrm.importers import (
|
||||
BookwyrmBooksImporter,
|
||||
BookwyrmImporter,
|
||||
CalibreImporter,
|
||||
LibrarythingImporter,
|
||||
GoodreadsImporter,
|
||||
StorygraphImporter,
|
||||
OpenLibraryImporter,
|
||||
)
|
||||
from bookwyrm.models.bookwyrm_import_job import BookwyrmImportJob
|
||||
from bookwyrm.settings import PAGE_LENGTH
|
||||
from bookwyrm.utils.cache import get_or_set
|
||||
|
||||
|
@ -134,3 +135,61 @@ def get_average_import_time() -> float:
|
|||
if recent_avg:
|
||||
return recent_avg.total_seconds()
|
||||
return None
|
||||
|
||||
|
||||
# pylint: disable= no-self-use
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
class UserImport(View):
|
||||
"""import user view"""
|
||||
|
||||
def get(self, request, invalid=False):
|
||||
"""load user import page"""
|
||||
|
||||
jobs = BookwyrmImportJob.objects.filter(user=request.user).order_by(
|
||||
"-created_date"
|
||||
)
|
||||
site = models.SiteSettings.objects.get()
|
||||
hours = site.user_import_time_limit
|
||||
allowed = (
|
||||
jobs.first().created_date < timezone.now() - datetime.timedelta(hours=hours)
|
||||
if jobs.first()
|
||||
else True
|
||||
)
|
||||
next_available = (
|
||||
jobs.first().created_date + datetime.timedelta(hours=hours)
|
||||
if not allowed
|
||||
else False
|
||||
)
|
||||
paginated = Paginator(jobs, PAGE_LENGTH)
|
||||
page = paginated.get_page(request.GET.get("page"))
|
||||
data = {
|
||||
"import_form": forms.ImportUserForm(),
|
||||
"jobs": page,
|
||||
"user_import_hours": hours,
|
||||
"next_available": next_available,
|
||||
"page_range": paginated.get_elided_page_range(
|
||||
page.number, on_each_side=2, on_ends=1
|
||||
),
|
||||
"invalid": invalid,
|
||||
}
|
||||
|
||||
return TemplateResponse(request, "import/import_user.html", data)
|
||||
|
||||
def post(self, request):
|
||||
"""ingest a Bookwyrm json file"""
|
||||
|
||||
importer = BookwyrmImporter()
|
||||
|
||||
form = forms.ImportUserForm(request.POST, request.FILES)
|
||||
if not form.is_valid():
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
job = importer.process_import(
|
||||
user=request.user,
|
||||
archive_file=request.FILES["archive_file"],
|
||||
settings=request.POST,
|
||||
)
|
||||
|
||||
job.start_job()
|
||||
|
||||
return redirect("user-import")
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
""" class views for login/register views """
|
||||
import pytz
|
||||
import zoneinfo
|
||||
from django.contrib.auth import login
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
|
@ -57,9 +57,11 @@ class Register(View):
|
|||
email = form.data["email"]
|
||||
password = form.data["password"]
|
||||
try:
|
||||
preferred_timezone = pytz.timezone(form.data.get("preferred_timezone"))
|
||||
except pytz.exceptions.UnknownTimeZoneError:
|
||||
preferred_timezone = pytz.utc
|
||||
preferred_timezone = zoneinfo.ZoneInfo(
|
||||
form.data.get("preferred_timezone", "")
|
||||
)
|
||||
except (ValueError, zoneinfo.ZoneInfoNotFoundError):
|
||||
preferred_timezone = zoneinfo.ZoneInfo("UTC")
|
||||
|
||||
# make sure the email isn't blocked as spam
|
||||
email_domain = email.split("@")[-1]
|
||||
|
|
|
@ -1,17 +1,27 @@
|
|||
""" Let users export their book data """
|
||||
from datetime import timedelta
|
||||
import csv
|
||||
import io
|
||||
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.paginator import Paginator
|
||||
from django.db.models import Q
|
||||
from django.http import HttpResponse
|
||||
from django.http import HttpResponse, HttpResponseServerError, Http404
|
||||
from django.template.response import TemplateResponse
|
||||
from django.utils import timezone
|
||||
from django.views import View
|
||||
from django.urls import reverse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.shortcuts import redirect
|
||||
|
||||
from storages.backends.s3 import S3Storage
|
||||
|
||||
from bookwyrm import models
|
||||
from bookwyrm.models.bookwyrm_export_job import BookwyrmExportJob
|
||||
from bookwyrm import settings
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
|
||||
# pylint: disable=no-self-use,too-many-locals
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
class Export(View):
|
||||
"""Let users export data"""
|
||||
|
@ -49,14 +59,17 @@ class Export(View):
|
|||
["title", "author_text"]
|
||||
+ deduplication_fields
|
||||
+ [
|
||||
"start_date",
|
||||
"finish_date",
|
||||
"stopped_date",
|
||||
"rating",
|
||||
"review_published",
|
||||
"review_name",
|
||||
"review_cw",
|
||||
"review_content",
|
||||
"review_published",
|
||||
"shelf",
|
||||
"shelf_name",
|
||||
"date_added",
|
||||
"shelf_date",
|
||||
]
|
||||
)
|
||||
writer.writerow(fields)
|
||||
|
@ -73,6 +86,24 @@ class Export(View):
|
|||
|
||||
book.rating = review_rating.rating if review_rating else None
|
||||
|
||||
readthrough = (
|
||||
models.ReadThrough.objects.filter(user=request.user, book=book)
|
||||
.order_by("-start_date", "-finish_date")
|
||||
.first()
|
||||
)
|
||||
if readthrough:
|
||||
book.start_date = (
|
||||
readthrough.start_date.date() if readthrough.start_date else None
|
||||
)
|
||||
book.finish_date = (
|
||||
readthrough.finish_date.date() if readthrough.finish_date else None
|
||||
)
|
||||
book.stopped_date = (
|
||||
readthrough.stopped_date.date()
|
||||
if readthrough.stopped_date
|
||||
else None
|
||||
)
|
||||
|
||||
review = (
|
||||
models.Review.objects.filter(
|
||||
user=request.user, book=book, content__isnull=False
|
||||
|
@ -81,22 +112,26 @@ class Export(View):
|
|||
.first()
|
||||
)
|
||||
if review:
|
||||
book.review_published = review.published_date
|
||||
book.review_published = (
|
||||
review.published_date.date() if review.published_date else None
|
||||
)
|
||||
book.review_name = review.name
|
||||
book.review_cw = review.content_warning
|
||||
book.review_content = (
|
||||
review.raw_content
|
||||
) # do imported reviews not have raw content?
|
||||
review.raw_content if review.raw_content else review.content
|
||||
) # GoodReads imported reviews do not have raw_content, but content.
|
||||
|
||||
shelfbook = (
|
||||
models.ShelfBook.objects.filter(book=book, user=request.user)
|
||||
.order_by("shelved_date")
|
||||
models.ShelfBook.objects.filter(user=request.user, book=book)
|
||||
.order_by("-shelved_date", "-created_date", "-updated_date")
|
||||
.last()
|
||||
)
|
||||
if shelfbook:
|
||||
book.shelf = shelfbook.shelf.identifier
|
||||
book.shelf_name = shelfbook.shelf.name
|
||||
book.date_added = shelfbook.shelved_date
|
||||
book.shelf_date = (
|
||||
shelfbook.shelved_date.date() if shelfbook.shelved_date else None
|
||||
)
|
||||
|
||||
writer.writerow([getattr(book, field, "") or "" for field in fields])
|
||||
|
||||
|
@ -107,3 +142,114 @@ class Export(View):
|
|||
"Content-Disposition": 'attachment; filename="bookwyrm-export.csv"'
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
class ExportUser(View):
|
||||
"""
|
||||
Let users request and download an archive of user data to import into
|
||||
another Bookwyrm instance.
|
||||
"""
|
||||
|
||||
user_jobs = None
|
||||
|
||||
def setup(self, request, *args, **kwargs):
|
||||
super().setup(request, *args, **kwargs)
|
||||
|
||||
self.user_jobs = BookwyrmExportJob.objects.filter(user=request.user).order_by(
|
||||
"-created_date"
|
||||
)
|
||||
|
||||
def new_export_blocked_until(self):
|
||||
"""whether the user is allowed to request a new export"""
|
||||
last_job = self.user_jobs.first()
|
||||
if not last_job:
|
||||
return None
|
||||
site = models.SiteSettings.objects.get()
|
||||
blocked_until = last_job.created_date + timedelta(
|
||||
hours=site.user_import_time_limit
|
||||
)
|
||||
return blocked_until if blocked_until > timezone.now() else None
|
||||
|
||||
def get(self, request):
|
||||
"""Request tar file"""
|
||||
|
||||
exports = []
|
||||
for job in self.user_jobs:
|
||||
export = {"job": job}
|
||||
|
||||
if job.export_data:
|
||||
try:
|
||||
export["size"] = job.export_data.size
|
||||
export["url"] = reverse("prefs-export-file", args=[job.task_id])
|
||||
except FileNotFoundError:
|
||||
# file no longer exists locally
|
||||
export["unavailable"] = True
|
||||
except Exception: # pylint: disable=broad-except
|
||||
# file no longer exists on storage backend
|
||||
export["unavailable"] = True
|
||||
|
||||
exports.append(export)
|
||||
|
||||
next_available = self.new_export_blocked_until()
|
||||
paginated = Paginator(exports, settings.PAGE_LENGTH)
|
||||
page = paginated.get_page(request.GET.get("page"))
|
||||
data = {
|
||||
"jobs": page,
|
||||
"next_available": next_available,
|
||||
"page_range": paginated.get_elided_page_range(
|
||||
page.number, on_each_side=2, on_ends=1
|
||||
),
|
||||
}
|
||||
|
||||
return TemplateResponse(request, "preferences/export-user.html", data)
|
||||
|
||||
def post(self, request):
|
||||
"""Trigger processing of a new user export file"""
|
||||
if self.new_export_blocked_until() is not None:
|
||||
return HttpResponse(status=429) # Too Many Requests
|
||||
|
||||
job = BookwyrmExportJob.objects.create(user=request.user)
|
||||
job.start_job()
|
||||
|
||||
return redirect("prefs-user-export")
|
||||
|
||||
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
class ExportArchive(View):
|
||||
"""Serve the archive file"""
|
||||
|
||||
def get(self, request, archive_id):
|
||||
"""download user export file"""
|
||||
export = BookwyrmExportJob.objects.get(task_id=archive_id, user=request.user)
|
||||
|
||||
if settings.USE_S3:
|
||||
# make custom_domain None so we can sign the url
|
||||
# see https://github.com/jschneier/django-storages/issues/944
|
||||
storage = S3Storage(querystring_auth=True, custom_domain=None)
|
||||
try:
|
||||
url = S3Storage.url(
|
||||
storage,
|
||||
f"/exports/{export.task_id}.tar.gz",
|
||||
expire=settings.S3_SIGNED_URL_EXPIRY,
|
||||
)
|
||||
except Exception:
|
||||
raise Http404()
|
||||
return redirect(url)
|
||||
|
||||
if settings.USE_AZURE:
|
||||
# not implemented
|
||||
return HttpResponseServerError()
|
||||
|
||||
try:
|
||||
return HttpResponse(
|
||||
export.export_data,
|
||||
content_type="application/gzip",
|
||||
headers={
|
||||
# pylint: disable=line-too-long
|
||||
"Content-Disposition": 'attachment; filename="bookwyrm-account-export.tar.gz"'
|
||||
},
|
||||
)
|
||||
except FileNotFoundError:
|
||||
raise Http404()
|
||||
|
|
|
@ -32,7 +32,7 @@ class MoveUser(View):
|
|||
|
||||
if form.is_valid() and user.check_password(form.cleaned_data["password"]):
|
||||
username = form.cleaned_data["target"]
|
||||
target = handle_remote_webfinger(username)
|
||||
target = handle_remote_webfinger(username, refresh=True)
|
||||
|
||||
try:
|
||||
models.MoveUser.objects.create(
|
||||
|
@ -81,6 +81,7 @@ class AliasUser(View):
|
|||
return TemplateResponse(request, "preferences/alias_user.html", data)
|
||||
|
||||
user.also_known_as.add(remote_user.id)
|
||||
user.save(broadcast=True) # broadcast the alias
|
||||
|
||||
return redirect("prefs-alias")
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
""" the good stuff! the books! """
|
||||
|
||||
import logging
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.cache import cache
|
||||
|
@ -11,6 +12,7 @@ from django.views import View
|
|||
from django.views.decorators.http import require_POST
|
||||
|
||||
from bookwyrm import forms, models
|
||||
from bookwyrm.views.helpers import get_mergeable_object_or_404
|
||||
from bookwyrm.views.shelf.shelf_actions import unshelve
|
||||
from .status import CreateStatus
|
||||
from .helpers import get_edition, handle_reading_status, is_api_request
|
||||
|
@ -130,7 +132,7 @@ class ReadThrough(View):
|
|||
|
||||
def get(self, request, book_id, readthrough_id=None):
|
||||
"""standalone form in case of errors"""
|
||||
book = get_object_or_404(models.Edition, id=book_id)
|
||||
book = get_mergeable_object_or_404(models.Edition, id=book_id)
|
||||
form = forms.ReadThroughForm()
|
||||
data = {"form": form, "book": book}
|
||||
if readthrough_id:
|
||||
|
@ -152,7 +154,7 @@ class ReadThrough(View):
|
|||
)
|
||||
form = forms.ReadThroughForm(request.POST)
|
||||
if not form.is_valid():
|
||||
book = get_object_or_404(models.Edition, id=book_id)
|
||||
book = get_mergeable_object_or_404(models.Edition, id=book_id)
|
||||
data = {"form": form, "book": book}
|
||||
if request.POST.get("id"):
|
||||
data["readthrough"] = get_object_or_404(
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
""" search views"""
|
||||
|
||||
import re
|
||||
|
||||
from django.contrib.postgres.search import TrigramSimilarity
|
||||
from django.contrib.postgres.search import TrigramSimilarity, SearchRank, SearchQuery
|
||||
from django.core.paginator import Paginator
|
||||
from django.db.models import F
|
||||
from django.db.models.functions import Greatest
|
||||
from django.http import JsonResponse
|
||||
from django.template.response import TemplateResponse
|
||||
|
@ -39,6 +41,7 @@ class Search(View):
|
|||
|
||||
endpoints = {
|
||||
"book": book_search,
|
||||
"author": author_search,
|
||||
"user": user_search,
|
||||
"list": list_search,
|
||||
}
|
||||
|
@ -51,7 +54,7 @@ class Search(View):
|
|||
def api_book_search(request):
|
||||
"""Return books via API response"""
|
||||
query = request.GET.get("q")
|
||||
query = isbn_check(query)
|
||||
query = isbn_check_and_format(query)
|
||||
min_confidence = request.GET.get("min_confidence", 0)
|
||||
# only return local book results via json so we don't cascade
|
||||
book_results = search(query, min_confidence=min_confidence)
|
||||
|
@ -64,7 +67,7 @@ def book_search(request):
|
|||
"""the real business is elsewhere"""
|
||||
query = request.GET.get("q")
|
||||
# check if query is isbn
|
||||
query = isbn_check(query)
|
||||
query = isbn_check_and_format(query)
|
||||
min_confidence = request.GET.get("min_confidence", 0)
|
||||
search_remote = request.GET.get("remote", False) and request.user.is_authenticated
|
||||
|
||||
|
@ -90,6 +93,33 @@ def book_search(request):
|
|||
return TemplateResponse(request, "search/book.html", data)
|
||||
|
||||
|
||||
def author_search(request):
|
||||
"""search for an author"""
|
||||
query = request.GET.get("q").strip()
|
||||
search_query = SearchQuery(query, config="simple")
|
||||
min_confidence = 0
|
||||
|
||||
results = (
|
||||
models.Author.objects.filter(search_vector=search_query)
|
||||
.annotate(rank=SearchRank(F("search_vector"), search_query))
|
||||
.filter(rank__gt=min_confidence)
|
||||
.order_by("-rank")
|
||||
)
|
||||
|
||||
paginated = Paginator(results, PAGE_LENGTH)
|
||||
page = paginated.get_page(request.GET.get("page"))
|
||||
|
||||
data = {
|
||||
"type": "author",
|
||||
"query": query,
|
||||
"results": page,
|
||||
"page_range": paginated.get_elided_page_range(
|
||||
page.number, on_each_side=2, on_ends=1
|
||||
),
|
||||
}
|
||||
return TemplateResponse(request, "search/author.html", data)
|
||||
|
||||
|
||||
def user_search(request):
|
||||
"""user search: search for a user"""
|
||||
viewer = request.user
|
||||
|
@ -159,7 +189,7 @@ def list_search(request):
|
|||
return TemplateResponse(request, "search/list.html", data)
|
||||
|
||||
|
||||
def isbn_check(query):
|
||||
def isbn_check_and_format(query):
|
||||
"""isbn10 or isbn13 check, if so remove separators"""
|
||||
if query:
|
||||
su_num = re.sub(r"(?<=\d)\D(?=\d|[xX])", "", query)
|
||||
|
|
|
@ -15,12 +15,14 @@ from bookwyrm import forms, models
|
|||
from bookwyrm.activitypub import ActivitypubResponse
|
||||
from bookwyrm.settings import PAGE_LENGTH
|
||||
from bookwyrm.views.helpers import is_api_request, get_user_from_username
|
||||
from bookwyrm.book_search import search
|
||||
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
class Shelf(View):
|
||||
"""shelf page"""
|
||||
|
||||
# pylint: disable=R0914
|
||||
def get(self, request, username, shelf_identifier=None):
|
||||
"""display a shelf"""
|
||||
user = get_user_from_username(request.user, username)
|
||||
|
@ -32,6 +34,8 @@ class Shelf(View):
|
|||
else:
|
||||
shelves = models.Shelf.privacy_filter(request.user).filter(user=user).all()
|
||||
|
||||
shelves_filter_query = request.GET.get("filter")
|
||||
|
||||
# get the shelf and make sure the logged in user should be able to see it
|
||||
if shelf_identifier:
|
||||
shelf = get_object_or_404(user.shelf_set, identifier=shelf_identifier)
|
||||
|
@ -42,6 +46,7 @@ class Shelf(View):
|
|||
FakeShelf = namedtuple(
|
||||
"Shelf", ("identifier", "name", "user", "books", "privacy")
|
||||
)
|
||||
|
||||
books = (
|
||||
models.Edition.viewer_aware_objects(request.user)
|
||||
.filter(
|
||||
|
@ -50,6 +55,7 @@ class Shelf(View):
|
|||
)
|
||||
.distinct()
|
||||
)
|
||||
|
||||
shelf = FakeShelf("all", _("All books"), user, books, "public")
|
||||
|
||||
if is_api_request(request) and shelf_identifier:
|
||||
|
@ -86,6 +92,9 @@ class Shelf(View):
|
|||
|
||||
books = sort_books(books, request.GET.get("sort"))
|
||||
|
||||
if shelves_filter_query:
|
||||
books = search(shelves_filter_query, books=books)
|
||||
|
||||
paginated = Paginator(
|
||||
books,
|
||||
PAGE_LENGTH,
|
||||
|
@ -103,6 +112,8 @@ class Shelf(View):
|
|||
"page_range": paginated.get_elided_page_range(
|
||||
page.number, on_each_side=2, on_ends=1
|
||||
),
|
||||
"shelves_filter_query": shelves_filter_query,
|
||||
"size": "small",
|
||||
}
|
||||
|
||||
return TemplateResponse(request, "shelf/shelf.html", data)
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
""" shelf views """
|
||||
|
||||
from django.db import IntegrityError, transaction
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.views.decorators.http import require_POST
|
||||
|
||||
from bookwyrm import forms, models
|
||||
from bookwyrm.views.helpers import redirect_to_referer
|
||||
from bookwyrm.views.helpers import redirect_to_referer, get_mergeable_object_or_404
|
||||
|
||||
|
||||
@login_required
|
||||
|
@ -36,7 +37,7 @@ def delete_shelf(request, shelf_id):
|
|||
@transaction.atomic
|
||||
def shelve(request):
|
||||
"""put a book on a user's shelf"""
|
||||
book = get_object_or_404(models.Edition, id=request.POST.get("book"))
|
||||
book = get_mergeable_object_or_404(models.Edition, id=request.POST.get("book"))
|
||||
desired_shelf = get_object_or_404(
|
||||
request.user.shelf_set, identifier=request.POST.get("shelf")
|
||||
)
|
||||
|
@ -97,7 +98,7 @@ def shelve(request):
|
|||
def unshelve(request, book_id=False):
|
||||
"""remove a book from a user's shelf"""
|
||||
identity = book_id if book_id else request.POST.get("book")
|
||||
book = get_object_or_404(models.Edition, id=identity)
|
||||
book = get_mergeable_object_or_404(models.Edition, id=identity)
|
||||
shelf_book = get_object_or_404(
|
||||
models.ShelfBook, book=book, shelf__id=request.POST["shelf"]
|
||||
)
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
""" what are we here for if not for posting """
|
||||
|
||||
import re
|
||||
import logging
|
||||
|
||||
|
@ -19,6 +20,7 @@ from markdown import markdown
|
|||
from bookwyrm import forms, models
|
||||
from bookwyrm.models.report import DELETE_ITEM
|
||||
from bookwyrm.utils import regex, sanitizer
|
||||
from bookwyrm.views.helpers import get_mergeable_object_or_404
|
||||
from .helpers import handle_remote_webfinger, is_api_request
|
||||
from .helpers import load_date_in_user_tz_as_utc, redirect_to_referer
|
||||
|
||||
|
@ -52,7 +54,7 @@ class CreateStatus(View):
|
|||
|
||||
def get(self, request, status_type): # pylint: disable=unused-argument
|
||||
"""compose view (...not used?)"""
|
||||
book = get_object_or_404(models.Edition, id=request.GET.get("book"))
|
||||
book = get_mergeable_object_or_404(models.Edition, id=request.GET.get("book"))
|
||||
data = {"book": book}
|
||||
return TemplateResponse(request, "compose.html", data)
|
||||
|
||||
|
@ -98,7 +100,7 @@ class CreateStatus(View):
|
|||
# inspect the text for user tags
|
||||
content = status.content
|
||||
mentions = find_mentions(request.user, content)
|
||||
for (_, mention_user) in mentions.items():
|
||||
for _, mention_user in mentions.items():
|
||||
# add them to status mentions fk
|
||||
status.mention_users.add(mention_user)
|
||||
content = format_mentions(content, mentions)
|
||||
|
@ -109,7 +111,7 @@ class CreateStatus(View):
|
|||
|
||||
# inspect the text for hashtags
|
||||
hashtags = find_or_create_hashtags(content)
|
||||
for (_, mention_hashtag) in hashtags.items():
|
||||
for _, mention_hashtag in hashtags.items():
|
||||
# add them to status mentions fk
|
||||
status.mention_hashtags.add(mention_hashtag)
|
||||
content = format_hashtags(content, hashtags)
|
||||
|
@ -140,7 +142,7 @@ class CreateStatus(View):
|
|||
|
||||
def format_mentions(content, mentions):
|
||||
"""Detect @mentions and make them links"""
|
||||
for (mention_text, mention_user) in mentions.items():
|
||||
for mention_text, mention_user in mentions.items():
|
||||
# turn the mention into a link
|
||||
content = re.sub(
|
||||
rf"(?<!/)\B{mention_text}\b(?!@)",
|
||||
|
@ -152,7 +154,7 @@ def format_mentions(content, mentions):
|
|||
|
||||
def format_hashtags(content, hashtags):
|
||||
"""Detect #hashtags and make them links"""
|
||||
for (mention_text, mention_hashtag) in hashtags.items():
|
||||
for mention_text, mention_hashtag in hashtags.items():
|
||||
# turn the mention into a link
|
||||
content = re.sub(
|
||||
rf"(?<!/)\B{mention_text}\b(?!@)",
|
||||
|
|
|
@ -9,7 +9,7 @@ from django.utils import timezone
|
|||
from django.views.decorators.http import require_GET
|
||||
|
||||
from bookwyrm import models
|
||||
from bookwyrm.settings import DOMAIN, VERSION, LANGUAGE_CODE
|
||||
from bookwyrm.settings import BASE_URL, DOMAIN, VERSION, LANGUAGE_CODE
|
||||
|
||||
|
||||
@require_GET
|
||||
|
@ -34,7 +34,7 @@ def webfinger(request):
|
|||
},
|
||||
{
|
||||
"rel": "http://ostatus.org/schema/1.0/subscribe",
|
||||
"template": f"https://{DOMAIN}/ostatus_subscribe?acct={{uri}}",
|
||||
"template": f"{BASE_URL}/ostatus_subscribe?acct={{uri}}",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue