Skip to content

Commit 43c4580

Browse files
authored
Fixed django-cms#6035 -- Don't alter the AdvancedSettingsForm form data (django-cms#6262)
1 parent ab3a0e5 commit 43c4580

File tree

7 files changed

+197
-21
lines changed

7 files changed

+197
-21
lines changed

CHANGELOG.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
deprecation warnings.
1010
* Fixed a bug where the sitemap would ignore the ``public`` setting of the site languages
1111
and thus display hidden languages.
12+
* Fixed an ``AttributeError`` raised when adding or removing apphooks in Django 1.11.
1213

1314

1415
=== 3.4.5 (2017-10-12) ===

cms/admin/forms.py

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -233,8 +233,11 @@ class AdvancedSettingsForm(forms.ModelForm):
233233

234234
# This is really a 'fake' field which does not correspond to any Page attribute
235235
# But creates a stub field to be populate by js
236-
application_configs = forms.ChoiceField(label=_('Application configurations'),
237-
choices=(), required=False, widget=ApplicationConfigSelect)
236+
application_configs = forms.CharField(
237+
label=_('Application configurations'),
238+
required=False,
239+
widget=ApplicationConfigSelect,
240+
)
238241
fieldsets = (
239242
(None, {
240243
'fields': ('overwrite_url', 'redirect'),
@@ -282,32 +285,32 @@ def __init__(self, *args, **kwargs):
282285
if app_configs:
283286
self.fields['application_configs'].widget = ApplicationConfigSelect(
284287
attrs={'id': 'application_configs'},
285-
app_configs=app_configs)
288+
app_configs=app_configs,
289+
)
286290

287291
if page_data.get('application_urls', False) and page_data['application_urls'] in app_configs:
288-
self.fields['application_configs'].choices = [(config.pk, force_text(config)) for config in app_configs[page_data['application_urls']].get_configs()]
292+
configs = app_configs[page_data['application_urls']].get_configs()
293+
self.fields['application_configs'].widget.choices = [(config.pk, force_text(config)) for config in configs]
289294

290-
apphook = page_data.get('application_urls', False)
291295
try:
292-
config = apphook_pool.get_apphook(apphook).get_configs().get(namespace=self.initial['application_namespace'])
296+
config = configs.get(namespace=self.initial['application_namespace'])
293297
self.fields['application_configs'].initial = config.pk
294298
except ObjectDoesNotExist:
295299
# Provided apphook configuration doesn't exist (anymore),
296300
# just skip it
297301
# The user will choose another value anyway
298302
pass
299-
else:
300-
# If app_config apphook is not selected, drop any value
301-
# for application_configs to avoid the field data from
302-
# being validated by the field itself
303-
try:
304-
del self.data['application_configs']
305-
except KeyError:
306-
pass
307303

308304
if 'redirect' in self.fields:
309305
self.fields['redirect'].widget.language = self.fields['language'].initial
310306

307+
def get_apphooks(self):
308+
for hook in apphook_pool.get_apphooks():
309+
yield (hook[0], apphook_pool.get_apphook(hook[0]))
310+
311+
def get_apphooks_with_config(self):
312+
return {key: app for key, app in self.get_apphooks() if app.app_config}
313+
311314
def get_navigation_extenders(self):
312315
return menu_pool.get_menus_by_attribute("cms_enabled", True)
313316

@@ -364,12 +367,29 @@ def clean(self):
364367
instance_namespace = cleaned_data.get('application_namespace', None)
365368
application_config = cleaned_data.get('application_configs', None)
366369
if apphook:
370+
apphooks_with_config = self.get_apphooks_with_config()
371+
367372
# application_config wins over application_namespace
368-
if application_config:
373+
if apphook in apphooks_with_config and application_config:
369374
# the value of the application config namespace is saved in
370375
# the 'usual' namespace field to be backward compatible
371376
# with existing apphooks
372-
config = apphook_pool.get_apphook(apphook).get_configs().get(pk=int(application_config))
377+
try:
378+
appconfig_pk = forms.IntegerField(required=True).to_python(application_config)
379+
except ValidationError:
380+
self._errors['application_configs'] = ErrorList([
381+
_('Invalid application config value')
382+
])
383+
return self.cleaned_data
384+
385+
try:
386+
config = apphooks_with_config[apphook].get_configs().get(pk=appconfig_pk)
387+
except ObjectDoesNotExist:
388+
self._errors['application_configs'] = ErrorList([
389+
_('Invalid application config value')
390+
])
391+
return self.cleaned_data
392+
373393
if self._check_unique_namespace_instance(config.namespace):
374394
# Looks like there's already one with the default instance
375395
# namespace defined.

cms/test_utils/project/sampleapp/admin.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
from cms.admin.placeholderadmin import PlaceholderAdminMixin
21
from django.contrib import admin
3-
from cms.test_utils.project.sampleapp.models import Picture, Category
2+
3+
from cms.admin.placeholderadmin import PlaceholderAdminMixin
4+
from cms.test_utils.project.sampleapp.models import Picture, Category, SampleAppConfig
45

56

67
class PictureInline(admin.StackedInline):
@@ -11,3 +12,4 @@ class CategoryAdmin(PlaceholderAdminMixin, admin.ModelAdmin):
1112
inlines = [PictureInline]
1213

1314
admin.site.register(Category, CategoryAdmin)
15+
admin.site.register(SampleAppConfig)

cms/test_utils/project/sampleapp/cms_apps.py

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1-
from cms.app_base import CMSApp
2-
from cms.test_utils.project.sampleapp.cms_menus import SampleAppMenu, StaticMenu3, StaticMenu4
3-
from cms.apphook_pool import apphook_pool
41
from django.conf.urls import url
2+
from django.core.exceptions import ObjectDoesNotExist
3+
from django.core.urlresolvers import reverse
54
from django.http import HttpResponse
65
from django.utils.translation import ugettext_lazy as _
76

7+
from cms.app_base import CMSApp
8+
from cms.test_utils.project.sampleapp.cms_menus import SampleAppMenu, StaticMenu3, StaticMenu4
9+
from cms.apphook_pool import apphook_pool
10+
11+
from .models import SampleAppConfig
12+
813

914
class SampleApp(CMSApp):
1015
name = _("Sample App")
@@ -15,6 +20,26 @@ class SampleApp(CMSApp):
1520
apphook_pool.register(SampleApp)
1621

1722

23+
class SampleAppWithConfig(CMSApp):
24+
name = _("Sample App with config")
25+
app_config = SampleAppConfig
26+
27+
def get_urls(self, page=None, language=None, **kwargs):
28+
return ["cms.test_utils.project.sampleapp.urls_sample_config"]
29+
30+
def get_configs(self):
31+
return self.app_config.objects.all()
32+
33+
def get_config(self, namespace):
34+
try:
35+
return self.app_config.objects.get(namespace=namespace)
36+
except ObjectDoesNotExist:
37+
return None
38+
39+
def get_config_add_url(self):
40+
return reverse('admin:%s_%s_add' % (self.app_config._meta.app_label, self.app_config._meta.model_name))
41+
42+
1843
class SampleAppWithExcludedPermissions(CMSApp):
1944
name = _("Sample App with excluded permissions")
2045
urls = [

cms/test_utils/project/sampleapp/models.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,11 @@ class Meta:
2424
class Picture(models.Model):
2525
image = models.ImageField(upload_to="pictures")
2626
category = models.ForeignKey(Category)
27+
28+
29+
class SampleAppConfig(models.Model):
30+
namespace = models.CharField(
31+
default=None,
32+
max_length=100,
33+
unique=True,
34+
)
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from django.conf.urls import url
2+
3+
from . import views
4+
5+
6+
urlpatterns = [
7+
url(r'^$', views.sample_view, {'message': 'sample root page',}, name='sample-config-root'),
8+
]

cms/tests/test_page_admin.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
# -*- coding: utf-8 -*-
22
import datetime
3+
import sys
34

45
from django.core.cache import cache
6+
from django.core.urlresolvers import clear_url_caches
57
from django.contrib import admin
68
from django.contrib.sites.models import Site
79
from django.forms.models import model_to_dict
@@ -16,6 +18,7 @@
1618
from cms.admin.forms import AdvancedSettingsForm
1719
from cms.admin.pageadmin import PageAdmin
1820
from cms.api import create_page, add_plugin, create_title
21+
from cms.appresolver import clear_app_resolvers
1922
from cms.constants import (
2023
PUBLISHER_STATE_DEFAULT,
2124
PUBLISHER_STATE_DIRTY,
@@ -30,6 +33,7 @@
3033
CMSTestCase, URL_CMS_PAGE, URL_CMS_PAGE_MOVE,
3134
URL_CMS_PAGE_ADVANCED_CHANGE, URL_CMS_PAGE_CHANGE, URL_CMS_PAGE_ADD
3235
)
36+
from cms.test_utils.project.sampleapp.models import SampleAppConfig
3337
from cms.test_utils.util.context_managers import LanguageOverride, UserLoginContext
3438
from cms.utils import get_cms_setting
3539
from cms.utils.compat.dj import installed_apps
@@ -1046,6 +1050,114 @@ def test_advanced_settings_form(self):
10461050
self.assertEqual([no_translation_error],
10471051
form.errors['__all__'])
10481052

1053+
@override_settings(CMS_APPHOOKS=[
1054+
'cms.test_utils.project.sampleapp.cms_apps.SampleApp',
1055+
'cms.test_utils.project.sampleapp.cms_apps.SampleAppWithConfig',
1056+
])
1057+
def test_advanced_settings_form_apphook_config(self):
1058+
clear_app_resolvers()
1059+
clear_url_caches()
1060+
1061+
if 'cms.test_utils.project.sampleapp.cms_apps' in sys.modules:
1062+
del sys.modules['cms.test_utils.project.sampleapp.cms_apps']
1063+
1064+
self.apphook_clear()
1065+
1066+
superuser = self.get_superuser()
1067+
app_config = SampleAppConfig.objects.create(namespace='sample')
1068+
cms_page = create_page('app', 'nav_playground.html', 'en', published=True)
1069+
cms_pages = Page.objects.filter(pk__in=[cms_page.pk, cms_page.publisher_public_id])
1070+
redirect_to = self.get_admin_url(Page, 'changelist')
1071+
endpoint = self.get_admin_url(Page, 'advanced', cms_page.pk)
1072+
page_data = {
1073+
"redirect": "",
1074+
"language": "en",
1075+
"reverse_id": "",
1076+
"navigation_extenders": "",
1077+
"site": "1",
1078+
"xframe_options": "0",
1079+
"application_urls": "SampleAppWithConfig",
1080+
"application_configs": app_config.pk,
1081+
"application_namespace": "sampleapp",
1082+
"overwrite_url": "",
1083+
"template": "INHERIT",
1084+
}
1085+
1086+
with self.login_user_context(superuser):
1087+
# set the apphook config
1088+
response = self.client.post(endpoint, page_data)
1089+
self.assertRedirects(response, redirect_to)
1090+
self.assertEqual(
1091+
cms_pages.filter(
1092+
application_urls='SampleAppWithConfig',
1093+
application_namespace=app_config.namespace,
1094+
).count(),
1095+
2
1096+
)
1097+
1098+
with self.login_user_context(superuser):
1099+
# change from apphook with config to normal apphook
1100+
page_data['application_urls'] = 'SampleApp'
1101+
page_data['application_namespace'] = 'sampleapp'
1102+
response = self.client.post(endpoint, page_data)
1103+
self.assertRedirects(response, redirect_to)
1104+
self.assertEqual(
1105+
cms_pages.filter(
1106+
application_urls='SampleApp',
1107+
application_namespace='sampleapp',
1108+
).count(),
1109+
2
1110+
)
1111+
1112+
with self.login_user_context(superuser):
1113+
# set the apphook config again
1114+
page_data['application_urls'] = 'SampleAppWithConfig'
1115+
page_data['application_namespace'] = 'sampleapp'
1116+
response = self.client.post(endpoint, page_data)
1117+
self.assertRedirects(response, redirect_to)
1118+
self.assertEqual(
1119+
cms_pages.filter(
1120+
application_urls='SampleAppWithConfig',
1121+
application_namespace=app_config.namespace,
1122+
).count(),
1123+
2
1124+
)
1125+
1126+
with self.login_user_context(superuser):
1127+
# change the apphook config to an invalid value
1128+
expected_error = '<ul class="errorlist"><li>Invalid application config value</li></ul>'
1129+
page_data['application_configs'] = '2'
1130+
response = self.client.post(endpoint, page_data)
1131+
self.assertEqual(response.status_code, 200)
1132+
self.assertContains(response, expected_error)
1133+
self.assertEqual(
1134+
cms_pages.filter(
1135+
application_urls='SampleAppWithConfig',
1136+
application_namespace=app_config.namespace,
1137+
).count(),
1138+
2
1139+
)
1140+
1141+
with self.login_user_context(superuser):
1142+
# remove the apphook
1143+
page_data['application_urls'] = ''
1144+
page_data['application_namespace'] = ''
1145+
response = self.client.post(endpoint, page_data)
1146+
self.assertRedirects(response, redirect_to)
1147+
self.assertEqual(
1148+
cms_pages.filter(
1149+
application_urls='',
1150+
application_namespace=None,
1151+
).count(),
1152+
2,
1153+
)
1154+
clear_app_resolvers()
1155+
clear_url_caches()
1156+
1157+
if 'cms.test_utils.project.sampleapp.cms_apps' in sys.modules:
1158+
del sys.modules['cms.test_utils.project.sampleapp.cms_apps']
1159+
self.apphook_clear()
1160+
10491161
def test_form_url_page_change(self):
10501162
superuser = self.get_superuser()
10511163
with self.login_user_context(superuser):

0 commit comments

Comments
 (0)