Completed
Push — master ( 5a2fdf...cf9e20 )
by Batiste
01:43
created

PlaceholderNode.render()   D

Complexity

Conditions 9

Size

Total Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

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