1# -*- coding: utf-8 -*-
2from django import forms
3from django.apps import apps
4from django.contrib.auth import get_user_model, get_permission_codename
5from django.contrib.auth.models import Permission
6from django.contrib.contenttypes.models import ContentType
7from django.contrib.sites.models import Site
8from django.core.exceptions import ValidationError, ObjectDoesNotExist
9from django.forms.utils import ErrorList
10from django.forms.widgets import HiddenInput
11from django.template.defaultfilters import slugify
12from django.utils.encoding import force_text
13from django.utils.translation import ugettext, ugettext_lazy as _
14
15from cms import api
16from cms.apphook_pool import apphook_pool
17from cms.cache.permissions import clear_permission_cache
18from cms.exceptions import PluginLimitReached
19from cms.extensions import extension_pool
20from cms.constants import PAGE_TYPES_ID, PUBLISHER_STATE_DIRTY, ROOT_USER_LEVEL
21from cms.forms.validators import validate_relative_url, validate_url_uniqueness
22from cms.forms.widgets import UserSelectAdminWidget, AppHookSelect, ApplicationConfigSelect
23from cms.models import (CMSPlugin, Page, PageType, PagePermission, PageUser, PageUserGroup, Title,
24                        Placeholder, GlobalPagePermission, TreeNode)
25from cms.models.permissionmodels import User
26from cms.plugin_pool import plugin_pool
27from cms.signals.apphook import set_restart_trigger
28from cms.utils.conf import get_cms_setting
29from cms.utils.compat.forms import UserChangeForm
30from cms.utils.i18n import get_language_list, get_language_object
31from cms.utils.permissions import (
32    get_current_user,
33    get_subordinate_users,
34    get_subordinate_groups,
35    get_user_permission_level,
36)
37from menus.menu_pool import menu_pool
38
39
40def get_permission_accessor(obj):
41    User = get_user_model()
42
43    if isinstance(obj, (PageUser, User,)):
44        rel_name = 'user_permissions'
45    else:
46        rel_name = 'permissions'
47    return getattr(obj, rel_name)
48
49
50def get_page_changed_by_filter_choices():
51    # This is not site-aware
52    # Been like this forever
53    # Would be nice for it to filter out by site
54    values = (
55        Page
56        .objects
57        .filter(publisher_is_draft=True)
58        .distinct()
59        .order_by('changed_by')
60        .values_list('changed_by', flat=True)
61    )
62
63    yield ('', _('All'))
64
65    for value in values:
66        yield (value, value)
67
68
69def get_page_template_filter_choices():
70    yield ('', _('All'))
71
72    for value, name in get_cms_setting('TEMPLATES'):
73        yield (value, name)
74
75
76def save_permissions(data, obj):
77    models = (
78        (Page, 'page'),
79        (PageUser, 'pageuser'),
80        (PageUserGroup, 'pageuser'),
81        (PagePermission, 'pagepermission'),
82    )
83
84    if not obj.pk:
85        # save obj, otherwise we can't assign permissions to him
86        obj.save()
87
88    permission_accessor = get_permission_accessor(obj)
89
90    for model, name in models:
91        content_type = ContentType.objects.get_for_model(model)
92        for key in ('add', 'change', 'delete'):
93            # add permission `key` for model `model`
94            codename = get_permission_codename(key, model._meta)
95            permission = Permission.objects.get(content_type=content_type, codename=codename)
96            field = 'can_%s_%s' % (key, name)
97
98            if data.get(field):
99                permission_accessor.add(permission)
100            elif field in data:
101                permission_accessor.remove(permission)
102
103
104class CopyPermissionForm(forms.Form):
105    """
106    Holds the specific field for permissions
107    """
108    copy_permissions = forms.BooleanField(
109        label=_('Copy permissions'),
110        required=False,
111        initial=True,
112    )
113
114
115class BasePageForm(forms.ModelForm):
116    _user = None
117    _site = None
118    _language = None
119
120    title = forms.CharField(label=_("Title"), max_length=255, widget=forms.TextInput(),
121                            help_text=_('The default title'))
122    slug = forms.CharField(label=_("Slug"), max_length=255, widget=forms.TextInput(),
123                           help_text=_('The part of the title that is used in the URL'))
124    menu_title = forms.CharField(label=_("Menu Title"), widget=forms.TextInput(),
125                                 help_text=_('Overwrite what is displayed in the menu'), required=False)
126    page_title = forms.CharField(label=_("Page Title"), widget=forms.TextInput(),
127                                 help_text=_('Overwrites what is displayed at the top of your browser or in bookmarks'),
128                                 required=False)
129    meta_description = forms.CharField(label=_('Description meta tag'), required=False,
130                                       widget=forms.Textarea(attrs={'maxlength': '320', 'rows': '4'}),
131                                       help_text=_('A description of the page used by search engines.'),
132                                       max_length=320)
133
134    class Meta:
135        model = Page
136        fields = []
137
138    def clean_slug(self):
139        slug = slugify(self.cleaned_data['slug'])
140
141        if not slug:
142            raise ValidationError(_("Slug must not be empty."))
143        return slug
144
145
146class AddPageForm(BasePageForm):
147    source = forms.ModelChoiceField(
148        label=_(u'Page type'),
149        queryset=Page.objects.filter(
150            is_page_type=True,
151            publisher_is_draft=True,
152        ),
153        required=False,
154    )
155    parent_node = forms.ModelChoiceField(
156        queryset=TreeNode.objects.all(),
157        required=False,
158        widget=forms.HiddenInput(),
159    )
160
161    class Meta:
162        model = Page
163        fields = ['source']
164
165    def __init__(self, *args, **kwargs):
166        super(AddPageForm, self).__init__(*args, **kwargs)
167
168        source_field = self.fields.get('source')
169
170        if not source_field or source_field.widget.is_hidden:
171            return
172
173        root_page = PageType.get_root_page(site=self._site)
174
175        if root_page:
176            # Set the choicefield's choices to the various page_types
177            descendants = root_page.get_descendant_pages().filter(is_page_type=True)
178            titles = Title.objects.filter(page__in=descendants, language=self._language)
179            choices = [('', '---------')]
180            choices.extend((title.page_id, title.title) for title in titles)
181            source_field.choices = choices
182        else:
183            choices = []
184
185        if len(choices) < 2:
186            source_field.widget = forms.HiddenInput()
187
188    def clean(self):
189        data = self.cleaned_data
190
191        if self._errors:
192            # Form already has errors, best to let those be
193            # addressed first.
194            return data
195
196        parent_node = data.get('parent_node')
197
198        if parent_node:
199            slug = data['slug']
200            parent_path = parent_node.item.get_path(self._language)
201            path = u'%s/%s' % (parent_path, slug) if parent_path else slug
202        else:
203            path = data['slug']
204
205        try:
206            # Validate the url
207            validate_url_uniqueness(
208                self._site,
209                path=path,
210                language=self._language,
211            )
212        except ValidationError as error:
213            self.add_error('slug', error)
214        else:
215            data['path'] = path
216        return data
217
218    def clean_parent_node(self):
219        parent_node = self.cleaned_data.get('parent_node')
220
221        if parent_node and parent_node.site_id != self._site.pk:
222            raise ValidationError("Site doesn't match the parent's page site")
223        return parent_node
224
225    def create_translation(self, page):
226        data = self.cleaned_data
227        title_kwargs = {
228            'page': page,
229            'language': self._language,
230            'slug': data['slug'],
231            'path': data['path'],
232            'title': data['title'],
233        }
234
235        if 'menu_title' in data:
236            title_kwargs['menu_title'] = data['menu_title']
237
238        if 'page_title' in data:
239            title_kwargs['page_title'] = data['page_title']
240
241        if 'meta_description' in data:
242            title_kwargs['meta_description'] = data['meta_description']
243        return api.create_title(**title_kwargs)
244
245    def from_source(self, source, parent=None):
246        new_page = source.copy(
247            site=self._site,
248            parent_node=parent,
249            language=self._language,
250            translations=False,
251            permissions=False,
252            extensions=False,
253        )
254        new_page.update(is_page_type=False, in_navigation=True)
255        return new_page
256
257    def get_template(self):
258        return Page.TEMPLATE_DEFAULT
259
260    def save(self, *args, **kwargs):
261        source = self.cleaned_data.get('source')
262        parent = self.cleaned_data.get('parent_node')
263
264        if source:
265            new_page = self.from_source(source, parent=parent)
266
267            for lang in source.get_languages():
268                source._copy_contents(new_page, lang)
269        else:
270            new_page = super(AddPageForm, self).save(commit=False)
271            new_page.template = self.get_template()
272            new_page.set_tree_node(self._site, target=parent, position='last-child')
273            new_page.save()
274
275        translation = self.create_translation(new_page)
276
277        if source:
278            extension_pool.copy_extensions(
279                source_page=source,
280                target_page=new_page,
281                languages=[translation.language],
282            )
283
284        is_first = not (
285            TreeNode
286            .objects
287            .get_for_site(self._site)
288            .exclude(pk=new_page.node_id)
289            .exists()
290        )
291        new_page.rescan_placeholders()
292
293        if is_first and not new_page.is_page_type:
294            # its the first page. publish it right away
295            new_page.publish(translation.language)
296            new_page.set_as_homepage(self._user)
297
298        new_page.clear_cache(menu=True)
299        return new_page
300
301
302class AddPageTypeForm(AddPageForm):
303    menu_title = None
304    meta_description = None
305    page_title = None
306    source = forms.ModelChoiceField(
307        queryset=Page.objects.drafts(),
308        required=False,
309        widget=forms.HiddenInput(),
310    )
311
312    def get_or_create_root(self):
313        """
314        Creates the root node used to store all page types
315        for the current site if it doesn't exist.
316        """
317        root_page = PageType.get_root_page(site=self._site)
318
319        if not root_page:
320            root_page = Page(
321                publisher_is_draft=True,
322                in_navigation=False,
323                is_page_type=True,
324            )
325            root_page.set_tree_node(self._site)
326            root_page.save()
327
328        if not root_page.has_translation(self._language):
329            api.create_title(
330                language=self._language,
331                title=ugettext('Page Types'),
332                page=root_page,
333                slug=PAGE_TYPES_ID,
334                path=PAGE_TYPES_ID,
335            )
336        return root_page.node
337
338    def clean_parent_node(self):
339        parent_node = super(AddPageTypeForm, self).clean_parent_node()
340
341        if parent_node and not parent_node.item.is_page_type:
342            raise ValidationError("Parent has to be a page type.")
343
344        if not parent_node:
345            # parent was not explicitly selected.
346            # fallback to the page types root
347            parent_node = self.get_or_create_root()
348        return parent_node
349
350    def from_source(self, source, parent=None):
351        new_page = source.copy(
352            site=self._site,
353            parent_node=parent,
354            language=self._language,
355            translations=False,
356            permissions=False,
357            extensions=False,
358        )
359        new_page.update(is_page_type=True, in_navigation=False)
360        return new_page
361
362    def save(self, *args, **kwargs):
363        new_page = super(AddPageTypeForm, self).save(*args, **kwargs)
364
365        if not self.cleaned_data.get('source'):
366            # User has created a page-type via "Add page"
367            # instead of from another page.
368            new_page.update(
369                draft_only=True,
370                is_page_type=True,
371                in_navigation=False,
372            )
373        return new_page
374
375
376class DuplicatePageForm(AddPageForm):
377    source = forms.ModelChoiceField(
378        queryset=Page.objects.drafts(),
379        required=True,
380        widget=forms.HiddenInput(),
381    )
382
383
384class ChangePageForm(BasePageForm):
385
386    translation_fields = (
387        'slug',
388        'title',
389        'meta_description',
390        'menu_title',
391        'page_title',
392    )
393
394    def __init__(self, *args, **kwargs):
395        super(ChangePageForm, self).__init__(*args, **kwargs)
396        self.title_obj = self.instance.get_title_obj(
397            language=self._language,
398            fallback=False,
399            force_reload=True,
400        )
401
402        for field in self.translation_fields:
403            if field in self.fields:
404                self.fields[field].initial = getattr(self.title_obj, field)
405
406    def clean(self):
407        data = super(ChangePageForm, self).clean()
408
409        if self._errors:
410            # Form already has errors, best to let those be
411            # addressed first.
412            return data
413
414        page = self.instance
415
416        if page.is_home:
417            data['path'] = ''
418            return data
419
420        if self.title_obj.has_url_overwrite:
421            data['path'] = self.title_obj.path
422            return data
423
424        if 'slug' not in self.fields:
425            # the {% edit_title_fields %} template tag
426            # allows users to edit specific fields for a translation.
427            # as a result, slug might not always be there.
428            return data
429
430        if page.parent_page:
431            slug = data['slug']
432            parent_path = page.parent_page.get_path(self._language)
433            path = u'%s/%s' % (parent_path, slug) if parent_path else slug
434        else:
435            path = data['slug']
436
437        try:
438            # Validate the url
439            validate_url_uniqueness(
440                self._site,
441                path=path,
442                language=self._language,
443                exclude_page=page,
444            )
445        except ValidationError as error:
446            self.add_error('slug', error)
447        else:
448            data['path'] = path
449        return data
450
451    def save(self, commit=True):
452        data = self.cleaned_data
453        cms_page = super(ChangePageForm, self).save(commit=False)
454
455        translation_data = {field: data[field]
456                            for field in self.translation_fields if field in data}
457
458        if 'path' in data:
459            # The path key is set if
460            # the slug field is present in the form,
461            # or if the page being edited is the home page,
462            # or if the translation has a url override.
463            translation_data['path'] = data['path']
464
465        update_count = cms_page.update_translations(
466            self._language,
467            publisher_state=PUBLISHER_STATE_DIRTY,
468            **translation_data
469        )
470
471        if self._language in cms_page.title_cache:
472            del cms_page.title_cache[self._language]
473
474        if update_count == 0:
475            api.create_title(language=self._language, page=cms_page, **translation_data)
476        else:
477            cms_page._update_title_path_recursive(self._language)
478        cms_page.clear_cache(menu=True)
479        return cms_page
480
481
482class PublicationDatesForm(forms.ModelForm):
483
484    class Meta:
485        model = Page
486        fields = ['publication_date', 'publication_end_date']
487
488    def save(self, *args, **kwargs):
489        page = super(PublicationDatesForm, self).save(*args, **kwargs)
490        page.clear_cache(menu=True)
491        return page
492
493
494class AdvancedSettingsForm(forms.ModelForm):
495    from cms.forms.fields import PageSmartLinkField
496
497    _user = None
498    _site = None
499    _language = None
500
501    application_urls = forms.ChoiceField(label=_('Application'),
502                                         choices=(), required=False,
503                                         help_text=_('Hook application to this page.'))
504    overwrite_url = forms.CharField(label=_('Overwrite URL'), max_length=255, required=False,
505                                    help_text=_('Keep this field empty if standard path should be used.'))
506
507    xframe_options = forms.ChoiceField(
508        choices=Page._meta.get_field('xframe_options').choices,
509        label=_('X Frame Options'),
510        help_text=_('Whether this page can be embedded in other pages or websites'),
511        initial=Page._meta.get_field('xframe_options').default,
512        required=False
513    )
514
515    redirect = PageSmartLinkField(label=_('Redirect'), required=False,
516                                  help_text=_('Redirects to this URL.'),
517                                  placeholder_text=_('Start typing...'),
518                                  ajax_view='admin:cms_page_get_published_pagelist',
519    )
520
521    # This is really a 'fake' field which does not correspond to any Page attribute
522    # But creates a stub field to be populate by js
523    application_configs = forms.CharField(
524        label=_('Application configurations'),
525        required=False,
526        widget=ApplicationConfigSelect,
527    )
528    fieldsets = (
529        (None, {
530            'fields': ('overwrite_url', 'redirect'),
531        }),
532        (_('Language independent options'), {
533            'fields': ('template', 'reverse_id', 'soft_root', 'navigation_extenders',
534                       'application_urls', 'application_namespace', 'application_configs',
535                       'xframe_options',)
536        })
537    )
538
539    class Meta:
540        model = Page
541        fields = [
542            'template', 'reverse_id', 'overwrite_url', 'redirect', 'soft_root', 'navigation_extenders',
543            'application_urls', 'application_namespace', "xframe_options",
544        ]
545
546    def __init__(self, *args, **kwargs):
547        super(AdvancedSettingsForm, self).__init__(*args, **kwargs)
548        self.title_obj = self.instance.get_title_obj(
549            language=self._language,
550            fallback=False,
551            force_reload=True,
552        )
553
554        if 'navigation_extenders' in self.fields:
555            navigation_extenders = self.get_navigation_extenders()
556            self.fields['navigation_extenders'].widget = forms.Select(
557                {}, [('', "---------")] + navigation_extenders)
558        if 'application_urls' in self.fields:
559            # Prepare a dict mapping the apps by class name ('PollApp') to
560            # their app_name attribute ('polls'), if any.
561            app_namespaces = {}
562            app_configs = {}
563            for hook in apphook_pool.get_apphooks():
564                app = apphook_pool.get_apphook(hook[0])
565                if app.app_name:
566                    app_namespaces[hook[0]] = app.app_name
567                if app.app_config:
568                    app_configs[hook[0]] = app
569
570            self.fields['application_urls'].widget = AppHookSelect(
571                attrs={'id': 'application_urls'},
572                app_namespaces=app_namespaces
573            )
574            self.fields['application_urls'].choices = [('', "---------")] + apphook_pool.get_apphooks()
575
576            page_data = self.data if self.data else self.initial
577            if app_configs:
578                self.fields['application_configs'].widget = ApplicationConfigSelect(
579                    attrs={'id': 'application_configs'},
580                    app_configs=app_configs,
581                )
582
583                if page_data.get('application_urls', False) and page_data['application_urls'] in app_configs:
584                    configs = app_configs[page_data['application_urls']].get_configs()
585                    self.fields['application_configs'].widget.choices = [(config.pk, force_text(config)) for config in configs]
586
587                    try:
588                        config = configs.get(namespace=self.initial['application_namespace'])
589                        self.fields['application_configs'].initial = config.pk
590                    except ObjectDoesNotExist:
591                        # Provided apphook configuration doesn't exist (anymore),
592                        # just skip it
593                        # The user will choose another value anyway
594                        pass
595
596        if 'redirect' in self.fields:
597            self.fields['redirect'].widget.language = self._language
598            self.fields['redirect'].initial = self.title_obj.redirect
599
600        if 'overwrite_url' in self.fields and self.title_obj.has_url_overwrite:
601            self.fields['overwrite_url'].initial = self.title_obj.path
602
603    def get_apphooks(self):
604        for hook in apphook_pool.get_apphooks():
605            yield (hook[0], apphook_pool.get_apphook(hook[0]))
606
607    def get_apphooks_with_config(self):
608        return {key: app for key, app in self.get_apphooks() if app.app_config}
609
610    def get_navigation_extenders(self):
611        return menu_pool.get_menus_by_attribute("cms_enabled", True)
612
613    def _check_unique_namespace_instance(self, namespace):
614        return Page.objects.drafts().on_site(self._site).filter(
615            application_namespace=namespace
616        ).exclude(pk=self.instance.pk).exists()
617
618    def clean(self):
619        cleaned_data = super(AdvancedSettingsForm, self).clean()
620
621        if self._errors:
622            # Fail fast if there's errors in the form
623            return cleaned_data
624
625        # Language has been validated already
626        # so we know it exists.
627        language_name = get_language_object(
628            self._language,
629            site_id=self._site.pk,
630        )['name']
631
632        if not self.title_obj.slug:
633            # This covers all cases where users try to edit
634            # page advanced settings without setting a title slug
635            # for page titles that already exist.
636            message = _("Please set the %(language)s slug "
637                        "before editing its advanced settings.")
638            raise ValidationError(message % {'language': language_name})
639
640        if 'reverse_id' in self.fields:
641            reverse_id = cleaned_data['reverse_id']
642            if reverse_id:
643                lookup = Page.objects.drafts().on_site(self._site).filter(reverse_id=reverse_id)
644                if lookup.exclude(pk=self.instance.pk).exists():
645                    self._errors['reverse_id'] = self.error_class(
646                        [_('A page with this reverse URL id exists already.')])
647        apphook = cleaned_data.get('application_urls', None)
648        # The field 'application_namespace' is a misnomer. It should be
649        # 'instance_namespace'.
650        instance_namespace = cleaned_data.get('application_namespace', None)
651        application_config = cleaned_data.get('application_configs', None)
652        if apphook:
653            apphooks_with_config = self.get_apphooks_with_config()
654
655            # application_config wins over application_namespace
656            if apphook in apphooks_with_config and application_config:
657                # the value of the application config namespace is saved in
658                # the 'usual' namespace field to be backward compatible
659                # with existing apphooks
660                try:
661                    appconfig_pk = forms.IntegerField(required=True).to_python(application_config)
662                except ValidationError:
663                    self._errors['application_configs'] = ErrorList([
664                        _('Invalid application config value')
665                    ])
666                    return self.cleaned_data
667
668                try:
669                    config = apphooks_with_config[apphook].get_configs().get(pk=appconfig_pk)
670                except ObjectDoesNotExist:
671                    self._errors['application_configs'] = ErrorList([
672                        _('Invalid application config value')
673                    ])
674                    return self.cleaned_data
675
676                if self._check_unique_namespace_instance(config.namespace):
677                    # Looks like there's already one with the default instance
678                    # namespace defined.
679                    self._errors['application_configs'] = ErrorList([
680                        _('An application instance using this configuration already exists.')
681                    ])
682                else:
683                    self.cleaned_data['application_namespace'] = config.namespace
684            else:
685                if instance_namespace:
686                    if self._check_unique_namespace_instance(instance_namespace):
687                        self._errors['application_namespace'] = ErrorList([
688                            _('An application instance with this name already exists.')
689                        ])
690                else:
691                    # The attribute on the apps 'app_name' is a misnomer, it should be
692                    # 'application_namespace'.
693                    application_namespace = apphook_pool.get_apphook(apphook).app_name
694                    if application_namespace and not instance_namespace:
695                        if self._check_unique_namespace_instance(application_namespace):
696                            # Looks like there's already one with the default instance
697                            # namespace defined.
698                            self._errors['application_namespace'] = ErrorList([
699                                _('An application instance with this name already exists.')
700                            ])
701                        else:
702                            # OK, there are zero instances of THIS app that use the
703                            # default instance namespace, so, since the user didn't
704                            # provide one, we'll use the default. NOTE: The following
705                            # line is really setting the "instance namespace" of the
706                            # new app to the app’s "application namespace", which is
707                            # the default instance namespace.
708                            self.cleaned_data['application_namespace'] = application_namespace
709
710        if instance_namespace and not apphook:
711            self.cleaned_data['application_namespace'] = None
712
713        if application_config and not apphook:
714            self.cleaned_data['application_configs'] = None
715        return self.cleaned_data
716
717    def clean_xframe_options(self):
718        if 'xframe_options' not in self.fields:
719            return  # nothing to do, field isn't present
720
721        xframe_options = self.cleaned_data['xframe_options']
722        if xframe_options == '':
723            return Page._meta.get_field('xframe_options').default
724
725        return xframe_options
726
727    def clean_overwrite_url(self):
728        path_override = self.cleaned_data.get('overwrite_url')
729
730        if path_override:
731            path = path_override.strip('/')
732        else:
733            path = self.instance.get_path_for_slug(self.title_obj.slug, self._language)
734
735        validate_url_uniqueness(
736            self._site,
737            path=path,
738            language=self._language,
739            exclude_page=self.instance,
740        )
741        self.cleaned_data['path'] = path
742        return path_override
743
744    def has_changed_apphooks(self):
745        changed_data = self.changed_data
746
747        if 'application_urls' in changed_data:
748            return True
749        return 'application_namespace' in changed_data
750
751    def update_apphooks(self):
752        # User has changed the apphooks on the page.
753        # Update the public version of the page to reflect this change immediately.
754        public_id = self.instance.publisher_public_id
755        self._meta.model.objects.filter(pk=public_id).update(
756            application_urls=self.instance.application_urls,
757            application_namespace=(self.instance.application_namespace or None),
758        )
759
760        # Connects the apphook restart handler to the request finished signal
761        set_restart_trigger()
762
763    def save(self, *args, **kwargs):
764        data = self.cleaned_data
765        page = super(AdvancedSettingsForm, self).save(*args, **kwargs)
766        page.update_translations(
767            self._language,
768            path=data['path'],
769            redirect=(data.get('redirect') or None),
770            publisher_state=PUBLISHER_STATE_DIRTY,
771            has_url_overwrite=bool(data.get('overwrite_url')),
772        )
773        is_draft_and_has_public = page.publisher_is_draft and page.publisher_public_id
774
775        if is_draft_and_has_public and self.has_changed_apphooks():
776            self.update_apphooks()
777        page.clear_cache(menu=True)
778        return page
779
780
781class PagePermissionForm(forms.ModelForm):
782
783    class Meta:
784        model = Page
785        fields = ['login_required', 'limit_visibility_in_menu']
786
787    def save(self, *args, **kwargs):
788        page = super(PagePermissionForm, self).save(*args, **kwargs)
789        page.clear_cache(menu=True)
790        clear_permission_cache()
791        return page
792
793
794class PageTreeForm(forms.Form):
795
796    position = forms.IntegerField(initial=0, required=True)
797    target = forms.ModelChoiceField(queryset=Page.objects.none(), required=False)
798
799    def __init__(self, *args, **kwargs):
800        self.page = kwargs.pop('page')
801        self._site = kwargs.pop('site', Site.objects.get_current())
802        super(PageTreeForm, self).__init__(*args, **kwargs)
803        self.fields['target'].queryset = Page.objects.drafts().filter(
804            node__site=self._site,
805            is_page_type=self.page.is_page_type,
806        )
807
808    def get_root_nodes(self):
809        # TODO: this needs to avoid using the pages accessor directly
810        nodes = TreeNode.get_root_nodes()
811        return nodes.exclude(cms_pages__is_page_type=not(self.page.is_page_type))
812
813    def get_tree_options(self):
814        position = self.cleaned_data['position']
815        target_page = self.cleaned_data.get('target')
816        parent_node = target_page.node if target_page else None
817
818        if parent_node:
819            return self._get_tree_options_for_parent(parent_node, position)
820        return self._get_tree_options_for_root(position)
821
822    def _get_tree_options_for_root(self, position):
823        siblings = self.get_root_nodes().filter(site=self._site)
824
825        try:
826            target_node = siblings[position]
827        except IndexError:
828            # The position requested is not occupied.
829            # Add the node as the last root node,
830            # relative to the current site.
831            return (siblings.reverse()[0], 'right')
832        return (target_node, 'left')
833
834    def _get_tree_options_for_parent(self, parent_node, position):
835        if position == 0:
836            return (parent_node, 'first-child')
837
838        siblings = parent_node.get_children().filter(site=self._site)
839
840        try:
841            target_node = siblings[position]
842        except IndexError:
843            # The position requested is not occupied.
844            # Add the node to be the parent's first child
845            return (parent_node, 'last-child')
846        return (target_node, 'left')
847
848
849class MovePageForm(PageTreeForm):
850
851    def clean(self):
852        cleaned_data = super(MovePageForm, self).clean()
853
854        if self.page.is_home and cleaned_data.get('target'):
855            self.add_error('target', force_text(_('You can\'t move the home page inside another page')))
856        return cleaned_data
857
858    def get_tree_options(self):
859        options = super(MovePageForm, self).get_tree_options()
860        target_node, target_node_position = options
861
862        if target_node_position != 'left':
863            return (target_node, target_node_position)
864
865        node = self.page.node
866        node_is_first = node.path < target_node.path
867
868        if node_is_first and node.is_sibling_of(target_node):
869            # The node being moved appears before the target node
870            # and is a sibling of the target node.
871            # The user is moving from left to right.
872            target_node_position = 'right'
873        elif node_is_first:
874            # The node being moved appears before the target node
875            # but is not a sibling of the target node.
876            # The user is moving from right to left.
877            target_node_position = 'left'
878        else:
879            # The node being moved appears after the target node.
880            # The user is moving from right to left.
881            target_node_position = 'left'
882        return (target_node, target_node_position)
883
884    def move_page(self):
885        self.page.move_page(*self.get_tree_options())
886
887
888class CopyPageForm(PageTreeForm):
889    source_site = forms.ModelChoiceField(queryset=Site.objects.all(), required=True)
890    copy_permissions = forms.BooleanField(initial=False, required=False)
891
892    def copy_page(self):
893        target, position = self.get_tree_options()
894        copy_permissions = self.cleaned_data.get('copy_permissions', False)
895        new_page = self.page.copy_with_descendants(
896            target_node=target,
897            position=position,
898            copy_permissions=copy_permissions,
899            target_site=self._site,
900        )
901        new_page.clear_cache(menu=True)
902        return new_page
903
904    def _get_tree_options_for_root(self, position):
905        try:
906            return super(CopyPageForm, self)._get_tree_options_for_root(position)
907        except IndexError:
908            # The user is copying a page to a site with no pages
909            # Add the node as the last root node.
910            siblings = self.get_root_nodes().reverse()
911            return (siblings[0], 'right')
912
913
914class ChangeListForm(forms.Form):
915    BOOLEAN_CHOICES = (
916        ('', _('All')),
917        ('1', _('Yes')),
918        ('0', _('No')),
919    )
920
921    q = forms.CharField(required=False, widget=forms.HiddenInput())
922    in_navigation = forms.ChoiceField(required=False, choices=BOOLEAN_CHOICES)
923    template = forms.ChoiceField(required=False)
924    changed_by = forms.ChoiceField(required=False)
925    soft_root = forms.ChoiceField(required=False, choices=BOOLEAN_CHOICES)
926
927    def __init__(self, *args, **kwargs):
928        super(ChangeListForm, self).__init__(*args, **kwargs)
929        self.fields['changed_by'].choices = get_page_changed_by_filter_choices()
930        self.fields['template'].choices = get_page_template_filter_choices()
931
932    def is_filtered(self):
933        data = self.cleaned_data
934
935        if self.cleaned_data.get('q'):
936            return True
937        return any(bool(data.get(field.name)) for field in self.visible_fields())
938
939    def get_filter_items(self):
940        for field in self.visible_fields():
941            value = self.cleaned_data.get(field.name)
942
943            if value:
944                yield (field.name, value)
945
946    def run_filters(self, queryset):
947        for field, value in self.get_filter_items():
948            query = {'{}__exact'.format(field): value}
949            queryset = queryset.filter(**query)
950        return queryset
951
952
953class BasePermissionAdminForm(forms.ModelForm):
954
955    def __init__(self, *args, **kwargs):
956        super(BasePermissionAdminForm, self).__init__(*args, **kwargs)
957        permission_fields = self._meta.model.get_all_permissions()
958
959        for field in permission_fields:
960            if field not in self.base_fields:
961                setattr(self.instance, field, False)
962
963
964class PagePermissionInlineAdminForm(BasePermissionAdminForm):
965    """
966    Page permission inline admin form used in inline admin. Required, because
967    user and group queryset must be changed. User can see only users on the same
968    level or under him in chosen page tree, and users which were created by him,
969    but aren't assigned to higher page level than current user.
970    """
971    page = forms.ModelChoiceField(
972        queryset=Page.objects.all(),
973        label=_('user'),
974        widget=HiddenInput(),
975        required=True,
976    )
977
978    def __init__(self, *args, **kwargs):
979        super(PagePermissionInlineAdminForm, self).__init__(*args, **kwargs)
980        user = get_current_user() # current user from threadlocals
981        site = Site.objects.get_current()
982        sub_users = get_subordinate_users(user, site)
983
984        limit_choices = True
985        use_raw_id = False
986
987        # Unfortunately, if there are > 500 users in the system, non-superusers
988        # won't see any benefit here because if we ask Django to put all the
989        # user PKs in limit_choices_to in the query string of the popup we're
990        # in danger of causing 414 errors so we fall back to the normal input
991        # widget.
992        if get_cms_setting('RAW_ID_USERS'):
993            if sub_users.count() < 500:
994                # If there aren't too many users, proceed as normal and use a
995                # raw id field with limit_choices_to
996                limit_choices = True
997                use_raw_id = True
998            elif get_user_permission_level(user, site) == ROOT_USER_LEVEL:
999                # If there are enough choices to possibly cause a 414 request
1000                # URI too large error, we only proceed with the raw id field if
1001                # the user is a superuser & thus can legitimately circumvent
1002                # the limit_choices_to condition.
1003                limit_choices = False
1004                use_raw_id = True
1005
1006        # We don't use the fancy custom widget if the admin form wants to use a
1007        # raw id field for the user
1008        if use_raw_id:
1009            from django.contrib.admin.widgets import ForeignKeyRawIdWidget
1010            # This check will be False if the number of users in the system
1011            # is less than the threshold set by the RAW_ID_USERS setting.
1012            if isinstance(self.fields['user'].widget, ForeignKeyRawIdWidget):
1013                # We can't set a queryset on a raw id lookup, but we can use
1014                # the fact that it respects the limit_choices_to parameter.
1015                if limit_choices:
1016                    self.fields['user'].widget.rel.limit_choices_to = dict(
1017                        id__in=list(sub_users.values_list('pk', flat=True))
1018                    )
1019        else:
1020            self.fields['user'].widget = UserSelectAdminWidget()
1021            self.fields['user'].queryset = sub_users
1022            self.fields['user'].widget.user = user # assign current user
1023
1024        self.fields['group'].queryset = get_subordinate_groups(user, site)
1025
1026    class Meta:
1027        fields = [
1028            'user',
1029            'group',
1030            'can_add',
1031            'can_change',
1032            'can_delete',
1033            'can_publish',
1034            'can_change_advanced_settings',
1035            'can_change_permissions',
1036            'can_move_page',
1037            'grant_on',
1038        ]
1039        model = PagePermission
1040
1041
1042class ViewRestrictionInlineAdminForm(BasePermissionAdminForm):
1043    page = forms.ModelChoiceField(
1044        queryset=Page.objects.all(),
1045        label=_('user'),
1046        widget=HiddenInput(),
1047        required=True,
1048    )
1049    can_view = forms.BooleanField(
1050        label=_('can_view'),
1051        widget=HiddenInput(),
1052        initial=True,
1053    )
1054
1055    class Meta:
1056        fields = [
1057            'user',
1058            'group',
1059            'grant_on',
1060            'can_view',
1061        ]
1062        model = PagePermission
1063
1064    def clean_can_view(self):
1065        return True
1066
1067
1068class GlobalPagePermissionAdminForm(BasePermissionAdminForm):
1069
1070    class Meta:
1071        fields = [
1072            'user',
1073            'group',
1074            'can_add',
1075            'can_change',
1076            'can_delete',
1077            'can_publish',
1078            'can_change_advanced_settings',
1079            'can_change_permissions',
1080            'can_move_page',
1081            'can_view',
1082            'sites',
1083        ]
1084        model = GlobalPagePermission
1085
1086
1087class GenericCmsPermissionForm(forms.ModelForm):
1088    """Generic form for User & Grup permissions in cms
1089    """
1090    _current_user = None
1091
1092    can_add_page = forms.BooleanField(label=_('Add'), required=False, initial=True)
1093    can_change_page = forms.BooleanField(label=_('Change'), required=False, initial=True)
1094    can_delete_page = forms.BooleanField(label=_('Delete'), required=False)
1095
1096    # pageuser is for pageuser & group - they are combined together,
1097    # and read out from PageUser model
1098    can_add_pageuser = forms.BooleanField(label=_('Add'), required=False)
1099    can_change_pageuser = forms.BooleanField(label=_('Change'), required=False)
1100    can_delete_pageuser = forms.BooleanField(label=_('Delete'), required=False)
1101
1102    can_add_pagepermission = forms.BooleanField(label=_('Add'), required=False)
1103    can_change_pagepermission = forms.BooleanField(label=_('Change'), required=False)
1104    can_delete_pagepermission = forms.BooleanField(label=_('Delete'), required=False)
1105
1106    def __init__(self, *args, **kwargs):
1107        instance = kwargs.get('instance')
1108        initial = kwargs.get('initial') or {}
1109
1110        if instance:
1111            initial = initial or {}
1112            initial.update(self.populate_initials(instance))
1113            kwargs['initial'] = initial
1114        super(GenericCmsPermissionForm, self).__init__(*args, **kwargs)
1115
1116    def clean(self):
1117        data = super(GenericCmsPermissionForm, self).clean()
1118
1119        # Validate Page options
1120        if not data.get('can_change_page'):
1121            if data.get('can_add_page'):
1122                message = _("Users can't create a page without permissions "
1123                            "to change the created page. Edit permissions required.")
1124                raise ValidationError(message)
1125
1126            if data.get('can_delete_page'):
1127                message = _("Users can't delete a page without permissions "
1128                            "to change the page. Edit permissions required.")
1129                raise ValidationError(message)
1130
1131            if data.get('can_add_pagepermission'):
1132                message = _("Users can't set page permissions without permissions "
1133                            "to change a page. Edit permissions required.")
1134                raise ValidationError(message)
1135
1136            if data.get('can_delete_pagepermission'):
1137                message = _("Users can't delete page permissions without permissions "
1138                            "to change a page. Edit permissions required.")
1139                raise ValidationError(message)
1140
1141        # Validate PagePermission options
1142        if not data.get('can_change_pagepermission'):
1143            if data.get('can_add_pagepermission'):
1144                message = _("Users can't create page permissions without permissions "
1145                            "to change the created permission. Edit permissions required.")
1146                raise ValidationError(message)
1147
1148            if data.get('can_delete_pagepermission'):
1149                message = _("Users can't delete page permissions without permissions "
1150                            "to change permissions. Edit permissions required.")
1151                raise ValidationError(message)
1152
1153    def populate_initials(self, obj):
1154        """Read out permissions from permission system.
1155        """
1156        initials = {}
1157        permission_accessor = get_permission_accessor(obj)
1158
1159        for model in (Page, PageUser, PagePermission):
1160            name = model.__name__.lower()
1161            content_type = ContentType.objects.get_for_model(model)
1162            permissions = permission_accessor.filter(content_type=content_type).values_list('codename', flat=True)
1163            for key in ('add', 'change', 'delete'):
1164                codename = get_permission_codename(key, model._meta)
1165                initials['can_%s_%s' % (key, name)] = codename in permissions
1166        return initials
1167
1168    def save(self, commit=True):
1169        instance = super(GenericCmsPermissionForm, self).save(commit=False)
1170        instance.save()
1171        save_permissions(self.cleaned_data, instance)
1172        return instance
1173
1174
1175class PageUserAddForm(forms.ModelForm):
1176    _current_user = None
1177
1178    user = forms.ModelChoiceField(queryset=User.objects.none())
1179
1180    class Meta:
1181        fields = ['user']
1182        model = PageUser
1183
1184    def __init__(self, *args, **kwargs):
1185        super(PageUserAddForm, self).__init__(*args, **kwargs)
1186        self.fields['user'].queryset = self.get_subordinates()
1187
1188    def get_subordinates(self):
1189        subordinates = get_subordinate_users(self._current_user, self._current_site)
1190        return subordinates.filter(pageuser__isnull=True)
1191
1192    def save(self, commit=True):
1193        user = self.cleaned_data['user']
1194        instance = super(PageUserAddForm, self).save(commit=False)
1195        instance.created_by = self._current_user
1196
1197        for field in user._meta.fields:
1198            # assign all the fields - we can do this, because object is
1199            # subclassing User (one to one relation)
1200            value = getattr(user, field.name)
1201            setattr(instance, field.name, value)
1202
1203        if commit:
1204            instance.save()
1205        return instance
1206
1207
1208class PageUserChangeForm(UserChangeForm):
1209
1210    _current_user = None
1211
1212    class Meta:
1213        fields = '__all__'
1214        model = PageUser
1215
1216    def __init__(self, *args, **kwargs):
1217        super(PageUserChangeForm, self).__init__(*args, **kwargs)
1218
1219        if not self._current_user.is_superuser:
1220            # Limit permissions to include only
1221            # the permissions available to the manager.
1222            permissions = self.get_available_permissions()
1223            self.fields['user_permissions'].queryset = permissions
1224
1225            # Limit groups to include only those where
1226            # the manager is a member.
1227            self.fields['groups'].queryset = self.get_available_groups()
1228
1229    def get_available_permissions(self):
1230        permissions = self._current_user.get_all_permissions()
1231        permission_codes = (perm.rpartition('.')[-1] for perm in permissions)
1232        return Permission.objects.filter(codename__in=permission_codes)
1233
1234    def get_available_groups(self):
1235        return self._current_user.groups.all()
1236
1237
1238class PageUserGroupForm(GenericCmsPermissionForm):
1239
1240    class Meta:
1241        model = PageUserGroup
1242        fields = ('name', )
1243
1244    def save(self, commit=True):
1245        if not self.instance.pk:
1246            self.instance.created_by = self._current_user
1247        return super(PageUserGroupForm, self).save(commit=commit)
1248
1249
1250class PluginAddValidationForm(forms.Form):
1251    placeholder_id = forms.ModelChoiceField(
1252        queryset=Placeholder.objects.all(),
1253        required=True,
1254    )
1255    plugin_language = forms.CharField(required=True)
1256    plugin_parent = forms.ModelChoiceField(
1257        CMSPlugin.objects.all(),
1258        required=False,
1259    )
1260    plugin_type = forms.CharField(required=True)
1261
1262    def clean_plugin_type(self):
1263        plugin_type = self.cleaned_data['plugin_type']
1264
1265        try:
1266            plugin_pool.get_plugin(plugin_type)
1267        except KeyError:
1268            message = ugettext("Invalid plugin type '%s'") % plugin_type
1269            raise ValidationError(message)
1270        return plugin_type
1271
1272    def clean(self):
1273        from cms.utils.plugins import has_reached_plugin_limit
1274
1275        data = self.cleaned_data
1276
1277        if self.errors:
1278            return data
1279
1280        language = data['plugin_language']
1281        placeholder = data['placeholder_id']
1282        parent_plugin = data.get('plugin_parent')
1283
1284        if language not in get_language_list():
1285            message = ugettext("Language must be set to a supported language!")
1286            self.add_error('plugin_language', message)
1287            return self.cleaned_data
1288
1289        if parent_plugin:
1290            if parent_plugin.language != language:
1291                message = ugettext("Parent plugin language must be same as language!")
1292                self.add_error('plugin_language', message)
1293                return self.cleaned_data
1294
1295            if parent_plugin.placeholder_id != placeholder.pk:
1296                message = ugettext("Parent plugin placeholder must be same as placeholder!")
1297                self.add_error('placeholder_id', message)
1298                return self.cleaned_data
1299
1300        page = placeholder.page
1301        template = page.get_template() if page else None
1302
1303        try:
1304            has_reached_plugin_limit(
1305                placeholder,
1306                data['plugin_type'],
1307                language,
1308                template=template
1309            )
1310        except PluginLimitReached as error:
1311            self.add_error(None, force_text(error))
1312        return self.cleaned_data
1313
1314
1315class RequestToolbarForm(forms.Form):
1316
1317    obj_id = forms.CharField(required=False)
1318    obj_type = forms.CharField(required=False)
1319    cms_path = forms.CharField(required=False)
1320
1321    def clean(self):
1322        data = self.cleaned_data
1323
1324        obj_id = data.get('obj_id')
1325        obj_type = data.get('obj_type')
1326
1327        if not bool(obj_id or obj_type):
1328            return data
1329
1330        if (obj_id and not obj_type) or (obj_type and not obj_id):
1331            message = 'Invalid object lookup. Both obj_id and obj_type are required'
1332            raise forms.ValidationError(message)
1333
1334        app, sep, model = obj_type.rpartition('.')
1335
1336        try:
1337            model_class = apps.get_model(app_label=app, model_name=model)
1338        except LookupError:
1339            message = 'Invalid object lookup. Both obj_id and obj_type are required'
1340            raise forms.ValidationError(message)
1341
1342        try:
1343            generic_obj = model_class.objects.get(pk=obj_id)
1344        except model_class.DoesNotExist:
1345            message = 'Invalid object lookup. Both obj_id and obj_type are required'
1346            raise forms.ValidationError(message)
1347        else:
1348            data['attached_obj'] = generic_obj
1349        return data
1350
1351    def clean_cms_path(self):
1352        path = self.cleaned_data.get('cms_path')
1353
1354        if path:
1355            validate_relative_url(path)
1356        return path
1357