Merge branch 'main' into suggestions-redis
This commit is contained in:
@ -1,4 +1,5 @@
|
||||
""" make sure all our nice views are available """
|
||||
from .announcements import Announcements, Announcement, delete_announcement
|
||||
from .authentication import Login, Register, Logout
|
||||
from .author import Author, EditAuthor
|
||||
from .block import Block, unblock
|
||||
|
97
bookwyrm/views/announcements.py
Normal file
97
bookwyrm/views/announcements.py
Normal file
@ -0,0 +1,97 @@
|
||||
""" make announcements """
|
||||
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
|
||||
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.settings import PAGE_LENGTH
|
||||
|
||||
|
||||
# pylint: disable=no-self-use
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
@method_decorator(
|
||||
permission_required("bookwyrm.edit_instance_settings", raise_exception=True),
|
||||
name="dispatch",
|
||||
)
|
||||
class Announcements(View):
|
||||
"""tell everyone"""
|
||||
|
||||
def get(self, request):
|
||||
"""view and create announcements"""
|
||||
announcements = models.Announcement.objects
|
||||
|
||||
sort = request.GET.get("sort", "-created_date")
|
||||
sort_fields = [
|
||||
"created_date",
|
||||
"preview",
|
||||
"start_date",
|
||||
"end_date",
|
||||
"active",
|
||||
]
|
||||
if sort in sort_fields + ["-{:s}".format(f) for f in sort_fields]:
|
||||
announcements = announcements.order_by(sort)
|
||||
data = {
|
||||
"announcements": Paginator(announcements, PAGE_LENGTH).get_page(
|
||||
request.GET.get("page")
|
||||
),
|
||||
"form": forms.AnnouncementForm(),
|
||||
"sort": sort,
|
||||
}
|
||||
return TemplateResponse(request, "settings/announcements.html", data)
|
||||
|
||||
def post(self, request):
|
||||
"""edit the site settings"""
|
||||
form = forms.AnnouncementForm(request.POST)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
# reset the create form
|
||||
form = forms.AnnouncementForm()
|
||||
data = {
|
||||
"announcements": Paginator(
|
||||
models.Announcement.objects.all(), PAGE_LENGTH
|
||||
).get_page(request.GET.get("page")),
|
||||
"form": form,
|
||||
}
|
||||
return TemplateResponse(request, "settings/announcements.html", data)
|
||||
|
||||
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
@method_decorator(
|
||||
permission_required("bookwyrm.edit_instance_settings", raise_exception=True),
|
||||
name="dispatch",
|
||||
)
|
||||
class Announcement(View):
|
||||
"""edit an announcement"""
|
||||
|
||||
def get(self, request, announcement_id):
|
||||
"""view announcement"""
|
||||
announcement = get_object_or_404(models.Announcement, id=announcement_id)
|
||||
data = {
|
||||
"announcement": announcement,
|
||||
"form": forms.AnnouncementForm(instance=announcement),
|
||||
}
|
||||
return TemplateResponse(request, "settings/announcement.html", data)
|
||||
|
||||
def post(self, request, announcement_id):
|
||||
"""edit announcement"""
|
||||
announcement = get_object_or_404(models.Announcement, id=announcement_id)
|
||||
form = forms.AnnouncementForm(request.POST, instance=announcement)
|
||||
if form.is_valid():
|
||||
announcement = form.save()
|
||||
data = {
|
||||
"announcement": announcement,
|
||||
"form": form,
|
||||
}
|
||||
return TemplateResponse(request, "settings/announcement.html", data)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required("bookwyrm.edit_instance_settings", raise_exception=True)
|
||||
def delete_announcement(_, announcement_id):
|
||||
"""delete announcement"""
|
||||
announcement = get_object_or_404(models.Announcement, id=announcement_id)
|
||||
announcement.delete()
|
||||
return redirect("settings-announcements")
|
@ -27,9 +27,9 @@ class Author(View):
|
||||
).distinct()
|
||||
data = {
|
||||
"author": author,
|
||||
"books": [b.get_default_edition() for b in books],
|
||||
"books": [b.default_edition for b in books],
|
||||
}
|
||||
return TemplateResponse(request, "author.html", data)
|
||||
return TemplateResponse(request, "author/author.html", data)
|
||||
|
||||
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
@ -43,7 +43,7 @@ class EditAuthor(View):
|
||||
"""info about a book"""
|
||||
author = get_object_or_404(models.Author, id=author_id)
|
||||
data = {"author": author, "form": forms.AuthorForm(instance=author)}
|
||||
return TemplateResponse(request, "edit_author.html", data)
|
||||
return TemplateResponse(request, "author/edit_author.html", data)
|
||||
|
||||
def post(self, request, author_id):
|
||||
"""edit a author cool"""
|
||||
@ -52,7 +52,7 @@ class EditAuthor(View):
|
||||
form = forms.AuthorForm(request.POST, request.FILES, instance=author)
|
||||
if not form.is_valid():
|
||||
data = {"author": author, "form": form}
|
||||
return TemplateResponse(request, "edit_author.html", data)
|
||||
return TemplateResponse(request, "author/edit_author.html", data)
|
||||
author = form.save()
|
||||
|
||||
return redirect("/author/%s" % author.id)
|
||||
|
@ -30,6 +30,7 @@ class Book(View):
|
||||
|
||||
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:
|
||||
@ -39,7 +40,7 @@ class Book(View):
|
||||
return ActivitypubResponse(book.to_activity())
|
||||
|
||||
if isinstance(book, models.Work):
|
||||
book = book.get_default_edition()
|
||||
book = book.default_edition
|
||||
if not book or not book.parent_work:
|
||||
return HttpResponseNotFound()
|
||||
|
||||
@ -51,9 +52,9 @@ class Book(View):
|
||||
)
|
||||
|
||||
# the reviews to show
|
||||
if user_statuses and request.user.is_authenticated:
|
||||
if user_statuses:
|
||||
if user_statuses == "review":
|
||||
queryset = book.review_set
|
||||
queryset = book.review_set.select_subclasses()
|
||||
elif user_statuses == "comment":
|
||||
queryset = book.comment_set
|
||||
else:
|
||||
@ -67,7 +68,9 @@ 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="")),
|
||||
"ratings": reviews.filter(Q(content__isnull=True) | Q(content=""))
|
||||
if not user_statuses
|
||||
else None,
|
||||
"rating": reviews.aggregate(Avg("rating"))["rating__avg"],
|
||||
"lists": privacy_filter(
|
||||
request.user, book.list_set.filter(listitem__approved=True)
|
||||
@ -156,7 +159,6 @@ class EditBook(View):
|
||||
),
|
||||
}
|
||||
)
|
||||
print(data["author_matches"])
|
||||
|
||||
# we're creating a new book
|
||||
if not book:
|
||||
@ -317,7 +319,10 @@ def upload_cover(request, book_id):
|
||||
|
||||
def set_cover_from_url(url):
|
||||
"""load it from a url"""
|
||||
image_file = get_image(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]
|
||||
|
@ -2,7 +2,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
|
||||
from django.http import HttpResponseNotFound, Http404
|
||||
from django.template.response import TemplateResponse
|
||||
from django.utils import timezone
|
||||
from django.utils.decorators import method_decorator
|
||||
@ -63,7 +63,7 @@ class DirectMessage(View):
|
||||
if username:
|
||||
try:
|
||||
user = get_user_from_username(request.user, username)
|
||||
except models.User.DoesNotExist:
|
||||
except Http404:
|
||||
pass
|
||||
if user:
|
||||
queryset = queryset.filter(Q(user=user) | Q(mention_users=user))
|
||||
@ -95,7 +95,7 @@ class Status(View):
|
||||
status = models.Status.objects.select_subclasses().get(
|
||||
id=status_id, deleted=False
|
||||
)
|
||||
except (ValueError, models.Status.DoesNotExist, models.User.DoesNotExist):
|
||||
except (ValueError, models.Status.DoesNotExist):
|
||||
return HttpResponseNotFound()
|
||||
|
||||
# the url should have the poster's username in it
|
||||
|
@ -14,10 +14,7 @@ from .helpers import get_user_from_username
|
||||
def follow(request):
|
||||
"""follow another user, here or abroad"""
|
||||
username = request.POST["user"]
|
||||
try:
|
||||
to_follow = get_user_from_username(request.user, username)
|
||||
except models.User.DoesNotExist:
|
||||
return HttpResponseBadRequest()
|
||||
to_follow = get_user_from_username(request.user, username)
|
||||
|
||||
try:
|
||||
models.UserFollowRequest.objects.create(
|
||||
@ -35,10 +32,7 @@ def follow(request):
|
||||
def unfollow(request):
|
||||
"""unfollow a user"""
|
||||
username = request.POST["user"]
|
||||
try:
|
||||
to_unfollow = get_user_from_username(request.user, username)
|
||||
except models.User.DoesNotExist:
|
||||
return HttpResponseBadRequest()
|
||||
to_unfollow = get_user_from_username(request.user, username)
|
||||
|
||||
try:
|
||||
models.UserFollows.objects.get(
|
||||
@ -63,10 +57,7 @@ def unfollow(request):
|
||||
def accept_follow_request(request):
|
||||
"""a user accepts a follow request"""
|
||||
username = request.POST["user"]
|
||||
try:
|
||||
requester = get_user_from_username(request.user, username)
|
||||
except models.User.DoesNotExist:
|
||||
return HttpResponseBadRequest()
|
||||
requester = get_user_from_username(request.user, username)
|
||||
|
||||
try:
|
||||
follow_request = models.UserFollowRequest.objects.get(
|
||||
@ -85,10 +76,7 @@ def accept_follow_request(request):
|
||||
def delete_follow_request(request):
|
||||
"""a user rejects a follow request"""
|
||||
username = request.POST["user"]
|
||||
try:
|
||||
requester = get_user_from_username(request.user, username)
|
||||
except models.User.DoesNotExist:
|
||||
return HttpResponseBadRequest()
|
||||
requester = get_user_from_username(request.user, username)
|
||||
|
||||
try:
|
||||
follow_request = models.UserFollowRequest.objects.get(
|
||||
|
@ -119,9 +119,10 @@ class GetStartedUsers(View):
|
||||
)
|
||||
|
||||
if user_results.count() < 5:
|
||||
suggested_users = None # get_suggested_users(request.user)
|
||||
suggested_users = [] # TODO: get_suggested_users(request.user)
|
||||
user_results = list(user_results) + list(suggested_users))
|
||||
|
||||
data = {
|
||||
"suggested_users": list(user_results) + list(suggested_users),
|
||||
"suggested_users": user_results,
|
||||
}
|
||||
return TemplateResponse(request, "get_started/users.html", data)
|
||||
return TemplateResponse(request, "get_started/users.html", data)
|
@ -3,6 +3,7 @@ import re
|
||||
from requests import HTTPError
|
||||
from django.core.exceptions import FieldError
|
||||
from django.db.models import Count, Max, Q
|
||||
from django.http import Http404
|
||||
|
||||
from bookwyrm import activitypub, models
|
||||
from bookwyrm.connectors import ConnectorException, get_data
|
||||
@ -12,11 +13,17 @@ from bookwyrm.utils import regex
|
||||
|
||||
def get_user_from_username(viewer, username):
|
||||
"""helper function to resolve a localname or a username to a user"""
|
||||
# raises DoesNotExist if user is now found
|
||||
# raises 404 if the user isn't found
|
||||
try:
|
||||
return models.User.viewer_aware_objects(viewer).get(localname=username)
|
||||
except models.User.DoesNotExist:
|
||||
pass
|
||||
|
||||
# if the localname didn't match, try the username
|
||||
try:
|
||||
return models.User.viewer_aware_objects(viewer).get(username=username)
|
||||
except models.User.DoesNotExist:
|
||||
raise Http404()
|
||||
|
||||
|
||||
def is_api_request(request):
|
||||
@ -123,7 +130,7 @@ def get_edition(book_id):
|
||||
"""look up a book in the db and return an edition"""
|
||||
book = models.Book.objects.select_subclasses().get(id=book_id)
|
||||
if isinstance(book, models.Work):
|
||||
book = book.get_default_edition()
|
||||
book = book.default_edition
|
||||
return book
|
||||
|
||||
|
||||
@ -165,4 +172,4 @@ def get_discover_books():
|
||||
.annotate(Max("review__published_date"))
|
||||
.order_by("-review__published_date__max")[:6]
|
||||
)
|
||||
)
|
||||
)
|
@ -7,10 +7,16 @@ 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
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views import View
|
||||
|
||||
from bookwyrm import forms, models
|
||||
from bookwyrm.importers import Importer, LibrarythingImporter, GoodreadsImporter
|
||||
from bookwyrm.importers import (
|
||||
Importer,
|
||||
LibrarythingImporter,
|
||||
GoodreadsImporter,
|
||||
StorygraphImporter,
|
||||
)
|
||||
from bookwyrm.tasks import app
|
||||
|
||||
# pylint: disable= no-self-use
|
||||
@ -42,6 +48,8 @@ class Import(View):
|
||||
importer = None
|
||||
if source == "LibraryThing":
|
||||
importer = LibrarythingImporter()
|
||||
elif source == "Storygraph":
|
||||
importer = StorygraphImporter()
|
||||
else:
|
||||
# Default : GoodReads
|
||||
importer = GoodreadsImporter()
|
||||
@ -55,8 +63,8 @@ class Import(View):
|
||||
include_reviews,
|
||||
privacy,
|
||||
)
|
||||
except (UnicodeDecodeError, ValueError):
|
||||
return HttpResponseBadRequest("Not a valid csv file")
|
||||
except (UnicodeDecodeError, ValueError, KeyError):
|
||||
return HttpResponseBadRequest(_("Not a valid csv file"))
|
||||
|
||||
importer.start_import(job)
|
||||
|
||||
|
@ -1,14 +1,16 @@
|
||||
""" book list views"""
|
||||
from typing import Optional
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.paginator import Paginator
|
||||
from django.db import IntegrityError, transaction
|
||||
from django.db.models import Avg, Count, Q, Max
|
||||
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.shortcuts import get_object_or_404, redirect
|
||||
from django.template.response import TemplateResponse
|
||||
from django.urls import reverse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views import View
|
||||
from django.views.decorators.http import require_POST
|
||||
@ -16,6 +18,7 @@ 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.settings import PAGE_LENGTH
|
||||
from .helpers import is_api_request, privacy_filter
|
||||
from .helpers import get_user_from_username
|
||||
|
||||
@ -105,37 +108,33 @@ class List(View):
|
||||
if direction not in ("ascending", "descending"):
|
||||
direction = "ascending"
|
||||
|
||||
internal_sort_by = {
|
||||
directional_sort_by = {
|
||||
"order": "order",
|
||||
"title": "book__title",
|
||||
"rating": "average_rating",
|
||||
}
|
||||
directional_sort_by = internal_sort_by[sort_by]
|
||||
}[sort_by]
|
||||
if direction == "descending":
|
||||
directional_sort_by = "-" + directional_sort_by
|
||||
|
||||
if sort_by == "order":
|
||||
items = book_list.listitem_set.filter(approved=True).order_by(
|
||||
directional_sort_by
|
||||
)
|
||||
elif sort_by == "title":
|
||||
items = book_list.listitem_set.filter(approved=True).order_by(
|
||||
directional_sort_by
|
||||
)
|
||||
elif sort_by == "rating":
|
||||
items = (
|
||||
book_list.listitem_set.annotate(
|
||||
average_rating=Avg(Coalesce("book__review__rating", 0))
|
||||
items = book_list.listitem_set
|
||||
if sort_by == "rating":
|
||||
items = items.annotate(
|
||||
average_rating=Avg(
|
||||
Coalesce("book__review__rating", 0.0),
|
||||
output_field=DecimalField(),
|
||||
)
|
||||
.filter(approved=True)
|
||||
.order_by(directional_sort_by)
|
||||
)
|
||||
items = items.filter(approved=True).order_by(directional_sort_by)
|
||||
|
||||
paginated = Paginator(items, 12)
|
||||
paginated = Paginator(items, PAGE_LENGTH)
|
||||
|
||||
if query and request.user.is_authenticated:
|
||||
# search for books
|
||||
suggestions = connector_manager.local_search(query, raw=True)
|
||||
suggestions = connector_manager.local_search(
|
||||
query,
|
||||
raw=True,
|
||||
filters=[~Q(parent_work__editions__in=book_list.books.all())],
|
||||
)
|
||||
elif request.user.is_authenticated:
|
||||
# just suggest whatever books are nearby
|
||||
suggestions = request.user.shelfbook_set.filter(
|
||||
@ -150,9 +149,13 @@ class List(View):
|
||||
).order_by("-updated_date")
|
||||
][: 5 - len(suggestions)]
|
||||
|
||||
page = paginated.get_page(request.GET.get("page"))
|
||||
data = {
|
||||
"list": book_list,
|
||||
"items": paginated.get_page(request.GET.get("page")),
|
||||
"items": page,
|
||||
"page_range": paginated.get_elided_page_range(
|
||||
page.number, on_each_side=2, on_ends=1
|
||||
),
|
||||
"pending_count": book_list.listitem_set.filter(approved=False).count(),
|
||||
"suggested_books": suggestions,
|
||||
"list_form": forms.ListForm(instance=book_list),
|
||||
@ -263,7 +266,10 @@ def add_book(request):
|
||||
# if the book is already on the list, don't flip out
|
||||
pass
|
||||
|
||||
return redirect("list", book_list.id)
|
||||
path = reverse("list", args=[book_list.id])
|
||||
params = request.GET.copy()
|
||||
params["updated"] = True
|
||||
return redirect("{:s}?{:s}".format(path, urlencode(params)))
|
||||
|
||||
|
||||
@require_POST
|
||||
|
@ -11,12 +11,16 @@ from django.views import View
|
||||
class Notifications(View):
|
||||
"""notifications view"""
|
||||
|
||||
def get(self, request):
|
||||
def get(self, request, notification_type=None):
|
||||
"""people are interacting with you, get hyped"""
|
||||
notifications = request.user.notification_set.all().order_by("-created_date")
|
||||
unread = [n.id for n in notifications.filter(read=False)]
|
||||
if notification_type == "mentions":
|
||||
notifications = notifications.filter(
|
||||
notification_type__in=["REPLY", "MENTION", "TAG"]
|
||||
)
|
||||
unread = [n.id for n in notifications.filter(read=False)[:50]]
|
||||
data = {
|
||||
"notifications": notifications,
|
||||
"notifications": notifications[:50],
|
||||
"unread": unread,
|
||||
}
|
||||
notifications.update(read=True)
|
||||
|
@ -27,7 +27,7 @@ class PasswordResetRequest(View):
|
||||
"""create a password reset token"""
|
||||
email = request.POST.get("email")
|
||||
try:
|
||||
user = models.User.objects.get(email=email)
|
||||
user = models.User.objects.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)
|
||||
|
@ -7,8 +7,8 @@ from .helpers import get_user_from_username, privacy_filter
|
||||
class RssFeed(Feed):
|
||||
"""serialize user's posts in rss feed"""
|
||||
|
||||
description_template = "snippets/rss_content.html"
|
||||
title_template = "snippets/rss_title.html"
|
||||
description_template = "rss/content.html"
|
||||
title_template = "rss/title.html"
|
||||
|
||||
def get_object(self, request, username):
|
||||
"""the user who's posts get serialized"""
|
||||
|
@ -2,6 +2,7 @@
|
||||
import re
|
||||
|
||||
from django.contrib.postgres.search import TrigramSimilarity
|
||||
from django.core.paginator import Paginator
|
||||
from django.db.models.functions import Greatest
|
||||
from django.http import JsonResponse
|
||||
from django.template.response import TemplateResponse
|
||||
@ -9,6 +10,7 @@ from django.views import View
|
||||
|
||||
from bookwyrm import models
|
||||
from bookwyrm.connectors import connector_manager
|
||||
from bookwyrm.settings import PAGE_LENGTH
|
||||
from bookwyrm.utils import regex
|
||||
from .helpers import is_api_request, privacy_filter
|
||||
from .helpers import handle_remote_webfinger
|
||||
@ -22,6 +24,10 @@ class Search(View):
|
||||
"""that search bar up top"""
|
||||
query = request.GET.get("q")
|
||||
min_confidence = request.GET.get("min_confidence", 0.1)
|
||||
search_type = request.GET.get("type")
|
||||
search_remote = (
|
||||
request.GET.get("remote", False) and request.user.is_authenticated
|
||||
)
|
||||
|
||||
if is_api_request(request):
|
||||
# only return local book results via json so we don't cascade
|
||||
@ -30,49 +36,87 @@ class Search(View):
|
||||
)
|
||||
return JsonResponse([r.json() for r in book_results], safe=False)
|
||||
|
||||
# use webfinger for mastodon style account@domain.com username
|
||||
if query and re.match(regex.full_username, query):
|
||||
handle_remote_webfinger(query)
|
||||
if query and not search_type:
|
||||
search_type = "user" if "@" in query else "book"
|
||||
|
||||
# do a user search
|
||||
user_results = (
|
||||
models.User.viewer_aware_objects(request.user)
|
||||
.annotate(
|
||||
similarity=Greatest(
|
||||
TrigramSimilarity("username", query),
|
||||
TrigramSimilarity("localname", query),
|
||||
)
|
||||
)
|
||||
.filter(
|
||||
similarity__gt=0.5,
|
||||
)
|
||||
.order_by("-similarity")[:10]
|
||||
)
|
||||
|
||||
# any relevent lists?
|
||||
list_results = (
|
||||
privacy_filter(
|
||||
request.user,
|
||||
models.List.objects,
|
||||
privacy_levels=["public", "followers"],
|
||||
)
|
||||
.annotate(
|
||||
similarity=Greatest(
|
||||
TrigramSimilarity("name", query),
|
||||
TrigramSimilarity("description", query),
|
||||
)
|
||||
)
|
||||
.filter(
|
||||
similarity__gt=0.1,
|
||||
)
|
||||
.order_by("-similarity")[:10]
|
||||
)
|
||||
|
||||
book_results = connector_manager.search(query, min_confidence=min_confidence)
|
||||
data = {
|
||||
"book_results": book_results,
|
||||
"user_results": user_results,
|
||||
"list_results": list_results,
|
||||
"query": query or "",
|
||||
endpoints = {
|
||||
"book": book_search,
|
||||
"user": user_search,
|
||||
"list": list_search,
|
||||
}
|
||||
return TemplateResponse(request, "search_results.html", data)
|
||||
if not search_type in endpoints:
|
||||
search_type = "book"
|
||||
|
||||
data = {
|
||||
"query": query or "",
|
||||
"type": search_type,
|
||||
"remote": search_remote,
|
||||
}
|
||||
if query:
|
||||
results = endpoints[search_type](
|
||||
query, request.user, min_confidence, search_remote
|
||||
)
|
||||
if results:
|
||||
paginated = Paginator(results, PAGE_LENGTH).get_page(
|
||||
request.GET.get("page")
|
||||
)
|
||||
data["results"] = paginated
|
||||
|
||||
return TemplateResponse(request, "search/{:s}.html".format(search_type), data)
|
||||
|
||||
|
||||
def book_search(query, _, min_confidence, search_remote=False):
|
||||
"""the real business is elsewhere"""
|
||||
if search_remote:
|
||||
return connector_manager.search(query, min_confidence=min_confidence)
|
||||
results = connector_manager.local_search(query, min_confidence=min_confidence)
|
||||
if not results:
|
||||
return None
|
||||
return [{"results": results}]
|
||||
|
||||
|
||||
def user_search(query, viewer, *_):
|
||||
"""cool kids members only user search"""
|
||||
# logged out viewers can't search users
|
||||
if not viewer.is_authenticated:
|
||||
return models.User.objects.none()
|
||||
|
||||
# use webfinger for mastodon style account@domain.com username to load the user if
|
||||
# they don't exist locally (handle_remote_webfinger will check the db)
|
||||
if re.match(regex.full_username, query):
|
||||
handle_remote_webfinger(query)
|
||||
|
||||
return (
|
||||
models.User.viewer_aware_objects(viewer)
|
||||
.annotate(
|
||||
similarity=Greatest(
|
||||
TrigramSimilarity("username", query),
|
||||
TrigramSimilarity("localname", query),
|
||||
)
|
||||
)
|
||||
.filter(
|
||||
similarity__gt=0.5,
|
||||
)
|
||||
.order_by("-similarity")[:10]
|
||||
)
|
||||
|
||||
|
||||
def list_search(query, viewer, *_):
|
||||
"""any relevent lists?"""
|
||||
return (
|
||||
privacy_filter(
|
||||
viewer,
|
||||
models.List.objects,
|
||||
privacy_levels=["public", "followers"],
|
||||
)
|
||||
.annotate(
|
||||
similarity=Greatest(
|
||||
TrigramSimilarity("name", query),
|
||||
TrigramSimilarity("description", query),
|
||||
)
|
||||
)
|
||||
.filter(
|
||||
similarity__gt=0.1,
|
||||
)
|
||||
.order_by("-similarity")[:10]
|
||||
)
|
||||
|
@ -2,6 +2,7 @@
|
||||
from collections import namedtuple
|
||||
|
||||
from django.db import IntegrityError
|
||||
from django.db.models import Count, OuterRef, Subquery, F, Q
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.paginator import Paginator
|
||||
from django.http import HttpResponseBadRequest, HttpResponseNotFound
|
||||
@ -25,10 +26,7 @@ class Shelf(View):
|
||||
|
||||
def get(self, request, username, shelf_identifier=None):
|
||||
"""display a shelf"""
|
||||
try:
|
||||
user = get_user_from_username(request.user, username)
|
||||
except models.User.DoesNotExist:
|
||||
return HttpResponseNotFound()
|
||||
user = get_user_from_username(request.user, username)
|
||||
|
||||
shelves = privacy_filter(request.user, user.shelf_set)
|
||||
|
||||
@ -40,35 +38,50 @@ class Shelf(View):
|
||||
return HttpResponseNotFound()
|
||||
if not shelf.visible_to_user(request.user):
|
||||
return HttpResponseNotFound()
|
||||
books = shelf.books
|
||||
# this is a constructed "all books" view, with a fake "shelf" obj
|
||||
else:
|
||||
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()
|
||||
shelf = FakeShelf("all", _("All books"), user, books, "public")
|
||||
|
||||
is_self = request.user == user
|
||||
|
||||
if is_api_request(request):
|
||||
return ActivitypubResponse(shelf.to_activity(**request.GET))
|
||||
|
||||
reviews = privacy_filter(
|
||||
request.user,
|
||||
models.Review.objects.filter(
|
||||
user=user,
|
||||
rating__isnull=False,
|
||||
book__id=OuterRef("id"),
|
||||
),
|
||||
).order_by("-published_date")
|
||||
|
||||
books = books.annotate(rating=Subquery(reviews.values("rating")[:1]))
|
||||
|
||||
paginated = Paginator(
|
||||
shelf.books.order_by("-updated_date"),
|
||||
books.order_by("-updated_date"),
|
||||
PAGE_LENGTH,
|
||||
)
|
||||
|
||||
page = paginated.get_page(request.GET.get("page"))
|
||||
data = {
|
||||
"user": user,
|
||||
"is_self": is_self,
|
||||
"is_self": request.user == user,
|
||||
"shelves": shelves.all(),
|
||||
"shelf": shelf,
|
||||
"books": paginated.get_page(request.GET.get("page")),
|
||||
"books": page,
|
||||
"page_range": paginated.get_elided_page_range(
|
||||
page.number, on_each_side=2, on_ends=1
|
||||
),
|
||||
}
|
||||
|
||||
return TemplateResponse(request, "user/shelf.html", data)
|
||||
return TemplateResponse(request, "user/shelf/shelf.html", data)
|
||||
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
# pylint: disable=unused-argument
|
||||
|
@ -21,7 +21,7 @@ from .reading import edit_readthrough
|
||||
class CreateStatus(View):
|
||||
"""the view for *posting*"""
|
||||
|
||||
def get(self, request):
|
||||
def get(self, request, status_type): # pylint: disable=unused-argument
|
||||
"""compose view (used for delete-and-redraft"""
|
||||
book = get_object_or_404(models.Edition, id=request.GET.get("book"))
|
||||
data = {"book": book}
|
||||
|
@ -10,7 +10,8 @@ def get_notification_count(request):
|
||||
"""any notifications waiting?"""
|
||||
return JsonResponse(
|
||||
{
|
||||
"count": request.user.notification_set.filter(read=False).count(),
|
||||
"count": request.user.unread_notification_count,
|
||||
"has_mentions": request.user.has_unread_mentions,
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -6,7 +6,6 @@ from PIL import Image
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.files.base import ContentFile
|
||||
from django.core.paginator import Paginator
|
||||
from django.http import HttpResponseNotFound
|
||||
from django.shortcuts import redirect
|
||||
from django.template.response import TemplateResponse
|
||||
from django.utils import timezone
|
||||
@ -17,7 +16,7 @@ from bookwyrm import forms, models
|
||||
from bookwyrm.activitypub import ActivitypubResponse
|
||||
from bookwyrm.settings import PAGE_LENGTH
|
||||
from .helpers import get_user_from_username, is_api_request
|
||||
from .helpers import is_blocked, privacy_filter
|
||||
from .helpers import privacy_filter
|
||||
|
||||
|
||||
# pylint: disable= no-self-use
|
||||
@ -26,14 +25,7 @@ class User(View):
|
||||
|
||||
def get(self, request, username):
|
||||
"""profile page for a user"""
|
||||
try:
|
||||
user = get_user_from_username(request.user, username)
|
||||
except models.User.DoesNotExist:
|
||||
return HttpResponseNotFound()
|
||||
|
||||
# make sure we're not blocked
|
||||
if is_blocked(request.user, user):
|
||||
return HttpResponseNotFound()
|
||||
user = get_user_from_username(request.user, username)
|
||||
|
||||
if is_api_request(request):
|
||||
# we have a json request
|
||||
@ -94,24 +86,18 @@ class Followers(View):
|
||||
|
||||
def get(self, request, username):
|
||||
"""list of followers"""
|
||||
try:
|
||||
user = get_user_from_username(request.user, username)
|
||||
except models.User.DoesNotExist:
|
||||
return HttpResponseNotFound()
|
||||
|
||||
# make sure we're not blocked
|
||||
if is_blocked(request.user, user):
|
||||
return HttpResponseNotFound()
|
||||
user = get_user_from_username(request.user, username)
|
||||
|
||||
if is_api_request(request):
|
||||
return ActivitypubResponse(user.to_followers_activity(**request.GET))
|
||||
|
||||
paginated = Paginator(user.followers.all(), PAGE_LENGTH)
|
||||
data = {
|
||||
"user": user,
|
||||
"is_self": request.user.id == user.id,
|
||||
"followers": user.followers.all(),
|
||||
"follow_list": paginated.get_page(request.GET.get("page")),
|
||||
}
|
||||
return TemplateResponse(request, "user/followers.html", data)
|
||||
return TemplateResponse(request, "user/relationships/followers.html", data)
|
||||
|
||||
|
||||
class Following(View):
|
||||
@ -119,24 +105,18 @@ class Following(View):
|
||||
|
||||
def get(self, request, username):
|
||||
"""list of followers"""
|
||||
try:
|
||||
user = get_user_from_username(request.user, username)
|
||||
except models.User.DoesNotExist:
|
||||
return HttpResponseNotFound()
|
||||
|
||||
# make sure we're not blocked
|
||||
if is_blocked(request.user, user):
|
||||
return HttpResponseNotFound()
|
||||
user = get_user_from_username(request.user, username)
|
||||
|
||||
if is_api_request(request):
|
||||
return ActivitypubResponse(user.to_following_activity(**request.GET))
|
||||
|
||||
paginated = Paginator(user.following.all(), PAGE_LENGTH)
|
||||
data = {
|
||||
"user": user,
|
||||
"is_self": request.user.id == user.id,
|
||||
"following": user.following.all(),
|
||||
"follow_list": paginated.get_page(request.GET.get("page")),
|
||||
}
|
||||
return TemplateResponse(request, "user/following.html", data)
|
||||
return TemplateResponse(request, "user/relationships/following.html", data)
|
||||
|
||||
|
||||
@method_decorator(login_required, name="dispatch")
|
||||
|
Reference in New Issue
Block a user