1 | """Placeholder module, that's where the smart things happen.""" |
||
2 | from pages.widgets_registry import get_widget |
||
3 | from pages import settings |
||
4 | from pages.models import Content, Media |
||
5 | from pages.widgets import ImageInput, FileInput |
||
6 | from pages.utils import slugify |
||
7 | |||
8 | from django import forms |
||
9 | from django.core.mail import send_mail |
||
10 | from django import template |
||
11 | from django.template import TemplateSyntaxError |
||
12 | from django.core.files.storage import default_storage |
||
13 | from django.forms import Textarea, ImageField, CharField, FileField |
||
14 | from django.forms import TextInput |
||
15 | from django.conf import settings as global_settings |
||
16 | from django.utils.translation import ugettext_lazy as _ |
||
17 | from django.utils.safestring import mark_safe |
||
18 | from django.utils.text import unescape_string_literal |
||
19 | from django.template.loader import render_to_string |
||
20 | from django.template import RequestContext |
||
21 | from django.core.files.uploadedfile import UploadedFile |
||
22 | import logging |
||
23 | import os |
||
24 | import time |
||
25 | import six |
||
26 | import copy |
||
27 | import uuid |
||
28 | |||
29 | logging.basicConfig() |
||
30 | logger = logging.getLogger("pages") |
||
31 | |||
32 | PLACEHOLDER_ERROR = _("[Placeholder %(name)s had syntax error: %(error)s]") |
||
33 | |||
34 | |||
35 | def parse_placeholder(parser, token): |
||
36 | """Parse the `PlaceholderNode` parameters. |
||
37 | |||
38 | Return a tuple with the name and parameters.""" |
||
39 | params = {} |
||
40 | |||
41 | bits = token.split_contents() |
||
42 | count = len(bits) |
||
43 | error_string = '%r tag requires at least one argument' % bits[0] |
||
44 | if count <= 1: |
||
45 | raise TemplateSyntaxError(error_string) |
||
46 | try: |
||
47 | name = unescape_string_literal(bits[1]) |
||
48 | except ValueError: |
||
49 | name = bits[1] |
||
50 | remaining = bits[2:] |
||
51 | simple_options = ['parsed', 'inherited', 'untranslated', 'shared', 'block'] |
||
52 | param_options = ['as', 'on', 'with', 'section'] |
||
53 | all_options = simple_options + param_options |
||
54 | while remaining: |
||
55 | bit = remaining[0] |
||
56 | if bit not in all_options: |
||
57 | raise TemplateSyntaxError( |
||
58 | "%r is not an correct option for a placeholder" % bit) |
||
59 | if bit in param_options: |
||
60 | if len(remaining) < 2: |
||
61 | raise TemplateSyntaxError( |
||
62 | "Placeholder option '%s' need a parameter" % bit) |
||
63 | if bit == 'as': |
||
64 | params['as_varname'] = remaining[1] |
||
65 | if bit == 'with': |
||
66 | params['widget'] = remaining[1] |
||
67 | if bit == 'on': |
||
68 | params['page'] = remaining[1] |
||
69 | if bit == 'section': |
||
70 | params['section'] = unescape_string_literal(remaining[1]) |
||
71 | remaining = remaining[2:] |
||
72 | elif bit == 'parsed': |
||
73 | params['parsed'] = True |
||
74 | remaining = remaining[1:] |
||
75 | elif bit == 'inherited': |
||
76 | params['inherited'] = True |
||
77 | remaining = remaining[1:] |
||
78 | elif bit == 'untranslated': |
||
79 | params['untranslated'] = True |
||
80 | remaining = remaining[1:] |
||
81 | elif bit == 'shared': |
||
82 | params['shared'] = True |
||
83 | remaining = remaining[1:] |
||
84 | elif bit == 'block': |
||
85 | remaining = remaining[1:] |
||
86 | nodelist = parser.parse(('endplaceholder',)) |
||
87 | parser.delete_first_token() |
||
88 | params['nodelist'] = nodelist |
||
89 | |||
90 | return name, params |
||
91 | |||
92 | |||
93 | class PlaceholderNode(template.Node): |
||
94 | """This template node is used to output and save page content and |
||
95 | dynamically generate input fields in the admin. |
||
96 | |||
97 | :param name: the name of the placeholder you want to show/create |
||
98 | :param page: the optional page object |
||
99 | :param widget: the widget you want to use in the admin interface. Take |
||
100 | a look into :mod:`pages.widgets` to see which widgets |
||
101 | are available. |
||
102 | :param parsed: if the ``parsed`` word is given, the content of the |
||
103 | placeholder is evaluated as template code, within the current |
||
104 | context. |
||
105 | :param as_varname: if ``as_varname`` is defined, no value will be |
||
106 | returned. A variable will be created in the context |
||
107 | with the defined name. |
||
108 | :param inherited: inherit content from parent's pages. |
||
109 | :param untranslated: the placeholder's content is the same for |
||
110 | every language. |
||
111 | """ |
||
112 | |||
113 | field = CharField |
||
114 | widget = TextInput |
||
115 | |||
116 | def __init__( |
||
117 | self, name, page=None, widget=None, parsed=False, |
||
118 | as_varname=None, inherited=False, untranslated=False, |
||
119 | has_revision=True, section=None, shared=False, nodelist=None): |
||
120 | """Gather parameters for the `PlaceholderNode`. |
||
121 | |||
122 | These values should be thread safe and don't change between calls.""" |
||
123 | self.page = page or 'current_page' |
||
124 | self.name = name |
||
125 | self.ctype = name.replace(" ", "_") |
||
126 | if widget: |
||
127 | self.widget = widget |
||
128 | self.parsed = parsed |
||
129 | self.inherited = inherited |
||
130 | self.untranslated = untranslated |
||
131 | self.as_varname = as_varname |
||
132 | self.section = section |
||
133 | self.shared = shared |
||
134 | self.nodelist = nodelist or [] # should be an iterable |
||
135 | |||
136 | self.found_in_block = None |
||
137 | |||
138 | def get_widget(self, page, language, fallback=Textarea): |
||
139 | """Given the name of a placeholder return a `Widget` subclass |
||
140 | like Textarea or TextInput.""" |
||
141 | is_str = isinstance(self.widget, six.string_types) |
||
142 | if is_str: |
||
143 | widget = get_widget(self.widget) |
||
144 | else: |
||
145 | widget = self.widget |
||
146 | try: |
||
147 | return widget(page=page, language=language) |
||
148 | except: |
||
149 | pass |
||
150 | return widget() |
||
151 | |||
152 | def get_extra_data(self, data): |
||
153 | """Get eventual extra data for this placeholder from the |
||
154 | admin form. This method is called when the Page is |
||
155 | saved in the admin and passed to the placeholder save |
||
156 | method.""" |
||
157 | result = {} |
||
158 | for key in list(data.keys()): |
||
159 | if key.startswith(self.ctype + '-'): |
||
160 | new_key = key.replace(self.ctype + '-', '') |
||
161 | result[new_key] = data[key] |
||
162 | return result |
||
163 | |||
164 | def get_field(self, page, language, initial=None): |
||
165 | """The field that will be shown within the admin.""" |
||
166 | if self.parsed: |
||
167 | help_text = _('Note: This field is evaluated as template code.') |
||
168 | else: |
||
169 | help_text = '' |
||
170 | widget = self.get_widget(page, language) |
||
171 | return self.field( |
||
172 | widget=widget, initial=initial, |
||
173 | help_text=help_text, required=False) |
||
174 | |||
175 | def save(self, page, language, data, change, extra_data=None): |
||
176 | """Actually save the placeholder data into the Content object.""" |
||
177 | # if this placeholder is untranslated, we save everything |
||
178 | # in the default language |
||
179 | if self.untranslated: |
||
180 | language = settings.PAGE_DEFAULT_LANGUAGE |
||
181 | |||
182 | if self.shared: |
||
183 | page = None |
||
184 | |||
185 | # the page is being changed |
||
186 | if change: |
||
187 | # we need create a new content if revision is enabled |
||
188 | if(settings.PAGE_CONTENT_REVISION and self.name |
||
189 | not in settings.PAGE_CONTENT_REVISION_EXCLUDE_LIST): |
||
190 | Content.objects.create_content_if_changed( |
||
191 | page, |
||
192 | language, |
||
193 | self.ctype, |
||
194 | data |
||
195 | ) |
||
196 | else: |
||
197 | Content.objects.set_or_create_content( |
||
198 | page, |
||
199 | language, |
||
200 | self.ctype, |
||
201 | data |
||
202 | ) |
||
203 | # the page is being added |
||
204 | else: |
||
205 | Content.objects.set_or_create_content( |
||
206 | page, |
||
207 | language, |
||
208 | self.ctype, |
||
209 | data |
||
210 | ) |
||
211 | |||
212 | def get_content(self, page_obj, lang, lang_fallback=True): |
||
213 | if self.untranslated: |
||
214 | lang = settings.PAGE_DEFAULT_LANGUAGE |
||
215 | lang_fallback = False |
||
216 | if self.shared: |
||
217 | return Content.objects.get_content( |
||
218 | None, lang, self.ctype, lang_fallback) |
||
219 | content = Content.objects.get_content( |
||
220 | page_obj, lang, self.ctype, lang_fallback) |
||
221 | if self.inherited and not content: |
||
222 | for ancestor in page_obj.get_ancestors(): |
||
223 | content = Content.objects.get_content( |
||
224 | ancestor, lang, |
||
225 | self.ctype, lang_fallback) |
||
226 | if content: |
||
227 | break |
||
228 | return content |
||
229 | |||
230 | def get_lang(self, context): |
||
231 | if self.untranslated: |
||
232 | lang = settings.PAGE_DEFAULT_LANGUAGE |
||
233 | else: |
||
234 | lang = context.get('lang', settings.PAGE_DEFAULT_LANGUAGE) |
||
235 | return lang |
||
236 | |||
237 | def get_content_from_context(self, context): |
||
238 | if self.untranslated: |
||
239 | lang_fallback = False |
||
240 | else: |
||
241 | lang_fallback = True |
||
242 | |||
243 | if self.shared: |
||
244 | return self.get_content( |
||
245 | None, |
||
246 | self.get_lang(context), |
||
247 | lang_fallback) |
||
248 | if self.page not in context: |
||
249 | return '' |
||
250 | # current_page can be set to None |
||
251 | if not context[self.page]: |
||
252 | return '' |
||
253 | |||
254 | return self.get_content( |
||
255 | context[self.page], |
||
256 | self.get_lang(context), |
||
257 | lang_fallback) |
||
258 | |||
259 | def get_render_content(self, context): |
||
260 | if self.nodelist: |
||
261 | with context.push(): |
||
262 | context['content'] = self.get_content_from_context(context) |
||
263 | output = self.nodelist.render(context) |
||
264 | return mark_safe(output) |
||
265 | return mark_safe(self.get_content_from_context(context)) |
||
266 | |||
267 | def render_parsed(self, context, content): |
||
268 | try: |
||
269 | content_template = template.Template(content, name=self.name) |
||
270 | new_content = mark_safe(content_template.render(context)) |
||
271 | except TemplateSyntaxError as error: |
||
272 | if global_settings.DEBUG: |
||
273 | new_content = PLACEHOLDER_ERROR % { |
||
274 | 'name': self.name, |
||
275 | 'error': error, |
||
276 | } |
||
277 | else: |
||
278 | new_content = '' |
||
279 | return new_content |
||
280 | |||
281 | def edit_tag(self): |
||
282 | return u"""<!--placeholder ;{};-->""".format(self.name) |
||
283 | |||
284 | def render(self, context): |
||
285 | """Output the content of the `PlaceholdeNode` as a template.""" |
||
286 | content = self.get_render_content(context) |
||
287 | request = context.get('request') |
||
288 | render_edit_tag = False |
||
289 | if request and request.user.is_staff and request.COOKIES.get('enable_edit_mode'): |
||
290 | render_edit_tag = True |
||
291 | |||
292 | if not content: |
||
293 | if not render_edit_tag: |
||
294 | return '' |
||
295 | return self.edit_tag() |
||
296 | |||
297 | if self.parsed: |
||
298 | content = self.render_parsed(context, content) |
||
299 | |||
300 | if self.as_varname is None: |
||
301 | if not render_edit_tag: |
||
302 | return content |
||
303 | return content + self.edit_tag() |
||
304 | context[self.as_varname] = content |
||
305 | return '' |
||
306 | |||
307 | def __repr__(self): |
||
308 | return "<Placeholder Node: %s>" % self.name |
||
309 | |||
310 | |||
311 | View Code Duplication | def get_filename(page, content_type, data): |
|
0 ignored issues
–
show
Duplication
introduced
by
![]() |
|||
312 | """ |
||
313 | Generate a stable filename using the original filename of the type. |
||
314 | |||
315 | |||
316 | """ |
||
317 | avoid_collision = uuid.uuid4().hex[:8] |
||
318 | |||
319 | name_parts = data.name.split('.') |
||
320 | if len(name_parts) > 1: |
||
321 | name = slugify('.'.join(name_parts[:-1]), allow_unicode=True) |
||
322 | ext = slugify(name_parts[-1]) |
||
323 | name = name + '.' + ext |
||
324 | else: |
||
325 | name = slugify(data.name) |
||
326 | filename = os.path.join( |
||
327 | settings.PAGE_UPLOAD_ROOT, |
||
328 | 'page_' + str(page.id), |
||
329 | content_type + '-' + avoid_collision + '-' + name |
||
330 | ) |
||
331 | return filename |
||
332 | |||
333 | |||
334 | class FilePlaceholderNode(PlaceholderNode): |
||
335 | """A `PlaceholderNode` that saves one file on disk. |
||
336 | |||
337 | `PAGE_UPLOAD_ROOT` setting define where to save the file. |
||
338 | """ |
||
339 | |||
340 | def get_field(self, page, language, initial=None): |
||
341 | help_text = "" |
||
342 | widget = FileInput(page, language) |
||
343 | return FileField( |
||
344 | widget=widget, |
||
345 | initial=initial, |
||
346 | help_text=help_text, |
||
347 | required=False |
||
348 | ) |
||
349 | |||
350 | def save(self, page, language, data, change, extra_data=None): |
||
351 | if self.shared: |
||
352 | page = None |
||
353 | |||
354 | if extra_data and 'delete' in extra_data: |
||
355 | return super(FilePlaceholderNode, self).save( |
||
356 | page, |
||
357 | language, |
||
358 | "", |
||
359 | change |
||
360 | ) |
||
361 | return |
||
362 | if extra_data and 'revision' in extra_data: |
||
363 | return super(FilePlaceholderNode, self).save( |
||
364 | page, |
||
365 | language, |
||
366 | extra_data['revision'], |
||
367 | change |
||
368 | ) |
||
369 | return |
||
370 | if extra_data and 'selected' in extra_data and extra_data['selected']: |
||
371 | return super(FilePlaceholderNode, self).save( |
||
372 | page, |
||
373 | language, |
||
374 | extra_data['selected'], |
||
375 | change |
||
376 | ) |
||
377 | return |
||
378 | |||
379 | filename = '' |
||
380 | if change and data: |
||
381 | # the image URL is posted if not changed |
||
382 | if not isinstance(data, UploadedFile): |
||
383 | return |
||
384 | |||
385 | filename = get_filename(page, self.ctype, data) |
||
386 | filename = default_storage.save(filename, data) |
||
387 | media = Media(url=filename) |
||
388 | media.save() |
||
389 | return super(FilePlaceholderNode, self).save( |
||
390 | page, |
||
391 | language, |
||
392 | filename, |
||
393 | change |
||
394 | ) |
||
395 | |||
396 | |||
397 | class ImagePlaceholderNode(FilePlaceholderNode): |
||
398 | """A `PlaceholderNode` that saves one image on disk. |
||
399 | |||
400 | `PAGE_UPLOAD_ROOT` setting define where to save the image. |
||
401 | """ |
||
402 | |||
403 | def get_field(self, page, language, initial=None): |
||
404 | help_text = "" |
||
405 | widget = ImageInput(page, language) |
||
406 | return ImageField( |
||
407 | widget=widget, |
||
408 | initial=initial, |
||
409 | help_text=help_text, |
||
410 | required=False |
||
411 | ) |
||
412 | |||
413 | |||
414 | class ContactForm(forms.Form): |
||
415 | """ |
||
416 | Simple contact form |
||
417 | """ |
||
418 | email = forms.EmailField(label=_('Your email')) |
||
419 | subject = forms.CharField( |
||
420 | label=_('Subject'), max_length=150) |
||
421 | message = forms.CharField( |
||
422 | widget=forms.Textarea(), label=_('Your message')) |
||
423 | |||
424 | |||
425 | class ContactPlaceholderNode(PlaceholderNode): |
||
426 | """A contact `PlaceholderNode` example.""" |
||
427 | |||
428 | def render(self, context): |
||
429 | request = context.get('request', None) |
||
430 | if not request: |
||
431 | raise ValueError('request not available in the context.') |
||
432 | if request.method == 'POST': |
||
433 | form = ContactForm(request.POST) |
||
434 | if form.is_valid(): |
||
435 | data = form.cleaned_data |
||
436 | recipients = [adm[1] for adm in global_settings.ADMINS] |
||
437 | try: |
||
438 | send_mail( |
||
439 | data['subject'], data['message'], |
||
440 | data['email'], recipients, fail_silently=False) |
||
441 | return _("Your email has been sent. Thank you.") |
||
442 | except: |
||
443 | return _("An error as occured: your email has not been sent.") |
||
444 | else: |
||
445 | form = ContactForm() |
||
446 | renderer = render_to_string( |
||
447 | 'pages/contact.html', {'form': form}, RequestContext(request)) |
||
448 | return mark_safe(renderer) |
||
449 | |||
450 | |||
451 | class JsonPlaceholderNode(PlaceholderNode): |
||
452 | """ |
||
453 | A `PlaceholderNode` that try to return a deserialized JSON object |
||
454 | in the template. |
||
455 | """ |
||
456 | |||
457 | def get_render_content(self, context): |
||
458 | import json |
||
459 | content = self.get_content_from_context(context) |
||
460 | try: |
||
461 | return json.loads(str(content)) |
||
462 | except: |
||
463 | logger.warning("JsonPlaceholderNode: coudn't decode json") |
||
464 | return content |
||
465 | |||
466 | |||
467 | class MarkdownPlaceholderNode(PlaceholderNode): |
||
468 | """ |
||
469 | A `PlaceholderNode` that return HTML from MarkDown format |
||
470 | """ |
||
471 | |||
472 | widget = Textarea |
||
473 | |||
474 | def render(self, context): |
||
475 | """Render markdown.""" |
||
476 | import markdown |
||
477 | content = self.get_content_from_context(context) |
||
478 | return markdown.markdown(content) |
||
479 |