forked from mirrors/bookwyrm
Merge branch 'main' into smaller-statuses-dense-cards
This commit is contained in:
commit
2cacf5146b
30 changed files with 392 additions and 177 deletions
|
@ -150,6 +150,12 @@ class LimitedEditUserForm(CustomForm):
|
||||||
help_texts = {f: None for f in fields}
|
help_texts = {f: None for f in fields}
|
||||||
|
|
||||||
|
|
||||||
|
class UserGroupForm(CustomForm):
|
||||||
|
class Meta:
|
||||||
|
model = models.User
|
||||||
|
fields = ["groups"]
|
||||||
|
|
||||||
|
|
||||||
class TagForm(CustomForm):
|
class TagForm(CustomForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Tag
|
model = models.Tag
|
||||||
|
|
|
@ -109,7 +109,10 @@
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
<p class="help is-danger">{{ error | escape }}</p>
|
||||||
{% endfor %}
|
{% 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 %}
|
{% for error in form.series.errors %}
|
||||||
<p class="help is-danger">{{ error | escape }}</p>
|
<p class="help is-danger">{{ error | escape }}</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
{% spaceless %}
|
{% spaceless %}
|
||||||
|
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
{% load humanize %}
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
{% with format=book.physical_format pages=book.pages %}
|
{% with format=book.physical_format pages=book.pages %}
|
||||||
|
@ -39,7 +40,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<p>
|
<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 %}
|
{% if date or book.first_published_date %}
|
||||||
<meta
|
<meta
|
||||||
itemprop="datePublished"
|
itemprop="datePublished"
|
||||||
|
|
|
@ -30,26 +30,32 @@
|
||||||
{% include 'snippets/shelve_button/shelve_button.html' with book=item.book %}
|
{% include 'snippets/shelve_button/shelve_button.html' with book=item.book %}
|
||||||
</div>
|
</div>
|
||||||
</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 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>
|
<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>
|
</div>
|
||||||
{% if list.user == request.user or list.curation == 'open' and item.user == request.user %}
|
{% 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">
|
<form name="add-book" method="post" action="{% url 'list-remove-book' list.id %}" class="card-footer-item">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="item" value="{{ item.id }}">
|
<input type="hidden" name="item" value="{{ item.id }}">
|
||||||
<button type="submit" class="button is-small is-danger">{% trans "Remove" %}</button>
|
<button type="submit" class="button is-small is-danger">{% trans "Remove" %}</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
|
||||||
<div class="card-footer has-background-white-bis">
|
|
||||||
<div>
|
|
||||||
<form name="set-position" method="post" action="{% url 'list-set-book-position' item.id %}" class="card-footer-item">
|
|
||||||
{% csrf_token %}
|
|
||||||
<label for="input-list-position" class="is-sr-only">{% trans "List position" %}</label>
|
|
||||||
<input id="input-list-position" class="input" type="number" min="1" name="position" value="{{ item.order }}">
|
|
||||||
<button type="submit" class="button is-small is-info">{% trans "List position" %}</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -15,76 +15,9 @@
|
||||||
{% include 'moderation/report_preview.html' with report=report %}
|
{% include 'moderation/report_preview.html' with report=report %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="block columns">
|
{% include 'user_admin/user_info.html' with user=report.user %}
|
||||||
<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 %}
|
|
||||||
|
|
||||||
<p class="mt-2"><a href="{{ report.user.local_path }}">{% trans "View user profile" %}</a></p>
|
{% include 'user_admin/user_moderation_actions.html' with user=report.user %}
|
||||||
</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>
|
|
||||||
|
|
||||||
<div class="block">
|
<div class="block">
|
||||||
<h3 class="title is-4">{% trans "Moderator Comments" %}</h3>
|
<h3 class="title is-4">{% trans "Moderator Comments" %}</h3>
|
||||||
|
|
|
@ -30,7 +30,7 @@
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% include 'settings/user_admin_filters.html' %}
|
{% include 'user_admin/user_admin_filters.html' %}
|
||||||
|
|
||||||
<div class="block">
|
<div class="block">
|
||||||
{% if not reports %}
|
{% if not reports %}
|
||||||
|
|
|
@ -123,7 +123,7 @@
|
||||||
{% include 'snippets/status_preview.html' with status=related_status %}
|
{% include 'snippets/status_preview.html' with status=related_status %}
|
||||||
</div>
|
</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 %}">
|
<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 %}
|
{% include 'snippets/privacy-icons.html' with item=related_status %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
{% spaceless %}
|
|
||||||
|
|
||||||
{% load bookwyrm_tags %}
|
{% load bookwyrm_tags %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
|
@ -127,4 +125,4 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endspaceless %}
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
{% extends 'components/card.html' %}
|
{% extends 'components/card.html' %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% load bookwyrm_tags %}
|
{% load bookwyrm_tags %}
|
||||||
{% load humanize %}
|
{% load humanize %}
|
||||||
|
|
||||||
|
|
|
@ -11,3 +11,4 @@
|
||||||
|
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
134
bookwyrm/templates/snippets/status/status_content.html
Normal file
134
bookwyrm/templates/snippets/status/status_content.html
Normal file
|
@ -0,0 +1,134 @@
|
||||||
|
{% load bookwyrm_tags %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% with status_type=status.status_type %}
|
||||||
|
<div
|
||||||
|
class="block"
|
||||||
|
|
||||||
|
{% if status_type == 'Review' %}
|
||||||
|
{% firstof "reviewBody" as body_prop %}
|
||||||
|
{% firstof 'itemprop="reviewRating" itemscope itemtype="https://schema.org/Rating"' as rating_type %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if status_type == 'Rating' %}
|
||||||
|
itemprop="rating"
|
||||||
|
itemtype="https://schema.org/Rating"
|
||||||
|
{% endif %}
|
||||||
|
>
|
||||||
|
{% if status_type == 'Review' or status_type == 'Rating' %}
|
||||||
|
<div>
|
||||||
|
{% if status.name %}
|
||||||
|
<h3
|
||||||
|
class="title is-5 has-subtitle"
|
||||||
|
dir="auto"
|
||||||
|
itemprop="name"
|
||||||
|
>
|
||||||
|
{{ status.name|escape }}
|
||||||
|
</h3>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<span
|
||||||
|
class="is-sr-only"
|
||||||
|
{{ rating_type }}
|
||||||
|
>
|
||||||
|
<meta itemprop="ratingValue" content="{{ status.rating|floatformat }}">
|
||||||
|
|
||||||
|
{% if status_type == 'Rating' %}
|
||||||
|
{# @todo Is it possible to not hard-code the value? #}
|
||||||
|
<meta itemprop="bestRating" content="5">
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{% include 'snippets/stars.html' with rating=status.rating %}
|
||||||
|
</div>
|
||||||
|
{% 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> — {% 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=body_prop %}
|
||||||
|
{% 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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if not hide_book %}
|
||||||
|
{% if status.book or status.mention_books.count %}
|
||||||
|
<div
|
||||||
|
{% if status_type != 'GeneratedNote' %}
|
||||||
|
class="box has-background-white-bis"
|
||||||
|
{% endif %}
|
||||||
|
>
|
||||||
|
{% if status.book %}
|
||||||
|
{% with book=status.book %}
|
||||||
|
{% include 'snippets/status/book_preview.html' %}
|
||||||
|
{% endwith %}
|
||||||
|
{% elif status.mention_books.count %}
|
||||||
|
{% with book=status.mention_books.first %}
|
||||||
|
{% include 'snippets/status/book_preview.html' %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
|
@ -1,4 +1,3 @@
|
||||||
{% spaceless %}
|
|
||||||
{% load bookwyrm_tags %}
|
{% load bookwyrm_tags %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
|
@ -46,4 +45,3 @@
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endspaceless %}
|
|
||||||
|
|
19
bookwyrm/templates/user_admin/user.html
Normal file
19
bookwyrm/templates/user_admin/user.html
Normal 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 %}
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
|
|
||||||
{% block panel %}
|
{% block panel %}
|
||||||
|
|
||||||
{% include 'settings/user_admin_filters.html' %}
|
{% include 'user_admin/user_admin_filters.html' %}
|
||||||
|
|
||||||
<table class="table is-striped">
|
<table class="table is-striped">
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -41,7 +41,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
{% for user in users %}
|
{% for user in users %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ user.username }}</td>
|
<td><a href="{% url 'settings-user' user.id %}">{{ user.username }}</a></td>
|
||||||
<td>{{ user.created_date }}</td>
|
<td>{{ user.created_date }}</td>
|
||||||
<td>{{ user.last_active_date }}</td>
|
<td>{{ user.last_active_date }}</td>
|
||||||
<td>{% if user.is_active %}{% trans "Active" %}{% else %}{% trans "Inactive" %}{% endif %}</td>
|
<td>{% if user.is_active %}{% trans "Active" %}{% else %}{% trans "Inactive" %}{% endif %}</td>
|
|
@ -1,6 +1,6 @@
|
||||||
{% extends 'snippets/filters_panel/filters_panel.html' %}
|
{% extends 'snippets/filters_panel/filters_panel.html' %}
|
||||||
|
|
||||||
{% block filter_fields %}
|
{% block filter_fields %}
|
||||||
{% include 'settings/server_filter.html' %}
|
{% include 'user_admin/server_filter.html' %}
|
||||||
{% include 'settings/username_filter.html' %}
|
{% include 'user_admin/username_filter.html' %}
|
||||||
{% endblock %}
|
{% endblock %}
|
56
bookwyrm/templates/user_admin/user_info.html
Normal file
56
bookwyrm/templates/user_admin/user_info.html
Normal 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>
|
||||||
|
|
42
bookwyrm/templates/user_admin/user_moderation_actions.html
Normal file
42
bookwyrm/templates/user_admin/user_moderation_actions.html
Normal 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>
|
|
@ -1,11 +1,8 @@
|
||||||
""" template filters """
|
""" template filters """
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from dateutil.relativedelta import relativedelta
|
|
||||||
from django import template
|
from django import template
|
||||||
from django.db.models import Avg
|
from django.db.models import Avg
|
||||||
from django.utils import timezone
|
|
||||||
|
|
||||||
from bookwyrm import models, views
|
from bookwyrm import models, views
|
||||||
from bookwyrm.views.status import to_markdown
|
from bookwyrm.views.status import to_markdown
|
||||||
|
@ -129,28 +126,6 @@ def get_uuid(identifier):
|
||||||
return "%s%s" % (identifier, uuid4())
|
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")
|
@register.filter(name="to_markdown")
|
||||||
def get_markdown(content):
|
def get_markdown(content):
|
||||||
""" convert markdown to html """
|
""" convert markdown to html """
|
||||||
|
|
|
@ -181,36 +181,6 @@ class TemplateTags(TestCase):
|
||||||
uuid = bookwyrm_tags.get_uuid("hi")
|
uuid = bookwyrm_tags.get_uuid("hi")
|
||||||
self.assertTrue(re.match(r"hi[A-Za-z0-9\-]", uuid))
|
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, _):
|
def test_get_markdown(self, _):
|
||||||
""" mardown format data """
|
""" mardown format data """
|
||||||
result = bookwyrm_tags.get_markdown("_hi_")
|
result = bookwyrm_tags.get_markdown("_hi_")
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
""" test for app action functionality """
|
""" test for app action functionality """
|
||||||
from unittest.mock import patch
|
|
||||||
from django.template.response import TemplateResponse
|
from django.template.response import TemplateResponse
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.test.client import RequestFactory
|
from django.test.client import RequestFactory
|
||||||
|
@ -115,22 +114,19 @@ class ReportViews(TestCase):
|
||||||
report.refresh_from_db()
|
report.refresh_from_db()
|
||||||
self.assertFalse(report.resolved)
|
self.assertFalse(report.resolved)
|
||||||
|
|
||||||
def test_deactivate_user(self):
|
def test_suspend_user(self):
|
||||||
""" toggle whether a user is able to log in """
|
""" toggle whether a user is able to log in """
|
||||||
self.assertTrue(self.rat.is_active)
|
self.assertTrue(self.rat.is_active)
|
||||||
report = models.Report.objects.create(reporter=self.local_user, user=self.rat)
|
|
||||||
request = self.factory.post("")
|
request = self.factory.post("")
|
||||||
request.user = self.local_user
|
request.user = self.local_user
|
||||||
request.user.is_superuser = True
|
request.user.is_superuser = True
|
||||||
|
|
||||||
# de-activate
|
# de-activate
|
||||||
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
views.suspend_user(request, self.rat.id)
|
||||||
views.deactivate_user(request, report.id)
|
|
||||||
self.rat.refresh_from_db()
|
self.rat.refresh_from_db()
|
||||||
self.assertFalse(self.rat.is_active)
|
self.assertFalse(self.rat.is_active)
|
||||||
|
|
||||||
# re-activate
|
# re-activate
|
||||||
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
|
views.suspend_user(request, self.rat.id)
|
||||||
views.deactivate_user(request, report.id)
|
|
||||||
self.rat.refresh_from_db()
|
self.rat.refresh_from_db()
|
||||||
self.assertTrue(self.rat.is_active)
|
self.assertTrue(self.rat.is_active)
|
||||||
|
|
|
@ -30,6 +30,14 @@ class UserViews(TestCase):
|
||||||
self.rat = models.User.objects.create_user(
|
self.rat = models.User.objects.create_user(
|
||||||
"rat@local.com", "rat@rat.rat", "password", local=True, localname="rat"
|
"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()
|
models.SiteSettings.objects.create()
|
||||||
self.anonymous_user = AnonymousUser
|
self.anonymous_user = AnonymousUser
|
||||||
self.anonymous_user.is_authenticated = False
|
self.anonymous_user.is_authenticated = False
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
""" test for app action functionality """
|
""" 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.template.response import TemplateResponse
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.test.client import RequestFactory
|
from django.test.client import RequestFactory
|
||||||
|
@ -21,9 +23,9 @@ class UserAdminViews(TestCase):
|
||||||
)
|
)
|
||||||
models.SiteSettings.objects.create()
|
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 """
|
""" 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 = self.factory.get("")
|
||||||
request.user = self.local_user
|
request.user = self.local_user
|
||||||
request.user.is_superuser = True
|
request.user.is_superuser = True
|
||||||
|
@ -31,3 +33,38 @@ class UserAdminViews(TestCase):
|
||||||
self.assertIsInstance(result, TemplateResponse)
|
self.assertIsInstance(result, TemplateResponse)
|
||||||
result.render()
|
result.render()
|
||||||
self.assertEqual(result.status_code, 200)
|
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"]
|
||||||
|
)
|
||||||
|
|
|
@ -51,13 +51,20 @@ urlpatterns = [
|
||||||
r"^password-reset/(?P<code>[A-Za-z0-9]+)/?$", views.PasswordReset.as_view()
|
r"^password-reset/(?P<code>[A-Za-z0-9]+)/?$", views.PasswordReset.as_view()
|
||||||
),
|
),
|
||||||
# admin
|
# 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(
|
re_path(
|
||||||
r"^settings/email-preview",
|
r"^settings/email-preview/?$",
|
||||||
views.site.email_preview,
|
views.site.email_preview,
|
||||||
name="settings-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(
|
re_path(
|
||||||
r"^settings/federation/?$",
|
r"^settings/federation/?$",
|
||||||
views.Federation.as_view(),
|
views.Federation.as_view(),
|
||||||
|
@ -113,9 +120,9 @@ urlpatterns = [
|
||||||
name="settings-report",
|
name="settings-report",
|
||||||
),
|
),
|
||||||
re_path(
|
re_path(
|
||||||
r"^settings/reports/(?P<report_id>\d+)/deactivate/?$",
|
r"^settings/reports/(?P<user_id>\d+)/suspend/?$",
|
||||||
views.deactivate_user,
|
views.suspend_user,
|
||||||
name="settings-report-deactivate",
|
name="settings-report-suspend",
|
||||||
),
|
),
|
||||||
re_path(
|
re_path(
|
||||||
r"^settings/reports/(?P<report_id>\d+)/resolve/?$",
|
r"^settings/reports/(?P<report_id>\d+)/resolve/?$",
|
||||||
|
|
|
@ -25,7 +25,7 @@ from .notifications import Notifications
|
||||||
from .outbox import Outbox
|
from .outbox import Outbox
|
||||||
from .reading import edit_readthrough, create_readthrough, delete_readthrough
|
from .reading import edit_readthrough, create_readthrough, delete_readthrough
|
||||||
from .reading import start_reading, finish_reading, delete_progressupdate
|
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 .rss_feed import RssFeed
|
||||||
from .password import PasswordResetRequest, PasswordReset, ChangePassword
|
from .password import PasswordResetRequest, PasswordReset, ChangePassword
|
||||||
from .search import Search
|
from .search import Search
|
||||||
|
@ -37,5 +37,5 @@ from .status import CreateStatus, DeleteStatus, DeleteAndRedraft
|
||||||
from .tag import Tag, AddTag, RemoveTag
|
from .tag import Tag, AddTag, RemoveTag
|
||||||
from .updates import get_notification_count, get_unread_status_count
|
from .updates import get_notification_count, get_unread_status_count
|
||||||
from .user import User, EditUser, Followers, Following
|
from .user import User, EditUser, Followers, Following
|
||||||
from .user_admin import UserAdmin
|
from .user_admin import UserAdmin, UserAdminList
|
||||||
from .wellknown import *
|
from .wellknown import *
|
||||||
|
|
|
@ -270,10 +270,10 @@ class Editions(View):
|
||||||
if request.GET.get("format"):
|
if request.GET.get("format"):
|
||||||
filters["physical_format__iexact"] = 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], []))
|
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 = {
|
data = {
|
||||||
"editions": paginated.get_page(request.GET.get("page")),
|
"editions": paginated.get_page(request.GET.get("page")),
|
||||||
"work": work,
|
"work": work,
|
||||||
|
|
|
@ -74,12 +74,13 @@ class Report(View):
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@permission_required("bookwyrm_moderate_user")
|
@permission_required("bookwyrm_moderate_user")
|
||||||
def deactivate_user(_, report_id):
|
def suspend_user(_, user_id):
|
||||||
""" mark an account as inactive """
|
""" mark an account as inactive """
|
||||||
report = get_object_or_404(models.Report, id=report_id)
|
user = get_object_or_404(models.User, id=user_id)
|
||||||
report.user.is_active = not report.user.is_active
|
user.is_active = not user.is_active
|
||||||
report.user.save()
|
# this isn't a full deletion, so we don't want to tell the world
|
||||||
return redirect("settings-report", report.id)
|
user.save(broadcast=False)
|
||||||
|
return redirect("settings-user", user.id)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
|
|
|
@ -59,7 +59,7 @@ class User(View):
|
||||||
{
|
{
|
||||||
"name": user_shelf.name,
|
"name": user_shelf.name,
|
||||||
"local_path": user_shelf.local_path,
|
"local_path": user_shelf.local_path,
|
||||||
"books": user_shelf.books[:3],
|
"books": user_shelf.books.all()[:3],
|
||||||
"size": user_shelf.books.count(),
|
"size": user_shelf.books.count(),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
""" manage user """
|
""" manage user """
|
||||||
from django.contrib.auth.decorators import login_required, permission_required
|
from django.contrib.auth.decorators import login_required, permission_required
|
||||||
from django.core.paginator import Paginator
|
from django.core.paginator import Paginator
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
from django.template.response import TemplateResponse
|
from django.template.response import TemplateResponse
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from django.views import View
|
from django.views import View
|
||||||
|
|
||||||
from bookwyrm import models
|
from bookwyrm import forms, models
|
||||||
from bookwyrm.settings import PAGE_LENGTH
|
from bookwyrm.settings import PAGE_LENGTH
|
||||||
|
|
||||||
|
|
||||||
|
@ -15,7 +16,7 @@ from bookwyrm.settings import PAGE_LENGTH
|
||||||
permission_required("bookwyrm.moderate_users", raise_exception=True),
|
permission_required("bookwyrm.moderate_users", raise_exception=True),
|
||||||
name="dispatch",
|
name="dispatch",
|
||||||
)
|
)
|
||||||
class UserAdmin(View):
|
class UserAdminList(View):
|
||||||
""" admin view of users on this server """
|
""" admin view of users on this server """
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
|
@ -49,4 +50,28 @@ class UserAdmin(View):
|
||||||
"sort": sort,
|
"sort": sort,
|
||||||
"server": server,
|
"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)
|
||||||
|
|
Loading…
Reference in a new issue