Merge branch 'main' into book-format-choices

This commit is contained in:
Mouse Reeve 2021-09-29 11:30:23 -07:00
commit 2f93e6d723
149 changed files with 6807 additions and 5952 deletions

View File

@ -8,4 +8,4 @@ WORKDIR /app
COPY requirements.txt /app/ COPY requirements.txt /app/
RUN pip install -r requirements.txt --no-cache-dir RUN pip install -r requirements.txt --no-cache-dir
RUN apt-get update && apt-get install -y gettext libgettextpo-dev && apt-get clean RUN apt-get update && apt-get install -y gettext libgettextpo-dev tidy && apt-get clean

View File

@ -29,8 +29,7 @@ class CustomForm(ModelForm):
input_type = visible.field.widget.input_type input_type = visible.field.widget.input_type
if isinstance(visible.field.widget, Textarea): if isinstance(visible.field.widget, Textarea):
input_type = "textarea" input_type = "textarea"
visible.field.widget.attrs["cols"] = None visible.field.widget.attrs["rows"] = 5
visible.field.widget.attrs["rows"] = None
visible.field.widget.attrs["class"] = css_classes[input_type] visible.field.widget.attrs["class"] = css_classes[input_type]
@ -228,7 +227,7 @@ class ExpiryWidget(widgets.Select):
elif selected_string == "forever": elif selected_string == "forever":
return None return None
else: else:
return selected_string # "This will raise return selected_string # This will raise
return timezone.now() + interval return timezone.now() + interval
@ -269,7 +268,7 @@ class CreateInviteForm(CustomForm):
class ShelfForm(CustomForm): class ShelfForm(CustomForm):
class Meta: class Meta:
model = models.Shelf model = models.Shelf
fields = ["user", "name", "privacy"] fields = ["user", "name", "privacy", "description"]
class GoalForm(CustomForm): class GoalForm(CustomForm):

View File

@ -0,0 +1,37 @@
# Generated by Django 3.2.4 on 2021-09-22 16:53
from django.db import migrations, models
def set_active_readthrough(apps, schema_editor):
"""best-guess for deactivation date"""
db_alias = schema_editor.connection.alias
apps.get_model("bookwyrm", "ReadThrough").objects.using(db_alias).filter(
start_date__isnull=False,
finish_date__isnull=True,
).update(is_active=True)
def reverse_func(apps, schema_editor):
"""noop"""
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0098_auto_20210918_2238"),
]
operations = [
migrations.AddField(
model_name="readthrough",
name="is_active",
field=models.BooleanField(default=False),
),
migrations.RunPython(set_active_readthrough, reverse_func),
migrations.AlterField(
model_name="readthrough",
name="is_active",
field=models.BooleanField(default=True),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.5 on 2021-09-28 23:20
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0099_readthrough_is_active"),
]
operations = [
migrations.AddField(
model_name="shelf",
name="description",
field=models.TextField(blank=True, max_length=500, null=True),
),
]

View File

@ -1,8 +1,11 @@
""" base model with default fields """ """ base model with default fields """
import base64 import base64
from Crypto import Random from Crypto import Random
from django.core.exceptions import PermissionDenied
from django.db import models from django.db import models
from django.dispatch import receiver from django.dispatch import receiver
from django.http import Http404
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from bookwyrm.settings import DOMAIN from bookwyrm.settings import DOMAIN
@ -48,26 +51,26 @@ class BookWyrmModel(models.Model):
"""how to link to this object in the local app""" """how to link to this object in the local app"""
return self.get_remote_id().replace(f"https://{DOMAIN}", "") return self.get_remote_id().replace(f"https://{DOMAIN}", "")
def visible_to_user(self, viewer): def raise_visible_to_user(self, viewer):
"""is a user authorized to view an object?""" """is a user authorized to view an object?"""
# make sure this is an object with privacy owned by a user # make sure this is an object with privacy owned by a user
if not hasattr(self, "user") or not hasattr(self, "privacy"): if not hasattr(self, "user") or not hasattr(self, "privacy"):
return None return
# viewer can't see it if the object's owner blocked them # viewer can't see it if the object's owner blocked them
if viewer in self.user.blocks.all(): if viewer in self.user.blocks.all():
return False raise Http404()
# you can see your own posts and any public or unlisted posts # you can see your own posts and any public or unlisted posts
if viewer == self.user or self.privacy in ["public", "unlisted"]: if viewer == self.user or self.privacy in ["public", "unlisted"]:
return True return
# you can see the followers only posts of people you follow # you can see the followers only posts of people you follow
if ( if (
self.privacy == "followers" self.privacy == "followers"
and self.user.followers.filter(id=viewer.id).first() and self.user.followers.filter(id=viewer.id).first()
): ):
return True return
# you can see dms you are tagged in # you can see dms you are tagged in
if hasattr(self, "mention_users"): if hasattr(self, "mention_users"):
@ -75,8 +78,32 @@ class BookWyrmModel(models.Model):
self.privacy == "direct" self.privacy == "direct"
and self.mention_users.filter(id=viewer.id).first() and self.mention_users.filter(id=viewer.id).first()
): ):
return True return
return False raise Http404()
def raise_not_editable(self, viewer):
"""does this user have permission to edit this object? liable to be overwritten
by models that inherit this base model class"""
if not hasattr(self, "user"):
return
# generally moderators shouldn't be able to edit other people's stuff
if self.user == viewer:
return
raise PermissionDenied()
def raise_not_deletable(self, viewer):
"""does this user have permission to delete this object? liable to be
overwritten by models that inherit this base model class"""
if not hasattr(self, "user"):
return
# but generally moderators can delete other people's stuff
if self.user == viewer or viewer.has_perm("moderate_post"):
return
raise PermissionDenied()
@receiver(models.signals.post_save) @receiver(models.signals.post_save)

View File

@ -3,8 +3,8 @@ import re
from django.contrib.postgres.search import SearchVectorField from django.contrib.postgres.search import SearchVectorField
from django.contrib.postgres.indexes import GinIndex from django.contrib.postgres.indexes import GinIndex
from django.db import models from django.db import models, transaction
from django.db import transaction from django.db.models import Prefetch
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from model_utils import FieldTracker from model_utils import FieldTracker
@ -321,6 +321,27 @@ class Edition(Book):
return super().save(*args, **kwargs) return super().save(*args, **kwargs)
@classmethod
def viewer_aware_objects(cls, viewer):
"""annotate a book query with metadata related to the user"""
queryset = cls.objects
if not viewer or not viewer.is_authenticated:
return queryset
queryset = queryset.prefetch_related(
Prefetch(
"shelfbook_set",
queryset=viewer.shelfbook_set.all(),
to_attr="current_shelves",
),
Prefetch(
"readthrough_set",
queryset=viewer.readthrough_set.filter(is_active=True).all(),
to_attr="active_readthroughs",
),
)
return queryset
def isbn_10_to_13(isbn_10): def isbn_10_to_13(isbn_10):
"""convert an isbn 10 into an isbn 13""" """convert an isbn 10 into an isbn 13"""

View File

@ -92,6 +92,12 @@ class ListItem(CollectionItemMixin, BookWyrmModel):
notification_type="ADD", notification_type="ADD",
) )
def raise_not_deletable(self, viewer):
"""the associated user OR the list owner can delete"""
if self.book_list.user == viewer:
return
super().raise_not_deletable(viewer)
class Meta: class Meta:
"""A book may only be placed into a list once, """A book may only be placed into a list once,
and each order in the list may be used only once""" and each order in the list may be used only once"""

View File

@ -26,10 +26,14 @@ class ReadThrough(BookWyrmModel):
) )
start_date = models.DateTimeField(blank=True, null=True) start_date = models.DateTimeField(blank=True, null=True)
finish_date = models.DateTimeField(blank=True, null=True) finish_date = models.DateTimeField(blank=True, null=True)
is_active = models.BooleanField(default=True)
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
"""update user active time""" """update user active time"""
self.user.update_active_date() self.user.update_active_date()
# an active readthrough must have an unset finish date
if self.finish_date:
self.is_active = False
super().save(*args, **kwargs) super().save(*args, **kwargs)
def create_update(self): def create_update(self):

View File

@ -1,5 +1,6 @@
""" puttin' books on shelves """ """ puttin' books on shelves """
import re import re
from django.core.exceptions import PermissionDenied
from django.db import models from django.db import models
from django.utils import timezone from django.utils import timezone
@ -20,6 +21,7 @@ class Shelf(OrderedCollectionMixin, BookWyrmModel):
name = fields.CharField(max_length=100) name = fields.CharField(max_length=100)
identifier = models.CharField(max_length=100) identifier = models.CharField(max_length=100)
description = models.TextField(blank=True, null=True, max_length=500)
user = fields.ForeignKey( user = fields.ForeignKey(
"User", on_delete=models.PROTECT, activitypub_field="owner" "User", on_delete=models.PROTECT, activitypub_field="owner"
) )
@ -51,12 +53,23 @@ class Shelf(OrderedCollectionMixin, BookWyrmModel):
"""list of books for this shelf, overrides OrderedCollectionMixin""" """list of books for this shelf, overrides OrderedCollectionMixin"""
return self.books.order_by("shelfbook") return self.books.order_by("shelfbook")
@property
def deletable(self):
"""can the shelf be safely deleted?"""
return self.editable and not self.shelfbook_set.exists()
def get_remote_id(self): def get_remote_id(self):
"""shelf identifier instead of id""" """shelf identifier instead of id"""
base_path = self.user.remote_id base_path = self.user.remote_id
identifier = self.identifier or self.get_identifier() identifier = self.identifier or self.get_identifier()
return f"{base_path}/books/{identifier}" return f"{base_path}/books/{identifier}"
def raise_not_deletable(self, viewer):
"""don't let anyone delete a default shelf"""
super().raise_not_deletable(viewer)
if not self.deletable:
raise PermissionDenied()
class Meta: class Meta:
"""user/shelf unqiueness""" """user/shelf unqiueness"""

View File

@ -3,6 +3,7 @@ from dataclasses import MISSING
import re import re
from django.apps import apps from django.apps import apps
from django.core.exceptions import PermissionDenied
from django.core.validators import MaxValueValidator, MinValueValidator from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models from django.db import models
from django.dispatch import receiver from django.dispatch import receiver
@ -187,6 +188,13 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
"""json serialized activitypub class""" """json serialized activitypub class"""
return self.to_activity_dataclass(pure=pure).serialize() return self.to_activity_dataclass(pure=pure).serialize()
def raise_not_editable(self, viewer):
"""certain types of status aren't editable"""
# first, the standard raise
super().raise_not_editable(viewer)
if isinstance(self, (GeneratedNote, ReviewRating)):
raise PermissionDenied()
class GeneratedNote(Status): class GeneratedNote(Status):
"""these are app-generated messages about user activity""" """these are app-generated messages about user activity"""

View File

@ -13,7 +13,7 @@ VERSION = "0.0.1"
PAGE_LENGTH = env("PAGE_LENGTH", 15) PAGE_LENGTH = env("PAGE_LENGTH", 15)
DEFAULT_LANGUAGE = env("DEFAULT_LANGUAGE", "English") DEFAULT_LANGUAGE = env("DEFAULT_LANGUAGE", "English")
JS_CACHE = "7f2343cf" JS_CACHE = "e2bc0653"
# email # email
EMAIL_BACKEND = env("EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend") EMAIL_BACKEND = env("EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend")

View File

@ -141,8 +141,10 @@ let StatusCache = new class {
modal.getElementsByClassName("modal-close")[0].click(); modal.getElementsByClassName("modal-close")[0].click();
// Update shelve buttons // Update shelve buttons
document.querySelectorAll("[data-shelve-button-book='" + form.book.value +"']") if (form.reading_status) {
.forEach(button => this.cycleShelveButtons(button, form.reading_status.value)); document.querySelectorAll("[data-shelve-button-book='" + form.book.value +"']")
.forEach(button => this.cycleShelveButtons(button, form.reading_status.value));
}
return; return;
} }

View File

@ -236,14 +236,12 @@
<label class="label" for="id_cover">{% trans "Upload cover:" %}</label> <label class="label" for="id_cover">{% trans "Upload cover:" %}</label>
{{ form.cover }} {{ form.cover }}
</div> </div>
{% if book %}
<div class="field"> <div class="field">
<label class="label" for="id_cover_url"> <label class="label" for="id_cover_url">
{% trans "Load cover from url:" %} {% trans "Load cover from url:" %}
</label> </label>
<input class="input" name="cover-url" id="id_cover_url"> <input class="input" name="cover-url" id="id_cover_url" type="url" value="{{ cover_url|default:'' }}">
</div> </div>
{% endif %}
{% for error in form.cover.errors %} {% for error in form.cover.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
{% endfor %} {% endfor %}

View File

@ -1,5 +1,5 @@
{% load i18n %} {% load i18n %}
<section class="card is-hidden {{ class }}" id="{{ controls_text }}{% if controls_uid %}-{{ controls_uid }}{% endif %}"> <section class="card {% if not visible %}is-hidden {% endif %}{{ class }}" id="{{ controls_text }}{% if controls_uid %}-{{ controls_uid }}{% endif %}">
<header class="card-header has-background-white-ter"> <header class="card-header has-background-white-ter">
<h2 class="card-header-title" tabindex="0" id="{{ controls_text }}{% if controls_uid %}-{{ controls_uid }}{% endif %}_header"> <h2 class="card-header-title" tabindex="0" id="{{ controls_text }}{% if controls_uid %}-{{ controls_uid }}{% endif %}_header">
{% block header %}{% endblock %} {% block header %}{% endblock %}

View File

@ -8,7 +8,7 @@
{% trans "Local users" %} {% trans "Local users" %}
</label> </label>
<label class="is-block"> <label class="is-block">
<input type="radio" class="radio" name="scope" value="federated" {% if not request.GET.sort or request.GET.scope == "federated" %}checked{% endif %}> <input type="radio" class="radio" name="scope" value="federated" {% if request.GET.scope == "federated" %}checked{% endif %}>
{% trans "Federated community" %} {% trans "Federated community" %}
</label> </label>
{% endblock %} {% endblock %}

View File

@ -5,8 +5,8 @@
<label class="label" for="id_sort">{% trans "Order by" %}</label> <label class="label" for="id_sort">{% trans "Order by" %}</label>
<div class="select"> <div class="select">
<select name="sort" id="id_sort"> <select name="sort" id="id_sort">
<option value="suggested" {% if not request.GET.sort or request.GET.sort == "suggested" %}checked{% endif %}>{% trans "Suggested" %}</option> <option value="recent" {% if request.GET.sort == "recent" %}selected{% endif %}>{% trans "Recently active" %}</option>
<option value="recent" {% if request.GET.sort == "suggested" %}checked{% endif %}>{% trans "Recently active" %}</option> <option value="suggested" {% if request.GET.sort == "suggested" %}selected{% endif %}>{% trans "Suggested" %}</option>
</select> </select>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -25,7 +25,7 @@
{% if request.user.show_goal and not goal and tab.key == 'home' %} {% if request.user.show_goal and not goal and tab.key == 'home' %}
{% now 'Y' as year %} {% now 'Y' as year %}
<section class="block"> <section class="block">
{% include 'snippets/goal_card.html' with year=year %} {% include 'feed/goal_card.html' with year=year %}
<hr> <hr>
</section> </section>
{% endif %} {% endif %}

View File

@ -7,13 +7,8 @@
</h3> </h3>
{% endblock %} {% endblock %}
{% block card-content %} {% block card-content %}
<div class="content"> {% include 'snippets/goal_form.html' %}
<p>{% blocktrans %}Set a goal for how many books you'll finish reading in {{ year }}, and track your progress throughout the year.{% endblocktrans %}</p>
{% include 'snippets/goal_form.html' %}
</div>
{% endblock %} {% endblock %}
{% block card-footer %} {% block card-footer %}

View File

@ -10,6 +10,8 @@
<link rel="stylesheet" href="{% static "css/vendor/icons.css" %}"> <link rel="stylesheet" href="{% static "css/vendor/icons.css" %}">
<link rel="stylesheet" href="{% static "css/bookwyrm.css" %}"> <link rel="stylesheet" href="{% static "css/bookwyrm.css" %}">
<link rel="search" type="application/opensearchdescription+xml" href="{% url 'opensearch' %}" title="{% blocktrans with site_name=site.name %}{{ site_name }} search{% endblocktrans %}" />
<link rel="shortcut icon" type="image/x-icon" href="{% if site.favicon %}{% get_media_prefix %}{{ site.favicon }}{% else %}{% static "images/favicon.ico" %}{% endif %}"> <link rel="shortcut icon" type="image/x-icon" href="{% if site.favicon %}{% get_media_prefix %}{{ site.favicon }}{% else %}{% static "images/favicon.ico" %}{% endif %}">
{% if preview_images_enabled is True %} {% if preview_images_enabled is True %}
@ -34,7 +36,7 @@
<a class="navbar-item" href="/"> <a class="navbar-item" href="/">
<img class="image logo" src="{% if site.logo_small %}{% get_media_prefix %}{{ site.logo_small }}{% else %}{% static "images/logo-small.png" %}{% endif %}" alt="Home page"> <img class="image logo" src="{% if site.logo_small %}{% get_media_prefix %}{{ site.logo_small }}{% else %}{% static "images/logo-small.png" %}{% endif %}" alt="Home page">
</a> </a>
<form class="navbar-item column" action="/search/"> <form class="navbar-item column" action="{% url 'search' %}">
<div class="field has-addons"> <div class="field has-addons">
<div class="control"> <div class="control">
{% if user.is_authenticated %} {% if user.is_authenticated %}
@ -115,7 +117,7 @@
</a> </a>
</li> </li>
{% if perms.bookwyrm.create_invites or perms.moderate_user %} {% if perms.bookwyrm.create_invites or perms.moderate_user %}
<li class="navbar-divider" role="presentation"></li> <li class="navbar-divider" role="presentation">&nbsp;</li>
{% endif %} {% endif %}
{% if perms.bookwyrm.create_invites and not site.allow_registration %} {% if perms.bookwyrm.create_invites and not site.allow_registration %}
<li> <li>
@ -131,7 +133,7 @@
</a> </a>
</li> </li>
{% endif %} {% endif %}
<li class="navbar-divider" role="presentation"></li> <li class="navbar-divider" role="presentation">&nbsp;</li>
<li> <li>
<a href="{% url 'logout' %}" class="navbar-item"> <a href="{% url 'logout' %}" class="navbar-item">
{% trans 'Log out' %} {% trans 'Log out' %}

View File

@ -66,14 +66,14 @@
<p>{% blocktrans with username=item.user.display_name user_path=item.user.local_path %}Added by <a href="{{ user_path }}">{{ username }}</a>{% endblocktrans %}</p> <p>{% blocktrans with username=item.user.display_name user_path=item.user.local_path %}Added by <a href="{{ user_path }}">{{ username }}</a>{% endblocktrans %}</p>
</div> </div>
</div> </div>
{% if list.user == request.user or list.curation == 'open' and item.user == request.user %} {% if list.user == request.user %}
<div class="card-footer-item"> <div class="card-footer-item">
<form name="set-position" method="post" action="{% url 'list-set-book-position' item.id %}"> <form name="set-position" method="post" action="{% url 'list-set-book-position' item.id %}">
{% csrf_token %}
<div class="field has-addons mb-0"> <div class="field has-addons mb-0">
<div class="control"> <div class="control">
<label for="input-list-position" class="button is-transparent is-small">{% trans "List position" %}</label> <label for="input-list-position" class="button is-transparent is-small">{% trans "List position" %}</label>
</div> </div>
{% csrf_token %}
<div class="control"> <div class="control">
<input id="input_list_position" class="input is-small" type="number" min="1" name="position" value="{{ item.order }}"> <input id="input_list_position" class="input is-small" type="number" min="1" name="position" value="{{ item.order }}">
</div> </div>
@ -83,7 +83,9 @@
</div> </div>
</form> </form>
</div> </div>
<form name="add-book" method="post" action="{% url 'list-remove-book' list.id %}" class="card-footer-item"> {% endif %}
{% if list.user == request.user or list.curation == 'open' and item.user == request.user %}
<form name="remove-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>

View File

@ -0,0 +1,16 @@
{% load i18n %}{% load static %}<?xml version="1.0" encoding="UTF-8"?>
<OpenSearchDescription
xmlns="http://a9.com/-/spec/opensearch/1.1/"
xmlns:moz="http://www.mozilla.org/2006/browser/search/"
>
<ShortName>BW</ShortName>
<Description>{% blocktrans trimmed with site_name=site.name %}
{{ site_name }} search
{% endblocktrans %}</Description>
<Image width="16" height="16" type="image/x-icon">{{ image }}</Image>
<Url
type="text/html"
method="get"
template="https://{{ DOMAIN }}{% url 'search' %}?q={searchTerms}"
/>
</OpenSearchDescription>

View File

@ -9,7 +9,7 @@
{% block panel %} {% block panel %}
{% if not request.user.blocks.exists %} {% if not request.user.blocks.exists %}
<p>{% trans "No users currently blocked." %}</p> <p><em>{% trans "No users currently blocked." %}</em></p>
{% else %} {% else %}
<ul> <ul>
{% for user in request.user.blocks.all %} {% for user in request.user.blocks.all %}

View File

@ -10,11 +10,11 @@
{% block panel %} {% block panel %}
<form name="edit-profile" action="{% url 'prefs-password' %}" method="post" enctype="multipart/form-data"> <form name="edit-profile" action="{% url 'prefs-password' %}" method="post" enctype="multipart/form-data">
{% csrf_token %} {% csrf_token %}
<div class="block"> <div class="field">
<label class="label" for="id_password">{% trans "New password:" %}</label> <label class="label" for="id_password">{% trans "New password:" %}</label>
<input type="password" name="password" maxlength="128" class="input" required="" id="id_password"> <input type="password" name="password" maxlength="128" class="input" required="" id="id_password">
</div> </div>
<div class="block"> <div class="field">
<label class="label" for="id_confirm_password">{% trans "Confirm password:" %}</label> <label class="label" for="id_confirm_password">{% trans "Confirm password:" %}</label>
<input type="password" name="confirm-password" maxlength="128" class="input" required="" id="id_confirm_password"> <input type="password" name="confirm-password" maxlength="128" class="input" required="" id="id_confirm_password">
</div> </div>

View File

@ -7,76 +7,114 @@
{% trans "Edit Profile" %} {% trans "Edit Profile" %}
{% endblock %} {% endblock %}
{% block profile-tabs %}
<ul class="menu-list">
<li><a href="#profile">{% trans "Profile" %}</a></li>
<li><a href="#display-preferences">{% trans "Display preferences" %}</a></li>
<li><a href="#privacy">{% trans "Privacy" %}</a></li>
</ul>
{% endblock %}
{% block panel %} {% block panel %}
{% if form.non_field_errors %} {% if form.non_field_errors %}
<p class="notification is-danger">{{ form.non_field_errors }}</p> <p class="notification is-danger">{{ form.non_field_errors }}</p>
{% endif %} {% endif %}
<form name="edit-profile" action="{% url 'prefs-profile' %}" method="post" enctype="multipart/form-data"> <form name="edit-profile" action="{% url 'prefs-profile' %}" method="post" enctype="multipart/form-data">
{% csrf_token %} {% csrf_token %}
<div class="block"> <section class="block" id="profile">
<label class="label" for="id_avatar">{% trans "Avatar:" %}</label> <h2 class="title is-4">{% trans "Profile" %}</h2>
{{ form.avatar }} <div class="box">
{% for error in form.avatar.errors %} <label class="label" for="id_avatar">{% trans "Avatar:" %}</label>
<p class="help is-danger">{{ error | escape }}</p> <div class="field columns is-mobile">
{% endfor %} {% if request.user.avatar %}
</div> <div class="column is-narrow">
<div class="block"> {% include 'snippets/avatar.html' with user=request.user large=True %}
<label class="label" for="id_name">{% trans "Display name:" %}</label> </div>
{{ form.name }} {% endif %}
{% for error in form.name.errors %} <div class="column">
<p class="help is-danger">{{ error | escape }}</p> {{ form.avatar }}
{% endfor %} {% for error in form.avatar.errors %}
</div> <p class="help is-danger">{{ error | escape }}</p>
<div class="block"> {% endfor %}
<label class="label" for="id_summary">{% trans "Summary:" %}</label> </div>
{{ form.summary }} </div>
{% for error in form.summary.errors %} <div class="field">
<p class="help is-danger">{{ error | escape }}</p> <label class="label" for="id_name">{% trans "Display name:" %}</label>
{% endfor %} {{ form.name }}
</div> {% for error in form.name.errors %}
<div class="block"> <p class="help is-danger">{{ error | escape }}</p>
<label class="label" for="id_email">{% trans "Email address:" %}</label> {% endfor %}
{{ form.email }} </div>
{% for error in form.email.errors %} <div class="field">
<p class="help is-danger">{{ error | escape }}</p> <label class="label" for="id_summary">{% trans "Summary:" %}</label>
{% endfor %} {{ form.summary }}
</div> {% for error in form.summary.errors %}
<div class="block"> <p class="help is-danger">{{ error | escape }}</p>
<label class="checkbox label" for="id_show_goal"> {% endfor %}
{% trans "Show reading goal prompt in feed:" %} </div>
{{ form.show_goal }} <div class="field">
</label> <label class="label" for="id_email">{% trans "Email address:" %}</label>
<label class="checkbox label" for="id_show_goal"> {{ form.email }}
{% trans "Show suggested users:" %} {% for error in form.email.errors %}
{{ form.show_suggested_users }} <p class="help is-danger">{{ error | escape }}</p>
</label> {% endfor %}
<label class="checkbox label" for="id_discoverable"> </div>
{% trans "Show this account in suggested users:" %}
{{ form.discoverable }}
</label>
{% url 'directory' as path %}
<p class="help">{% blocktrans %}Your account will show up in the <a href="{{ path }}">directory</a>, and may be recommended to other BookWyrm users.{% endblocktrans %}</p>
</div>
<div class="block">
<label class="checkbox label" for="id_manually_approves_followers">
{% trans "Manually approve followers:" %}
{{ form.manually_approves_followers }}
</label>
</div>
<div class="block">
<label class="label" for="id_default_post_privacy">
{% trans "Default post privacy:" %}
</label>
<div class="select">
{{ form.default_post_privacy }}
</div> </div>
</div> </section>
<div class="block">
<label class="label" for="id_preferred_timezone">{% trans "Preferred Timezone: " %}</label> <hr aria-hidden="true">
<div class="select">
{{ form.preferred_timezone }} <section class="block" id="display-preferences">
<h2 class="title is-4">{% trans "Display preferences" %}</h2>
<div class="box">
<div class="field">
<label class="checkbox label" for="id_show_goal">
{% trans "Show reading goal prompt in feed:" %}
{{ form.show_goal }}
</label>
<label class="checkbox label" for="id_show_suggested_users">
{% trans "Show suggested users:" %}
{{ form.show_suggested_users }}
</label>
<label class="checkbox label" for="id_discoverable">
{% trans "Show this account in suggested users:" %}
{{ form.discoverable }}
</label>
{% url 'directory' as path %}
<p class="help">
{% blocktrans %}Your account will show up in the <a href="{{ path }}">directory</a>, and may be recommended to other BookWyrm users.{% endblocktrans %}
</p>
</div>
<div class="field">
<label class="label" for="id_preferred_timezone">{% trans "Preferred Timezone: " %}</label>
<div class="select">
{{ form.preferred_timezone }}
</div>
</div>
</div> </div>
</div> </section>
<div class="block"><button class="button is-primary" type="submit">{% trans "Save" %}</button></div>
<hr aria-hidden="true">
<section class="block" id="privacy">
<h2 class="title is-4">{% trans "Privacy" %}</h2>
<div class="box">
<div class="field">
<label class="checkbox label" for="id_manually_approves_followers">
{% trans "Manually approve followers:" %}
{{ form.manually_approves_followers }}
</label>
</div>
<div class="field">
<label class="label" for="id_default_post_privacy">
{% trans "Default post privacy:" %}
</label>
<div class="select">
{{ form.default_post_privacy }}
</div>
</div>
</div>
</section>
<div class="field"><button class="button is-primary" type="submit">{% trans "Save" %}</button></div>
</form> </form>
{% endblock %} {% endblock %}

View File

@ -12,7 +12,8 @@
<ul class="menu-list"> <ul class="menu-list">
<li> <li>
{% url 'prefs-profile' as url %} {% url 'prefs-profile' as url %}
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Profile" %}</a> <a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Edit Profile" %}</a>
{% block profile-tabs %}{% endblock %}
</li> </li>
<li> <li>
{% url 'prefs-password' as url %} {% url 'prefs-password' as url %}

View File

@ -26,7 +26,7 @@
{% block panel %} {% block panel %}
<form name="edit-announcement" method="post" action="{% url 'settings-announcements' announcement.id %}" class="block"> <form name="edit-announcement" method="post" action="{% url 'settings-announcements' announcement.id %}" class="block">
{% include 'settings/announcement_form.html' with controls_text="edit_announcement" %} {% include 'settings/announcements/announcement_form.html' with controls_text="edit_announcement" %}
</form> </form>
<div class="block content"> <div class="block content">

View File

@ -11,7 +11,7 @@
{% block panel %} {% block panel %}
<form name="create-announcement" method="post" action="{% url 'settings-announcements' %}" class="block"> <form name="create-announcement" method="post" action="{% url 'settings-announcements' %}" class="block">
{% include 'settings/announcement_form.html' with controls_text="create_announcement" %} {% include 'settings/announcements/announcement_form.html' with controls_text="create_announcement" %}
</form> </form>
<div class="block"> <div class="block">
@ -48,11 +48,10 @@
<td>{% if announcement.active %}{% trans "active" %}{% else %}{% trans "inactive" %}{% endif %}</td> <td>{% if announcement.active %}{% trans "active" %}{% else %}{% trans "inactive" %}{% endif %}</td>
</tr> </tr>
{% endfor %} {% endfor %}
{% if not announcements %}
<tr><td colspan="5"><em>{% trans "No announcements found" %}</em></td></tr>
{% endif %}
</table> </table>
{% if not announcements %}
<p><em>{% trans "No announcements found." %}</em></p>
{% endif %}
</div> </div>
{% include 'snippets/pagination.html' with page=announcements path=request.path %} {% include 'snippets/pagination.html' with page=announcements path=request.path %}

View File

@ -67,27 +67,27 @@
<form method="get" action="{% url 'settings-dashboard' %}" class="notification has-background-white-bis"> <form method="get" action="{% url 'settings-dashboard' %}" class="notification has-background-white-bis">
<div class="is-flex is-align-items-flex-end"> <div class="is-flex is-align-items-flex-end">
<div class="ml-1 mr-1"> <div class="ml-1 mr-1">
<label class="label"> <label class="label" for="id_start">
{% trans "Start date:" %} {% trans "Start date:" %}
<input class="input" type="date" name="start" value="{{ start }}">
</label> </label>
<input class="input" type="date" name="start" value="{{ start }}" id="id_start">
</div> </div>
<div class="ml-1 mr-1"> <div class="ml-1 mr-1">
<label class="label"> <label class="label" for="id_end">
{% trans "End date:" %} {% trans "End date:" %}
<input class="input" type="date" name="end" value="{{ end }}">
</label> </label>
<input class="input" type="date" name="end" value="{{ end }}" id="id_end">
</div> </div>
<div class="ml-1 mr-1"> <div class="ml-1 mr-1">
<label class="label"> <label class="label" for="id_interval">
{% trans "Interval:" %} {% trans "Interval:" %}
<div class="select">
<select name="days">
<option value="1" {% if interval == 1 %}selected{% endif %}>{% trans "Days" %}</option>
<option value="7" {% if interval == 7 %}selected{% endif %}>{% trans "Weeks" %}</option>
</select>
</div>
</label> </label>
<div class="select">
<select name="days" id="id_interval">
<option value="1" {% if interval == 1 %}selected{% endif %}>{% trans "Days" %}</option>
<option value="7" {% if interval == 7 %}selected{% endif %}>{% trans "Weeks" %}</option>
</select>
</div>
</div> </div>
<div class="ml-1 mr-1"> <div class="ml-1 mr-1">
<button class="button is-link" type="submit">{% trans "Submit" %}</button> <button class="button is-link" type="submit">{% trans "Submit" %}</button>
@ -115,6 +115,6 @@
{% block scripts %} {% block scripts %}
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.5.1/dist/chart.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/chart.js@3.5.1/dist/chart.min.js"></script>
{% include 'settings/dashboard_user_chart.html' %} {% include 'settings/dashboard/dashboard_user_chart.html' %}
{% include 'settings/dashboard_status_chart.html' %} {% include 'settings/dashboard/dashboard_status_chart.html' %}
{% endblock %} {% endblock %}

View File

@ -12,7 +12,7 @@
{% endblock %} {% endblock %}
{% block panel %} {% block panel %}
{% include 'settings/domain_form.html' with controls_text="add_domain" class="block" %} {% include 'settings/email_blocklist/domain_form.html' with controls_text="add_domain" class="block" %}
<p class="notification block"> <p class="notification block">
{% trans "When someone tries to register with an email from this domain, no account will be created. The registration process will appear to have worked." %} {% trans "When someone tries to register with an email from this domain, no account will be created. The registration process will appear to have worked." %}
@ -55,7 +55,11 @@
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
{% if not domains.exists %}
<tr><td colspan="5"><em>{% trans "No email domains currently blocked" %}</em></td></tr>
{% endif %}
</table> </table>
{% endblock %} {% endblock %}

View File

@ -33,6 +33,8 @@
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
{% endfor %} {% endfor %}
</div> </div>
</div>
<div class="column is-half">
<div class="field"> <div class="field">
<label class="label" for="id_status">{% trans "Status:" %}</label> <label class="label" for="id_status">{% trans "Status:" %}</label>
<div class="select"> <div class="select">
@ -43,6 +45,8 @@
</div> </div>
</div> </div>
</div> </div>
</div>
<div class="columns">
<div class="column is-half"> <div class="column is-half">
<div class="field"> <div class="field">
<label class="label" for="id_application_type">{% trans "Software:" %}</label> <label class="label" for="id_application_type">{% trans "Software:" %}</label>
@ -51,6 +55,8 @@
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
{% endfor %} {% endfor %}
</div> </div>
</div>
<div class="column is-half">
<div class="field"> <div class="field">
<label class="label" for="id_application_version">{% trans "Version:" %}</label> <label class="label" for="id_application_version">{% trans "Version:" %}</label>
<input type="text" name="application_version" maxlength="255" class="input" id="id_application_version" value="{{ form.application_version.value|default:'' }}"> <input type="text" name="application_version" maxlength="255" class="input" id="id_application_version" value="{{ form.application_version.value|default:'' }}">
@ -62,7 +68,7 @@
</div> </div>
<div class="field"> <div class="field">
<label class="label" for="id_notes">{% trans "Notes:" %}</label> <label class="label" for="id_notes">{% trans "Notes:" %}</label>
<textarea name="notes" cols="None" rows="None" class="textarea" id="id_notes">{{ form.notes.value|default:'' }}</textarea> <textarea name="notes" cols="40" rows="5" class="textarea" id="id_notes">{{ form.notes.value|default:'' }}</textarea>
</div> </div>
<button type="submit" class="button is-primary">{% trans "Save" %}</button> <button type="submit" class="button is-primary">{% trans "Save" %}</button>

View File

@ -19,18 +19,14 @@
<h2 class="title is-4">{% trans "Details" %}</h2> <h2 class="title is-4">{% trans "Details" %}</h2>
<div class="box is-flex-grow-1 content"> <div class="box is-flex-grow-1 content">
<dl> <dl>
<div class="is-flex"> <dt class="is-pulled-left mr-5">{% trans "Software:" %}</dt>
<dt>{% trans "Software:" %}</dt> <dd>{{ server.application_type }}</dd>
<dd>{{ server.application_type }}</dd>
</div> <dt class="is-pulled-left mr-5">{% trans "Version:" %}</dt>
<div class="is-flex"> <dd>{{ server.application_version }}</dd>
<dt>{% trans "Version:" %}</dt>
<dd>{{ server.application_version }}</dd> <dt class="is-pulled-left mr-5">{% trans "Status:" %}</dt>
</div> <dd>{{ server.get_status_display }}</dd>
<div class="is-flex">
<dt>{% trans "Status:" %}</dt>
<dd>{{ server.get_status_display }}</dd>
</div>
</dl> </dl>
</div> </div>
</section> </section>
@ -39,38 +35,32 @@
<h2 class="title is-4">{% trans "Activity" %}</h2> <h2 class="title is-4">{% trans "Activity" %}</h2>
<div class="box is-flex-grow-1 content"> <div class="box is-flex-grow-1 content">
<dl> <dl>
<div class="is-flex"> <dt class="is-pulled-left mr-5">{% trans "Users:" %}</dt>
<dt>{% trans "Users:" %}</dt> <dd>
<dd> {{ users.count }}
{{ users.count }} {% if server.user_set.count %}(<a href="{% url 'settings-users' %}?server={{ server.server_name }}">{% trans "View all" %}</a>){% endif %}
{% if server.user_set.count %}(<a href="{% url 'settings-users' %}?server={{ server.server_name }}">{% trans "View all" %}</a>){% endif %} </dd>
</dd>
</div> <dt class="is-pulled-left mr-5">{% trans "Reports:" %}</dt>
<div class="is-flex"> <dd>
<dt>{% trans "Reports:" %}</dt> {{ reports.count }}
<dd> {% if reports.count %}(<a href="{% url 'settings-reports' %}?server={{ server.server_name }}">{% trans "View all" %}</a>){% endif %}
{{ reports.count }} </dd>
{% if reports.count %}(<a href="{% url 'settings-reports' %}?server={{ server.server_name }}">{% trans "View all" %}</a>){% endif %}
</dd> <dt class="is-pulled-left mr-5">{% trans "Followed by us:" %}</dt>
</div> <dd>
<div class="is-flex"> {{ followed_by_us.count }}
<dt>{% trans "Followed by us:" %}</dt> </dd>
<dd>
{{ followed_by_us.count }} <dt class="is-pulled-left mr-5">{% trans "Followed by them:" %}</dt>
</dd> <dd>
</div> {{ followed_by_them.count }}
<div class="is-flex"> </dd>
<dt>{% trans "Followed by them:" %}</dt>
<dd> <dt class="is-pulled-left mr-5">{% trans "Blocked by us:" %}</dt>
{{ followed_by_them.count }} <dd>
</dd> {{ blocked_by_us.count }}
</div> </dd>
<div class="is-flex">
<dt>{% trans "Blocked by us:" %}</dt>
<dd>
{{ blocked_by_us.count }}
</dd>
</div>
</dl> </dl>
</div> </div>
</section> </section>
@ -86,14 +76,13 @@
{% include 'snippets/toggle/open_button.html' with text=button_text icon_with_text="pencil" controls_text="edit_notes" %} {% include 'snippets/toggle/open_button.html' with text=button_text icon_with_text="pencil" controls_text="edit_notes" %}
</div> </div>
</header> </header>
{% if server.notes %} {% trans "<em>No notes</em>" as null_text %}
<div class="box" id="hide_edit_notes">{{ server.notes|to_markdown|safe }}</div> <div class="box" id="hide_edit_notes">{{ server.notes|to_markdown|default:null_text|safe }}</div>
{% endif %}
<form class="box is-hidden" method="POST" action="{% url 'settings-federated-server' server.id %}" id="edit_notes"> <form class="box is-hidden" method="POST" action="{% url 'settings-federated-server' server.id %}" id="edit_notes">
{% csrf_token %} {% csrf_token %}
<p> <p>
<label class="is-sr-only" for="id_notes">Notes:</label> <label class="is-sr-only" for="id_notes">Notes:</label>
<textarea name="notes" cols="None" rows="None" class="textarea" id="id_notes">{{ server.notes|default:"" }}</textarea> <textarea name="notes" cols="40" rows="5" class="textarea" id="id_notes">{{ server.notes|default:"" }}</textarea>
</p> </p>
<button type="submit" class="button is-primary">{% trans "Save" %}</button> <button type="submit" class="button is-primary">{% trans "Save" %}</button>
{% trans "Cancel" as button_text %} {% trans "Cancel" as button_text %}

View File

@ -59,7 +59,11 @@
<td>{{ server.get_status_display }}</td> <td>{{ server.get_status_display }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
{% if not servers %}
<tr><td colspan="5"><em>{% trans "No instances found" %}</em></td></tr>
{% endif %}
</table> </table>
{% include 'snippets/pagination.html' with page=servers path=request.path %} {% include 'snippets/pagination.html' with page=servers path=request.path %}
{% endblock %} {% endblock %}

View File

@ -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/status_filter.html' %} {% include 'settings/invites/status_filter.html' %}
{% endblock %} {% endblock %}

View File

@ -26,7 +26,7 @@
{% endif %} ({{ count }}) {% endif %} ({{ count }})
</h2> </h2>
{% include 'settings/invite_request_filters.html' %} {% include 'settings/invites/invite_request_filters.html' %}
<table class="table is-striped is-fullwidth"> <table class="table is-striped is-fullwidth">
{% url 'settings-invite-requests' as url %} {% url 'settings-invite-requests' as url %}
@ -47,7 +47,7 @@
<th>{% trans "Action" %}</th> <th>{% trans "Action" %}</th>
</tr> </tr>
{% if not requests %} {% if not requests %}
<tr><td colspan="4">{% trans "No requests" %}</td></tr> <tr><td colspan="5"><em>{% trans "No requests" %}</em></td></tr>
{% endif %} {% endif %}
{% for req in requests %} {% for req in requests %}
<tr> <tr>

View File

@ -12,7 +12,7 @@
{% endblock %} {% endblock %}
{% block panel %} {% block panel %}
{% include 'settings/ip_address_form.html' with controls_text="add_address" class="block" %} {% include 'settings/ip_blocklist/ip_address_form.html' with controls_text="add_address" class="block" %}
<p class="notification block"> <p class="notification block">
{% trans "Any traffic from this IP address will get a 404 response when trying to access any part of the application." %} {% trans "Any traffic from this IP address will get a 404 response when trying to access any part of the application." %}
@ -42,6 +42,9 @@
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
{% if not addresses.exists %}
<tr><td colspan="2"><em>{% trans "No IP addresses currently blocked" %}</em></td></tr>
{% endif %}
</table> </table>
{% endblock %} {% endblock %}

View File

@ -74,14 +74,7 @@
<li> <li>
{% url 'settings-site' as url %} {% url 'settings-site' as url %}
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Site Settings" %}</a> <a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Site Settings" %}</a>
{% if url in request.path %} {% block site-subtabs %}{% endblock %}
<ul class="emnu-list">
<li><a href="{{ url }}#instance-info">{% trans "Instance Info" %}</a></li>
<li><a href="{{ url }}#images">{% trans "Images" %}</a></li>
<li><a href="{{ url }}#footer">{% trans "Footer Content" %}</a></li>
<li><a href="{{ url }}#registration">{% trans "Registration" %}</a></li>
</ul>
{% endif %}
</li> </li>
</ul> </ul>
{% endif %} {% endif %}

View File

@ -3,20 +3,21 @@
{% load humanize %} {% load humanize %}
{% block title %}{% blocktrans with report_id=report.id username=report.user.username %}Report #{{ report_id }}: {{ username }}{% endblocktrans %}{% endblock %} {% block title %}{% blocktrans with report_id=report.id username=report.user.username %}Report #{{ report_id }}: {{ username }}{% endblocktrans %}{% endblock %}
{% block header %}{% blocktrans with report_id=report.id username=report.user.username %}Report #{{ report_id }}: {{ username }}{% endblocktrans %}{% endblock %}
{% block header %}
{% blocktrans with report_id=report.id username=report.user.username %}Report #{{ report_id }}: {{ username }}{% endblocktrans %}
<a href="{% url 'settings-reports' %}" class="has-text-weight-normal help">{% trans "Back to reports" %}</a>
{% endblock %}
{% block panel %} {% block panel %}
<div class="block">
<a href="{% url 'settings-reports' %}">{% trans "Back to reports" %}</a>
</div>
<div class="block"> <div class="block">
{% include 'moderation/report_preview.html' with report=report %} {% include 'settings/reports/report_preview.html' with report=report %}
</div> </div>
{% include 'user_admin/user_info.html' with user=report.user %} {% include 'settings/users/user_info.html' with user=report.user %}
{% include 'user_admin/user_moderation_actions.html' with user=report.user %} {% include 'settings/users/user_moderation_actions.html' with user=report.user %}
<div class="block"> <div class="block">
<h3 class="title is-4">{% trans "Moderator Comments" %}</h3> <h3 class="title is-4">{% trans "Moderator Comments" %}</h3>

View File

@ -30,7 +30,7 @@
</ul> </ul>
</div> </div>
{% include 'user_admin/user_admin_filters.html' %} {% include 'settings/users/user_admin_filters.html' %}
<div class="block"> <div class="block">
{% if not reports %} {% if not reports %}
@ -39,7 +39,7 @@
{% for report in reports %} {% for report in reports %}
<div class="block"> <div class="block">
{% include 'moderation/report_preview.html' with report=report %} {% include 'settings/reports/report_preview.html' with report=report %}
</div> </div>
{% endfor %} {% endfor %}
</div> </div>

View File

@ -5,36 +5,46 @@
{% block header %}{% trans "Site Settings" %}{% endblock %} {% block header %}{% trans "Site Settings" %}{% endblock %}
{% block panel %} {% block site-subtabs %}
<ul class="menu-list">
<li><a href="#instance-info">{% trans "Instance Info" %}</a></li>
<li><a href="#images">{% trans "Images" %}</a></li>
<li><a href="#footer">{% trans "Footer Content" %}</a></li>
<li><a href="#registration">{% trans "Registration" %}</a></li>
</ul>
{% endblock %}
{% block panel %}
<form action="{% url 'settings-site' %}" method="POST" class="content" enctype="multipart/form-data"> <form action="{% url 'settings-site' %}" method="POST" class="content" enctype="multipart/form-data">
{% csrf_token %} {% csrf_token %}
<section class="block" id="instance_info"> <section class="block" id="instance_info">
<h2 class="title is-4">{% trans "Instance Info" %}</h2> <h2 class="title is-4">{% trans "Instance Info" %}</h2>
<div class="field"> <div class="box">
<label class="label" for="id_name">{% trans "Instance Name:" %}</label> <div class="field">
{{ site_form.name }} <label class="label" for="id_name">{% trans "Instance Name:" %}</label>
</div> {{ site_form.name }}
<div class="field"> </div>
<label class="label" for="id_instance_tagline">{% trans "Tagline:" %}</label> <div class="field">
{{ site_form.instance_tagline }} <label class="label" for="id_instance_tagline">{% trans "Tagline:" %}</label>
</div> {{ site_form.instance_tagline }}
<div class="field"> </div>
<label class="label" for="id_instance_description">{% trans "Instance description:" %}</label> <div class="field">
{{ site_form.instance_description }} <label class="label" for="id_instance_description">{% trans "Instance description:" %}</label>
</div> {{ site_form.instance_description }}
<div class="field"> </div>
<label class="label mb-0" for="id_short_description">{% trans "Short description:" %}</label> <div class="field">
<p class="help">{% trans "Used when the instance is previewed on joinbookwyrm.com. Does not support html or markdown." %}</p> <label class="label mb-0" for="id_short_description">{% trans "Short description:" %}</label>
{{ site_form.instance_short_description }} <p class="help">{% trans "Used when the instance is previewed on joinbookwyrm.com. Does not support html or markdown." %}</p>
</div> {{ site_form.instance_short_description }}
<div class="field"> </div>
<label class="label" for="id_code_of_conduct">{% trans "Code of conduct:" %}</label> <div class="field">
{{ site_form.code_of_conduct }} <label class="label" for="id_code_of_conduct">{% trans "Code of conduct:" %}</label>
</div> {{ site_form.code_of_conduct }}
<div class="field"> </div>
<label class="label" for="id_privacy_policy">{% trans "Privacy Policy:" %}</label> <div class="field">
{{ site_form.privacy_policy }} <label class="label" for="id_privacy_policy">{% trans "Privacy Policy:" %}</label>
{{ site_form.privacy_policy }}
</div>
</div> </div>
</section> </section>
@ -42,16 +52,16 @@
<section class="block" id="images"> <section class="block" id="images">
<h2 class="title is-4">{% trans "Images" %}</h2> <h2 class="title is-4">{% trans "Images" %}</h2>
<div class="columns"> <div class="box is-flex">
<div class="column"> <div>
<label class="label" for="id_logo">{% trans "Logo:" %}</label> <label class="label" for="id_logo">{% trans "Logo:" %}</label>
{{ site_form.logo }} {{ site_form.logo }}
</div> </div>
<div class="column"> <div>
<label class="label" for="id_logo_small">{% trans "Logo small:" %}</label> <label class="label" for="id_logo_small">{% trans "Logo small:" %}</label>
{{ site_form.logo_small }} {{ site_form.logo_small }}
</div> </div>
<div class="column"> <div>
<label class="label" for="id_favicon">{% trans "Favicon:" %}</label> <label class="label" for="id_favicon">{% trans "Favicon:" %}</label>
{{ site_form.favicon }} {{ site_form.favicon }}
</div> </div>
@ -62,21 +72,23 @@
<section class="block" id="footer"> <section class="block" id="footer">
<h2 class="title is-4">{% trans "Footer Content" %}</h2> <h2 class="title is-4">{% trans "Footer Content" %}</h2>
<div class="field"> <div class="box">
<label class="label" for="id_support_link">{% trans "Support link:" %}</label> <div class="field">
<input type="text" name="support_link" maxlength="255" class="input" id="id_support_link" placeholder="https://www.patreon.com/bookwyrm"{% if site.support_link %} value="{{ site.support_link }}"{% endif %}> <label class="label" for="id_support_link">{% trans "Support link:" %}</label>
</div> <input type="text" name="support_link" maxlength="255" class="input" id="id_support_link" placeholder="https://www.patreon.com/bookwyrm"{% if site.support_link %} value="{{ site.support_link }}"{% endif %}>
<div class="field"> </div>
<label class="label" for="id_support_title">{% trans "Support title:" %}</label> <div class="field">
<input type="text" name="support_title" maxlength="100" class="input" id="id_support_title" placeholder="Patreon"{% if site.support_title %} value="{{ site.support_title }}"{% endif %}> <label class="label" for="id_support_title">{% trans "Support title:" %}</label>
</div> <input type="text" name="support_title" maxlength="100" class="input" id="id_support_title" placeholder="Patreon"{% if site.support_title %} value="{{ site.support_title }}"{% endif %}>
<div class="field"> </div>
<label class="label" for="id_admin_email">{% trans "Admin email:" %}</label> <div class="field">
{{ site_form.admin_email }} <label class="label" for="id_admin_email">{% trans "Admin email:" %}</label>
</div> {{ site_form.admin_email }}
<div class="field"> </div>
<label class="label" for="id_footer_item">{% trans "Additional info:" %}</label> <div class="field">
{{ site_form.footer_item }} <label class="label" for="id_footer_item">{% trans "Additional info:" %}</label>
{{ site_form.footer_item }}
</div>
</div> </div>
</section> </section>
@ -84,35 +96,37 @@
<section class="block" id="registration"> <section class="block" id="registration">
<h2 class="title is-4">{% trans "Registration" %}</h2> <h2 class="title is-4">{% trans "Registration" %}</h2>
<div class="field"> <div class="box">
<label class="label" for="id_allow_registration"> <div class="field">
{{ site_form.allow_registration }} <label class="label" for="id_allow_registration">
{% trans "Allow registration" %} {{ site_form.allow_registration }}
</label> {% trans "Allow registration" %}
</div> </label>
<div class="field"> </div>
<label class="label" for="id_allow_invite_requests"> <div class="field">
{{ site_form.allow_invite_requests }} <label class="label" for="id_allow_invite_requests">
{% trans "Allow invite requests" %} {{ site_form.allow_invite_requests }}
</label> {% trans "Allow invite requests" %}
</div> </label>
<div class="field"> </div>
<label class="label mb-0" for="id_allow_invite_requests"> <div class="field">
{{ site_form.require_confirm_email }} <label class="label mb-0" for="id_require_confirm_email">
{% trans "Require users to confirm email address" %} {{ site_form.require_confirm_email }}
</label> {% trans "Require users to confirm email address" %}
<p class="help">{% trans "(Recommended if registration is open)" %}</p> </label>
</div> <p class="help">{% trans "(Recommended if registration is open)" %}</p>
<div class="field"> </div>
<label class="label" for="id_registration_closed_text">{% trans "Registration closed text:" %}</label> <div class="field">
{{ site_form.registration_closed_text }} <label class="label" for="id_registration_closed_text">{% trans "Registration closed text:" %}</label>
</div> {{ site_form.registration_closed_text }}
<div class="field"> </div>
<label class="label" for="id_invite_request_text">{% trans "Invite request text:" %}</label> <div class="field">
{{ site_form.invite_request_text }} <label class="label" for="id_invite_request_text">{% trans "Invite request text:" %}</label>
{% for error in site_form.invite_request_text.errors %} {{ site_form.invite_request_text }}
<p class="help is-danger">{{ error|escape }}</p> {% for error in site_form.invite_request_text.errors %}
{% endfor %} <p class="help is-danger">{{ error|escape }}</p>
{% endfor %}
</div>
</div> </div>
</section> </section>

View File

@ -0,0 +1,16 @@
{% extends 'settings/layout.html' %}
{% load i18n %}
{% block title %}{{ user.username }}{% endblock %}
{% block header %}
{{ user.username }}
<a class="help has-text-weight-normal" href="{% url 'settings-users' %}">{% trans "Back to users" %}</a>
{% endblock %}
{% block panel %}
{% include 'settings/users/user_info.html' with user=user %}
{% include 'settings/users/user_moderation_actions.html' with user=user %}
{% endblock %}

View File

@ -13,7 +13,7 @@
{% block panel %} {% block panel %}
{% include 'user_admin/user_admin_filters.html' %} {% include 'settings/users/user_admin_filters.html' %}
<table class="table is-striped"> <table class="table is-striped">
<tr> <tr>

View File

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

View File

@ -48,58 +48,42 @@
<div class="box content is-flex-grow-1"> <div class="box content is-flex-grow-1">
<dl> <dl>
{% if user.local %} {% if user.local %}
<div class="is-flex"> <dt class="is-pulled-left mr-5">{% trans "Email:" %}</dt>
<dt>{% trans "Email:" %}</dt> <dd>{{ user.email }}</dd>
<dd>{{ user.email }}</dd>
</div>
{% endif %} {% endif %}
{% with report_count=user.report_set.count %} {% with report_count=user.report_set.count %}
<div class="is-flex"> <dt class="is-pulled-left mr-5">{% trans "Reports:" %}</dt>
<dt>{% trans "Reports:" %}</dt> <dd>
<dd> {{ report_count|intcomma }}
{{ report_count|intcomma }} {% if report_count > 0 %}
{% if report_count > 0 %} <a href="{% url 'settings-reports' %}?username={{ user.username }}">
<a href="{% url 'settings-reports' %}?username={{ user.username }}"> {% trans "(View reports)" %}
{% trans "(View reports)" %} </a>
</a> {% endif %}
{% endif %} </dd>
</dd>
</div>
{% endwith %} {% endwith %}
<div class="is-flex"> <dt class="is-pulled-left mr-5">{% trans "Blocked by count:" %}</dt>
<dt>{% trans "Blocked by count:" %}</dt> <dd>{{ user.blocked_by.count }}</dd>
<dd>{{ user.blocked_by.count }}</dd>
</div>
<div class="is-flex"> <dt class="is-pulled-left mr-5">{% trans "Last active date:" %}</dt>
<dt>{% trans "Last active date:" %}</dt> <dd>{{ user.last_active_date }}</dd>
<dd>{{ user.last_active_date }}</dd>
</div>
<div class="is-flex"> <dt class="is-pulled-left mr-5">{% trans "Manually approved followers:" %}</dt>
<dt>{% trans "Manually approved followers:" %}</dt> <dd>{{ user.manually_approves_followers }}</dd>
<dd>{{ user.manually_approves_followers }}</dd>
</div>
<div class="is-flex"> <dt class="is-pulled-left mr-5">{% trans "Discoverable:" %}</dt>
<dt>{% trans "Discoverable:" %}</dt> <dd>{{ user.discoverable }}</dd>
<dd>{{ user.discoverable }}</dd>
</div>
{% if not user.is_active %} {% if not user.is_active %}
<div class="is-flex"> <dt class="is-pulled-left mr-5">{% trans "Deactivation reason:" %}</dt>
<dt>{% trans "Deactivation reason:" %}</dt> <dd>{{ user.deactivation_reason }}</dd>
<dd>{{ user.deactivation_reason }}</dd>
</div>
{% endif %} {% endif %}
{% if not user.is_active and user.deactivation_reason == "pending" %} {% if not user.is_active and user.deactivation_reason == "pending" %}
<div class="is-flex"> <dt class="is-pulled-left mr-5">{% trans "Confirmation code:" %}</dt>
<dt>{% trans "Confirmation code:" %}</dt> <dd>{{ user.confirmation_code }}</dd>
<dd>{{ user.confirmation_code }}</dd>
</div>
{% endif %} {% endif %}
</dl> </dl>
</div> </div>
@ -113,18 +97,14 @@
{% if server %} {% if server %}
<h5>{{ server.server_name }}</h5> <h5>{{ server.server_name }}</h5>
<dl> <dl>
<div class="is-flex"> <dt class="is-pulled-left mr-5">{% trans "Software:" %}</dt>
<dt>{% trans "Software:" %}</dt> <dd>{{ server.application_type }}</dd>
<dd>{{ server.application_type }}</dd>
</div> <dt class="is-pulled-left mr-5">{% trans "Version:" %}</dt>
<div class="is-flex"> <dd>{{ server.application_version }}</dd>
<dt>{% trans "Version:" %}</dt>
<dd>{{ server.application_version }}</dd> <dt class="is-pulled-left mr-5">{% trans "Status:" %}</dt>
</div> <dd>{{ server.status }}</dd>
<div class="is-flex">
<dt>{% trans "Status:" %}</dt>
<dd>{{ server.status }}</dd>
</div>
</dl> </dl>
{% if server.notes %} {% if server.notes %}
<h5>{% trans "Notes" %}</h5> <h5>{% trans "Notes" %}</h5>

View File

@ -36,7 +36,7 @@
{% if user.local %} {% if user.local %}
<div> <div>
{% include "user_admin/delete_user_form.html" with controls_text="delete_user" class="mt-2 mb-2" %} {% include "settings/users/delete_user_form.html" with controls_text="delete_user" class="mt-2 mb-2" %}
</div> </div>
{% endif %} {% endif %}

View File

@ -0,0 +1,13 @@
{% extends 'components/inline_form.html' %}
{% load i18n %}
{% block header %}
{% trans "Create Shelf" %}
{% endblock %}
{% block form %}
<form name="create-shelf" action="{% url 'shelf-create' %}" method="post">
{% include "shelf/form.html" with editable=shelf.editable form=create_form %}
</form>
{% endblock %}

View File

@ -0,0 +1,13 @@
{% extends 'components/inline_form.html' %}
{% load i18n %}
{% block header %}
{% trans "Edit Shelf" %}
{% endblock %}
{% block form %}
<form name="edit-shelf" action="{{ shelf.local_path }}" method="post">
{% include "shelf/form.html" with editable=shelf.editable form=edit_form privacy=shelf.privacy %}
</form>
{% endblock %}

View File

@ -0,0 +1,28 @@
{% load i18n %}
{% load utilities %}
{% with 0|uuid as uuid %}
{% csrf_token %}
<input type="hidden" name="user" value="{{ request.user.id }}">
{% if editable %}
<div class="field">
<label class="label" for="id_name">{% trans "Name:" %}</label>
<input type="text" name="name" value="{{ form.name.value|default:'' }}" maxlength="100" class="input" required="" id="id_name">
</div>
{% else %}
<input type="hidden" name="name" required="true" value="{{ shelf.name }}">
{% endif %}
<div class="field">
<label class="label" for="id_description_{{ uuid }}">{% trans "Description:" %}</label>
<textarea name="description" cols="40" rows="5" maxlength="500" class="textarea" id="id_description_{{ uuid }}">{{ form.description.value|default:'' }}</textarea>
</div>
<div class="field has-addons">
<div class="control">
{% include 'snippets/privacy_select.html' with current=privacy %}
</div>
<div class="control">
<button class="button is-primary" type="submit">{% trans "Save" %}</button>
</div>
</div>
{% endwith %}

View File

@ -5,7 +5,7 @@
{% load i18n %} {% load i18n %}
{% block title %} {% block title %}
{% include 'user/shelf/books_header.html' %} {% include 'user/books_header.html' %}
{% endblock %} {% endblock %}
{% block opengraph_images %} {% block opengraph_images %}
@ -15,7 +15,7 @@
{% block content %} {% block content %}
<header class="block"> <header class="block">
<h1 class="title"> <h1 class="title">
{% include 'user/shelf/books_header.html' %} {% include 'user/books_header.html' %}
</h1> </h1>
</header> </header>
@ -60,45 +60,62 @@
</div> </div>
<div class="block"> <div class="block">
{% include 'user/shelf/create_shelf_form.html' with controls_text='create_shelf_form' %} {% include 'shelf/create_shelf_form.html' with controls_text='create_shelf_form' %}
</div> </div>
<div class="block columns is-mobile"> <div>
<div class="column"> <div class="block columns is-mobile">
<h2 class="title is-3"> <div class="column">
{{ shelf.name }} <h2 class="title is-3">
<span class="subtitle"> {{ shelf.name }}
{% include 'snippets/privacy-icons.html' with item=shelf %} <span class="subtitle">
</span> {% include 'snippets/privacy-icons.html' with item=shelf %}
{% with count=books.paginator.count %} </span>
{% if count %} {% with count=books.paginator.count %}
<p class="help"> {% if count %}
{% blocktrans trimmed count counter=count with formatted_count=count|intcomma %} <p class="help">
{{ formatted_count }} book {% blocktrans trimmed count counter=count with formatted_count=count|intcomma %}
{% plural %} {{ formatted_count }} book
{{ formatted_count }} books {% plural %}
{% endblocktrans %} {{ formatted_count }} books
{% if books.has_other_pages %}
{% blocktrans trimmed with start=books.start_index end=books.end_index %}
(showing {{ start }}-{{ end }})
{% endblocktrans %} {% endblocktrans %}
{% if books.has_other_pages %}
{% blocktrans trimmed with start=books.start_index end=books.end_index %}
(showing {{ start }}-{{ end }})
{% endblocktrans %}
{% endif %}
</p>
{% endif %} {% endif %}
</p> {% endwith %}
{% endif %} </h2>
{% endwith %} </div>
</h2> {% if is_self and shelf.id %}
</div> <div class="column is-narrow">
{% if is_self and shelf.id %} <div class="is-flex">
<div class="column is-narrow"> {% trans "Edit shelf" as button_text %}
{% trans "Edit shelf" as button_text %} {% include 'snippets/toggle/open_button.html' with text=button_text icon_with_text="pencil" controls_text="edit_shelf_form" focus="edit_shelf_form_header" %}
{% include 'snippets/toggle/open_button.html' with text=button_text icon_with_text="pencil" controls_text="edit_shelf_form" focus="edit_shelf_form_header" %}
{% if shelf.deletable %}
<form class="ml-1" name="delete-shelf" action="/delete-shelf/{{ shelf.id }}" method="post">
{% csrf_token %}
<input type="hidden" name="user" value="{{ request.user.id }}">
<button class="button is-danger is-light" type="submit">
{% trans "Delete shelf" %}
</button>
</form>
{% endif %}
</div>
</div>
{% endif %}
</div> </div>
{% if shelf.description %}
<p>{{ shelf.description }}</p>
{% endif %} {% endif %}
</div> </div>
<div class="block"> <div class="block">
{% include 'user/shelf/edit_shelf_form.html' with controls_text="edit_shelf_form" %} {% include 'shelf/edit_shelf_form.html' with controls_text="edit_shelf_form" %}
</div> </div>
<div class="block"> <div class="block">
@ -167,17 +184,7 @@
</tbody> </tbody>
</table> </table>
{% else %} {% else %}
<p>{% trans "This shelf is empty." %}</p> <p><em>{% trans "This shelf is empty." %}</em></p>
{% if shelf.id and shelf.editable %}
<form name="delete-shelf" action="/delete-shelf/{{ shelf.id }}" method="post">
{% csrf_token %}
<input type="hidden" name="user" value="{{ request.user.id }}">
<button class="button is-danger is-light" type="submit">
{% trans "Delete shelf" %}
</button>
</form>
{% endif %}
{% endif %} {% endif %}
</div> </div>

View File

@ -1,36 +1,36 @@
{% load i18n %} {% load i18n %}
<form method="post" name="goal" action="{% url 'user-goal' request.user.localname year %}"> <div class="content">
{% csrf_token %} <p>{% blocktrans %}Set a goal for how many books you'll finish reading in {{ year }}, and track your progress throughout the year.{% endblocktrans %}</p>
<input type="hidden" name="year" value="{% if goal %}{{ goal.year }}{% else %}{{ year }}{% endif %}">
<input type="hidden" name="user" value="{{ request.user.id }}">
<div class="columns"> <form method="post" name="goal" action="{% url 'user-goal' request.user.localname year %}">
<div class="column"> {% csrf_token %}
<label class="label" for="id_goal">{% trans "Reading goal:" %}</label> <input type="hidden" name="year" value="{% if goal %}{{ goal.year }}{% else %}{{ year }}{% endif %}">
<div class="field has-addons"> <input type="hidden" name="user" value="{{ request.user.id }}">
<div class="control">
<input type="number" class="input" name="goal" min="1" id="id_goal" value="{% if goal %}{{ goal.goal }}{% else %}12{% endif %}"> <div class="columns">
<div class="column">
<label class="label" for="id_goal">{% trans "Reading goal:" %}</label>
<div class="field has-addons">
<div class="control">
<input type="number" class="input" name="goal" min="1" id="id_goal" value="{% if goal %}{{ goal.goal }}{% else %}12{% endif %}">
</div>
<p class="button is-static" aria-hidden="true">{% trans "books" %}</p>
</div> </div>
<p class="button is-static" aria-hidden="true">{% trans "books" %}</p> </div>
<div class="column">
<label class="label" for="privacy_{{ goal.id }}">{% trans "Goal privacy:" %}</label>
{% include 'snippets/privacy_select.html' with no_label=True current=goal.privacy uuid=goal.id %}
</div> </div>
</div> </div>
<div class="column"> <label for="post_status" class="label">
<label class="label"><p class="mb-2">{% trans "Goal privacy:" %}</p> <input type="checkbox" name="post-status" id="post_status" class="checkbox" checked>
{% include 'snippets/privacy_select.html' with no_label=True current=goal.privacy %} {% trans "Post to feed" %}
</label> </label>
</div>
</div>
<label for="post_status" class="label">
<input type="checkbox" name="post-status" id="post_status" class="checkbox" checked>
{% trans "Post to feed" %}
</label>
<p> <div class="field">
<button type="submit" class="button is-link">{% trans "Set goal" %}</button> <button type="submit" class="button is-link">{% trans "Set goal" %}</button>
{% if goal %} </div>
{% trans "Cancel" as button_text %} </form>
{% include 'snippets/toggle/close_button.html' with text=button_text controls_text="show_edit_goal" %} </div>
{% endif %}
</p>
</form>

View File

@ -1,7 +1,7 @@
{% load i18n %} {% load i18n %}
{% load utilities %} {% load utilities %}
<div class="select {{ class }}"> <div class="select {{ class }}">
{% with 0|uuid as uuid %} {% firstof uuid 0|uuid as uuid %}
{% if not no_label %} {% if not no_label %}
<label class="is-sr-only" for="privacy_{{ uuid }}">{% trans "Post privacy" %}</label> <label class="is-sr-only" for="privacy_{{ uuid }}">{% trans "Post privacy" %}</label>
{% endif %} {% endif %}
@ -20,6 +20,5 @@
{% trans "Private" %} {% trans "Private" %}
</option> </option>
</select> </select>
{% endwith %}
</div> </div>

View File

@ -0,0 +1,27 @@
{% load i18n %}
<div class="field has-addons">
<div class="control">
<input
type="number"
name="progress"
class="input"
id="id_progress_{{ readthrough.id }}"
value="{{ readthrough.progress }}"
{% if progress_required %}required{% endif %}
>
</div>
<div class="control select">
<select name="progress_mode" aria-label="Progress mode">
<option
value="PG"
{% if readthrough.progress_mode == 'PG' %}selected{% endif %}>
{% trans "pages" %}
</option>
<option
value="PCT"
{% if readthrough.progress_mode == 'PCT' %}selected{% endif %}>
{% trans "percent" %}
</option>
</select>
</div>
</div>

View File

@ -11,6 +11,7 @@ Finish "<em>{{ book_title }}</em>"
{% block modal-form-open %} {% block modal-form-open %}
<form name="finish-reading" action="{% url 'reading-status' 'finish' book.id %}" method="post" class="submit-status"> <form name="finish-reading" action="{% url 'reading-status' 'finish' book.id %}" method="post" class="submit-status">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="id" value="{{ readthrough.id }}">
<input type="hidden" name="reading_status" value="read"> <input type="hidden" name="reading_status" value="read">
{% endblock %} {% endblock %}

View File

@ -5,12 +5,13 @@
{% block content_label %} {% block content_label %}
{% trans "Comment:" %} {% trans "Comment:" %}
{% if optional %}
<span class="help mt-0 has-text-weight-normal">{% trans "(Optional)" %}</span> <span class="help mt-0 has-text-weight-normal">{% trans "(Optional)" %}</span>
{% endif %}
{% endblock %} {% endblock %}
{% block initial_fields %} {% block initial_fields %}
<input type="hidden" name="user" value="{{ request.user.id }}"> <input type="hidden" name="user" value="{{ request.user.id }}">
<input type="hidden" name="mention_books" value="{{ book.id }}"> <input type="hidden" name="mention_books" value="{{ book.id }}">
<input type="hidden" name="book" value="{{ book.id }}"> <input type="hidden" name="book" value="{{ book.id }}">
<input type="hidden" name="id" value="{{ readthrough.id }}">
{% endblock %} {% endblock %}

View File

@ -13,14 +13,18 @@
{% trans "Post to feed" %} {% trans "Post to feed" %}
</label> </label>
<div class="is-hidden" id="hide_reading_content_{{ local_uuid }}_{{ uuid }}"> <div class="is-hidden" id="hide_reading_content_{{ local_uuid }}_{{ uuid }}">
<button class="button is-link" type="submit">{% trans "Save" %}</button> <button class="button is-link" type="submit">
<span class="icon icon-spinner" aria-hidden="true"></span>
<span>{% trans "Save" %}</span>
</button>
</div> </div>
</div> </div>
<div id="reading_content_{{ local_uuid }}_{{ uuid }}"> <div id="reading_content_{{ local_uuid }}_{{ uuid }}">
<hr aria-hidden="true"> <hr aria-hidden="true">
<fieldset id="reading_content_fieldset_{{ local_uuid }}_{{ uuid }}"> <fieldset id="reading_content_fieldset_{{ local_uuid }}_{{ uuid }}">
{% include "snippets/reading_modals/form.html" with optional=True %} {% comparison_bool controls_text "progress_update" True as optional %}
{% include "snippets/reading_modals/form.html" with optional=optional %}
</fieldset> </fieldset>
</div> </div>
{% endwith %} {% endwith %}

View File

@ -1,4 +1,4 @@
{% extends 'components/modal.html' %} {% extends 'snippets/reading_modals/layout.html' %}
{% load i18n %} {% load i18n %}
{% block modal-title %} {% block modal-title %}
@ -6,42 +6,12 @@
{% endblock %} {% endblock %}
{% block modal-form-open %} {% block modal-form-open %}
<form action="{% url 'edit-readthrough' %}" method="POST" class="submit-status"> <form name="reading-progress" action="{% url 'reading-status-update' book.id %}" method="POST" class="submit-status">
{% endblock %}
{% block modal-body %}
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="id" value="{{ readthrough.id }}"/> <input type="hidden" name="id" value="{{ readthrough.id }}">
<div class="field">
<label class="label is-align-self-center mb-0 pr-2" for="progress">{% trans "Progress:" %}</label>
<div class="field has-addons mb-0">
<div class="control">
<input
aria-label="{% if readthrough.progress_mode == 'PG' %}Current page{% else %}Percent read{% endif %}"
class="input"
type="number"
min="0"
name="progress"
size="3"
value="{{ readthrough.progress|default:'' }}"
>
</div>
<div class="control select">
<select name="progress_mode" aria-label="Progress mode">
<option value="PG" {% if readthrough.progress_mode == 'PG' %}selected{% endif %}>{% trans "pages" %}</option>
<option value="PCT" {% if readthrough.progress_mode == 'PCT' %}selected{% endif %}>{% trans "percent" %}</option>
</select>
</div>
</div>
{% if readthrough.progress_mode == 'PG' and book.pages %}
<p class="help">{% blocktrans with pages=book.pages %}of {{ pages }} pages{% endblocktrans %}</p>
{% endif %}
</div>
{% endblock %} {% endblock %}
{% block modal-footer %} {% block reading-dates %}
<button class="button is-success" type="submit">{% trans "Save" %}</button> <label for="id_progress_{{ readthrough.id }}" class="label">{% trans "Progress:" %}</label>
{% trans "Cancel" as button_text %} {% include "snippets/progress_field.html" with progress_required=True %}
{% include 'snippets/toggle/toggle_button.html' with text=button_text %}
{% endblock %} {% endblock %}
{% block modal-form-close %}</form>{% endblock %}

View File

@ -13,17 +13,7 @@
<label class="label" for="id_progress_{{ readthrough.id }}"> <label class="label" for="id_progress_{{ readthrough.id }}">
{% trans "Progress" %} {% trans "Progress" %}
</label> </label>
<div class="field has-addons"> {% include "snippets/progress_field.html" %}
<div class="control">
<input type="number" name="progress" class="input" id="id_progress_{{ readthrough.id }}" value="{{ readthrough.progress }}">
</div>
<div class="control select">
<select name="progress_mode" aria-label="Progress mode">
<option value="PG" {% if readthrough.progress_mode == 'PG' %}selected{% endif %}>{% trans "pages" %}</option>
<option value="PCT" {% if readthrough.progress_mode == 'PCT' %}selected{% endif %}>{% trans "percent" %}</option>
</select>
</div>
</div>
{% endif %} {% endif %}
<div class="field"> <div class="field">
<label class="label" for="id_finish_date_{{ readthrough.id }}"> <label class="label" for="id_finish_date_{{ readthrough.id }}">

View File

@ -6,6 +6,6 @@
{% trans "Report" as button_text %} {% trans "Report" as button_text %}
{% include 'snippets/toggle/toggle_button.html' with class="is-danger is-light is-small is-fullwidth" text=button_text controls_text="report" controls_uid=report_uuid focus="modal_title_report" disabled=is_current %} {% include 'snippets/toggle/toggle_button.html' with class="is-danger is-light is-small is-fullwidth" text=button_text controls_text="report" controls_uid=report_uuid focus="modal_title_report" disabled=is_current %}
{% include 'moderation/report_modal.html' with user=user reporter=request.user controls_text="report" controls_uid=report_uuid %} {% include 'snippets/report_modal.html' with user=user reporter=request.user controls_text="report" controls_uid=report_uuid %}
{% endwith %} {% endwith %}

View File

@ -25,7 +25,7 @@
{% include 'snippets/reading_modals/finish_reading_modal.html' with book=active_shelf.book controls_text="finish_reading" controls_uid=uuid readthrough=readthrough %} {% include 'snippets/reading_modals/finish_reading_modal.html' with book=active_shelf.book controls_text="finish_reading" controls_uid=uuid readthrough=readthrough %}
{% include 'snippets/reading_modals/progress_update_modal.html' with book=active_shelf_book.book controls_text="progress_update" controls_uid=uuid readthrough=readthrough %} {% include 'snippets/reading_modals/progress_update_modal.html' with book=active_shelf.book controls_text="progress_update" controls_uid=uuid readthrough=readthrough %}
{% endwith %} {% endwith %}
{% endif %} {% endif %}

View File

@ -1,5 +1,6 @@
{% extends 'user/layout.html' %} {% extends 'user/layout.html' %}
{% load i18n %} {% load i18n %}
{% load utilities %}
{% block header %} {% block header %}
<div class="columns is-mobile"> <div class="columns is-mobile">
@ -19,20 +20,8 @@
<section class="block"> <section class="block">
{% now 'Y' as current_year %} {% now 'Y' as current_year %}
{% if user == request.user and year|add:0 == current_year|add:0 %} {% if user == request.user and year|add:0 == current_year|add:0 %}
<div class="block"> {% comparison_bool goal None as visible %}
<section class="card {% if goal %}is-hidden{% endif %}" id="show_edit_goal"> {% include 'user/goal_form.html' with goal=goal year=year visible=visible controls_text="show_edit_goal" class="block" %}
<header class="card-header">
<h2 class="card-header-title has-background-primary has-text-white" tabindex="0" id="edit_form_header">
<span class="icon icon-book is-size-3 mr-2" aria-hidden="true"></span> {% blocktrans %}{{ year }} Reading Goal{% endblocktrans %}
</h2>
</header>
<section class="card-content content">
<p>{% blocktrans %}Set a goal for how many books you'll finish reading in {{ year }}, and track your progress throughout the year.{% endblocktrans %}</p>
{% include 'snippets/goal_form.html' with goal=goal year=year %}
</section>
</section>
</div>
{% endif %} {% endif %}
{% if not goal and user != request.user %} {% if not goal and user != request.user %}

View File

@ -0,0 +1,12 @@
{% extends 'components/inline_form.html' %}
{% load i18n %}
{% block header %}
<span class="icon icon-book is-size-3 mr-2" aria-hidden="true"></span>
{% blocktrans %}{{ year }} Reading Goal{% endblocktrans %}
{% endblock %}
{% block form %}
{% include "snippets/goal_form.html" %}
{% endblock %}

View File

@ -9,6 +9,6 @@
{% block nullstate %} {% block nullstate %}
<div> <div>
{% blocktrans with username=user.display_name %}{{ username }} has no followers{% endblocktrans %} <em>{% blocktrans with username=user.display_name %}{{ username }} has no followers{% endblocktrans %}</em>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -9,7 +9,7 @@
{% block nullstate %} {% block nullstate %}
<div> <div>
{% blocktrans with username=user.display_name %}{{ username }} isn't following any users{% endblocktrans %} <em>{% blocktrans with username=user.display_name %}{{ username }} isn't following any users{% endblocktrans %}</em>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -1,27 +0,0 @@
{% extends 'components/inline_form.html' %}
{% load i18n %}
{% block header %}
{% trans "Create Shelf" %}
{% endblock %}
{% block form %}
<form name="create-shelf" action="{% url 'shelf-create' %}" method="post">
{% csrf_token %}
<input type="hidden" name="user" value="{{ request.user.id }}">
<div class="field">
<label class="label" for="id_name_create">{% trans "Name:" %}</label>
<input type="text" name="name" maxlength="100" class="input" required="true" id="id_name_create">
</div>
<div class="field has-addons">
<div class="control">
{% include 'snippets/privacy_select.html' %}
</div>
<div class="control">
<button class="button is-primary" type="submit">{% trans "Create Shelf" %}</button>
</div>
</div>
</form>
{% endblock %}

View File

@ -1,31 +0,0 @@
{% extends 'components/inline_form.html' %}
{% load i18n %}
{% block header %}
{% trans "Edit Shelf" %}
{% endblock %}
{% block form %}
<form name="edit-shelf" action="{{ shelf.local_path }}" method="post">
{% csrf_token %}
<input type="hidden" name="user" value="{{ request.user.id }}">
{% if shelf.editable %}
<div class="field">
<label class="label" for="id_name">{% trans "Name:" %}</label>
<input type="text" name="name" maxlength="100" class="input" required="true" value="{{ shelf.name }}" id="id_name">
</div>
{% else %}
<input type="hidden" name="name" required="true" value="{{ shelf.name }}">
{% endif %}
<div class="field has-addons">
<div class="control">
{% include 'snippets/privacy_select.html' with current=shelf.privacy %}
</div>
<div class="control">
<button class="button is-primary" type="submit">{% trans "Update shelf" %}</button>
</div>
</div>
</form>
{% endblock %}

View File

@ -24,7 +24,7 @@
{% if user.bookwyrm_user %} {% if user.bookwyrm_user %}
<div class="block"> <div class="block">
<h2 class="title"> <h2 class="title">
{% include 'user/shelf/books_header.html' %} {% include 'user/books_header.html' %}
</h2> </h2>
<div class="columns is-mobile scroll-x"> <div class="columns is-mobile scroll-x">
{% for shelf in shelves %} {% for shelf in shelves %}

View File

@ -1,19 +0,0 @@
{% extends 'settings/layout.html' %}
{% load i18n %}
{% block title %}{{ user.username }}{% endblock %}
{% block header %}
{{ user.username }}
<p class="help has-text-weight-normal">
<a href="{% url 'settings-users' %}">{% trans "Back to users" %}</a>
</p>
{% endblock %}
{% block panel %}
{% include 'user_admin/user_info.html' with user=user %}
{% include 'user_admin/user_moderation_actions.html' with user=user %}
{% endblock %}

View File

@ -70,6 +70,9 @@ def related_status(notification):
@register.simple_tag(takes_context=True) @register.simple_tag(takes_context=True)
def active_shelf(context, book): def active_shelf(context, book):
"""check what shelf a user has a book on, if any""" """check what shelf a user has a book on, if any"""
if hasattr(book, "current_shelves"):
return book.current_shelves[0] if len(book.current_shelves) else {"book": book}
shelf = ( shelf = (
models.ShelfBook.objects.filter( models.ShelfBook.objects.filter(
shelf__user=context["request"].user, shelf__user=context["request"].user,
@ -84,8 +87,11 @@ def active_shelf(context, book):
@register.simple_tag(takes_context=False) @register.simple_tag(takes_context=False)
def latest_read_through(book, user): def latest_read_through(book, user):
"""the most recent read activity""" """the most recent read activity"""
if hasattr(book, "active_readthroughs"):
return book.active_readthroughs[0] if len(book.active_readthroughs) else None
return ( return (
models.ReadThrough.objects.filter(user=user, book=book) models.ReadThrough.objects.filter(user=user, book=book, is_active=True)
.order_by("-start_date") .order_by("-start_date")
.first() .first()
) )

View File

@ -36,8 +36,10 @@ def get_title(book, too_short=5):
@register.simple_tag(takes_context=False) @register.simple_tag(takes_context=False)
def comparison_bool(str1, str2): def comparison_bool(str1, str2, reverse=False):
"""idk why I need to write a tag for this, it returns a bool""" """idk why I need to write a tag for this, it returns a bool"""
if reverse:
return str1 != str2
return str1 == str2 return str1 == str2

View File

@ -1,5 +1,6 @@
""" testing models """ """ testing models """
from unittest.mock import patch from unittest.mock import patch
from django.http import Http404
from django.test import TestCase from django.test import TestCase
from bookwyrm import models from bookwyrm import models
@ -39,14 +40,14 @@ class BaseModel(TestCase):
"""these should be generated""" """these should be generated"""
self.test_model.id = 1 self.test_model.id = 1
expected = self.test_model.get_remote_id() expected = self.test_model.get_remote_id()
self.assertEqual(expected, "https://%s/bookwyrmtestmodel/1" % DOMAIN) self.assertEqual(expected, f"https://{DOMAIN}/bookwyrmtestmodel/1")
def test_remote_id_with_user(self): def test_remote_id_with_user(self):
"""format of remote id when there's a user object""" """format of remote id when there's a user object"""
self.test_model.user = self.local_user self.test_model.user = self.local_user
self.test_model.id = 1 self.test_model.id = 1
expected = self.test_model.get_remote_id() expected = self.test_model.get_remote_id()
self.assertEqual(expected, "https://%s/user/mouse/bookwyrmtestmodel/1" % DOMAIN) self.assertEqual(expected, f"https://{DOMAIN}/user/mouse/bookwyrmtestmodel/1")
def test_set_remote_id(self): def test_set_remote_id(self):
"""this function sets remote ids after creation""" """this function sets remote ids after creation"""
@ -55,9 +56,7 @@ class BaseModel(TestCase):
instance = models.Work.objects.create(title="work title") instance = models.Work.objects.create(title="work title")
instance.remote_id = None instance.remote_id = None
base_model.set_remote_id(None, instance, True) base_model.set_remote_id(None, instance, True)
self.assertEqual( self.assertEqual(instance.remote_id, f"https://{DOMAIN}/book/{instance.id}")
instance.remote_id, "https://%s/book/%d" % (DOMAIN, instance.id)
)
# shouldn't set remote_id if it's not created # shouldn't set remote_id if it's not created
instance.remote_id = None instance.remote_id = None
@ -70,28 +69,30 @@ class BaseModel(TestCase):
obj = models.Status.objects.create( obj = models.Status.objects.create(
content="hi", user=self.remote_user, privacy="public" content="hi", user=self.remote_user, privacy="public"
) )
self.assertTrue(obj.visible_to_user(self.local_user)) self.assertIsNone(obj.raise_visible_to_user(self.local_user))
obj = models.Shelf.objects.create( obj = models.Shelf.objects.create(
name="test", user=self.remote_user, privacy="unlisted" name="test", user=self.remote_user, privacy="unlisted"
) )
self.assertTrue(obj.visible_to_user(self.local_user)) self.assertIsNone(obj.raise_visible_to_user(self.local_user))
obj = models.Status.objects.create( obj = models.Status.objects.create(
content="hi", user=self.remote_user, privacy="followers" content="hi", user=self.remote_user, privacy="followers"
) )
self.assertFalse(obj.visible_to_user(self.local_user)) with self.assertRaises(Http404):
obj.raise_visible_to_user(self.local_user)
obj = models.Status.objects.create( obj = models.Status.objects.create(
content="hi", user=self.remote_user, privacy="direct" content="hi", user=self.remote_user, privacy="direct"
) )
self.assertFalse(obj.visible_to_user(self.local_user)) with self.assertRaises(Http404):
obj.raise_visible_to_user(self.local_user)
obj = models.Status.objects.create( obj = models.Status.objects.create(
content="hi", user=self.remote_user, privacy="direct" content="hi", user=self.remote_user, privacy="direct"
) )
obj.mention_users.add(self.local_user) obj.mention_users.add(self.local_user)
self.assertTrue(obj.visible_to_user(self.local_user)) self.assertIsNone(obj.raise_visible_to_user(self.local_user))
@patch("bookwyrm.activitystreams.add_status_task.delay") @patch("bookwyrm.activitystreams.add_status_task.delay")
def test_object_visible_to_user_follower(self, _): def test_object_visible_to_user_follower(self, _):
@ -100,18 +101,19 @@ class BaseModel(TestCase):
obj = models.Status.objects.create( obj = models.Status.objects.create(
content="hi", user=self.remote_user, privacy="followers" content="hi", user=self.remote_user, privacy="followers"
) )
self.assertTrue(obj.visible_to_user(self.local_user)) self.assertIsNone(obj.raise_visible_to_user(self.local_user))
obj = models.Status.objects.create( obj = models.Status.objects.create(
content="hi", user=self.remote_user, privacy="direct" content="hi", user=self.remote_user, privacy="direct"
) )
self.assertFalse(obj.visible_to_user(self.local_user)) with self.assertRaises(Http404):
obj.raise_visible_to_user(self.local_user)
obj = models.Status.objects.create( obj = models.Status.objects.create(
content="hi", user=self.remote_user, privacy="direct" content="hi", user=self.remote_user, privacy="direct"
) )
obj.mention_users.add(self.local_user) obj.mention_users.add(self.local_user)
self.assertTrue(obj.visible_to_user(self.local_user)) self.assertIsNone(obj.raise_visible_to_user(self.local_user))
@patch("bookwyrm.activitystreams.add_status_task.delay") @patch("bookwyrm.activitystreams.add_status_task.delay")
def test_object_visible_to_user_blocked(self, _): def test_object_visible_to_user_blocked(self, _):
@ -120,9 +122,11 @@ class BaseModel(TestCase):
obj = models.Status.objects.create( obj = models.Status.objects.create(
content="hi", user=self.remote_user, privacy="public" content="hi", user=self.remote_user, privacy="public"
) )
self.assertFalse(obj.visible_to_user(self.local_user)) with self.assertRaises(Http404):
obj.raise_visible_to_user(self.local_user)
obj = models.Shelf.objects.create( obj = models.Shelf.objects.create(
name="test", user=self.remote_user, privacy="unlisted" name="test", user=self.remote_user, privacy="unlisted"
) )
self.assertFalse(obj.visible_to_user(self.local_user)) with self.assertRaises(Http404):
obj.raise_visible_to_user(self.local_user)

View File

@ -0,0 +1 @@
from . import *

View File

@ -1,5 +1,6 @@
""" test for app action functionality """ """ test for app action functionality """
from unittest.mock import patch from unittest.mock import patch
from tidylib import tidy_document
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
@ -34,5 +35,8 @@ class DashboardViews(TestCase):
request.user.is_superuser = True request.user.is_superuser = True
result = view(request) result = view(request)
self.assertIsInstance(result, TemplateResponse) self.assertIsInstance(result, TemplateResponse)
result.render() html = result.render()
_, errors = tidy_document(html.content)
if errors:
raise Exception(errors)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)

View File

@ -1,5 +1,7 @@
""" test for app action functionality """ """ test for app action functionality """
from unittest.mock import patch from unittest.mock import patch
from tidylib import tidy_document
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
@ -36,7 +38,10 @@ class EmailBlocklistViews(TestCase):
result = view(request) result = view(request)
self.assertIsInstance(result, TemplateResponse) self.assertIsInstance(result, TemplateResponse)
result.render() html = result.render()
_, errors = tidy_document(html.content, options={"drop-empty-elements": False})
if errors:
raise Exception(errors)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
def test_blocklist_page_post(self): def test_blocklist_page_post(self):
@ -49,7 +54,10 @@ class EmailBlocklistViews(TestCase):
result = view(request) result = view(request)
self.assertIsInstance(result, TemplateResponse) self.assertIsInstance(result, TemplateResponse)
result.render() html = result.render()
_, errors = tidy_document(html.content, options={"drop-empty-elements": False})
if errors:
raise Exception(errors)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
self.assertTrue( self.assertTrue(

View File

@ -1,6 +1,8 @@
""" test for app action functionality """ """ test for app action functionality """
import json import json
from unittest.mock import patch from unittest.mock import patch
from tidylib import tidy_document
from django.core.files.uploadedfile import SimpleUploadedFile from django.core.files.uploadedfile import SimpleUploadedFile
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.test import TestCase from django.test import TestCase
@ -46,10 +48,19 @@ class FederationViews(TestCase):
request.user.is_superuser = True request.user.is_superuser = True
result = view(request) result = view(request)
self.assertIsInstance(result, TemplateResponse) self.assertIsInstance(result, TemplateResponse)
result.render() html = result.render()
_, errors = tidy_document(
html.content,
options={
"drop-empty-elements": False,
"warn-proprietary-attributes": False,
},
)
if errors:
raise Exception(errors)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
def test_server_page(self): def test_instance_page(self):
"""there are so many views, this just makes sure it LOADS""" """there are so many views, this just makes sure it LOADS"""
server = models.FederatedServer.objects.create(server_name="hi.there.com") server = models.FederatedServer.objects.create(server_name="hi.there.com")
view = views.FederatedServer.as_view() view = views.FederatedServer.as_view()
@ -59,7 +70,10 @@ class FederationViews(TestCase):
result = view(request, server.id) result = view(request, server.id)
self.assertIsInstance(result, TemplateResponse) self.assertIsInstance(result, TemplateResponse)
result.render() html = result.render()
_, errors = tidy_document(html.content, options={"drop-empty-elements": False})
if errors:
raise Exception(errors)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
def test_server_page_block(self): def test_server_page_block(self):
@ -148,7 +162,10 @@ class FederationViews(TestCase):
result = view(request) result = view(request)
self.assertIsInstance(result, TemplateResponse) self.assertIsInstance(result, TemplateResponse)
result.render() html = result.render()
_, errors = tidy_document(html.content)
if errors:
raise Exception(errors)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
def test_add_view_post_create(self): def test_add_view_post_create(self):
@ -169,6 +186,7 @@ class FederationViews(TestCase):
self.assertEqual(server.application_type, "coolsoft") self.assertEqual(server.application_type, "coolsoft")
self.assertEqual(server.status, "blocked") self.assertEqual(server.status, "blocked")
# pylint: disable=consider-using-with
def test_import_blocklist(self): def test_import_blocklist(self):
"""load a json file with a list of servers to block""" """load a json file with a list of servers to block"""
server = models.FederatedServer.objects.create(server_name="hi.there.com") server = models.FederatedServer.objects.create(server_name="hi.there.com")
@ -180,7 +198,7 @@ class FederationViews(TestCase):
{"instance": "hi.there.com", "url": "https://explanation.url"}, # existing {"instance": "hi.there.com", "url": "https://explanation.url"}, # existing
{"a": "b"}, # invalid {"a": "b"}, # invalid
] ]
json.dump(data, open("file.json", "w")) json.dump(data, open("file.json", "w")) # pylint: disable=unspecified-encoding
view = views.ImportServerBlocklist.as_view() view = views.ImportServerBlocklist.as_view()
request = self.factory.post( request = self.factory.post(

View File

@ -0,0 +1,44 @@
""" test for app action functionality """
from unittest.mock import patch
from tidylib import tidy_document
from django.template.response import TemplateResponse
from django.test import TestCase
from django.test.client import RequestFactory
from bookwyrm import models, views
class IPBlocklistViews(TestCase):
"""every response to a get request, html or json"""
def setUp(self):
"""we need basic test data and mocks"""
self.factory = RequestFactory()
with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), patch(
"bookwyrm.activitystreams.populate_stream_task.delay"
):
self.local_user = models.User.objects.create_user(
"mouse@local.com",
"mouse@mouse.mouse",
"password",
local=True,
localname="mouse",
)
models.SiteSettings.objects.create()
def test_blocklist_page_get(self):
"""there are so many views, this just makes sure it LOADS"""
view = views.IPBlocklist.as_view()
request = self.factory.get("")
request.user = self.local_user
request.user.is_superuser = True
result = view(request)
self.assertIsInstance(result, TemplateResponse)
html = result.render()
_, errors = tidy_document(html.content, options={"drop-empty-elements": False})
if errors:
raise Exception(errors)
self.assertEqual(result.status_code, 200)

View File

@ -1,6 +1,8 @@
""" test for app action functionality """ """ test for app action functionality """
import json import json
from unittest.mock import patch from unittest.mock import patch
from tidylib import tidy_document
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
@ -42,7 +44,16 @@ class ReportViews(TestCase):
result = view(request) result = view(request)
self.assertIsInstance(result, TemplateResponse) self.assertIsInstance(result, TemplateResponse)
result.render() html = result.render()
_, errors = tidy_document(
html.content,
options={
"drop-empty-elements": False,
"warn-proprietary-attributes": False,
},
)
if errors:
raise Exception(errors)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
def test_reports_page_with_data(self): def test_reports_page_with_data(self):
@ -55,7 +66,16 @@ class ReportViews(TestCase):
result = view(request) result = view(request)
self.assertIsInstance(result, TemplateResponse) self.assertIsInstance(result, TemplateResponse)
result.render() html = result.render()
_, errors = tidy_document(
html.content,
options={
"drop-empty-elements": False,
"warn-proprietary-attributes": False,
},
)
if errors:
raise Exception(errors)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
def test_report_page(self): def test_report_page(self):
@ -69,7 +89,10 @@ class ReportViews(TestCase):
result = view(request, report.id) result = view(request, report.id)
self.assertIsInstance(result, TemplateResponse) self.assertIsInstance(result, TemplateResponse)
result.render() html = result.render()
_, errors = tidy_document(html.content, options={"drop-empty-elements": False})
if errors:
raise Exception(errors)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
def test_report_comment(self): def test_report_comment(self):

View File

@ -1,5 +1,7 @@
""" test for app action functionality """ """ test for app action functionality """
from unittest.mock import patch from unittest.mock import patch
from tidylib import tidy_document
from django.contrib.auth.models import Group 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
@ -34,7 +36,10 @@ class UserAdminViews(TestCase):
request.user.is_superuser = True request.user.is_superuser = True
result = view(request) result = view(request)
self.assertIsInstance(result, TemplateResponse) self.assertIsInstance(result, TemplateResponse)
result.render() html = result.render()
_, errors = tidy_document(html.content, options={"drop-empty-elements": False})
if errors:
raise Exception(errors)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
def test_user_admin_page(self): def test_user_admin_page(self):
@ -47,7 +52,10 @@ class UserAdminViews(TestCase):
result = view(request, self.local_user.id) result = view(request, self.local_user.id)
self.assertIsInstance(result, TemplateResponse) self.assertIsInstance(result, TemplateResponse)
result.render() html = result.render()
_, errors = tidy_document(html.content, options={"drop-empty-elements": False})
if errors:
raise Exception(errors)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
@patch("bookwyrm.suggested_users.rerank_suggestions_task.delay") @patch("bookwyrm.suggested_users.rerank_suggestions_task.delay")
@ -69,7 +77,10 @@ class UserAdminViews(TestCase):
result = view(request, self.local_user.id) result = view(request, self.local_user.id)
self.assertIsInstance(result, TemplateResponse) self.assertIsInstance(result, TemplateResponse)
result.render() html = result.render()
_, errors = tidy_document(html.content, options={"drop-empty-elements": False})
if errors:
raise Exception(errors)
self.assertEqual( self.assertEqual(
list(self.local_user.groups.values_list("name", flat=True)), ["editor"] list(self.local_user.groups.values_list("name", flat=True)), ["editor"]

View File

@ -3,6 +3,7 @@ import json
import pathlib import pathlib
from unittest.mock import patch from unittest.mock import patch
from django.core.exceptions import PermissionDenied
from django.http import HttpResponseNotAllowed, HttpResponseNotFound from django.http import HttpResponseNotAllowed, HttpResponseNotFound
from django.test import TestCase, Client from django.test import TestCase, Client
from django.test.client import RequestFactory from django.test.client import RequestFactory
@ -130,22 +131,24 @@ class Inbox(TestCase):
"", "",
HTTP_USER_AGENT="http.rb/4.4.1 (Mastodon/3.3.0; +https://mastodon.social/)", HTTP_USER_AGENT="http.rb/4.4.1 (Mastodon/3.3.0; +https://mastodon.social/)",
) )
self.assertFalse(views.inbox.is_blocked_user_agent(request)) self.assertIsNone(views.inbox.raise_is_blocked_user_agent(request))
models.FederatedServer.objects.create( models.FederatedServer.objects.create(
server_name="mastodon.social", status="blocked" server_name="mastodon.social", status="blocked"
) )
self.assertTrue(views.inbox.is_blocked_user_agent(request)) with self.assertRaises(PermissionDenied):
views.inbox.raise_is_blocked_user_agent(request)
def test_is_blocked_activity(self): def test_is_blocked_activity(self):
"""check for blocked servers""" """check for blocked servers"""
activity = {"actor": "https://mastodon.social/user/whaatever/else"} activity = {"actor": "https://mastodon.social/user/whaatever/else"}
self.assertFalse(views.inbox.is_blocked_activity(activity)) self.assertIsNone(views.inbox.raise_is_blocked_activity(activity))
models.FederatedServer.objects.create( models.FederatedServer.objects.create(
server_name="mastodon.social", status="blocked" server_name="mastodon.social", status="blocked"
) )
self.assertTrue(views.inbox.is_blocked_activity(activity)) with self.assertRaises(PermissionDenied):
views.inbox.raise_is_blocked_activity(activity)
@patch("bookwyrm.suggested_users.remove_user_task.delay") @patch("bookwyrm.suggested_users.remove_user_task.delay")
def test_create_by_deactivated_user(self, _): def test_create_by_deactivated_user(self, _):
@ -157,11 +160,11 @@ class Inbox(TestCase):
activity = self.create_json activity = self.create_json
activity["actor"] = self.remote_user.remote_id activity["actor"] = self.remote_user.remote_id
activity["object"] = status_data activity["object"] = status_data
activity["type"] = "Create"
with patch("bookwyrm.views.inbox.has_valid_signature") as mock_valid: response = self.client.post(
mock_valid.return_value = True "/inbox",
json.dumps(activity),
result = self.client.post( content_type="application/json",
"/inbox", json.dumps(activity), content_type="application/json" )
) self.assertEqual(response.status_code, 403)
self.assertEqual(result.status_code, 403)

View File

@ -0,0 +1 @@
from . import *

View File

@ -1,5 +1,7 @@
""" test for app action functionality """ """ test for app action functionality """
from unittest.mock import patch from unittest.mock import patch
from tidylib import tidy_document
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
@ -44,7 +46,10 @@ class BlockViews(TestCase):
request.user = self.local_user request.user = self.local_user
result = view(request) result = view(request)
self.assertIsInstance(result, TemplateResponse) self.assertIsInstance(result, TemplateResponse)
result.render() html = result.render()
_, errors = tidy_document(html.content)
if errors:
raise Exception(errors)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
def test_block_post(self, _): def test_block_post(self, _):
@ -75,6 +80,6 @@ class BlockViews(TestCase):
request.user = self.local_user request.user = self.local_user
with patch("bookwyrm.activitystreams.add_user_statuses_task.delay"): with patch("bookwyrm.activitystreams.add_user_statuses_task.delay"):
views.block.unblock(request, self.remote_user.id) views.unblock(request, self.remote_user.id)
self.assertFalse(models.UserBlocks.objects.exists()) self.assertFalse(models.UserBlocks.objects.exists())

View File

@ -0,0 +1,61 @@
""" test for app action functionality """
from unittest.mock import patch
from tidylib import tidy_document
from django.template.response import TemplateResponse
from django.test import TestCase
from django.test.client import RequestFactory
from bookwyrm import models, views
class ChangePasswordViews(TestCase):
"""view user and edit profile"""
def setUp(self):
"""we need basic test data and mocks"""
self.factory = RequestFactory()
with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), patch(
"bookwyrm.activitystreams.populate_stream_task.delay"
):
self.local_user = models.User.objects.create_user(
"mouse@local.com",
"mouse@mouse.com",
"password",
local=True,
localname="mouse",
)
models.SiteSettings.objects.create(id=1)
def test_password_change_get(self):
"""there are so many views, this just makes sure it LOADS"""
view = views.ChangePassword.as_view()
request = self.factory.get("")
request.user = self.local_user
result = view(request)
self.assertIsInstance(result, TemplateResponse)
html = result.render()
_, errors = tidy_document(html.content)
if errors:
raise Exception(errors)
self.assertEqual(result.status_code, 200)
def test_password_change(self):
"""change password"""
view = views.ChangePassword.as_view()
password_hash = self.local_user.password
request = self.factory.post("", {"password": "hi", "confirm-password": "hi"})
request.user = self.local_user
with patch("bookwyrm.views.preferences.change_password.login"):
view(request)
self.assertNotEqual(self.local_user.password, password_hash)
def test_password_change_mismatch(self):
"""change password"""
view = views.ChangePassword.as_view()
password_hash = self.local_user.password
request = self.factory.post("", {"password": "hi", "confirm-password": "hihi"})
request.user = self.local_user
view(request)
self.assertEqual(self.local_user.password, password_hash)

View File

@ -0,0 +1,89 @@
""" test for app action functionality """
import json
from unittest.mock import patch
from tidylib import tidy_document
from django.contrib.sessions.middleware import SessionMiddleware
from django.template.response import TemplateResponse
from django.test import TestCase
from django.test.client import RequestFactory
from bookwyrm import forms, models, views
@patch("bookwyrm.suggested_users.remove_user_task.delay")
class DeleteUserViews(TestCase):
"""view user and edit profile"""
def setUp(self):
"""we need basic test data and mocks"""
self.factory = RequestFactory()
with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), patch(
"bookwyrm.activitystreams.populate_stream_task.delay"
):
self.local_user = models.User.objects.create_user(
"mouse@local.com",
"mouse@mouse.mouse",
"password",
local=True,
localname="mouse",
)
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", parent_work=models.Work.objects.create(title="test work")
)
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"), patch(
"bookwyrm.activitystreams.add_book_statuses_task.delay"
):
models.ShelfBook.objects.create(
book=self.book,
user=self.local_user,
shelf=self.local_user.shelf_set.first(),
)
models.SiteSettings.objects.create()
def test_delete_user_page(self, _):
"""there are so many views, this just makes sure it LOADS"""
view = views.DeleteUser.as_view()
request = self.factory.get("")
request.user = self.local_user
result = view(request)
self.assertIsInstance(result, TemplateResponse)
html = result.render()
_, errors = tidy_document(html.content)
if errors:
raise Exception(errors)
self.assertEqual(result.status_code, 200)
@patch("bookwyrm.suggested_users.rerank_suggestions_task")
def test_delete_user(self, *_):
"""use a form to update a user"""
view = views.DeleteUser.as_view()
form = forms.DeleteUserForm()
form.data["password"] = "password"
request = self.factory.post("", form.data)
request.user = self.local_user
middleware = SessionMiddleware()
middleware.process_request(request)
request.session.save()
self.assertIsNone(self.local_user.name)
with patch(
"bookwyrm.models.activitypub_mixin.broadcast_task.delay"
) as delay_mock:
view(request)
self.assertEqual(delay_mock.call_count, 1)
activity = json.loads(delay_mock.call_args[0][1])
self.assertEqual(activity["type"], "Delete")
self.assertEqual(activity["actor"], self.local_user.remote_id)
self.assertEqual(
activity["cc"][0], "https://www.w3.org/ns/activitystreams#Public"
)
self.local_user.refresh_from_db()
self.assertFalse(self.local_user.is_active)
self.assertEqual(self.local_user.deactivation_reason, "self_deletion")

View File

@ -1,11 +1,10 @@
""" test for app action functionality """ """ test for app action functionality """
import json
import pathlib import pathlib
from unittest.mock import patch from unittest.mock import patch
from PIL import Image from PIL import Image
from tidylib import tidy_document
from django.contrib.auth.models import AnonymousUser from django.contrib.auth.models import AnonymousUser
from django.contrib.sessions.middleware import SessionMiddleware
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from django.core.files.uploadedfile import SimpleUploadedFile from django.core.files.uploadedfile import SimpleUploadedFile
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
@ -59,7 +58,10 @@ class EditUserViews(TestCase):
request.user = self.local_user request.user = self.local_user
result = view(request) result = view(request)
self.assertIsInstance(result, TemplateResponse) self.assertIsInstance(result, TemplateResponse)
result.render() html = result.render()
_, errors = tidy_document(html.content)
if errors:
raise Exception(errors)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
def test_edit_user(self, _): def test_edit_user(self, _):
@ -91,8 +93,9 @@ class EditUserViews(TestCase):
form.data["default_post_privacy"] = "public" form.data["default_post_privacy"] = "public"
form.data["preferred_timezone"] = "UTC" form.data["preferred_timezone"] = "UTC"
image_file = pathlib.Path(__file__).parent.joinpath( image_file = pathlib.Path(__file__).parent.joinpath(
"../../static/images/no_cover.jpg" "../../../static/images/no_cover.jpg"
) )
# pylint: disable=consider-using-with
form.data["avatar"] = SimpleUploadedFile( form.data["avatar"] = SimpleUploadedFile(
image_file, open(image_file, "rb").read(), content_type="image/jpeg" image_file, open(image_file, "rb").read(), content_type="image/jpeg"
) )
@ -113,50 +116,11 @@ class EditUserViews(TestCase):
def test_crop_avatar(self, _): def test_crop_avatar(self, _):
"""reduce that image size""" """reduce that image size"""
image_file = pathlib.Path(__file__).parent.joinpath( image_file = pathlib.Path(__file__).parent.joinpath(
"../../static/images/no_cover.jpg" "../../../static/images/no_cover.jpg"
) )
image = Image.open(image_file) image = Image.open(image_file)
result = views.edit_user.crop_avatar(image) result = views.preferences.edit_user.crop_avatar(image)
self.assertIsInstance(result, ContentFile) self.assertIsInstance(result, ContentFile)
image_result = Image.open(result) image_result = Image.open(result)
self.assertEqual(image_result.size, (120, 120)) self.assertEqual(image_result.size, (120, 120))
def test_delete_user_page(self, _):
"""there are so many views, this just makes sure it LOADS"""
view = views.DeleteUser.as_view()
request = self.factory.get("")
request.user = self.local_user
result = view(request)
self.assertIsInstance(result, TemplateResponse)
result.render()
self.assertEqual(result.status_code, 200)
@patch("bookwyrm.suggested_users.rerank_suggestions_task")
def test_delete_user(self, *_):
"""use a form to update a user"""
view = views.DeleteUser.as_view()
form = forms.DeleteUserForm()
form.data["password"] = "password"
request = self.factory.post("", form.data)
request.user = self.local_user
middleware = SessionMiddleware()
middleware.process_request(request)
request.session.save()
self.assertIsNone(self.local_user.name)
with patch(
"bookwyrm.models.activitypub_mixin.broadcast_task.delay"
) as delay_mock:
view(request)
self.assertEqual(delay_mock.call_count, 1)
activity = json.loads(delay_mock.call_args[0][1])
self.assertEqual(activity["type"], "Delete")
self.assertEqual(activity["actor"], self.local_user.remote_id)
self.assertEqual(
activity["cc"][0], "https://www.w3.org/ns/activitystreams#Public"
)
self.local_user.refresh_from_db()
self.assertFalse(self.local_user.is_active)
self.assertEqual(self.local_user.deactivation_reason, "self_deletion")

View File

@ -9,6 +9,7 @@ import responses
from django.contrib.auth.models import Group, Permission from django.contrib.auth.models import Group, Permission
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.files.uploadedfile import SimpleUploadedFile from django.core.files.uploadedfile import SimpleUploadedFile
from django.http import Http404
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
@ -133,8 +134,8 @@ class BookViews(TestCase):
request.user = self.local_user request.user = self.local_user
with patch("bookwyrm.views.books.is_api_request") as is_api: with patch("bookwyrm.views.books.is_api_request") as is_api:
is_api.return_value = False is_api.return_value = False
result = view(request, 0) with self.assertRaises(Http404):
self.assertEqual(result.status_code, 404) view(request, 0)
def test_book_page_work_id(self): def test_book_page_work_id(self):
"""there are so many views, this just makes sure it LOADS""" """there are so many views, this just makes sure it LOADS"""
@ -282,6 +283,46 @@ class BookViews(TestCase):
self.assertEqual(book.authors.first().name, "Sappho") self.assertEqual(book.authors.first().name, "Sappho")
self.assertEqual(book.authors.first(), book.parent_work.authors.first()) self.assertEqual(book.authors.first(), book.parent_work.authors.first())
def _setup_cover_url(self):
cover_url = "http://example.com"
image_file = pathlib.Path(__file__).parent.joinpath(
"../../static/images/default_avi.jpg"
)
image = Image.open(image_file)
output = BytesIO()
image.save(output, format=image.format)
responses.add(
responses.GET,
cover_url,
body=output.getvalue(),
status=200,
)
return cover_url
@responses.activate
def test_create_book_upload_cover_url(self):
"""create an entirely new book and work with cover url"""
self.assertFalse(self.book.cover)
view = views.ConfirmEditBook.as_view()
self.local_user.groups.add(self.group)
cover_url = self._setup_cover_url()
form = forms.EditionForm()
form.data["title"] = "New Title"
form.data["last_edited_by"] = self.local_user.id
form.data["cover-url"] = cover_url
request = self.factory.post("", form.data)
request.user = self.local_user
with patch(
"bookwyrm.models.activitypub_mixin.broadcast_task.delay"
) as delay_mock:
views.upload_cover(request, self.book.id)
self.assertEqual(delay_mock.call_count, 1)
self.book.refresh_from_db()
self.assertTrue(self.book.cover)
def test_upload_cover_file(self): def test_upload_cover_file(self):
"""add a cover via file upload""" """add a cover via file upload"""
self.assertFalse(self.book.cover) self.assertFalse(self.book.cover)
@ -310,21 +351,8 @@ class BookViews(TestCase):
def test_upload_cover_url(self): def test_upload_cover_url(self):
"""add a cover via url""" """add a cover via url"""
self.assertFalse(self.book.cover) self.assertFalse(self.book.cover)
image_file = pathlib.Path(__file__).parent.joinpath(
"../../static/images/default_avi.jpg"
)
image = Image.open(image_file)
output = BytesIO()
image.save(output, format=image.format)
responses.add(
responses.GET,
"http://example.com",
body=output.getvalue(),
status=200,
)
form = forms.CoverForm(instance=self.book) form = forms.CoverForm(instance=self.book)
form.data["cover-url"] = "http://example.com" form.data["cover-url"] = self._setup_cover_url()
request = self.factory.post("", form.data) request = self.factory.post("", form.data)
request.user = self.local_user request.user = self.local_user

View File

@ -1,5 +1,6 @@
""" test for app action functionality """ """ test for app action functionality """
from unittest.mock import patch from unittest.mock import patch
from tidylib import tidy_document
from django.contrib.auth.models import AnonymousUser from django.contrib.auth.models import AnonymousUser
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
@ -51,7 +52,16 @@ class DirectoryViews(TestCase):
result = view(request) result = view(request)
self.assertIsInstance(result, TemplateResponse) self.assertIsInstance(result, TemplateResponse)
result.render() html = result.render()
_, errors = tidy_document(
html.content,
options={
"drop-empty-elements": False,
"warn-proprietary-attributes": False,
},
)
if errors:
raise Exception(errors)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
def test_directory_page_empty(self): def test_directory_page_empty(self):
@ -62,7 +72,10 @@ class DirectoryViews(TestCase):
result = view(request) result = view(request)
self.assertIsInstance(result, TemplateResponse) self.assertIsInstance(result, TemplateResponse)
result.render() html = result.render()
_, errors = tidy_document(html.content, options={"drop-empty-elements": False})
if errors:
raise Exception(errors)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
def test_directory_page_logged_out(self): def test_directory_page_logged_out(self):

Some files were not shown because too many files have changed in this diff Show More