1from django import forms
2from django.conf import settings
3from django.utils import timezone
4from django.utils.translation import gettext as _
5from django.utils.translation import ngettext
6
7from wagtail.admin import widgets
8from wagtail.core.models import Page, PageViewRestriction
9
10from .models import WagtailAdminModelForm
11from .view_restrictions import BaseViewRestrictionForm
12
13
14class CopyForm(forms.Form):
15    def __init__(self, *args, **kwargs):
16        # CopyPage must be passed a 'page' kwarg indicating the page to be copied
17        self.page = kwargs.pop('page')
18        self.user = kwargs.pop('user', None)
19        can_publish = kwargs.pop('can_publish')
20        super().__init__(*args, **kwargs)
21        self.fields['new_title'] = forms.CharField(initial=self.page.title, label=_("New title"))
22        allow_unicode = getattr(settings, 'WAGTAIL_ALLOW_UNICODE_SLUGS', True)
23        self.fields['new_slug'] = forms.SlugField(initial=self.page.slug, label=_("New slug"), allow_unicode=allow_unicode)
24        self.fields['new_parent_page'] = forms.ModelChoiceField(
25            initial=self.page.get_parent(),
26            queryset=Page.objects.all(),
27            widget=widgets.AdminPageChooser(can_choose_root=True, user_perms='copy_to'),
28            label=_("New parent page"),
29            help_text=_("This copy will be a child of this given parent page.")
30        )
31        pages_to_copy = self.page.get_descendants(inclusive=True)
32        subpage_count = pages_to_copy.count() - 1
33        if subpage_count > 0:
34            self.fields['copy_subpages'] = forms.BooleanField(
35                required=False, initial=True, label=_("Copy subpages"),
36                help_text=ngettext(
37                    "This will copy %(count)s subpage.",
38                    "This will copy %(count)s subpages.",
39                    subpage_count) % {'count': subpage_count})
40
41        if can_publish:
42            pages_to_publish_count = pages_to_copy.live().count()
43            if pages_to_publish_count > 0:
44                # In the specific case that there are no subpages, customise the field label and help text
45                if subpage_count == 0:
46                    label = _("Publish copied page")
47                    help_text = _("This page is live. Would you like to publish its copy as well?")
48                else:
49                    label = _("Publish copies")
50                    help_text = ngettext(
51                        "%(count)s of the pages being copied is live. Would you like to publish its copy?",
52                        "%(count)s of the pages being copied are live. Would you like to publish their copies?",
53                        pages_to_publish_count) % {'count': pages_to_publish_count}
54
55                self.fields['publish_copies'] = forms.BooleanField(
56                    required=False, initial=True, label=label, help_text=help_text
57                )
58
59            # Note that only users who can publish in the new parent page can create an alias.
60            # This is because alias pages must always match their original page's state.
61            self.fields['alias'] = forms.BooleanField(
62                required=False, initial=False, label=_("Alias"),
63                help_text=_("Keep the new pages updated with future changes")
64            )
65
66    def clean(self):
67        cleaned_data = super().clean()
68
69        # Make sure the slug isn't already in use
70        slug = cleaned_data.get('new_slug')
71
72        # New parent page given in form or parent of source, if parent_page is empty
73        parent_page = cleaned_data.get('new_parent_page') or self.page.get_parent()
74
75        # check if user is allowed to create a page at given location.
76        if not parent_page.permissions_for_user(self.user).can_add_subpage():
77            self._errors['new_parent_page'] = self.error_class([
78                _("You do not have permission to copy to page \"%(page_title)s\"") % {'page_title': parent_page.specific_deferred.get_admin_display_title()}
79            ])
80
81        # Count the pages with the same slug within the context of our copy's parent page
82        if slug and parent_page.get_children().filter(slug=slug).count():
83            self._errors['new_slug'] = self.error_class(
84                [_("This slug is already in use within the context of its parent page \"%s\"") % parent_page]
85            )
86            # The slug is no longer valid, hence remove it from cleaned_data
87            del cleaned_data['new_slug']
88
89        # Don't allow recursive copies into self
90        if cleaned_data.get('copy_subpages') and (self.page == parent_page or parent_page.is_descendant_of(self.page)):
91            self._errors['new_parent_page'] = self.error_class(
92                [_("You cannot copy a page into itself when copying subpages")]
93            )
94
95        return cleaned_data
96
97
98class PageViewRestrictionForm(BaseViewRestrictionForm):
99
100    class Meta:
101        model = PageViewRestriction
102        fields = ('restriction_type', 'password', 'groups')
103
104
105class WagtailAdminPageForm(WagtailAdminModelForm):
106    comment_notifications = forms.BooleanField(widget=forms.CheckboxInput(), required=False)
107
108    # Could be set to False by a subclass constructed by TabbedInterface
109    show_comments_toggle = True
110
111    class Meta:
112        # (dealing with Treebeard's tree-related fields that really should have
113        # been editable=False)
114        exclude = ['content_type', 'path', 'depth', 'numchild']
115
116    def __init__(self, data=None, files=None, parent_page=None, subscription=None, *args, **kwargs):
117        self.subscription = subscription
118
119        initial = kwargs.pop('initial', {})
120        if self.subscription:
121            initial['comment_notifications'] = subscription.comment_notifications
122
123        super().__init__(data, files, *args, initial=initial, **kwargs)
124
125        self.parent_page = parent_page
126
127        if not self.show_comments_toggle:
128            del self.fields['comment_notifications']
129
130    def save(self, commit=True):
131        # Save comment notifications updates to PageSubscription
132        if self.show_comments_toggle and self.subscription:
133            self.subscription.comment_notifications = self.cleaned_data['comment_notifications']
134            if commit:
135                self.subscription.save()
136
137        return super().save(commit=commit)
138
139    def is_valid(self):
140        comments = self.formsets.get('comments')
141        # Remove the comments formset if the management form is invalid
142        if comments:
143            try:
144                # As of Django 3.2, this no longer raises an error
145                has_form = comments.management_form.is_valid()
146            except forms.ValidationError:
147                has_form = False
148            if not has_form:
149                del self.formsets['comments']
150        return super().is_valid()
151
152    def clean(self):
153        cleaned_data = super().clean()
154        if 'slug' in self.cleaned_data:
155            if not Page._slug_is_available(
156                cleaned_data['slug'], self.parent_page, self.instance
157            ):
158                self.add_error('slug', forms.ValidationError(_("This slug is already in use")))
159
160        # Check scheduled publishing fields
161        go_live_at = cleaned_data.get('go_live_at')
162        expire_at = cleaned_data.get('expire_at')
163
164        # Go live must be before expire
165        if go_live_at and expire_at:
166            if go_live_at > expire_at:
167                msg = _('Go live date/time must be before expiry date/time')
168                self.add_error('go_live_at', forms.ValidationError(msg))
169                self.add_error('expire_at', forms.ValidationError(msg))
170
171        # Expire at must be in the future
172        if expire_at and expire_at < timezone.now():
173            self.add_error('expire_at', forms.ValidationError(_('Expiry date/time must be in the future')))
174
175        # Don't allow an existing first_published_at to be unset by clearing the field
176        if 'first_published_at' in cleaned_data and not cleaned_data['first_published_at']:
177            del cleaned_data['first_published_at']
178
179        return cleaned_data
180