Merge branch 'dropdown-style' of https://github.com/joachimesque/bookwyrm into production

This commit is contained in:
Mouse Reeve 2021-04-21 12:49:35 -07:00
commit b54979d39c
60 changed files with 2774 additions and 719 deletions

View file

@ -3,7 +3,7 @@ import datetime
from collections import defaultdict
from django import forms
from django.forms import ModelForm, PasswordInput, widgets
from django.forms import ModelForm, PasswordInput, widgets, ChoiceField
from django.forms.widgets import Textarea
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
@ -150,6 +150,12 @@ class LimitedEditUserForm(CustomForm):
help_texts = {f: None for f in fields}
class UserGroupForm(CustomForm):
class Meta:
model = models.User
fields = ["groups"]
class TagForm(CustomForm):
class Meta:
model = models.Tag
@ -287,3 +293,20 @@ class ServerForm(CustomForm):
class Meta:
model = models.FederatedServer
exclude = ["remote_id"]
class SortListForm(forms.Form):
sort_by = ChoiceField(
choices=(
("order", _("List Order")),
("title", _("Book Title")),
("rating", _("Rating")),
),
label=_("Sort By"),
)
direction = ChoiceField(
choices=(
("ascending", _("Ascending")),
("descending", _("Descending")),
),
)

View file

@ -0,0 +1,30 @@
from django.db import migrations
def forwards_func(apps, schema_editor):
# Set all values for ListItem.order
BookList = apps.get_model("bookwyrm", "List")
db_alias = schema_editor.connection.alias
for book_list in BookList.objects.using(db_alias).all():
for i, item in enumerate(book_list.listitem_set.order_by("id"), 1):
item.order = i
item.save()
def reverse_func(apps, schema_editor):
# null all values for ListItem.order
BookList = apps.get_model("bookwyrm", "List")
db_alias = schema_editor.connection.alias
for book_list in BookList.objects.using(db_alias).all():
for item in book_list.listitem_set.order_by("id"):
item.order = None
item.save()
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0066_user_deactivation_reason"),
]
operations = [migrations.RunPython(forwards_func, reverse_func)]

View file

@ -0,0 +1,23 @@
# Generated by Django 3.1.6 on 2021-04-08 16:15
import bookwyrm.models.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0067_denullify_list_item_order"),
]
operations = [
migrations.AlterField(
model_name="listitem",
name="order",
field=bookwyrm.models.fields.IntegerField(),
),
migrations.AlterUniqueTogether(
name="listitem",
unique_together={("order", "book_list"), ("book", "book_list")},
),
]

View file

@ -47,7 +47,7 @@ class List(OrderedCollectionMixin, BookWyrmModel):
@property
def collection_queryset(self):
""" list of books for this shelf, overrides OrderedCollectionMixin """
return self.books.filter(listitem__approved=True).all().order_by("listitem")
return self.books.filter(listitem__approved=True).order_by("listitem")
class Meta:
""" default sorting """
@ -67,7 +67,7 @@ class ListItem(CollectionItemMixin, BookWyrmModel):
)
notes = fields.TextField(blank=True, null=True)
approved = models.BooleanField(default=True)
order = fields.IntegerField(blank=True, null=True)
order = fields.IntegerField()
endorsement = models.ManyToManyField("User", related_name="endorsers")
activity_serializer = activitypub.ListItem
@ -93,7 +93,7 @@ class ListItem(CollectionItemMixin, BookWyrmModel):
)
class Meta:
""" an opinionated constraint! you can't put a book on a list twice """
unique_together = ("book", "book_list")
# A book may only be placed into a list once, and each order in the list may be used only
# once
unique_together = (("book", "book_list"), ("order", "book_list"))
ordering = ("-created_date",)

View file

@ -50,11 +50,10 @@ class UserRelationship(BookWyrmModel):
),
]
def get_remote_id(self, status=None): # pylint: disable=arguments-differ
def get_remote_id(self):
""" use shelf identifier in remote_id """
status = status or "follows"
base_path = self.user_subject.remote_id
return "%s#%s/%d" % (base_path, status, self.id)
return "%s#follows/%d" % (base_path, self.id)
class UserFollows(ActivityMixin, UserRelationship):
@ -138,12 +137,18 @@ class UserFollowRequest(ActivitypubMixin, UserRelationship):
notification_type=notification_type,
)
def get_accept_reject_id(self, status):
""" get id for sending an accept or reject of a local user """
base_path = self.user_object.remote_id
return "%s#%s/%d" % (base_path, status, self.id)
def accept(self):
""" turn this request into the real deal"""
user = self.user_object
if not self.user_subject.local:
activity = activitypub.Accept(
id=self.get_remote_id(status="accepts"),
id=self.get_accept_reject_id(status="accepts"),
actor=self.user_object.remote_id,
object=self.to_activity(),
).serialize()
@ -156,7 +161,7 @@ class UserFollowRequest(ActivitypubMixin, UserRelationship):
""" generate a Reject for this follow request """
if self.user_object.local:
activity = activitypub.Reject(
id=self.get_remote_id(status="rejects"),
id=self.get_accept_reject_id(status="rejects"),
actor=self.user_object.remote_id,
object=self.to_activity(),
).serialize()

View file

@ -48,7 +48,7 @@ class Shelf(OrderedCollectionMixin, BookWyrmModel):
@property
def collection_queryset(self):
""" list of books for this shelf, overrides OrderedCollectionMixin """
return self.books.all().order_by("shelfbook")
return self.books.order_by("shelfbook")
def get_remote_id(self):
""" shelf identifier instead of id """

View file

@ -67,31 +67,16 @@
</div>
{% endif %}
<section class="content is-clipped">
<dl>
{% if book.isbn_13 %}
<div class="is-flex is-justify-content-space-between is-align-items-center">
<dt>{% trans "ISBN:" %}</dt>
<dd itemprop="isbn">{{ book.isbn_13 }}</dd>
<section class="is-clipped">
{% with book=book %}
<div class="content">
{% include 'book/publisher_info.html' %}
</div>
{% endif %}
{% if book.oclc_number %}
<div class="is-flex is-justify-content-space-between is-align-items-center">
<dt>{% trans "OCLC Number:" %}</dt>
<dd>{{ book.oclc_number }}</dd>
<div class="my-3">
{% include 'book/book_identifiers.html' %}
</div>
{% endif %}
{% if book.asin %}
<div class="is-flex is-justify-content-space-between is-align-items-center">
<dt>{% trans "ASIN:" %}</dt>
<dd>{{ book.asin }}</dd>
</div>
{% endif %}
</dl>
{% include 'book/publisher_info.html' with book=book %}
{% endwith %}
{% if book.openlibrary_key %}
<p><a href="https://openlibrary.org/books/{{ book.openlibrary_key }}" target="_blank" rel="noopener">{% trans "View on OpenLibrary" %}</a></p>

View file

@ -0,0 +1,27 @@
{% spaceless %}
{% load i18n %}
<dl>
{% if book.isbn_13 %}
<div class="is-flex">
<dt class="mr-1">{% trans "ISBN:" %}</dt>
<dd itemprop="isbn">{{ book.isbn_13 }}</dd>
</div>
{% endif %}
{% if book.oclc_number %}
<div class="is-flex">
<dt class="mr-1">{% trans "OCLC Number:" %}</dt>
<dd>{{ book.oclc_number }}</dd>
</div>
{% endif %}
{% if book.asin %}
<div class="is-flex">
<dt class="mr-1">{% trans "ASIN:" %}</dt>
<dd>{{ book.asin }}</dd>
</div>
{% endif %}
</dl>
{% endspaceless %}

View file

@ -109,7 +109,10 @@
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
<p class="mb-2"><label class="label" for="id_series">{% trans "Series:" %}</label> {{ form.series }} </p>
<p class="mb-2">
<label class="label" for="id_series">{% trans "Series:" %}</label>
<input type="text" class="input" name="series" id="id_series" value="{{ form.series.value|default:'' }}">
</p>
{% for error in form.series.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}

View file

@ -25,7 +25,18 @@
{{ book.title }}
</a>
</h2>
{% include 'book/publisher_info.html' with book=book %}
{% with book=book %}
<div class="columns is-multiline">
<div class="column is-half">
{% include 'book/publisher_info.html' %}
</div>
<div class="column is-half ">
{% include 'book/book_identifiers.html' %}
</div>
</div>
{% endwith %}
</div>
<div class="column is-3">
{% include 'snippets/shelve_button/shelve_button.html' with book=book switch_mode=True %}

View file

@ -1,6 +1,7 @@
{% spaceless %}
{% load i18n %}
{% load humanize %}
<p>
{% with format=book.physical_format pages=book.pages %}
@ -39,7 +40,7 @@
{% endif %}
<p>
{% with date=book.published_date|date:'M jS Y' publisher=book.publishers|join:', ' %}
{% with date=book.published_date|naturalday publisher=book.publishers|join:', ' %}
{% if date or book.first_published_date %}
<meta
itemprop="datePublished"

View file

@ -23,7 +23,7 @@
<div class="dropdown-menu">
<ul
id="menu-options-{{ uuid }}"
class="dropdown-content"
class="dropdown-content p-0 is-clipped"
role="menu"
>
{% block dropdown-list %}{% endblock %}

View file

@ -13,10 +13,10 @@
<div class="columns mt-3">
<section class="column is-three-quarters">
{% if not items.exists %}
{% if not items.object_list.exists %}
<p>{% trans "This list is currently empty" %}</p>
{% else %}
<ol>
<ol start="{{ items.start_index }}">
{% for item in items %}
<li class="block pb-3">
<div class="card">
@ -30,11 +30,27 @@
{% include 'snippets/shelve_button/shelve_button.html' with book=item.book %}
</div>
</div>
<div class="card-footer has-background-white-bis">
<div class="card-footer has-background-white-bis is-align-items-baseline">
<div class="card-footer-item">
<div>
<p>{% blocktrans with username=item.user.display_name user_path=user.local_path %}Added by <a href="{{ user_path }}">{{ username }}</a>{% endblocktrans %}</p>
</div>
</div>
{% if list.user == request.user or list.curation == 'open' and item.user == request.user %}
<div class="card-footer-item">
<form name="set-position" method="post" action="{% url 'list-set-book-position' item.id %}">
<div class="field has-addons mb-0">
{% csrf_token %}
<div class="control">
<input id="input-list-position" class="input is-small" type="number" min="1" name="position" value="{{ item.order }}">
</div>
<div class="control">
<button type="submit" class="button is-info is-small is-tablet">{% trans "Set" %}</button>
</div>
</div>
<label for="input-list-position" class="help">{% trans "List position" %}</label>
</form>
</div>
<form name="add-book" method="post" action="{% url 'list-remove-book' list.id %}" class="card-footer-item">
{% csrf_token %}
<input type="hidden" name="item" value="{{ item.id }}">
@ -47,10 +63,27 @@
{% endfor %}
</ol>
{% endif %}
{% include "snippets/pagination.html" with page=items %}
</section>
{% if request.user.is_authenticated and not list.curation == 'closed' or request.user == list.user %}
<section class="column is-one-quarter content">
<h2>{% trans "Sort List" %}</h2>
<form name="sort" action="{% url 'list' list.id %}" method="GET" class="block">
<label class="label" for="id_sort_by">{% trans "Sort By" %}</label>
<div class="select is-fullwidth">
{{ sort_form.sort_by }}
</div>
<label class="label" for="id_direction">{% trans "Direction" %}</label>
<div class="select is-fullwidth">
{{ sort_form.direction }}
</div>
<div>
<button class="button is-primary is-fullwidth" type="submit">
{% trans "Sort List" %}
</button>
</div>
</form>
{% if request.user.is_authenticated and not list.curation == 'closed' or request.user == list.user %}
<h2>{% if list.curation == 'open' or request.user == list.user %}{% trans "Add Books" %}{% else %}{% trans "Suggest Books" %}{% endif %}</h2>
<form name="search" action="{% url 'list' list.id %}" method="GET" class="block">
<div class="field has-addons">
@ -93,7 +126,7 @@
</div>
{% endif %}
{% endfor %}
</section>
{% endif %}
</section>
</div>
{% endblock %}

View file

@ -15,76 +15,9 @@
{% include 'moderation/report_preview.html' with report=report %}
</div>
<div class="block columns">
<div class="column is-flex is-flex-direction-column">
<h4 class="title is-4">{% trans "User details" %}</h4>
<div class="box is-flex-grow-1">
{% include 'user/user_preview.html' with user=report.user %}
{% if report.user.summary %}
<div class="box content has-background-white-ter is-shadowless">
{{ report.user.summary | to_markdown | safe }}
</div>
{% endif %}
{% include 'user_admin/user_info.html' with user=report.user %}
<p class="mt-2"><a href="{{ report.user.local_path }}">{% trans "View user profile" %}</a></p>
</div>
</div>
{% if not report.user.local %}
{% with server=report.user.federated_server %}
<div class="column is-half is-flex is-flex-direction-column">
<h4 class="title is-4">{% trans "Instance details" %}</h4>
<div class="box content is-flex-grow-1">
{% if server %}
<h5>{{ server.server_name }}</h5>
<dl>
<div class="is-flex">
<dt>{% trans "Software:" %}</dt>
<dd>{{ server.application_type }}</dd>
</div>
<div class="is-flex">
<dt>{% trans "Version:" %}</dt>
<dd>{{ server.application_version }}</dd>
</div>
<div class="is-flex">
<dt>{% trans "Status:" %}</dt>
<dd>{{ server.status }}</dd>
</div>
</dl>
{% if server.notes %}
<h5>{% trans "Notes" %}</h5>
<div class="box content has-background-white-ter is-shadowless">
{{ server.notes }}
</div>
{% endif %}
<p>
<a href="{% url 'settings-federated-server' server.id %}">{% trans "View instance" %}</a>
</p>
{% else %}
<em>{% trans "Not set" %}</em>
{% endif %}
</div>
</div>
{% endwith %}
{% endif %}
</div>
<div class="block content">
<h3>{% trans "Actions" %}</h3>
<div class="is-flex">
<p class="mr-1">
<a class="button" href="{% url 'direct-messages-user' report.user.username %}">{% trans "Send direct message" %}</a>
</p>
<form name="deactivate" method="post" action="{% url 'settings-report-deactivate' report.id %}">
{% csrf_token %}
{% if report.user.is_active %}
<button type="submit" class="button is-danger is-light">{% trans "Deactivate user" %}</button>
{% else %}
<button class="button">{% trans "Reactivate user" %}</button>
{% endif %}
</form>
</div>
</div>
{% include 'user_admin/user_moderation_actions.html' with user=report.user %}
<div class="block">
<h3 class="title is-4">{% trans "Moderator Comments" %}</h3>

View file

@ -30,7 +30,7 @@
</ul>
</div>
{% include 'settings/user_admin_filters.html' %}
{% include 'user_admin/user_admin_filters.html' %}
<div class="block">
{% if not reports %}

View file

@ -123,7 +123,7 @@
{% include 'snippets/status_preview.html' with status=related_status %}
</div>
<div class="column is-narrow {% if notification.notification_type == 'REPLY' or notification.notification_type == 'MENTION' %}has-text-black{% else %}has-text-grey-dark{% endif %}">
{{ related_status.published_date | post_date }}
{{ related_status.published_date|timesince }}
{% include 'snippets/privacy-icons.html' with item=related_status %}
</div>
</div>

View file

@ -7,23 +7,23 @@
{% block dropdown-list %}
{% for shelf in request.user.shelf_set.all %}
<li role="menuitem">
<form class="dropdown-item pt-0 pb-0" name="shelve" action="/shelve/" method="post">
<li role="menuitem" class="dropdown-item p-0">
<form name="shelve" action="/shelve/" method="post">
{% csrf_token %}
<input type="hidden" name="book" value="{{ book.id }}">
<input type="hidden" name="change-shelf-from" value="{{ current.identifier }}">
<input type="hidden" name="shelf" value="{{ shelf.identifier }}">
<button class="button is-fullwidth is-small shelf-option" type="submit" {% if shelf.identifier == current.identifier %}disabled{% endif %}><span>{{ shelf.name }}</span></button>
<button class="button is-fullwidth is-small shelf-option is-radiusless is-white" type="submit" {% if shelf.identifier == current.identifier %}disabled{% endif %}><span>{{ shelf.name }}</span></button>
</form>
</li>
{% endfor %}
<li class="navbar-divider" role="presentation"></li>
<li>
<form class="dropdown-item pt-0 pb-0" name="shelve" action="/unshelve/" method="post">
<li class="navbar-divider" role="separator"></li>
<li role="menuitem" class="dropdown-item p-0">
<form name="shelve" action="/unshelve/" method="post">
{% csrf_token %}
<input type="hidden" name="book" value="{{ book.id }}">
<input type="hidden" name="shelf" value="{{ current.id }}">
<button class="button is-fullwidth is-small is-danger is-light" type="submit">{% trans "Remove" %}</button>
<button class="button is-fullwidth is-small is-radiusless is-danger is-light" type="submit">{% trans "Remove" %}</button>
</form>
</li>
{% endblock %}

View file

@ -7,5 +7,5 @@
{% endblock %}
{% block dropdown-list %}
{% include 'snippets/shelve_button/shelve_button_options.html' with active_shelf=active_shelf shelves=request.user.shelf_set.all dropdown=True class="shelf-option is-fullwidth is-small" %}
{% include 'snippets/shelve_button/shelve_button_options.html' with active_shelf=active_shelf shelves=request.user.shelf_set.all dropdown=True class="shelf-option is-fullwidth is-small is-radiusless is-white" %}
{% endblock %}

View file

@ -2,8 +2,8 @@
{% load i18n %}
{% for shelf in shelves %}
{% comparison_bool shelf.identifier active_shelf.shelf.identifier as is_current %}
{% if dropdown %}<li role="menuitem">{% endif %}
<div class="{% if dropdown %}dropdown-item pt-0 pb-0{% elif active_shelf.shelf.identifier|next_shelf != shelf.identifier %}is-hidden{% endif %}">
{% if dropdown %}<li role="menuitem" class="dropdown-item p-0">{% endif %}
<div class="{% if not dropdown and active_shelf.shelf.identifier|next_shelf != shelf.identifier %}is-hidden{% endif %}">
{% if shelf.identifier == 'reading' %}{% if not dropdown or active_shelf.shelf.identifier|next_shelf != shelf.identifier %}
{% trans "Start reading" as button_text %}
{% include 'snippets/toggle/toggle_button.html' with class=class text=button_text controls_text="start-reading" controls_uid=button_uuid focus="modal-title-start-reading" disabled=is_current %}
@ -30,24 +30,20 @@
{% if dropdown %}
{% if readthrough and active_shelf.shelf.identifier != 'read' %}
<li role="menuitem">
<div class="dropdown-item pt-0 pb-0">
{% trans "Update progress" as button_text %}
{% include 'snippets/toggle/toggle_button.html' with class=class text=button_text controls_text="progress-update" controls_uid=button_uuid focus="modal-title-progress-update" %}
</div>
<li role="menuitem" class="dropdown-item p-0">
{% trans "Update progress" as button_text %}
{% include 'snippets/toggle/toggle_button.html' with class=class text=button_text controls_text="progress-update" controls_uid=button_uuid focus="modal-title-progress-update" %}
</li>
{% endif %}
{% if active_shelf.shelf %}
<li role="menuitem">
<div class="dropdown-item pt-0 pb-0">
<form name="shelve" action="/unshelve/" method="post">
{% csrf_token %}
<input type="hidden" name="book" value="{{ active_shelf.book.id }}">
<input type="hidden" name="shelf" value="{{ active_shelf.shelf.id }}">
<button class="button is-fullwidth is-small is-danger is-light" type="submit">{% blocktrans with name=active_shelf.shelf.name %}Remove from {{ name }}{% endblocktrans %}</button>
</form>
</div>
<li role="menuitem" class="dropdown-item p-0">
<form name="shelve" action="/unshelve/" method="post">
{% csrf_token %}
<input type="hidden" name="book" value="{{ active_shelf.book.id }}">
<input type="hidden" name="shelf" value="{{ active_shelf.shelf.id }}">
<button class="button is-fullwidth is-small{% if dropdown %} is-radiusless{% endif %} is-danger is-light" type="submit">{% blocktrans with name=active_shelf.shelf.name %}Remove from {{ name }}{% endblocktrans %}</button>
</form>
</li>
{% endif %}

View file

@ -1,7 +1,7 @@
{% spaceless %}
{% load i18n %}
<p class="stars">
<span class="stars">
<span class="is-sr-only">
{% if rating %}
{% blocktranslate trimmed with rating=rating|floatformat count counter=rating|length %}
@ -23,5 +23,5 @@
aria-hidden="true"
></span>
{% endfor %}
</p>
</span>
{% endspaceless %}

View file

@ -1,14 +0,0 @@
{% load bookwyrm_tags %}
<div class="columns">
<div class="column is-narrow">
<div>
<a href="{{ book.local_path }}">{% include 'snippets/book_cover.html' with book=book %}</a>
{% include 'snippets/stars.html' with rating=book|rating:request.user %}
{% include 'snippets/shelve_button/shelve_button.html' with book=book %}
</div>
</div>
<div class="column">
<h3 class="title is-6">{% include 'snippets/book_titleby.html' with book=book %}</h3>
{% include 'snippets/trimmed_text.html' with full=book|book_description %}
</div>
</div>

View file

@ -0,0 +1,128 @@
{% load bookwyrm_tags %}
{% load i18n %}
{% with status_type=status.status_type %}
<div
class="block"
{% if status_type == "Review" %}
itemprop="rating"
itemtype="https://schema.org/Rating"
{% endif %}
>
<div class="columns">
{% if not hide_book %}
{% with book=status.book|default:status.mention_books.first %}
{% if book %}
<div class="column is-narrow is-hidden-mobile">
<a href="{{ book.local_path }}">{% include 'snippets/book_cover.html' with book=book %}</a>
{% include 'snippets/stars.html' with rating=book|rating:request.user %}
{% include 'snippets/shelve_button/shelve_button.html' with book=book %}
</div>
{% endif %}
{% endwith %}
{% endif %}
<article class="column">
{% if status_type == 'Review' %}
<header class="mb-2">
<h3
class="title is-5 has-subtitle"
dir="auto"
itemprop="name"
>
{{ status.name|escape }}
</h3>
<h4 class="subtitle is-6">
<span
class="is-hidden"
{% if status_type == "Review" %}
itemprop="reviewRating"
itemscope
itemtype="https://schema.org/Rating"
{% endif %}
>
<meta itemprop="ratingValue" content="{{ status.rating|floatformat }}">
{# @todo Is it possible to not hard-code the value? #}
<meta itemprop="bestRating" content="5">
</span>
{% include 'snippets/stars.html' with rating=status.rating %}
</h4>
</header>
{% endif %}
{% if status.content_warning %}
<div>
<p>{{ status.content_warning }}</p>
{% trans "Show more" as button_text %}
{% with text=button_text class="is-small" controls_text="show-status-cw" controls_uid=status.id %}
{% include 'snippets/toggle/open_button.html' %}
{% endwith %}
</div>
{% endif %}
<div
{% if status.content_warning %}
id="show-status-cw-{{ status.id }}"
class="is-hidden"
{% endif %}
>
{% if status.content_warning %}
{% trans "Show less" as button_text %}
{% with text=button_text class="is-small" controls_text="show-status-cw" controls_uid=status.id %}
{% include 'snippets/toggle/close_button.html' %}
{% endwith %}
{% endif %}
{% if status.quote %}
<div class="quote block">
<blockquote dir="auto" class="content mb-2">{{ status.quote | safe }}</blockquote>
<p> &mdash; {% include 'snippets/book_titleby.html' with book=status.book %}</p>
</div>
{% endif %}
{% if status.content and status_type != 'GeneratedNote' and status_type != 'Announce' %}
{% with full=status.content|safe no_trim=status.content_warning itemprop="reviewBody" %}
{% include 'snippets/trimmed_text.html' %}
{% endwith %}
{% endif %}
{% if status.attachments.exists %}
<div class="block">
<div class="columns">
{% for attachment in status.attachments.all %}
<div class="column is-narrow">
<figure class="image is-128x128">
<a
href="/images/{{ attachment.image }}"
target="_blank"
aria-label="{% trans 'Open image in new window' %}"
>
<img
src="/images/{{ attachment.image }}"
{% if attachment.caption %}
alt="{{ attachment.caption }}"
title="{{ attachment.caption }}"
{% endif %}
>
</a>
</figure>
</div>
{% endfor %}
</div>
</div>
{% endif %}
</div>
</article>
</div>
</div>
{% endwith %}

View file

@ -0,0 +1,23 @@
{% spaceless %}
{% load bookwyrm_tags %}
{% load i18n %}
{% if not hide_book %}
{% with book=status.book|default:status.mention_books.first %}
<div class="columns is-mobile">
<div class="column is-narrow">
<div>
<a href="{{ book.local_path }}">{% include 'snippets/book_cover.html' with book=book size="small" %}</a>
</div>
</div>
<div class="column">
<h3 class="title is-6 mb-1">{% include 'snippets/book_titleby.html' with book=book %}</h3>
<p>{{ book|book_description|default:""|truncatewords_html:20 }}</p>
{% include 'snippets/shelve_button/shelve_button.html' with book=book %}
</div>
</div>
{% endwith %}
{% endif %}
{% endspaceless %}

View file

@ -0,0 +1,85 @@
{% extends 'components/card.html' %}
{% load i18n %}
{% load bookwyrm_tags %}
{% load humanize %}
{% block card-header %}
<h3 class="card-header-title has-background-white-ter is-block">
{% include 'snippets/status/status_header.html' with status=status %}
</h3>
{% endblock %}
{% block card-content %}{% endblock %}
{% block card-footer %}
<div class="card-footer-item">
{% if moderation_mode and perms.bookwyrm.moderate_post %}
{# moderation options #}
<form name="delete-{{status.id}}" action="/delete-status/{{ status.id }}" method="post">
{% csrf_token %}
<button class="button is-danger is-light" type="submit">
{% trans "Delete status" %}
</button>
</form>
{% elif no_interact %}
{# nothing here #}
{% elif request.user.is_authenticated %}
<div class="field has-addons">
<div class="control">
{% trans "Reply" as button_text %}
{% include 'snippets/toggle/toggle_button.html' with controls_text="show-comment" controls_uid=status.id text=button_text icon="comment" class="is-small toggle-button" focus="id_content_reply" %}
</div>
<div class="control">
{% include 'snippets/boost_button.html' with status=status %}
</div>
<div class="control">
{% include 'snippets/fav_button.html' with status=status %}
</div>
</div>
{% else %}
<a href="/login">
<span class="icon icon-comment" title="{% trans 'Reply' %}">
<span class="is-sr-only">{% trans "Reply" %}</span>
</span>
<span class="icon icon-boost" title="{% trans 'Boost status' %}">
<span class="is-sr-only">{% trans "Boost status" %}</span>
</span>
<span class="icon icon-heart" title="{% trans 'Like status' %}">
<span class="is-sr-only">{% trans "Like status" %}</span>
</span>
</a>
{% endif %}
</div>
<div class="card-footer-item">
{% include 'snippets/privacy-icons.html' with item=status %}
</div>
<div class="card-footer-item">
<a href="{{ status.remote_id }}">{{ status.published_date|timesince }}</a>
</div>
{% if not moderation_mode %}
<div class="card-footer-item">
{% include 'snippets/status/status_options.html' with class="is-small" right=True %}
</div>
{% endif %}
{% endblock %}
{% block card-bonus %}
{% if request.user.is_authenticated and not moderation_mode %}
{% with status.id|uuid as uuid %}
<section class="is-hidden" id="show-comment-{{ status.id }}">
<div class="card-footer">
<div class="card-footer-item">
{% include 'snippets/create_status_form.html' with reply_parent=status type="reply" %}
</div>
</div>
</section>
{% endwith %}
{% endif %}
{% endblock %}

View file

@ -1,90 +1,14 @@
{% extends 'components/card.html' %}
{% load i18n %}
{% load bookwyrm_tags %}
{% load humanize %}
{% block card-header %}
<h3 class="card-header-title has-background-white-ter is-block">
{% include 'snippets/status/status_header.html' with status=status %}
</h3>
{% endblock %}
{% extends 'snippets/status/layout.html' %}
{% block card-content %}
{% include 'snippets/status/status_content.html' with status=status %}
{% endblock %}
{% with status_type=status.status_type %}
{% block card-footer %}
<div class="card-footer-item">
{% if moderation_mode and perms.bookwyrm.moderate_post %}
{# moderation options #}
<form class="dropdown-item pt-0 pb-0" name="delete-{{status.id}}" action="/delete-status/{{ status.id }}" method="post">
{% csrf_token %}
<button class="button is-danger is-light" type="submit">
{% trans "Delete status" %}
</button>
</form>
{% elif no_interact %}
{# nothing here #}
{% elif request.user.is_authenticated %}
<div class="field has-addons">
<div class="control">
{% trans "Reply" as button_text %}
{% include 'snippets/toggle/toggle_button.html' with controls_text="show-comment" controls_uid=status.id text=button_text icon="comment" class="is-small toggle-button" focus="id_content_reply" %}
</div>
<div class="control">
{% include 'snippets/boost_button.html' with status=status %}
</div>
<div class="control">
{% include 'snippets/fav_button.html' with status=status %}
</div>
</div>
{% else %}
<a href="/login">
<span class="icon icon-comment" title="{% trans 'Reply' %}">
<span class="is-sr-only">{% trans "Reply" %}</span>
</span>
<span class="icon icon-boost" title="{% trans 'Boost status' %}">
<span class="is-sr-only">{% trans "Boost status" %}</span>
</span>
<span class="icon icon-heart" title="{% trans 'Like status' %}">
<span class="is-sr-only">{% trans "Like status" %}</span>
</span>
</a>
{% endif %}
</div>
<div class="card-footer-item">
{% include 'snippets/privacy-icons.html' with item=status %}
</div>
<div class="card-footer-item">
<a href="{{ status.remote_id }}">{{ status.published_date | post_date }}</a>
</div>
{% if not moderation_mode %}
<div class="card-footer-item">
{% include 'snippets/status/status_options.html' with class="is-small" right=True %}
</div>
{% if status_type == 'GeneratedNote' or status_type == 'Rating' %}
{% include 'snippets/status/generated_status.html' with status=status %}
{% else %}
{% include 'snippets/status/content_status.html' with status=status %}
{% endif %}
{% endblock %}
{% block card-bonus %}
{% if request.user.is_authenticated and not moderation_mode %}
{% with status.id|uuid as uuid %}
<section class="is-hidden" id="show-comment-{{ status.id }}">
<div class="card-footer">
<div class="card-footer-item">
{% include 'snippets/create_status_form.html' with reply_parent=status type="reply" %}
</div>
</div>
</section>
{% endwith %}
{% endif %}
{% endblock %}

View file

@ -1,5 +1,3 @@
{% spaceless %}
{% load bookwyrm_tags %}
{% load i18n %}
@ -134,4 +132,3 @@
{% endif %}
{% endif %}
{% endwith %}
{% endspaceless %}

View file

@ -40,10 +40,29 @@
{% endwith %}
{% endif %}
{% if status.book %}
<a href="/book/{{ status.book.id }}">{{ status.book.title }}</a>
{% if status.status_type == 'GeneratedNote' or status.status_type == 'Rating' %}
<a href="/book/{{ status.book.id }}">{{ status.book.title }}</a>{% if status.status_type == 'Rating' %}:
<span
itemprop="reviewRating"
itemscope
itemtype="https://schema.org/Rating"
>
<span class="is-hidden" {{ rating_type }}>
<meta itemprop="ratingValue" content="{{ status.rating|floatformat }}">
{# @todo Is it possible to not hard-code the value? #}
<meta itemprop="bestRating" content="5">
</span>
{% include 'snippets/stars.html' with rating=status.rating %}
{% endif %}
{% else %}
{% include 'snippets/book_titleby.html' with book=status.book %}
{% endif %}
{% elif status.mention_books %}
<a href="/book/{{ status.mention_books.first.id }}">{{ status.mention_books.first.title }}</a>
<a href="/book/{{ status.mention_books.first.id }}">{{ status.mention_books.first.title }}</a>
{% endif %}
{% if status.progress %}

View file

@ -11,19 +11,19 @@
{% block dropdown-list %}
{% if status.user == request.user %}
{# things you can do to your own statuses #}
<li role="menuitem">
<form class="dropdown-item pt-0 pb-0" name="delete-{{status.id}}" action="/delete-status/{{ status.id }}" method="post">
<li role="menuitem" class="dropdown-item p-0">
<form name="delete-{{status.id}}" action="/delete-status/{{ status.id }}" method="post">
{% csrf_token %}
<button class="button is-danger is-light is-fullwidth is-small" type="submit">
<button class="button is-radiusless is-danger is-light is-fullwidth is-small" type="submit">
{% trans "Delete status" %}
</button>
</form>
</li>
{% if status.status_type != 'GeneratedNote' and status.status_type != 'Rating' %}
<li role="menuitem">
<form class="dropdown-item pt-0 pb-0" name="delete-{{status.id}}" action="{% url 'redraft' status.id %}" method="post">
<li role="menuitem" class="dropdown-item p-0">
<form class="" name="delete-{{status.id}}" action="{% url 'redraft' status.id %}" method="post">
{% csrf_token %}
<button class="button is-danger is-light is-fullwidth is-small" type="submit">
<button class="button is-radiusless is-danger is-light is-fullwidth is-small" type="submit">
{% trans "Delete & re-draft" %}
</button>
</form>
@ -31,13 +31,15 @@
{% endif %}
{% else %}
{# things you can do to other people's statuses #}
<li role="menuitem">
<a href="{% url 'direct-messages-user' status.user|username %}" class="button is-small is-fullwidth">{% trans "Send direct message" %}</a>
<li role="menuitem" class="dropdown-item p-0">
<a href="{% url 'direct-messages-user' status.user|username %}" class="button is-small is-white is-radiusless is-fullwidth">
{% trans "Send direct message" %}
</a>
</li>
<li role="menuitem">
<li role="menuitem" class="dropdown-item p-0">
{% include 'snippets/report_button.html' with user=status.user status=status %}
</li>
<li role="menuitem">
<li role="menuitem" class="dropdown-item p-0">
{% include 'snippets/block_button.html' with user=status.user class="is-fullwidth" %}
</li>
{% endif %}

View file

@ -1,11 +1,10 @@
{% spaceless %}
{% load bookwyrm_tags %}
{% load i18n %}
{% with 0|uuid as uuid %}
{% if full %}
{% with full|to_markdown|safe as full %}
{% with full|to_markdown|safe|truncatewords_html:60 as trimmed %}
{% with full|to_markdown|safe|truncatewords_html:150 as trimmed %}
{% if not no_trim and trimmed != full %}
<div id="hide-full-{{ uuid }}">
<div class="content" id="trimmed-{{ uuid }}">
@ -46,4 +45,3 @@
{% endwith %}
{% endif %}
{% endwith %}
{% endspaceless %}

View file

@ -0,0 +1,19 @@
{% extends 'settings/admin_layout.html' %}
{% load i18n %}
{% load bookwyrm_tags %}
{% load humanize %}
{% block title %}{{ user.username }}{% endblock %}
{% block header %}{{ user.username }}{% endblock %}
{% block panel %}
<div class="block">
<a href="{% url 'settings-users' %}">{% trans "Back to users" %}</a>
</div>
{% include 'user_admin/user_info.html' with user=user %}
{% include 'user_admin/user_moderation_actions.html' with user=user %}
{% endblock %}

View file

@ -13,7 +13,7 @@
{% block panel %}
{% include 'settings/user_admin_filters.html' %}
{% include 'user_admin/user_admin_filters.html' %}
<table class="table is-striped">
<tr>
@ -41,7 +41,7 @@
</tr>
{% for user in users %}
<tr>
<td>{{ user.username }}</td>
<td><a href="{% url 'settings-user' user.id %}">{{ user.username }}</a></td>
<td>{{ user.created_date }}</td>
<td>{{ user.last_active_date }}</td>
<td>{% if user.is_active %}{% trans "Active" %}{% else %}{% trans "Inactive" %}{% endif %}</td>

View file

@ -1,6 +1,6 @@
{% extends 'snippets/filters_panel/filters_panel.html' %}
{% block filter_fields %}
{% include 'settings/server_filter.html' %}
{% include 'settings/username_filter.html' %}
{% include 'user_admin/server_filter.html' %}
{% include 'user_admin/username_filter.html' %}
{% endblock %}

View file

@ -0,0 +1,56 @@
{% load i18n %}
{% load bookwyrm_tags %}
<div class="block columns">
<div class="column is-flex is-flex-direction-column">
<h4 class="title is-4">{% trans "User details" %}</h4>
<div class="box is-flex-grow-1">
{% include 'user/user_preview.html' with user=user %}
{% if user.summary %}
<div class="box content has-background-white-ter is-shadowless">
{{ user.summary | to_markdown | safe }}
</div>
{% endif %}
<p class="mt-2"><a href="{{ user.local_path }}">{% trans "View user profile" %}</a></p>
</div>
</div>
{% if not user.local %}
{% with server=user.federated_server %}
<div class="column is-half is-flex is-flex-direction-column">
<h4 class="title is-4">{% trans "Instance details" %}</h4>
<div class="box content is-flex-grow-1">
{% if server %}
<h5>{{ server.server_name }}</h5>
<dl>
<div class="is-flex">
<dt>{% trans "Software:" %}</dt>
<dd>{{ server.application_type }}</dd>
</div>
<div class="is-flex">
<dt>{% trans "Version:" %}</dt>
<dd>{{ server.application_version }}</dd>
</div>
<div class="is-flex">
<dt>{% trans "Status:" %}</dt>
<dd>{{ server.status }}</dd>
</div>
</dl>
{% if server.notes %}
<h5>{% trans "Notes" %}</h5>
<div class="box content has-background-white-ter is-shadowless">
{{ server.notes }}
</div>
{% endif %}
<p>
<a href="{% url 'settings-federated-server' server.id %}">{% trans "View instance" %}</a>
</p>
{% else %}
<em>{% trans "Not set" %}</em>
{% endif %}
</div>
</div>
{% endwith %}
{% endif %}
</div>

View file

@ -0,0 +1,42 @@
{% load i18n %}
<div class="block content">
<h3>{% trans "Actions" %}</h3>
<div class="is-flex">
<p class="mr-1">
<a class="button" href="{% url 'direct-messages-user' user.username %}">{% trans "Send direct message" %}</a>
</p>
<form name="suspend" method="post" action="{% url 'settings-report-suspend' user.id %}">
{% csrf_token %}
{% if user.is_active %}
<button type="submit" class="button is-danger is-light">{% trans "Suspend user" %}</button>
{% else %}
<button class="button">{% trans "Un-suspend user" %}</button>
{% endif %}
</form>
</div>
{% if user.local %}
<div>
<form name="permission" method="post" action="{% url 'settings-user' user.id %}">
{% csrf_token %}
<label class="label" for="id_user_group">{% trans "Access level:" %}</label>
{% if group_form.non_field_errors %}
{{ group_form.non_field_errors }}
{% endif %}
{% with group=user.groups.first %}
<div class="select">
<select name="groups" id="id_user_group">
{% for value, name in group_form.fields.groups.choices %}
<option value="{{ value }}" {% if name == group.name %}selected{% endif %}>{{ name|title }}</option>
{% endfor %}
<option value="" {% if not group %}selected{% endif %}>User</option>
</select>
</div>
{% for error in group_form.groups.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
{% endwith %}
<button class="button">{% trans "Save" %}</button>
</form>
</div>
{% endif %}
</div>

View file

@ -1,11 +1,8 @@
""" template filters """
from uuid import uuid4
from datetime import datetime
from dateutil.relativedelta import relativedelta
from django import template
from django.db.models import Avg
from django.utils import timezone
from bookwyrm import models, views
from bookwyrm.views.status import to_markdown
@ -62,14 +59,10 @@ def get_notification_count(user):
def get_replies(status):
""" get all direct replies to a status """
# TODO: this limit could cause problems
return (
models.Status.objects.filter(
reply_parent=status,
deleted=False,
)
.select_subclasses()
.all()[:10]
)
return models.Status.objects.filter(
reply_parent=status,
deleted=False,
).select_subclasses()[:10]
@register.filter(name="parent")
@ -133,28 +126,6 @@ def get_uuid(identifier):
return "%s%s" % (identifier, uuid4())
@register.filter(name="post_date")
def time_since(date):
""" concise time ago function """
if not isinstance(date, datetime):
return ""
now = timezone.now()
if date < (now - relativedelta(weeks=1)):
formatter = "%b %-d"
if date.year != now.year:
formatter += " %Y"
return date.strftime(formatter)
delta = relativedelta(now, date)
if delta.days:
return "%dd" % delta.days
if delta.hours:
return "%dh" % delta.hours
if delta.minutes:
return "%dm" % delta.minutes
return "%ds" % delta.seconds
@register.filter(name="to_markdown")
def get_markdown(content):
""" convert markdown to html """

View file

@ -51,6 +51,7 @@ class List(TestCase):
book_list=book_list,
book=self.book,
user=self.local_user,
order=1,
)
self.assertTrue(item.approved)
@ -65,7 +66,11 @@ class List(TestCase):
)
item = models.ListItem.objects.create(
book_list=book_list, book=self.book, user=self.local_user, approved=False
book_list=book_list,
book=self.book,
user=self.local_user,
approved=False,
order=1,
)
self.assertFalse(item.approved)

View file

@ -181,36 +181,6 @@ class TemplateTags(TestCase):
uuid = bookwyrm_tags.get_uuid("hi")
self.assertTrue(re.match(r"hi[A-Za-z0-9\-]", uuid))
def test_time_since(self, _):
""" ultraconcise timestamps """
self.assertEqual(bookwyrm_tags.time_since("bleh"), "")
now = timezone.now()
self.assertEqual(bookwyrm_tags.time_since(now), "0s")
seconds_ago = now - relativedelta(seconds=4)
self.assertEqual(bookwyrm_tags.time_since(seconds_ago), "4s")
minutes_ago = now - relativedelta(minutes=8)
self.assertEqual(bookwyrm_tags.time_since(minutes_ago), "8m")
hours_ago = now - relativedelta(hours=9)
self.assertEqual(bookwyrm_tags.time_since(hours_ago), "9h")
days_ago = now - relativedelta(days=3)
self.assertEqual(bookwyrm_tags.time_since(days_ago), "3d")
# I am not going to figure out how to mock dates tonight.
months_ago = now - relativedelta(months=5)
self.assertTrue(
re.match(r"[A-Z][a-z]{2} \d?\d", bookwyrm_tags.time_since(months_ago))
)
years_ago = now - relativedelta(years=10)
self.assertTrue(
re.match(r"[A-Z][a-z]{2} \d?\d \d{4}", bookwyrm_tags.time_since(years_ago))
)
def test_get_markdown(self, _):
""" mardown format data """
result = bookwyrm_tags.get_markdown("_hi_")

View file

@ -94,6 +94,7 @@ class InboxAdd(TestCase):
"type": "ListItem",
"book": self.book.remote_id,
"id": "https://bookwyrm.social/listbook/6189",
"order": 1,
},
"target": "https://bookwyrm.social/user/mouse/list/to-read",
"@context": "https://www.w3.org/ns/activitystreams",

View file

@ -80,6 +80,7 @@ class InboxRemove(TestCase):
user=self.local_user,
book=self.book,
book_list=booklist,
order=1,
)
self.assertEqual(booklist.books.count(), 1)

View file

@ -39,6 +39,25 @@ class ListViews(TestCase):
remote_id="https://example.com/book/1",
parent_work=work,
)
work_two = models.Work.objects.create(title="Labori")
self.book_two = models.Edition.objects.create(
title="Example Edition 2",
remote_id="https://example.com/book/2",
parent_work=work_two,
)
work_three = models.Work.objects.create(title="Trabajar")
self.book_three = models.Edition.objects.create(
title="Example Edition 3",
remote_id="https://example.com/book/3",
parent_work=work_three,
)
work_four = models.Work.objects.create(title="Travailler")
self.book_four = models.Edition.objects.create(
title="Example Edition 4",
remote_id="https://example.com/book/4",
parent_work=work_four,
)
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
self.list = models.List.objects.create(
name="Test List", user=self.local_user
@ -194,6 +213,7 @@ class ListViews(TestCase):
user=self.local_user,
book=self.book,
approved=False,
order=1,
)
request = self.factory.post(
@ -208,7 +228,7 @@ class ListViews(TestCase):
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay") as mock:
view(request, self.list.id)
self.assertEqual(mock.call_count, 1)
self.assertEqual(mock.call_count, 2)
activity = json.loads(mock.call_args[0][1])
self.assertEqual(activity["type"], "Add")
self.assertEqual(activity["actor"], self.local_user.remote_id)
@ -228,6 +248,7 @@ class ListViews(TestCase):
user=self.local_user,
book=self.book,
approved=False,
order=1,
)
request = self.factory.post(
@ -268,6 +289,261 @@ class ListViews(TestCase):
self.assertEqual(item.user, self.local_user)
self.assertTrue(item.approved)
def test_add_two_books(self):
"""
Putting two books on the list. The first should have an order value of
1 and the second should have an order value of 2.
"""
request_one = self.factory.post(
"",
{
"book": self.book.id,
"list": self.list.id,
},
)
request_one.user = self.local_user
request_two = self.factory.post(
"",
{
"book": self.book_two.id,
"list": self.list.id,
},
)
request_two.user = self.local_user
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
views.list.add_book(request_one)
views.list.add_book(request_two)
items = self.list.listitem_set.order_by("order").all()
self.assertEqual(items[0].book, self.book)
self.assertEqual(items[1].book, self.book_two)
self.assertEqual(items[0].order, 1)
self.assertEqual(items[1].order, 2)
def test_add_three_books_and_remove_second(self):
"""
Put three books on a list and then remove the one in the middle. The
ordering of the list should adjust to not have a gap.
"""
request_one = self.factory.post(
"",
{
"book": self.book.id,
"list": self.list.id,
},
)
request_one.user = self.local_user
request_two = self.factory.post(
"",
{
"book": self.book_two.id,
"list": self.list.id,
},
)
request_two.user = self.local_user
request_three = self.factory.post(
"",
{
"book": self.book_three.id,
"list": self.list.id,
},
)
request_three.user = self.local_user
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
views.list.add_book(request_one)
views.list.add_book(request_two)
views.list.add_book(request_three)
items = self.list.listitem_set.order_by("order").all()
self.assertEqual(items[0].book, self.book)
self.assertEqual(items[1].book, self.book_two)
self.assertEqual(items[2].book, self.book_three)
self.assertEqual(items[0].order, 1)
self.assertEqual(items[1].order, 2)
self.assertEqual(items[2].order, 3)
remove_request = self.factory.post("", {"item": items[1].id})
remove_request.user = self.local_user
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
views.list.remove_book(remove_request, self.list.id)
items = self.list.listitem_set.order_by("order").all()
self.assertEqual(items[0].book, self.book)
self.assertEqual(items[1].book, self.book_three)
self.assertEqual(items[0].order, 1)
self.assertEqual(items[1].order, 2)
def test_adding_book_with_a_pending_book(self):
"""
When a list contains any pending books, the pending books should have
be at the end of the list by order. If a book is added while a book is
pending, its order should precede the pending books.
"""
request = self.factory.post(
"",
{
"book": self.book_three.id,
"list": self.list.id,
},
)
request.user = self.local_user
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
models.ListItem.objects.create(
book_list=self.list,
user=self.local_user,
book=self.book,
approved=True,
order=1,
)
models.ListItem.objects.create(
book_list=self.list,
user=self.rat,
book=self.book_two,
approved=False,
order=2,
)
views.list.add_book(request)
items = self.list.listitem_set.order_by("order").all()
self.assertEqual(items[0].book, self.book)
self.assertEqual(items[0].order, 1)
self.assertTrue(items[0].approved)
self.assertEqual(items[1].book, self.book_three)
self.assertEqual(items[1].order, 2)
self.assertTrue(items[1].approved)
self.assertEqual(items[2].book, self.book_two)
self.assertEqual(items[2].order, 3)
self.assertFalse(items[2].approved)
def test_approving_one_pending_book_from_multiple(self):
"""
When a list contains any pending books, the pending books should have
be at the end of the list by order. If a pending book is approved, then
its order should be at the end of the approved books and before the
remaining pending books.
"""
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
models.ListItem.objects.create(
book_list=self.list,
user=self.local_user,
book=self.book,
approved=True,
order=1,
)
models.ListItem.objects.create(
book_list=self.list,
user=self.local_user,
book=self.book_two,
approved=True,
order=2,
)
models.ListItem.objects.create(
book_list=self.list,
user=self.rat,
book=self.book_three,
approved=False,
order=3,
)
to_be_approved = models.ListItem.objects.create(
book_list=self.list,
user=self.rat,
book=self.book_four,
approved=False,
order=4,
)
view = views.Curate.as_view()
request = self.factory.post(
"",
{
"item": to_be_approved.id,
"approved": "true",
},
)
request.user = self.local_user
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
view(request, self.list.id)
items = self.list.listitem_set.order_by("order").all()
self.assertEqual(items[0].book, self.book)
self.assertEqual(items[0].order, 1)
self.assertTrue(items[0].approved)
self.assertEqual(items[1].book, self.book_two)
self.assertEqual(items[1].order, 2)
self.assertTrue(items[1].approved)
self.assertEqual(items[2].book, self.book_four)
self.assertEqual(items[2].order, 3)
self.assertTrue(items[2].approved)
self.assertEqual(items[3].book, self.book_three)
self.assertEqual(items[3].order, 4)
self.assertFalse(items[3].approved)
def test_add_three_books_and_move_last_to_first(self):
"""
Put three books on the list and move the last book to the first
position.
"""
request_one = self.factory.post(
"",
{
"book": self.book.id,
"list": self.list.id,
},
)
request_one.user = self.local_user
request_two = self.factory.post(
"",
{
"book": self.book_two.id,
"list": self.list.id,
},
)
request_two.user = self.local_user
request_three = self.factory.post(
"",
{
"book": self.book_three.id,
"list": self.list.id,
},
)
request_three.user = self.local_user
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
views.list.add_book(request_one)
views.list.add_book(request_two)
views.list.add_book(request_three)
items = self.list.listitem_set.order_by("order").all()
self.assertEqual(items[0].book, self.book)
self.assertEqual(items[1].book, self.book_two)
self.assertEqual(items[2].book, self.book_three)
self.assertEqual(items[0].order, 1)
self.assertEqual(items[1].order, 2)
self.assertEqual(items[2].order, 3)
set_position_request = self.factory.post("", {"position": 1})
set_position_request.user = self.local_user
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
views.list.set_book_position(set_position_request, items[2].id)
items = self.list.listitem_set.order_by("order").all()
self.assertEqual(items[0].book, self.book_three)
self.assertEqual(items[1].book, self.book)
self.assertEqual(items[2].book, self.book_two)
self.assertEqual(items[0].order, 1)
self.assertEqual(items[1].order, 2)
self.assertEqual(items[2].order, 3)
def test_add_book_outsider(self):
""" put a book on a list """
self.list.curation = "open"
@ -358,6 +634,7 @@ class ListViews(TestCase):
book_list=self.list,
user=self.local_user,
book=self.book,
order=1,
)
self.assertTrue(self.list.listitem_set.exists())
@ -377,9 +654,7 @@ class ListViews(TestCase):
""" take an item off a list """
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
item = models.ListItem.objects.create(
book_list=self.list,
user=self.local_user,
book=self.book,
book_list=self.list, user=self.local_user, book=self.book, order=1
)
self.assertTrue(self.list.listitem_set.exists())
request = self.factory.post(

View file

@ -1,5 +1,4 @@
""" test for app action functionality """
from unittest.mock import patch
from django.template.response import TemplateResponse
from django.test import TestCase
from django.test.client import RequestFactory
@ -115,22 +114,19 @@ class ReportViews(TestCase):
report.refresh_from_db()
self.assertFalse(report.resolved)
def test_deactivate_user(self):
def test_suspend_user(self):
""" toggle whether a user is able to log in """
self.assertTrue(self.rat.is_active)
report = models.Report.objects.create(reporter=self.local_user, user=self.rat)
request = self.factory.post("")
request.user = self.local_user
request.user.is_superuser = True
# de-activate
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
views.deactivate_user(request, report.id)
views.suspend_user(request, self.rat.id)
self.rat.refresh_from_db()
self.assertFalse(self.rat.is_active)
# re-activate
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
views.deactivate_user(request, report.id)
views.suspend_user(request, self.rat.id)
self.rat.refresh_from_db()
self.assertTrue(self.rat.is_active)

View file

@ -30,6 +30,14 @@ class UserViews(TestCase):
self.rat = models.User.objects.create_user(
"rat@local.com", "rat@rat.rat", "password", local=True, localname="rat"
)
self.book = models.Edition.objects.create(title="test")
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
models.ShelfBook.objects.create(
book=self.book,
user=self.local_user,
shelf=self.local_user.shelf_set.first(),
)
models.SiteSettings.objects.create()
self.anonymous_user = AnonymousUser
self.anonymous_user.is_authenticated = False

View file

@ -1,4 +1,6 @@
""" test for app action functionality """
from unittest.mock import patch
from django.contrib.auth.models import Group
from django.template.response import TemplateResponse
from django.test import TestCase
from django.test.client import RequestFactory
@ -21,9 +23,9 @@ class UserAdminViews(TestCase):
)
models.SiteSettings.objects.create()
def test_user_admin_page(self):
def test_user_admin_list_page(self):
""" there are so many views, this just makes sure it LOADS """
view = views.UserAdmin.as_view()
view = views.UserAdminList.as_view()
request = self.factory.get("")
request.user = self.local_user
request.user.is_superuser = True
@ -31,3 +33,38 @@ class UserAdminViews(TestCase):
self.assertIsInstance(result, TemplateResponse)
result.render()
self.assertEqual(result.status_code, 200)
def test_user_admin_page(self):
""" there are so many views, this just makes sure it LOADS """
view = views.UserAdmin.as_view()
request = self.factory.get("")
request.user = self.local_user
request.user.is_superuser = True
result = view(request, self.local_user.id)
self.assertIsInstance(result, TemplateResponse)
result.render()
self.assertEqual(result.status_code, 200)
def test_user_admin_page_post(self):
""" set the user's group """
group = Group.objects.create(name="editor")
self.assertEqual(
list(self.local_user.groups.values_list("name", flat=True)), []
)
view = views.UserAdmin.as_view()
request = self.factory.post("", {"groups": [group.id]})
request.user = self.local_user
request.user.is_superuser = True
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
result = view(request, self.local_user.id)
self.assertIsInstance(result, TemplateResponse)
result.render()
self.assertEqual(
list(self.local_user.groups.values_list("name", flat=True)), ["editor"]
)

View file

@ -51,13 +51,20 @@ urlpatterns = [
r"^password-reset/(?P<code>[A-Za-z0-9]+)/?$", views.PasswordReset.as_view()
),
# admin
re_path(r"^settings/site-settings", views.Site.as_view(), name="settings-site"),
re_path(r"^settings/site-settings/?$", views.Site.as_view(), name="settings-site"),
re_path(
r"^settings/email-preview",
r"^settings/email-preview/?$",
views.site.email_preview,
name="settings-email-preview",
),
re_path(r"^settings/users", views.UserAdmin.as_view(), name="settings-users"),
re_path(
r"^settings/users/?$", views.UserAdminList.as_view(), name="settings-users"
),
re_path(
r"^settings/users/(?P<user>\d+)/?$",
views.UserAdmin.as_view(),
name="settings-user",
),
re_path(
r"^settings/federation/?$",
views.Federation.as_view(),
@ -113,9 +120,9 @@ urlpatterns = [
name="settings-report",
),
re_path(
r"^settings/reports/(?P<report_id>\d+)/deactivate/?$",
views.deactivate_user,
name="settings-report-deactivate",
r"^settings/reports/(?P<user_id>\d+)/suspend/?$",
views.suspend_user,
name="settings-report-suspend",
),
re_path(
r"^settings/reports/(?P<report_id>\d+)/resolve/?$",
@ -184,6 +191,11 @@ urlpatterns = [
views.list.remove_book,
name="list-remove-book",
),
re_path(
r"^list-item/(?P<list_item_id>\d+)/set-position$",
views.list.set_book_position,
name="list-set-book-position",
),
re_path(
r"^list/(?P<list_id>\d+)/curate/?$", views.Curate.as_view(), name="list-curate"
),

View file

@ -25,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
@ -37,5 +37,5 @@ from .status import CreateStatus, DeleteStatus, DeleteAndRedraft
from .tag import Tag, AddTag, RemoveTag
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

@ -30,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:
@ -60,7 +55,7 @@ class Book(View):
paginated = Paginator(
reviews.exclude(Q(content__isnull=True) | Q(content="")), PAGE_LENGTH
)
reviews_page = paginated.get_page(page)
reviews_page = paginated.get_page(request.GET.get("page"))
user_tags = readthroughs = user_shelves = other_edition_shelves = []
if request.user.is_authenticated:
@ -266,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 = {}
@ -280,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.get_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.get_page(page),
"users": paginated.get_page(request.GET.get("page")),
}
return TemplateResponse(request, "directory/directory.html", data)

View file

@ -24,11 +24,6 @@ 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
sort = request.GET.get("sort")
@ -40,7 +35,7 @@ class Federation(View):
paginated = Paginator(servers, PAGE_LENGTH)
data = {
"servers": paginated.get_page(page),
"servers": paginated.get_page(request.GET.get("page")),
"sort": sort,
"form": forms.ServerForm(),
}

View file

@ -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.get_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.get_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",
},
}
@ -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

@ -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.get_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.get_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
@ -16,17 +19,13 @@ from bookwyrm.connectors import connector_manager
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.get_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.get_page(page),
"lists": paginated.get_page(request.GET.get("page")),
"list_form": forms.ListForm(),
"path": user.local_path + "/lists",
}
@ -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:
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)
@ -183,19 +231,30 @@ def add_book(request):
# 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

@ -74,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

View file

@ -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
@ -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.get_page(page),
"books": paginated.get_page(request.GET.get("page")),
}
return TemplateResponse(request, "user/shelf.html", data)

View file

@ -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
@ -87,7 +82,7 @@ class User(View):
"is_self": is_self,
"shelves": shelf_preview,
"shelf_count": shelves.count(),
"activities": paginated.get_page(page),
"activities": paginated.get_page(request.GET.get("page", 1)),
"goal": goal,
}

View file

@ -1,11 +1,12 @@
""" manage user """
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.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
@ -15,16 +16,11 @@ 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:
@ -50,8 +46,32 @@ class UserAdmin(View):
paginated = Paginator(users, PAGE_LENGTH)
data = {
"users": paginated.get_page(page),
"users": paginated.get_page(request.GET.get("page")),
"sort": sort,
"server": server,
}
return TemplateResponse(request, "settings/user_admin.html", data)
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)

Binary file not shown.

File diff suppressed because it is too large Load diff