Merge branch 'main' into production

This commit is contained in:
Mouse Reeve 2021-09-29 11:25:09 -07:00
commit 57396499ff
147 changed files with 2479 additions and 1659 deletions

View file

@ -29,8 +29,7 @@ class CustomForm(ModelForm):
input_type = visible.field.widget.input_type
if isinstance(visible.field.widget, Textarea):
input_type = "textarea"
visible.field.widget.attrs["cols"] = None
visible.field.widget.attrs["rows"] = None
visible.field.widget.attrs["rows"] = 5
visible.field.widget.attrs["class"] = css_classes[input_type]
@ -228,7 +227,7 @@ class ExpiryWidget(widgets.Select):
elif selected_string == "forever":
return None
else:
return selected_string # "This will raise
return selected_string # This will raise
return timezone.now() + interval
@ -269,7 +268,7 @@ class CreateInviteForm(CustomForm):
class ShelfForm(CustomForm):
class Meta:
model = models.Shelf
fields = ["user", "name", "privacy"]
fields = ["user", "name", "privacy", "description"]
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 """
import base64
from Crypto import Random
from django.core.exceptions import PermissionDenied
from django.db import models
from django.dispatch import receiver
from django.http import Http404
from django.utils.translation import gettext_lazy as _
from bookwyrm.settings import DOMAIN
@ -48,26 +51,26 @@ class BookWyrmModel(models.Model):
"""how to link to this object in the local app"""
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?"""
# make sure this is an object with privacy owned by a user
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
if viewer in self.user.blocks.all():
return False
raise Http404()
# you can see your own posts and any public or unlisted posts
if viewer == self.user or self.privacy in ["public", "unlisted"]:
return True
return
# you can see the followers only posts of people you follow
if (
self.privacy == "followers"
and self.user.followers.filter(id=viewer.id).first()
):
return True
return
# you can see dms you are tagged in
if hasattr(self, "mention_users"):
@ -75,8 +78,32 @@ class BookWyrmModel(models.Model):
self.privacy == "direct"
and self.mention_users.filter(id=viewer.id).first()
):
return True
return False
return
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)

View file

@ -3,8 +3,8 @@ import re
from django.contrib.postgres.search import SearchVectorField
from django.contrib.postgres.indexes import GinIndex
from django.db import models
from django.db import transaction
from django.db import models, transaction
from django.db.models import Prefetch
from django.dispatch import receiver
from model_utils import FieldTracker
from model_utils.managers import InheritanceManager
@ -307,6 +307,27 @@ class Edition(Book):
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):
"""convert an isbn 10 into an isbn 13"""

View file

@ -92,6 +92,12 @@ class ListItem(CollectionItemMixin, BookWyrmModel):
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:
"""A book may only be placed into a list 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)
finish_date = models.DateTimeField(blank=True, null=True)
is_active = models.BooleanField(default=True)
def save(self, *args, **kwargs):
"""update user active time"""
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)
def create_update(self):

View file

@ -1,5 +1,6 @@
""" puttin' books on shelves """
import re
from django.core.exceptions import PermissionDenied
from django.db import models
from django.utils import timezone
@ -20,6 +21,7 @@ class Shelf(OrderedCollectionMixin, BookWyrmModel):
name = fields.CharField(max_length=100)
identifier = models.CharField(max_length=100)
description = models.TextField(blank=True, null=True, max_length=500)
user = fields.ForeignKey(
"User", on_delete=models.PROTECT, activitypub_field="owner"
)
@ -51,12 +53,23 @@ class Shelf(OrderedCollectionMixin, BookWyrmModel):
"""list of books for this shelf, overrides OrderedCollectionMixin"""
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):
"""shelf identifier instead of id"""
base_path = self.user.remote_id
identifier = self.identifier or self.get_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:
"""user/shelf unqiueness"""

View file

@ -3,6 +3,7 @@ from dataclasses import MISSING
import re
from django.apps import apps
from django.core.exceptions import PermissionDenied
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.dispatch import receiver
@ -187,6 +188,13 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
"""json serialized activitypub class"""
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):
"""these are app-generated messages about user activity"""

View file

@ -13,7 +13,7 @@ VERSION = "0.0.1"
PAGE_LENGTH = env("PAGE_LENGTH", 15)
DEFAULT_LANGUAGE = env("DEFAULT_LANGUAGE", "English")
JS_CACHE = "7f2343cf"
JS_CACHE = "e2bc0653"
# email
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();
// Update shelve buttons
document.querySelectorAll("[data-shelve-button-book='" + form.book.value +"']")
.forEach(button => this.cycleShelveButtons(button, form.reading_status.value));
if (form.reading_status) {
document.querySelectorAll("[data-shelve-button-book='" + form.book.value +"']")
.forEach(button => this.cycleShelveButtons(button, form.reading_status.value));
}
return;
}

View file

@ -236,14 +236,12 @@
<label class="label" for="id_cover">{% trans "Upload cover:" %}</label>
{{ form.cover }}
</div>
{% if book %}
<div class="field">
<label class="label" for="id_cover_url">
{% trans "Load cover from url:" %}
</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>
{% endif %}
{% for error in form.cover.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}

View file

@ -1,5 +1,5 @@
{% 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">
<h2 class="card-header-title" tabindex="0" id="{{ controls_text }}{% if controls_uid %}-{{ controls_uid }}{% endif %}_header">
{% block header %}{% endblock %}

View file

@ -8,7 +8,7 @@
{% trans "Local users" %}
</label>
<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" %}
</label>
{% endblock %}

View file

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

View file

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

View file

@ -7,13 +7,8 @@
</h3>
{% endblock %}
{% block card-content %}
<div class="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' %}
</div>
{% include 'snippets/goal_form.html' %}
{% endblock %}
{% block card-footer %}

View file

@ -10,6 +10,8 @@
<link rel="stylesheet" href="{% static "css/vendor/icons.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 %}">
{% if preview_images_enabled is True %}
@ -34,7 +36,7 @@
<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">
</a>
<form class="navbar-item column" action="/search/">
<form class="navbar-item column" action="{% url 'search' %}">
<div class="field has-addons">
<div class="control">
{% if user.is_authenticated %}
@ -115,7 +117,7 @@
</a>
</li>
{% 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 %}
{% if perms.bookwyrm.create_invites and not site.allow_registration %}
<li>
@ -131,7 +133,7 @@
</a>
</li>
{% endif %}
<li class="navbar-divider" role="presentation"></li>
<li class="navbar-divider" role="presentation">&nbsp;</li>
<li>
<a href="{% url 'logout' %}" class="navbar-item">
{% 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>
</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">
<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="control">
<label for="input-list-position" class="button is-transparent is-small">{% trans "List position" %}</label>
</div>
{% csrf_token %}
<div class="control">
<input id="input_list_position" class="input is-small" type="number" min="1" name="position" value="{{ item.order }}">
</div>
@ -83,7 +83,9 @@
</div>
</form>
</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 %}
<input type="hidden" name="item" value="{{ item.id }}">
<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 %}
{% if not request.user.blocks.exists %}
<p>{% trans "No users currently blocked." %}</p>
<p><em>{% trans "No users currently blocked." %}</em></p>
{% else %}
<ul>
{% for user in request.user.blocks.all %}

View file

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

View file

@ -7,76 +7,114 @@
{% trans "Edit Profile" %}
{% 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 %}
{% if form.non_field_errors %}
<p class="notification is-danger">{{ form.non_field_errors }}</p>
{% endif %}
<form name="edit-profile" action="{% url 'prefs-profile' %}" method="post" enctype="multipart/form-data">
{% csrf_token %}
<div class="block">
<label class="label" for="id_avatar">{% trans "Avatar:" %}</label>
{{ form.avatar }}
{% for error in form.avatar.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
</div>
<div class="block">
<label class="label" for="id_name">{% trans "Display name:" %}</label>
{{ form.name }}
{% for error in form.name.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
</div>
<div class="block">
<label class="label" for="id_summary">{% trans "Summary:" %}</label>
{{ form.summary }}
{% for error in form.summary.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
</div>
<div class="block">
<label class="label" for="id_email">{% trans "Email address:" %}</label>
{{ form.email }}
{% for error in form.email.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
</div>
<div class="block">
<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_goal">
{% 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="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 }}
<section class="block" id="profile">
<h2 class="title is-4">{% trans "Profile" %}</h2>
<div class="box">
<label class="label" for="id_avatar">{% trans "Avatar:" %}</label>
<div class="field columns is-mobile">
{% if request.user.avatar %}
<div class="column is-narrow">
{% include 'snippets/avatar.html' with user=request.user large=True %}
</div>
{% endif %}
<div class="column">
{{ form.avatar }}
{% for error in form.avatar.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
</div>
</div>
<div class="field">
<label class="label" for="id_name">{% trans "Display name:" %}</label>
{{ form.name }}
{% for error in form.name.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
</div>
<div class="field">
<label class="label" for="id_summary">{% trans "Summary:" %}</label>
{{ form.summary }}
{% for error in form.summary.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
</div>
<div class="field">
<label class="label" for="id_email">{% trans "Email address:" %}</label>
{{ form.email }}
{% for error in form.email.errors %}
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
</div>
</div>
</div>
<div class="block">
<label class="label" for="id_preferred_timezone">{% trans "Preferred Timezone: " %}</label>
<div class="select">
{{ form.preferred_timezone }}
</section>
<hr aria-hidden="true">
<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 class="block"><button class="button is-primary" type="submit">{% trans "Save" %}</button></div>
</section>
<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>
{% endblock %}

View file

@ -12,7 +12,8 @@
<ul class="menu-list">
<li>
{% 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>
{% url 'prefs-password' as url %}

View file

@ -26,7 +26,7 @@
{% block panel %}
<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>
<div class="block content">

View file

@ -11,7 +11,7 @@
{% block panel %}
<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>
<div class="block">
@ -48,11 +48,10 @@
<td>{% if announcement.active %}{% trans "active" %}{% else %}{% trans "inactive" %}{% endif %}</td>
</tr>
{% endfor %}
{% if not announcements %}
<tr><td colspan="5"><em>{% trans "No announcements found" %}</em></td></tr>
{% endif %}
</table>
{% if not announcements %}
<p><em>{% trans "No announcements found." %}</em></p>
{% endif %}
</div>
{% 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">
<div class="is-flex is-align-items-flex-end">
<div class="ml-1 mr-1">
<label class="label">
<label class="label" for="id_start">
{% trans "Start date:" %}
<input class="input" type="date" name="start" value="{{ start }}">
</label>
<input class="input" type="date" name="start" value="{{ start }}" id="id_start">
</div>
<div class="ml-1 mr-1">
<label class="label">
<label class="label" for="id_end">
{% trans "End date:" %}
<input class="input" type="date" name="end" value="{{ end }}">
</label>
<input class="input" type="date" name="end" value="{{ end }}" id="id_end">
</div>
<div class="ml-1 mr-1">
<label class="label">
<label class="label" for="id_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>
<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 class="ml-1 mr-1">
<button class="button is-link" type="submit">{% trans "Submit" %}</button>
@ -115,6 +115,6 @@
{% block scripts %}
<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_status_chart.html' %}
{% include 'settings/dashboard/dashboard_user_chart.html' %}
{% include 'settings/dashboard/dashboard_status_chart.html' %}
{% endblock %}

View file

@ -12,7 +12,7 @@
{% endblock %}
{% 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">
{% 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>
</tr>
{% endfor %}
{% if not domains.exists %}
<tr><td colspan="5"><em>{% trans "No email domains currently blocked" %}</em></td></tr>
{% endif %}
</table>
{% endblock %}

View file

@ -33,6 +33,8 @@
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
</div>
</div>
<div class="column is-half">
<div class="field">
<label class="label" for="id_status">{% trans "Status:" %}</label>
<div class="select">
@ -43,6 +45,8 @@
</div>
</div>
</div>
</div>
<div class="columns">
<div class="column is-half">
<div class="field">
<label class="label" for="id_application_type">{% trans "Software:" %}</label>
@ -51,6 +55,8 @@
<p class="help is-danger">{{ error | escape }}</p>
{% endfor %}
</div>
</div>
<div class="column is-half">
<div class="field">
<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:'' }}">
@ -62,7 +68,7 @@
</div>
<div class="field">
<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>
<button type="submit" class="button is-primary">{% trans "Save" %}</button>

View file

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

View file

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

View file

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

View file

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

View file

@ -12,7 +12,7 @@
{% endblock %}
{% 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">
{% 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>
</tr>
{% endfor %}
{% if not addresses.exists %}
<tr><td colspan="2"><em>{% trans "No IP addresses currently blocked" %}</em></td></tr>
{% endif %}
</table>
{% endblock %}

View file

@ -74,14 +74,7 @@
<li>
{% url 'settings-site' as url %}
<a href="{{ url }}"{% if url in request.path %} class="is-active" aria-selected="true"{% endif %}>{% trans "Site Settings" %}</a>
{% if url in request.path %}
<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 %}
{% block site-subtabs %}{% endblock %}
</li>
</ul>
{% endif %}

View file

@ -3,20 +3,21 @@
{% load humanize %}
{% 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 %}
<div class="block">
<a href="{% url 'settings-reports' %}">{% trans "Back to reports" %}</a>
</div>
<div class="block">
{% include 'moderation/report_preview.html' with report=report %}
{% include 'settings/reports/report_preview.html' with report=report %}
</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">
<h3 class="title is-4">{% trans "Moderator Comments" %}</h3>

View file

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

View file

@ -5,36 +5,46 @@
{% 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">
{% csrf_token %}
<section class="block" id="instance_info">
<h2 class="title is-4">{% trans "Instance Info" %}</h2>
<div class="field">
<label class="label" for="id_name">{% trans "Instance Name:" %}</label>
{{ site_form.name }}
</div>
<div class="field">
<label class="label" for="id_instance_tagline">{% trans "Tagline:" %}</label>
{{ site_form.instance_tagline }}
</div>
<div class="field">
<label class="label" for="id_instance_description">{% trans "Instance description:" %}</label>
{{ site_form.instance_description }}
</div>
<div class="field">
<label class="label mb-0" for="id_short_description">{% trans "Short description:" %}</label>
<p class="help">{% trans "Used when the instance is previewed on joinbookwyrm.com. Does not support html or markdown." %}</p>
{{ site_form.instance_short_description }}
</div>
<div class="field">
<label class="label" for="id_code_of_conduct">{% trans "Code of conduct:" %}</label>
{{ site_form.code_of_conduct }}
</div>
<div class="field">
<label class="label" for="id_privacy_policy">{% trans "Privacy Policy:" %}</label>
{{ site_form.privacy_policy }}
<div class="box">
<div class="field">
<label class="label" for="id_name">{% trans "Instance Name:" %}</label>
{{ site_form.name }}
</div>
<div class="field">
<label class="label" for="id_instance_tagline">{% trans "Tagline:" %}</label>
{{ site_form.instance_tagline }}
</div>
<div class="field">
<label class="label" for="id_instance_description">{% trans "Instance description:" %}</label>
{{ site_form.instance_description }}
</div>
<div class="field">
<label class="label mb-0" for="id_short_description">{% trans "Short description:" %}</label>
<p class="help">{% trans "Used when the instance is previewed on joinbookwyrm.com. Does not support html or markdown." %}</p>
{{ site_form.instance_short_description }}
</div>
<div class="field">
<label class="label" for="id_code_of_conduct">{% trans "Code of conduct:" %}</label>
{{ site_form.code_of_conduct }}
</div>
<div class="field">
<label class="label" for="id_privacy_policy">{% trans "Privacy Policy:" %}</label>
{{ site_form.privacy_policy }}
</div>
</div>
</section>
@ -42,16 +52,16 @@
<section class="block" id="images">
<h2 class="title is-4">{% trans "Images" %}</h2>
<div class="columns">
<div class="column">
<div class="box is-flex">
<div>
<label class="label" for="id_logo">{% trans "Logo:" %}</label>
{{ site_form.logo }}
</div>
<div class="column">
<div>
<label class="label" for="id_logo_small">{% trans "Logo small:" %}</label>
{{ site_form.logo_small }}
</div>
<div class="column">
<div>
<label class="label" for="id_favicon">{% trans "Favicon:" %}</label>
{{ site_form.favicon }}
</div>
@ -62,21 +72,23 @@
<section class="block" id="footer">
<h2 class="title is-4">{% trans "Footer Content" %}</h2>
<div class="field">
<label class="label" for="id_support_link">{% trans "Support link:" %}</label>
<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>
<div class="field">
<label class="label" for="id_support_title">{% trans "Support title:" %}</label>
<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>
<div class="field">
<label class="label" for="id_admin_email">{% trans "Admin email:" %}</label>
{{ site_form.admin_email }}
</div>
<div class="field">
<label class="label" for="id_footer_item">{% trans "Additional info:" %}</label>
{{ site_form.footer_item }}
<div class="box">
<div class="field">
<label class="label" for="id_support_link">{% trans "Support link:" %}</label>
<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>
<div class="field">
<label class="label" for="id_support_title">{% trans "Support title:" %}</label>
<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>
<div class="field">
<label class="label" for="id_admin_email">{% trans "Admin email:" %}</label>
{{ site_form.admin_email }}
</div>
<div class="field">
<label class="label" for="id_footer_item">{% trans "Additional info:" %}</label>
{{ site_form.footer_item }}
</div>
</div>
</section>
@ -84,35 +96,37 @@
<section class="block" id="registration">
<h2 class="title is-4">{% trans "Registration" %}</h2>
<div class="field">
<label class="label" for="id_allow_registration">
{{ site_form.allow_registration }}
{% trans "Allow registration" %}
</label>
</div>
<div class="field">
<label class="label" for="id_allow_invite_requests">
{{ site_form.allow_invite_requests }}
{% trans "Allow invite requests" %}
</label>
</div>
<div class="field">
<label class="label mb-0" for="id_allow_invite_requests">
{{ site_form.require_confirm_email }}
{% trans "Require users to confirm email address" %}
</label>
<p class="help">{% trans "(Recommended if registration is open)" %}</p>
</div>
<div class="field">
<label class="label" for="id_registration_closed_text">{% trans "Registration closed text:" %}</label>
{{ site_form.registration_closed_text }}
</div>
<div class="field">
<label class="label" for="id_invite_request_text">{% trans "Invite request text:" %}</label>
{{ site_form.invite_request_text }}
{% for error in site_form.invite_request_text.errors %}
<p class="help is-danger">{{ error|escape }}</p>
{% endfor %}
<div class="box">
<div class="field">
<label class="label" for="id_allow_registration">
{{ site_form.allow_registration }}
{% trans "Allow registration" %}
</label>
</div>
<div class="field">
<label class="label" for="id_allow_invite_requests">
{{ site_form.allow_invite_requests }}
{% trans "Allow invite requests" %}
</label>
</div>
<div class="field">
<label class="label mb-0" for="id_require_confirm_email">
{{ site_form.require_confirm_email }}
{% trans "Require users to confirm email address" %}
</label>
<p class="help">{% trans "(Recommended if registration is open)" %}</p>
</div>
<div class="field">
<label class="label" for="id_registration_closed_text">{% trans "Registration closed text:" %}</label>
{{ site_form.registration_closed_text }}
</div>
<div class="field">
<label class="label" for="id_invite_request_text">{% trans "Invite request text:" %}</label>
{{ site_form.invite_request_text }}
{% for error in site_form.invite_request_text.errors %}
<p class="help is-danger">{{ error|escape }}</p>
{% endfor %}
</div>
</div>
</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 %}
{% include 'user_admin/user_admin_filters.html' %}
{% include 'settings/users/user_admin_filters.html' %}
<table class="table is-striped">
<tr>

View file

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

View file

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

View file

@ -36,7 +36,7 @@
{% if user.local %}
<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>
{% 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 %}
{% block title %}
{% include 'user/shelf/books_header.html' %}
{% include 'user/books_header.html' %}
{% endblock %}
{% block opengraph_images %}
@ -15,7 +15,7 @@
{% block content %}
<header class="block">
<h1 class="title">
{% include 'user/shelf/books_header.html' %}
{% include 'user/books_header.html' %}
</h1>
</header>
@ -60,45 +60,62 @@
</div>
<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 class="block columns is-mobile">
<div class="column">
<h2 class="title is-3">
{{ shelf.name }}
<span class="subtitle">
{% include 'snippets/privacy-icons.html' with item=shelf %}
</span>
{% with count=books.paginator.count %}
{% if count %}
<p class="help">
{% blocktrans trimmed count counter=count with formatted_count=count|intcomma %}
{{ formatted_count }} book
{% plural %}
{{ formatted_count }} books
{% endblocktrans %}
{% if books.has_other_pages %}
{% blocktrans trimmed with start=books.start_index end=books.end_index %}
(showing {{ start }}-{{ end }})
<div>
<div class="block columns is-mobile">
<div class="column">
<h2 class="title is-3">
{{ shelf.name }}
<span class="subtitle">
{% include 'snippets/privacy-icons.html' with item=shelf %}
</span>
{% with count=books.paginator.count %}
{% if count %}
<p class="help">
{% blocktrans trimmed count counter=count with formatted_count=count|intcomma %}
{{ formatted_count }} book
{% plural %}
{{ formatted_count }} books
{% endblocktrans %}
{% if books.has_other_pages %}
{% blocktrans trimmed with start=books.start_index end=books.end_index %}
(showing {{ start }}-{{ end }})
{% endblocktrans %}
{% endif %}
</p>
{% endif %}
</p>
{% endif %}
{% endwith %}
</h2>
</div>
{% if is_self and shelf.id %}
<div class="column is-narrow">
{% 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" %}
{% endwith %}
</h2>
</div>
{% if is_self and shelf.id %}
<div class="column is-narrow">
<div class="is-flex">
{% 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" %}
{% 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>
{% if shelf.description %}
<p>{{ shelf.description }}</p>
{% endif %}
</div>
<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 class="block">
@ -167,17 +184,7 @@
</tbody>
</table>
{% else %}
<p>{% trans "This shelf is empty." %}</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 %}
<p><em>{% trans "This shelf is empty." %}</em></p>
{% endif %}
</div>

View file

@ -1,36 +1,36 @@
{% load i18n %}
<form method="post" name="goal" action="{% url 'user-goal' request.user.localname year %}">
{% csrf_token %}
<input type="hidden" name="year" value="{% if goal %}{{ goal.year }}{% else %}{{ year }}{% endif %}">
<input type="hidden" name="user" value="{{ request.user.id }}">
<div class="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>
<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 %}">
<form method="post" name="goal" action="{% url 'user-goal' request.user.localname year %}">
{% csrf_token %}
<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">
<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>
<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 class="column">
<label class="label"><p class="mb-2">{% trans "Goal privacy:" %}</p>
{% include 'snippets/privacy_select.html' with no_label=True current=goal.privacy %}
</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>
<label for="post_status" class="label">
<input type="checkbox" name="post-status" id="post_status" class="checkbox" checked>
{% trans "Post to feed" %}
</label>
<p>
<button type="submit" class="button is-link">{% trans "Set goal" %}</button>
{% if goal %}
{% trans "Cancel" as button_text %}
{% include 'snippets/toggle/close_button.html' with text=button_text controls_text="show_edit_goal" %}
{% endif %}
</p>
</form>
<div class="field">
<button type="submit" class="button is-link">{% trans "Set goal" %}</button>
</div>
</form>
</div>

View file

@ -1,7 +1,7 @@
{% load i18n %}
{% load utilities %}
<div class="select {{ class }}">
{% with 0|uuid as uuid %}
{% firstof uuid 0|uuid as uuid %}
{% if not no_label %}
<label class="is-sr-only" for="privacy_{{ uuid }}">{% trans "Post privacy" %}</label>
{% endif %}
@ -20,6 +20,5 @@
{% trans "Private" %}
</option>
</select>
{% endwith %}
</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 %}
<form name="finish-reading" action="{% url 'reading-status' 'finish' book.id %}" method="post" class="submit-status">
{% csrf_token %}
<input type="hidden" name="id" value="{{ readthrough.id }}">
<input type="hidden" name="reading_status" value="read">
{% endblock %}

View file

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

View file

@ -13,14 +13,18 @@
{% trans "Post to feed" %}
</label>
<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 id="reading_content_{{ local_uuid }}_{{ uuid }}">
<hr aria-hidden="true">
<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>
</div>
{% endwith %}

View file

@ -1,4 +1,4 @@
{% extends 'components/modal.html' %}
{% extends 'snippets/reading_modals/layout.html' %}
{% load i18n %}
{% block modal-title %}
@ -6,42 +6,12 @@
{% endblock %}
{% block modal-form-open %}
<form action="{% url 'edit-readthrough' %}" method="POST" class="submit-status">
{% endblock %}
{% block modal-body %}
<form name="reading-progress" action="{% url 'reading-status-update' book.id %}" method="POST" class="submit-status">
{% csrf_token %}
<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>
<input type="hidden" name="id" value="{{ readthrough.id }}">
{% endblock %}
{% block modal-footer %}
<button class="button is-success" type="submit">{% trans "Save" %}</button>
{% trans "Cancel" as button_text %}
{% include 'snippets/toggle/toggle_button.html' with text=button_text %}
{% block reading-dates %}
<label for="id_progress_{{ readthrough.id }}" class="label">{% trans "Progress:" %}</label>
{% include "snippets/progress_field.html" with progress_required=True %}
{% endblock %}
{% block modal-form-close %}</form>{% endblock %}

View file

@ -13,17 +13,7 @@
<label class="label" for="id_progress_{{ readthrough.id }}">
{% trans "Progress" %}
</label>
<div class="field has-addons">
<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>
{% include "snippets/progress_field.html" %}
{% endif %}
<div class="field">
<label class="label" for="id_finish_date_{{ readthrough.id }}">

View file

@ -6,6 +6,6 @@
{% 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 '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 %}

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/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 %}
{% endif %}

View file

@ -1,5 +1,6 @@
{% extends 'user/layout.html' %}
{% load i18n %}
{% load utilities %}
{% block header %}
<div class="columns is-mobile">
@ -19,20 +20,8 @@
<section class="block">
{% now 'Y' as current_year %}
{% if user == request.user and year|add:0 == current_year|add:0 %}
<div class="block">
<section class="card {% if goal %}is-hidden{% endif %}" id="show_edit_goal">
<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>
{% comparison_bool goal None as visible %}
{% include 'user/goal_form.html' with goal=goal year=year visible=visible controls_text="show_edit_goal" class="block" %}
{% endif %}
{% 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 %}
<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>
{% endblock %}

View file

@ -9,7 +9,7 @@
{% block nullstate %}
<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>
{% 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 %}
<div class="block">
<h2 class="title">
{% include 'user/shelf/books_header.html' %}
{% include 'user/books_header.html' %}
</h2>
<div class="columns is-mobile scroll-x">
{% 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)
def active_shelf(context, book):
"""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 = (
models.ShelfBook.objects.filter(
shelf__user=context["request"].user,
@ -84,8 +87,11 @@ def active_shelf(context, book):
@register.simple_tag(takes_context=False)
def latest_read_through(book, user):
"""the most recent read activity"""
if hasattr(book, "active_readthroughs"):
return book.active_readthroughs[0] if len(book.active_readthroughs) else None
return (
models.ReadThrough.objects.filter(user=user, book=book)
models.ReadThrough.objects.filter(user=user, book=book, is_active=True)
.order_by("-start_date")
.first()
)

View file

@ -36,8 +36,10 @@ def get_title(book, too_short=5):
@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"""
if reverse:
return str1 != str2
return str1 == str2

View file

@ -1,5 +1,6 @@
""" testing models """
from unittest.mock import patch
from django.http import Http404
from django.test import TestCase
from bookwyrm import models
@ -39,14 +40,14 @@ class BaseModel(TestCase):
"""these should be generated"""
self.test_model.id = 1
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):
"""format of remote id when there's a user object"""
self.test_model.user = self.local_user
self.test_model.id = 1
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):
"""this function sets remote ids after creation"""
@ -55,9 +56,7 @@ class BaseModel(TestCase):
instance = models.Work.objects.create(title="work title")
instance.remote_id = None
base_model.set_remote_id(None, instance, True)
self.assertEqual(
instance.remote_id, "https://%s/book/%d" % (DOMAIN, instance.id)
)
self.assertEqual(instance.remote_id, f"https://{DOMAIN}/book/{instance.id}")
# shouldn't set remote_id if it's not created
instance.remote_id = None
@ -70,28 +69,30 @@ class BaseModel(TestCase):
obj = models.Status.objects.create(
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(
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(
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(
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(
content="hi", user=self.remote_user, privacy="direct"
)
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")
def test_object_visible_to_user_follower(self, _):
@ -100,18 +101,19 @@ class BaseModel(TestCase):
obj = models.Status.objects.create(
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(
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(
content="hi", user=self.remote_user, privacy="direct"
)
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")
def test_object_visible_to_user_blocked(self, _):
@ -120,9 +122,11 @@ class BaseModel(TestCase):
obj = models.Status.objects.create(
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(
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 """
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
@ -34,5 +35,8 @@ class DashboardViews(TestCase):
request.user.is_superuser = True
result = view(request)
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)

View file

@ -1,5 +1,7 @@
""" 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
@ -36,7 +38,10 @@ class EmailBlocklistViews(TestCase):
result = view(request)
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)
def test_blocklist_page_post(self):
@ -49,7 +54,10 @@ class EmailBlocklistViews(TestCase):
result = view(request)
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.assertTrue(

View file

@ -1,6 +1,8 @@
""" test for app action functionality """
import json
from unittest.mock import patch
from tidylib import tidy_document
from django.core.files.uploadedfile import SimpleUploadedFile
from django.template.response import TemplateResponse
from django.test import TestCase
@ -46,10 +48,19 @@ class FederationViews(TestCase):
request.user.is_superuser = True
result = view(request)
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)
def test_server_page(self):
def test_instance_page(self):
"""there are so many views, this just makes sure it LOADS"""
server = models.FederatedServer.objects.create(server_name="hi.there.com")
view = views.FederatedServer.as_view()
@ -59,7 +70,10 @@ class FederationViews(TestCase):
result = view(request, server.id)
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)
def test_server_page_block(self):
@ -148,7 +162,10 @@ class FederationViews(TestCase):
result = view(request)
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)
def test_add_view_post_create(self):
@ -169,6 +186,7 @@ class FederationViews(TestCase):
self.assertEqual(server.application_type, "coolsoft")
self.assertEqual(server.status, "blocked")
# pylint: disable=consider-using-with
def test_import_blocklist(self):
"""load a json file with a list of servers to block"""
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
{"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()
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 """
import json
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
@ -42,7 +44,16 @@ class ReportViews(TestCase):
result = view(request)
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)
def test_reports_page_with_data(self):
@ -55,7 +66,16 @@ class ReportViews(TestCase):
result = view(request)
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)
def test_report_page(self):
@ -69,7 +89,10 @@ class ReportViews(TestCase):
result = view(request, report.id)
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)
def test_report_comment(self):

View file

@ -1,5 +1,7 @@
""" test for app action functionality """
from unittest.mock import patch
from tidylib import tidy_document
from django.contrib.auth.models import Group
from django.template.response import TemplateResponse
from django.test import TestCase
@ -34,7 +36,10 @@ class UserAdminViews(TestCase):
request.user.is_superuser = True
result = view(request)
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)
def test_user_admin_page(self):
@ -47,7 +52,10 @@ class UserAdminViews(TestCase):
result = view(request, self.local_user.id)
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)
@patch("bookwyrm.suggested_users.rerank_suggestions_task.delay")
@ -69,7 +77,10 @@ class UserAdminViews(TestCase):
result = view(request, self.local_user.id)
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(
list(self.local_user.groups.values_list("name", flat=True)), ["editor"]

View file

@ -3,6 +3,7 @@ import json
import pathlib
from unittest.mock import patch
from django.core.exceptions import PermissionDenied
from django.http import HttpResponseNotAllowed, HttpResponseNotFound
from django.test import TestCase, Client
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/)",
)
self.assertFalse(views.inbox.is_blocked_user_agent(request))
self.assertIsNone(views.inbox.raise_is_blocked_user_agent(request))
models.FederatedServer.objects.create(
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):
"""check for blocked servers"""
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(
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")
def test_create_by_deactivated_user(self, _):
@ -157,11 +160,11 @@ class Inbox(TestCase):
activity = self.create_json
activity["actor"] = self.remote_user.remote_id
activity["object"] = status_data
activity["type"] = "Create"
with patch("bookwyrm.views.inbox.has_valid_signature") as mock_valid:
mock_valid.return_value = True
result = self.client.post(
"/inbox", json.dumps(activity), content_type="application/json"
)
self.assertEqual(result.status_code, 403)
response = self.client.post(
"/inbox",
json.dumps(activity),
content_type="application/json",
)
self.assertEqual(response.status_code, 403)

View file

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

View file

@ -1,5 +1,7 @@
""" 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
@ -44,7 +46,10 @@ class BlockViews(TestCase):
request.user = self.local_user
result = view(request)
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)
def test_block_post(self, _):
@ -75,6 +80,6 @@ class BlockViews(TestCase):
request.user = self.local_user
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())

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 """
import json
import pathlib
from unittest.mock import patch
from PIL import Image
from tidylib import tidy_document
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.uploadedfile import SimpleUploadedFile
from django.template.response import TemplateResponse
@ -59,7 +58,10 @@ class EditUserViews(TestCase):
request.user = self.local_user
result = view(request)
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)
def test_edit_user(self, _):
@ -91,8 +93,9 @@ class EditUserViews(TestCase):
form.data["default_post_privacy"] = "public"
form.data["preferred_timezone"] = "UTC"
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(
image_file, open(image_file, "rb").read(), content_type="image/jpeg"
)
@ -113,50 +116,11 @@ class EditUserViews(TestCase):
def test_crop_avatar(self, _):
"""reduce that image size"""
image_file = pathlib.Path(__file__).parent.joinpath(
"../../static/images/no_cover.jpg"
"../../../static/images/no_cover.jpg"
)
image = Image.open(image_file)
result = views.edit_user.crop_avatar(image)
result = views.preferences.edit_user.crop_avatar(image)
self.assertIsInstance(result, ContentFile)
image_result = Image.open(result)
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.contenttypes.models import ContentType
from django.core.files.uploadedfile import SimpleUploadedFile
from django.http import Http404
from django.template.response import TemplateResponse
from django.test import TestCase
from django.test.client import RequestFactory
@ -133,8 +134,8 @@ class BookViews(TestCase):
request.user = self.local_user
with patch("bookwyrm.views.books.is_api_request") as is_api:
is_api.return_value = False
result = view(request, 0)
self.assertEqual(result.status_code, 404)
with self.assertRaises(Http404):
view(request, 0)
def test_book_page_work_id(self):
"""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(), 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):
"""add a cover via file upload"""
self.assertFalse(self.book.cover)
@ -310,21 +351,8 @@ class BookViews(TestCase):
def test_upload_cover_url(self):
"""add a cover via url"""
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.data["cover-url"] = "http://example.com"
form.data["cover-url"] = self._setup_cover_url()
request = self.factory.post("", form.data)
request.user = self.local_user

View file

@ -1,5 +1,6 @@
""" test for app action functionality """
from unittest.mock import patch
from tidylib import tidy_document
from django.contrib.auth.models import AnonymousUser
from django.template.response import TemplateResponse
@ -51,7 +52,16 @@ class DirectoryViews(TestCase):
result = view(request)
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)
def test_directory_page_empty(self):
@ -62,7 +72,10 @@ class DirectoryViews(TestCase):
result = view(request)
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)
def test_directory_page_logged_out(self):

View file

@ -5,6 +5,7 @@ import pathlib
from PIL import Image
from django.core.files.base import ContentFile
from django.http import Http404
from django.template.response import TemplateResponse
from django.test import TestCase
from django.test.client import RequestFactory
@ -81,9 +82,8 @@ class FeedViews(TestCase):
request.user = self.local_user
with patch("bookwyrm.views.feed.is_api_request") as is_api:
is_api.return_value = False
result = view(request, "mouse", 12345)
self.assertEqual(result.status_code, 404)
with self.assertRaises(Http404):
view(request, "mouse", 12345)
def test_status_page_not_found_wrong_user(self, *_):
"""there are so many views, this just makes sure it LOADS"""
@ -102,9 +102,8 @@ class FeedViews(TestCase):
request.user = self.local_user
with patch("bookwyrm.views.feed.is_api_request") as is_api:
is_api.return_value = False
result = view(request, "mouse", status.id)
self.assertEqual(result.status_code, 404)
with self.assertRaises(Http404):
view(request, "mouse", status.id)
def test_status_page_with_image(self, *_):
"""there are so many views, this just makes sure it LOADS"""

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