Repository: django-cms/django-cms

Issue:
Stack placeholders title is always stack_content instead of code name
Found error in plugin_rendering.py, row 154 in function render_placeholder. Should be:

```
if edit:
        toolbar_content = mark_safe(render_placeholder_toolbar(placeholder, context, '', name_fallback=name_fallback))
```

Why does render_placeholder_toolbar take the content argument and doesn't use it? Removing it will make the above adjustment unnecessary.



Discussion:

Resolving patch:
diff --git a/cms/models/fields.py b/cms/models/fields.py
--- a/cms/models/fields.py
+++ b/cms/models/fields.py
@@ -8,10 +8,12 @@
 
 
 class PlaceholderField(models.ForeignKey):
+
     def __init__(self, slotname, default_width=None, actions=PlaceholderNoAction, **kwargs):
-        validate_placeholder_name(slotname)
         if kwargs.get('related_name', None) == '+':
             raise ValueError("PlaceholderField does not support disabling of related names via '+'.")
+        if not callable(slotname):
+            validate_placeholder_name(slotname)
         self.slotname = slotname
         self.default_width = default_width
         self.actions = actions()
@@ -19,22 +21,32 @@ def __init__(self, slotname, default_width=None, actions=PlaceholderNoAction, **
         kwargs.update({'editable': False}) # never allow edits in admin
         super(PlaceholderField, self).__init__(Placeholder, **kwargs)
 
-    def _get_new_placeholder(self):
-        return Placeholder.objects.create(slot=self.slotname,
-            default_width=self.default_width)
+    def _get_new_placeholder(self, instance):
+        return Placeholder.objects.create(slot=self._get_placeholder_slot(instance), default_width=self.default_width)
+
+    def _get_placeholder_slot(self, model_instance):
+        if callable(self.slotname):
+            slotname = self.slotname(model_instance)
+            validate_placeholder_name(slotname)
+        else:
+            slotname = self.slotname
+        return slotname
 
     def pre_save(self, model_instance, add):
         if not model_instance.pk:
-            setattr(model_instance, self.name, self._get_new_placeholder())
+            setattr(model_instance, self.name, self._get_new_placeholder(model_instance))
+        else:
+            slot = self._get_placeholder_slot(model_instance)
+            placeholder = getattr(model_instance, self.name)
+            if placeholder.slot != slot:
+                placeholder.slot = slot
+                placeholder.save()
         return super(PlaceholderField, self).pre_save(model_instance, add)
 
     def save_form_data(self, instance, data):
-        if not instance.pk:
-            data = self._get_new_placeholder()
-        else:
-            data = getattr(instance, self.name)
-            if not isinstance(data, Placeholder):
-                data = self._get_new_placeholder()
+        data = getattr(instance, self.name, '')
+        if not isinstance(data, Placeholder):
+            data = self._get_new_placeholder(model_instance)
         super(PlaceholderField, self).save_form_data(instance, data)
 
     def south_field_triple(self):
diff --git a/cms/stacks/models.py b/cms/stacks/models.py
--- a/cms/stacks/models.py
+++ b/cms/stacks/models.py
@@ -8,6 +8,15 @@
 from cms.models.pluginmodel import CMSPlugin
 
 
+
+def stack_slotname(instance):
+    """
+    Returns a string to be used as the slot
+    for the stack's content placeholder field.
+    """
+    return instance.code
+
+
 @python_2_unicode_compatible
 class Stack(models.Model):
     CREATION_BY_TEMPLATE = 'template'
@@ -22,8 +31,7 @@ class Stack(models.Model):
     code = models.CharField(
         verbose_name=_(u'stack code'), max_length=255, unique=True, blank=True,
         help_text=_(u'To render the stack in templates.'))
-    content = PlaceholderField(
-        slotname=u'stack_content', verbose_name=_(u'stack content'), related_name='stacks_contents')
+    content = PlaceholderField(stack_slotname, verbose_name=_(u'stack content'), related_name='stacks_contents')
 
     creation_method = models.CharField(
         verbose_name=('creation_method'), choices=CREATION_METHODS, default=CREATION_BY_CODE,
diff --git a/cms/utils/placeholder.py b/cms/utils/placeholder.py
--- a/cms/utils/placeholder.py
+++ b/cms/utils/placeholder.py
@@ -1,5 +1,6 @@
 # -*- coding: utf-8 -*-
 from cms.utils import get_cms_setting
+from cms.utils.compat.type_checks import string_types
 from django.conf import settings
 from django.core.exceptions import ImproperlyConfigured
 from django.db.models.query_utils import Q
@@ -41,6 +42,9 @@ def get_page_from_placeholder_if_exists(placeholder):
 
 
 def validate_placeholder_name(name):
+    if not isinstance(name, string_types):
+        raise ImproperlyConfigured("Placeholder identifier names need to be of type string. ")
+
     if not all(ord(char) < 128 for char in name):
         raise ImproperlyConfigured("Placeholder identifiers names may not "
                                    "contain non-ascii characters. If you wish your placeholder "


Test patch:
diff --git a/cms/test_utils/project/placeholderapp/models.py b/cms/test_utils/project/placeholderapp/models.py
--- a/cms/test_utils/project/placeholderapp/models.py
+++ b/cms/test_utils/project/placeholderapp/models.py
@@ -4,6 +4,14 @@
 from hvad.models import TranslatableModel, TranslatedFields
 
 
+def dynamic_placeholder_1(instance):
+    return instance.char_1
+
+
+def dynamic_placeholder_2(instance):
+    return instance.char_2
+
+
 class Example1(models.Model):
     char_1 = models.CharField(u'char_1', max_length=255)
     char_2 = models.CharField(u'char_2', max_length=255)
@@ -21,6 +29,13 @@ class TwoPlaceholderExample(models.Model):
     placeholder_2 = PlaceholderField('placeholder_2', related_name='p2')
 
 
+class DynamicPlaceholderSlotExample(models.Model):
+    char_1 = models.CharField(u'char_1', max_length=255)
+    char_2 = models.CharField(u'char_2', max_length=255)
+    placeholder_1 = PlaceholderField(dynamic_placeholder_1, related_name='dynamic_pl_1')
+    placeholder_2 = PlaceholderField(dynamic_placeholder_2, related_name='dynamic_pl_2')
+
+
 @python_2_unicode_compatible
 class MultilingualExample1(TranslatableModel):
     translations = TranslatedFields(
diff --git a/cms/tests/placeholder.py b/cms/tests/placeholder.py
--- a/cms/tests/placeholder.py
+++ b/cms/tests/placeholder.py
@@ -14,7 +14,12 @@
 from djangocms_text_ckeditor.models import Text
 from cms.test_utils.fixtures.fakemlng import FakemlngFixtures
 from cms.test_utils.project.fakemlng.models import Translations
-from cms.test_utils.project.placeholderapp.models import (Example1, TwoPlaceholderExample, MultilingualExample1)
+from cms.test_utils.project.placeholderapp.models import (
+    Example1,
+    TwoPlaceholderExample,
+    DynamicPlaceholderSlotExample,
+    MultilingualExample1
+)
 from cms.test_utils.testcases import CMSTestCase
 from cms.test_utils.util.context_managers import (SettingsOverride, UserLoginContext)
 from cms.test_utils.util.mock import AttributeObject
@@ -25,7 +30,9 @@
 from django.contrib import admin
 from django.contrib.auth.models import User, Permission
 from django.contrib.messages.storage import default_storage
+from django.core.exceptions import ImproperlyConfigured
 from django.core.urlresolvers import reverse
+from django.db.models import Model
 from django.http import HttpResponseForbidden, HttpResponse
 from django.template import TemplateSyntaxError, Template
 from django.template.context import Context, RequestContext
@@ -248,6 +255,45 @@ def test_placeholder_scanning_nested_super(self):
     def test_placeholder_field_no_related_name(self):
         self.assertRaises(ValueError, PlaceholderField, 'placeholder', related_name='+')
 
+    def test_placeholder_field_valid_slotname(self):
+        self.assertRaises(ImproperlyConfigured, PlaceholderField, 10)
+
+    def test_placeholder_field_dynamic_slot_generation(self):
+        instance = DynamicPlaceholderSlotExample.objects.create(char_1='slot1', char_2='slot2')
+        self.assertEquals(instance.char_1, instance.placeholder_1.slot)
+        self.assertEquals(instance.char_2, instance.placeholder_2.slot)
+
+    def test_placeholder_field_dynamic_slot_update(self):
+        instance = DynamicPlaceholderSlotExample.objects.create(char_1='slot1', char_2='slot2')
+
+        # Plugin counts
+        old_placeholder_1_plugin_count = len(instance.placeholder_1.get_plugins())
+        old_placeholder_2_plugin_count = len(instance.placeholder_2.get_plugins())
+
+        # Switch around the slot names
+        instance.char_1, instance.char_2 = instance.char_2, instance.char_1
+
+        # Store the ids before save, to test that a new placeholder is NOT created.
+        placeholder_1_id = instance.placeholder_1.pk
+        placeholder_2_id = instance.placeholder_2.pk
+
+        # Save instance
+        instance.save()
+
+        current_placeholder_1_plugin_count = len(instance.placeholder_1.get_plugins())
+        current_placeholder_2_plugin_count = len(instance.placeholder_2.get_plugins())
+
+        # Now test that the placeholder slots have changed
+        self.assertEquals(instance.char_2, 'slot1')
+        self.assertEquals(instance.char_1, 'slot2')
+        # Test that a new placeholder was never created
+        self.assertEquals(instance.placeholder_1.pk, placeholder_1_id)
+        self.assertEquals(instance.placeholder_2.pk, placeholder_2_id)
+        # And test the plugin counts remain the same
+        self.assertEqual(old_placeholder_1_plugin_count, current_placeholder_1_plugin_count)
+        self.assertEqual(old_placeholder_2_plugin_count, current_placeholder_2_plugin_count)
+
+
     def test_plugins_language_fallback(self):
         """ Tests language_fallback placeholder configuration """
         page_en = create_page('page_en', 'col_two.html', 'en')
diff --git a/cms/tests/stacks.py b/cms/tests/stacks.py
--- a/cms/tests/stacks.py
+++ b/cms/tests/stacks.py
@@ -40,15 +40,18 @@ def fill_placeholder(self, placeholder=None):
 
     def test_template_creation(self):
         self.assertObjectDoesNotExist(Stack.objects.all(), code='foobar')
+        self.assertObjectDoesNotExist(Placeholder.objects.all(), slot='foobar')
         t = Template('{% load stack_tags %}{% stack "foobar" %}')
         t.render(self.get_context('/'))
         self.assertObjectExist(Stack.objects.all(), code='foobar', creation_method=Stack.CREATION_BY_TEMPLATE)
+        self.assertObjectExist(Placeholder.objects.all(), slot='foobar')
 
     def test_create_stack_from_placeholder(self):
         placeholder = self.fill_placeholder()
         response = self.client.post(reverse('admin:stacks_stack_create_stack', kwargs={'placeholder_id': placeholder.pk}), data={'name': 'foo', 'code': 'bar'})
         new_placeholder = Placeholder.objects.get(stacks_contents__code='bar')
         self.assertEqual(len(placeholder.get_plugins()), len(new_placeholder.get_plugins()))
+        self.assertEqual(new_placeholder.slot, 'bar')
 
     def test_create_stack_from_plugin(self):
         placeholder = self.fill_placeholder()
@@ -56,6 +59,7 @@ def test_create_stack_from_plugin(self):
         response = self.client.post(reverse('admin:stacks_stack_create_stack_from_plugin', kwargs={'placeholder_id': placeholder.pk, 'plugin_id': plugin.pk}), data={'name': 'foo', 'code': 'bar'})
         new_placeholder = Placeholder.objects.get(stacks_contents__code='bar')
         self.assertEqual(len(placeholder.get_plugins()), len(new_placeholder.get_plugins()))
+        self.assertEqual(new_placeholder.slot, 'bar')
 
     def test_insert_stack_plugin(self):
         placeholder = self.fill_placeholder()