Completed
Push — master ( ca28b2...64d3bb )
by Batiste
9s
created

PlaceholderNode.get_render_content()   A

Complexity

Conditions 1

Size

Total Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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