1
0
Fork 0

Merge branch 'main' into remove-tags

This commit is contained in:
Mouse Reeve 2021-04-22 18:18:24 -07:00
commit 563623616c
158 changed files with 5668 additions and 2066 deletions

View file

@ -6,6 +6,8 @@ from .books import Book, EditBook, ConfirmEditBook, Editions
from .books import upload_cover, add_description, switch_edition, resolve_book
from .directory import Directory
from .federation import Federation, FederatedServer
from .federation import AddFederatedServer, ImportServerBlocklist
from .federation import block_server, unblock_server
from .feed import DirectMessage, Feed, Replies, Status
from .follow import follow, unfollow
from .follow import accept_follow_request, delete_follow_request
@ -23,7 +25,7 @@ from .notifications import Notifications
from .outbox import Outbox
from .reading import edit_readthrough, create_readthrough, delete_readthrough
from .reading import start_reading, finish_reading, delete_progressupdate
from .reports import Report, Reports, make_report, resolve_report, deactivate_user
from .reports import Report, Reports, make_report, resolve_report, suspend_user
from .rss_feed import RssFeed
from .password import PasswordResetRequest, PasswordReset, ChangePassword
from .search import Search
@ -34,5 +36,5 @@ from .site import Site
from .status import CreateStatus, DeleteStatus, DeleteAndRedraft
from .updates import get_notification_count, get_unread_status_count
from .user import User, EditUser, Followers, Following
from .user_admin import UserAdmin
from .user_admin import UserAdmin, UserAdminList
from .wellknown import *

View file

@ -1,5 +1,4 @@
""" the good stuff! the books! """
from datetime import datetime
from uuid import uuid4
from dateutil.parser import parse as dateparse
@ -31,11 +30,6 @@ class Book(View):
def get(self, request, book_id):
""" info about a book """
try:
page = int(request.GET.get("page", 1))
except ValueError:
page = 1
try:
book = models.Book.objects.select_subclasses().get(id=book_id)
except models.Book.DoesNotExist:
@ -61,7 +55,7 @@ class Book(View):
paginated = Paginator(
reviews.exclude(Q(content__isnull=True) | Q(content="")), PAGE_LENGTH
)
reviews_page = paginated.page(page)
reviews_page = paginated.get_page(request.GET.get("page"))
user_tags = readthroughs = user_shelves = other_edition_shelves = []
if request.user.is_authenticated:
@ -175,18 +169,18 @@ 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")
# we have to make sure the dates are passed in as datetime, they're currently a string
# make sure the dates are passed in as datetime, they're currently a string
# QueryDicts are immutable, we need to copy
formcopy = data["form"].data.copy()
try:
formcopy["first_published_date"] = dateparse(
formcopy["first_published_date"]
)
except MultiValueDictKeyError:
except (MultiValueDictKeyError, ValueError):
pass
try:
formcopy["published_date"] = dateparse(formcopy["published_date"])
except MultiValueDictKeyError:
except (MultiValueDictKeyError, ValueError):
pass
data["form"].data = formcopy
return TemplateResponse(request, "book/edit_book.html", data)
@ -267,11 +261,6 @@ class Editions(View):
""" list of editions of a book """
work = get_object_or_404(models.Work, id=book_id)
try:
page = int(request.GET.get("page", 1))
except ValueError:
page = 1
if is_api_request(request):
return ActivitypubResponse(work.to_edition_list(**request.GET))
filters = {}
@ -281,12 +270,12 @@ class Editions(View):
if request.GET.get("format"):
filters["physical_format__iexact"] = request.GET.get("format")
editions = work.editions.order_by("-edition_rank").all()
editions = work.editions.order_by("-edition_rank")
languages = set(sum([e.languages for e in editions], []))
paginated = Paginator(editions.filter(**filters).all(), PAGE_LENGTH)
paginated = Paginator(editions.filter(**filters), PAGE_LENGTH)
data = {
"editions": paginated.page(page),
"editions": paginated.get_page(request.GET.get("page")),
"work": work,
"languages": languages,
"formats": set(

View file

@ -15,12 +15,6 @@ class Directory(View):
def get(self, request):
""" lets see your cute faces """
try:
page = int(request.GET.get("page", 1))
except ValueError:
page = 1
# filters
filters = {}
software = request.GET.get("software")
if not software or software == "bookwyrm":
@ -39,7 +33,7 @@ class Directory(View):
paginated = Paginator(users, 12)
data = {
"users": paginated.page(page),
"users": paginated.get_page(request.GET.get("page")),
}
return TemplateResponse(request, "directory/directory.html", data)

View file

@ -1,12 +1,15 @@
""" manage federated servers """
import json
from django.contrib.auth.decorators import login_required, permission_required
from django.core.paginator import Paginator
from django.shortcuts import get_object_or_404
from django.db import transaction
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 bookwyrm import models
from bookwyrm import forms, models
from bookwyrm.settings import PAGE_LENGTH
@ -21,23 +24,78 @@ class Federation(View):
def get(self, request):
""" list of servers """
try:
page = int(request.GET.get("page", 1))
except ValueError:
page = 1
servers = models.FederatedServer.objects.all()
servers = models.FederatedServer.objects
sort = request.GET.get("sort")
sort_fields = ["created_date", "application_type", "server_name"]
if sort in sort_fields + ["-{:s}".format(f) for f in sort_fields]:
servers = servers.order_by(sort)
if not sort in sort_fields + ["-{:s}".format(f) for f in sort_fields]:
sort = "created_date"
servers = servers.order_by(sort)
paginated = Paginator(servers, PAGE_LENGTH)
data = {"servers": paginated.page(page), "sort": sort}
data = {
"servers": paginated.get_page(request.GET.get("page")),
"sort": sort,
"form": forms.ServerForm(),
}
return TemplateResponse(request, "settings/federation.html", data)
class AddFederatedServer(View):
""" manually add a server """
def get(self, request):
""" add server form """
data = {"form": forms.ServerForm()}
return TemplateResponse(request, "settings/edit_server.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)
server = form.save()
return redirect("settings-federated-server", server.id)
@method_decorator(login_required, name="dispatch")
@method_decorator(
permission_required("bookwyrm.control_federation", raise_exception=True),
name="dispatch",
)
class ImportServerBlocklist(View):
""" manually add a server """
def get(self, request):
""" add server form """
return TemplateResponse(request, "settings/server_blocklist.html")
def post(self, request):
""" add a server from the admin panel """
json_data = json.load(request.FILES["json_file"])
failed = []
success_count = 0
for item in json_data:
server_name = item.get("instance")
if not server_name:
failed.append(item)
continue
info_link = item.get("url")
with transaction.atomic():
server, _ = models.FederatedServer.objects.get_or_create(
server_name=server_name,
)
server.notes = info_link
server.save()
server.block()
success_count += 1
data = {"failed": failed, "succeeded": success_count}
return TemplateResponse(request, "settings/server_blocklist.html", data)
@method_decorator(login_required, name="dispatch")
@method_decorator(
permission_required("bookwyrm.control_federation", raise_exception=True),
@ -61,3 +119,32 @@ class FederatedServer(View):
),
}
return TemplateResponse(request, "settings/federated_server.html", data)
def post(self, request, server): # pylint: disable=unused-argument
""" update note """
server = get_object_or_404(models.FederatedServer, id=server)
server.notes = request.POST.get("notes")
server.save()
return redirect("settings-federated-server", server.id)
@login_required
@require_POST
@permission_required("bookwyrm.control_federation", raise_exception=True)
# pylint: disable=unused-argument
def block_server(request, server):
""" block a server """
server = get_object_or_404(models.FederatedServer, id=server)
server.block()
return redirect("settings-federated-server", server.id)
@login_required
@require_POST
@permission_required("bookwyrm.control_federation", raise_exception=True)
# pylint: disable=unused-argument
def unblock_server(request, server):
""" unblock a server """
server = get_object_or_404(models.FederatedServer, id=server)
server.unblock()
return redirect("settings-federated-server", server.id)

View file

@ -12,7 +12,7 @@ from bookwyrm import activitystreams, forms, models
from bookwyrm.activitypub import ActivitypubResponse
from bookwyrm.settings import PAGE_LENGTH, STREAMS
from .helpers import get_user_from_username, privacy_filter, get_suggested_users
from .helpers import is_api_request, is_bookwyrm_request, object_visible_to_user
from .helpers import is_api_request, is_bookwyrm_request
# pylint: disable= no-self-use
@ -22,11 +22,6 @@ class Feed(View):
def get(self, request, tab):
""" user's homepage with activity feed """
try:
page = int(request.GET.get("page", 1))
except ValueError:
page = 1
if not tab in STREAMS:
tab = "home"
@ -39,7 +34,7 @@ class Feed(View):
**feed_page_data(request.user),
**{
"user": request.user,
"activities": paginated.page(page),
"activities": paginated.get_page(request.GET.get("page")),
"suggested_users": suggested_users,
"tab": tab,
"goal_form": forms.GoalForm(),
@ -55,11 +50,6 @@ class DirectMessage(View):
def get(self, request, username=None):
""" like a feed but for dms only """
try:
page = int(request.GET.get("page", 1))
except ValueError:
page = 1
# remove fancy subclasses of status, keep just good ol' notes
queryset = models.Status.objects.filter(
review__isnull=True,
@ -82,13 +72,12 @@ class DirectMessage(View):
).order_by("-published_date")
paginated = Paginator(activities, PAGE_LENGTH)
activity_page = paginated.page(page)
data = {
**feed_page_data(request.user),
**{
"user": request.user,
"partner": user,
"activities": activity_page,
"activities": paginated.get_page(request.GET.get("page")),
"path": "/direct-messages",
},
}
@ -105,7 +94,7 @@ class Status(View):
status = models.Status.objects.select_subclasses().get(
id=status_id, deleted=False
)
except (ValueError, models.Status.DoesNotExist):
except (ValueError, models.Status.DoesNotExist, models.User.DoesNotExist):
return HttpResponseNotFound()
# the url should have the poster's username in it
@ -113,7 +102,7 @@ class Status(View):
return HttpResponseNotFound()
# make sure the user is authorized to see the status
if not object_visible_to_user(request.user, status):
if not status.visible_to_user(request.user):
return HttpResponseNotFound()
if is_api_request(request):
@ -174,7 +163,7 @@ def get_suggested_books(user, max_books=5):
)
shelf = user.shelf_set.get(identifier=preset)
shelf_books = shelf.shelfbook_set.order_by("-updated_date").all()[:limit]
shelf_books = shelf.shelfbook_set.order_by("-updated_date")[:limit]
if not shelf_books:
continue
shelf_preview = {

View file

@ -10,7 +10,7 @@ from django.views.decorators.http import require_POST
from bookwyrm import forms, models
from bookwyrm.status import create_generated_note
from .helpers import get_user_from_username, object_visible_to_user
from .helpers import get_user_from_username
# pylint: disable= no-self-use
@ -26,7 +26,7 @@ class Goal(View):
if not goal and user != request.user:
return HttpResponseNotFound()
if goal and not object_visible_to_user(request.user, goal):
if goal and not goal.visible_to_user(request.user):
return HttpResponseNotFound()
data = {

View file

@ -32,30 +32,6 @@ def is_bookwyrm_request(request):
return True
def object_visible_to_user(viewer, obj):
""" is a user authorized to view an object? """
if not obj:
return False
# viewer can't see it if the object's owner blocked them
if viewer in obj.user.blocks.all():
return False
# you can see your own posts and any public or unlisted posts
if viewer == obj.user or obj.privacy in ["public", "unlisted"]:
return True
# you can see the followers only posts of people you follow
if obj.privacy == "followers" and obj.user.followers.filter(id=viewer.id).first():
return True
# you can see dms you are tagged in
if isinstance(obj, models.Status):
if obj.privacy == "direct" and obj.mention_users.filter(id=viewer.id).first():
return True
return False
def privacy_filter(viewer, queryset, privacy_levels=None, following_only=False):
""" filter objects that have "user" and "privacy" fields """
privacy_levels = privacy_levels or ["public", "unlisted", "followers", "direct"]

View file

@ -1,9 +1,10 @@
""" incoming activities """
import json
import re
from urllib.parse import urldefrag
from django.http import HttpResponse
from django.http import HttpResponseBadRequest, HttpResponseNotFound
from django.http import HttpResponse, HttpResponseNotFound
from django.http import HttpResponseBadRequest, HttpResponseForbidden
from django.utils.decorators import method_decorator
from django.views import View
from django.views.decorators.csrf import csrf_exempt
@ -12,6 +13,7 @@ import requests
from bookwyrm import activitypub, models
from bookwyrm.tasks import app
from bookwyrm.signatures import Signature
from bookwyrm.utils import regex
@method_decorator(csrf_exempt, name="dispatch")
@ -21,6 +23,10 @@ class Inbox(View):
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()
# make sure the user's inbox even exists
if username:
try:
@ -34,6 +40,10 @@ class Inbox(View):
except json.decoder.JSONDecodeError:
return HttpResponseBadRequest()
# let's be extra sure we didn't block this domain
if is_blocked_activity(activity_json):
return HttpResponseForbidden()
if (
not "object" in activity_json
or not "type" in activity_json
@ -54,6 +64,34 @@ class Inbox(View):
return HttpResponse()
def 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
url = re.search(r"https?://{:s}/?".format(regex.domain), user_agent)
if not url:
return False
url = url.group()
return models.FederatedServer.is_blocked(url)
def 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
if not actor:
# well I guess it's not even a valid activity so who knows
return False
return models.FederatedServer.is_blocked(actor)
@app.task
def activity_task(activity_json):
""" do something with this json we think is legit """

View file

@ -30,11 +30,6 @@ class ManageInvites(View):
def get(self, request):
""" invite management page """
try:
page = int(request.GET.get("page", 1))
except ValueError:
page = 1
paginated = Paginator(
models.SiteInvite.objects.filter(user=request.user).order_by(
"-created_date"
@ -43,7 +38,7 @@ class ManageInvites(View):
)
data = {
"invites": paginated.page(page),
"invites": paginated.get_page(request.GET.get("page")),
"form": forms.CreateInviteForm(),
}
return TemplateResponse(request, "settings/manage_invites.html", data)
@ -93,11 +88,6 @@ class ManageInviteRequests(View):
def get(self, request):
""" view a list of requests """
ignored = request.GET.get("ignored", False)
try:
page = int(request.GET.get("page", 1))
except ValueError:
page = 1
sort = request.GET.get("sort")
sort_fields = [
"created_date",
@ -136,7 +126,7 @@ class ManageInviteRequests(View):
data = {
"ignored": ignored,
"count": paginated.count,
"requests": paginated.page(page),
"requests": paginated.get_page(request.GET.get("page")),
"sort": sort,
}
return TemplateResponse(request, "settings/manage_invite_requests.html", data)

View file

@ -1,9 +1,12 @@
""" book list views"""
from typing import Optional
from django.contrib.auth.decorators import login_required
from django.core.paginator import Paginator
from django.db import IntegrityError
from django.db.models import Count, Q
from django.http import HttpResponseNotFound, HttpResponseBadRequest
from django.db import IntegrityError, transaction
from django.db.models import Avg, Count, 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.utils.decorators import method_decorator
@ -13,20 +16,16 @@ 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 .helpers import is_api_request, object_visible_to_user, privacy_filter
from .helpers import is_api_request, privacy_filter
from .helpers import get_user_from_username
# pylint: disable=no-self-use
class Lists(View):
""" book list page """
def get(self, request):
""" display a book list """
try:
page = int(request.GET.get("page", 1))
except ValueError:
page = 1
# hide lists with no approved books
lists = (
models.List.objects.annotate(
@ -35,7 +34,6 @@ class Lists(View):
.filter(item_count__gt=0)
.order_by("-updated_date")
.distinct()
.all()
)
lists = privacy_filter(
@ -44,7 +42,7 @@ class Lists(View):
paginated = Paginator(lists, 12)
data = {
"lists": paginated.page(page),
"lists": paginated.get_page(request.GET.get("page")),
"list_form": forms.ListForm(),
"path": "/list",
}
@ -67,19 +65,15 @@ class UserLists(View):
def get(self, request, username):
""" display a book list """
try:
page = int(request.GET.get("page", 1))
except ValueError:
page = 1
user = get_user_from_username(request.user, username)
lists = models.List.objects.filter(user=user).all()
lists = models.List.objects.filter(user=user)
lists = privacy_filter(request.user, lists)
paginated = Paginator(lists, 12)
data = {
"user": user,
"is_self": request.user.id == user.id,
"lists": paginated.page(page),
"lists": paginated.get_page(request.GET.get("page")),
"list_form": forms.ListForm(),
"path": user.local_path + "/lists",
}
@ -92,7 +86,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 object_visible_to_user(request.user, book_list):
if not book_list.visible_to_user(request.user):
return HttpResponseNotFound()
if is_api_request(request):
@ -100,6 +94,45 @@ class List(View):
query = request.GET.get("q")
suggestions = None
# sort_by shall be "order" unless a valid alternative is given
sort_by = request.GET.get("sort_by", "order")
if sort_by not in ("order", "title", "rating"):
sort_by = "order"
# direction shall be "ascending" unless a valid alternative is given
direction = request.GET.get("direction", "ascending")
if direction not in ("ascending", "descending"):
direction = "ascending"
internal_sort_by = {
"order": "order",
"title": "book__title",
"rating": "average_rating",
}
directional_sort_by = internal_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))
)
.filter(approved=True)
.order_by(directional_sort_by)
)
paginated = Paginator(items, 12)
if query and request.user.is_authenticated:
# search for books
suggestions = connector_manager.local_search(query, raw=True)
@ -119,11 +152,14 @@ class List(View):
data = {
"list": book_list,
"items": book_list.listitem_set.filter(approved=True),
"items": paginated.get_page(request.GET.get("page")),
"pending_count": book_list.listitem_set.filter(approved=False).count(),
"suggested_books": suggestions,
"list_form": forms.ListForm(instance=book_list),
"query": query or "",
"sort_form": forms.SortListForm(
{"direction": direction, "sort_by": sort_by}
),
}
return TemplateResponse(request, "lists/list.html", data)
@ -165,10 +201,22 @@ class Curate(View):
suggestion = get_object_or_404(models.ListItem, id=request.POST.get("item"))
approved = request.POST.get("approved") == "true"
if approved:
# update the book and set it to be the last in the order of approved books,
# before any pending books
suggestion.approved = True
order_max = (
book_list.listitem_set.filter(approved=True).aggregate(Max("order"))[
"order__max"
]
or 0
) + 1
suggestion.order = order_max
increment_order_in_reverse(book_list.id, order_max)
suggestion.save()
else:
suggestion.delete()
deleted_order = suggestion.order
suggestion.delete(broadcast=False)
normalize_book_list_ordering(book_list.id, start=deleted_order)
return redirect("list-curate", book_list.id)
@ -176,26 +224,37 @@ class Curate(View):
def add_book(request):
""" put a book on a list """
book_list = get_object_or_404(models.List, id=request.POST.get("list"))
if not object_visible_to_user(request.user, book_list):
if not book_list.visible_to_user(request.user):
return HttpResponseNotFound()
book = get_object_or_404(models.Edition, id=request.POST.get("book"))
# do you have permission to add to the list?
try:
if request.user == book_list.user or book_list.curation == "open":
# go ahead and add it
# add the book at the latest order of approved books, before pending books
order_max = (
book_list.listitem_set.filter(approved=True).aggregate(Max("order"))[
"order__max"
]
) or 0
increment_order_in_reverse(book_list.id, order_max + 1)
models.ListItem.objects.create(
book=book,
book_list=book_list,
user=request.user,
order=order_max + 1,
)
elif book_list.curation == "curated":
# make a pending entry
# make a pending entry at the end of the list
order_max = (
book_list.listitem_set.aggregate(Max("order"))["order__max"]
) or 0
models.ListItem.objects.create(
approved=False,
book=book,
book_list=book_list,
user=request.user,
order=order_max + 1,
)
else:
# you can't add to this list, what were you THINKING
@ -209,12 +268,113 @@ def add_book(request):
@require_POST
def remove_book(request, list_id):
""" put a book on a list """
book_list = get_object_or_404(models.List, id=list_id)
item = get_object_or_404(models.ListItem, id=request.POST.get("item"))
""" 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"))
if not book_list.user == request.user and not item.user == request.user:
return HttpResponseNotFound()
if not book_list.user == request.user and not item.user == request.user:
return HttpResponseNotFound()
item.delete()
deleted_order = item.order
item.delete()
normalize_book_list_ordering(book_list.id, start=deleted_order)
return redirect("list", list_id)
@require_POST
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.
"""
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"]
if int_position > order_max:
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()
increment_order_in_reverse(book_list.id, int_position, original_order)
else:
list_item.order = -1
list_item.save()
decrement_order(book_list.id, original_order, int_position)
list_item.order = int_position
list_item.save()
return redirect("list", book_list.id)
@transaction.atomic
def increment_order_in_reverse(
book_list_id: int, start: int, end: Optional[int] = None
):
""" increase the order nu,ber for every item in a list """
try:
book_list = models.List.objects.get(id=book_list_id)
except models.List.DoesNotExist:
return
items = book_list.listitem_set.filter(order__gte=start)
if end is not None:
items = items.filter(order__lt=end)
items = items.order_by("-order")
for item in items:
item.order += 1
item.save()
@transaction.atomic
def decrement_order(book_list_id, start, end):
""" decrement the order value for every item in a list """
try:
book_list = models.List.objects.get(id=book_list_id)
except models.List.DoesNotExist:
return
items = book_list.listitem_set.filter(order__gt=start, order__lte=end).order_by(
"order"
)
for item in items:
item.order -= 1
item.save()
@transaction.atomic
def normalize_book_list_ordering(book_list_id, start=0, add_offset=0):
""" gives each book in a list the proper sequential order number """
try:
book_list = models.List.objects.get(id=book_list_id)
except models.List.DoesNotExist:
return
items = book_list.listitem_set.filter(order__gt=start).order_by("order")
for i, item in enumerate(items, start):
effective_order = i + add_offset
if item.order != effective_order:
item.order = effective_order
item.save()

View file

@ -145,6 +145,7 @@ def create_readthrough(request):
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)

View file

@ -29,8 +29,10 @@ class Reports(View):
resolved = request.GET.get("resolved") == "true"
server = request.GET.get("server")
if server:
server = get_object_or_404(models.FederatedServer, id=server)
filters["user__federated_server"] = server
filters["user__federated_server__server_name"] = server
username = request.GET.get("username")
if username:
filters["user__username__icontains"] = username
filters["resolved"] = resolved
data = {
"resolved": resolved,
@ -72,12 +74,13 @@ class Report(View):
@login_required
@permission_required("bookwyrm_moderate_user")
def deactivate_user(_, report_id):
def suspend_user(_, user_id):
""" mark an account as inactive """
report = get_object_or_404(models.Report, id=report_id)
report.user.is_active = not report.user.is_active
report.user.save()
return redirect("settings-report", report.id)
user = get_object_or_404(models.User, id=user_id)
user.is_active = not user.is_active
# this isn't a full deletion, so we don't want to tell the world
user.save(broadcast=False)
return redirect("settings-user", user.id)
@login_required
@ -98,8 +101,7 @@ def make_report(request):
""" a user reports something """
form = forms.ReportForm(request.POST)
if not form.is_valid():
print(form.errors)
return redirect(request.headers.get("Referer", "/"))
raise ValueError(form.errors)
form.save()
return redirect(request.headers.get("Referer", "/"))

View file

@ -16,7 +16,7 @@ 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 handle_reading_status, privacy_filter, object_visible_to_user
from .helpers import handle_reading_status, privacy_filter
# pylint: disable= no-self-use
@ -30,11 +30,6 @@ class Shelf(View):
except models.User.DoesNotExist:
return HttpResponseNotFound()
try:
page = int(request.GET.get("page", 1))
except ValueError:
page = 1
shelves = privacy_filter(request.user, user.shelf_set)
# get the shelf and make sure the logged in user should be able to see it
@ -43,7 +38,7 @@ class Shelf(View):
shelf = user.shelf_set.get(identifier=shelf_identifier)
except models.Shelf.DoesNotExist:
return HttpResponseNotFound()
if not object_visible_to_user(request.user, shelf):
if not shelf.visible_to_user(request.user):
return HttpResponseNotFound()
# this is a constructed "all books" view, with a fake "shelf" obj
else:
@ -61,7 +56,7 @@ class Shelf(View):
return ActivitypubResponse(shelf.to_activity(**request.GET))
paginated = Paginator(
shelf.books.order_by("-updated_date").all(),
shelf.books.order_by("-updated_date"),
PAGE_LENGTH,
)
@ -70,7 +65,7 @@ class Shelf(View):
"is_self": is_self,
"shelves": shelves.all(),
"shelf": shelf,
"books": paginated.page(page),
"books": paginated.get_page(request.GET.get("page")),
}
return TemplateResponse(request, "user/shelf.html", data)

View file

@ -17,7 +17,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, object_visible_to_user
from .helpers import is_blocked, privacy_filter
# pylint: disable= no-self-use
@ -40,11 +40,6 @@ class User(View):
return ActivitypubResponse(user.to_activity())
# otherwise we're at a UI view
try:
page = int(request.GET.get("page", 1))
except ValueError:
page = 1
shelf_preview = []
# only show other shelves that should be visible
@ -80,14 +75,14 @@ class User(View):
goal = models.AnnualGoal.objects.filter(
user=user, year=timezone.now().year
).first()
if not object_visible_to_user(request.user, goal):
if goal and not goal.visible_to_user(request.user):
goal = None
data = {
"user": user,
"is_self": is_self,
"shelves": shelf_preview,
"shelf_count": shelves.count(),
"activities": paginated.page(page),
"activities": paginated.get_page(request.GET.get("page", 1)),
"goal": goal,
}

View file

@ -6,7 +6,7 @@ from django.template.response import TemplateResponse
from django.utils.decorators import method_decorator
from django.views import View
from bookwyrm import models
from bookwyrm import forms, models
from bookwyrm.settings import PAGE_LENGTH
@ -16,23 +16,22 @@ from bookwyrm.settings import PAGE_LENGTH
permission_required("bookwyrm.moderate_users", raise_exception=True),
name="dispatch",
)
class UserAdmin(View):
class UserAdminList(View):
""" admin view of users on this server """
def get(self, request):
""" list of users """
try:
page = int(request.GET.get("page", 1))
except ValueError:
page = 1
filters = {}
server = request.GET.get("server")
if server:
server = get_object_or_404(models.FederatedServer, id=server)
server = models.FederatedServer.objects.filter(server_name=server).first()
filters["federated_server"] = server
filters["federated_server__isnull"] = False
username = request.GET.get("username")
if username:
filters["username__icontains"] = username
users = models.User.objects.filter(**filters).all()
users = models.User.objects.filter(**filters)
sort = request.GET.get("sort", "-created_date")
sort_fields = [
@ -46,5 +45,33 @@ class UserAdmin(View):
users = users.order_by(sort)
paginated = Paginator(users, PAGE_LENGTH)
data = {"users": paginated.page(page), "sort": sort, "server": server}
return TemplateResponse(request, "settings/user_admin.html", data)
data = {
"users": paginated.get_page(request.GET.get("page")),
"sort": sort,
"server": server,
}
return TemplateResponse(request, "user_admin/user_admin.html", data)
@method_decorator(login_required, name="dispatch")
@method_decorator(
permission_required("bookwyrm.moderate_users", raise_exception=True),
name="dispatch",
)
class UserAdmin(View):
""" moderate an individual user """
def get(self, request, user):
""" 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)
def post(self, request, user):
""" update user group """
user = get_object_or_404(models.User, id=user)
form = forms.UserGroupForm(request.POST, instance=user)
if form.is_valid():
form.save()
data = {"user": user, "group_form": form}
return TemplateResponse(request, "user_admin/user.html", data)