This repository was archived by the owner on Dec 26, 2023. It is now read-only.
forked from feincms/feincms
-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathmodels.py
More file actions
303 lines (233 loc) · 11.6 KB
/
Copy pathmodels.py
File metadata and controls
303 lines (233 loc) · 11.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
import re
from django.core import urlresolvers
from django.core.urlresolvers import Resolver404, resolve, reverse as _reverse, NoReverseMatch
from django.db import models
from django.http import HttpResponse
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
try:
from functools import partial
except ImportError:
from django.utils.functional import curry as partial
from feincms.admin.editor import ItemEditorForm
from feincms.contrib.fields import JSONField
try:
from threading import local
except ImportError:
from django.utils._threading_local import local
_local = local()
def retrieve_page_information(page):
_local.proximity_info = (page.tree_id, page.lft, page.rght, page.level)
def _empty_reverse_cache():
_local.reverse_cache = {}
APPLICATIONCONTENT_RE = re.compile(r'^([\.\w]+)/([\.\w]+)$')
def reverse(viewname, urlconf=None, args=None, kwargs=None, prefix=None, *vargs, **vkwargs):
"""
This reverse replacement adds two new capabilities to the Django reverse method:
- If reverse is called from inside ``ApplicationContent.process``, it
automatically prepends the URL of the page the ``ApplicationContent``
is attached to, thereby allowing ``reverse`` and ``{% url %}`` to
return correct URLs without hard-coding the application integration
point into the templates or URLconf files.
- If the viewname contains a slash, the part before the slash is
interpreted as the path to an URLconf file. This allows the template
author to resolve URLs only reachable via an ``ApplicationContent``,
even inside another application contents' ``process`` method::
{% url registration.urls/auth_logout %}
"""
if isinstance(viewname, basestring) and APPLICATIONCONTENT_RE.match(viewname):
# try to reverse an URL inside another applicationcontent
other_urlconf, other_viewname = viewname.split('/')
if hasattr(_local, 'urlconf') and other_urlconf == _local.urlconf[0]:
# We are reversing an URL from our own ApplicationContent
return _reverse(other_viewname, other_urlconf, args, kwargs, _local.urlconf[1], *vargs, **vkwargs)
if not hasattr(_local, 'reverse_cache'):
_local.reverse_cache = {}
# Update this when more items are used for the proximity analysis below
proximity_info = getattr(_local, 'proximity_info', None)
if proximity_info:
urlconf_cache_key = '%s_%s' % (other_urlconf, proximity_info[0])
else:
urlconf_cache_key = '%s_noprox' % other_urlconf
if urlconf_cache_key not in _local.reverse_cache:
# TODO do not use internal feincms data structures as much
model_class = ApplicationContent._feincms_content_models[0]
contents = model_class.objects.filter(
urlconf_path=other_urlconf).select_related('parent')
if proximity_info:
# Poor man's proximity analysis. Filter by tree_id :-)
try:
content = contents.get(parent__tree_id=proximity_info[0])
except (model_class.DoesNotExist, model_class.MultipleObjectsReturned):
try:
content = contents[0]
except IndexError:
content = None
else:
try:
content = contents[0]
except IndexError:
content = None
_local.reverse_cache[urlconf_cache_key] = content
else:
content = _local.reverse_cache[urlconf_cache_key]
if content:
# Save information from _urlconfs in case we are inside another
# application contents' ``process`` method currently
saved_cfg = getattr(_local, 'urlconf', None)
# Initialize application content reverse hackery for the other application
_local.urlconf = (other_urlconf, content.parent.get_absolute_url())
try:
url = reverse(other_viewname, other_urlconf, args, kwargs, prefix, *vargs, **vkwargs)
except:
# We really must not fail here. We absolutely need to remove/restore
# the _urlconfs information
url = None
if saved_cfg:
_local.urlconf = saved_cfg
else:
del _local.urlconf
# We found an URL somewhere in here... return it. Otherwise, we continue
# below
if url:
return url
if hasattr(_local, 'urlconf'):
# Special handling inside ApplicationContent.render; override urlconf
# and prefix variables so that reverse works as expected.
urlconf1, prefix1 = _local.urlconf
try:
return _reverse(viewname, urlconf1, args, kwargs, prefix1, *vargs, **vkwargs)
except NoReverseMatch:
# fall through to calling reverse with default arguments
pass
return _reverse(viewname, urlconf, args, kwargs, prefix, *vargs, **vkwargs)
urlresolvers.reverse = reverse
class ApplicationContent(models.Model):
#: parameters is used to serialize instance-specific data which will be
# provided to the view code. This allows customization (e.g. "Embed
# MyBlogApp for blog <slug>")
parameters = JSONField(null=True, editable=False)
ALL_APPS_CONFIG = {}
class Meta:
abstract = True
verbose_name = _('application content')
verbose_name_plural = _('application contents')
@classmethod
def initialize_type(cls, APPLICATIONS):
# Generate a more flexible application configuration structure from
# the legacy pattern:
# TODO: Consider changing the input signature to something cleaner, at
# the cost of a one-time backwards incompatible change
for i in APPLICATIONS:
if not 2 <= len(i) <= 3:
raise ValueError("APPLICATIONS must be provided with tuples containing at least two parameters (urls, name) and an optional extra config dict")
urls, name = i[0:2]
if len(i) == 3:
app_conf = i[2]
if not isinstance(app_conf, dict):
raise ValueError("The third parameter of an APPLICATIONS entry must be a dict or the name of one!")
else:
app_conf = {}
cls.ALL_APPS_CONFIG[urls] = {
"urls": urls,
"name": name,
"config": app_conf
}
application_choices = sorted([(c['urls'], c['name']) for c in cls.ALL_APPS_CONFIG.values()],
key=lambda application: application[1])
cls.add_to_class('urlconf_path',
models.CharField(_('application'), max_length=100, choices=application_choices)
)
class ApplicationContentItemEditorForm(ItemEditorForm):
app_config = {}
custom_fields = {}
def __init__(self, *args, **kwargs):
super(ApplicationContentItemEditorForm, self).__init__(*args, **kwargs)
instance = kwargs.get("instance", None)
if instance:
self.app_config = cls.ALL_APPS_CONFIG[instance.urlconf_path]['config']
admin_fields = self.app_config.get('admin_fields', {})
if isinstance(admin_fields, dict):
self.custom_fields.update(admin_fields)
else:
get_fields = urlresolvers.get_callable(admin_fields)
self.custom_fields.update(get_fields(self, *args, **kwargs))
for k, v in self.custom_fields.items():
self.fields[k] = v
def clean(self, *args, **kwargs):
cleaned_data = super(ApplicationContentItemEditorForm, self).clean(*args, **kwargs)
# TODO: Check for newly added instances so we can force a re-validation of their custom fields
return cleaned_data
def save(self, commit=True, *args, **kwargs):
# Django ModelForms return the model instance from save. We'll
# call save with commit=False first to do any necessary work &
# get the model so we can set .parameters to the values of our
# custom fields before calling save(commit=True)
m = super(ApplicationContentItemEditorForm, self).save(commit=False, *args, **kwargs)
m.parameters = dict((k, self.cleaned_data[k]) for k in self.custom_fields if k in self.cleaned_data)
if commit:
m.save(**kwargs)
return m
#: This provides hooks for us to customize the admin interface for embedded instances:
cls.feincms_item_editor_form = ApplicationContentItemEditorForm
def __init__(self, *args, **kwargs):
super(ApplicationContent, self).__init__(*args, **kwargs)
self.app_config = self.ALL_APPS_CONFIG.get(self.urlconf_path, {}).get('config', {})
def render(self, request, **kwargs):
return getattr(request, "_feincms_applicationcontents", {}).get(self.id, u'')
def process(self, request):
page_url = self.parent.get_absolute_url()
# Get the rest of the URL
# Provide a way for appcontent items to customize URL processing by
# altering the perceived path of the page:
if "path_mapper" in self.app_config:
path_mapper = urlresolvers.get_callable(self.app_config["path_mapper"])
path, page_url = path_mapper(
request.path,
page_url,
appcontent_parameters=self.parameters
)
else:
path = re.sub('^' + re.escape(page_url[:-1]), '', request.path)
# Change the prefix and urlconf for the monkey-patched reverse function ...
_local.urlconf = (self.urlconf_path, page_url)
try:
fn, args, kwargs = resolve(path, self.urlconf_path)
except (ValueError, Resolver404):
del _local.urlconf
raise Resolver404
#: Variables from the ApplicationContent parameters are added to request
# so we can expose them to our templates via the appcontent_parameters
# context_processor
request._feincms_appcontent_parameters.update(self.parameters)
view_wrapper = self.app_config.get("view_wrapper", None)
if view_wrapper:
fn = partial(
urlresolvers.get_callable(view_wrapper),
view=fn,
appcontent_parameters=self.parameters
)
try:
output = fn(request, *args, **kwargs)
except:
# We want exceptions to propagate, but we cannot allow the
# modifications to reverse() to stay here.
del _local.urlconf
raise
# ... and restore it after processing the view
del _local.urlconf
if isinstance(output, HttpResponse):
if output.status_code == 200:
if not getattr(output, 'standalone', False):
request._feincms_applicationcontents[self.id] = mark_safe(output.content.decode('utf-8'))
return output
else:
request._feincms_applicationcontents[self.id] = mark_safe(output)
def save(self, *args, **kwargs):
super(ApplicationContent, self).save(*args, **kwargs)
# Clear reverse() cache
_empty_reverse_cache()
def delete(self, *args, **kwargs):
super(ApplicationContent, self).delete(*args, **kwargs)
# Clear reverse() cache
_empty_reverse_cache()