Completed
Push — master ( beb414...ca28b2 )
by Batiste
10s
created

PlaceholderNode.__init__()   A

Complexity

Conditions 2

Size

Total Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

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