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