""" using django model forms """ import datetime from collections import defaultdict from urllib.parse import urlparse from django import forms from django.contrib.staticfiles.utils import get_files from django.contrib.staticfiles.storage import StaticFilesStorage from django.forms import ModelForm, PasswordInput, widgets, ChoiceField from django.forms.widgets import Textarea from django.utils import timezone from django.utils.translation import gettext_lazy as _ from bookwyrm import models from bookwyrm.models.fields import ClearableFileInputWithWarning from bookwyrm.models.user import FeedFilterChoices 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] # 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 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"] 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", "preferred_timezone", "preferred_language", ] 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"] class UserGroupForm(CustomForm): class Meta: model = models.User fields = ["groups"] 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 CoverForm(CustomForm): class Meta: model = models.Book fields = ["cover"] help_texts = {f: None for f in fields} 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." ), ) 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"} ), "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"}), } 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"} ), } class ImportForm(forms.Form): csv_file = forms.FileField() 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 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"] 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 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 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 SiteThemeForm(CustomForm): class Meta: model = models.SiteSettings fields = ["default_theme"] def get_theme_choices(): """static files""" choices = list(get_files(StaticFilesStorage(), location="css/themes")) current = models.Theme.objects.values_list("path", flat=True) return [(c, c) for c in choices if c not in current and c[-5:] == ".scss"] class ThemeForm(CustomForm): class Meta: model = models.Theme fields = ["name", "path"] widgets = { "name": forms.TextInput(attrs={"aria-describedby": "desc_name"}), "path": forms.Select( attrs={"aria-describedby": "desc_path"}, choices=get_theme_choices() ), } 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 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 GroupForm(CustomForm): class Meta: model = models.Group fields = ["user", "privacy", "name", "description"] class ReportForm(CustomForm): class Meta: model = models.Report fields = ["user", "reporter", "status", "links", "note"] 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 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")), ), ) 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"] class AutoModRuleForm(CustomForm): class Meta: model = models.AutoMod fields = ["string_match", "flag_users", "flag_statuses", "created_by"]