1# -*- coding: utf-8 -*- 2import uuid 3import warnings 4 5from django.conf.urls import url 6from django.contrib.admin.helpers import AdminForm 7from django.contrib.admin.utils import get_deleted_objects 8from django.core.exceptions import PermissionDenied 9from django.db import router, transaction 10from django.http import ( 11 HttpResponse, 12 HttpResponseBadRequest, 13 HttpResponseForbidden, 14 HttpResponseNotFound, 15 HttpResponseRedirect, 16) 17from django.shortcuts import get_list_or_404, get_object_or_404, render 18from django.template.response import TemplateResponse 19from django.utils import six 20from django.utils.six.moves.urllib.parse import parse_qsl, urlparse 21from django.utils.decorators import method_decorator 22from django.utils.encoding import force_text 23from django.utils import translation 24from django.utils.translation import ugettext as _ 25from django.views.decorators.clickjacking import xframe_options_sameorigin 26from django.views.decorators.http import require_POST 27 28from cms import operations 29from cms.admin.forms import PluginAddValidationForm 30from cms.constants import SLUG_REGEXP 31from cms.exceptions import PluginLimitReached 32from cms.models.placeholdermodel import Placeholder 33from cms.models.placeholderpluginmodel import PlaceholderReference 34from cms.models.pluginmodel import CMSPlugin 35from cms.plugin_pool import plugin_pool 36from cms.signals import pre_placeholder_operation, post_placeholder_operation 37from cms.toolbar.utils import get_plugin_tree_as_json 38from cms.utils import copy_plugins, get_current_site 39from cms.utils.compat import DJANGO_2_0 40from cms.utils.conf import get_cms_setting 41from cms.utils.i18n import get_language_code, get_language_list 42from cms.utils.plugins import has_reached_plugin_limit, reorder_plugins 43from cms.utils.urlutils import admin_reverse 44 45_no_default = object() 46 47 48def get_int(int_str, default=_no_default): 49 """ 50 For convenience a get-like method for taking the int() of a string. 51 :param int_str: the string to convert to integer 52 :param default: an optional value to return if ValueError is raised. 53 :return: the int() of «int_str» or «default» on exception. 54 """ 55 if default == _no_default: 56 return int(int_str) 57 else: 58 try: 59 return int(int_str) 60 except ValueError: 61 return default 62 63 64def _instance_overrides_method(base, instance, method_name): 65 """ 66 Returns True if instance overrides a method (method_name) 67 inherited from base. 68 """ 69 bound_method = getattr(instance, method_name) 70 unbound_method = getattr(base, method_name) 71 return six.get_unbound_function(unbound_method) != six.get_method_function(bound_method) 72 73 74class FrontendEditableAdminMixin(object): 75 frontend_editable_fields = [] 76 77 def get_urls(self): 78 """ 79 Register the url for the single field edit view 80 """ 81 info = "%s_%s" % (self.model._meta.app_label, self.model._meta.model_name) 82 pat = lambda regex, fn: url(regex, self.admin_site.admin_view(fn), name='%s_%s' % (info, fn.__name__)) 83 url_patterns = [ 84 pat(r'edit-field/(%s)/([a-z\-]+)/$' % SLUG_REGEXP, self.edit_field), 85 ] 86 return url_patterns + super(FrontendEditableAdminMixin, self).get_urls() 87 88 def _get_object_for_single_field(self, object_id, language): 89 # Quick and dirty way to retrieve objects for django-hvad 90 # Cleaner implementation will extend this method in a child mixin 91 try: 92 return self.model.objects.language(language).get(pk=object_id) 93 except AttributeError: 94 return self.model.objects.get(pk=object_id) 95 96 def edit_field(self, request, object_id, language): 97 obj = self._get_object_for_single_field(object_id, language) 98 opts = obj.__class__._meta 99 saved_successfully = False 100 cancel_clicked = request.POST.get("_cancel", False) 101 raw_fields = request.GET.get("edit_fields") 102 fields = [field for field in raw_fields.split(",") if field in self.frontend_editable_fields] 103 if not fields: 104 context = { 105 'opts': opts, 106 'message': force_text(_("Field %s not found")) % raw_fields 107 } 108 return render(request, 'admin/cms/page/plugin/error_form.html', context) 109 if not request.user.has_perm("{0}.change_{1}".format(self.model._meta.app_label, 110 self.model._meta.model_name)): 111 context = { 112 'opts': opts, 113 'message': force_text(_("You do not have permission to edit this item")) 114 } 115 return render(request, 'admin/cms/page/plugin/error_form.html', context) 116 # Dynamically creates the form class with only `field_name` field 117 # enabled 118 form_class = self.get_form(request, obj, fields=fields) 119 if not cancel_clicked and request.method == 'POST': 120 form = form_class(instance=obj, data=request.POST) 121 if form.is_valid(): 122 form.save() 123 saved_successfully = True 124 else: 125 form = form_class(instance=obj) 126 admin_form = AdminForm(form, fieldsets=[(None, {'fields': fields})], prepopulated_fields={}, 127 model_admin=self) 128 media = self.media + admin_form.media 129 context = { 130 'CMS_MEDIA_URL': get_cms_setting('MEDIA_URL'), 131 'title': opts.verbose_name, 132 'plugin': None, 133 'plugin_id': None, 134 'adminform': admin_form, 135 'add': False, 136 'is_popup': True, 137 'media': media, 138 'opts': opts, 139 'change': True, 140 'save_as': False, 141 'has_add_permission': False, 142 'window_close_timeout': 10, 143 } 144 if cancel_clicked: 145 # cancel button was clicked 146 context.update({ 147 'cancel': True, 148 }) 149 return render(request, 'admin/cms/page/plugin/confirm_form.html', context) 150 if not cancel_clicked and request.method == 'POST' and saved_successfully: 151 return render(request, 'admin/cms/page/plugin/confirm_form.html', context) 152 return render(request, 'admin/cms/page/plugin/change_form.html', context) 153 154 155class PlaceholderAdminMixin(object): 156 157 def _get_attached_admin(self, placeholder): 158 return placeholder._get_attached_admin(admin_site=self.admin_site) 159 160 def _get_operation_language(self, request): 161 # Unfortunately the ?language GET query 162 # has a special meaning on the CMS. 163 # It allows users to see another language while maintaining 164 # the same url. This complicates language detection. 165 site = get_current_site() 166 parsed_url = urlparse(request.GET['cms_path']) 167 queries = dict(parse_qsl(parsed_url.query)) 168 language = queries.get('language') 169 170 if not language: 171 language = translation.get_language_from_path(parsed_url.path) 172 return get_language_code(language, site_id=site.pk) 173 174 def _get_operation_origin(self, request): 175 return urlparse(request.GET['cms_path']).path 176 177 def _send_pre_placeholder_operation(self, request, operation, **kwargs): 178 token = str(uuid.uuid4()) 179 180 if not request.GET.get('cms_path'): 181 warnings.warn('All custom placeholder admin endpoints require ' 182 'a "cms_path" GET query which points to the path ' 183 'where the request originates from.' 184 'This backwards compatible shim will be removed on 3.5 ' 185 'and an HttpBadRequest response will be returned instead.', 186 UserWarning) 187 return token 188 189 pre_placeholder_operation.send( 190 sender=self.__class__, 191 operation=operation, 192 request=request, 193 language=self._get_operation_language(request), 194 token=token, 195 origin=self._get_operation_origin(request), 196 **kwargs 197 ) 198 return token 199 200 def _send_post_placeholder_operation(self, request, operation, token, **kwargs): 201 if not request.GET.get('cms_path'): 202 # No need to re-raise the warning 203 return 204 205 post_placeholder_operation.send( 206 sender=self.__class__, 207 operation=operation, 208 request=request, 209 language=self._get_operation_language(request), 210 token=token, 211 origin=self._get_operation_origin(request), 212 **kwargs 213 ) 214 215 def _get_plugin_from_id(self, plugin_id): 216 queryset = CMSPlugin.objects.values_list('plugin_type', flat=True) 217 plugin_type = get_list_or_404(queryset, pk=plugin_id)[0] 218 # CMSPluginBase subclass 219 plugin_class = plugin_pool.get_plugin(plugin_type) 220 real_queryset = plugin_class.get_render_queryset().select_related('parent', 'placeholder') 221 return get_object_or_404(real_queryset, pk=plugin_id) 222 223 def get_urls(self): 224 """ 225 Register the plugin specific urls (add/edit/copy/remove/move) 226 """ 227 info = "%s_%s" % (self.model._meta.app_label, self.model._meta.model_name) 228 pat = lambda regex, fn: url(regex, self.admin_site.admin_view(fn), name='%s_%s' % (info, fn.__name__)) 229 url_patterns = [ 230 pat(r'copy-plugins/$', self.copy_plugins), 231 pat(r'add-plugin/$', self.add_plugin), 232 pat(r'edit-plugin/(%s)/$' % SLUG_REGEXP, self.edit_plugin), 233 pat(r'delete-plugin/(%s)/$' % SLUG_REGEXP, self.delete_plugin), 234 pat(r'clear-placeholder/(%s)/$' % SLUG_REGEXP, self.clear_placeholder), 235 pat(r'move-plugin/$', self.move_plugin), 236 ] 237 return url_patterns + super(PlaceholderAdminMixin, self).get_urls() 238 239 def has_add_plugin_permission(self, request, placeholder, plugin_type): 240 return placeholder.has_add_plugin_permission(request.user, plugin_type) 241 242 def has_change_plugin_permission(self, request, plugin): 243 placeholder = plugin.placeholder 244 return placeholder.has_change_plugin_permission(request.user, plugin) 245 246 def has_delete_plugin_permission(self, request, plugin): 247 placeholder = plugin.placeholder 248 return placeholder.has_delete_plugin_permission(request.user, plugin) 249 250 def has_copy_plugins_permission(self, request, plugins): 251 # Plugins can only be copied to the clipboard 252 placeholder = request.toolbar.clipboard 253 return placeholder.has_add_plugins_permission(request.user, plugins) 254 255 def has_copy_from_clipboard_permission(self, request, placeholder, plugins): 256 return placeholder.has_add_plugins_permission(request.user, plugins) 257 258 def has_copy_from_placeholder_permission(self, request, source_placeholder, target_placeholder, plugins): 259 if not source_placeholder.has_add_plugins_permission(request.user, plugins): 260 return False 261 return target_placeholder.has_add_plugins_permission(request.user, plugins) 262 263 def has_move_plugin_permission(self, request, plugin, target_placeholder): 264 placeholder = plugin.placeholder 265 return placeholder.has_move_plugin_permission(request.user, plugin, target_placeholder) 266 267 def has_clear_placeholder_permission(self, request, placeholder, language=None): 268 if language: 269 languages = [language] 270 else: 271 # fetch all languages this placeholder contains 272 # based on it's plugins 273 languages = ( 274 placeholder 275 .cmsplugin_set 276 .values_list('language', flat=True) 277 .distinct() 278 .order_by() 279 ) 280 return placeholder.has_clear_permission(request.user, languages) 281 282 def get_placeholder_template(self, request, placeholder): 283 pass 284 285 @xframe_options_sameorigin 286 def add_plugin(self, request): 287 """ 288 Shows the add plugin form and saves it on POST. 289 290 Requires the following GET parameters: 291 - cms_path 292 - placeholder_id 293 - plugin_type 294 - plugin_language 295 - plugin_parent (optional) 296 - plugin_position (optional) 297 """ 298 form = PluginAddValidationForm(request.GET) 299 300 if not form.is_valid(): 301 # list() is necessary for python 3 compatibility. 302 # errors is s dict mapping fields to a list of errors 303 # for that field. 304 error = list(form.errors.values())[0][0] 305 return HttpResponseBadRequest(force_text(error)) 306 307 plugin_data = form.cleaned_data 308 placeholder = plugin_data['placeholder_id'] 309 plugin_type = plugin_data['plugin_type'] 310 311 if not self.has_add_plugin_permission(request, placeholder, plugin_type): 312 message = force_text(_('You do not have permission to add a plugin')) 313 return HttpResponseForbidden(message) 314 315 parent = plugin_data.get('plugin_parent') 316 317 if parent: 318 position = parent.cmsplugin_set.count() 319 else: 320 position = CMSPlugin.objects.filter( 321 parent__isnull=True, 322 language=plugin_data['plugin_language'], 323 placeholder=placeholder, 324 ).count() 325 326 plugin_data['position'] = position 327 328 plugin_class = plugin_pool.get_plugin(plugin_type) 329 plugin_instance = plugin_class(plugin_class.model, self.admin_site) 330 331 # Setting attributes on the form class is perfectly fine. 332 # The form class is created by modelform factory every time 333 # this get_form() method is called. 334 plugin_instance._cms_initial_attributes = { 335 'language': plugin_data['plugin_language'], 336 'placeholder': plugin_data['placeholder_id'], 337 'parent': plugin_data.get('plugin_parent', None), 338 'plugin_type': plugin_data['plugin_type'], 339 'position': plugin_data['position'], 340 } 341 342 response = plugin_instance.add_view(request) 343 344 plugin = getattr(plugin_instance, 'saved_object', None) 345 346 if plugin: 347 plugin.placeholder.mark_as_dirty(plugin.language, clear_cache=False) 348 349 if plugin_instance._operation_token: 350 tree_order = placeholder.get_plugin_tree_order(plugin.parent_id) 351 self._send_post_placeholder_operation( 352 request, 353 operation=operations.ADD_PLUGIN, 354 token=plugin_instance._operation_token, 355 plugin=plugin, 356 placeholder=plugin.placeholder, 357 tree_order=tree_order, 358 ) 359 return response 360 361 @method_decorator(require_POST) 362 @xframe_options_sameorigin 363 @transaction.atomic 364 def copy_plugins(self, request): 365 """ 366 POST request should have the following data: 367 368 - cms_path 369 - source_language 370 - source_placeholder_id 371 - source_plugin_id (optional) 372 - target_language 373 - target_placeholder_id 374 - target_plugin_id (deprecated/unused) 375 """ 376 source_placeholder_id = request.POST['source_placeholder_id'] 377 target_language = request.POST['target_language'] 378 target_placeholder_id = request.POST['target_placeholder_id'] 379 source_placeholder = get_object_or_404(Placeholder, pk=source_placeholder_id) 380 target_placeholder = get_object_or_404(Placeholder, pk=target_placeholder_id) 381 382 if not target_language or not target_language in get_language_list(): 383 return HttpResponseBadRequest(force_text(_("Language must be set to a supported language!"))) 384 385 copy_to_clipboard = target_placeholder.pk == request.toolbar.clipboard.pk 386 source_plugin_id = request.POST.get('source_plugin_id', None) 387 388 if copy_to_clipboard and source_plugin_id: 389 new_plugin = self._copy_plugin_to_clipboard( 390 request, 391 source_placeholder, 392 target_placeholder, 393 ) 394 new_plugins = [new_plugin] 395 elif copy_to_clipboard: 396 new_plugin = self._copy_placeholder_to_clipboard( 397 request, 398 source_placeholder, 399 target_placeholder, 400 ) 401 new_plugins = [new_plugin] 402 else: 403 new_plugins = self._add_plugins_from_placeholder( 404 request, 405 source_placeholder, 406 target_placeholder, 407 ) 408 data = get_plugin_tree_as_json(request, new_plugins) 409 return HttpResponse(data, content_type='application/json') 410 411 def _copy_plugin_to_clipboard(self, request, source_placeholder, target_placeholder): 412 source_language = request.POST['source_language'] 413 source_plugin_id = request.POST.get('source_plugin_id') 414 target_language = request.POST['target_language'] 415 416 source_plugin = get_object_or_404( 417 CMSPlugin, 418 pk=source_plugin_id, 419 language=source_language, 420 ) 421 422 old_plugins = ( 423 CMSPlugin 424 .get_tree(parent=source_plugin) 425 .filter(placeholder=source_placeholder) 426 .order_by('path') 427 ) 428 429 if not self.has_copy_plugins_permission(request, old_plugins): 430 message = _('You do not have permission to copy these plugins.') 431 raise PermissionDenied(force_text(message)) 432 433 # Empty the clipboard 434 target_placeholder.clear() 435 436 plugin_pairs = copy_plugins.copy_plugins_to( 437 old_plugins, 438 to_placeholder=target_placeholder, 439 to_language=target_language, 440 ) 441 return plugin_pairs[0][0] 442 443 def _copy_placeholder_to_clipboard(self, request, source_placeholder, target_placeholder): 444 source_language = request.POST['source_language'] 445 target_language = request.POST['target_language'] 446 447 # User is copying the whole placeholder to the clipboard. 448 old_plugins = source_placeholder.get_plugins_list(language=source_language) 449 450 if not self.has_copy_plugins_permission(request, old_plugins): 451 message = _('You do not have permission to copy this placeholder.') 452 raise PermissionDenied(force_text(message)) 453 454 # Empty the clipboard 455 target_placeholder.clear() 456 457 # Create a PlaceholderReference plugin which in turn 458 # creates a blank placeholder called "clipboard" 459 # the real clipboard has the reference placeholder inside but the plugins 460 # are inside of the newly created blank clipboard. 461 # This allows us to wrap all plugins in the clipboard under one plugin 462 reference = PlaceholderReference.objects.create( 463 name=source_placeholder.get_label(), 464 plugin_type='PlaceholderPlugin', 465 language=target_language, 466 placeholder=target_placeholder, 467 ) 468 469 copy_plugins.copy_plugins_to( 470 old_plugins, 471 to_placeholder=reference.placeholder_ref, 472 to_language=target_language, 473 ) 474 return reference 475 476 def _add_plugins_from_placeholder(self, request, source_placeholder, target_placeholder): 477 # Plugins are being copied from a placeholder in another language 478 # using the "Copy from language" placeholder operation. 479 source_language = request.POST['source_language'] 480 target_language = request.POST['target_language'] 481 482 old_plugins = source_placeholder.get_plugins_list(language=source_language) 483 484 # Check if the user can copy plugins from source placeholder to 485 # target placeholder. 486 has_permissions = self.has_copy_from_placeholder_permission( 487 request, 488 source_placeholder, 489 target_placeholder, 490 old_plugins, 491 ) 492 493 if not has_permissions: 494 message = _('You do not have permission to copy these plugins.') 495 raise PermissionDenied(force_text(message)) 496 497 target_tree_order = target_placeholder.get_plugin_tree_order( 498 language=target_language, 499 parent_id=None, 500 ) 501 502 operation_token = self._send_pre_placeholder_operation( 503 request, 504 operation=operations.ADD_PLUGINS_FROM_PLACEHOLDER, 505 plugins=old_plugins, 506 source_language=source_language, 507 source_placeholder=source_placeholder, 508 target_language=target_language, 509 target_placeholder=target_placeholder, 510 target_order=target_tree_order, 511 ) 512 513 copied_plugins = copy_plugins.copy_plugins_to( 514 old_plugins, 515 to_placeholder=target_placeholder, 516 to_language=target_language, 517 ) 518 519 new_plugin_ids = (new.pk for new, old in copied_plugins) 520 521 # Creates a list of PKs for the top-level plugins ordered by 522 # their position. 523 top_plugins = (pair for pair in copied_plugins if not pair[0].parent_id) 524 top_plugins_pks = [p[0].pk for p in sorted(top_plugins, key=lambda pair: pair[1].position)] 525 526 # All new plugins are added to the bottom 527 target_tree_order = target_tree_order + top_plugins_pks 528 529 reorder_plugins( 530 target_placeholder, 531 parent_id=None, 532 language=target_language, 533 order=target_tree_order, 534 ) 535 target_placeholder.mark_as_dirty(target_language, clear_cache=False) 536 537 new_plugins = CMSPlugin.objects.filter(pk__in=new_plugin_ids).order_by('path') 538 new_plugins = list(new_plugins) 539 540 self._send_post_placeholder_operation( 541 request, 542 operation=operations.ADD_PLUGINS_FROM_PLACEHOLDER, 543 token=operation_token, 544 plugins=new_plugins, 545 source_language=source_language, 546 source_placeholder=source_placeholder, 547 target_language=target_language, 548 target_placeholder=target_placeholder, 549 target_order=target_tree_order, 550 ) 551 return new_plugins 552 553 @xframe_options_sameorigin 554 def edit_plugin(self, request, plugin_id): 555 try: 556 plugin_id = int(plugin_id) 557 except ValueError: 558 return HttpResponseNotFound(force_text(_("Plugin not found"))) 559 560 obj = self._get_plugin_from_id(plugin_id) 561 562 # CMSPluginBase subclass instance 563 plugin_instance = obj.get_plugin_class_instance(admin=self.admin_site) 564 565 if not self.has_change_plugin_permission(request, obj): 566 return HttpResponseForbidden(force_text(_("You do not have permission to edit this plugin"))) 567 568 response = plugin_instance.change_view(request, str(plugin_id)) 569 570 plugin = getattr(plugin_instance, 'saved_object', None) 571 572 if plugin: 573 plugin.placeholder.mark_as_dirty(plugin.language, clear_cache=False) 574 575 if plugin_instance._operation_token: 576 self._send_post_placeholder_operation( 577 request, 578 operation=operations.CHANGE_PLUGIN, 579 token=plugin_instance._operation_token, 580 old_plugin=obj, 581 new_plugin=plugin, 582 placeholder=plugin.placeholder, 583 ) 584 return response 585 586 @method_decorator(require_POST) 587 @xframe_options_sameorigin 588 @transaction.atomic 589 def move_plugin(self, request): 590 """ 591 Performs a move or a "paste" operation (when «move_a_copy» is set) 592 593 POST request with following parameters: 594 - plugin_id 595 - placeholder_id 596 - plugin_language (optional) 597 - plugin_parent (optional) 598 - plugin_order (array, optional) 599 - move_a_copy (Boolean, optional) (anything supplied here except a case- 600 insensitive "false" is True) 601 NOTE: If move_a_copy is set, the plugin_order should contain an item 602 '__COPY__' with the desired destination of the copied plugin. 603 """ 604 # plugin_id and placeholder_id are required, so, if nothing is supplied, 605 # an ValueError exception will be raised by get_int(). 606 try: 607 plugin_id = get_int(request.POST.get('plugin_id')) 608 except TypeError: 609 raise RuntimeError("'plugin_id' is a required parameter.") 610 611 plugin = self._get_plugin_from_id(plugin_id) 612 613 try: 614 placeholder_id = get_int(request.POST.get('placeholder_id')) 615 except TypeError: 616 raise RuntimeError("'placeholder_id' is a required parameter.") 617 except ValueError: 618 raise RuntimeError("'placeholder_id' must be an integer string.") 619 620 placeholder = Placeholder.objects.get(pk=placeholder_id) 621 622 # The rest are optional 623 parent_id = get_int(request.POST.get('plugin_parent', ""), None) 624 target_language = request.POST['target_language'] 625 move_a_copy = request.POST.get('move_a_copy') 626 move_a_copy = (move_a_copy and move_a_copy != "0" and 627 move_a_copy.lower() != "false") 628 move_to_clipboard = placeholder == request.toolbar.clipboard 629 source_placeholder = plugin.placeholder 630 631 order = request.POST.getlist("plugin_order[]") 632 633 if placeholder != source_placeholder: 634 try: 635 template = self.get_placeholder_template(request, placeholder) 636 has_reached_plugin_limit(placeholder, plugin.plugin_type, 637 target_language, template=template) 638 except PluginLimitReached as er: 639 return HttpResponseBadRequest(er) 640 641 # order should be a list of plugin primary keys 642 # it's important that the plugins being referenced 643 # are all part of the same tree. 644 exclude_from_order_check = ['__COPY__', str(plugin.pk)] 645 ordered_plugin_ids = [int(pk) for pk in order if pk not in exclude_from_order_check] 646 plugins_in_tree_count = ( 647 placeholder 648 .get_plugins(target_language) 649 .filter(parent=parent_id, pk__in=ordered_plugin_ids) 650 .count() 651 ) 652 653 if len(ordered_plugin_ids) != plugins_in_tree_count: 654 # order does not match the tree on the db 655 message = _('order parameter references plugins in different trees') 656 return HttpResponseBadRequest(force_text(message)) 657 658 # True if the plugin is not being moved from the clipboard 659 # to a placeholder or from a placeholder to the clipboard. 660 move_a_plugin = not move_a_copy and not move_to_clipboard 661 662 if parent_id and plugin.parent_id != parent_id: 663 target_parent = get_object_or_404(CMSPlugin, pk=parent_id) 664 665 if move_a_plugin and target_parent.placeholder_id != placeholder.pk: 666 return HttpResponseBadRequest(force_text( 667 _('parent must be in the same placeholder'))) 668 669 if move_a_plugin and target_parent.language != target_language: 670 return HttpResponseBadRequest(force_text( 671 _('parent must be in the same language as ' 672 'plugin_language'))) 673 elif parent_id: 674 target_parent = plugin.parent 675 else: 676 target_parent = None 677 678 new_plugin = None 679 fetch_tree = False 680 681 if move_a_copy and plugin.plugin_type == "PlaceholderPlugin": 682 new_plugins = self._paste_placeholder( 683 request, 684 plugin=plugin, 685 target_language=target_language, 686 target_placeholder=placeholder, 687 tree_order=order, 688 ) 689 elif move_a_copy: 690 fetch_tree = True 691 new_plugin = self._paste_plugin( 692 request, 693 plugin=plugin, 694 target_parent=target_parent, 695 target_language=target_language, 696 target_placeholder=placeholder, 697 tree_order=order, 698 ) 699 elif move_to_clipboard: 700 new_plugin = self._cut_plugin( 701 request, 702 plugin=plugin, 703 target_language=target_language, 704 target_placeholder=placeholder, 705 ) 706 new_plugins = [new_plugin] 707 else: 708 fetch_tree = True 709 new_plugin = self._move_plugin( 710 request, 711 plugin=plugin, 712 target_parent=target_parent, 713 target_language=target_language, 714 target_placeholder=placeholder, 715 tree_order=order, 716 ) 717 718 if new_plugin and fetch_tree: 719 root = (new_plugin.parent or new_plugin) 720 new_plugins = [root] + list(root.get_descendants().order_by('path')) 721 722 # Mark the target placeholder as dirty 723 placeholder.mark_as_dirty(target_language) 724 725 if placeholder != source_placeholder: 726 # Plugin is being moved or copied into a separate placeholder 727 # Mark source placeholder as dirty 728 source_placeholder.mark_as_dirty(plugin.language) 729 data = get_plugin_tree_as_json(request, new_plugins) 730 return HttpResponse(data, content_type='application/json') 731 732 def _paste_plugin(self, request, plugin, target_language, 733 target_placeholder, tree_order, target_parent=None): 734 plugins = ( 735 CMSPlugin 736 .get_tree(parent=plugin) 737 .filter(placeholder=plugin.placeholder_id) 738 .order_by('path') 739 ) 740 plugins = list(plugins) 741 742 if not self.has_copy_from_clipboard_permission(request, target_placeholder, plugins): 743 message = force_text(_("You have no permission to paste this plugin")) 744 raise PermissionDenied(message) 745 746 if target_parent: 747 target_parent_id = target_parent.pk 748 else: 749 target_parent_id = None 750 751 target_tree_order = [int(pk) for pk in tree_order if not pk == '__COPY__'] 752 753 action_token = self._send_pre_placeholder_operation( 754 request, 755 operation=operations.PASTE_PLUGIN, 756 plugin=plugin, 757 target_language=target_language, 758 target_placeholder=target_placeholder, 759 target_parent_id=target_parent_id, 760 target_order=target_tree_order, 761 ) 762 763 plugin_pairs = copy_plugins.copy_plugins_to( 764 plugins, 765 to_placeholder=target_placeholder, 766 to_language=target_language, 767 parent_plugin_id=target_parent_id, 768 ) 769 root_plugin = plugin_pairs[0][0] 770 771 # If an ordering was supplied, replace the item that has 772 # been copied with the new copy 773 target_tree_order.insert(tree_order.index('__COPY__'), root_plugin.pk) 774 775 reorder_plugins( 776 target_placeholder, 777 parent_id=target_parent_id, 778 language=target_language, 779 order=target_tree_order, 780 ) 781 target_placeholder.mark_as_dirty(target_language, clear_cache=False) 782 783 # Fetch from db to update position and other tree values 784 root_plugin.refresh_from_db() 785 786 self._send_post_placeholder_operation( 787 request, 788 operation=operations.PASTE_PLUGIN, 789 plugin=root_plugin.get_bound_plugin(), 790 token=action_token, 791 target_language=target_language, 792 target_placeholder=target_placeholder, 793 target_parent_id=target_parent_id, 794 target_order=target_tree_order, 795 ) 796 return root_plugin 797 798 def _paste_placeholder(self, request, plugin, target_language, 799 target_placeholder, tree_order): 800 plugins = plugin.placeholder_ref.get_plugins_list() 801 802 if not self.has_copy_from_clipboard_permission(request, target_placeholder, plugins): 803 message = force_text(_("You have no permission to paste this placeholder")) 804 raise PermissionDenied(message) 805 806 target_tree_order = [int(pk) for pk in tree_order if not pk == '__COPY__'] 807 808 action_token = self._send_pre_placeholder_operation( 809 request, 810 operation=operations.PASTE_PLACEHOLDER, 811 plugins=plugins, 812 target_language=target_language, 813 target_placeholder=target_placeholder, 814 target_order=target_tree_order, 815 ) 816 817 new_plugins = copy_plugins.copy_plugins_to( 818 plugins, 819 to_placeholder=target_placeholder, 820 to_language=target_language, 821 ) 822 823 new_plugin_ids = (new.pk for new, old in new_plugins) 824 825 # Creates a list of PKs for the top-level plugins ordered by 826 # their position. 827 top_plugins = (pair for pair in new_plugins if not pair[0].parent_id) 828 top_plugins_pks = [p[0].pk for p in sorted(top_plugins, key=lambda pair: pair[1].position)] 829 830 # If an ordering was supplied, we should replace the item that has 831 # been copied with the new plugins 832 target_tree_order[tree_order.index('__COPY__'):0] = top_plugins_pks 833 834 reorder_plugins( 835 target_placeholder, 836 parent_id=None, 837 language=target_language, 838 order=target_tree_order, 839 ) 840 target_placeholder.mark_as_dirty(target_language, clear_cache=False) 841 842 new_plugins = ( 843 CMSPlugin 844 .objects 845 .filter(pk__in=new_plugin_ids) 846 .order_by('path') 847 .select_related('placeholder') 848 ) 849 new_plugins = list(new_plugins) 850 851 self._send_post_placeholder_operation( 852 request, 853 operation=operations.PASTE_PLACEHOLDER, 854 token=action_token, 855 plugins=new_plugins, 856 target_language=target_language, 857 target_placeholder=target_placeholder, 858 target_order=target_tree_order, 859 ) 860 return new_plugins 861 862 def _move_plugin(self, request, plugin, target_language, 863 target_placeholder, tree_order, target_parent=None): 864 if not self.has_move_plugin_permission(request, plugin, target_placeholder): 865 message = force_text(_("You have no permission to move this plugin")) 866 raise PermissionDenied(message) 867 868 plugin_data = { 869 'language': target_language, 870 'placeholder': target_placeholder, 871 } 872 873 source_language = plugin.language 874 source_placeholder = plugin.placeholder 875 source_tree_order = source_placeholder.get_plugin_tree_order( 876 language=source_language, 877 parent_id=plugin.parent_id, 878 ) 879 880 if target_parent: 881 target_parent_id = target_parent.pk 882 else: 883 target_parent_id = None 884 885 if target_placeholder != source_placeholder: 886 target_tree_order = target_placeholder.get_plugin_tree_order( 887 language=target_language, 888 parent_id=target_parent_id, 889 ) 890 else: 891 target_tree_order = source_tree_order 892 893 action_token = self._send_pre_placeholder_operation( 894 request, 895 operation=operations.MOVE_PLUGIN, 896 plugin=plugin, 897 source_language=source_language, 898 source_placeholder=source_placeholder, 899 source_parent_id=plugin.parent_id, 900 source_order=source_tree_order, 901 target_language=target_language, 902 target_placeholder=target_placeholder, 903 target_parent_id=target_parent_id, 904 target_order=target_tree_order, 905 ) 906 907 if target_parent and plugin.parent != target_parent: 908 # Plugin is being moved to another tree (under another parent) 909 updated_plugin = plugin.update(refresh=True, parent=target_parent, **plugin_data) 910 updated_plugin = updated_plugin.move(target_parent, pos='last-child') 911 elif target_parent: 912 # Plugin is being moved within the same tree (different position, same parent) 913 updated_plugin = plugin.update(refresh=True, **plugin_data) 914 else: 915 # Plugin is being moved to the root (no parent) 916 target = CMSPlugin.get_last_root_node() 917 updated_plugin = plugin.update(refresh=True, parent=None, **plugin_data) 918 updated_plugin = updated_plugin.move(target, pos='right') 919 920 # Update all children to match the parent's 921 # language and placeholder 922 updated_plugin.get_descendants().update(**plugin_data) 923 924 # Avoid query by removing the plugin being moved 925 # from the source order 926 new_source_order = list(source_tree_order) 927 new_source_order.remove(updated_plugin.pk) 928 929 # Reorder all plugins in the target placeholder according to the 930 # passed order 931 new_target_order = [int(pk) for pk in tree_order] 932 reorder_plugins( 933 target_placeholder, 934 parent_id=target_parent_id, 935 language=target_language, 936 order=new_target_order, 937 ) 938 target_placeholder.mark_as_dirty(target_language, clear_cache=False) 939 940 if source_placeholder != target_placeholder: 941 source_placeholder.mark_as_dirty(source_language, clear_cache=False) 942 943 # Refresh plugin to get new tree and position values 944 updated_plugin.refresh_from_db() 945 946 self._send_post_placeholder_operation( 947 request, 948 operation=operations.MOVE_PLUGIN, 949 plugin=updated_plugin.get_bound_plugin(), 950 token=action_token, 951 source_language=source_language, 952 source_placeholder=source_placeholder, 953 source_parent_id=plugin.parent_id, 954 source_order=new_source_order, 955 target_language=target_language, 956 target_placeholder=target_placeholder, 957 target_parent_id=target_parent_id, 958 target_order=new_target_order, 959 ) 960 return updated_plugin 961 962 def _cut_plugin(self, request, plugin, target_language, target_placeholder): 963 if not self.has_move_plugin_permission(request, plugin, target_placeholder): 964 message = force_text(_("You have no permission to cut this plugin")) 965 raise PermissionDenied(message) 966 967 plugin_data = { 968 'language': target_language, 969 'placeholder': target_placeholder, 970 } 971 972 source_language = plugin.language 973 source_placeholder = plugin.placeholder 974 source_tree_order = source_placeholder.get_plugin_tree_order( 975 language=source_language, 976 parent_id=plugin.parent_id, 977 ) 978 979 action_token = self._send_pre_placeholder_operation( 980 request, 981 operation=operations.CUT_PLUGIN, 982 plugin=plugin, 983 clipboard=target_placeholder, 984 clipboard_language=target_language, 985 source_language=source_language, 986 source_placeholder=source_placeholder, 987 source_parent_id=plugin.parent_id, 988 source_order=source_tree_order, 989 ) 990 991 # Empty the clipboard 992 target_placeholder.clear() 993 994 target = CMSPlugin.get_last_root_node() 995 updated_plugin = plugin.update(refresh=True, parent=None, **plugin_data) 996 updated_plugin = updated_plugin.move(target, pos='right') 997 998 # Update all children to match the parent's 999 # language and placeholder (clipboard) 1000 updated_plugin.get_descendants().update(**plugin_data) 1001 1002 # Avoid query by removing the plugin being moved 1003 # from the source order 1004 new_source_order = list(source_tree_order) 1005 new_source_order.remove(updated_plugin.pk) 1006 1007 source_placeholder.mark_as_dirty(target_language, clear_cache=False) 1008 1009 self._send_post_placeholder_operation( 1010 request, 1011 operation=operations.CUT_PLUGIN, 1012 token=action_token, 1013 plugin=updated_plugin.get_bound_plugin(), 1014 clipboard=target_placeholder, 1015 clipboard_language=target_language, 1016 source_language=source_language, 1017 source_placeholder=source_placeholder, 1018 source_parent_id=plugin.parent_id, 1019 source_order=new_source_order, 1020 ) 1021 return updated_plugin 1022 1023 @xframe_options_sameorigin 1024 def delete_plugin(self, request, plugin_id): 1025 plugin = self._get_plugin_from_id(plugin_id) 1026 1027 if not self.has_delete_plugin_permission(request, plugin): 1028 return HttpResponseForbidden(force_text( 1029 _("You do not have permission to delete this plugin"))) 1030 1031 opts = plugin._meta 1032 using = router.db_for_write(opts.model) 1033 if DJANGO_2_0: 1034 get_deleted_objects_additional_kwargs = { 1035 'opts': opts, 1036 'using': using, 1037 'user': request.user, 1038 } 1039 else: 1040 get_deleted_objects_additional_kwargs = {'request': request} 1041 deleted_objects, __, perms_needed, protected = get_deleted_objects( 1042 [plugin], admin_site=self.admin_site, 1043 **get_deleted_objects_additional_kwargs 1044 ) 1045 1046 if request.POST: # The user has already confirmed the deletion. 1047 if perms_needed: 1048 raise PermissionDenied(_("You do not have permission to delete this plugin")) 1049 obj_display = force_text(plugin) 1050 placeholder = plugin.placeholder 1051 plugin_tree_order = placeholder.get_plugin_tree_order( 1052 language=plugin.language, 1053 parent_id=plugin.parent_id, 1054 ) 1055 1056 operation_token = self._send_pre_placeholder_operation( 1057 request, 1058 operation=operations.DELETE_PLUGIN, 1059 plugin=plugin, 1060 placeholder=placeholder, 1061 tree_order=plugin_tree_order, 1062 ) 1063 1064 plugin.delete() 1065 placeholder.mark_as_dirty(plugin.language, clear_cache=False) 1066 reorder_plugins( 1067 placeholder=placeholder, 1068 parent_id=plugin.parent_id, 1069 language=plugin.language, 1070 ) 1071 1072 self.log_deletion(request, plugin, obj_display) 1073 self.message_user(request, _('The %(name)s plugin "%(obj)s" was deleted successfully.') % { 1074 'name': force_text(opts.verbose_name), 'obj': force_text(obj_display)}) 1075 1076 # Avoid query by removing the plugin being deleted 1077 # from the tree order list 1078 new_plugin_tree_order = list(plugin_tree_order) 1079 new_plugin_tree_order.remove(plugin.pk) 1080 1081 self._send_post_placeholder_operation( 1082 request, 1083 operation=operations.DELETE_PLUGIN, 1084 token=operation_token, 1085 plugin=plugin, 1086 placeholder=placeholder, 1087 tree_order=new_plugin_tree_order, 1088 ) 1089 return HttpResponseRedirect(admin_reverse('index', current_app=self.admin_site.name)) 1090 1091 plugin_name = force_text(plugin.get_plugin_class().name) 1092 1093 if perms_needed or protected: 1094 title = _("Cannot delete %(name)s") % {"name": plugin_name} 1095 else: 1096 title = _("Are you sure?") 1097 context = { 1098 "title": title, 1099 "object_name": plugin_name, 1100 "object": plugin, 1101 "deleted_objects": deleted_objects, 1102 "perms_lacking": perms_needed, 1103 "protected": protected, 1104 "opts": opts, 1105 "app_label": opts.app_label, 1106 } 1107 request.current_app = self.admin_site.name 1108 return TemplateResponse( 1109 request, "admin/cms/page/plugin/delete_confirmation.html", context 1110 ) 1111 1112 @xframe_options_sameorigin 1113 def clear_placeholder(self, request, placeholder_id): 1114 placeholder = get_object_or_404(Placeholder, pk=placeholder_id) 1115 language = request.GET.get('language') 1116 1117 if placeholder.pk == request.toolbar.clipboard.pk: 1118 # User is clearing the clipboard, no need for permission 1119 # checks here as the clipboard is unique per user. 1120 # There could be a case where a plugin has relationship to 1121 # an object the user does not have permission to delete. 1122 placeholder.clear(language) 1123 return HttpResponseRedirect(admin_reverse('index', current_app=self.admin_site.name)) 1124 1125 if not self.has_clear_placeholder_permission(request, placeholder, language): 1126 return HttpResponseForbidden(force_text(_("You do not have permission to clear this placeholder"))) 1127 1128 opts = Placeholder._meta 1129 using = router.db_for_write(Placeholder) 1130 plugins = placeholder.get_plugins_list(language) 1131 1132 if DJANGO_2_0: 1133 get_deleted_objects_additional_kwargs = { 1134 'opts': opts, 1135 'using': using, 1136 'user': request.user, 1137 } 1138 else: 1139 get_deleted_objects_additional_kwargs = {'request': request} 1140 deleted_objects, __, perms_needed, protected = get_deleted_objects( 1141 plugins, admin_site=self.admin_site, 1142 **get_deleted_objects_additional_kwargs 1143 ) 1144 1145 obj_display = force_text(placeholder) 1146 1147 if request.POST: 1148 # The user has already confirmed the deletion. 1149 if perms_needed: 1150 return HttpResponseForbidden(force_text(_("You do not have permission to clear this placeholder"))) 1151 1152 operation_token = self._send_pre_placeholder_operation( 1153 request, 1154 operation=operations.CLEAR_PLACEHOLDER, 1155 plugins=plugins, 1156 placeholder=placeholder, 1157 ) 1158 1159 placeholder.clear(language) 1160 placeholder.mark_as_dirty(language, clear_cache=False) 1161 1162 self.log_deletion(request, placeholder, obj_display) 1163 self.message_user(request, _('The placeholder "%(obj)s" was cleared successfully.') % { 1164 'obj': obj_display}) 1165 1166 self._send_post_placeholder_operation( 1167 request, 1168 operation=operations.CLEAR_PLACEHOLDER, 1169 token=operation_token, 1170 plugins=plugins, 1171 placeholder=placeholder, 1172 ) 1173 return HttpResponseRedirect(admin_reverse('index', current_app=self.admin_site.name)) 1174 1175 if perms_needed or protected: 1176 title = _("Cannot delete %(name)s") % {"name": obj_display} 1177 else: 1178 title = _("Are you sure?") 1179 1180 context = { 1181 "title": title, 1182 "object_name": _("placeholder"), 1183 "object": placeholder, 1184 "deleted_objects": deleted_objects, 1185 "perms_lacking": perms_needed, 1186 "protected": protected, 1187 "opts": opts, 1188 "app_label": opts.app_label, 1189 } 1190 request.current_app = self.admin_site.name 1191 return TemplateResponse(request, "admin/cms/page/plugin/delete_confirmation.html", context) 1192