Merge branch 'main' into groups-merge-test
Big merge of a couple of weeks' work from the main project back into this branch. :ohno:
This commit is contained in:
commit
602664b8d7
231 changed files with 8983 additions and 7361 deletions
|
@ -1,4 +1,5 @@
|
|||
""" make sure all our nice views are available """
|
||||
# site admin
|
||||
from .admin.announcements import Announcements, Announcement, delete_announcement
|
||||
from .admin.dashboard import Dashboard
|
||||
from .admin.federation import Federation, FederatedServer
|
||||
|
@ -19,14 +20,22 @@ from .admin.reports import (
|
|||
)
|
||||
from .admin.site import Site
|
||||
from .admin.user_admin import UserAdmin, UserAdminList
|
||||
|
||||
# user preferences
|
||||
from .preferences.change_password import ChangePassword
|
||||
from .preferences.edit_user import EditUser
|
||||
from .preferences.delete_user import DeleteUser
|
||||
from .preferences.block import Block, unblock
|
||||
|
||||
# books
|
||||
from .books.books import Book, upload_cover, add_description, resolve_book
|
||||
from .books.edit_book import EditBook, ConfirmEditBook
|
||||
from .books.editions import Editions, switch_edition
|
||||
|
||||
# misc views
|
||||
from .author import Author, EditAuthor
|
||||
from .block import Block, unblock
|
||||
from .books import Book, EditBook, ConfirmEditBook
|
||||
from .books import upload_cover, add_description, resolve_book
|
||||
from .directory import Directory
|
||||
from .discover import Discover
|
||||
from .edit_user import EditUser, DeleteUser
|
||||
from .editions import Editions, switch_edition
|
||||
from .feed import DirectMessage, Feed, Replies, Status
|
||||
from .follow import follow, unfollow
|
||||
from .follow import accept_follow_request, delete_follow_request
|
||||
|
@ -43,17 +52,17 @@ from .list import save_list, unsave_list, delete_list
|
|||
from .login import Login, Logout
|
||||
from .notifications import Notifications
|
||||
from .outbox import Outbox
|
||||
from .reading import edit_readthrough, create_readthrough
|
||||
from .reading import delete_readthrough, delete_progressupdate
|
||||
from .reading import create_readthrough, delete_readthrough, delete_progressupdate
|
||||
from .reading import ReadingStatus
|
||||
from .register import Register, ConfirmEmail, ConfirmEmailCode, resend_link
|
||||
from .rss_feed import RssFeed
|
||||
from .password import PasswordResetRequest, PasswordReset, ChangePassword
|
||||
from .password import PasswordResetRequest, PasswordReset
|
||||
from .search import Search
|
||||
from .shelf import Shelf
|
||||
from .shelf import create_shelf, delete_shelf
|
||||
from .shelf import shelve, unshelve
|
||||
from .status import CreateStatus, DeleteStatus, DeleteAndRedraft
|
||||
from .status import CreateStatus, DeleteStatus, DeleteAndRedraft, update_progress
|
||||
from .status import edit_readthrough
|
||||
from .updates import get_notification_count, get_unread_status_count
|
||||
from .user import User, Followers, Following, hide_suggestions
|
||||
from .wellknown import *
|
||||
|
|
0
bookwyrm/views/admin/__init__.py
Normal file
0
bookwyrm/views/admin/__init__.py
Normal file
|
@ -31,6 +31,7 @@ class Announcements(View):
|
|||
"end_date",
|
||||
"active",
|
||||
]
|
||||
# pylint: disable=consider-using-f-string
|
||||
if sort in sort_fields + ["-{:s}".format(f) for f in sort_fields]:
|
||||
announcements = announcements.order_by(sort)
|
||||
data = {
|
||||
|
@ -40,7 +41,9 @@ class Announcements(View):
|
|||
"form": forms.AnnouncementForm(),
|
||||
"sort": sort,
|
||||
}
|
||||
return TemplateResponse(request, "settings/announcements.html", data)
|
||||
return TemplateResponse(
|
||||
request, "settings/announcements/announcements.html", data
|
||||
)
|
||||
|
||||
def post(self, request):
|
||||
"""edit the site settings"""
|
||||
|
@ -55,7 +58,9 @@ class Announcements(View):
|
|||
).get_page(request.GET.get("page")),
|
||||
"form": form,
|
||||
}
|
||||
return TemplateResponse(request, "settings/announcements.html", data)
|
||||
return TemplateResponse(
|
||||
request, "settings/announcements/announcements.html", data
|
||||
)
|
||||
|
||||
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
|
@ -73,7 +78,9 @@ class Announcement(View):
|
|||
"announcement": announcement,
|
||||
"form": forms.AnnouncementForm(instance=announcement),
|
||||
}
|
||||
return TemplateResponse(request, "settings/announcement.html", data)
|
||||
return TemplateResponse(
|
||||
request, "settings/announcements/announcement.html", data
|
||||
)
|
||||
|
||||
def post(self, request, announcement_id):
|
||||
"""edit announcement"""
|
||||
|
@ -86,7 +93,9 @@ class Announcement(View):
|
|||
"announcement": announcement,
|
||||
"form": form,
|
||||
}
|
||||
return TemplateResponse(request, "settings/announcement.html", data)
|
||||
return TemplateResponse(
|
||||
request, "settings/announcements/announcement.html", data
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
|
|
|
@ -85,4 +85,4 @@ class Dashboard(View):
|
|||
"user_stats": user_stats,
|
||||
"status_stats": status_stats,
|
||||
}
|
||||
return TemplateResponse(request, "settings/dashboard.html", data)
|
||||
return TemplateResponse(request, "settings/dashboard/dashboard.html", data)
|
||||
|
|
|
@ -22,7 +22,9 @@ class EmailBlocklist(View):
|
|||
"domains": models.EmailBlocklist.objects.order_by("-created_date").all(),
|
||||
"form": forms.EmailBlocklistForm(),
|
||||
}
|
||||
return TemplateResponse(request, "settings/email_blocklist.html", data)
|
||||
return TemplateResponse(
|
||||
request, "settings/email_blocklist/email_blocklist.html", data
|
||||
)
|
||||
|
||||
def post(self, request, domain_id=None):
|
||||
"""create a new domain block"""
|
||||
|
@ -35,11 +37,15 @@ class EmailBlocklist(View):
|
|||
"form": form,
|
||||
}
|
||||
if not form.is_valid():
|
||||
return TemplateResponse(request, "settings/email_blocklist.html", data)
|
||||
return TemplateResponse(
|
||||
request, "settings/email_blocklist/email_blocklist.html", data
|
||||
)
|
||||
form.save()
|
||||
|
||||
data["form"] = forms.EmailBlocklistForm()
|
||||
return TemplateResponse(request, "settings/email_blocklist.html", data)
|
||||
return TemplateResponse(
|
||||
request, "settings/email_blocklist/email_blocklist.html", data
|
||||
)
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def delete(self, request, domain_id):
|
||||
|
|
|
@ -28,6 +28,7 @@ class Federation(View):
|
|||
|
||||
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 = "-created_date"
|
||||
servers = servers.order_by(sort)
|
||||
|
@ -43,7 +44,7 @@ class Federation(View):
|
|||
"sort": sort,
|
||||
"form": forms.ServerForm(),
|
||||
}
|
||||
return TemplateResponse(request, "settings/federation.html", data)
|
||||
return TemplateResponse(request, "settings/federation/instance_list.html", data)
|
||||
|
||||
|
||||
class AddFederatedServer(View):
|
||||
|
@ -52,14 +53,16 @@ class AddFederatedServer(View):
|
|||
def get(self, request):
|
||||
"""add server form"""
|
||||
data = {"form": forms.ServerForm()}
|
||||
return TemplateResponse(request, "settings/edit_server.html", data)
|
||||
return TemplateResponse(request, "settings/federation/edit_instance.html", data)
|
||||
|
||||
def post(self, request):
|
||||
"""add a server from the admin panel"""
|
||||
form = forms.ServerForm(request.POST)
|
||||
if not form.is_valid():
|
||||
data = {"form": form}
|
||||
return TemplateResponse(request, "settings/edit_server.html", data)
|
||||
return TemplateResponse(
|
||||
request, "settings/federation/edit_instance.html", data
|
||||
)
|
||||
server = form.save()
|
||||
return redirect("settings-federated-server", server.id)
|
||||
|
||||
|
@ -74,7 +77,7 @@ class ImportServerBlocklist(View):
|
|||
|
||||
def get(self, request):
|
||||
"""add server form"""
|
||||
return TemplateResponse(request, "settings/server_blocklist.html")
|
||||
return TemplateResponse(request, "settings/federation/instance_blocklist.html")
|
||||
|
||||
def post(self, request):
|
||||
"""add a server from the admin panel"""
|
||||
|
@ -97,7 +100,9 @@ class ImportServerBlocklist(View):
|
|||
server.block()
|
||||
success_count += 1
|
||||
data = {"failed": failed, "succeeded": success_count}
|
||||
return TemplateResponse(request, "settings/server_blocklist.html", data)
|
||||
return TemplateResponse(
|
||||
request, "settings/federation/instance_blocklist.html", data
|
||||
)
|
||||
|
||||
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
|
@ -122,7 +127,7 @@ class FederatedServer(View):
|
|||
user_subject__in=users.all()
|
||||
),
|
||||
}
|
||||
return TemplateResponse(request, "settings/federated_server.html", data)
|
||||
return TemplateResponse(request, "settings/federation/instance.html", data)
|
||||
|
||||
def post(self, request, server): # pylint: disable=unused-argument
|
||||
"""update note"""
|
||||
|
|
|
@ -45,13 +45,13 @@ class ManageInvites(View):
|
|||
),
|
||||
"form": forms.CreateInviteForm(),
|
||||
}
|
||||
return TemplateResponse(request, "settings/manage_invites.html", data)
|
||||
return TemplateResponse(request, "settings/invites/manage_invites.html", data)
|
||||
|
||||
def post(self, request):
|
||||
"""creates an invite database entry"""
|
||||
form = forms.CreateInviteForm(request.POST)
|
||||
if not form.is_valid():
|
||||
return HttpResponseBadRequest("ERRORS : %s" % (form.errors,))
|
||||
return HttpResponseBadRequest(f"ERRORS: {form.errors}")
|
||||
|
||||
invite = form.save(commit=False)
|
||||
invite.user = request.user
|
||||
|
@ -64,7 +64,7 @@ class ManageInvites(View):
|
|||
PAGE_LENGTH,
|
||||
)
|
||||
data = {"invites": paginated.page(1), "form": form}
|
||||
return TemplateResponse(request, "settings/manage_invites.html", data)
|
||||
return TemplateResponse(request, "settings/invites/manage_invites.html", data)
|
||||
|
||||
|
||||
class Invite(View):
|
||||
|
@ -98,6 +98,7 @@ class ManageInviteRequests(View):
|
|||
"invite__times_used",
|
||||
"invite__invitees__created_date",
|
||||
]
|
||||
# pylint: disable=consider-using-f-string
|
||||
if not sort in sort_fields + ["-{:s}".format(f) for f in sort_fields]:
|
||||
sort = "-created_date"
|
||||
|
||||
|
@ -134,7 +135,9 @@ class ManageInviteRequests(View):
|
|||
),
|
||||
"sort": sort,
|
||||
}
|
||||
return TemplateResponse(request, "settings/manage_invite_requests.html", data)
|
||||
return TemplateResponse(
|
||||
request, "settings/invites/manage_invite_requests.html", data
|
||||
)
|
||||
|
||||
def post(self, request):
|
||||
"""send out an invite"""
|
||||
|
@ -149,6 +152,7 @@ class ManageInviteRequests(View):
|
|||
)
|
||||
invite_request.save()
|
||||
emailing.invite_email(invite_request)
|
||||
# pylint: disable=consider-using-f-string
|
||||
return redirect(
|
||||
"{:s}?{:s}".format(
|
||||
reverse("settings-invite-requests"), urlencode(request.GET.dict())
|
||||
|
|
|
@ -22,7 +22,9 @@ class IPBlocklist(View):
|
|||
"addresses": models.IPBlocklist.objects.all(),
|
||||
"form": forms.IPBlocklistForm(),
|
||||
}
|
||||
return TemplateResponse(request, "settings/ip_blocklist.html", data)
|
||||
return TemplateResponse(
|
||||
request, "settings/ip_blocklist/ip_blocklist.html", data
|
||||
)
|
||||
|
||||
def post(self, request, block_id=None):
|
||||
"""create a new ip address block"""
|
||||
|
@ -35,11 +37,15 @@ class IPBlocklist(View):
|
|||
"form": form,
|
||||
}
|
||||
if not form.is_valid():
|
||||
return TemplateResponse(request, "settings/ip_blocklist.html", data)
|
||||
return TemplateResponse(
|
||||
request, "settings/ip_blocklist/ip_blocklist.html", data
|
||||
)
|
||||
form.save()
|
||||
|
||||
data["form"] = forms.IPBlocklistForm()
|
||||
return TemplateResponse(request, "settings/ip_blocklist.html", data)
|
||||
return TemplateResponse(
|
||||
request, "settings/ip_blocklist/ip_blocklist.html", data
|
||||
)
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def delete(self, request, domain_id):
|
||||
|
|
|
@ -40,7 +40,7 @@ class Reports(View):
|
|||
"server": server,
|
||||
"reports": models.Report.objects.filter(**filters),
|
||||
}
|
||||
return TemplateResponse(request, "moderation/reports.html", data)
|
||||
return TemplateResponse(request, "settings/reports/reports.html", data)
|
||||
|
||||
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
|
@ -60,7 +60,7 @@ class Report(View):
|
|||
data = {
|
||||
"report": get_object_or_404(models.Report, id=report_id),
|
||||
}
|
||||
return TemplateResponse(request, "moderation/report.html", data)
|
||||
return TemplateResponse(request, "settings/reports/report.html", data)
|
||||
|
||||
def post(self, request, report_id):
|
||||
"""comment on a report"""
|
||||
|
@ -105,7 +105,7 @@ def moderator_delete_user(request, user_id):
|
|||
|
||||
# we can't delete users on other instances
|
||||
if not user.local:
|
||||
raise PermissionDenied
|
||||
raise PermissionDenied()
|
||||
|
||||
form = forms.DeleteUserForm(request.POST, instance=user)
|
||||
|
||||
|
|
|
@ -41,9 +41,9 @@ def email_preview(request):
|
|||
"""for development, renders and example email template"""
|
||||
template = request.GET.get("email")
|
||||
data = emailing.email_data()
|
||||
data["subject_path"] = "email/{}/subject.html".format(template)
|
||||
data["html_content_path"] = "email/{}/html_content.html".format(template)
|
||||
data["text_content_path"] = "email/{}/text_content.html".format(template)
|
||||
data["subject_path"] = f"email/{template}/subject.html"
|
||||
data["html_content_path"] = f"email/{template}/html_content.html"
|
||||
data["text_content_path"] = f"email/{template}/text_content.html"
|
||||
data["reset_link"] = "https://example.com/link"
|
||||
data["invite_link"] = "https://example.com/link"
|
||||
data["confirmation_link"] = "https://example.com/link"
|
||||
|
|
|
@ -47,6 +47,7 @@ class UserAdminList(View):
|
|||
"federated_server__server_name",
|
||||
"is_active",
|
||||
]
|
||||
# pylint: disable=consider-using-f-string
|
||||
if sort in sort_fields + ["-{:s}".format(f) for f in sort_fields]:
|
||||
users = users.order_by(sort)
|
||||
|
||||
|
@ -56,7 +57,7 @@ class UserAdminList(View):
|
|||
"sort": sort,
|
||||
"server": server,
|
||||
}
|
||||
return TemplateResponse(request, "user_admin/user_admin.html", data)
|
||||
return TemplateResponse(request, "settings/users/user_admin.html", data)
|
||||
|
||||
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
|
@ -71,7 +72,7 @@ class UserAdmin(View):
|
|||
"""user view"""
|
||||
user = get_object_or_404(models.User, id=user)
|
||||
data = {"user": user, "group_form": forms.UserGroupForm()}
|
||||
return TemplateResponse(request, "user_admin/user.html", data)
|
||||
return TemplateResponse(request, "settings/users/user.html", data)
|
||||
|
||||
def post(self, request, user):
|
||||
"""update user group"""
|
||||
|
@ -80,4 +81,4 @@ class UserAdmin(View):
|
|||
if form.is_valid():
|
||||
form.save()
|
||||
data = {"user": user, "group_form": form}
|
||||
return TemplateResponse(request, "user_admin/user.html", data)
|
||||
return TemplateResponse(request, "settings/users/user.html", data)
|
||||
|
|
0
bookwyrm/views/books/__init__.py
Normal file
0
bookwyrm/views/books/__init__.py
Normal file
182
bookwyrm/views/books/books.py
Normal file
182
bookwyrm/views/books/books.py
Normal file
|
@ -0,0 +1,182 @@
|
|||
""" the good stuff! the books! """
|
||||
from uuid import uuid4
|
||||
|
||||
from django.contrib.auth.decorators import login_required, permission_required
|
||||
from django.core.files.base import ContentFile
|
||||
from django.core.paginator import Paginator
|
||||
from django.db.models import Avg, Q
|
||||
from django.http import Http404
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.template.response import TemplateResponse
|
||||
from django.views import View
|
||||
from django.views.decorators.http import require_POST
|
||||
|
||||
from bookwyrm import forms, models
|
||||
from bookwyrm.activitypub import ActivitypubResponse
|
||||
from bookwyrm.connectors import connector_manager
|
||||
from bookwyrm.connectors.abstract_connector import get_image
|
||||
from bookwyrm.settings import PAGE_LENGTH
|
||||
from bookwyrm.views.helpers import is_api_request, privacy_filter
|
||||
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
class Book(View):
|
||||
"""a book! this is the stuff"""
|
||||
|
||||
def get(self, request, book_id, user_statuses=False):
|
||||
"""info about a book"""
|
||||
if is_api_request(request):
|
||||
book = get_object_or_404(
|
||||
models.Book.objects.select_subclasses(), id=book_id
|
||||
)
|
||||
return ActivitypubResponse(book.to_activity())
|
||||
|
||||
user_statuses = user_statuses if request.user.is_authenticated else False
|
||||
|
||||
# it's safe to use this OR because edition and work and subclasses of the same
|
||||
# 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))
|
||||
.order_by("-edition_rank")
|
||||
.select_related("parent_work")
|
||||
.prefetch_related("authors")
|
||||
.first()
|
||||
)
|
||||
|
||||
if not book or not book.parent_work:
|
||||
raise Http404()
|
||||
|
||||
# all reviews for all editions of the book
|
||||
reviews = privacy_filter(
|
||||
request.user, models.Review.objects.filter(book__parent_work__editions=book)
|
||||
)
|
||||
|
||||
# the reviews to show
|
||||
if user_statuses:
|
||||
if user_statuses == "review":
|
||||
queryset = book.review_set.select_subclasses()
|
||||
elif user_statuses == "comment":
|
||||
queryset = book.comment_set
|
||||
else:
|
||||
queryset = book.quotation_set
|
||||
queryset = queryset.filter(user=request.user, deleted=False)
|
||||
else:
|
||||
queryset = reviews.exclude(Q(content__isnull=True) | Q(content=""))
|
||||
queryset = queryset.select_related("user").order_by("-published_date")
|
||||
paginated = Paginator(queryset, PAGE_LENGTH)
|
||||
|
||||
lists = privacy_filter(
|
||||
request.user,
|
||||
models.List.objects.filter(
|
||||
listitem__approved=True,
|
||||
listitem__book__in=book.parent_work.editions.all(),
|
||||
),
|
||||
)
|
||||
data = {
|
||||
"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,
|
||||
"rating": reviews.aggregate(Avg("rating"))["rating__avg"],
|
||||
"lists": lists,
|
||||
}
|
||||
|
||||
if request.user.is_authenticated:
|
||||
readthroughs = models.ReadThrough.objects.filter(
|
||||
user=request.user,
|
||||
book=book,
|
||||
).order_by("start_date")
|
||||
|
||||
for readthrough in readthroughs:
|
||||
readthrough.progress_updates = (
|
||||
readthrough.progressupdate_set.all().order_by("-updated_date")
|
||||
)
|
||||
data["readthroughs"] = readthroughs
|
||||
|
||||
data["user_shelfbooks"] = models.ShelfBook.objects.filter(
|
||||
user=request.user, book=book
|
||||
).select_related("shelf")
|
||||
|
||||
data["other_edition_shelves"] = models.ShelfBook.objects.filter(
|
||||
~Q(book=book),
|
||||
user=request.user,
|
||||
book__parent_work=book.parent_work,
|
||||
).select_related("shelf", "book")
|
||||
|
||||
filters = {"user": request.user, "deleted": False}
|
||||
data["user_statuses"] = {
|
||||
"review_count": book.review_set.filter(**filters).count(),
|
||||
"comment_count": book.comment_set.filter(**filters).count(),
|
||||
"quotation_count": book.quotation_set.filter(**filters).count(),
|
||||
}
|
||||
|
||||
return TemplateResponse(request, "book/book.html", data)
|
||||
|
||||
|
||||
@login_required
|
||||
@require_POST
|
||||
def upload_cover(request, book_id):
|
||||
"""upload a new cover"""
|
||||
book = get_object_or_404(models.Edition, id=book_id)
|
||||
book.last_edited_by = request.user
|
||||
|
||||
url = request.POST.get("cover-url")
|
||||
if url:
|
||||
image = set_cover_from_url(url)
|
||||
if image:
|
||||
book.cover.save(*image)
|
||||
|
||||
return redirect(f"{book.local_path}?cover_error=True")
|
||||
|
||||
form = forms.CoverForm(request.POST, request.FILES, instance=book)
|
||||
if not form.is_valid() or not form.files.get("cover"):
|
||||
return redirect(book.local_path)
|
||||
|
||||
book.cover = form.files["cover"]
|
||||
book.save()
|
||||
|
||||
return redirect(book.local_path)
|
||||
|
||||
|
||||
def set_cover_from_url(url):
|
||||
"""load it from a url"""
|
||||
try:
|
||||
image_file = get_image(url)
|
||||
except: # pylint: disable=bare-except
|
||||
return None
|
||||
if not image_file:
|
||||
return None
|
||||
image_name = str(uuid4()) + "." + url.split(".")[-1]
|
||||
image_content = ContentFile(image_file.content)
|
||||
return [image_name, image_content]
|
||||
|
||||
|
||||
@login_required
|
||||
@require_POST
|
||||
@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)
|
||||
|
||||
description = request.POST.get("description")
|
||||
|
||||
book.description = description
|
||||
book.last_edited_by = request.user
|
||||
book.save(update_fields=["description", "last_edited_by"])
|
||||
|
||||
return redirect("book", book.id)
|
||||
|
||||
|
||||
@require_POST
|
||||
def resolve_book(request):
|
||||
"""figure out the local path to a book from a remote_id"""
|
||||
remote_id = request.POST.get("remote_id")
|
||||
connector = connector_manager.get_or_create_connector(remote_id)
|
||||
book = connector.get_or_create_book(remote_id)
|
||||
|
||||
return redirect("book", book.id)
|
|
@ -1,122 +1,22 @@
|
|||
""" the good stuff! the books! """
|
||||
from uuid import uuid4
|
||||
|
||||
from dateutil.parser import parse as dateparse
|
||||
from django.contrib.auth.decorators import login_required, permission_required
|
||||
from django.contrib.postgres.search import SearchRank, SearchVector
|
||||
from django.core.files.base import ContentFile
|
||||
from django.core.paginator import Paginator
|
||||
from django.db import transaction
|
||||
from django.db.models import Avg, Q
|
||||
from django.http import HttpResponseBadRequest, HttpResponseNotFound
|
||||
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 import View
|
||||
from django.views.decorators.http import require_POST
|
||||
|
||||
from bookwyrm import forms, models
|
||||
from bookwyrm.activitypub import ActivitypubResponse
|
||||
from bookwyrm.connectors import connector_manager
|
||||
from bookwyrm.connectors.abstract_connector import get_image
|
||||
from bookwyrm.settings import PAGE_LENGTH
|
||||
from .helpers import is_api_request, get_edition, privacy_filter
|
||||
from bookwyrm.views.helpers import get_edition
|
||||
from .books import set_cover_from_url
|
||||
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
class Book(View):
|
||||
"""a book! this is the stuff"""
|
||||
|
||||
def get(self, request, book_id, user_statuses=False):
|
||||
"""info about a book"""
|
||||
user_statuses = user_statuses if request.user.is_authenticated else False
|
||||
try:
|
||||
book = models.Book.objects.select_subclasses().get(id=book_id)
|
||||
except models.Book.DoesNotExist:
|
||||
return HttpResponseNotFound()
|
||||
|
||||
if is_api_request(request):
|
||||
return ActivitypubResponse(book.to_activity())
|
||||
|
||||
if isinstance(book, models.Work):
|
||||
book = book.default_edition
|
||||
if not book or not book.parent_work:
|
||||
return HttpResponseNotFound()
|
||||
|
||||
work = book.parent_work
|
||||
|
||||
# all reviews for the book
|
||||
reviews = privacy_filter(
|
||||
request.user, models.Review.objects.filter(book__in=work.editions.all())
|
||||
)
|
||||
|
||||
# the reviews to show
|
||||
if user_statuses:
|
||||
if user_statuses == "review":
|
||||
queryset = book.review_set.select_subclasses()
|
||||
elif user_statuses == "comment":
|
||||
queryset = book.comment_set
|
||||
else:
|
||||
queryset = book.quotation_set
|
||||
queryset = queryset.filter(user=request.user, deleted=False)
|
||||
else:
|
||||
queryset = reviews.exclude(Q(content__isnull=True) | Q(content=""))
|
||||
queryset = queryset.select_related("user").order_by("-published_date")
|
||||
paginated = Paginator(queryset, PAGE_LENGTH)
|
||||
|
||||
lists = privacy_filter(
|
||||
request.user,
|
||||
models.List.objects.filter(
|
||||
listitem__approved=True,
|
||||
listitem__book__in=book.parent_work.editions.all(),
|
||||
),
|
||||
)
|
||||
data = {
|
||||
"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,
|
||||
"rating": reviews.aggregate(Avg("rating"))["rating__avg"],
|
||||
"lists": lists,
|
||||
}
|
||||
|
||||
if request.user.is_authenticated:
|
||||
readthroughs = models.ReadThrough.objects.filter(
|
||||
user=request.user,
|
||||
book=book,
|
||||
).order_by("start_date")
|
||||
|
||||
for readthrough in readthroughs:
|
||||
readthrough.progress_updates = (
|
||||
readthrough.progressupdate_set.all().order_by("-updated_date")
|
||||
)
|
||||
data["readthroughs"] = readthroughs
|
||||
|
||||
data["user_shelfbooks"] = models.ShelfBook.objects.filter(
|
||||
user=request.user, book=book
|
||||
).select_related("shelf")
|
||||
|
||||
data["other_edition_shelves"] = models.ShelfBook.objects.filter(
|
||||
~Q(book=book),
|
||||
user=request.user,
|
||||
book__parent_work=book.parent_work,
|
||||
).select_related("shelf", "book")
|
||||
|
||||
filters = {"user": request.user, "deleted": False}
|
||||
data["user_statuses"] = {
|
||||
"review_count": book.review_set.filter(**filters).count(),
|
||||
"comment_count": book.comment_set.filter(**filters).count(),
|
||||
"quotation_count": book.quotation_set.filter(**filters).count(),
|
||||
}
|
||||
|
||||
return TemplateResponse(request, "book/book.html", data)
|
||||
|
||||
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
@method_decorator(
|
||||
permission_required("bookwyrm.edit_book", raise_exception=True), name="dispatch"
|
||||
|
@ -132,7 +32,7 @@ class EditBook(View):
|
|||
if not book.description:
|
||||
book.description = book.parent_work.description
|
||||
data = {"book": book, "form": forms.EditionForm(instance=book)}
|
||||
return TemplateResponse(request, "book/edit_book.html", data)
|
||||
return TemplateResponse(request, "book/edit/edit_book.html", data)
|
||||
|
||||
def post(self, request, book_id=None):
|
||||
"""edit a book cool"""
|
||||
|
@ -142,7 +42,7 @@ class EditBook(View):
|
|||
|
||||
data = {"book": book, "form": form}
|
||||
if not form.is_valid():
|
||||
return TemplateResponse(request, "book/edit_book.html", data)
|
||||
return TemplateResponse(request, "book/edit/edit_book.html", data)
|
||||
|
||||
add_author = request.POST.get("add_author")
|
||||
# we're adding an author through a free text field
|
||||
|
@ -185,6 +85,8 @@ class EditBook(View):
|
|||
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()
|
||||
|
@ -199,7 +101,7 @@ class EditBook(View):
|
|||
except (MultiValueDictKeyError, ValueError):
|
||||
pass
|
||||
data["form"].data = formcopy
|
||||
return TemplateResponse(request, "book/edit_book.html", data)
|
||||
return TemplateResponse(request, "book/edit/edit_book.html", data)
|
||||
|
||||
remove_authors = request.POST.getlist("remove_authors")
|
||||
for author_id in remove_authors:
|
||||
|
@ -230,7 +132,7 @@ class ConfirmEditBook(View):
|
|||
|
||||
data = {"book": book, "form": form}
|
||||
if not form.is_valid():
|
||||
return TemplateResponse(request, "book/edit_book.html", data)
|
||||
return TemplateResponse(request, "book/edit/edit_book.html", data)
|
||||
|
||||
with transaction.atomic():
|
||||
# save book
|
||||
|
@ -261,74 +163,18 @@ class ConfirmEditBook(View):
|
|||
work = models.Work.objects.create(title=form.cleaned_data["title"])
|
||||
work.authors.set(book.authors.all())
|
||||
book.parent_work = work
|
||||
# we don't tell the world when creating a book
|
||||
book.save(broadcast=False)
|
||||
|
||||
for author_id in request.POST.getlist("remove_authors"):
|
||||
book.authors.remove(author_id)
|
||||
|
||||
# import cover, if requested
|
||||
url = request.POST.get("cover-url")
|
||||
if url:
|
||||
image = set_cover_from_url(url)
|
||||
if image:
|
||||
book.cover.save(*image, save=False)
|
||||
|
||||
# we don't tell the world when creating a book
|
||||
book.save(broadcast=False)
|
||||
|
||||
return redirect(f"/book/{book.id}")
|
||||
|
||||
|
||||
@login_required
|
||||
@require_POST
|
||||
def upload_cover(request, book_id):
|
||||
"""upload a new cover"""
|
||||
book = get_object_or_404(models.Edition, id=book_id)
|
||||
book.last_edited_by = request.user
|
||||
|
||||
url = request.POST.get("cover-url")
|
||||
if url:
|
||||
image = set_cover_from_url(url)
|
||||
if image:
|
||||
book.cover.save(*image)
|
||||
|
||||
return redirect(f"{book.local_path}?cover_error=True")
|
||||
|
||||
form = forms.CoverForm(request.POST, request.FILES, instance=book)
|
||||
if not form.is_valid() or not form.files.get("cover"):
|
||||
return redirect(book.local_path)
|
||||
|
||||
book.cover = form.files["cover"]
|
||||
book.save()
|
||||
|
||||
return redirect(book.local_path)
|
||||
|
||||
|
||||
def set_cover_from_url(url):
|
||||
"""load it from a url"""
|
||||
try:
|
||||
image_file = get_image(url)
|
||||
except: # pylint: disable=bare-except
|
||||
return None
|
||||
if not image_file:
|
||||
return None
|
||||
image_name = str(uuid4()) + "." + url.split(".")[-1]
|
||||
image_content = ContentFile(image_file.content)
|
||||
return [image_name, image_content]
|
||||
|
||||
|
||||
@login_required
|
||||
@require_POST
|
||||
@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)
|
||||
|
||||
description = request.POST.get("description")
|
||||
|
||||
book.description = description
|
||||
book.last_edited_by = request.user
|
||||
book.save(update_fields=["description", "last_edited_by"])
|
||||
|
||||
return redirect("book", book.id)
|
||||
|
||||
|
||||
@require_POST
|
||||
def resolve_book(request):
|
||||
"""figure out the local path to a book from a remote_id"""
|
||||
remote_id = request.POST.get("remote_id")
|
||||
connector = connector_manager.get_or_create_connector(remote_id)
|
||||
book = connector.get_or_create_book(remote_id)
|
||||
|
||||
return redirect("book", book.id)
|
|
@ -14,7 +14,7 @@ from django.views.decorators.http import require_POST
|
|||
from bookwyrm import models
|
||||
from bookwyrm.activitypub import ActivitypubResponse
|
||||
from bookwyrm.settings import PAGE_LENGTH
|
||||
from .helpers import is_api_request
|
||||
from bookwyrm.views.helpers import is_api_request
|
||||
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
|
@ -66,7 +66,7 @@ class Editions(View):
|
|||
e.physical_format.lower() for e in editions if e.physical_format
|
||||
),
|
||||
}
|
||||
return TemplateResponse(request, "book/editions.html", data)
|
||||
return TemplateResponse(request, "book/editions/editions.html", data)
|
||||
|
||||
|
||||
@login_required
|
|
@ -25,10 +25,10 @@ class Directory(View):
|
|||
|
||||
users = suggested_users.get_annotated_users(request.user, **filters)
|
||||
sort = request.GET.get("sort")
|
||||
if sort == "recent":
|
||||
users = users.order_by("-last_active_date")
|
||||
else:
|
||||
if sort == "suggested":
|
||||
users = users.order_by("-mutuals", "-last_active_date")
|
||||
else:
|
||||
users = users.order_by("-last_active_date")
|
||||
|
||||
paginated = Paginator(users, 12)
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ from django.contrib.auth.decorators import login_required
|
|||
from django.core.paginator import Paginator
|
||||
from django.db.models import Q
|
||||
from django.http import HttpResponseNotFound, Http404
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.template.response import TemplateResponse
|
||||
from django.utils import timezone
|
||||
from django.utils.decorators import method_decorator
|
||||
|
@ -93,17 +94,15 @@ class Status(View):
|
|||
|
||||
def get(self, request, username, status_id):
|
||||
"""display a particular status (and replies, etc)"""
|
||||
try:
|
||||
user = get_user_from_username(request.user, username)
|
||||
status = models.Status.objects.select_subclasses().get(
|
||||
user=user, id=status_id, deleted=False
|
||||
)
|
||||
except (ValueError, models.Status.DoesNotExist):
|
||||
return HttpResponseNotFound()
|
||||
|
||||
user = get_user_from_username(request.user, username)
|
||||
status = get_object_or_404(
|
||||
models.Status.objects.select_subclasses(),
|
||||
user=user,
|
||||
id=status_id,
|
||||
deleted=False,
|
||||
)
|
||||
# make sure the user is authorized to see the status
|
||||
if not status.visible_to_user(request.user):
|
||||
return HttpResponseNotFound()
|
||||
status.raise_visible_to_user(request.user)
|
||||
|
||||
if is_api_request(request):
|
||||
return ActivitypubResponse(
|
||||
|
@ -133,6 +132,7 @@ class Replies(View):
|
|||
status = models.Status.objects.get(id=status_id)
|
||||
if status.user.localname != username:
|
||||
return HttpResponseNotFound()
|
||||
status.raise_visible_to_user(request.user)
|
||||
|
||||
return ActivitypubResponse(status.to_replies(**request.GET))
|
||||
|
||||
|
@ -168,9 +168,11 @@ def get_suggested_books(user, max_books=5):
|
|||
shelf_preview = {
|
||||
"name": shelf.name,
|
||||
"identifier": shelf.identifier,
|
||||
"books": shelf.books.order_by("shelfbook").prefetch_related("authors")[
|
||||
:limit
|
||||
],
|
||||
"books": models.Edition.viewer_aware_objects(user)
|
||||
.filter(
|
||||
shelfbook__shelf=shelf,
|
||||
)
|
||||
.prefetch_related("authors")[:limit],
|
||||
}
|
||||
suggested_books.append(shelf_preview)
|
||||
book_count += len(shelf_preview["books"])
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
""" views for actions you can take in the application """
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.db import IntegrityError
|
||||
from django.http import HttpResponseBadRequest
|
||||
from django.shortcuts import redirect
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.views.decorators.http import require_POST
|
||||
|
||||
from bookwyrm import models
|
||||
|
@ -78,12 +77,10 @@ def delete_follow_request(request):
|
|||
username = request.POST["user"]
|
||||
requester = get_user_from_username(request.user, username)
|
||||
|
||||
try:
|
||||
follow_request = models.UserFollowRequest.objects.get(
|
||||
user_subject=requester, user_object=request.user
|
||||
)
|
||||
except models.UserFollowRequest.DoesNotExist:
|
||||
return HttpResponseBadRequest()
|
||||
follow_request = get_object_or_404(
|
||||
models.UserFollowRequest, user_subject=requester, user_object=request.user
|
||||
)
|
||||
follow_request.raise_not_deletable(request.user)
|
||||
|
||||
follow_request.delete()
|
||||
return redirect(f"/user/{request.user.localname}")
|
||||
|
|
|
@ -5,16 +5,14 @@ from django.contrib.auth.decorators import login_required
|
|||
from django.contrib.postgres.search import TrigramSimilarity
|
||||
from django.db.models.functions import Greatest
|
||||
from django.db.models import Count, Q
|
||||
from django.http import HttpResponseNotFound
|
||||
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 bookwyrm import forms, models
|
||||
from bookwyrm.connectors import connector_manager
|
||||
from bookwyrm import book_search, forms, models
|
||||
from bookwyrm.suggested_users import suggested_users
|
||||
from .edit_user import save_user_form
|
||||
from .preferences.edit_user import save_user_form
|
||||
|
||||
|
||||
# pylint: disable= no-self-use
|
||||
|
@ -55,7 +53,7 @@ class GetStartedBooks(View):
|
|||
query = request.GET.get("query")
|
||||
book_results = popular_books = []
|
||||
if query:
|
||||
book_results = connector_manager.local_search(query, raw=True)[:5]
|
||||
book_results = book_search.search(query)[:5]
|
||||
if len(book_results) < 5:
|
||||
popular_books = (
|
||||
models.Edition.objects.exclude(
|
||||
|
@ -91,9 +89,8 @@ class GetStartedBooks(View):
|
|||
for (book_id, shelf_id) in shelve_actions:
|
||||
book = get_object_or_404(models.Edition, id=book_id)
|
||||
shelf = get_object_or_404(models.Shelf, id=shelf_id)
|
||||
if shelf.user != request.user:
|
||||
# hmmmmm
|
||||
return HttpResponseNotFound()
|
||||
shelf.raise_not_editable(request.user)
|
||||
|
||||
models.ShelfBook.objects.create(book=book, shelf=shelf, user=request.user)
|
||||
return redirect(self.next_view)
|
||||
|
||||
|
|
|
@ -31,8 +31,8 @@ class Goal(View):
|
|||
if not goal and year != timezone.now().year:
|
||||
return redirect("user-goal", username, current_year)
|
||||
|
||||
if goal and not goal.visible_to_user(request.user):
|
||||
return HttpResponseNotFound()
|
||||
if goal:
|
||||
goal.raise_visible_to_user(request.user)
|
||||
|
||||
data = {
|
||||
"goal_form": forms.GoalForm(instance=goal),
|
||||
|
@ -41,16 +41,16 @@ class Goal(View):
|
|||
"year": year,
|
||||
"is_self": request.user == user,
|
||||
}
|
||||
return TemplateResponse(request, "goal.html", data)
|
||||
return TemplateResponse(request, "user/goal.html", data)
|
||||
|
||||
def post(self, request, username, year):
|
||||
"""update or create an annual goal"""
|
||||
user = get_user_from_username(request.user, username)
|
||||
if user != request.user:
|
||||
return HttpResponseNotFound()
|
||||
|
||||
year = int(year)
|
||||
goal = models.AnnualGoal.objects.filter(year=year, user=request.user).first()
|
||||
user = get_user_from_username(request.user, username)
|
||||
goal = models.AnnualGoal.objects.filter(year=year, user=user).first()
|
||||
if goal:
|
||||
goal.raise_not_editable(request.user)
|
||||
|
||||
form = forms.GoalForm(request.POST, instance=goal)
|
||||
if not form.is_valid():
|
||||
data = {
|
||||
|
@ -58,15 +58,15 @@ class Goal(View):
|
|||
"goal": goal,
|
||||
"year": year,
|
||||
}
|
||||
return TemplateResponse(request, "goal.html", data)
|
||||
return TemplateResponse(request, "user/goal.html", data)
|
||||
goal = form.save()
|
||||
|
||||
if request.POST.get("post-status"):
|
||||
# create status, if appropraite
|
||||
# create status, if appropriate
|
||||
template = get_template("snippets/generated_status/goal.html")
|
||||
create_generated_note(
|
||||
request.user,
|
||||
template.render({"goal": goal, "user": request.user}).strip(),
|
||||
template.render({"goal": goal, "user": user}).strip(),
|
||||
privacy=goal.privacy,
|
||||
)
|
||||
|
||||
|
@ -78,5 +78,5 @@ class Goal(View):
|
|||
def hide_goal(request):
|
||||
"""don't keep bugging people to set a goal"""
|
||||
request.user.show_goal = False
|
||||
request.user.save(broadcast=False)
|
||||
request.user.save(broadcast=False, update_fields=["show_goal"])
|
||||
return redirect(request.headers.get("Referer", "/"))
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
""" helper functions used in various views """
|
||||
import re
|
||||
from datetime import datetime
|
||||
import dateutil.parser
|
||||
import dateutil.tz
|
||||
from dateutil.parser import ParserError
|
||||
|
||||
from requests import HTTPError
|
||||
from django.core.exceptions import FieldError
|
||||
from django.db.models import Q
|
||||
|
@ -32,7 +37,9 @@ def get_user_from_username(viewer, username):
|
|||
|
||||
def is_api_request(request):
|
||||
"""check whether a request is asking for html or data"""
|
||||
return "json" in request.headers.get("Accept", "") or request.path[-5:] == ".json"
|
||||
return "json" in request.headers.get("Accept", "") or re.match(
|
||||
r".*\.json/?$", request.path
|
||||
)
|
||||
|
||||
|
||||
def is_bookwyrm_request(request):
|
||||
|
@ -178,3 +185,15 @@ def get_landing_books():
|
|||
.order_by("-review__published_date")[:6]
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def load_date_in_user_tz_as_utc(date_str: str, user: models.User) -> datetime:
|
||||
"""ensures that data is stored consistently in the UTC timezone"""
|
||||
if not date_str:
|
||||
return None
|
||||
user_tz = dateutil.tz.gettz(user.preferred_timezone)
|
||||
date = dateutil.parser.parse(date_str, ignoretz=True)
|
||||
try:
|
||||
return date.replace(tzinfo=user_tz).astimezone(dateutil.tz.UTC)
|
||||
except ParserError:
|
||||
return None
|
||||
|
|
|
@ -80,7 +80,7 @@ class ImportStatus(View):
|
|||
"""status of an import job"""
|
||||
job = get_object_or_404(models.ImportJob, id=job_id)
|
||||
if job.user != request.user:
|
||||
raise PermissionDenied
|
||||
raise PermissionDenied()
|
||||
|
||||
try:
|
||||
task = app.AsyncResult(job.task_id)
|
||||
|
|
|
@ -3,8 +3,9 @@ import json
|
|||
import re
|
||||
from urllib.parse import urldefrag
|
||||
|
||||
from django.http import HttpResponse, HttpResponseNotFound
|
||||
from django.http import HttpResponseBadRequest, HttpResponseForbidden
|
||||
from django.http import HttpResponse, Http404
|
||||
from django.core.exceptions import BadRequest, PermissionDenied
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views import View
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
@ -21,36 +22,30 @@ from bookwyrm.utils import regex
|
|||
class Inbox(View):
|
||||
"""requests sent by outside servers"""
|
||||
|
||||
# pylint: disable=too-many-return-statements
|
||||
def post(self, request, username=None):
|
||||
"""only works as POST request"""
|
||||
# first check if this server is on our shitlist
|
||||
if is_blocked_user_agent(request):
|
||||
return HttpResponseForbidden()
|
||||
raise_is_blocked_user_agent(request)
|
||||
|
||||
# make sure the user's inbox even exists
|
||||
if username:
|
||||
try:
|
||||
models.User.objects.get(localname=username)
|
||||
except models.User.DoesNotExist:
|
||||
return HttpResponseNotFound()
|
||||
get_object_or_404(models.User, localname=username, is_active=True)
|
||||
|
||||
# is it valid json? does it at least vaguely resemble an activity?
|
||||
try:
|
||||
activity_json = json.loads(request.body)
|
||||
except json.decoder.JSONDecodeError:
|
||||
return HttpResponseBadRequest()
|
||||
raise BadRequest()
|
||||
|
||||
# let's be extra sure we didn't block this domain
|
||||
if is_blocked_activity(activity_json):
|
||||
return HttpResponseForbidden()
|
||||
raise_is_blocked_activity(activity_json)
|
||||
|
||||
if (
|
||||
not "object" in activity_json
|
||||
or not "type" in activity_json
|
||||
or not activity_json["type"] in activitypub.activity_objects
|
||||
):
|
||||
return HttpResponseNotFound()
|
||||
raise Http404()
|
||||
|
||||
# verify the signature
|
||||
if not has_valid_signature(request, activity_json):
|
||||
|
@ -65,32 +60,35 @@ class Inbox(View):
|
|||
return HttpResponse()
|
||||
|
||||
|
||||
def is_blocked_user_agent(request):
|
||||
def raise_is_blocked_user_agent(request):
|
||||
"""check if a request is from a blocked server based on user agent"""
|
||||
# check user agent
|
||||
user_agent = request.headers.get("User-Agent")
|
||||
if not user_agent:
|
||||
return False
|
||||
return
|
||||
url = re.search(rf"https?://{regex.DOMAIN}/?", user_agent)
|
||||
if not url:
|
||||
return False
|
||||
return
|
||||
url = url.group()
|
||||
return models.FederatedServer.is_blocked(url)
|
||||
if models.FederatedServer.is_blocked(url):
|
||||
raise PermissionDenied()
|
||||
|
||||
|
||||
def is_blocked_activity(activity_json):
|
||||
def raise_is_blocked_activity(activity_json):
|
||||
"""get the sender out of activity json and check if it's blocked"""
|
||||
actor = activity_json.get("actor")
|
||||
|
||||
# check if the user is banned/deleted
|
||||
existing = models.User.find_existing_by_remote_id(actor)
|
||||
if existing and existing.deleted:
|
||||
return True
|
||||
raise PermissionDenied()
|
||||
|
||||
if not actor:
|
||||
# well I guess it's not even a valid activity so who knows
|
||||
return False
|
||||
return models.FederatedServer.is_blocked(actor)
|
||||
return
|
||||
|
||||
if models.FederatedServer.is_blocked(actor):
|
||||
raise PermissionDenied()
|
||||
|
||||
|
||||
@app.task(queue="medium_priority")
|
||||
|
|
|
@ -4,7 +4,7 @@ from django.http import JsonResponse
|
|||
from django.template.response import TemplateResponse
|
||||
from django.views import View
|
||||
|
||||
from bookwyrm.connectors import connector_manager
|
||||
from bookwyrm import book_search
|
||||
from bookwyrm.settings import PAGE_LENGTH
|
||||
from .helpers import is_api_request
|
||||
|
||||
|
@ -14,10 +14,12 @@ class Isbn(View):
|
|||
|
||||
def get(self, request, isbn):
|
||||
"""info about a book"""
|
||||
book_results = connector_manager.isbn_local_search(isbn)
|
||||
book_results = book_search.isbn_search(isbn)
|
||||
|
||||
if is_api_request(request):
|
||||
return JsonResponse([r.json() for r in book_results], safe=False)
|
||||
return JsonResponse(
|
||||
[book_search.format_search_result(r) for r in book_results], safe=False
|
||||
)
|
||||
|
||||
paginated = Paginator(book_results, PAGE_LENGTH).get_page(
|
||||
request.GET.get("page")
|
||||
|
|
|
@ -3,12 +3,11 @@ from typing import Optional
|
|||
from urllib.parse import urlencode
|
||||
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.core.paginator import Paginator
|
||||
from django.db import IntegrityError, transaction
|
||||
from django.db.models import Avg, Count, DecimalField, Q, Max
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.http import HttpResponseNotFound, HttpResponseBadRequest, HttpResponse
|
||||
from django.http import HttpResponseBadRequest, HttpResponse
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.template.response import TemplateResponse
|
||||
from django.urls import reverse
|
||||
|
@ -16,9 +15,8 @@ from django.utils.decorators import method_decorator
|
|||
from django.views import View
|
||||
from django.views.decorators.http import require_POST
|
||||
|
||||
from bookwyrm import forms, models
|
||||
from bookwyrm import book_search, forms, models
|
||||
from bookwyrm.activitypub import ActivitypubResponse
|
||||
from bookwyrm.connectors import connector_manager
|
||||
from bookwyrm.settings import PAGE_LENGTH
|
||||
from .helpers import is_api_request, privacy_filter
|
||||
from .helpers import get_user_from_username
|
||||
|
@ -114,8 +112,7 @@ class List(View):
|
|||
def get(self, request, list_id):
|
||||
"""display a book list"""
|
||||
book_list = get_object_or_404(models.List, id=list_id)
|
||||
if not book_list.visible_to_user(request.user):
|
||||
return HttpResponseNotFound()
|
||||
book_list.raise_visible_to_user(request.user)
|
||||
|
||||
if is_api_request(request):
|
||||
return ActivitypubResponse(book_list.to_activity(**request.GET))
|
||||
|
@ -155,9 +152,8 @@ class List(View):
|
|||
|
||||
if query and request.user.is_authenticated:
|
||||
# search for books
|
||||
suggestions = connector_manager.local_search(
|
||||
suggestions = book_search.search(
|
||||
query,
|
||||
raw=True,
|
||||
filters=[~Q(parent_work__editions__in=book_list.books.all())],
|
||||
)
|
||||
elif request.user.is_authenticated:
|
||||
|
@ -196,6 +192,8 @@ class List(View):
|
|||
def post(self, request, list_id):
|
||||
"""edit a list"""
|
||||
book_list = get_object_or_404(models.List, id=list_id)
|
||||
book_list.raise_not_editable(request.user)
|
||||
|
||||
form = forms.ListForm(request.POST, instance=book_list)
|
||||
if not form.is_valid():
|
||||
return redirect("list", book_list.id)
|
||||
|
@ -213,9 +211,7 @@ class Curate(View):
|
|||
def get(self, request, list_id):
|
||||
"""display a pending list"""
|
||||
book_list = get_object_or_404(models.List, id=list_id)
|
||||
if not book_list.user == request.user:
|
||||
# only the creater can curate the list
|
||||
return HttpResponseNotFound()
|
||||
book_list.raise_not_editable(request.user)
|
||||
|
||||
data = {
|
||||
"list": book_list,
|
||||
|
@ -229,6 +225,8 @@ class Curate(View):
|
|||
def post(self, request, list_id):
|
||||
"""edit a book_list"""
|
||||
book_list = get_object_or_404(models.List, id=list_id)
|
||||
book_list.raise_not_editable(request.user)
|
||||
|
||||
suggestion = get_object_or_404(models.ListItem, id=request.POST.get("item"))
|
||||
approved = request.POST.get("approved") == "true"
|
||||
if approved:
|
||||
|
@ -276,8 +274,7 @@ def delete_list(request, list_id):
|
|||
book_list = get_object_or_404(models.List, id=list_id)
|
||||
|
||||
# only the owner or a moderator can delete a list
|
||||
if book_list.user != request.user and not request.user.has_perm("moderate_post"):
|
||||
raise PermissionDenied
|
||||
book_list.raise_not_deletable(request.user)
|
||||
|
||||
book_list.delete()
|
||||
return redirect("lists")
|
||||
|
@ -291,8 +288,8 @@ def add_book(request):
|
|||
is_group_member = False
|
||||
if book_list.curation == "group":
|
||||
is_group_member = models.GroupMember.objects.filter(group=book_list.group, user=request.user).exists()
|
||||
if not book_list.visible_to_user(request.user):
|
||||
return HttpResponseNotFound()
|
||||
|
||||
book_list.raise_visible_to_user(request.user)
|
||||
|
||||
book = get_object_or_404(models.Edition, id=request.POST.get("book"))
|
||||
# do you have permission to add to the list?
|
||||
|
@ -340,16 +337,22 @@ def add_book(request):
|
|||
@login_required
|
||||
def remove_book(request, list_id):
|
||||
"""remove a book from a list"""
|
||||
with transaction.atomic():
|
||||
book_list = get_object_or_404(models.List, id=list_id)
|
||||
item = get_object_or_404(models.ListItem, id=request.POST.get("item"))
|
||||
is_group_member = models.GroupMember.objects.filter(group=book_list.group, user=request.user).exists()
|
||||
if not book_list.user == request.user and not item.user == request.user and not is_group_member:
|
||||
return HttpResponseNotFound()
|
||||
|
||||
book_list = get_object_or_404(models.List, id=list_id)
|
||||
item = get_object_or_404(models.ListItem, id=request.POST.get("item"))
|
||||
|
||||
# TODO: put this logiv into raise_not_deletable
|
||||
# is_group_member = models.GroupMember.objects.filter(group=book_list.group, user=request.user).exists()
|
||||
# if not book_list.user == request.user and not item.user == request.user and not is_group_member:
|
||||
# return HttpResponseNotFound()
|
||||
|
||||
item.raise_not_deletable(request.user)
|
||||
|
||||
with transaction.atomic():
|
||||
deleted_order = item.order
|
||||
item.delete()
|
||||
normalize_book_list_ordering(book_list.id, start=deleted_order)
|
||||
normalize_book_list_ordering(book_list.id, start=deleted_order)
|
||||
|
||||
return redirect("list", list_id)
|
||||
|
||||
|
||||
|
@ -360,34 +363,32 @@ def set_book_position(request, list_item_id):
|
|||
Action for when the list user manually specifies a list position, takes
|
||||
special care with the unique ordering per list.
|
||||
"""
|
||||
list_item = get_object_or_404(models.ListItem, id=list_item_id)
|
||||
list_item.book_list.raise_not_editable(request.user)
|
||||
try:
|
||||
int_position = int(request.POST.get("position"))
|
||||
except ValueError:
|
||||
return HttpResponseBadRequest("bad value for position. should be an integer")
|
||||
|
||||
if int_position < 1:
|
||||
return HttpResponseBadRequest("position cannot be less than 1")
|
||||
|
||||
book_list = list_item.book_list
|
||||
|
||||
# the max position to which a book may be set is the highest order for
|
||||
# books which are approved
|
||||
order_max = book_list.listitem_set.filter(approved=True).aggregate(Max("order"))[
|
||||
"order__max"
|
||||
]
|
||||
|
||||
int_position = min(int_position, order_max)
|
||||
|
||||
original_order = list_item.order
|
||||
if original_order == int_position:
|
||||
# no change
|
||||
return HttpResponse(status=204)
|
||||
|
||||
with transaction.atomic():
|
||||
list_item = get_object_or_404(models.ListItem, id=list_item_id)
|
||||
try:
|
||||
int_position = int(request.POST.get("position"))
|
||||
except ValueError:
|
||||
return HttpResponseBadRequest(
|
||||
"bad value for position. should be an integer"
|
||||
)
|
||||
|
||||
if int_position < 1:
|
||||
return HttpResponseBadRequest("position cannot be less than 1")
|
||||
|
||||
book_list = list_item.book_list
|
||||
|
||||
# the max position to which a book may be set is the highest order for
|
||||
# books which are approved
|
||||
order_max = book_list.listitem_set.filter(approved=True).aggregate(
|
||||
Max("order")
|
||||
)["order__max"]
|
||||
|
||||
int_position = min(int_position, order_max)
|
||||
|
||||
if request.user not in (book_list.user, list_item.user):
|
||||
return HttpResponseNotFound()
|
||||
|
||||
original_order = list_item.order
|
||||
if original_order == int_position:
|
||||
return HttpResponse(status=204)
|
||||
if original_order > int_position:
|
||||
list_item.order = -1
|
||||
list_item.save()
|
||||
|
|
|
@ -13,7 +13,20 @@ class Notifications(View):
|
|||
|
||||
def get(self, request, notification_type=None):
|
||||
"""people are interacting with you, get hyped"""
|
||||
notifications = request.user.notification_set.all().order_by("-created_date")
|
||||
notifications = (
|
||||
request.user.notification_set.all()
|
||||
.order_by("-created_date")
|
||||
.select_related(
|
||||
"related_status",
|
||||
"related_status__reply_parent",
|
||||
"related_import",
|
||||
"related_report",
|
||||
"related_user",
|
||||
"related_book",
|
||||
"related_list_item",
|
||||
"related_list_item__book",
|
||||
)
|
||||
)
|
||||
if notification_type == "mentions":
|
||||
notifications = notifications.filter(
|
||||
notification_type__in=["REPLY", "MENTION", "TAG"]
|
||||
|
@ -24,9 +37,9 @@ class Notifications(View):
|
|||
"unread": unread,
|
||||
}
|
||||
notifications.update(read=True)
|
||||
return TemplateResponse(request, "notifications.html", data)
|
||||
return TemplateResponse(request, "notifications/notifications_page.html", data)
|
||||
|
||||
def post(self, request):
|
||||
"""permanently delete notification for user"""
|
||||
request.user.notification_set.filter(read=True).delete()
|
||||
return redirect("/notifications")
|
||||
return redirect("notifications")
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
""" class views for password management """
|
||||
from django.contrib.auth import login
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.shortcuts import redirect
|
||||
from django.template.response import TemplateResponse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views import View
|
||||
|
||||
|
@ -27,7 +25,9 @@ class PasswordResetRequest(View):
|
|||
"""create a password reset token"""
|
||||
email = request.POST.get("email")
|
||||
try:
|
||||
user = models.User.objects.get(email=email, email__isnull=False)
|
||||
user = models.User.viewer_aware_objects(request.user).get(
|
||||
email=email, email__isnull=False
|
||||
)
|
||||
except models.User.DoesNotExist:
|
||||
data = {"error": _("No user with that email address was found.")}
|
||||
return TemplateResponse(request, "password_reset_request.html", data)
|
||||
|
@ -52,9 +52,9 @@ class PasswordReset(View):
|
|||
try:
|
||||
reset_code = models.PasswordReset.objects.get(code=code)
|
||||
if not reset_code.valid():
|
||||
raise PermissionDenied
|
||||
raise PermissionDenied()
|
||||
except models.PasswordReset.DoesNotExist:
|
||||
raise PermissionDenied
|
||||
raise PermissionDenied()
|
||||
|
||||
return TemplateResponse(request, "password_reset.html", {"code": code})
|
||||
|
||||
|
@ -80,26 +80,3 @@ class PasswordReset(View):
|
|||
login(request, user)
|
||||
reset_code.delete()
|
||||
return redirect("/")
|
||||
|
||||
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
class ChangePassword(View):
|
||||
"""change password as logged in user"""
|
||||
|
||||
def get(self, request):
|
||||
"""change password page"""
|
||||
data = {"user": request.user}
|
||||
return TemplateResponse(request, "preferences/change_password.html", data)
|
||||
|
||||
def post(self, request):
|
||||
"""allow a user to change their password"""
|
||||
new_password = request.POST.get("password")
|
||||
confirm_password = request.POST.get("confirm-password")
|
||||
|
||||
if new_password != confirm_password:
|
||||
return redirect("preferences/password")
|
||||
|
||||
request.user.set_password(new_password)
|
||||
request.user.save(broadcast=False, update_fields=["password"])
|
||||
login(request, request.user)
|
||||
return redirect(request.user.local_path)
|
||||
|
|
0
bookwyrm/views/preferences/__init__.py
Normal file
0
bookwyrm/views/preferences/__init__.py
Normal file
|
@ -1,6 +1,5 @@
|
|||
""" views for actions you can take in the application """
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.http import HttpResponseNotFound
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.template.response import TemplateResponse
|
||||
from django.utils.decorators import method_decorator
|
||||
|
@ -24,7 +23,7 @@ class Block(View):
|
|||
models.UserBlocks.objects.create(
|
||||
user_subject=request.user, user_object=to_block
|
||||
)
|
||||
return redirect("/preferences/block")
|
||||
return redirect("prefs-block")
|
||||
|
||||
|
||||
@require_POST
|
||||
|
@ -32,12 +31,10 @@ class Block(View):
|
|||
def unblock(request, user_id):
|
||||
"""undo a block"""
|
||||
to_unblock = get_object_or_404(models.User, id=user_id)
|
||||
try:
|
||||
block = models.UserBlocks.objects.get(
|
||||
user_subject=request.user,
|
||||
user_object=to_unblock,
|
||||
)
|
||||
except models.UserBlocks.DoesNotExist:
|
||||
return HttpResponseNotFound()
|
||||
block = get_object_or_404(
|
||||
models.UserBlocks,
|
||||
user_subject=request.user,
|
||||
user_object=to_unblock,
|
||||
)
|
||||
block.delete()
|
||||
return redirect("/preferences/block")
|
||||
return redirect("prefs-block")
|
31
bookwyrm/views/preferences/change_password.py
Normal file
31
bookwyrm/views/preferences/change_password.py
Normal file
|
@ -0,0 +1,31 @@
|
|||
""" class views for password management """
|
||||
from django.contrib.auth import login
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.shortcuts import redirect
|
||||
from django.template.response import TemplateResponse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views import View
|
||||
|
||||
|
||||
# pylint: disable= no-self-use
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
class ChangePassword(View):
|
||||
"""change password as logged in user"""
|
||||
|
||||
def get(self, request):
|
||||
"""change password page"""
|
||||
data = {"user": request.user}
|
||||
return TemplateResponse(request, "preferences/change_password.html", data)
|
||||
|
||||
def post(self, request):
|
||||
"""allow a user to change their password"""
|
||||
new_password = request.POST.get("password")
|
||||
confirm_password = request.POST.get("confirm-password")
|
||||
|
||||
if new_password != confirm_password:
|
||||
return redirect("prefs-password")
|
||||
|
||||
request.user.set_password(new_password)
|
||||
request.user.save(broadcast=False, update_fields=["password"])
|
||||
login(request, request.user)
|
||||
return redirect("user-feed", request.user.localname)
|
38
bookwyrm/views/preferences/delete_user.py
Normal file
38
bookwyrm/views/preferences/delete_user.py
Normal file
|
@ -0,0 +1,38 @@
|
|||
""" edit your own account """
|
||||
from django.contrib.auth import logout
|
||||
from django.contrib.auth.decorators import login_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 bookwyrm import forms, models
|
||||
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
class DeleteUser(View):
|
||||
"""delete user view"""
|
||||
|
||||
def get(self, request):
|
||||
"""delete page for a user"""
|
||||
data = {
|
||||
"form": forms.DeleteUserForm(),
|
||||
"user": request.user,
|
||||
}
|
||||
return TemplateResponse(request, "preferences/delete_user.html", data)
|
||||
|
||||
def post(self, request):
|
||||
"""les get fancy with images"""
|
||||
form = forms.DeleteUserForm(request.POST, instance=request.user)
|
||||
# idk why but I couldn't get check_password to work on request.user
|
||||
user = models.User.objects.get(id=request.user.id)
|
||||
if form.is_valid() and user.check_password(form.cleaned_data["password"]):
|
||||
user.deactivation_reason = "self_deletion"
|
||||
user.delete()
|
||||
logout(request)
|
||||
return redirect("/")
|
||||
|
||||
form.errors["password"] = ["Invalid password"]
|
||||
data = {"form": form, "user": request.user}
|
||||
return TemplateResponse(request, "preferences/delete_user.html", data)
|
|
@ -1,9 +1,8 @@
|
|||
""" edit or delete ones own account"""
|
||||
""" edit your own account """
|
||||
from io import BytesIO
|
||||
from uuid import uuid4
|
||||
from PIL import Image
|
||||
|
||||
from django.contrib.auth import logout
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.files.base import ContentFile
|
||||
from django.shortcuts import redirect
|
||||
|
@ -11,7 +10,7 @@ from django.template.response import TemplateResponse
|
|||
from django.utils.decorators import method_decorator
|
||||
from django.views import View
|
||||
|
||||
from bookwyrm import forms, models
|
||||
from bookwyrm import forms
|
||||
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
|
@ -34,38 +33,9 @@ class EditUser(View):
|
|||
data = {"form": form, "user": request.user}
|
||||
return TemplateResponse(request, "preferences/edit_user.html", data)
|
||||
|
||||
user = save_user_form(form)
|
||||
save_user_form(form)
|
||||
|
||||
return redirect(user.local_path)
|
||||
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
class DeleteUser(View):
|
||||
"""delete user view"""
|
||||
|
||||
def get(self, request):
|
||||
"""delete page for a user"""
|
||||
data = {
|
||||
"form": forms.DeleteUserForm(),
|
||||
"user": request.user,
|
||||
}
|
||||
return TemplateResponse(request, "preferences/delete_user.html", data)
|
||||
|
||||
def post(self, request):
|
||||
"""les get fancy with images"""
|
||||
form = forms.DeleteUserForm(request.POST, instance=request.user)
|
||||
# idk why but I couldn't get check_password to work on request.user
|
||||
user = models.User.objects.get(id=request.user.id)
|
||||
if form.is_valid() and user.check_password(form.cleaned_data["password"]):
|
||||
user.deactivation_reason = "self_deletion"
|
||||
user.delete()
|
||||
logout(request)
|
||||
return redirect("/")
|
||||
|
||||
form.errors["password"] = ["Invalid password"]
|
||||
data = {"form": form, "user": request.user}
|
||||
return TemplateResponse(request, "preferences/delete_user.html", data)
|
||||
return redirect("user-feed", request.user.localname)
|
||||
|
||||
|
||||
def save_user_form(form):
|
|
@ -1,10 +1,6 @@
|
|||
""" the good stuff! the books! """
|
||||
from datetime import datetime
|
||||
import dateutil.parser
|
||||
import dateutil.tz
|
||||
from dateutil.parser import ParserError
|
||||
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.db import transaction
|
||||
from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseNotFound
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.template.response import TemplateResponse
|
||||
|
@ -12,8 +8,10 @@ from django.utils.decorators import method_decorator
|
|||
from django.views import View
|
||||
from django.views.decorators.http import require_POST
|
||||
|
||||
from bookwyrm import forms, models
|
||||
from bookwyrm import models
|
||||
from .status import CreateStatus
|
||||
from .helpers import get_edition, handle_reading_status, is_api_request
|
||||
from .helpers import load_date_in_user_tz_as_utc
|
||||
|
||||
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
|
@ -35,7 +33,7 @@ class ReadingStatus(View):
|
|||
return TemplateResponse(request, f"reading_progress/{template}", {"book": book})
|
||||
|
||||
def post(self, request, status, book_id):
|
||||
"""desire a book"""
|
||||
"""Change the state of a book by shelving it and adding reading dates"""
|
||||
identifier = {
|
||||
"want": models.Shelf.TO_READ,
|
||||
"start": models.Shelf.READING,
|
||||
|
@ -44,22 +42,25 @@ class ReadingStatus(View):
|
|||
if not identifier:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
desired_shelf = models.Shelf.objects.filter(
|
||||
identifier=identifier, user=request.user
|
||||
).first()
|
||||
|
||||
book = get_edition(book_id)
|
||||
|
||||
current_status_shelfbook = (
|
||||
models.ShelfBook.objects.select_related("shelf")
|
||||
.filter(
|
||||
shelf__identifier__in=models.Shelf.READ_STATUS_IDENTIFIERS,
|
||||
user=request.user,
|
||||
book=book,
|
||||
)
|
||||
.first()
|
||||
desired_shelf = get_object_or_404(
|
||||
models.Shelf, identifier=identifier, user=request.user
|
||||
)
|
||||
|
||||
book = (
|
||||
models.Edition.viewer_aware_objects(request.user)
|
||||
.prefetch_related("shelfbook_set__shelf")
|
||||
.get(id=book_id)
|
||||
)
|
||||
|
||||
# gets the first shelf that indicates a reading status, or None
|
||||
shelves = [
|
||||
s
|
||||
for s in book.current_shelves
|
||||
if s.shelf.identifier in models.Shelf.READ_STATUS_IDENTIFIERS
|
||||
]
|
||||
current_status_shelfbook = shelves[0] if shelves else None
|
||||
|
||||
# checking the referer prevents redirecting back to the modal page
|
||||
referer = request.headers.get("Referer", "/")
|
||||
referer = "/" if "reading-status" in referer else referer
|
||||
if current_status_shelfbook is not None:
|
||||
|
@ -72,49 +73,55 @@ class ReadingStatus(View):
|
|||
book=book, shelf=desired_shelf, user=request.user
|
||||
)
|
||||
|
||||
if desired_shelf.identifier != models.Shelf.TO_READ:
|
||||
# update or create a readthrough
|
||||
readthrough = update_readthrough(request, book=book)
|
||||
if readthrough:
|
||||
readthrough.save()
|
||||
update_readthrough_on_shelve(
|
||||
request.user,
|
||||
book,
|
||||
desired_shelf.identifier,
|
||||
start_date=request.POST.get("start_date"),
|
||||
finish_date=request.POST.get("finish_date"),
|
||||
)
|
||||
|
||||
# post about it (if you want)
|
||||
if request.POST.get("post-status"):
|
||||
# is it a comment?
|
||||
if request.POST.get("content"):
|
||||
form = forms.CommentForm(request.POST)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
else:
|
||||
# uh oh
|
||||
raise Exception(form.errors)
|
||||
else:
|
||||
privacy = request.POST.get("privacy")
|
||||
handle_reading_status(request.user, desired_shelf, book, privacy)
|
||||
return CreateStatus.as_view()(request, "comment")
|
||||
privacy = request.POST.get("privacy")
|
||||
handle_reading_status(request.user, desired_shelf, book, privacy)
|
||||
|
||||
if is_api_request(request):
|
||||
return HttpResponse()
|
||||
return redirect(referer)
|
||||
|
||||
|
||||
@login_required
|
||||
@require_POST
|
||||
def edit_readthrough(request):
|
||||
"""can't use the form because the dates are too finnicky"""
|
||||
readthrough = update_readthrough(request, create=False)
|
||||
if not readthrough:
|
||||
return HttpResponseNotFound()
|
||||
@transaction.atomic
|
||||
def update_readthrough_on_shelve(
|
||||
user, annotated_book, status, start_date=None, finish_date=None
|
||||
):
|
||||
"""update the current readthrough for a book when it is re-shelved"""
|
||||
# there *should* only be one of current active readthrough, but it's a list
|
||||
active_readthrough = next(iter(annotated_book.active_readthroughs), None)
|
||||
|
||||
# don't let people edit other people's data
|
||||
if request.user != readthrough.user:
|
||||
return HttpResponseBadRequest()
|
||||
readthrough.save()
|
||||
# deactivate all existing active readthroughs
|
||||
for readthrough in annotated_book.active_readthroughs:
|
||||
readthrough.is_active = False
|
||||
readthrough.save()
|
||||
|
||||
# record the progress update individually
|
||||
# use default now for date field
|
||||
readthrough.create_update()
|
||||
# if the state is want-to-read, deactivating existing readthroughs is all we need
|
||||
if status == models.Shelf.TO_READ:
|
||||
return
|
||||
|
||||
return redirect(request.headers.get("Referer", "/"))
|
||||
# if we're starting a book, we need a fresh clean active readthrough
|
||||
if status == models.Shelf.READING or not active_readthrough:
|
||||
active_readthrough = models.ReadThrough.objects.create(
|
||||
user=user, book=annotated_book
|
||||
)
|
||||
# santiize and set dates
|
||||
active_readthrough.start_date = load_date_in_user_tz_as_utc(start_date, user)
|
||||
# if the finish date is set, the readthrough will be automatically set as inactive
|
||||
active_readthrough.finish_date = load_date_in_user_tz_as_utc(finish_date, user)
|
||||
|
||||
active_readthrough.save()
|
||||
|
||||
|
||||
@login_required
|
||||
|
@ -122,10 +129,7 @@ def edit_readthrough(request):
|
|||
def delete_readthrough(request):
|
||||
"""remove a readthrough"""
|
||||
readthrough = get_object_or_404(models.ReadThrough, id=request.POST.get("id"))
|
||||
|
||||
# don't let people edit other people's data
|
||||
if request.user != readthrough.user:
|
||||
return HttpResponseBadRequest()
|
||||
readthrough.raise_not_deletable(request.user)
|
||||
|
||||
readthrough.delete()
|
||||
return redirect(request.headers.get("Referer", "/"))
|
||||
|
@ -136,73 +140,20 @@ def delete_readthrough(request):
|
|||
def create_readthrough(request):
|
||||
"""can't use the form because the dates are too finnicky"""
|
||||
book = get_object_or_404(models.Edition, id=request.POST.get("book"))
|
||||
readthrough = update_readthrough(request, create=True, book=book)
|
||||
if not readthrough:
|
||||
return redirect(book.local_path)
|
||||
readthrough.save()
|
||||
return redirect(request.headers.get("Referer", "/"))
|
||||
|
||||
|
||||
def load_date_in_user_tz_as_utc(date_str: str, user: models.User) -> datetime:
|
||||
"""ensures that data is stored consistently in the UTC timezone"""
|
||||
user_tz = dateutil.tz.gettz(user.preferred_timezone)
|
||||
start_date = dateutil.parser.parse(date_str, ignoretz=True)
|
||||
return start_date.replace(tzinfo=user_tz).astimezone(dateutil.tz.UTC)
|
||||
|
||||
|
||||
def update_readthrough(request, book=None, create=True):
|
||||
"""updates but does not save dates on a readthrough"""
|
||||
try:
|
||||
read_id = request.POST.get("id")
|
||||
if not read_id:
|
||||
raise models.ReadThrough.DoesNotExist
|
||||
readthrough = models.ReadThrough.objects.get(id=read_id)
|
||||
except models.ReadThrough.DoesNotExist:
|
||||
if not create or not book:
|
||||
return None
|
||||
readthrough = models.ReadThrough(
|
||||
user=request.user,
|
||||
book=book,
|
||||
)
|
||||
|
||||
start_date = request.POST.get("start_date")
|
||||
if start_date:
|
||||
try:
|
||||
readthrough.start_date = load_date_in_user_tz_as_utc(
|
||||
start_date, request.user
|
||||
)
|
||||
except ParserError:
|
||||
pass
|
||||
|
||||
finish_date = request.POST.get("finish_date")
|
||||
if finish_date:
|
||||
try:
|
||||
readthrough.finish_date = load_date_in_user_tz_as_utc(
|
||||
finish_date, request.user
|
||||
)
|
||||
except ParserError:
|
||||
pass
|
||||
|
||||
progress = request.POST.get("progress")
|
||||
if progress:
|
||||
try:
|
||||
progress = int(progress)
|
||||
readthrough.progress = progress
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
progress_mode = request.POST.get("progress_mode")
|
||||
if progress_mode:
|
||||
try:
|
||||
progress_mode = models.ProgressMode(progress_mode)
|
||||
readthrough.progress_mode = progress_mode
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if not readthrough.start_date and not readthrough.finish_date:
|
||||
return None
|
||||
|
||||
return readthrough
|
||||
start_date = load_date_in_user_tz_as_utc(
|
||||
request.POST.get("start_date"), request.user
|
||||
)
|
||||
finish_date = load_date_in_user_tz_as_utc(
|
||||
request.POST.get("finish_date"), request.user
|
||||
)
|
||||
models.ReadThrough.objects.create(
|
||||
user=request.user,
|
||||
book=book,
|
||||
start_date=start_date,
|
||||
finish_date=finish_date,
|
||||
)
|
||||
return redirect("book", book.id)
|
||||
|
||||
|
||||
@login_required
|
||||
|
@ -210,10 +161,7 @@ def update_readthrough(request, book=None, create=True):
|
|||
def delete_progressupdate(request):
|
||||
"""remove a progress update"""
|
||||
update = get_object_or_404(models.ProgressUpdate, id=request.POST.get("id"))
|
||||
|
||||
# don't let people edit other people's data
|
||||
if request.user != update.user:
|
||||
return HttpResponseBadRequest()
|
||||
update.raise_not_deletable(request.user)
|
||||
|
||||
update.delete()
|
||||
return redirect(request.headers.get("Referer", "/"))
|
||||
|
|
|
@ -29,11 +29,11 @@ class Register(View):
|
|||
invite_code = request.POST.get("invite_code")
|
||||
|
||||
if not invite_code:
|
||||
raise PermissionDenied
|
||||
raise PermissionDenied()
|
||||
|
||||
invite = get_object_or_404(models.SiteInvite, code=invite_code)
|
||||
if not invite.valid():
|
||||
raise PermissionDenied
|
||||
raise PermissionDenied()
|
||||
else:
|
||||
invite = None
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ from django.views import View
|
|||
|
||||
from bookwyrm import models
|
||||
from bookwyrm.connectors import connector_manager
|
||||
from bookwyrm.book_search import search, format_search_result
|
||||
from bookwyrm.settings import PAGE_LENGTH
|
||||
from bookwyrm.utils import regex
|
||||
from .helpers import is_api_request, privacy_filter
|
||||
|
@ -31,10 +32,10 @@ class Search(View):
|
|||
|
||||
if is_api_request(request):
|
||||
# only return local book results via json so we don't cascade
|
||||
book_results = connector_manager.local_search(
|
||||
query, min_confidence=min_confidence
|
||||
book_results = search(query, min_confidence=min_confidence)
|
||||
return JsonResponse(
|
||||
[format_search_result(r) for r in book_results], safe=False
|
||||
)
|
||||
return JsonResponse([r.json() for r in book_results], safe=False)
|
||||
|
||||
if query and not search_type:
|
||||
search_type = "user" if "@" in query else "book"
|
||||
|
@ -69,13 +70,13 @@ class Search(View):
|
|||
def book_search(query, _, min_confidence, search_remote=False):
|
||||
"""the real business is elsewhere"""
|
||||
# try a local-only search
|
||||
if not search_remote:
|
||||
results = connector_manager.local_search(query, min_confidence=min_confidence)
|
||||
if results:
|
||||
# gret, we found something
|
||||
return [{"results": results}], False
|
||||
# if there weere no local results, or the request was for remote, search all sources
|
||||
return connector_manager.search(query, min_confidence=min_confidence), True
|
||||
results = [{"results": search(query, min_confidence=min_confidence)}]
|
||||
if results and not search_remote:
|
||||
return results, False
|
||||
|
||||
# if there were no local results, or the request was for remote, search all sources
|
||||
results += connector_manager.search(query, min_confidence=min_confidence)
|
||||
return results, True
|
||||
|
||||
|
||||
def user_search(query, viewer, *_):
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
""" shelf views"""
|
||||
""" shelf views """
|
||||
from collections import namedtuple
|
||||
|
||||
from django.db import IntegrityError
|
||||
from django.db import IntegrityError, transaction
|
||||
from django.db.models import OuterRef, Subquery, F
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.paginator import Paginator
|
||||
from django.http import HttpResponseBadRequest, HttpResponseNotFound
|
||||
from django.http import HttpResponseBadRequest
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.template.response import TemplateResponse
|
||||
from django.utils.decorators import method_decorator
|
||||
|
@ -16,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 .helpers import is_api_request, get_edition, get_user_from_username
|
||||
from .helpers import is_api_request, get_user_from_username
|
||||
from .helpers import privacy_filter
|
||||
|
||||
|
||||
|
@ -31,28 +31,28 @@ class Shelf(View):
|
|||
is_self = user == request.user
|
||||
|
||||
if is_self:
|
||||
shelves = user.shelf_set
|
||||
shelves = user.shelf_set.all()
|
||||
else:
|
||||
shelves = privacy_filter(request.user, user.shelf_set)
|
||||
shelves = privacy_filter(request.user, user.shelf_set).all()
|
||||
|
||||
# get the shelf and make sure the logged in user should be able to see it
|
||||
if shelf_identifier:
|
||||
try:
|
||||
shelf = user.shelf_set.get(identifier=shelf_identifier)
|
||||
except models.Shelf.DoesNotExist:
|
||||
return HttpResponseNotFound()
|
||||
if not shelf.visible_to_user(request.user):
|
||||
return HttpResponseNotFound()
|
||||
shelf = get_object_or_404(user.shelf_set, identifier=shelf_identifier)
|
||||
shelf.raise_visible_to_user(request.user)
|
||||
books = shelf.books
|
||||
# this is a constructed "all books" view, with a fake "shelf" obj
|
||||
else:
|
||||
# this is a constructed "all books" view, with a fake "shelf" obj
|
||||
FakeShelf = namedtuple(
|
||||
"Shelf", ("identifier", "name", "user", "books", "privacy")
|
||||
)
|
||||
books = models.Edition.objects.filter(
|
||||
# privacy is ensured because the shelves are already filtered above
|
||||
shelfbook__shelf__in=shelves.all()
|
||||
).distinct()
|
||||
books = (
|
||||
models.Edition.viewer_aware_objects(request.user)
|
||||
.filter(
|
||||
# privacy is ensured because the shelves are already filtered above
|
||||
shelfbook__shelf__in=shelves
|
||||
)
|
||||
.distinct()
|
||||
)
|
||||
shelf = FakeShelf("all", _("All books"), user, books, "public")
|
||||
|
||||
if is_api_request(request):
|
||||
|
@ -82,27 +82,27 @@ class Shelf(View):
|
|||
data = {
|
||||
"user": user,
|
||||
"is_self": is_self,
|
||||
"shelves": shelves.all(),
|
||||
"shelves": shelves,
|
||||
"shelf": shelf,
|
||||
"books": page,
|
||||
"edit_form": forms.ShelfForm(instance=shelf if shelf_identifier else None),
|
||||
"create_form": forms.ShelfForm(),
|
||||
"page_range": paginated.get_elided_page_range(
|
||||
page.number, on_each_side=2, on_ends=1
|
||||
),
|
||||
}
|
||||
|
||||
return TemplateResponse(request, "user/shelf/shelf.html", data)
|
||||
return TemplateResponse(request, "shelf/shelf.html", data)
|
||||
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
# pylint: disable=unused-argument
|
||||
def post(self, request, username, shelf_identifier):
|
||||
"""edit a shelf"""
|
||||
try:
|
||||
shelf = request.user.shelf_set.get(identifier=shelf_identifier)
|
||||
except models.Shelf.DoesNotExist:
|
||||
return HttpResponseNotFound()
|
||||
user = get_user_from_username(request.user, username)
|
||||
shelf = get_object_or_404(user.shelf_set, identifier=shelf_identifier)
|
||||
shelf.raise_not_editable(request.user)
|
||||
|
||||
if request.user != shelf.user:
|
||||
return HttpResponseBadRequest()
|
||||
# you can't change the name of the default shelves
|
||||
if not shelf.editable and request.POST.get("name") != shelf.name:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
|
@ -130,8 +130,7 @@ def create_shelf(request):
|
|||
def delete_shelf(request, shelf_id):
|
||||
"""user generated shelves"""
|
||||
shelf = get_object_or_404(models.Shelf, id=shelf_id)
|
||||
if request.user != shelf.user or not shelf.editable:
|
||||
return HttpResponseBadRequest()
|
||||
shelf.raise_not_deletable(request.user)
|
||||
|
||||
shelf.delete()
|
||||
return redirect("user-shelves", request.user.localname)
|
||||
|
@ -139,25 +138,28 @@ def delete_shelf(request, shelf_id):
|
|||
|
||||
@login_required
|
||||
@require_POST
|
||||
@transaction.atomic
|
||||
def shelve(request):
|
||||
"""put a book on a user's shelf"""
|
||||
book = get_edition(request.POST.get("book"))
|
||||
|
||||
desired_shelf = models.Shelf.objects.filter(
|
||||
identifier=request.POST.get("shelf"), user=request.user
|
||||
).first()
|
||||
if not desired_shelf:
|
||||
return HttpResponseNotFound()
|
||||
book = get_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")
|
||||
)
|
||||
|
||||
# first we need to remove from the specified shelf
|
||||
change_from_current_identifier = request.POST.get("change-shelf-from")
|
||||
if change_from_current_identifier is not None:
|
||||
current_shelf = models.Shelf.objects.get(
|
||||
user=request.user, identifier=change_from_current_identifier
|
||||
)
|
||||
handle_unshelve(book, current_shelf)
|
||||
if change_from_current_identifier:
|
||||
# find the shelfbook obj and delete it
|
||||
get_object_or_404(
|
||||
models.ShelfBook,
|
||||
book=book,
|
||||
user=request.user,
|
||||
shelf__identifier=change_from_current_identifier,
|
||||
).delete()
|
||||
|
||||
# A book can be on multiple shelves, but only on one read status shelf at a time
|
||||
if desired_shelf.identifier in models.Shelf.READ_STATUS_IDENTIFIERS:
|
||||
# figure out where state shelf it's currently on (if any)
|
||||
current_read_status_shelfbook = (
|
||||
models.ShelfBook.objects.select_related("shelf")
|
||||
.filter(
|
||||
|
@ -172,14 +174,16 @@ def shelve(request):
|
|||
current_read_status_shelfbook.shelf.identifier
|
||||
!= desired_shelf.identifier
|
||||
):
|
||||
handle_unshelve(book, current_read_status_shelfbook.shelf)
|
||||
current_read_status_shelfbook.delete()
|
||||
else: # It is already on the shelf
|
||||
return redirect(request.headers.get("Referer", "/"))
|
||||
|
||||
# create the new shelf-book entry
|
||||
models.ShelfBook.objects.create(
|
||||
book=book, shelf=desired_shelf, user=request.user
|
||||
)
|
||||
else:
|
||||
# we're putting it on a custom shelf
|
||||
try:
|
||||
models.ShelfBook.objects.create(
|
||||
book=book, shelf=desired_shelf, user=request.user
|
||||
|
@ -194,15 +198,12 @@ def shelve(request):
|
|||
@login_required
|
||||
@require_POST
|
||||
def unshelve(request):
|
||||
"""put a on a user's shelf"""
|
||||
book = models.Edition.objects.get(id=request.POST["book"])
|
||||
current_shelf = models.Shelf.objects.get(id=request.POST["shelf"])
|
||||
"""put a on a user's shelf"""
|
||||
book = get_object_or_404(models.Edition, id=request.POST.get("book"))
|
||||
shelf_book = get_object_or_404(
|
||||
models.ShelfBook, book=book, shelf__id=request.POST["shelf"]
|
||||
)
|
||||
shelf_book.raise_not_deletable(request.user)
|
||||
|
||||
handle_unshelve(book, current_shelf)
|
||||
shelf_book.delete()
|
||||
return redirect(request.headers.get("Referer", "/"))
|
||||
|
||||
|
||||
def handle_unshelve(book, shelf):
|
||||
"""unshelve a book"""
|
||||
row = models.ShelfBook.objects.get(book=book, shelf=shelf)
|
||||
row.delete()
|
||||
|
|
|
@ -5,11 +5,12 @@ from urllib.parse import urlparse
|
|||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.validators import URLValidator
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.http import HttpResponse, HttpResponseBadRequest
|
||||
from django.http import HttpResponse, HttpResponseBadRequest, Http404
|
||||
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 markdown import markdown
|
||||
from bookwyrm import forms, models
|
||||
|
@ -17,7 +18,7 @@ from bookwyrm.sanitize_html import InputHtmlParser
|
|||
from bookwyrm.settings import DOMAIN
|
||||
from bookwyrm.utils import regex
|
||||
from .helpers import handle_remote_webfinger, is_api_request
|
||||
from .reading import edit_readthrough
|
||||
from .helpers import load_date_in_user_tz_as_utc
|
||||
|
||||
|
||||
# pylint: disable= no-self-use
|
||||
|
@ -79,7 +80,10 @@ class CreateStatus(View):
|
|||
status.save(created=True)
|
||||
|
||||
# update a readthorugh, if needed
|
||||
edit_readthrough(request)
|
||||
try:
|
||||
edit_readthrough(request)
|
||||
except Http404:
|
||||
pass
|
||||
|
||||
if is_api_request(request):
|
||||
return HttpResponse()
|
||||
|
@ -95,8 +99,7 @@ class DeleteStatus(View):
|
|||
status = get_object_or_404(models.Status, id=status_id)
|
||||
|
||||
# don't let people delete other people's statuses
|
||||
if status.user != request.user and not request.user.has_perm("moderate_post"):
|
||||
return HttpResponseBadRequest()
|
||||
status.raise_not_deletable(request.user)
|
||||
|
||||
# perform deletion
|
||||
status.delete()
|
||||
|
@ -112,12 +115,8 @@ class DeleteAndRedraft(View):
|
|||
status = get_object_or_404(
|
||||
models.Status.objects.select_subclasses(), id=status_id
|
||||
)
|
||||
if isinstance(status, (models.GeneratedNote, models.ReviewRating)):
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
# don't let people redraft other people's statuses
|
||||
if status.user != request.user:
|
||||
return HttpResponseBadRequest()
|
||||
status.raise_not_editable(request.user)
|
||||
|
||||
status_type = status.status_type.lower()
|
||||
if status.reply_parent:
|
||||
|
@ -137,6 +136,54 @@ class DeleteAndRedraft(View):
|
|||
return TemplateResponse(request, "compose.html", data)
|
||||
|
||||
|
||||
@login_required
|
||||
@require_POST
|
||||
def update_progress(request, book_id): # pylint: disable=unused-argument
|
||||
"""Either it's just a progress update, or it's a comment with a progress update"""
|
||||
if request.POST.get("post-status"):
|
||||
return CreateStatus.as_view()(request, "comment")
|
||||
return edit_readthrough(request)
|
||||
|
||||
|
||||
@login_required
|
||||
@require_POST
|
||||
def edit_readthrough(request):
|
||||
"""can't use the form because the dates are too finnicky"""
|
||||
readthrough = get_object_or_404(models.ReadThrough, id=request.POST.get("id"))
|
||||
readthrough.raise_not_editable(request.user)
|
||||
|
||||
readthrough.start_date = load_date_in_user_tz_as_utc(
|
||||
request.POST.get("start_date"), request.user
|
||||
)
|
||||
readthrough.finish_date = load_date_in_user_tz_as_utc(
|
||||
request.POST.get("finish_date"), request.user
|
||||
)
|
||||
|
||||
progress = request.POST.get("progress")
|
||||
try:
|
||||
progress = int(progress)
|
||||
readthrough.progress = progress
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
progress_mode = request.POST.get("progress_mode")
|
||||
try:
|
||||
progress_mode = models.ProgressMode(progress_mode)
|
||||
readthrough.progress_mode = progress_mode
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
readthrough.save()
|
||||
|
||||
# record the progress update individually
|
||||
# use default now for date field
|
||||
readthrough.create_update()
|
||||
|
||||
if is_api_request(request):
|
||||
return HttpResponse()
|
||||
return redirect(request.headers.get("Referer", "/"))
|
||||
|
||||
|
||||
def find_mentions(content):
|
||||
"""detect @mentions in raw status content"""
|
||||
if not content:
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
""" non-interactive pages """
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.paginator import Paginator
|
||||
from django.http import Http404
|
||||
from django.shortcuts import redirect
|
||||
from django.template.response import TemplateResponse
|
||||
from django.utils import timezone
|
||||
|
@ -77,8 +78,12 @@ class User(View):
|
|||
goal = models.AnnualGoal.objects.filter(
|
||||
user=user, year=timezone.now().year
|
||||
).first()
|
||||
if goal and not goal.visible_to_user(request.user):
|
||||
goal = None
|
||||
if goal:
|
||||
try:
|
||||
goal.raise_visible_to_user(request.user)
|
||||
except Http404:
|
||||
goal = None
|
||||
|
||||
data = {
|
||||
"user": user,
|
||||
"is_self": is_self,
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
from dateutil.relativedelta import relativedelta
|
||||
from django.http import HttpResponseNotFound
|
||||
from django.http import JsonResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.template.response import TemplateResponse
|
||||
from django.utils import timezone
|
||||
from django.views.decorators.http import require_GET
|
||||
|
@ -19,10 +20,7 @@ def webfinger(request):
|
|||
return HttpResponseNotFound()
|
||||
|
||||
username = resource.replace("acct:", "")
|
||||
try:
|
||||
user = models.User.objects.get(username__iexact=username)
|
||||
except models.User.DoesNotExist:
|
||||
return HttpResponseNotFound("No account found")
|
||||
user = get_object_or_404(models.User, username__iexact=username)
|
||||
|
||||
return JsonResponse(
|
||||
{
|
||||
|
@ -130,3 +128,14 @@ def peers(_):
|
|||
def host_meta(request):
|
||||
"""meta of the host"""
|
||||
return TemplateResponse(request, "host_meta.xml", {"DOMAIN": DOMAIN})
|
||||
|
||||
|
||||
@require_GET
|
||||
def opensearch(request):
|
||||
"""Open Search xml spec"""
|
||||
site = models.SiteSettings.get()
|
||||
logo_path = site.favicon or "images/favicon.png"
|
||||
logo = f"{MEDIA_FULL_URL}{logo_path}"
|
||||
return TemplateResponse(
|
||||
request, "opensearch.xml", {"image": logo, "DOMAIN": DOMAIN}
|
||||
)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue