Completed
Push — master ( 64d3bb...b5d639 )
by Batiste
01:16 queued 11s
created

PlaceholderNode.render()   F

Complexity

Conditions 10

Size

Total Lines 30

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
c 1
b 1
f 0
dl 0
loc 30
rs 3.1304
cc 10

How to fix   Complexity   

Complexity

Complex classes like PlaceholderNode.render() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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 edit_tag(self):
253
        return u"""<!--placeholder ;{};-->""".format(self.name)
254
255
    def render(self, context):
256
        """Output the content of the `PlaceholdeNode` as a template."""
257
        content = self.get_render_content(context)
258
        request = context.get('request')
259
        render_edit_tag = False
260
        if request and request.user.is_staff:
261
            render_edit_tag = True
262
263
        if not content:
264
            if not render_edit_tag:
265
                return ''
266
            return self.edit_tag()
267
        if self.parsed:
268
            try:
269
                t = template.Template(content, name=self.name)
270
                content = mark_safe(t.render(context))
271
            except TemplateSyntaxError as error:
272
                if global_settings.DEBUG:
273
                    content = PLACEHOLDER_ERROR % {
274
                        'name': self.name,
275
                        'error': error,
276
                    }
277
                else:
278
                    content = ''
279
        if self.as_varname is None:
280
            if not render_edit_tag:
281
                return content
282
            return content + self.edit_tag()
283
        context[self.as_varname] = content
284
        return ''
285
286
    def __repr__(self):
287
        return "<Placeholder Node: %s>" % self.name
288
289
290
def get_filename(page, placeholder, data):
291
    """
292
    Generate a stable filename using the orinal filename.
293
    """
294
    name_parts = data.name.split('.')
295
    if len(name_parts) > 1:
296
        name = slugify('.'.join(name_parts[:-1]), allow_unicode=True)
297
        ext = slugify(name_parts[-1])
298
        name = name + '.' + ext
299
    else:
300
        name = slugify(data.name)
301
    filename = os.path.join(
302
        settings.PAGE_UPLOAD_ROOT,
303
        'page_' + str(page.id),
304
        placeholder.ctype + '-' + str(time.time()) + '-' + name
305
    )
306
    return filename
307
308
309
class FilePlaceholderNode(PlaceholderNode):
310
    """A `PlaceholderNode` that saves one file on disk.
311
312
    `PAGE_UPLOAD_ROOT` setting define where to save the file.
313
    """
314
315
    def get_field(self, page, language, initial=None):
316
        help_text = ""
317
        widget = FileInput(page, language)
318
        return FileField(
319
            widget=widget,
320
            initial=initial,
321
            help_text=help_text,
322
            required=False
323
        )
324
325
    def save(self, page, language, data, change, extra_data=None):
326
        if self.shared:
327
            page = None
328
        if extra_data and 'delete' in extra_data:
329
            return super(FilePlaceholderNode, self).save(
330
                page,
331
                language,
332
                "",
333
                change
334
            )
335
        filename = ''
336
        if change and data:
337
            # the image URL is posted if not changed
338
            if not isinstance(data, UploadedFile):
339
                return
340
341
            filename = get_filename(page, self, data)
342
            filename = default_storage.save(filename, data)
343
            return super(FilePlaceholderNode, self).save(
344
                page,
345
                language,
346
                filename,
347
                change
348
            )
349
350
351
class ImagePlaceholderNode(FilePlaceholderNode):
352
    """A `PlaceholderNode` that saves one image on disk.
353
354
    `PAGE_UPLOAD_ROOT` setting define where to save the image.
355
    """
356
357
    def get_field(self, page, language, initial=None):
358
        help_text = ""
359
        widget = ImageInput(page, language)
360
        return ImageField(
361
            widget=widget,
362
            initial=initial,
363
            help_text=help_text,
364
            required=False
365
        )
366
367
368
class ContactForm(forms.Form):
369
    """
370
    Simple contact form
371
    """
372
    email = forms.EmailField(label=_('Your email'))
373
    subject = forms.CharField(
374
        label=_('Subject'), max_length=150)
375
    message = forms.CharField(
376
        widget=forms.Textarea(), label=_('Your message'))
377
378
379
class ContactPlaceholderNode(PlaceholderNode):
380
    """A contact `PlaceholderNode` example."""
381
382
    def render(self, context):
383
        request = context.get('request', None)
384
        if not request:
385
            raise ValueError('request not available in the context.')
386
        if request.method == 'POST':
387
            form = ContactForm(request.POST)
388
            if form.is_valid():
389
                data = form.cleaned_data
390
                recipients = [adm[1] for adm in global_settings.ADMINS]
391
                try:
392
                    send_mail(
393
                        data['subject'], data['message'],
394
                        data['email'], recipients, fail_silently=False)
395
                    return _("Your email has been sent. Thank you.")
396
                except:
397
                    return _("An error as occured: your email has not been sent.")
398
        else:
399
            form = ContactForm()
400
        renderer = render_to_string(
401
            'pages/contact.html', {'form': form}, RequestContext(request))
402
        return mark_safe(renderer)
403
404
405
class JsonPlaceholderNode(PlaceholderNode):
406
    """
407
    A `PlaceholderNode` that try to return a deserialized JSON object
408
    in the template.
409
    """
410
411
    def get_render_content(self, context):
412
        import json
413
        content = self.get_content_from_context(context)
414
        try:
415
            return json.loads(str(content))
416
        except:
417
            logger.warning("JsonPlaceholderNode: coudn't decode json")
418
        return content
419
420
421
class MarkdownPlaceholderNode(PlaceholderNode):
422
    """
423
    A `PlaceholderNode` that return HTML from MarkDown format
424
    """
425
426
    widget = Textarea
427
428
    def render(self, context):
429
        """Render markdown."""
430
        import markdown
431
        content = self.get_content_from_context(context)
432
        return markdown.markdown(content)
433