forked from mirrors/bookwyrm
Merge branch 'main' into form-conflict
This commit is contained in:
commit
7b3b357756
51 changed files with 4388 additions and 2813 deletions
12
bookwyrm/forms/__init__.py
Normal file
12
bookwyrm/forms/__init__.py
Normal 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
141
bookwyrm/forms/admin.py
Normal 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
47
bookwyrm/forms/author.py
Normal 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
87
bookwyrm/forms/books.py
Normal 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"}),
|
||||||
|
}
|
26
bookwyrm/forms/custom_form.py
Normal file
26
bookwyrm/forms/custom_form.py
Normal 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]
|
68
bookwyrm/forms/edit_user.py
Normal file
68
bookwyrm/forms/edit_user.py
Normal 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
59
bookwyrm/forms/forms.py
Normal 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
16
bookwyrm/forms/groups.py
Normal 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
45
bookwyrm/forms/landing.py
Normal 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
48
bookwyrm/forms/links.py
Normal 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
37
bookwyrm/forms/lists.py
Normal 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
82
bookwyrm/forms/status.py
Normal 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"]
|
54
bookwyrm/management/commands/instance_version.py
Normal file
54
bookwyrm/management/commands/instance_version.py
Normal 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)
|
18
bookwyrm/migrations/0145_sitesettings_version.py
Normal file
18
bookwyrm/migrations/0145_sitesettings_version.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
|
@ -27,6 +27,7 @@ class SiteSettings(models.Model):
|
||||||
default_theme = models.ForeignKey(
|
default_theme = models.ForeignKey(
|
||||||
"Theme", null=True, blank=True, on_delete=models.SET_NULL
|
"Theme", null=True, blank=True, on_delete=models.SET_NULL
|
||||||
)
|
)
|
||||||
|
version = models.CharField(null=True, blank=True, max_length=10)
|
||||||
|
|
||||||
# admin setup options
|
# admin setup options
|
||||||
install_mode = models.BooleanField(default=False)
|
install_mode = models.BooleanField(default=False)
|
||||||
|
|
|
@ -11,7 +11,7 @@ from django.utils.translation import gettext_lazy as _
|
||||||
env = Env()
|
env = Env()
|
||||||
env.read_env()
|
env.read_env()
|
||||||
DOMAIN = env("DOMAIN")
|
DOMAIN = env("DOMAIN")
|
||||||
VERSION = "0.3.3"
|
VERSION = "0.3.4"
|
||||||
|
|
||||||
RELEASE_API = env(
|
RELEASE_API = env(
|
||||||
"RELEASE_API",
|
"RELEASE_API",
|
||||||
|
@ -90,6 +90,7 @@ INSTALLED_APPS = [
|
||||||
"sass_processor",
|
"sass_processor",
|
||||||
"bookwyrm",
|
"bookwyrm",
|
||||||
"celery",
|
"celery",
|
||||||
|
"django_celery_beat",
|
||||||
"imagekit",
|
"imagekit",
|
||||||
"storages",
|
"storages",
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,6 +1,19 @@
|
||||||
(function () {
|
(function () {
|
||||||
"use strict";
|
"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
|
* Duplicate an input field
|
||||||
*
|
*
|
||||||
|
@ -29,4 +42,8 @@
|
||||||
document
|
document
|
||||||
.querySelectorAll("[data-duplicate]")
|
.querySelectorAll("[data-duplicate]")
|
||||||
.forEach((node) => node.addEventListener("click", duplicateInput));
|
.forEach((node) => node.addEventListener("click", duplicateInput));
|
||||||
|
|
||||||
|
document
|
||||||
|
.querySelectorAll("[data-remove]")
|
||||||
|
.forEach((node) => node.addEventListener("click", removeInput));
|
||||||
})();
|
})();
|
||||||
|
|
|
@ -79,17 +79,56 @@
|
||||||
{% include 'snippets/form_errors.html' with errors_list=form.languages.errors id="desc_languages" %}
|
{% include 'snippets/form_errors.html' with errors_list=form.languages.errors id="desc_languages" %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div>
|
||||||
<label class="label" for="id_subjects">
|
<label class="label" for="id_add_subjects">
|
||||||
{% trans "Subjects:" %}
|
{% trans "Subjects:" %}
|
||||||
</label>
|
</label>
|
||||||
{{ form.subjects }}
|
{% for subject in book.subjects %}
|
||||||
<span class="help" id="desc_subjects_help">
|
<label class="label is-sr-only" for="id_add_subject={% if not forloop.first %}-{{forloop.counter}}{% endif %}">
|
||||||
{% trans "Separate multiple values with commas." %}
|
{% 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>
|
</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" %}
|
{% include 'snippets/form_errors.html' with errors_list=form.subjects.errors id="desc_subjects" %}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
@ -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 %}>
|
<input class="input" type="text" name="add_author" id="id_add_author" placeholder="{% trans 'Jane Doe' %}" value="{{ author }}" {% if confirm_mode %}readonly{% endif %}>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
<header class="block">
|
<header class="block">
|
||||||
<h1 class="title">
|
<h1 class="title">
|
||||||
{% blocktrans with title=book|book_title %}
|
{% blocktrans trimmed with title=book|book_title %}
|
||||||
Links for "<em>{{ title }}</em>"
|
Links for "<em>{{ title }}</em>"
|
||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
{% extends 'settings/layout.html' %}
|
{% extends 'settings/layout.html' %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
{% load humanize %}
|
||||||
{% load utilities %}
|
{% load utilities %}
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}
|
||||||
|
@ -16,14 +17,83 @@
|
||||||
<p>
|
<p>
|
||||||
{% trans "Auto-moderation rules will create reports for any local user or status with fields matching the provided string." %}
|
{% 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 "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>
|
</p>
|
||||||
|
</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' %}">
|
<form name="run-scan" method="POST" action="{% url 'settings-automod-run' %}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<button class="button is-warning">{% trans "Run scan" %}</button>
|
<button class="button">{% trans "Run now" %}</button>
|
||||||
|
<p class="help">{% trans "Last run date will not be updated" %}</p>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% else %}
|
||||||
|
<h2 class="title is-4">{% trans "Schedule scan" %}</h2>
|
||||||
|
<form name="schedule-scan" method="POST" action="{% url 'settings-automod-schedule' %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<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 %}
|
{% if success %}
|
||||||
<div class="notification is-success is-light">
|
<div class="notification is-success is-light">
|
||||||
<span class="icon icon-check" aria-hidden="true"></span>
|
<span class="icon icon-check" aria-hidden="true"></span>
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
{% with goal.progress as progress %}
|
{% with goal.progress as progress %}
|
||||||
<p>
|
<p>
|
||||||
{% if progress.percent >= 100 %}
|
{% if progress.percent >= 100 %}
|
||||||
{% trans "Success!" %}
|
{% trans "Success!" context "Goal successfully completed" %}
|
||||||
{% elif progress.percent %}
|
{% elif progress.percent %}
|
||||||
{% blocktrans with percent=progress.percent %}{{ percent }}% complete!{% endblocktrans %}
|
{% blocktrans with percent=progress.percent %}{{ percent }}% complete!{% endblocktrans %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -233,11 +233,23 @@ urlpatterns = [
|
||||||
# auto-moderation rules
|
# auto-moderation rules
|
||||||
re_path(r"^settings/automod/?$", views.AutoMod.as_view(), name="settings-automod"),
|
re_path(r"^settings/automod/?$", views.AutoMod.as_view(), name="settings-automod"),
|
||||||
re_path(
|
re_path(
|
||||||
r"^settings/automod/(?P<rule_id>\d+)/delete?$",
|
r"^settings/automod/(?P<rule_id>\d+)/delete/?$",
|
||||||
views.automod_delete,
|
views.automod_delete,
|
||||||
name="settings-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
|
# moderation
|
||||||
re_path(
|
re_path(
|
||||||
r"^settings/reports/?$", views.ReportsAdmin.as_view(), name="settings-reports"
|
r"^settings/reports/?$", views.ReportsAdmin.as_view(), name="settings-reports"
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
from .admin.announcements import Announcements, Announcement
|
from .admin.announcements import Announcements, Announcement
|
||||||
from .admin.announcements import EditAnnouncement, delete_announcement
|
from .admin.announcements import EditAnnouncement, delete_announcement
|
||||||
from .admin.automod import AutoMod, automod_delete, run_automod
|
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.dashboard import Dashboard
|
||||||
from .admin.federation import Federation, FederatedServer
|
from .admin.federation import Federation, FederatedServer
|
||||||
from .admin.federation import AddFederatedServer, ImportServerBlocklist
|
from .admin.federation import AddFederatedServer, ImportServerBlocklist
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
""" moderation via flagged posts and users """
|
""" moderation via flagged posts and users """
|
||||||
from django.contrib.auth.decorators import login_required, permission_required
|
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.shortcuts import get_object_or_404, redirect
|
||||||
from django.template.response import TemplateResponse
|
from django.template.response import TemplateResponse
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from django.views import View
|
from django.views import View
|
||||||
from django.views.decorators.http import require_POST
|
from django.views.decorators.http import require_POST
|
||||||
|
from django_celery_beat.models import PeriodicTask
|
||||||
|
|
||||||
from bookwyrm import forms, models
|
from bookwyrm import forms, models
|
||||||
|
|
||||||
|
@ -24,8 +26,9 @@ class AutoMod(View):
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
"""view rules"""
|
"""view rules"""
|
||||||
data = {"rules": models.AutoMod.objects.all(), "form": forms.AutoModRuleForm()}
|
return TemplateResponse(
|
||||||
return TemplateResponse(request, "settings/automod/rules.html", data)
|
request, "settings/automod/rules.html", automod_view_data()
|
||||||
|
)
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
"""add rule"""
|
"""add rule"""
|
||||||
|
@ -35,22 +38,49 @@ class AutoMod(View):
|
||||||
form.save()
|
form.save()
|
||||||
form = forms.AutoModRuleForm()
|
form = forms.AutoModRuleForm()
|
||||||
|
|
||||||
data = {
|
data = automod_view_data()
|
||||||
"rules": models.AutoMod.objects.all(),
|
data["form"] = form
|
||||||
"form": form,
|
|
||||||
"success": success,
|
|
||||||
}
|
|
||||||
return TemplateResponse(request, "settings/automod/rules.html", data)
|
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
|
@require_POST
|
||||||
@permission_required("bookwyrm.moderate_user", raise_exception=True)
|
@permission_required("bookwyrm.moderate_user", raise_exception=True)
|
||||||
@permission_required("bookwyrm.moderate_post", raise_exception=True)
|
@permission_required("bookwyrm.moderate_post", raise_exception=True)
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def automod_delete(request, rule_id):
|
def automod_delete(request, rule_id):
|
||||||
"""Remove a rule"""
|
"""Remove a rule"""
|
||||||
rule = get_object_or_404(models.AutoMod, id=rule_id)
|
get_object_or_404(models.AutoMod, id=rule_id).delete()
|
||||||
rule.delete()
|
|
||||||
return redirect("settings-automod")
|
return redirect("settings-automod")
|
||||||
|
|
||||||
|
|
||||||
|
@ -62,3 +92,18 @@ def run_automod(request):
|
||||||
"""run scan"""
|
"""run scan"""
|
||||||
models.automod_task.delay()
|
models.automod_task.delay()
|
||||||
return redirect("settings-automod")
|
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(),
|
||||||
|
}
|
||||||
|
|
2
bw-dev
2
bw-dev
|
@ -163,6 +163,7 @@ case "$CMD" in
|
||||||
update)
|
update)
|
||||||
git pull
|
git pull
|
||||||
docker-compose build
|
docker-compose build
|
||||||
|
./update.sh
|
||||||
runweb python manage.py migrate
|
runweb python manage.py migrate
|
||||||
runweb python manage.py collectstatic --no-input
|
runweb python manage.py collectstatic --no-input
|
||||||
docker-compose up -d
|
docker-compose up -d
|
||||||
|
@ -215,6 +216,7 @@ case "$CMD" in
|
||||||
;;
|
;;
|
||||||
setup)
|
setup)
|
||||||
migrate
|
migrate
|
||||||
|
migrate django_celery_beat
|
||||||
initdb
|
initdb
|
||||||
runweb python manage.py collectstatic --no-input
|
runweb python manage.py collectstatic --no-input
|
||||||
admin_code
|
admin_code
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
# pylint: disable=unused-wildcard-import
|
# pylint: disable=unused-wildcard-import
|
||||||
from bookwyrm.settings import *
|
from bookwyrm.settings import *
|
||||||
|
|
||||||
|
# pylint: disable=line-too-long
|
||||||
REDIS_BROKER_PASSWORD = requests.utils.quote(env("REDIS_BROKER_PASSWORD", None))
|
REDIS_BROKER_PASSWORD = requests.utils.quote(env("REDIS_BROKER_PASSWORD", None))
|
||||||
REDIS_BROKER_HOST = env("REDIS_BROKER_HOST", "redis_broker")
|
REDIS_BROKER_HOST = env("REDIS_BROKER_HOST", "redis_broker")
|
||||||
REDIS_BROKER_PORT = env("REDIS_BROKER_PORT", 6379)
|
REDIS_BROKER_PORT = env("REDIS_BROKER_PORT", 6379)
|
||||||
|
@ -16,6 +17,10 @@ CELERY_DEFAULT_QUEUE = "low_priority"
|
||||||
CELERY_ACCEPT_CONTENT = ["json"]
|
CELERY_ACCEPT_CONTENT = ["json"]
|
||||||
CELERY_TASK_SERIALIZER = "json"
|
CELERY_TASK_SERIALIZER = "json"
|
||||||
CELERY_RESULT_SERIALIZER = "json"
|
CELERY_RESULT_SERIALIZER = "json"
|
||||||
|
|
||||||
|
CELERY_BEAT_SCHEDULER = "django_celery_beat.schedulers:DatabaseScheduler"
|
||||||
|
CELERY_TIMEZONE = env("TIME_ZONE", "UTC")
|
||||||
|
|
||||||
FLOWER_PORT = env("FLOWER_PORT")
|
FLOWER_PORT = env("FLOWER_PORT")
|
||||||
|
|
||||||
INSTALLED_APPS = INSTALLED_APPS + [
|
INSTALLED_APPS = INSTALLED_APPS + [
|
||||||
|
|
|
@ -70,6 +70,19 @@ services:
|
||||||
- db
|
- db
|
||||||
- redis_broker
|
- redis_broker
|
||||||
restart: on-failure
|
restart: on-failure
|
||||||
|
celery_beat:
|
||||||
|
env_file: .env
|
||||||
|
build: .
|
||||||
|
networks:
|
||||||
|
- main
|
||||||
|
command: celery -A celerywyrm beat -l INFO --scheduler django_celery_beat.schedulers:DatabaseScheduler
|
||||||
|
volumes:
|
||||||
|
- .:/app
|
||||||
|
- static_volume:/app/static
|
||||||
|
- media_volume:/app/images
|
||||||
|
depends_on:
|
||||||
|
- celery_worker
|
||||||
|
restart: on-failure
|
||||||
flower:
|
flower:
|
||||||
build: .
|
build: .
|
||||||
command: celery -A celerywyrm flower --basic_auth=${FLOWER_USER}:${FLOWER_PASSWORD}
|
command: celery -A celerywyrm flower --basic_auth=${FLOWER_USER}:${FLOWER_PASSWORD}
|
||||||
|
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
@ -1,6 +1,7 @@
|
||||||
celery==5.2.2
|
celery==5.2.2
|
||||||
colorthief==0.2.1
|
colorthief==0.2.1
|
||||||
Django==3.2.12
|
Django==3.2.12
|
||||||
|
django-celery-beat==2.2.1
|
||||||
django-compressor==2.4.1
|
django-compressor==2.4.1
|
||||||
django-imagekit==4.1.0
|
django-imagekit==4.1.0
|
||||||
django-model-utils==4.0.0
|
django-model-utils==4.0.0
|
||||||
|
|
37
update.sh
Executable file
37
update.sh
Executable file
|
@ -0,0 +1,37 @@
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# determine inital and target versions
|
||||||
|
initial_version="`./bw-dev runweb python manage.py instance_version --current`"
|
||||||
|
target_version="`./bw-dev runweb python manage.py instance_version --target`"
|
||||||
|
|
||||||
|
initial_version="`echo $initial_version | tail -n 1 | xargs`"
|
||||||
|
target_version="`echo $target_version | tail -n 1 | xargs`"
|
||||||
|
if [[ "$initial_version" = "$target_version" ]]; then
|
||||||
|
echo "Already up to date; version $initial_version"
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "---------------------------------------"
|
||||||
|
echo "Updating from version: $initial_version"
|
||||||
|
echo ".......... to version: $target_version"
|
||||||
|
echo "---------------------------------------"
|
||||||
|
|
||||||
|
function version_gt() { test "$(printf '%s\n' "$@" | sort -V | head -n 1)" != "$1"; }
|
||||||
|
|
||||||
|
# execute scripts between initial and target
|
||||||
|
for version in `ls -A updates/ | sort -V `; do
|
||||||
|
if version_gt $initial_version $version; then
|
||||||
|
# too early
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if version_gt $version $target_version; then
|
||||||
|
# too late
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
echo "Running tasks for version $version"
|
||||||
|
./updates/$version
|
||||||
|
done
|
||||||
|
|
||||||
|
./bw-dev runweb python manage.py instance_version --update
|
||||||
|
echo "✨ ----------- Done! --------------- ✨"
|
1
updates/0.3.4.sh
Executable file
1
updates/0.3.4.sh
Executable file
|
@ -0,0 +1 @@
|
||||||
|
./bw-dev migrate django_celery_beat
|
Loading…
Reference in a new issue