Completed
Push — master ( 2922cd...a7dc5f )
by Batiste
01:09
created

FilePlaceholderNode   A

Complexity

Total Complexity 10

Size/Duplication

Total Lines 50
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 10
dl 0
loc 50
rs 10
c 0
b 0
f 0

2 Methods

Rating   Name   Duplication   Size   Complexity  
A get_field() 0 8 1
F save() 0 34 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
    # try to parse to an endblock
41
    parser_copy = copy.deepcopy(parser) # do a deep copy to avoid to change the state of the parser
42
    try:
43
        nodelist = parser_copy.parse(('endplaceholder',))
44
    except:
45
        pass
46
    else:
47
        nodelist = parser.parse(('endplaceholder',))
48
        parser.delete_first_token()
49
        params['nodelist'] = nodelist
50
51
52
    bits = token.split_contents()
53
    count = len(bits)
54
    error_string = '%r tag requires at least one argument' % bits[0]
55
    if count <= 1:
56
        raise TemplateSyntaxError(error_string)
57
    try:
58
        name = unescape_string_literal(bits[1])
59
    except ValueError:
60
        name = bits[1]
61
    remaining = bits[2:]
62
    simple_options = ['parsed', 'inherited', 'untranslated', 'shared']
63
    param_options = ['as', 'on', 'with', 'section']
64
    all_options = simple_options + param_options
65
    while remaining:
66
        bit = remaining[0]
67
        if bit not in all_options:
68
            raise TemplateSyntaxError(
69
                "%r is not an correct option for a placeholder" % bit)
70
        if bit in param_options:
71
            if len(remaining) < 2:
72
                raise TemplateSyntaxError(
73
                    "Placeholder option '%s' need a parameter" % bit)
74
            if bit == 'as':
75
                params['as_varname'] = remaining[1]
76
            if bit == 'with':
77
                params['widget'] = remaining[1]
78
            if bit == 'on':
79
                params['page'] = remaining[1]
80
            if bit == 'section':
81
                params['section'] = unescape_string_literal(remaining[1])
82
            remaining = remaining[2:]
83
        elif bit == 'parsed':
84
            params['parsed'] = True
85
            remaining = remaining[1:]
86
        elif bit == 'inherited':
87
            params['inherited'] = True
88
            remaining = remaining[1:]
89
        elif bit == 'untranslated':
90
            params['untranslated'] = True
91
            remaining = remaining[1:]
92
        elif bit == 'shared':
93
            params['shared'] = True
94
            remaining = remaining[1:]
95
    return name, params
96
97
98
class PlaceholderNode(template.Node):
99
    """This template node is used to output and save page content and
100
    dynamically generate input fields in the admin.
101
102
    :param name: the name of the placeholder you want to show/create
103
    :param page: the optional page object
104
    :param widget: the widget you want to use in the admin interface. Take
105
        a look into :mod:`pages.widgets` to see which widgets
106
        are available.
107
    :param parsed: if the ``parsed`` word is given, the content of the
108
        placeholder is evaluated as template code, within the current
109
        context.
110
    :param as_varname: if ``as_varname`` is defined, no value will be
111
        returned. A variable will be created in the context
112
        with the defined name.
113
    :param inherited: inherit content from parent's pages.
114
    :param untranslated: the placeholder's content is the same for
115
        every language.
116
    """
117
118
    field = CharField
119
    widget = TextInput
120
121
    def __init__(
122
            self, name, page=None, widget=None, parsed=False,
123
            as_varname=None, inherited=False, untranslated=False,
124
            has_revision=True, section=None, shared=False, nodelist=None):
125
        """Gather parameters for the `PlaceholderNode`.
126
127
        These values should be thread safe and don't change between calls."""
128
        self.page = page or 'current_page'
129
        self.name = name
130
        self.ctype = name.replace(" ", "_")
131
        if widget:
132
            self.widget = widget
133
        self.parsed = parsed
134
        self.inherited = inherited
135
        self.untranslated = untranslated
136
        self.as_varname = as_varname
137
        self.section = section
138
        self.shared = shared
139
        self.nodelist = nodelist
140
141
        self.found_in_block = None
142
143
    def get_widget(self, page, language, fallback=Textarea):
144
        """Given the name of a placeholder return a `Widget` subclass
145
        like Textarea or TextInput."""
146
        is_str = isinstance(self.widget, six.string_types)
147
        if is_str:
148
            widget = get_widget(self.widget)
149
        else:
150
            widget = self.widget
151
        try:
152
            return widget(page=page, language=language)
153
        except:
154
            pass
155
        return widget()
156
157
    def get_extra_data(self, data):
158
        """Get eventual extra data for this placeholder from the
159
        admin form. This method is called when the Page is
160
        saved in the admin and passed to the placeholder save
161
        method."""
162
        result = {}
163
        for key in list(data.keys()):
164
            if key.startswith(self.ctype + '-'):
165
                new_key = key.replace(self.ctype + '-', '')
166
                result[new_key] = data[key]
167
        return result
168
169
    def get_field(self, page, language, initial=None):
170
        """The field that will be shown within the admin."""
171
        if self.parsed:
172
            help_text = _('Note: This field is evaluated as template code.')
173
        else:
174
            help_text = ''
175
        widget = self.get_widget(page, language)
176
        return self.field(
177
            widget=widget, initial=initial,
178
            help_text=help_text, required=False)
179
180
    def save(self, page, language, data, change, extra_data=None):
181
        """Actually save the placeholder data into the Content object."""
182
        # if this placeholder is untranslated, we save everything
183
        # in the default language
184
        if self.untranslated:
185
            language = settings.PAGE_DEFAULT_LANGUAGE
186
187
        if self.shared:
188
            page = None
189
190
        # the page is being changed
191
        if change:
192
            # we need create a new content if revision is enabled
193
            if(settings.PAGE_CONTENT_REVISION and self.name
194
                    not in settings.PAGE_CONTENT_REVISION_EXCLUDE_LIST):
195
                Content.objects.create_content_if_changed(
196
                    page,
197
                    language,
198
                    self.ctype,
199
                    data
200
                )
201
            else:
202
                Content.objects.set_or_create_content(
203
                    page,
204
                    language,
205
                    self.ctype,
206
                    data
207
                )
208
        # the page is being added
209
        else:
210
            Content.objects.set_or_create_content(
211
                page,
212
                language,
213
                self.ctype,
214
                data
215
            )
216
217
    def get_content(self, page_obj, lang, lang_fallback=True):
218
        if self.untranslated:
219
            lang = settings.PAGE_DEFAULT_LANGUAGE
220
            lang_fallback = False
221
        if self.shared:
222
            return Content.objects.get_content(
223
                None, lang, self.ctype, lang_fallback)
224
        content = Content.objects.get_content(
225
            page_obj, lang, self.ctype, lang_fallback)
226
        if self.inherited and not content:
227
            for ancestor in page_obj.get_ancestors():
228
                content = Content.objects.get_content(
229
                    ancestor, lang,
230
                    self.ctype, lang_fallback)
231
                if content:
232
                    break
233
        return content
234
235
    def get_lang(self, context):
236
        if self.untranslated:
237
            lang = settings.PAGE_DEFAULT_LANGUAGE
238
        else:
239
            lang = context.get('lang', settings.PAGE_DEFAULT_LANGUAGE)
240
        return lang
241
242
    def get_content_from_context(self, context):
243
        if self.untranslated:
244
            lang_fallback = False
245
        else:
246
            lang_fallback = True
247
248
        if self.shared:
249
            return self.get_content(
250
                None,
251
                self.get_lang(context),
252
                lang_fallback)
253
        if self.page not in context:
254
            return ''
255
        # current_page can be set to None
256
        if not context[self.page]:
257
            return ''
258
259
        return self.get_content(
260
            context[self.page],
261
            self.get_lang(context),
262
            lang_fallback)
263
264
    def get_render_content(self, context):
265
        if self.nodelist:
266
            with context.push():
267
                context['content'] = self.get_content_from_context(context)
268
                output = self.nodelist.render(context)
269
            return mark_safe(output)
270
        return mark_safe(self.get_content_from_context(context))
271
272
    def edit_tag(self):
273
        return u"""<!--placeholder ;{};-->""".format(self.name)
274
275
    def render(self, context):
276
        """Output the content of the `PlaceholdeNode` as a template."""
277
        content = self.get_render_content(context)
278
        request = context.get('request')
279
        render_edit_tag = False
280
        if request and request.user.is_staff:
281
            render_edit_tag = True
282
283
        if not content:
284
            if not render_edit_tag:
285
                return ''
286
            return self.edit_tag()
287
        if self.parsed:
288
            try:
289
                t = template.Template(content, name=self.name)
290
                content = mark_safe(t.render(context))
291
            except TemplateSyntaxError as error:
292
                if global_settings.DEBUG:
293
                    content = PLACEHOLDER_ERROR % {
294
                        'name': self.name,
295
                        'error': error,
296
                    }
297
                else:
298
                    content = ''
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