diff --git a/bookwyrm/forms/__init__.py b/bookwyrm/forms/__init__.py new file mode 100644 index 000000000..075752936 --- /dev/null +++ b/bookwyrm/forms/__init__.py @@ -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 * diff --git a/bookwyrm/forms/admin.py b/bookwyrm/forms/admin.py new file mode 100644 index 000000000..4141327d3 --- /dev/null +++ b/bookwyrm/forms/admin.py @@ -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"}), + } diff --git a/bookwyrm/forms/author.py b/bookwyrm/forms/author.py new file mode 100644 index 000000000..ca59426de --- /dev/null +++ b/bookwyrm/forms/author.py @@ -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"} + ), + } diff --git a/bookwyrm/forms/books.py b/bookwyrm/forms/books.py new file mode 100644 index 000000000..72df1371c --- /dev/null +++ b/bookwyrm/forms/books.py @@ -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"}), + } diff --git a/bookwyrm/forms/custom_form.py b/bookwyrm/forms/custom_form.py new file mode 100644 index 000000000..74a3417a2 --- /dev/null +++ b/bookwyrm/forms/custom_form.py @@ -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] diff --git a/bookwyrm/forms/edit_user.py b/bookwyrm/forms/edit_user.py new file mode 100644 index 000000000..d609f15dc --- /dev/null +++ b/bookwyrm/forms/edit_user.py @@ -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"] diff --git a/bookwyrm/forms/forms.py b/bookwyrm/forms/forms.py new file mode 100644 index 000000000..8af8fb812 --- /dev/null +++ b/bookwyrm/forms/forms.py @@ -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"] diff --git a/bookwyrm/forms/groups.py b/bookwyrm/forms/groups.py new file mode 100644 index 000000000..15b27c0ae --- /dev/null +++ b/bookwyrm/forms/groups.py @@ -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"] diff --git a/bookwyrm/forms/landing.py b/bookwyrm/forms/landing.py new file mode 100644 index 000000000..b01c2cc98 --- /dev/null +++ b/bookwyrm/forms/landing.py @@ -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"] diff --git a/bookwyrm/forms/links.py b/bookwyrm/forms/links.py new file mode 100644 index 000000000..de229bc2d --- /dev/null +++ b/bookwyrm/forms/links.py @@ -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." + ), + ) diff --git a/bookwyrm/forms/lists.py b/bookwyrm/forms/lists.py new file mode 100644 index 000000000..647db3bfe --- /dev/null +++ b/bookwyrm/forms/lists.py @@ -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")), + ), + ) diff --git a/bookwyrm/forms/status.py b/bookwyrm/forms/status.py new file mode 100644 index 000000000..0800166bf --- /dev/null +++ b/bookwyrm/forms/status.py @@ -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"] diff --git a/bookwyrm/management/commands/instance_version.py b/bookwyrm/management/commands/instance_version.py new file mode 100644 index 000000000..ca150d640 --- /dev/null +++ b/bookwyrm/management/commands/instance_version.py @@ -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) diff --git a/bookwyrm/migrations/0145_sitesettings_version.py b/bookwyrm/migrations/0145_sitesettings_version.py new file mode 100644 index 000000000..649f90abe --- /dev/null +++ b/bookwyrm/migrations/0145_sitesettings_version.py @@ -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), + ), + ] diff --git a/bookwyrm/models/site.py b/bookwyrm/models/site.py index edffa1adb..7730391f1 100644 --- a/bookwyrm/models/site.py +++ b/bookwyrm/models/site.py @@ -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) diff --git a/bookwyrm/settings.py b/bookwyrm/settings.py index 5ab3c7a13..f8d4c397f 100644 --- a/bookwyrm/settings.py +++ b/bookwyrm/settings.py @@ -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", ] diff --git a/bookwyrm/static/js/forms.js b/bookwyrm/static/js/forms.js index 7d946d147..998873898 100644 --- a/bookwyrm/static/js/forms.js +++ b/bookwyrm/static/js/forms.js @@ -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)); })(); diff --git a/bookwyrm/templates/book/edit/edit_book_form.html b/bookwyrm/templates/book/edit/edit_book_form.html index 38a7fe35d..42f1840df 100644 --- a/bookwyrm/templates/book/edit/edit_book_form.html +++ b/bookwyrm/templates/book/edit/edit_book_form.html @@ -22,7 +22,7 @@ {% trans "Title:" %} - + {% include 'snippets/form_errors.html' with errors_list=form.title.errors id="desc_title" %} @@ -31,7 +31,7 @@ {% trans "Subtitle:" %} - + {% include 'snippets/form_errors.html' with errors_list=form.subtitle.errors id="desc_subtitle" %} @@ -40,7 +40,7 @@ {% trans "Description:" %} {{ form.description }} - + {% include 'snippets/form_errors.html' with errors_list=form.description.errors id="desc_description" %} @@ -51,7 +51,7 @@ {% trans "Series:" %} - + {% include 'snippets/form_errors.html' with errors_list=form.series.errors id="desc_series" %} @@ -61,7 +61,7 @@ {% trans "Series number:" %} {{ form.series_number }} - + {% include 'snippets/form_errors.html' with errors_list=form.series_number.errors id="desc_series_number" %} @@ -75,21 +75,60 @@ {% trans "Separate multiple values with commas." %} - + {% include 'snippets/form_errors.html' with errors_list=form.languages.errors id="desc_languages" %} -
{% 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 not being generated automatically, and you must manually trigger a scan." %}
-