Merge branch 'main' into form-conflict

This commit is contained in:
Mouse Reeve
2022-03-16 16:51:57 -07:00
51 changed files with 4388 additions and 2813 deletions

View File

@ -0,0 +1,12 @@
""" make forms available to the app """
# site admin
from .admin import *
from .author import *
from .books import *
from .edit_user import *
from .forms import *
from .groups import *
from .landing import *
from .links import *
from .lists import *
from .status import *

141
bookwyrm/forms/admin.py Normal file
View File

@ -0,0 +1,141 @@
""" using django model forms """
import datetime
from django import forms
from django.forms import widgets
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django_celery_beat.models import IntervalSchedule
from bookwyrm import models
from .custom_form import CustomForm
# pylint: disable=missing-class-docstring
class ExpiryWidget(widgets.Select):
def value_from_datadict(self, data, files, name):
"""human-readable exiration time buckets"""
selected_string = super().value_from_datadict(data, files, name)
if selected_string == "day":
interval = datetime.timedelta(days=1)
elif selected_string == "week":
interval = datetime.timedelta(days=7)
elif selected_string == "month":
interval = datetime.timedelta(days=31) # Close enough?
elif selected_string == "forever":
return None
else:
return selected_string # This will raise
return timezone.now() + interval
class CreateInviteForm(CustomForm):
class Meta:
model = models.SiteInvite
exclude = ["code", "user", "times_used", "invitees"]
widgets = {
"expiry": ExpiryWidget(
choices=[
("day", _("One Day")),
("week", _("One Week")),
("month", _("One Month")),
("forever", _("Does Not Expire")),
]
),
"use_limit": widgets.Select(
choices=[(i, _(f"{i} uses")) for i in [1, 5, 10, 25, 50, 100]]
+ [(None, _("Unlimited"))]
),
}
class SiteForm(CustomForm):
class Meta:
model = models.SiteSettings
exclude = ["admin_code", "install_mode"]
widgets = {
"instance_short_description": forms.TextInput(
attrs={"aria-describedby": "desc_instance_short_description"}
),
"require_confirm_email": forms.CheckboxInput(
attrs={"aria-describedby": "desc_require_confirm_email"}
),
"invite_request_text": forms.Textarea(
attrs={"aria-describedby": "desc_invite_request_text"}
),
}
class ThemeForm(CustomForm):
class Meta:
model = models.Theme
fields = ["name", "path"]
widgets = {
"name": forms.TextInput(attrs={"aria-describedby": "desc_name"}),
"path": forms.TextInput(
attrs={
"aria-describedby": "desc_path",
"placeholder": "css/themes/theme-name.scss",
}
),
}
class AnnouncementForm(CustomForm):
class Meta:
model = models.Announcement
exclude = ["remote_id"]
widgets = {
"preview": forms.TextInput(attrs={"aria-describedby": "desc_preview"}),
"content": forms.Textarea(attrs={"aria-describedby": "desc_content"}),
"event_date": forms.SelectDateWidget(
attrs={"aria-describedby": "desc_event_date"}
),
"start_date": forms.SelectDateWidget(
attrs={"aria-describedby": "desc_start_date"}
),
"end_date": forms.SelectDateWidget(
attrs={"aria-describedby": "desc_end_date"}
),
"active": forms.CheckboxInput(attrs={"aria-describedby": "desc_active"}),
}
class EmailBlocklistForm(CustomForm):
class Meta:
model = models.EmailBlocklist
fields = ["domain"]
widgets = {
"avatar": forms.TextInput(attrs={"aria-describedby": "desc_domain"}),
}
class IPBlocklistForm(CustomForm):
class Meta:
model = models.IPBlocklist
fields = ["address"]
class ServerForm(CustomForm):
class Meta:
model = models.FederatedServer
exclude = ["remote_id"]
class AutoModRuleForm(CustomForm):
class Meta:
model = models.AutoMod
fields = ["string_match", "flag_users", "flag_statuses", "created_by"]
class IntervalScheduleForm(CustomForm):
class Meta:
model = IntervalSchedule
fields = ["every", "period"]
widgets = {
"every": forms.NumberInput(attrs={"aria-describedby": "desc_every"}),
"period": forms.Select(attrs={"aria-describedby": "desc_period"}),
}

47
bookwyrm/forms/author.py Normal file
View File

@ -0,0 +1,47 @@
""" using django model forms """
from django import forms
from bookwyrm import models
from .custom_form import CustomForm
# pylint: disable=missing-class-docstring
class AuthorForm(CustomForm):
class Meta:
model = models.Author
fields = [
"last_edited_by",
"name",
"aliases",
"bio",
"wikipedia_link",
"born",
"died",
"openlibrary_key",
"inventaire_id",
"librarything_key",
"goodreads_key",
"isni",
]
widgets = {
"name": forms.TextInput(attrs={"aria-describedby": "desc_name"}),
"aliases": forms.TextInput(attrs={"aria-describedby": "desc_aliases"}),
"bio": forms.Textarea(attrs={"aria-describedby": "desc_bio"}),
"wikipedia_link": forms.TextInput(
attrs={"aria-describedby": "desc_wikipedia_link"}
),
"born": forms.SelectDateWidget(attrs={"aria-describedby": "desc_born"}),
"died": forms.SelectDateWidget(attrs={"aria-describedby": "desc_died"}),
"oepnlibrary_key": forms.TextInput(
attrs={"aria-describedby": "desc_oepnlibrary_key"}
),
"inventaire_id": forms.TextInput(
attrs={"aria-describedby": "desc_inventaire_id"}
),
"librarything_key": forms.TextInput(
attrs={"aria-describedby": "desc_librarything_key"}
),
"goodreads_key": forms.TextInput(
attrs={"aria-describedby": "desc_goodreads_key"}
),
}

87
bookwyrm/forms/books.py Normal file
View File

@ -0,0 +1,87 @@
""" using django model forms """
from django import forms
from bookwyrm import models
from bookwyrm.models.fields import ClearableFileInputWithWarning
from .custom_form import CustomForm
# pylint: disable=missing-class-docstring
class CoverForm(CustomForm):
class Meta:
model = models.Book
fields = ["cover"]
help_texts = {f: None for f in fields}
class ArrayWidget(forms.widgets.TextInput):
# pylint: disable=unused-argument
# pylint: disable=no-self-use
def value_from_datadict(self, data, files, name):
"""get all values for this name"""
return [i for i in data.getlist(name) if i]
class EditionForm(CustomForm):
class Meta:
model = models.Edition
exclude = [
"remote_id",
"origin_id",
"created_date",
"updated_date",
"edition_rank",
"authors",
"parent_work",
"shelves",
"connector",
"search_vector",
"links",
"file_links",
]
widgets = {
"title": forms.TextInput(attrs={"aria-describedby": "desc_title"}),
"subtitle": forms.TextInput(attrs={"aria-describedby": "desc_subtitle"}),
"description": forms.Textarea(
attrs={"aria-describedby": "desc_description"}
),
"series": forms.TextInput(attrs={"aria-describedby": "desc_series"}),
"series_number": forms.TextInput(
attrs={"aria-describedby": "desc_series_number"}
),
"subjects": ArrayWidget(),
"languages": forms.TextInput(
attrs={"aria-describedby": "desc_languages_help desc_languages"}
),
"publishers": forms.TextInput(
attrs={"aria-describedby": "desc_publishers_help desc_publishers"}
),
"first_published_date": forms.SelectDateWidget(
attrs={"aria-describedby": "desc_first_published_date"}
),
"published_date": forms.SelectDateWidget(
attrs={"aria-describedby": "desc_published_date"}
),
"cover": ClearableFileInputWithWarning(
attrs={"aria-describedby": "desc_cover"}
),
"physical_format": forms.Select(
attrs={"aria-describedby": "desc_physical_format"}
),
"physical_format_detail": forms.TextInput(
attrs={"aria-describedby": "desc_physical_format_detail"}
),
"pages": forms.NumberInput(attrs={"aria-describedby": "desc_pages"}),
"isbn_13": forms.TextInput(attrs={"aria-describedby": "desc_isbn_13"}),
"isbn_10": forms.TextInput(attrs={"aria-describedby": "desc_isbn_10"}),
"openlibrary_key": forms.TextInput(
attrs={"aria-describedby": "desc_openlibrary_key"}
),
"inventaire_id": forms.TextInput(
attrs={"aria-describedby": "desc_inventaire_id"}
),
"oclc_number": forms.TextInput(
attrs={"aria-describedby": "desc_oclc_number"}
),
"ASIN": forms.TextInput(attrs={"aria-describedby": "desc_ASIN"}),
}

View File

@ -0,0 +1,26 @@
""" Overrides django's default form class """
from collections import defaultdict
from django.forms import ModelForm
from django.forms.widgets import Textarea
class CustomForm(ModelForm):
"""add css classes to the forms"""
def __init__(self, *args, **kwargs):
css_classes = defaultdict(lambda: "")
css_classes["text"] = "input"
css_classes["password"] = "input"
css_classes["email"] = "input"
css_classes["number"] = "input"
css_classes["checkbox"] = "checkbox"
css_classes["textarea"] = "textarea"
# pylint: disable=super-with-arguments
super(CustomForm, self).__init__(*args, **kwargs)
for visible in self.visible_fields():
if hasattr(visible.field.widget, "input_type"):
input_type = visible.field.widget.input_type
if isinstance(visible.field.widget, Textarea):
input_type = "textarea"
visible.field.widget.attrs["rows"] = 5
visible.field.widget.attrs["class"] = css_classes[input_type]

View File

@ -0,0 +1,68 @@
""" using django model forms """
from django import forms
from bookwyrm import models
from bookwyrm.models.fields import ClearableFileInputWithWarning
from .custom_form import CustomForm
# pylint: disable=missing-class-docstring
class EditUserForm(CustomForm):
class Meta:
model = models.User
fields = [
"avatar",
"name",
"email",
"summary",
"show_goal",
"show_suggested_users",
"manually_approves_followers",
"default_post_privacy",
"discoverable",
"hide_follows",
"preferred_timezone",
"preferred_language",
"theme",
]
help_texts = {f: None for f in fields}
widgets = {
"avatar": ClearableFileInputWithWarning(
attrs={"aria-describedby": "desc_avatar"}
),
"name": forms.TextInput(attrs={"aria-describedby": "desc_name"}),
"summary": forms.Textarea(attrs={"aria-describedby": "desc_summary"}),
"email": forms.EmailInput(attrs={"aria-describedby": "desc_email"}),
"discoverable": forms.CheckboxInput(
attrs={"aria-describedby": "desc_discoverable"}
),
}
class LimitedEditUserForm(CustomForm):
class Meta:
model = models.User
fields = [
"avatar",
"name",
"summary",
"manually_approves_followers",
"discoverable",
]
help_texts = {f: None for f in fields}
widgets = {
"avatar": ClearableFileInputWithWarning(
attrs={"aria-describedby": "desc_avatar"}
),
"name": forms.TextInput(attrs={"aria-describedby": "desc_name"}),
"summary": forms.Textarea(attrs={"aria-describedby": "desc_summary"}),
"discoverable": forms.CheckboxInput(
attrs={"aria-describedby": "desc_discoverable"}
),
}
class DeleteUserForm(CustomForm):
class Meta:
model = models.User
fields = ["password"]

59
bookwyrm/forms/forms.py Normal file
View File

@ -0,0 +1,59 @@
""" using django model forms """
from django import forms
from django.forms import widgets
from django.utils.translation import gettext_lazy as _
from bookwyrm import models
from bookwyrm.models.user import FeedFilterChoices
from .custom_form import CustomForm
# pylint: disable=missing-class-docstring
class FeedStatusTypesForm(CustomForm):
class Meta:
model = models.User
fields = ["feed_status_types"]
help_texts = {f: None for f in fields}
widgets = {
"feed_status_types": widgets.CheckboxSelectMultiple(
choices=FeedFilterChoices,
),
}
class ImportForm(forms.Form):
csv_file = forms.FileField()
class ShelfForm(CustomForm):
class Meta:
model = models.Shelf
fields = ["user", "name", "privacy", "description"]
class GoalForm(CustomForm):
class Meta:
model = models.AnnualGoal
fields = ["user", "year", "goal", "privacy"]
class ReportForm(CustomForm):
class Meta:
model = models.Report
fields = ["user", "reporter", "status", "links", "note"]
class ReadThroughForm(CustomForm):
def clean(self):
"""make sure the email isn't in use by a registered user"""
cleaned_data = super().clean()
start_date = cleaned_data.get("start_date")
finish_date = cleaned_data.get("finish_date")
if start_date and finish_date and start_date > finish_date:
self.add_error(
"finish_date", _("Reading finish date cannot be before start date.")
)
class Meta:
model = models.ReadThrough
fields = ["user", "book", "start_date", "finish_date"]

16
bookwyrm/forms/groups.py Normal file
View File

@ -0,0 +1,16 @@
""" using django model forms """
from bookwyrm import models
from .custom_form import CustomForm
# pylint: disable=missing-class-docstring
class UserGroupForm(CustomForm):
class Meta:
model = models.User
fields = ["groups"]
class GroupForm(CustomForm):
class Meta:
model = models.Group
fields = ["user", "privacy", "name", "description"]

45
bookwyrm/forms/landing.py Normal file
View File

@ -0,0 +1,45 @@
""" Forms for the landing pages """
from django.forms import PasswordInput
from django.utils.translation import gettext_lazy as _
from bookwyrm import models
from .custom_form import CustomForm
# pylint: disable=missing-class-docstring
class LoginForm(CustomForm):
class Meta:
model = models.User
fields = ["localname", "password"]
help_texts = {f: None for f in fields}
widgets = {
"password": PasswordInput(),
}
class RegisterForm(CustomForm):
class Meta:
model = models.User
fields = ["localname", "email", "password"]
help_texts = {f: None for f in fields}
widgets = {"password": PasswordInput()}
def clean(self):
"""Check if the username is taken"""
cleaned_data = super().clean()
localname = cleaned_data.get("localname").strip()
if models.User.objects.filter(localname=localname).first():
self.add_error("localname", _("User with this username already exists"))
class InviteRequestForm(CustomForm):
def clean(self):
"""make sure the email isn't in use by a registered user"""
cleaned_data = super().clean()
email = cleaned_data.get("email")
if email and models.User.objects.filter(email=email).exists():
self.add_error("email", _("A user with this email already exists."))
class Meta:
model = models.InviteRequest
fields = ["email", "answer"]

48
bookwyrm/forms/links.py Normal file
View File

@ -0,0 +1,48 @@
""" using django model forms """
from urllib.parse import urlparse
from django.utils.translation import gettext_lazy as _
from bookwyrm import models
from .custom_form import CustomForm
# pylint: disable=missing-class-docstring
class LinkDomainForm(CustomForm):
class Meta:
model = models.LinkDomain
fields = ["name"]
class FileLinkForm(CustomForm):
class Meta:
model = models.FileLink
fields = ["url", "filetype", "availability", "book", "added_by"]
def clean(self):
"""make sure the domain isn't blocked or pending"""
cleaned_data = super().clean()
url = cleaned_data.get("url")
filetype = cleaned_data.get("filetype")
book = cleaned_data.get("book")
domain = urlparse(url).netloc
if models.LinkDomain.objects.filter(domain=domain).exists():
status = models.LinkDomain.objects.get(domain=domain).status
if status == "blocked":
# pylint: disable=line-too-long
self.add_error(
"url",
_(
"This domain is blocked. Please contact your administrator if you think this is an error."
),
)
elif models.FileLink.objects.filter(
url=url, book=book, filetype=filetype
).exists():
# pylint: disable=line-too-long
self.add_error(
"url",
_(
"This link with file type has already been added for this book. If it is not visible, the domain is still pending."
),
)

37
bookwyrm/forms/lists.py Normal file
View File

@ -0,0 +1,37 @@
""" using django model forms """
from django import forms
from django.forms import ChoiceField
from django.utils.translation import gettext_lazy as _
from bookwyrm import models
from .custom_form import CustomForm
# pylint: disable=missing-class-docstring
class ListForm(CustomForm):
class Meta:
model = models.List
fields = ["user", "name", "description", "curation", "privacy", "group"]
class ListItemForm(CustomForm):
class Meta:
model = models.ListItem
fields = ["user", "book", "book_list", "notes"]
class SortListForm(forms.Form):
sort_by = ChoiceField(
choices=(
("order", _("List Order")),
("title", _("Book Title")),
("rating", _("Rating")),
),
label=_("Sort By"),
)
direction = ChoiceField(
choices=(
("ascending", _("Ascending")),
("descending", _("Descending")),
),
)

82
bookwyrm/forms/status.py Normal file
View File

@ -0,0 +1,82 @@
""" using django model forms """
from bookwyrm import models
from .custom_form import CustomForm
# pylint: disable=missing-class-docstring
class RatingForm(CustomForm):
class Meta:
model = models.ReviewRating
fields = ["user", "book", "rating", "privacy"]
class ReviewForm(CustomForm):
class Meta:
model = models.Review
fields = [
"user",
"book",
"name",
"content",
"rating",
"content_warning",
"sensitive",
"privacy",
]
class CommentForm(CustomForm):
class Meta:
model = models.Comment
fields = [
"user",
"book",
"content",
"content_warning",
"sensitive",
"privacy",
"progress",
"progress_mode",
"reading_status",
]
class QuotationForm(CustomForm):
class Meta:
model = models.Quotation
fields = [
"user",
"book",
"quote",
"content",
"content_warning",
"sensitive",
"privacy",
"position",
"position_mode",
]
class ReplyForm(CustomForm):
class Meta:
model = models.Status
fields = [
"user",
"content",
"content_warning",
"sensitive",
"reply_parent",
"privacy",
]
class StatusForm(CustomForm):
class Meta:
model = models.Status
fields = ["user", "content", "content_warning", "sensitive", "privacy"]
class DirectForm(CustomForm):
class Meta:
model = models.Status
fields = ["user", "content", "content_warning", "sensitive", "privacy"]

View File

@ -0,0 +1,54 @@
""" Get your admin code to allow install """
from django.core.management.base import BaseCommand
from bookwyrm import models
from bookwyrm.settings import VERSION
# pylint: disable=no-self-use
class Command(BaseCommand):
"""command-line options"""
help = "What version is this?"
def add_arguments(self, parser):
"""specify which function to run"""
parser.add_argument(
"--current",
action="store_true",
help="Version stored in database",
)
parser.add_argument(
"--target",
action="store_true",
help="Version stored in settings",
)
parser.add_argument(
"--update",
action="store_true",
help="Update database version",
)
# pylint: disable=unused-argument
def handle(self, *args, **options):
"""execute init"""
site = models.SiteSettings.objects.get()
current = site.version or "0.0.1"
target = VERSION
if options.get("current"):
print(current)
return
if options.get("target"):
print(target)
return
if options.get("update"):
site.version = target
site.save()
return
if current != target:
print(f"{current}/{target}")
else:
print(current)

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.12 on 2022-03-16 18:10
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bookwyrm", "0144_alter_announcement_display_type"),
]
operations = [
migrations.AddField(
model_name="sitesettings",
name="version",
field=models.CharField(blank=True, max_length=10, null=True),
),
]

View File

@ -27,6 +27,7 @@ class SiteSettings(models.Model):
default_theme = models.ForeignKey(
"Theme", null=True, blank=True, on_delete=models.SET_NULL
)
version = models.CharField(null=True, blank=True, max_length=10)
# admin setup options
install_mode = models.BooleanField(default=False)

View File

@ -11,7 +11,7 @@ from django.utils.translation import gettext_lazy as _
env = Env()
env.read_env()
DOMAIN = env("DOMAIN")
VERSION = "0.3.3"
VERSION = "0.3.4"
RELEASE_API = env(
"RELEASE_API",
@ -90,6 +90,7 @@ INSTALLED_APPS = [
"sass_processor",
"bookwyrm",
"celery",
"django_celery_beat",
"imagekit",
"storages",
]

View File

@ -1,6 +1,19 @@
(function () {
"use strict";
/**
* Remoev input field
*
* @param {event} the button click event
*/
function removeInput(event) {
const trigger = event.currentTarget;
const input_id = trigger.dataset.remove;
const input = document.getElementById(input_id);
input.remove();
}
/**
* Duplicate an input field
*
@ -29,4 +42,8 @@
document
.querySelectorAll("[data-duplicate]")
.forEach((node) => node.addEventListener("click", duplicateInput));
document
.querySelectorAll("[data-remove]")
.forEach((node) => node.addEventListener("click", removeInput));
})();

View File

@ -22,7 +22,7 @@
{% trans "Title:" %}
</label>
<input type="text" name="title" value="{{ form.title.value|default:'' }}" maxlength="255" class="input" required="" id="id_title" aria-describedby="desc_title">
{% include 'snippets/form_errors.html' with errors_list=form.title.errors id="desc_title" %}
</div>
@ -31,7 +31,7 @@
{% trans "Subtitle:" %}
</label>
<input type="text" name="subtitle" value="{{ form.subtitle.value|default:'' }}" maxlength="255" class="input" id="id_subtitle" aria-describedby="desc_subtitle">
{% include 'snippets/form_errors.html' with errors_list=form.subtitle.errors id="desc_subtitle" %}
</div>
@ -40,7 +40,7 @@
{% trans "Description:" %}
</label>
{{ form.description }}
{% include 'snippets/form_errors.html' with errors_list=form.description.errors id="desc_description" %}
</div>
@ -51,7 +51,7 @@
{% trans "Series:" %}
</label>
<input type="text" class="input" name="series" id="id_series" value="{{ form.series.value|default:'' }}" aria-describedby="desc_series">
{% include 'snippets/form_errors.html' with errors_list=form.series.errors id="desc_series" %}
</div>
</div>
@ -61,7 +61,7 @@
{% trans "Series number:" %}
</label>
{{ form.series_number }}
{% include 'snippets/form_errors.html' with errors_list=form.series_number.errors id="desc_series_number" %}
</div>
</div>
@ -75,21 +75,60 @@
<span class="help" id="desc_languages_help">
{% trans "Separate multiple values with commas." %}
</span>
{% include 'snippets/form_errors.html' with errors_list=form.languages.errors id="desc_languages" %}
</div>
<div class="field">
<label class="label" for="id_subjects">
<div>
<label class="label" for="id_add_subjects">
{% trans "Subjects:" %}
</label>
{{ form.subjects }}
<span class="help" id="desc_subjects_help">
{% trans "Separate multiple values with commas." %}
</span>
{% for subject in book.subjects %}
<label class="label is-sr-only" for="id_add_subject={% if not forloop.first %}-{{forloop.counter}}{% endif %}">
{% trans "Add subject" %}
</label>
<div class="field has-addons" id="subject_field_wrapper_{{ forloop.counter }}">
<div class="control is-expanded">
<input
id="id_add_subject-{{ forloop.counter }}"
type="text"
name="subjects"
value="{{ subject }}"
class="input"
>
</div>
<div class="control">
<button
class="button is-danger is-light"
type="button"
data-remove="subject_field_wrapper_{{ forloop.counter }}"
>
{% trans "Remove subject" as text %}
<span class="icon icon-x" title="{{ text }}">
<span class="is-sr-only">{{ text }}</span>
</span>
</button>
</div>
</div>
{% endfor %}
<input
class="input"
type="text"
name="subjects"
id="id_add_subject"
value="{{ subject }}"
{% if confirm_mode %}readonly{% endif %}
>
{% include 'snippets/form_errors.html' with errors_list=form.subjects.errors id="desc_subjects" %}
</div>
<span class="help">
<button class="button is-small" type="button" data-duplicate="id_add_subject" id="another_subject_field">
<span class="icon icon-plus" aria-hidden="true"></span>
<span>{% trans "Add Another Subject" %}</span>
</button>
</span>
</div>
</section>
@ -106,7 +145,7 @@
<span class="help" id="desc_publishers_help">
{% trans "Separate multiple values with commas." %}
</span>
{% include 'snippets/form_errors.html' with errors_list=form.publishers.errors id="desc_publishers" %}
</div>
@ -115,7 +154,7 @@
{% trans "First published date:" %}
</label>
<input type="date" name="first_published_date" class="input" id="id_first_published_date"{% if form.first_published_date.value %} value="{{ form.first_published_date.value|date:'Y-m-d' }}"{% endif %} aria-describedby="desc_first_published_date">
{% include 'snippets/form_errors.html' with errors_list=form.first_published_date.errors id="desc_first_published_date" %}
</div>
@ -124,7 +163,7 @@
{% trans "Published date:" %}
</label>
<input type="date" name="published_date" class="input" id="id_published_date"{% if form.published_date.value %} value="{{ form.published_date.value|date:'Y-m-d'}}"{% endif %} aria-describedby="desc_published_date">
{% include 'snippets/form_errors.html' with errors_list=form.published_date.errors id="desc_published_date" %}
</div>
</div>
@ -162,7 +201,12 @@
<input class="input" type="text" name="add_author" id="id_add_author" placeholder="{% trans 'Jane Doe' %}" value="{{ author }}" {% if confirm_mode %}readonly{% endif %}>
{% endfor %}
</div>
<span class="help"><button class="button is-small" type="button" data-duplicate="id_add_author" id="another_author_field">{% trans "Add Another Author" %}</button></span>
<span class="help">
<button class="button is-small" type="button" data-duplicate="id_add_author" id="another_author_field">
<span class="icon icon-plus" aria-hidden="true"></span>
<span>{% trans "Add Another Author" %}</span>
</button>
</span>
</div>
</section>
</div>
@ -193,7 +237,7 @@
</label>
<input class="input" name="cover-url" id="id_cover_url" type="url" value="{{ cover_url|default:'' }}" aria-describedby="desc_cover">
</div>
{% include 'snippets/form_errors.html' with errors_list=form.cover.errors id="desc_cover" %}
</div>
</div>
@ -214,7 +258,7 @@
<div class="select">
{{ form.physical_format }}
</div>
{% include 'snippets/form_errors.html' with errors_list=form.physical_format.errors id="desc_physical_format" %}
</div>
</div>
@ -224,7 +268,7 @@
{% trans "Format details:" %}
</label>
{{ form.physical_format_detail }}
{% include 'snippets/form_errors.html' with errors_list=form.physical_format_detail.errors id="desc_physical_format_detail" %}
</div>
</div>
@ -235,7 +279,7 @@
{% trans "Pages:" %}
</label>
{{ form.pages }}
{% include 'snippets/form_errors.html' with errors_list=form.pages.errors id="desc_pages" %}
</div>
</div>
@ -251,7 +295,7 @@
{% trans "ISBN 13:" %}
</label>
{{ form.isbn_13 }}
{% include 'snippets/form_errors.html' with errors_list=form.isbn_13.errors id="desc_isbn_13" %}
</div>
@ -260,7 +304,7 @@
{% trans "ISBN 10:" %}
</label>
{{ form.isbn_10 }}
{% include 'snippets/form_errors.html' with errors_list=form.isbn_10.errors id="desc_isbn_10" %}
</div>
@ -269,7 +313,7 @@
{% trans "Openlibrary ID:" %}
</label>
{{ form.openlibrary_key }}
{% include 'snippets/form_errors.html' with errors_list=form.openlibrary_key.errors id="desc_openlibrary_key" %}
</div>
@ -278,7 +322,7 @@
{% trans "Inventaire ID:" %}
</label>
{{ form.inventaire_id }}
{% include 'snippets/form_errors.html' with errors_list=form.inventaire_id.errors id="desc_inventaire_id" %}
</div>
@ -287,7 +331,7 @@
{% trans "OCLC Number:" %}
</label>
{{ form.oclc_number }}
{% include 'snippets/form_errors.html' with errors_list=form.oclc_number.errors id="desc_oclc_number" %}
</div>
@ -296,7 +340,7 @@
{% trans "ASIN:" %}
</label>
{{ form.asin }}
{% include 'snippets/form_errors.html' with errors_list=form.ASIN.errors id="desc_ASIN" %}
</div>
</div>

View File

@ -8,7 +8,7 @@
<header class="block">
<h1 class="title">
{% blocktrans with title=book|book_title %}
{% blocktrans trimmed with title=book|book_title %}
Links for "<em>{{ title }}</em>"
{% endblocktrans %}
</h1>

View File

@ -1,5 +1,6 @@
{% extends 'settings/layout.html' %}
{% load i18n %}
{% load humanize %}
{% load utilities %}
{% block title %}
@ -16,12 +17,81 @@
<p>
{% trans "Auto-moderation rules will create reports for any local user or status with fields matching the provided string." %}
{% trans "Users or statuses that have already been reported (regardless of whether the report was resolved) will not be flagged." %}
{% trans "At this time, reports are <em>not</em> being generated automatically, and you must manually trigger a scan." %}
</p>
<form name="run-scan" method="POST" action="{% url 'settings-automod-run' %}">
</div>
<div class="box block">
{% if task %}
<dl class="block">
<dt class="is-pulled-left mr-5 has-text-weight-bold">
{% trans "Schedule:" %}
</dt>
<dd>
{{ task.schedule }}
</dd>
<dt class="is-pulled-left mr-5 has-text-weight-bold">
{% trans "Last run:" %}
</dt>
<dd>
{{ task.last_run_at|naturaltime }}
</dd>
<dt class="is-pulled-left mr-5 has-text-weight-bold">
{% trans "Total run count:" %}
</dt>
<dd>
{{ task.total_run_count }}
</dd>
<dt class="is-pulled-left mr-5 has-text-weight-bold">
{% trans "Enabled:" %}
</dt>
<dd>
<span class="tag {% if task.enabled %}is-success{% else %}is-danger{% endif %}">
{{ task.enabled|yesno }}
</span>
</dd>
</dl>
<div class="is-flex is-justify-content-space-between block">
<form name="unschedule-scan" method="POST" action="{% url 'settings-automod-unschedule' task.id %}">
{% csrf_token %}
<button class="button is-danger">{% trans "Delete schedule" %}</button>
</form>
<form name="run-scan" method="POST" action="{% url 'settings-automod-run' %}">
{% csrf_token %}
<button class="button">{% trans "Run now" %}</button>
<p class="help">{% trans "Last run date will not be updated" %}</p>
</form>
</div>
{% else %}
<h2 class="title is-4">{% trans "Schedule scan" %}</h2>
<form name="schedule-scan" method="POST" action="{% url 'settings-automod-schedule' %}">
{% csrf_token %}
<button class="button is-warning">{% trans "Run scan" %}</button>
<div class="field">
<label class="label" for="id_every">
{{ task_form.every.label }}
</label>
{{ task_form.every }}
<p class="help" id="desc_every">
{{ task_form.every.help_text }}
</p>
</div>
<div class="field">
<label class="label" for="id_period">
{{ task_form.period.label }}
</label>
<div class="select">
{{ task_form.period }}
</div>
<p class="help" id="desc_period">
{{ task_form.period.help_text }}
</p>
</div>
<button class="button is-warning">{% trans "Schedule scan" %}</button>
</form>
{% endif %}
</div>
{% if success %}

View File

@ -4,7 +4,7 @@
{% with goal.progress as progress %}
<p>
{% if progress.percent >= 100 %}
{% trans "Success!" %}
{% trans "Success!" context "Goal successfully completed" %}
{% elif progress.percent %}
{% blocktrans with percent=progress.percent %}{{ percent }}% complete!{% endblocktrans %}
{% endif %}

View File

@ -233,11 +233,23 @@ urlpatterns = [
# auto-moderation rules
re_path(r"^settings/automod/?$", views.AutoMod.as_view(), name="settings-automod"),
re_path(
r"^settings/automod/(?P<rule_id>\d+)/delete?$",
r"^settings/automod/(?P<rule_id>\d+)/delete/?$",
views.automod_delete,
name="settings-automod-delete",
),
re_path(r"^settings/automod/run?$", views.run_automod, name="settings-automod-run"),
re_path(
r"^settings/automod/schedule/?$",
views.schedule_automod_task,
name="settings-automod-schedule",
),
re_path(
r"^settings/automod/unschedule/(?P<task_id>\d+)/?$",
views.unschedule_automod_task,
name="settings-automod-unschedule",
),
re_path(
r"^settings/automod/run/?$", views.run_automod, name="settings-automod-run"
),
# moderation
re_path(
r"^settings/reports/?$", views.ReportsAdmin.as_view(), name="settings-reports"

View File

@ -3,6 +3,7 @@
from .admin.announcements import Announcements, Announcement
from .admin.announcements import EditAnnouncement, delete_announcement
from .admin.automod import AutoMod, automod_delete, run_automod
from .admin.automod import schedule_automod_task, unschedule_automod_task
from .admin.dashboard import Dashboard
from .admin.federation import Federation, FederatedServer
from .admin.federation import AddFederatedServer, ImportServerBlocklist

View File

@ -1,10 +1,12 @@
""" moderation via flagged posts and users """
from django.contrib.auth.decorators import login_required, permission_required
from django.db import transaction
from django.shortcuts import get_object_or_404, redirect
from django.template.response import TemplateResponse
from django.utils.decorators import method_decorator
from django.views import View
from django.views.decorators.http import require_POST
from django_celery_beat.models import PeriodicTask
from bookwyrm import forms, models
@ -24,8 +26,9 @@ class AutoMod(View):
def get(self, request):
"""view rules"""
data = {"rules": models.AutoMod.objects.all(), "form": forms.AutoModRuleForm()}
return TemplateResponse(request, "settings/automod/rules.html", data)
return TemplateResponse(
request, "settings/automod/rules.html", automod_view_data()
)
def post(self, request):
"""add rule"""
@ -35,22 +38,49 @@ class AutoMod(View):
form.save()
form = forms.AutoModRuleForm()
data = {
"rules": models.AutoMod.objects.all(),
"form": form,
"success": success,
}
data = automod_view_data()
data["form"] = form
return TemplateResponse(request, "settings/automod/rules.html", data)
@require_POST
@permission_required("bookwyrm.moderate_user", raise_exception=True)
@permission_required("bookwyrm.moderate_post", raise_exception=True)
def schedule_automod_task(request):
"""scheduler"""
form = forms.IntervalScheduleForm(request.POST)
if not form.is_valid():
data = automod_view_data()
data["task_form"] = form
return TemplateResponse(request, "settings/automod/rules.html", data)
with transaction.atomic():
schedule = form.save()
PeriodicTask.objects.get_or_create(
interval=schedule,
name="automod-task",
task="bookwyrm.models.antispam.automod_task",
)
return redirect("settings-automod")
@require_POST
@permission_required("bookwyrm.moderate_user", raise_exception=True)
@permission_required("bookwyrm.moderate_post", raise_exception=True)
# pylint: disable=unused-argument
def unschedule_automod_task(request, task_id):
"""unscheduler"""
get_object_or_404(PeriodicTask, id=task_id).delete()
return redirect("settings-automod")
@require_POST
@permission_required("bookwyrm.moderate_user", raise_exception=True)
@permission_required("bookwyrm.moderate_post", raise_exception=True)
# pylint: disable=unused-argument
def automod_delete(request, rule_id):
"""Remove a rule"""
rule = get_object_or_404(models.AutoMod, id=rule_id)
rule.delete()
get_object_or_404(models.AutoMod, id=rule_id).delete()
return redirect("settings-automod")
@ -62,3 +92,18 @@ def run_automod(request):
"""run scan"""
models.automod_task.delay()
return redirect("settings-automod")
def automod_view_data():
"""helper to get data used in the template"""
try:
task = PeriodicTask.objects.get(name="automod-task")
except PeriodicTask.DoesNotExist:
task = None
return {
"task": task,
"task_form": forms.IntervalScheduleForm(),
"rules": models.AutoMod.objects.all(),
"form": forms.AutoModRuleForm(),
}