""" using django model forms """ import datetime from collections import defaultdict from urllib.parse import urlparse from django import forms 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", "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"] 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", "answer"] 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"] 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"}), } 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"]