Completed
Push — master ( d5daa4...f437a8 )
by Batiste
01:24
created

PageHasContentNode.render()   A

Complexity

Conditions 2

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 7
rs 9.4285
cc 2
1
"""Page CMS page_tags template tags"""
2
from django import template
3
from django.utils.safestring import SafeText
4
from django.template import TemplateSyntaxError
5
from django.conf import settings
6
from django.utils.text import unescape_string_literal
7
8
from pages import settings as pages_settings
9
from pages.models import Content, Page
10
from pages.placeholders import PlaceholderNode, ImagePlaceholderNode, FilePlaceholderNode
11
from pages.placeholders import ContactPlaceholderNode, MarkdownPlaceholderNode
12
from pages.placeholders import JsonPlaceholderNode, parse_placeholder
13
from six.moves import urllib
14
import six
15
16
register = template.Library()
17
18
19
def get_page_from_string_or_id(page_string, lang=None):
20
    """Return a Page object from a slug or an id."""
21
    if type(page_string) == int:
22
        return Page.objects.get(pk=int(page_string))
23
    # if we have a string coming from some templates templates
24
    if (isinstance(page_string, SafeText) or
25
        isinstance(page_string, six.string_types)):
26
        if page_string.isdigit():
27
            return Page.objects.get(pk=int(page_string))
28
        return Page.objects.from_path(page_string, lang)
29
    # in any other case we return the input becasue it's probably
30
    # a Page object.
31
    return page_string
32
33
def _get_content(context, page, content_type, lang, fallback=True):
34
    """Helper function used by ``PlaceholderNode``."""
35
    if not page:
36
        return ''
37
38
    if not lang and 'lang' in context:
39
        lang = context.get('lang', pages_settings.PAGE_DEFAULT_LANGUAGE)
40
41
    page = get_page_from_string_or_id(page, lang)
42
43
    if not page:
44
        return ''
45
46
    content = Content.objects.get_content(page, lang, content_type, fallback)
47
    return content
48
49
"""Filters"""
50
51
52
def has_content_in(page, language):
53
    """Fitler that return ``True`` if the page has any content in a
54
    particular language.
55
56
    :param page: the current page
57
    :param language: the language you want to look at
58
    """
59
    return Content.objects.filter(page=page, language=language).count() > 0
60
register.filter(has_content_in)
61
62
63
"""Inclusion tags"""
64
65
66
def pages_menu(context, page, url='/'):
67
    """Render a nested list of all the descendents of the given page,
68
    including this page.
69
70
    :param page: the page where to start the menu from.
71
    :param url: not used anymore.
72
    """
73
    lang = context.get('lang', pages_settings.PAGE_DEFAULT_LANGUAGE)
74
    page = get_page_from_string_or_id(page, lang)
75
    if page:
76
        children = page.get_children_for_frontend()
77
        context.update({'children': children, 'page': page})
78
    return context
79
pages_menu = register.inclusion_tag('pages/menu.html',
80
                                    takes_context=True)(pages_menu)
81
82
83
def pages_sub_menu(context, page, url='/'):
84
    """Get the root page of the given page and
85
    render a nested list of all root's children pages.
86
    Good for rendering a secondary menu.
87
88
    :param page: the page where to start the menu from.
89
    :param url: not used anymore.
90
    """
91
    lang = context.get('lang', pages_settings.PAGE_DEFAULT_LANGUAGE)
92
    page = get_page_from_string_or_id(page, lang)
93
    if page:
94
        root = page.get_root()
95
        children = root.get_children_for_frontend()
96
        context.update({'children': children, 'page': page})
97
    return context
98
pages_sub_menu = register.inclusion_tag('pages/sub_menu.html',
99
                                        takes_context=True)(pages_sub_menu)
100
101
102
def pages_siblings_menu(context, page, url='/'):
103
    """Get the parent page of the given page and render a nested list of its
104
    child pages. Good for rendering a secondary menu.
105
106
    :param page: the page where to start the menu from.
107
    :param url: not used anymore.
108
    """
109
    lang = context.get('lang', pages_settings.PAGE_DEFAULT_LANGUAGE)
110
    page = get_page_from_string_or_id(page, lang)
111
    if page:
112
        siblings = page.get_siblings()
113
        context.update({'children': siblings, 'page': page})
114
    return context
115
pages_siblings_menu = register.inclusion_tag('pages/sub_menu.html',
116
                                    takes_context=True)(pages_siblings_menu)
117
118
119
def pages_admin_menu(context, page):
120
    """Render the admin table of pages."""
121
    request = context.get('request', None)
122
123
    expanded = False
124
    if request and "tree_expanded" in request.COOKIES:
125
        cookie_string = urllib.parse.unquote(request.COOKIES['tree_expanded'])
126
        if cookie_string:
127
            ids = [int(id) for id in
128
                urllib.parse.unquote(request.COOKIES['tree_expanded']).split(',')]
129
            if page.id in ids:
130
                expanded = True
131
    context.update({'expanded': expanded, 'page': page})
132
    return context
133
pages_admin_menu = register.inclusion_tag('admin/pages/page/menu.html',
134
                                        takes_context=True)(pages_admin_menu)
135
136
137
def show_content(context, page, content_type, lang=None, fallback=True):
138
    """Display a content type from a page.
139
140
    Example::
141
142
        {% show_content page_object "title" %}
143
144
    You can also use the slug of a page::
145
146
        {% show_content "my-page-slug" "title" %}
147
148
    Or even the id of a page::
149
150
        {% show_content 10 "title" %}
151
152
    :param page: the page object, slug or id
153
    :param content_type: content_type used by a placeholder
154
    :param lang: the wanted language
155
        (default None, use the request object to know)
156
    :param fallback: use fallback content from other language
157
    """
158
    return {'content': _get_content(context, page, content_type, lang,
159
                                                                fallback)}
160
show_content = register.inclusion_tag('pages/content.html',
161
                                      takes_context=True)(show_content)
162
163
164
def show_absolute_url(context, page, lang=None):
165
    """
166
    Show the url of a page in the right language
167
168
    Example ::
169
170
        {% show_absolute_url page_object %}
171
172
    You can also use the slug of a page::
173
174
        {% show_absolute_url "my-page-slug" %}
175
176
    Keyword arguments:
177
    :param page: the page object, slug or id
178
    :param lang: the wanted language \
179
        (defaults to `settings.PAGE_DEFAULT_LANGUAGE`)
180
    """
181
    if not lang:
182
        lang = context.get('lang', pages_settings.PAGE_DEFAULT_LANGUAGE)
183
    page = get_page_from_string_or_id(page, lang)
184
    if not page:
185
        return {'content': ''}
186
    url = page.get_url_path(language=lang)
187
    if url:
188
        return {'content': url}
189
    return {'content': ''}
190
show_absolute_url = register.inclusion_tag('pages/content.html',
191
                                      takes_context=True)(show_absolute_url)
192
193
194
def show_revisions(context, page, content_type, lang=None):
195
    """Render the last 10 revisions of a page content with a list using
196
        the ``pages/revisions.html`` template"""
197
    if (not pages_settings.PAGE_CONTENT_REVISION or
198
            content_type in pages_settings.PAGE_CONTENT_REVISION_EXCLUDE_LIST):
199
        return {'revisions': None}
200
    revisions = Content.objects.filter(page=page, language=lang,
201
                                type=content_type).order_by('-creation_date')
202
    if len(revisions) < 2:
203
        return {'revisions': None}
204
    return {'revisions': revisions[0:10]}
205
show_revisions = register.inclusion_tag('pages/revisions.html',
206
                                        takes_context=True)(show_revisions)
207
208
209
def pages_dynamic_tree_menu(context, page, url='/'):
210
    """
211
    Render a "dynamic" tree menu, with all nodes expanded which are either
212
    ancestors or the current page itself.
213
214
    Override ``pages/dynamic_tree_menu.html`` if you want to change the
215
    design.
216
217
    :param page: the current page
218
    :param url: not used anymore
219
    """
220
    lang = context.get('lang', pages_settings.PAGE_DEFAULT_LANGUAGE)
221
    page = get_page_from_string_or_id(page, lang)
222
    children = None
223
    if page and 'current_page' in context:
224
        current_page = context['current_page']
225
        # if this node is expanded, we also have to render its children
226
        # a node is expanded if it is the current node or one of its ancestors
227
        if(page.tree_id == current_page.tree_id and
228
            page.lft <= current_page.lft and
229
            page.rght >= current_page.rght):
230
            children = page.get_children_for_frontend()
231
    context.update({'children': children, 'page': page})
232
    return context
233
pages_dynamic_tree_menu = register.inclusion_tag(
234
    'pages/dynamic_tree_menu.html',
235
    takes_context=True
236
)(pages_dynamic_tree_menu)
237
238
239
def pages_breadcrumb(context, page, url='/'):
240
    """
241
    Render a breadcrumb like menu.
242
243
    Override ``pages/breadcrumb.html`` if you want to change the
244
    design.
245
246
    :param page: the current page
247
    :param url: not used anymore
248
    """
249
    lang = context.get('lang', pages_settings.PAGE_DEFAULT_LANGUAGE)
250
    page = get_page_from_string_or_id(page, lang)
251
    pages_navigation = None
252
    if page:
253
        pages_navigation = page.get_ancestors()
254
    context.update({'pages_navigation': pages_navigation, 'page': page})
255
    return context
256
pages_breadcrumb = register.inclusion_tag(
257
    'pages/breadcrumb.html',
258
    takes_context=True
259
)(pages_breadcrumb)
260
261
262
"""Tags"""
263
264
class GetPageNode(template.Node):
265
    """get_page Node"""
266
    def __init__(self, page_filter, varname):
267
        self.page_filter = page_filter
268
        self.varname = varname
269
270
    def render(self, context):
271
        page_or_id = self.page_filter.resolve(context)
272
        page = get_page_from_string_or_id(page_or_id)
273
        context[self.varname] = page
274
        return ''
275
276
277
def do_get_page(parser, token):
278
    """Retrieve a page and insert into the template's context.
279
280
    Example::
281
282
        {% get_page "news" as news_page %}
283
284
    :param page: the page object, slug or id
285
    :param name: name of the context variable to store the page in
286
    """
287
    bits = token.split_contents()
288
    if 4 != len(bits):
289
        raise TemplateSyntaxError('%r expects 4 arguments' % bits[0])
290
    if bits[-2] != 'as':
291
        raise TemplateSyntaxError(
292
            '%r expects "as" as the second argument' % bits[0])
293
    page_filter = parser.compile_filter(bits[1])
294
    varname = bits[-1]
295
    return GetPageNode(page_filter, varname)
296
do_get_page = register.tag('get_page', do_get_page)
297
298
299
class GetContentNode(template.Node):
300
    """Get content node"""
301
    def __init__(self, page, content_type, varname, lang, lang_filter):
302
        self.page = page
303
        self.content_type = content_type
304
        self.varname = varname
305
        self.lang = lang
306
        self.lang_filter = lang_filter
307
308
    def render(self, context):
309
        if self.lang_filter:
310
            self.lang = self.lang_filter.resolve(context)
311
        context[self.varname] = _get_content(
312
            context,
313
            self.page.resolve(context),
314
            self.content_type.resolve(context),
315
            self.lang
316
        )
317
        return ''
318
319
320
def do_get_content(parser, token):
321
    """Retrieve a Content object and insert it into the template's context.
322
323
    Example::
324
325
        {% get_content page_object "title" as content %}
326
327
    You can also use the slug of a page::
328
329
        {% get_content "my-page-slug" "title" as content %}
330
331
    Syntax::
332
333
        {% get_content page type [lang] as name %}
334
335
    :param page: the page object, slug or id
336
    :param type: content_type used by a placeholder
337
    :param name: name of the context variable to store the content in
338
    :param lang: the wanted language
339
    """
340
    bits = token.split_contents()
341
    if not 5 <= len(bits) <= 6:
342
        raise TemplateSyntaxError('%r expects 4 or 5 arguments' % bits[0])
343
    if bits[-2] != 'as':
344
        raise TemplateSyntaxError(
345
            '%r expects "as" as the second last argument' % bits[0])
346
    page = parser.compile_filter(bits[1])
347
    content_type = parser.compile_filter(bits[2])
348
    varname = bits[-1]
349
    lang = None
350
    lang_filter = None
351
    if len(bits) == 6:
352
        lang = bits[3]
353
    else:
354
        lang_filter = parser.compile_filter("lang")
355
    return GetContentNode(page, content_type, varname, lang, lang_filter)
356
do_get_content = register.tag('get_content', do_get_content)
357
358
359
class LoadPagesNode(template.Node):
360
    """Load page node."""
361
    def render(self, context):
362
        if 'pages_navigation' not in context:
363
            pages = Page.objects.navigation().order_by("tree_id")
364
            context.update({'pages_navigation': pages})
365
        if 'current_page' not in context:
366
            context.update({'current_page': None})
367
        return ''
368
369
370
def do_load_pages(parser, token):
371
    """Load the navigation pages, lang, and current_page variables into the
372
    current context.
373
374
    Example::
375
376
        <ul>
377
            {% load_pages %}
378
            {% for page in pages_navigation %}
379
                {% pages_menu page %}
380
            {% endfor %}
381
        </ul>
382
    """
383
    return LoadPagesNode()
384
do_load_pages = register.tag('load_pages', do_load_pages)
385
386
387
def do_placeholder(parser, token):
388
    """
389
    Method that parse the placeholder template tag.
390
391
    Syntax::
392
393
        {% placeholder <name> [on <page>] [with <widget>] \
394
[parsed] [as <varname>] %}
395
396
    Example usage::
397
398
        {% placeholder about %}
399
        {% placeholder body with TextArea as body_text %}
400
        {% placeholder welcome with TextArea parsed as welcome_text %}
401
        {% placeholder teaser on next_page with TextArea parsed %}
402
    """
403
    name, params = parse_placeholder(parser, token)
404
    return PlaceholderNode(name, **params)
405
register.tag('placeholder', do_placeholder)
406
407
def do_markdownlaceholder(parser, token):
408
    """
409
    Method that parse the markdownplaceholder template tag.
410
    """
411
    name, params = parse_placeholder(parser, token)
412
    return MarkdownPlaceholderNode(name, **params)
413
register.tag('markdownplaceholder', do_markdownlaceholder)
414
415
def do_imageplaceholder(parser, token):
416
    """
417
    Method that parse the imageplaceholder template tag.
418
    """
419
    name, params = parse_placeholder(parser, token)
420
    return ImagePlaceholderNode(name, **params)
421
register.tag('imageplaceholder', do_imageplaceholder)
422
423
def do_fileplaceholder(parser, token):
424
    """
425
    Method that parse the fileplaceholder template tag.
426
    """
427
    name, params = parse_placeholder(parser, token)
428
    return FilePlaceholderNode(name, **params)
429
register.tag('fileplaceholder', do_fileplaceholder)
430
431
def do_contactplaceholder(parser, token):
432
    """
433
    Method that parse the contactplaceholder template tag.
434
    """
435
    name, params = parse_placeholder(parser, token)
436
    return ContactPlaceholderNode(name, **params)
437
register.tag('contactplaceholder', do_contactplaceholder)
438
439
440
def do_jsonplaceholder(parser, token):
441
    """
442
    Method that parse the contactplaceholder template tag.
443
    """
444
    name, params = parse_placeholder(parser, token)
445
    return JsonPlaceholderNode(name, **params)
446
register.tag('jsonplaceholder', do_jsonplaceholder)
447
448
449
def language_content_up_to_date(page, language):
450
    """Tell if all the page content has been updated since the last
451
    change of the official version (settings.LANGUAGE_CODE)
452
453
    This is approximated by comparing the last modified date of any
454
    content in the page, not comparing each content block to its
455
    corresponding official language version.  That allows users to
456
    easily make "do nothing" changes to any content block when no
457
    change is required for a language.
458
    """
459
    lang_code = getattr(settings, 'LANGUAGE_CODE', None)
460
    if lang_code == language:
461
        # official version is always "up to date"
462
        return True
463
    # get the last modified date for the official version
464
    last_modified = Content.objects.filter(language=lang_code,
465
        page=page).order_by('-creation_date')
466
    if not last_modified:
467
        # no official version
468
        return True
469
    lang_modified = Content.objects.filter(language=language,
470
        page=page).order_by('-creation_date')[0].creation_date
471
    return lang_modified > last_modified[0].creation_date
472
register.filter(language_content_up_to_date)
473
474
475
@register.assignment_tag
476
def get_pages_with_tag(tag):
477
    """
478
    Return Pages with given tag
479
480
    Syntax::
481
482
        {% get_pages_with_tag <tag name> as <varname> %}
483
484
    Example use:
485
        {% get_pages_with_tag "footer" as pages %}
486
    """
487
    return Page.objects.filter(tags__name__in=[tag])
488
489
490
def do_page_has_content(parser, token):
491
    """
492
    Conditional tag that only renders its nodes if the page
493
    has content for a particular content type. By default the
494
    current page is used.
495
496
    Syntax::
497
498
        {% page_has_content <content_type> [<page var name>] %}
499
            ...
500
        {%_end page_has_content %}
501
502
    Example use:
503
        {% page_has_content 'header-image' %}
504
            <img src="{{ MEDIA_URL }}{% imageplaceholder 'header-image' %}">
505
        {% end_page_has_content %}
506
    """
507
    nodelist = parser.parse(('end_page_has_content',))
508
    parser.delete_first_token()
509
    args = token.split_contents()
510
    try:
511
        content_type = unescape_string_literal(args[1])
512
    except IndexError:
513
        raise template.TemplateSyntaxError(
514
            "%r tag requires arguments content_type" % args[0]
515
        )
516
    if len(args) > 2:
517
        page = args[2]
518
    else:
519
        page = None
520
    return PageHasContentNode(page, content_type, nodelist)
521
register.tag('page_has_content', do_page_has_content)
522
523
class PageHasContentNode(template.Node):
524
525
    def __init__(self, page, content_type, nodelist):
526
        self.page = page or 'current_page'
527
        self.content_type = content_type
528
        self.nodelist = nodelist
529
530
    def render(self, context):
531
        page = context[self.page]
532
        content = page.get_content(context.get('lang', None), self.content_type)
533
        if(content):
534
            output = self.nodelist.render(context)
535
            return output
536
        return ''
537
538