Initial commit
This commit is contained in:
9
env/lib/python3.10/site-packages/wagtail/admin/forms/__init__.py
vendored
Normal file
9
env/lib/python3.10/site-packages/wagtail/admin/forms/__init__.py
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
# definitions which are not being deprecated from wagtail.admin.forms
|
||||
from .models import ( # NOQA: F401
|
||||
DIRECT_FORM_FIELD_OVERRIDES,
|
||||
FORM_FIELD_OVERRIDES,
|
||||
WagtailAdminModelForm,
|
||||
WagtailAdminModelFormMetaclass,
|
||||
formfield_for_dbfield,
|
||||
)
|
||||
from .pages import WagtailAdminPageForm # NOQA: F401
|
||||
BIN
env/lib/python3.10/site-packages/wagtail/admin/forms/__pycache__/__init__.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/forms/__pycache__/__init__.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/forms/__pycache__/account.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/forms/__pycache__/account.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/forms/__pycache__/auth.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/forms/__pycache__/auth.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/forms/__pycache__/choosers.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/forms/__pycache__/choosers.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/forms/__pycache__/collections.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/forms/__pycache__/collections.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/forms/__pycache__/comments.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/forms/__pycache__/comments.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/forms/__pycache__/models.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/forms/__pycache__/models.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/forms/__pycache__/pages.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/forms/__pycache__/pages.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/forms/__pycache__/search.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/forms/__pycache__/search.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/forms/__pycache__/tags.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/forms/__pycache__/tags.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/forms/__pycache__/view_restrictions.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/forms/__pycache__/view_restrictions.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/forms/__pycache__/workflows.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/forms/__pycache__/workflows.cpython-310.pyc
vendored
Normal file
Binary file not shown.
143
env/lib/python3.10/site-packages/wagtail/admin/forms/account.py
vendored
Normal file
143
env/lib/python3.10/site-packages/wagtail/admin/forms/account.py
vendored
Normal file
@@ -0,0 +1,143 @@
|
||||
import warnings
|
||||
from operator import itemgetter
|
||||
|
||||
import l18n
|
||||
from django import forms
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.db.models.fields import BLANK_CHOICE_DASH
|
||||
from django.utils.translation import get_language_info
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from wagtail.admin.localization import (
|
||||
get_available_admin_languages,
|
||||
get_available_admin_time_zones,
|
||||
)
|
||||
from wagtail.admin.widgets import SwitchInput
|
||||
from wagtail.permissions import page_permission_policy
|
||||
from wagtail.users.models import UserProfile
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class NotificationPreferencesForm(forms.ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
permission_policy = page_permission_policy
|
||||
if not permission_policy.user_has_permission(self.instance.user, "publish"):
|
||||
del self.fields["submitted_notifications"]
|
||||
if not permission_policy.user_has_permission(self.instance.user, "change"):
|
||||
del self.fields["approved_notifications"]
|
||||
del self.fields["rejected_notifications"]
|
||||
del self.fields["updated_comments_notifications"]
|
||||
|
||||
class Meta:
|
||||
model = UserProfile
|
||||
fields = [
|
||||
"submitted_notifications",
|
||||
"approved_notifications",
|
||||
"rejected_notifications",
|
||||
"updated_comments_notifications",
|
||||
]
|
||||
widgets = {
|
||||
"submitted_notifications": SwitchInput(),
|
||||
"approved_notifications": SwitchInput(),
|
||||
"rejected_notifications": SwitchInput(),
|
||||
"updated_comments_notifications": SwitchInput(),
|
||||
}
|
||||
|
||||
|
||||
def _get_language_choices():
|
||||
language_choices = [
|
||||
(lang_code, get_language_info(lang_code)["name_local"])
|
||||
for lang_code, lang_name in get_available_admin_languages()
|
||||
]
|
||||
return sorted(
|
||||
BLANK_CHOICE_DASH + language_choices,
|
||||
key=lambda language_choice: language_choice[1].lower(),
|
||||
)
|
||||
|
||||
|
||||
def _get_time_zone_choices():
|
||||
time_zones = [
|
||||
(tz, str(l18n.tz_fullnames.get(tz, tz)))
|
||||
for tz in get_available_admin_time_zones()
|
||||
]
|
||||
time_zones.sort(key=itemgetter(1))
|
||||
return BLANK_CHOICE_DASH + time_zones
|
||||
|
||||
|
||||
class LocalePreferencesForm(forms.ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if len(get_available_admin_languages()) <= 1:
|
||||
del self.fields["preferred_language"]
|
||||
|
||||
if len(get_available_admin_time_zones()) <= 1:
|
||||
del self.fields["current_time_zone"]
|
||||
|
||||
preferred_language = forms.ChoiceField(
|
||||
required=False, choices=_get_language_choices, label=_("Preferred language")
|
||||
)
|
||||
|
||||
current_time_zone = forms.ChoiceField(
|
||||
required=False, choices=_get_time_zone_choices, label=_("Current time zone")
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = UserProfile
|
||||
fields = ["preferred_language", "current_time_zone"]
|
||||
|
||||
|
||||
class NameEmailForm(forms.ModelForm):
|
||||
first_name = forms.CharField(required=True, label=_("First Name"))
|
||||
last_name = forms.CharField(required=True, label=_("Last Name"))
|
||||
email = forms.EmailField(required=True, label=_("Email"))
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
from wagtail.admin.views.account import email_management_enabled
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if not email_management_enabled():
|
||||
del self.fields["email"]
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ["first_name", "last_name", "email"]
|
||||
|
||||
|
||||
class AvatarPreferencesForm(forms.ModelForm):
|
||||
avatar = forms.ImageField(label=_("Upload a profile picture"), required=False)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._original_avatar = self.instance.avatar
|
||||
|
||||
def save(self, commit=True):
|
||||
if (
|
||||
commit
|
||||
and self._original_avatar
|
||||
and (self._original_avatar != self.cleaned_data["avatar"])
|
||||
):
|
||||
# Call delete() on the storage backend directly, as calling self._original_avatar.delete()
|
||||
# will clear the now-updated field on self.instance too
|
||||
try:
|
||||
self._original_avatar.storage.delete(self._original_avatar.name)
|
||||
except OSError:
|
||||
# failure to delete the old avatar shouldn't prevent us from continuing
|
||||
warnings.warn(
|
||||
"Failed to delete old avatar file: %s" % self._original_avatar.name
|
||||
)
|
||||
super().save(commit=commit)
|
||||
|
||||
class Meta:
|
||||
model = UserProfile
|
||||
fields = ["avatar"]
|
||||
|
||||
|
||||
class ThemePreferencesForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = UserProfile
|
||||
fields = ["theme", "density"]
|
||||
81
env/lib/python3.10/site-packages/wagtail/admin/forms/auth.py
vendored
Normal file
81
env/lib/python3.10/site-packages/wagtail/admin/forms/auth.py
vendored
Normal file
@@ -0,0 +1,81 @@
|
||||
from django import forms
|
||||
from django.contrib.auth.forms import AuthenticationForm
|
||||
from django.contrib.auth.forms import PasswordChangeForm as DjangoPasswordChangeForm
|
||||
from django.contrib.auth.forms import PasswordResetForm as DjangoPasswordResetForm
|
||||
from django.utils.translation import gettext_lazy
|
||||
|
||||
|
||||
class LoginForm(AuthenticationForm):
|
||||
username = forms.CharField(max_length=254, widget=forms.TextInput())
|
||||
|
||||
password = forms.CharField(
|
||||
widget=forms.PasswordInput(
|
||||
attrs={
|
||||
"placeholder": gettext_lazy("Enter password"),
|
||||
}
|
||||
),
|
||||
strip=False,
|
||||
)
|
||||
|
||||
remember = forms.BooleanField(required=False)
|
||||
|
||||
error_messages = {
|
||||
**AuthenticationForm.error_messages,
|
||||
"invalid_login": gettext_lazy(
|
||||
"Your %(username_field)s and password didn't match. Please try again."
|
||||
),
|
||||
}
|
||||
|
||||
def __init__(self, request=None, *args, **kwargs):
|
||||
super().__init__(request=request, *args, **kwargs)
|
||||
self.fields["username"].widget.attrs["placeholder"] = gettext_lazy(
|
||||
"Enter your %(username_field_name)s"
|
||||
) % {"username_field_name": self.username_field.verbose_name}
|
||||
self.fields["username"].widget.attrs["autofocus"] = ""
|
||||
|
||||
@property
|
||||
def extra_fields(self):
|
||||
for field_name in self.fields.keys():
|
||||
if field_name not in ["username", "password", "remember"]:
|
||||
yield field_name, self[field_name]
|
||||
|
||||
def get_invalid_login_error(self):
|
||||
return forms.ValidationError(
|
||||
self.error_messages["invalid_login"],
|
||||
code="invalid_login",
|
||||
params={"username_field": self.username_field.verbose_name},
|
||||
)
|
||||
|
||||
|
||||
class PasswordResetForm(DjangoPasswordResetForm):
|
||||
email = forms.EmailField(
|
||||
label=gettext_lazy("Enter your email address to reset your password"),
|
||||
max_length=254,
|
||||
required=True,
|
||||
)
|
||||
|
||||
@property
|
||||
def extra_fields(self):
|
||||
for field_name in self.fields.keys():
|
||||
if field_name not in ["email"]:
|
||||
yield field_name, self[field_name]
|
||||
|
||||
|
||||
class PasswordChangeForm(DjangoPasswordChangeForm):
|
||||
"""
|
||||
Since this is displayed as part of a larger form, this differs from the vanilla Django
|
||||
PasswordChangeForm as follows:
|
||||
* the old-password field is not auto-focused
|
||||
* Fields are not marked as required
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
try:
|
||||
del self.fields["old_password"].widget.attrs["autofocus"]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
self.fields["old_password"].required = False
|
||||
self.fields["new_password1"].required = False
|
||||
self.fields["new_password2"].required = False
|
||||
151
env/lib/python3.10/site-packages/wagtail/admin/forms/choosers.py
vendored
Normal file
151
env/lib/python3.10/site-packages/wagtail/admin/forms/choosers.py
vendored
Normal file
@@ -0,0 +1,151 @@
|
||||
import warnings
|
||||
|
||||
from django import forms
|
||||
from django.core import validators
|
||||
from django.forms.widgets import TextInput
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from wagtail.models import Locale
|
||||
from wagtail.search.backends import get_search_backend
|
||||
|
||||
|
||||
class URLOrAbsolutePathValidator(validators.URLValidator):
|
||||
@staticmethod
|
||||
def is_absolute_path(value):
|
||||
return value.startswith("/")
|
||||
|
||||
def __call__(self, value):
|
||||
if URLOrAbsolutePathValidator.is_absolute_path(value):
|
||||
return None
|
||||
else:
|
||||
return super().__call__(value)
|
||||
|
||||
|
||||
class URLOrAbsolutePathField(forms.URLField):
|
||||
widget = TextInput
|
||||
default_validators = [URLOrAbsolutePathValidator()]
|
||||
|
||||
def to_python(self, value):
|
||||
if not URLOrAbsolutePathValidator.is_absolute_path(value):
|
||||
value = super().to_python(value)
|
||||
return value
|
||||
|
||||
|
||||
class ExternalLinkChooserForm(forms.Form):
|
||||
url = URLOrAbsolutePathField(required=True, label=_("URL"))
|
||||
link_text = forms.CharField(required=False)
|
||||
|
||||
|
||||
class AnchorLinkChooserForm(forms.Form):
|
||||
url = forms.CharField(required=True, label="#")
|
||||
link_text = forms.CharField(required=False)
|
||||
|
||||
|
||||
class EmailLinkChooserForm(forms.Form):
|
||||
email_address = forms.EmailField(required=True)
|
||||
link_text = forms.CharField(required=False)
|
||||
subject = forms.CharField(required=False)
|
||||
body = forms.CharField(required=False, widget=forms.Textarea(attrs={"rows": 3}))
|
||||
|
||||
|
||||
class PhoneLinkChooserForm(forms.Form):
|
||||
phone_number = forms.CharField(required=True)
|
||||
link_text = forms.CharField(required=False)
|
||||
|
||||
|
||||
class BaseFilterForm(forms.Form):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.is_searching = False
|
||||
self.is_filtering_by_collection = False
|
||||
self.search_query = None
|
||||
|
||||
def filter(self, objects):
|
||||
return objects
|
||||
|
||||
|
||||
class SearchFilterMixin(forms.Form):
|
||||
"""
|
||||
Mixin for a chooser listing filter form, to provide a search field
|
||||
"""
|
||||
|
||||
q = forms.CharField(
|
||||
label=_("Search term"),
|
||||
widget=forms.TextInput(attrs={"placeholder": _("Search")}),
|
||||
required=False,
|
||||
)
|
||||
|
||||
def filter(self, objects):
|
||||
objects = super().filter(objects)
|
||||
search_query = self.cleaned_data.get("q")
|
||||
if search_query:
|
||||
search_backend = get_search_backend()
|
||||
if objects.model.get_autocomplete_search_fields():
|
||||
objects = search_backend.autocomplete(search_query, objects)
|
||||
else:
|
||||
# fall back on non-autocompleting search
|
||||
warnings.warn(
|
||||
f"{objects.model} is defined as Indexable but does not specify "
|
||||
"any AutocompleteFields. Searches within the chooser will only "
|
||||
"respond to complete words.",
|
||||
category=RuntimeWarning,
|
||||
)
|
||||
|
||||
objects = search_backend.search(search_query, objects)
|
||||
self.is_searching = True
|
||||
self.search_query = search_query
|
||||
return objects
|
||||
|
||||
|
||||
class CollectionFilterMixin(forms.Form):
|
||||
"""
|
||||
Mixin for a chooser listing filter form, to provide a collection filter field.
|
||||
The view must pass a `collections` keyword argument when constructing the form
|
||||
"""
|
||||
|
||||
def __init__(self, *args, collections=None, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if collections:
|
||||
collection_choices = [
|
||||
("", _("All collections"))
|
||||
] + collections.get_indented_choices()
|
||||
self.fields["collection_id"] = forms.ChoiceField(
|
||||
label=_("Collection"),
|
||||
choices=collection_choices,
|
||||
required=False,
|
||||
widget=forms.Select(attrs={"data-chooser-modal-search-filter": True}),
|
||||
)
|
||||
|
||||
def filter(self, objects):
|
||||
collection_id = self.cleaned_data.get("collection_id")
|
||||
if collection_id:
|
||||
self.is_filtering_by_collection = True
|
||||
objects = objects.filter(collection=collection_id)
|
||||
return super().filter(objects)
|
||||
|
||||
|
||||
class LocaleFilterMixin(forms.Form):
|
||||
"""
|
||||
Mixin for a chooser listing filter form, to provide a locale filter field.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
locales = Locale.objects.all()
|
||||
if locales:
|
||||
self.fields["locale"] = forms.ChoiceField(
|
||||
choices=[
|
||||
(locale.language_code, locale.get_display_name())
|
||||
for locale in locales
|
||||
],
|
||||
required=False,
|
||||
widget=forms.Select(attrs={"data-chooser-modal-search-filter": True}),
|
||||
)
|
||||
|
||||
def filter(self, objects):
|
||||
selected_locale_code = self.cleaned_data.get("locale")
|
||||
if selected_locale_code:
|
||||
selected_locale = Locale.objects.get(language_code=selected_locale_code)
|
||||
objects = objects.filter(locale=selected_locale)
|
||||
return super().filter(objects)
|
||||
384
env/lib/python3.10/site-packages/wagtail/admin/forms/collections.py
vendored
Normal file
384
env/lib/python3.10/site-packages/wagtail/admin/forms/collections.py
vendored
Normal file
@@ -0,0 +1,384 @@
|
||||
from itertools import groupby
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import Group, Permission
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import transaction
|
||||
from django.db.models import Min
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.translation import gettext as _
|
||||
from django.utils.translation import gettext_lazy
|
||||
|
||||
from wagtail.models import (
|
||||
Collection,
|
||||
CollectionViewRestriction,
|
||||
GroupCollectionPermission,
|
||||
)
|
||||
|
||||
from .view_restrictions import BaseViewRestrictionForm
|
||||
|
||||
|
||||
class CollectionViewRestrictionForm(BaseViewRestrictionForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if not getattr(settings, "WAGTAILDOCS_PRIVATE_COLLECTION_OPTIONS", {}).get(
|
||||
"SHARED_PASSWORD",
|
||||
True,
|
||||
):
|
||||
self.fields["restriction_type"].choices = [
|
||||
choice
|
||||
for choice in CollectionViewRestriction.RESTRICTION_CHOICES
|
||||
if choice[0] != CollectionViewRestriction.PASSWORD
|
||||
]
|
||||
del self.fields["password"]
|
||||
|
||||
class Meta:
|
||||
model = CollectionViewRestriction
|
||||
fields = ("restriction_type", "password", "groups")
|
||||
|
||||
|
||||
class SelectWithDisabledOptions(forms.Select):
|
||||
"""
|
||||
Subclass of Django's select widget that allows disabling options.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.disabled_values = ()
|
||||
|
||||
def create_option(self, name, value, *args, **kwargs):
|
||||
option_dict = super().create_option(name, value, *args, **kwargs)
|
||||
if value in self.disabled_values:
|
||||
option_dict["attrs"]["disabled"] = "disabled"
|
||||
return option_dict
|
||||
|
||||
|
||||
class CollectionChoiceField(forms.ModelChoiceField):
|
||||
widget = SelectWithDisabledOptions
|
||||
|
||||
def __init__(self, *args, disabled_queryset=None, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._indentation_start_depth = 2
|
||||
self.disabled_queryset = disabled_queryset
|
||||
|
||||
def _get_disabled_queryset(self):
|
||||
return self._disabled_queryset
|
||||
|
||||
def _set_disabled_queryset(self, queryset):
|
||||
self._disabled_queryset = queryset
|
||||
if queryset is None:
|
||||
self.widget.disabled_values = ()
|
||||
else:
|
||||
self.widget.disabled_values = queryset.values_list(
|
||||
self.to_field_name or "pk", flat=True
|
||||
)
|
||||
|
||||
disabled_queryset = property(_get_disabled_queryset, _set_disabled_queryset)
|
||||
|
||||
def _set_queryset(self, queryset):
|
||||
min_depth = self.queryset.aggregate(Min("depth"))["depth__min"]
|
||||
if min_depth is None:
|
||||
self._indentation_start_depth = 2
|
||||
else:
|
||||
self._indentation_start_depth = min_depth + 1
|
||||
|
||||
def label_from_instance(self, obj):
|
||||
return obj.get_indented_name(self._indentation_start_depth, html=True)
|
||||
|
||||
|
||||
class CollectionForm(forms.ModelForm):
|
||||
parent = CollectionChoiceField(
|
||||
label=gettext_lazy("Parent"),
|
||||
queryset=Collection.objects.all(),
|
||||
required=True,
|
||||
help_text=gettext_lazy(
|
||||
"Select hierarchical position. Note: a collection cannot become a child of itself or one of its "
|
||||
"descendants."
|
||||
),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Collection
|
||||
fields = ("name",)
|
||||
|
||||
def clean_parent(self):
|
||||
"""
|
||||
Our rules about where a user may add or move a collection are as follows:
|
||||
1. The user must have 'add' permission on the parent collection (or its ancestors)
|
||||
2. We are not moving a collection used to assign permissions for this user
|
||||
3. We are not trying to move a collection to be parented by one of their descendants
|
||||
|
||||
The first 2 items are taken care in the Create and Edit views by deleting the 'parent' field
|
||||
from the edit form if the user cannot move the collection. This causes Django's form
|
||||
machinery to ignore the parent field for parent regardless of what the user submits.
|
||||
This methods enforces rule #3 when we are editing an existing collection.
|
||||
"""
|
||||
parent = self.cleaned_data["parent"]
|
||||
if not self.instance._state.adding and not parent.pk == self.initial.get(
|
||||
"parent"
|
||||
):
|
||||
old_descendants = list(
|
||||
self.instance.get_descendants(inclusive=True).values_list(
|
||||
"pk", flat=True
|
||||
)
|
||||
)
|
||||
if parent.pk in old_descendants:
|
||||
raise ValidationError(gettext_lazy("Please select another parent"))
|
||||
return parent
|
||||
|
||||
|
||||
class BaseCollectionMemberForm(forms.ModelForm):
|
||||
"""
|
||||
Abstract form handler for editing models that belong to a collection,
|
||||
such as documents and images. These forms are (optionally) instantiated
|
||||
with a 'user' kwarg, and take care of populating the 'collection' field's
|
||||
choices with the collections the user has permission for, as well as
|
||||
hiding the field when only one collection is available.
|
||||
|
||||
Subclasses must define a 'permission_policy' attribute.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
user = kwargs.pop("user", None)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if user is None:
|
||||
self.collections = Collection.objects.all()
|
||||
else:
|
||||
self.collections = (
|
||||
self.permission_policy.collections_user_has_permission_for(user, "add")
|
||||
)
|
||||
|
||||
if self.instance.pk:
|
||||
# editing an existing document; ensure that the list of available collections
|
||||
# includes its current collection
|
||||
self.collections = self.collections | Collection.objects.filter(
|
||||
id=self.instance.collection_id
|
||||
)
|
||||
|
||||
if len(self.collections) == 0:
|
||||
raise Exception(
|
||||
"Cannot construct %s for a user with no collection permissions"
|
||||
% type(self)
|
||||
)
|
||||
elif len(self.collections) == 1:
|
||||
# don't show collection field if only one collection is available
|
||||
del self.fields["collection"]
|
||||
else:
|
||||
self.fields["collection"].queryset = self.collections
|
||||
|
||||
def save(self, commit=True):
|
||||
if len(self.collections) == 1:
|
||||
# populate the instance's collection field with the one available collection
|
||||
self.instance.collection = self.collections[0]
|
||||
|
||||
return super().save(commit=commit)
|
||||
|
||||
|
||||
class BaseGroupCollectionMemberPermissionFormSet(forms.BaseFormSet):
|
||||
"""
|
||||
A base formset class for managing GroupCollectionPermissions for a
|
||||
model with CollectionMember behaviour. Subclasses should provide attributes:
|
||||
permission_types - a list of (codename, short_label, long_label) tuples for the permissions
|
||||
being managed here
|
||||
permission_queryset - a queryset of Permission objects for the above permissions
|
||||
default_prefix - prefix to use on form fields if one is not specified in __init__
|
||||
template = template filename
|
||||
"""
|
||||
|
||||
def __init__(self, data=None, files=None, instance=None, prefix=None):
|
||||
if prefix is None:
|
||||
prefix = self.default_prefix
|
||||
|
||||
if instance is None:
|
||||
instance = Group()
|
||||
|
||||
if instance.pk is None:
|
||||
full_collection_permissions = []
|
||||
else:
|
||||
full_collection_permissions = (
|
||||
instance.collection_permissions.filter(
|
||||
permission__in=self.permission_queryset
|
||||
)
|
||||
.select_related("permission__content_type", "collection")
|
||||
.order_by("collection")
|
||||
)
|
||||
|
||||
self.instance = instance
|
||||
|
||||
initial_data = []
|
||||
|
||||
for collection, collection_permissions in groupby(
|
||||
full_collection_permissions,
|
||||
lambda cp: cp.collection,
|
||||
):
|
||||
initial_data.append(
|
||||
{
|
||||
"collection": collection,
|
||||
"permissions": [cp.permission for cp in collection_permissions],
|
||||
}
|
||||
)
|
||||
|
||||
super().__init__(data, files, initial=initial_data, prefix=prefix)
|
||||
for form in self.forms:
|
||||
form.fields["DELETE"].widget = forms.HiddenInput()
|
||||
|
||||
@property
|
||||
def empty_form(self):
|
||||
empty_form = super().empty_form
|
||||
empty_form.fields["DELETE"].widget = forms.HiddenInput()
|
||||
return empty_form
|
||||
|
||||
def clean(self):
|
||||
"""Checks that no two forms refer to the same collection object"""
|
||||
if any(self.errors):
|
||||
# Don't bother validating the formset unless each form is valid on its own
|
||||
return
|
||||
|
||||
collections = [
|
||||
form.cleaned_data["collection"]
|
||||
for form in self.forms
|
||||
# need to check for presence of 'collection' in cleaned_data,
|
||||
# because a completely blank form passes validation
|
||||
if form not in self.deleted_forms and "collection" in form.cleaned_data
|
||||
]
|
||||
if len(set(collections)) != len(collections):
|
||||
# collections list contains duplicates
|
||||
raise forms.ValidationError(
|
||||
_(
|
||||
"You cannot have multiple permission records for the same collection."
|
||||
)
|
||||
)
|
||||
|
||||
@transaction.atomic
|
||||
def save(self):
|
||||
if self.instance.pk is None:
|
||||
raise Exception(
|
||||
"Cannot save a GroupCollectionMemberPermissionFormSet "
|
||||
"for an unsaved group instance"
|
||||
)
|
||||
|
||||
# get a set of (collection, permission) tuples for all ticked permissions
|
||||
forms_to_save = [
|
||||
form
|
||||
for form in self.forms
|
||||
if form not in self.deleted_forms and "collection" in form.cleaned_data
|
||||
]
|
||||
|
||||
final_permission_records = set()
|
||||
for form in forms_to_save:
|
||||
for permission in form.cleaned_data["permissions"]:
|
||||
final_permission_records.add(
|
||||
(form.cleaned_data["collection"], permission)
|
||||
)
|
||||
|
||||
# fetch the group's existing collection permission records for this model,
|
||||
# and from that, build a list of records to be created / deleted
|
||||
permission_ids_to_delete = []
|
||||
permission_records_to_keep = set()
|
||||
|
||||
for cp in self.instance.collection_permissions.filter(
|
||||
permission__in=self.permission_queryset,
|
||||
):
|
||||
if (cp.collection, cp.permission) in final_permission_records:
|
||||
permission_records_to_keep.add((cp.collection, cp.permission))
|
||||
else:
|
||||
permission_ids_to_delete.append(cp.id)
|
||||
|
||||
self.instance.collection_permissions.filter(
|
||||
id__in=permission_ids_to_delete
|
||||
).delete()
|
||||
|
||||
permissions_to_add = final_permission_records - permission_records_to_keep
|
||||
GroupCollectionPermission.objects.bulk_create(
|
||||
[
|
||||
GroupCollectionPermission(
|
||||
group=self.instance, collection=collection, permission=permission
|
||||
)
|
||||
for (collection, permission) in permissions_to_add
|
||||
]
|
||||
)
|
||||
|
||||
def as_admin_panel(self):
|
||||
return render_to_string(
|
||||
self.template,
|
||||
{"formset": self},
|
||||
)
|
||||
|
||||
|
||||
def collection_member_permission_formset_factory(
|
||||
model, permission_types, template, default_prefix=None
|
||||
):
|
||||
permission_queryset = Permission.objects.filter(
|
||||
content_type__app_label=model._meta.app_label,
|
||||
codename__in=[
|
||||
codename for codename, short_label, long_label in permission_types
|
||||
],
|
||||
).select_related("content_type")
|
||||
|
||||
if default_prefix is None:
|
||||
default_prefix = "%s_permissions" % model._meta.model_name
|
||||
|
||||
class PermissionMultipleChoiceField(forms.ModelMultipleChoiceField):
|
||||
"""
|
||||
Allows the custom labels from ``permission_types`` to be applied to
|
||||
permission checkboxes for the ``CollectionMemberPermissionsForm`` below
|
||||
"""
|
||||
|
||||
def label_from_instance(self, obj):
|
||||
for codename, short_label, long_label in permission_types:
|
||||
if codename == obj.codename:
|
||||
return long_label
|
||||
return str(obj)
|
||||
|
||||
class CollectionMemberPermissionsForm(forms.Form):
|
||||
"""
|
||||
For a given model with CollectionMember behaviour,
|
||||
defines the permissions that are assigned to an entity
|
||||
(such as a group or user) for a specific collection
|
||||
"""
|
||||
|
||||
collection = CollectionChoiceField(
|
||||
label=_("Collection"),
|
||||
queryset=Collection.objects.all().prefetch_related("group_permissions"),
|
||||
empty_label=None,
|
||||
)
|
||||
permissions = PermissionMultipleChoiceField(
|
||||
queryset=permission_queryset,
|
||||
required=False,
|
||||
widget=forms.CheckboxSelectMultiple,
|
||||
)
|
||||
|
||||
GroupCollectionMemberPermissionFormSet = type(
|
||||
"GroupCollectionMemberPermissionFormSet",
|
||||
(BaseGroupCollectionMemberPermissionFormSet,),
|
||||
{
|
||||
"permission_types": permission_types,
|
||||
"permission_queryset": permission_queryset,
|
||||
"default_prefix": default_prefix,
|
||||
"template": template,
|
||||
},
|
||||
)
|
||||
|
||||
return forms.formset_factory(
|
||||
CollectionMemberPermissionsForm,
|
||||
formset=GroupCollectionMemberPermissionFormSet,
|
||||
extra=0,
|
||||
can_delete=True,
|
||||
)
|
||||
|
||||
|
||||
GroupCollectionManagementPermissionFormSet = (
|
||||
collection_member_permission_formset_factory(
|
||||
Collection,
|
||||
[
|
||||
("add_collection", _("Add"), _("Add collections")),
|
||||
("change_collection", _("Edit"), _("Edit collections")),
|
||||
("delete_collection", _("Delete"), _("Delete collections")),
|
||||
],
|
||||
"wagtailadmin/permissions/includes/collection_management_permissions_form.html",
|
||||
)
|
||||
)
|
||||
87
env/lib/python3.10/site-packages/wagtail/admin/forms/comments.py
vendored
Normal file
87
env/lib/python3.10/site-packages/wagtail/admin/forms/comments.py
vendored
Normal file
@@ -0,0 +1,87 @@
|
||||
from django.forms import BooleanField, ValidationError
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext as _
|
||||
from modelcluster.forms import BaseChildFormSet
|
||||
|
||||
from .models import WagtailAdminModelForm
|
||||
|
||||
|
||||
class CommentReplyForm(WagtailAdminModelForm):
|
||||
class Meta:
|
||||
fields = ("text",)
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
user = self.for_user
|
||||
|
||||
if not self.instance.pk:
|
||||
self.instance.user = user
|
||||
elif self.instance.user != user:
|
||||
# trying to edit someone else's comment reply
|
||||
if any(field for field in self.changed_data):
|
||||
# includes DELETION_FIELD_NAME, as users cannot delete each other's individual comment replies
|
||||
# if deleting a whole thread, this should be done by deleting the parent Comment instead
|
||||
self.add_error(
|
||||
None, ValidationError(_("You cannot edit another user's comment."))
|
||||
)
|
||||
return cleaned_data
|
||||
|
||||
|
||||
class CommentForm(WagtailAdminModelForm):
|
||||
"""
|
||||
This is designed to be subclassed and have the user overridden to enable user-based validation within the edit handler system
|
||||
"""
|
||||
|
||||
resolved = BooleanField(required=False)
|
||||
|
||||
class Meta:
|
||||
formsets = {
|
||||
"replies": {
|
||||
"form": CommentReplyForm,
|
||||
"inherit_kwargs": ["for_user"],
|
||||
}
|
||||
}
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
user = self.for_user
|
||||
|
||||
if not self.instance.pk:
|
||||
self.instance.user = user
|
||||
elif self.instance.user != user:
|
||||
# trying to edit someone else's comment
|
||||
if (
|
||||
any(
|
||||
field
|
||||
for field in self.changed_data
|
||||
if field not in ["resolved", "position", "contentpath"]
|
||||
)
|
||||
or cleaned_data["contentpath"].split(".")[0]
|
||||
!= self.instance.contentpath.split(".")[0]
|
||||
):
|
||||
# users can resolve each other's base comments and change their positions within a field, or move a comment between blocks in a StreamField
|
||||
self.add_error(
|
||||
None, ValidationError(_("You cannot edit another user's comment."))
|
||||
)
|
||||
return cleaned_data
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.cleaned_data.get("resolved", False):
|
||||
if not getattr(self.instance, "resolved_at"):
|
||||
self.instance.resolved_at = now()
|
||||
self.instance.resolved_by = self.for_user
|
||||
else:
|
||||
self.instance.resolved_by = None
|
||||
self.instance.resolved_at = None
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class CommentFormSet(BaseChildFormSet):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
valid_comment_ids = [
|
||||
comment.id
|
||||
for comment in self.queryset
|
||||
if comment.has_valid_contentpath(self.instance)
|
||||
]
|
||||
self.queryset = self.queryset.filter(id__in=valid_comment_ids)
|
||||
169
env/lib/python3.10/site-packages/wagtail/admin/forms/models.py
vendored
Normal file
169
env/lib/python3.10/site-packages/wagtail/admin/forms/models.py
vendored
Normal file
@@ -0,0 +1,169 @@
|
||||
import copy
|
||||
|
||||
from django import forms
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext as _
|
||||
from modelcluster.forms import ClusterForm, ClusterFormMetaclass, ClusterFormOptions
|
||||
from permissionedforms import (
|
||||
PermissionedForm,
|
||||
PermissionedFormMetaclass,
|
||||
PermissionedFormOptionsMixin,
|
||||
)
|
||||
from taggit.managers import TaggableManager
|
||||
|
||||
from wagtail.admin import widgets
|
||||
from wagtail.admin.forms.tags import TagField
|
||||
from wagtail.models import Page
|
||||
from wagtail.utils.registry import ModelFieldRegistry
|
||||
|
||||
# Define a registry of form field properties to override for a given model field
|
||||
registry = ModelFieldRegistry()
|
||||
|
||||
# Aliases to lookups in the overrides registry, for backwards compatibility
|
||||
FORM_FIELD_OVERRIDES = registry.values_by_class
|
||||
DIRECT_FORM_FIELD_OVERRIDES = registry.values_by_exact_class
|
||||
|
||||
|
||||
def register_form_field_override(
|
||||
db_field_class, to=None, override=None, exact_class=False
|
||||
):
|
||||
"""
|
||||
Define parameters for form fields to be used by WagtailAdminModelForm for a given
|
||||
database field.
|
||||
"""
|
||||
|
||||
if override is None:
|
||||
raise ImproperlyConfigured(
|
||||
"register_form_field_override must be passed an 'override' keyword argument"
|
||||
)
|
||||
|
||||
if to and db_field_class != models.ForeignKey:
|
||||
raise ImproperlyConfigured(
|
||||
"The 'to' argument on register_form_field_override is only valid for ForeignKey fields"
|
||||
)
|
||||
|
||||
registry.register(db_field_class, to=to, value=override, exact_class=exact_class)
|
||||
|
||||
|
||||
# Define built-in overrides
|
||||
|
||||
# Date / time fields
|
||||
register_form_field_override(
|
||||
models.DateField, override={"widget": widgets.AdminDateInput}
|
||||
)
|
||||
register_form_field_override(
|
||||
models.TimeField, override={"widget": widgets.AdminTimeInput}
|
||||
)
|
||||
register_form_field_override(
|
||||
models.DateTimeField, override={"widget": widgets.AdminDateTimeInput}
|
||||
)
|
||||
|
||||
# Auto-height text fields (defined as exact_class=True so that it doesn't take effect for RichTextField)
|
||||
register_form_field_override(
|
||||
models.TextField,
|
||||
override={"widget": widgets.AdminAutoHeightTextInput},
|
||||
exact_class=True,
|
||||
)
|
||||
|
||||
# Page chooser
|
||||
register_form_field_override(
|
||||
models.ForeignKey,
|
||||
to=Page,
|
||||
override=lambda db_field: {
|
||||
"widget": widgets.AdminPageChooser(target_models=[db_field.remote_field.model])
|
||||
},
|
||||
)
|
||||
|
||||
# Tag fields
|
||||
register_form_field_override(
|
||||
TaggableManager,
|
||||
override=(
|
||||
lambda db_field: {"form_class": TagField, "tag_model": db_field.related_model}
|
||||
),
|
||||
)
|
||||
|
||||
# Slug fields
|
||||
register_form_field_override(
|
||||
models.SlugField,
|
||||
override={"widget": widgets.SlugInput},
|
||||
)
|
||||
|
||||
|
||||
# Callback to allow us to override the default form fields provided for each model field.
|
||||
def formfield_for_dbfield(db_field, **kwargs):
|
||||
overrides = registry.get(db_field)
|
||||
if overrides:
|
||||
kwargs = dict(copy.deepcopy(overrides), **kwargs)
|
||||
|
||||
return db_field.formfield(**kwargs)
|
||||
|
||||
|
||||
class WagtailAdminModelFormOptions(PermissionedFormOptionsMixin, ClusterFormOptions):
|
||||
# Container for the options set in the inner 'class Meta' of a model form, supporting
|
||||
# extensions for both ClusterForm ('formsets') and PermissionedForm ('field_permissions').
|
||||
pass
|
||||
|
||||
|
||||
class WagtailAdminModelFormMetaclass(PermissionedFormMetaclass, ClusterFormMetaclass):
|
||||
options_class = WagtailAdminModelFormOptions
|
||||
|
||||
# set extra_form_count to 0, as we're creating extra forms in JS
|
||||
extra_form_count = 0
|
||||
|
||||
@classmethod
|
||||
def child_form(cls):
|
||||
return WagtailAdminModelForm
|
||||
|
||||
|
||||
class WagtailAdminModelForm(
|
||||
PermissionedForm, ClusterForm, metaclass=WagtailAdminModelFormMetaclass
|
||||
):
|
||||
def __init__(self, *args, **kwargs):
|
||||
# keep hold of the `for_user` kwarg as well as passing it on to PermissionedForm
|
||||
self.for_user = kwargs.get("for_user")
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
class Meta:
|
||||
formfield_callback = formfield_for_dbfield
|
||||
|
||||
|
||||
# Now, any model forms built off WagtailAdminModelForm instead of ModelForm should pick up
|
||||
# the nice form fields defined in FORM_FIELD_OVERRIDES.
|
||||
|
||||
|
||||
class WagtailAdminDraftStateFormMixin:
|
||||
@property
|
||||
def show_schedule_publishing_toggle(self):
|
||||
return "go_live_at" in self.__class__.base_fields
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Check scheduled publishing fields
|
||||
go_live_at = self.cleaned_data.get("go_live_at")
|
||||
expire_at = self.cleaned_data.get("expire_at")
|
||||
|
||||
# Go live must be before expire
|
||||
if go_live_at and expire_at:
|
||||
if go_live_at > expire_at:
|
||||
msg = _("Go live date/time must be before expiry date/time")
|
||||
self.add_error("go_live_at", forms.ValidationError(msg))
|
||||
self.add_error("expire_at", forms.ValidationError(msg))
|
||||
|
||||
# Expire at must be in the future
|
||||
if expire_at and expire_at < timezone.now():
|
||||
self.add_error(
|
||||
"expire_at",
|
||||
forms.ValidationError(_("Expiry date/time must be in the future")),
|
||||
)
|
||||
|
||||
# Don't allow an existing first_published_at to be unset by clearing the field
|
||||
if (
|
||||
"first_published_at" in self.cleaned_data
|
||||
and not self.cleaned_data["first_published_at"]
|
||||
):
|
||||
del self.cleaned_data["first_published_at"]
|
||||
|
||||
return self.cleaned_data
|
||||
276
env/lib/python3.10/site-packages/wagtail/admin/forms/pages.py
vendored
Normal file
276
env/lib/python3.10/site-packages/wagtail/admin/forms/pages.py
vendored
Normal file
@@ -0,0 +1,276 @@
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.utils.translation import gettext as _
|
||||
from django.utils.translation import ngettext
|
||||
|
||||
from wagtail.admin import widgets
|
||||
from wagtail.models import Page, PageViewRestriction
|
||||
|
||||
from .models import WagtailAdminModelForm
|
||||
from .view_restrictions import BaseViewRestrictionForm
|
||||
|
||||
|
||||
class CopyForm(forms.Form):
|
||||
def __init__(self, *args, **kwargs):
|
||||
# CopyPage must be passed a 'page' kwarg indicating the page to be copied
|
||||
self.page = kwargs.pop("page")
|
||||
self.user = kwargs.pop("user", None)
|
||||
can_publish = kwargs.pop("can_publish")
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields["new_title"] = forms.CharField(
|
||||
initial=self.page.title, label=_("New title")
|
||||
)
|
||||
allow_unicode = getattr(settings, "WAGTAIL_ALLOW_UNICODE_SLUGS", True)
|
||||
self.fields["new_slug"] = forms.SlugField(
|
||||
initial=self.page.slug,
|
||||
label=_("New slug"),
|
||||
allow_unicode=allow_unicode,
|
||||
widget=widgets.SlugInput,
|
||||
)
|
||||
self.fields["new_parent_page"] = forms.ModelChoiceField(
|
||||
initial=self.page.get_parent(),
|
||||
queryset=Page.objects.all(),
|
||||
widget=widgets.AdminPageChooser(can_choose_root=True, user_perms="copy_to"),
|
||||
label=_("New parent page"),
|
||||
help_text=_("This copy will be a child of this given parent page."),
|
||||
)
|
||||
pages_to_copy = self.page.get_descendants(inclusive=True)
|
||||
subpage_count = pages_to_copy.count() - 1
|
||||
if subpage_count > 0:
|
||||
self.fields["copy_subpages"] = forms.BooleanField(
|
||||
required=False,
|
||||
initial=True,
|
||||
label=_("Copy subpages"),
|
||||
help_text=ngettext(
|
||||
"This will copy %(count)s subpage.",
|
||||
"This will copy %(count)s subpages.",
|
||||
subpage_count,
|
||||
)
|
||||
% {"count": subpage_count},
|
||||
)
|
||||
|
||||
if can_publish:
|
||||
pages_to_publish_count = pages_to_copy.live().count()
|
||||
if pages_to_publish_count > 0:
|
||||
# In the specific case that there are no subpages, customise the field label and help text
|
||||
if subpage_count == 0:
|
||||
label = _("Publish copied page")
|
||||
help_text = _(
|
||||
"This page is live. Would you like to publish its copy as well?"
|
||||
)
|
||||
else:
|
||||
label = _("Publish copies")
|
||||
help_text = ngettext(
|
||||
"%(count)s of the pages being copied is live. Would you like to publish its copy?",
|
||||
"%(count)s of the pages being copied are live. Would you like to publish their copies?",
|
||||
pages_to_publish_count,
|
||||
) % {"count": pages_to_publish_count}
|
||||
|
||||
self.fields["publish_copies"] = forms.BooleanField(
|
||||
required=False, initial=False, label=label, help_text=help_text
|
||||
)
|
||||
|
||||
# Note that only users who can publish in the new parent page can create an alias.
|
||||
# This is because alias pages must always match their original page's state.
|
||||
self.fields["alias"] = forms.BooleanField(
|
||||
required=False,
|
||||
initial=False,
|
||||
label=_("Alias"),
|
||||
help_text=_("Keep the new pages updated with future changes"),
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
|
||||
# Make sure the slug isn't already in use
|
||||
slug = cleaned_data.get("new_slug")
|
||||
|
||||
# New parent page given in form or parent of source, if parent_page is empty
|
||||
parent_page = cleaned_data.get("new_parent_page") or self.page.get_parent()
|
||||
|
||||
# check if user is allowed to create a page at given location.
|
||||
if not parent_page.permissions_for_user(self.user).can_add_subpage():
|
||||
self._errors["new_parent_page"] = self.error_class(
|
||||
[
|
||||
_('You do not have permission to copy to page "%(page_title)s"')
|
||||
% {
|
||||
"page_title": parent_page.specific_deferred.get_admin_display_title()
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
# Count the pages with the same slug within the context of our copy's parent page
|
||||
if slug and parent_page.get_children().filter(slug=slug).count():
|
||||
self._errors["new_slug"] = self.error_class(
|
||||
[
|
||||
_(
|
||||
'This slug is already in use within the context of its parent page "%(parent_page_title)s"'
|
||||
)
|
||||
% {"parent_page_title": parent_page}
|
||||
]
|
||||
)
|
||||
# The slug is no longer valid, hence remove it from cleaned_data
|
||||
del cleaned_data["new_slug"]
|
||||
|
||||
# Don't allow recursive copies into self
|
||||
if cleaned_data.get("copy_subpages") and (
|
||||
self.page == parent_page or parent_page.is_descendant_of(self.page)
|
||||
):
|
||||
self._errors["new_parent_page"] = self.error_class(
|
||||
[_("You cannot copy a page into itself when copying subpages")]
|
||||
)
|
||||
|
||||
return cleaned_data
|
||||
|
||||
|
||||
class PageViewRestrictionForm(BaseViewRestrictionForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
# get the list of private page options from the page
|
||||
private_page_options = kwargs.pop("private_page_options", [])
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if not getattr(settings, "WAGTAIL_PRIVATE_PAGE_OPTIONS", {}).get(
|
||||
"SHARED_PASSWORD", True
|
||||
):
|
||||
self.fields["restriction_type"].choices = [
|
||||
choice
|
||||
for choice in PageViewRestriction.RESTRICTION_CHOICES
|
||||
if choice[0] != PageViewRestriction.PASSWORD
|
||||
]
|
||||
del self.fields["password"]
|
||||
# Remove the fields that are not allowed for the page
|
||||
self.fields["restriction_type"].choices = [
|
||||
choice
|
||||
for choice in self.fields["restriction_type"].choices
|
||||
if choice[0] in private_page_options
|
||||
or choice[0] == PageViewRestriction.NONE
|
||||
]
|
||||
|
||||
class Meta:
|
||||
model = PageViewRestriction
|
||||
fields = ("restriction_type", "password", "groups")
|
||||
|
||||
|
||||
class WagtailAdminPageForm(WagtailAdminModelForm):
|
||||
comment_notifications = forms.BooleanField(
|
||||
widget=forms.CheckboxInput(), required=False
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
data=None,
|
||||
files=None,
|
||||
parent_page=None,
|
||||
subscription=None,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
self.subscription = subscription
|
||||
|
||||
initial = kwargs.pop("initial", {})
|
||||
if self.subscription:
|
||||
initial["comment_notifications"] = subscription.comment_notifications
|
||||
|
||||
super().__init__(data, files, *args, initial=initial, **kwargs)
|
||||
|
||||
self.parent_page = parent_page
|
||||
|
||||
if not self.show_comments_toggle:
|
||||
del self.fields["comment_notifications"]
|
||||
|
||||
@property
|
||||
def show_comments_toggle(self):
|
||||
return "comments" in self.__class__.formsets
|
||||
|
||||
def save(self, commit=True):
|
||||
# Save comment notifications updates to PageSubscription
|
||||
if self.show_comments_toggle and self.subscription:
|
||||
self.subscription.comment_notifications = self.cleaned_data[
|
||||
"comment_notifications"
|
||||
]
|
||||
if commit:
|
||||
self.subscription.save()
|
||||
|
||||
return super().save(commit=commit)
|
||||
|
||||
def is_valid(self):
|
||||
comments = self.formsets.get("comments")
|
||||
# Remove the comments formset if the management form is invalid
|
||||
if comments and not comments.management_form.is_valid():
|
||||
del self.formsets["comments"]
|
||||
return super().is_valid()
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
if "slug" in self.cleaned_data:
|
||||
page_slug = cleaned_data["slug"]
|
||||
if not Page._slug_is_available(page_slug, self.parent_page, self.instance):
|
||||
self.add_error(
|
||||
"slug",
|
||||
forms.ValidationError(
|
||||
_(
|
||||
"The slug '%(page_slug)s' is already in use within the parent page"
|
||||
)
|
||||
% {"page_slug": page_slug}
|
||||
),
|
||||
)
|
||||
|
||||
return cleaned_data
|
||||
|
||||
|
||||
class MoveForm(forms.Form):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.page_to_move = kwargs.pop("page_to_move")
|
||||
self.target_parent_models = kwargs.pop("target_parent_models")
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.fields["new_parent_page"] = forms.ModelChoiceField(
|
||||
initial=self.page_to_move.get_parent(),
|
||||
queryset=Page.objects.all(),
|
||||
widget=widgets.AdminPageMoveChooser(
|
||||
can_choose_root=True,
|
||||
user_perms="move_to",
|
||||
target_models=self.target_parent_models,
|
||||
pages_to_move=[self.page_to_move.pk],
|
||||
),
|
||||
label=_("New parent page"),
|
||||
help_text=_("Select a new parent for this page."),
|
||||
)
|
||||
|
||||
|
||||
class ParentChooserForm(forms.Form):
|
||||
def __init__(self, child_page_type, user, *args, **kwargs):
|
||||
self.child_page_type = child_page_type
|
||||
self.user = user
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields["parent_page"] = forms.ModelChoiceField(
|
||||
queryset=Page.objects.all(),
|
||||
widget=widgets.AdminPageChooser(
|
||||
target_models=self.child_page_type.allowed_parent_page_models(),
|
||||
can_choose_root=True,
|
||||
user_perms="add_subpage",
|
||||
),
|
||||
label=_("Parent page"),
|
||||
help_text=_("The new page will be a child of this given parent page."),
|
||||
)
|
||||
|
||||
def clean_parent_page(self):
|
||||
parent_page = self.cleaned_data["parent_page"].specific_deferred
|
||||
if not parent_page.permissions_for_user(self.user).can_add_subpage():
|
||||
raise forms.ValidationError(
|
||||
_('You do not have permission to create a page under "%(page_title)s".')
|
||||
% {"page_title": parent_page.get_admin_display_title()}
|
||||
)
|
||||
if not self.child_page_type.can_create_at(parent_page):
|
||||
raise forms.ValidationError(
|
||||
_(
|
||||
'You cannot create a page of type "%(page_type)s" under "%(page_title)s".'
|
||||
)
|
||||
% {
|
||||
"page_type": self.child_page_type.get_verbose_name(),
|
||||
"page_title": parent_page.get_admin_display_title(),
|
||||
}
|
||||
)
|
||||
return parent_page
|
||||
19
env/lib/python3.10/site-packages/wagtail/admin/forms/search.py
vendored
Normal file
19
env/lib/python3.10/site-packages/wagtail/admin/forms/search.py
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
from django import forms
|
||||
from django.utils.translation import gettext as _
|
||||
from django.utils.translation import gettext_lazy
|
||||
|
||||
|
||||
class SearchForm(forms.Form):
|
||||
def __init__(self, *args, **kwargs):
|
||||
placeholder = kwargs.pop("placeholder", _("Search…"))
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields["q"].widget.attrs = {
|
||||
"placeholder": placeholder,
|
||||
"data-w-swap-target": "input",
|
||||
}
|
||||
|
||||
q = forms.CharField(
|
||||
label=gettext_lazy("Search term"),
|
||||
widget=forms.TextInput(),
|
||||
required=False,
|
||||
)
|
||||
68
env/lib/python3.10/site-packages/wagtail/admin/forms/tags.py
vendored
Normal file
68
env/lib/python3.10/site-packages/wagtail/admin/forms/tags.py
vendored
Normal file
@@ -0,0 +1,68 @@
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from taggit.forms import TagField as TaggitTagField
|
||||
from taggit.models import Tag, TagBase
|
||||
|
||||
from wagtail.admin.widgets import AdminTagWidget
|
||||
|
||||
|
||||
def validate_tag_length(
|
||||
value, max_tag_length=TagBase._meta.get_field("name").max_length
|
||||
):
|
||||
if not value:
|
||||
return
|
||||
value_too_long = ""
|
||||
for val in value:
|
||||
if len(val) > max_tag_length:
|
||||
if value_too_long:
|
||||
value_too_long += ", "
|
||||
value_too_long += val
|
||||
if value_too_long:
|
||||
raise ValidationError(
|
||||
_("Tag(s) %(value_too_long)s are over %(max_tag_length)d characters")
|
||||
% {
|
||||
"value_too_long": value_too_long,
|
||||
"max_tag_length": max_tag_length,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class TagField(TaggitTagField):
|
||||
"""
|
||||
Extends taggit's TagField with the option to prevent creating tags that do not already exist
|
||||
"""
|
||||
|
||||
widget = AdminTagWidget
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.tag_model = kwargs.pop("tag_model", None)
|
||||
self.free_tagging = kwargs.pop("free_tagging", None)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# pass on tag_model and free_tagging kwargs to the widget,
|
||||
# if (and only if) they have been passed explicitly here.
|
||||
# Otherwise, set default values for clean() to use
|
||||
if self.tag_model is None:
|
||||
self.tag_model = Tag
|
||||
else:
|
||||
self.widget.tag_model = self.tag_model
|
||||
|
||||
if self.free_tagging is None:
|
||||
self.free_tagging = getattr(self.tag_model, "free_tagging", True)
|
||||
else:
|
||||
self.widget.free_tagging = self.free_tagging
|
||||
|
||||
def clean(self, value):
|
||||
value = super().clean(value)
|
||||
validate_tag_length(value, self.tag_model.name.field.max_length)
|
||||
|
||||
if not self.free_tagging:
|
||||
# filter value to just the tags that already exist in tag_model
|
||||
value = list(
|
||||
self.tag_model.objects.filter(name__in=value).values_list(
|
||||
"name", flat=True
|
||||
)
|
||||
)
|
||||
|
||||
return value
|
||||
44
env/lib/python3.10/site-packages/wagtail/admin/forms/view_restrictions.py
vendored
Normal file
44
env/lib/python3.10/site-packages/wagtail/admin/forms/view_restrictions.py
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
from django import forms
|
||||
from django.contrib.auth.models import Group
|
||||
from django.utils.translation import gettext as _
|
||||
from django.utils.translation import gettext_lazy
|
||||
|
||||
from wagtail.models import BaseViewRestriction
|
||||
|
||||
|
||||
class BaseViewRestrictionForm(forms.ModelForm):
|
||||
restriction_type = forms.ChoiceField(
|
||||
label=gettext_lazy("Visibility"),
|
||||
choices=BaseViewRestriction.RESTRICTION_CHOICES,
|
||||
widget=forms.RadioSelect,
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.fields["groups"].widget = forms.CheckboxSelectMultiple()
|
||||
self.fields["groups"].queryset = Group.objects.all()
|
||||
|
||||
def clean_password(self):
|
||||
password = self.cleaned_data.get("password")
|
||||
if (
|
||||
self.cleaned_data.get("restriction_type") == BaseViewRestriction.PASSWORD
|
||||
and not password
|
||||
):
|
||||
raise forms.ValidationError(_("This field is required."), code="invalid")
|
||||
return password
|
||||
|
||||
def clean_groups(self):
|
||||
groups = self.cleaned_data.get("groups")
|
||||
if (
|
||||
self.cleaned_data.get("restriction_type") == BaseViewRestriction.GROUPS
|
||||
and not groups
|
||||
):
|
||||
raise forms.ValidationError(
|
||||
_("Please select at least one group."), code="invalid"
|
||||
)
|
||||
return groups
|
||||
|
||||
class Meta:
|
||||
model = BaseViewRestriction
|
||||
fields = ("restriction_type", "password", "groups")
|
||||
326
env/lib/python3.10/site-packages/wagtail/admin/forms/workflows.py
vendored
Normal file
326
env/lib/python3.10/site-packages/wagtail/admin/forms/workflows.py
vendored
Normal file
@@ -0,0 +1,326 @@
|
||||
from django import forms
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ImproperlyConfigured, ValidationError
|
||||
from django.db import transaction
|
||||
from django.db.models import Q
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.text import capfirst
|
||||
from django.utils.translation import gettext as _
|
||||
from django.utils.translation import gettext_lazy
|
||||
|
||||
from wagtail.admin import widgets
|
||||
from wagtail.admin.forms import WagtailAdminModelForm
|
||||
from wagtail.admin.panels import FieldPanel, InlinePanel, ObjectList
|
||||
from wagtail.admin.widgets.workflows import AdminTaskChooser
|
||||
from wagtail.coreutils import get_content_type_label, get_model_string
|
||||
from wagtail.models import Page, Task, Workflow, WorkflowContentType, WorkflowPage
|
||||
from wagtail.snippets.models import get_workflow_enabled_models
|
||||
|
||||
|
||||
class TaskChooserSearchForm(forms.Form):
|
||||
q = forms.CharField(
|
||||
label=gettext_lazy("Search term"), widget=forms.TextInput(), required=False
|
||||
)
|
||||
|
||||
def __init__(self, *args, task_type_choices=None, **kwargs):
|
||||
placeholder = kwargs.pop("placeholder", _("Search…"))
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields["q"].widget.attrs = {"placeholder": placeholder}
|
||||
|
||||
# Add task type filter if there is more than one task type option
|
||||
if task_type_choices and len(task_type_choices) > 1:
|
||||
self.fields["task_type"] = forms.ChoiceField(
|
||||
choices=(
|
||||
# Append an "All types" choice to the beginning
|
||||
[(None, _("All types"))]
|
||||
# The task type choices that are passed in use the models as values, we need
|
||||
# to convert these to something that can be represented in HTML
|
||||
+ [
|
||||
(get_model_string(model), verbose_name)
|
||||
for model, verbose_name in task_type_choices
|
||||
]
|
||||
),
|
||||
required=False,
|
||||
)
|
||||
|
||||
# Save a mapping of task_type values back to the model that we can reference later
|
||||
self.task_type_choices = {
|
||||
get_model_string(model): model for model, verbose_name in task_type_choices
|
||||
}
|
||||
|
||||
def is_searching(self):
|
||||
"""
|
||||
Returns True if the user typed a search query
|
||||
"""
|
||||
return self.is_valid() and bool(self.cleaned_data.get("q"))
|
||||
|
||||
@cached_property
|
||||
def task_model(self):
|
||||
"""
|
||||
Returns the selected task model.
|
||||
|
||||
This looks for the task model in the following order:
|
||||
1) If there's only one task model option, return it
|
||||
2) If a task model has been selected, return it
|
||||
3) Return the generic Task model
|
||||
"""
|
||||
models = list(self.task_type_choices.values())
|
||||
if len(models) == 1:
|
||||
return models[0]
|
||||
|
||||
elif self.is_valid():
|
||||
model_name = self.cleaned_data.get("task_type")
|
||||
if model_name and model_name in self.task_type_choices:
|
||||
return self.task_type_choices[model_name]
|
||||
|
||||
return Task
|
||||
|
||||
def specific_task_model_selected(self):
|
||||
return self.task_model is not Task
|
||||
|
||||
|
||||
class WorkflowPageForm(forms.ModelForm):
|
||||
page = forms.ModelChoiceField(
|
||||
queryset=Page.objects.all(),
|
||||
widget=widgets.AdminPageChooser(target_models=[Page], can_choose_root=True),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = WorkflowPage
|
||||
fields = ["page"]
|
||||
|
||||
def clean(self):
|
||||
page = self.cleaned_data.get("page")
|
||||
try:
|
||||
existing_workflow = page.workflowpage.workflow
|
||||
if not self.errors and existing_workflow != self.cleaned_data["workflow"]:
|
||||
# If the form has no errors, Page has an existing Workflow assigned, that Workflow is not
|
||||
# the selected Workflow, and overwrite_existing is not True, add a new error. This should be used to
|
||||
# trigger the confirmation message in the view. This is why this error is only added if there are no
|
||||
# other errors - confirmation should be the final step.
|
||||
self.add_error(
|
||||
"page",
|
||||
ValidationError(
|
||||
_(
|
||||
"This page already has workflow '%(workflow_name)s' assigned."
|
||||
)
|
||||
% {"workflow_name": existing_workflow},
|
||||
code="existing_workflow",
|
||||
),
|
||||
)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
def save(self, commit=False):
|
||||
page = self.cleaned_data["page"]
|
||||
|
||||
if commit:
|
||||
WorkflowPage.objects.update_or_create(
|
||||
page=page,
|
||||
defaults={"workflow": self.cleaned_data["workflow"]},
|
||||
)
|
||||
|
||||
|
||||
class BaseWorkflowPagesFormSet(forms.BaseInlineFormSet):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
for form in self.forms:
|
||||
form.fields["DELETE"].widget = forms.HiddenInput()
|
||||
|
||||
@property
|
||||
def empty_form(self):
|
||||
empty_form = super().empty_form
|
||||
empty_form.fields["DELETE"].widget = forms.HiddenInput()
|
||||
return empty_form
|
||||
|
||||
def clean(self):
|
||||
"""Checks that no two forms refer to the same page object"""
|
||||
if any(self.errors):
|
||||
# Don't bother validating the formset unless each form is valid on its own
|
||||
return
|
||||
|
||||
pages = [
|
||||
form.cleaned_data["page"]
|
||||
for form in self.forms
|
||||
# need to check for presence of 'page' in cleaned_data,
|
||||
# because a completely blank form passes validation
|
||||
if form not in self.deleted_forms and "page" in form.cleaned_data
|
||||
]
|
||||
if len(set(pages)) != len(pages):
|
||||
# pages list contains duplicates
|
||||
raise forms.ValidationError(
|
||||
_("You cannot assign this workflow to the same page multiple times.")
|
||||
)
|
||||
|
||||
|
||||
class WorkflowContentTypeForm(forms.Form):
|
||||
class ContentTypeMultipleChoiceField(forms.ModelMultipleChoiceField):
|
||||
def label_from_instance(self, obj):
|
||||
return get_content_type_label(obj)
|
||||
|
||||
class CheckboxSelectMultiple(forms.CheckboxSelectMultiple):
|
||||
"""Custom CheckboxSelectMultiple widget that renders errors for each content type ID"""
|
||||
|
||||
option_template_name = (
|
||||
"wagtailadmin/workflows/includes/workflow_content_types_checkbox.html"
|
||||
)
|
||||
|
||||
def get_errors_by_id(self, errors):
|
||||
errors_by_id = {}
|
||||
for error in errors.as_data():
|
||||
ct_id = error.params and error.params.get("content_type_id")
|
||||
errors_by_id.setdefault(ct_id, []).append(error)
|
||||
return errors_by_id
|
||||
|
||||
def render_with_errors(
|
||||
self, name, value, attrs=None, renderer=None, errors=None
|
||||
):
|
||||
context = {
|
||||
**self.get_context(name, value, attrs),
|
||||
"errors_by_id": self.get_errors_by_id(errors),
|
||||
}
|
||||
return self._render(self.template_name, context, renderer)
|
||||
|
||||
content_types = ContentTypeMultipleChoiceField(
|
||||
queryset=ContentType.objects.none(),
|
||||
widget=CheckboxSelectMultiple(),
|
||||
required=False,
|
||||
)
|
||||
|
||||
def __init__(self, *args, workflow=None, **kwargs):
|
||||
self.workflow = workflow
|
||||
if workflow and "initial" not in kwargs:
|
||||
kwargs["initial"] = {"content_types": workflow.workflow_content_types.all()}
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Start with an always-false query, as Django can optimise it by
|
||||
# returning an empty queryset without running any database queries.
|
||||
workflow_enabled_q = Q(pk__in=[])
|
||||
|
||||
# Then union the query for each workflow-enabled model.
|
||||
for model in get_workflow_enabled_models():
|
||||
workflow_enabled_q |= Q(
|
||||
app_label=model._meta.app_label, model=model._meta.model_name
|
||||
)
|
||||
|
||||
self.fields["content_types"].queryset = ContentType.objects.filter(
|
||||
workflow_enabled_q
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
content_types = self.cleaned_data.get("content_types")
|
||||
if not content_types:
|
||||
return
|
||||
|
||||
existing_assignments = WorkflowContentType.objects.filter(
|
||||
content_type__in=content_types,
|
||||
workflow__active=True,
|
||||
).exclude(workflow=self.workflow)
|
||||
for assignment in existing_assignments:
|
||||
self.add_error(
|
||||
"content_types",
|
||||
ValidationError(
|
||||
_(
|
||||
"Snippet '%(content_type)s' already has workflow '%(workflow_name)s' assigned."
|
||||
)
|
||||
% {
|
||||
"content_type": capfirst(assignment.content_type.name),
|
||||
"workflow_name": assignment.workflow,
|
||||
},
|
||||
code="existing_workflow_content_type",
|
||||
params={"content_type_id": assignment.content_type_id},
|
||||
),
|
||||
)
|
||||
|
||||
def save(self, commit=True):
|
||||
if not commit:
|
||||
return
|
||||
|
||||
content_types = self.cleaned_data["content_types"]
|
||||
|
||||
with transaction.atomic():
|
||||
# Remove any content types that are no longer selected
|
||||
WorkflowContentType.objects.filter(workflow=self.workflow).exclude(
|
||||
content_type__in=content_types
|
||||
).delete()
|
||||
|
||||
# Add any new content types, ignoring conflicts with existing ones
|
||||
# to avoid additional query for existing content types
|
||||
objects = [
|
||||
WorkflowContentType(workflow=self.workflow, content_type=ct)
|
||||
for ct in content_types
|
||||
]
|
||||
WorkflowContentType.objects.bulk_create(objects, ignore_conflicts=True)
|
||||
|
||||
|
||||
WorkflowPagesFormSet = forms.inlineformset_factory(
|
||||
Workflow,
|
||||
WorkflowPage,
|
||||
form=WorkflowPageForm,
|
||||
formset=BaseWorkflowPagesFormSet,
|
||||
extra=1,
|
||||
can_delete=True,
|
||||
fields=["page"],
|
||||
)
|
||||
|
||||
|
||||
class BaseTaskForm(forms.ModelForm):
|
||||
pass
|
||||
|
||||
|
||||
def get_task_form_class(task_model, for_edit=False):
|
||||
"""
|
||||
Generates a form class for the given task model.
|
||||
|
||||
If the form is to edit an existing task, set for_edit to True. This applies
|
||||
the readonly restrictions on fields defined in admin_form_readonly_on_edit_fields.
|
||||
"""
|
||||
fields = task_model.admin_form_fields
|
||||
|
||||
form_class = forms.modelform_factory(
|
||||
task_model,
|
||||
form=BaseTaskForm,
|
||||
fields=fields,
|
||||
widgets=getattr(task_model, "admin_form_widgets", {}),
|
||||
)
|
||||
|
||||
if for_edit:
|
||||
for field_name in getattr(task_model, "admin_form_readonly_on_edit_fields", []):
|
||||
if field_name not in form_class.base_fields:
|
||||
raise ImproperlyConfigured(
|
||||
"`%s.admin_form_readonly_on_edit_fields` contains the field "
|
||||
"'%s' that doesn't exist. Did you forget to add "
|
||||
"it to `%s.admin_form_fields`?"
|
||||
% (task_model.__name__, field_name, task_model.__name__)
|
||||
)
|
||||
|
||||
form_class.base_fields[field_name].disabled = True
|
||||
|
||||
return form_class
|
||||
|
||||
|
||||
def get_workflow_edit_handler():
|
||||
"""
|
||||
Returns an edit handler which provides the "name" and "tasks" fields for workflow.
|
||||
"""
|
||||
# Note. It's a bit of a hack that we use edit handlers here. Ideally, it should be
|
||||
# made easier to reuse the inline panel templates for any formset.
|
||||
# Since this form is internal, we're OK with this for now. We might want to revisit
|
||||
# this decision later if we decide to allow custom fields on Workflows.
|
||||
|
||||
panels = [
|
||||
FieldPanel("name", heading=_("Give your workflow a name")),
|
||||
InlinePanel(
|
||||
"workflow_tasks",
|
||||
[
|
||||
FieldPanel("task", widget=AdminTaskChooser(show_clear_link=False)),
|
||||
],
|
||||
heading=_("Add tasks to your workflow"),
|
||||
label=_("Task"),
|
||||
icon="thumbtack",
|
||||
),
|
||||
]
|
||||
edit_handler = ObjectList(panels, base_form_class=WagtailAdminModelForm)
|
||||
return edit_handler.bind_to_model(Workflow)
|
||||
Reference in New Issue
Block a user