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

Page.title()   A

Complexity

Conditions 2

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 12
rs 9.4285
c 0
b 0
f 0
cc 2
1
"""Django page CMS ``models``."""
2
3
from pages.cache import cache
4
from pages.utils import get_placeholders, normalize_url, get_now
5
from pages.managers import PageManager, ContentManager
6
from pages.managers import PageAliasManager
7
from pages import settings
8
# checks
9
from pages import checks
10
11
from django.db import models
12
from django.conf import settings as django_settings
13
from django.utils.translation import ugettext_lazy as _
14
from django.utils.safestring import mark_safe
15
from django.core.urlresolvers import reverse
16
from django.conf import settings as global_settings
17
from django.utils.encoding import python_2_unicode_compatible
18
19
20
from mptt.models import MPTTModel
21
import uuid
22
23
PAGE_CONTENT_DICT_KEY = ContentManager.PAGE_CONTENT_DICT_KEY
24
25
if settings.PAGE_USE_SITE_ID:
26
    from django.contrib.sites.models import Site
27
28
29
@python_2_unicode_compatible
30
class Page(MPTTModel):
31
    """
32
    This model contain the status, dates, author, template.
33
    The real content of the page can be found in the
34
    :class:`Content <pages.models.Content>` model.
35
36
    .. attribute:: creation_date
37
       When the page has been created.
38
39
    .. attribute:: publication_date
40
       When the page should be visible.
41
42
    .. attribute:: publication_end_date
43
       When the publication of this page end.
44
45
    .. attribute:: last_modification_date
46
       Last time this page has been modified.
47
48
    .. attribute:: status
49
       The current status of the page. Could be DRAFT, PUBLISHED,
50
       EXPIRED or HIDDEN. You should the property :attr:`calculated_status` if
51
       you want that the dates are taken in account.
52
53
    .. attribute:: template
54
       A string containing the name of the template file for this page.
55
    """
56
57
    # some class constants to refer to, e.g. Page.DRAFT
58
    DRAFT = 0
59
    PUBLISHED = 1
60
    EXPIRED = 2
61
    HIDDEN = 3
62
    STATUSES = (
63
        (PUBLISHED, _('Published')),
64
        (HIDDEN, _('Hidden')),
65
        (DRAFT, _('Draft')),
66
    )
67
68
    PAGE_LANGUAGES_KEY = "page_%d_languages"
69
    PAGE_URL_KEY = "page_%d_url"
70
    ANCESTORS_KEY = 'ancestors_%d'
71
    CHILDREN_KEY = 'children_%d'
72
    PUB_CHILDREN_KEY = 'pub_children_%d'
73
74
    # used to identify pages across different databases
75
    uuid = models.UUIDField(default=uuid.uuid4, editable=False)
76
77
    author = models.ForeignKey(django_settings.AUTH_USER_MODEL,
78
            verbose_name=_('author'))
79
80
    parent = models.ForeignKey('self', null=True, blank=True,
81
            related_name='children', verbose_name=_('parent'))
82
    creation_date = models.DateTimeField(_('creation date'), editable=False,
83
            default=get_now)
84
    publication_date = models.DateTimeField(_('publication date'),
85
            null=True, blank=True, help_text=_('''When the page should go
86
            live. Status must be "Published" for page to go live.'''))
87
    publication_end_date = models.DateTimeField(_('publication end date'),
88
            null=True, blank=True, help_text=_('''When to expire the page.
89
            Leave empty to never expire.'''))
90
91
    last_modification_date = models.DateTimeField(_('last modification date'))
92
93
    status = models.IntegerField(_('status'), choices=STATUSES, default=DRAFT)
94
    template = models.CharField(_('template'), max_length=100, null=True,
95
            blank=True)
96
97
    delegate_to = models.CharField(_('delegate to'), max_length=100, null=True,
98
            blank=True)
99
100
    freeze_date = models.DateTimeField(_('freeze date'),
101
            null=True, blank=True, help_text=_('''Don't publish any content
102
            after this date.'''))
103
104
    if settings.PAGE_USE_SITE_ID:
105
        sites = models.ManyToManyField(Site,
106
            default=[global_settings.SITE_ID],
107
            help_text=_('The site(s) the page is accessible at.'),
108
            verbose_name=_('sites'))
109
110
    redirect_to_url = models.CharField(max_length=200, null=True, blank=True)
111
112
    redirect_to = models.ForeignKey('self', null=True, blank=True,
113
        related_name='redirected_pages')
114
115
    # Managers
116
    objects = PageManager()
117
118
    if settings.PAGE_TAGGING:
119
        tags = settings.PAGE_TAGGING_FIELD()
120
121
    class Meta:
122
        """Make sure the default page ordering is correct."""
123
        ordering = ['tree_id', 'lft']
124
        get_latest_by = "publication_date"
125
        verbose_name = _('page')
126
        verbose_name_plural = _('pages')
127
        permissions = settings.PAGE_EXTRA_PERMISSIONS
128
129
    def __init__(self, *args, **kwargs):
130
        """Instanciate the page object."""
131
        # per instance cache
132
        self._languages = None
133
        self._content_dict = None
134
        self._is_first_root = None
135
        self._complete_slug = None
136
        super(Page, self).__init__(*args, **kwargs)
137
138
    def save(self, *args, **kwargs):
139
        """Override the default ``save`` method."""
140
        if not self.status:
141
            self.status = self.DRAFT
142
        # Published pages should always have a publication date
143
        if self.publication_date is None and self.status == self.PUBLISHED:
144
            self.publication_date = get_now()
145
        # Drafts should not, unless they have been set to the future
146
        if self.status == self.DRAFT:
147
            if settings.PAGE_SHOW_START_DATE:
148
                if (self.publication_date and
149
                        self.publication_date <= get_now()):
150
                    self.publication_date = None
151
            else:
152
                self.publication_date = None
153
        self.last_modification_date = get_now()
154
        super(Page, self).save(*args, **kwargs)
155
        # fix sites many-to-many link when the're hidden from the form
156
        if settings.PAGE_HIDE_SITES and self.sites.count() == 0:
157
            self.sites.add(Site.objects.get(pk=global_settings.SITE_ID))
158
159
    def _get_calculated_status(self):
160
        """Get the calculated status of the page based on
161
        :attr:`Page.publication_date`,
162
        :attr:`Page.publication_end_date`,
163
        and :attr:`Page.status`."""
164
        if settings.PAGE_SHOW_START_DATE and self.publication_date:
165
            if self.publication_date > get_now():
166
                return self.DRAFT
167
168
        if settings.PAGE_SHOW_END_DATE and self.publication_end_date:
169
            if self.publication_end_date < get_now():
170
                return self.EXPIRED
171
172
        return self.status
173
    calculated_status = property(_get_calculated_status)
174
175
    def _visible(self):
176
        """Return True if the page is visible on the frontend."""
177
        return self.calculated_status in (self.PUBLISHED, self.HIDDEN)
178
    visible = property(_visible)
179
180
    def get_children(self):
181
        """Cache superclass result"""
182
        key = self.CHILDREN_KEY % self.id
183
        #children = cache.get(key, None)
184
        # if children is None:
185
        children = super(Page, self).get_children()
186
        #cache.set(key, children)
187
        return children
188
189
    def published_children(self):
190
        """Return a :class:`QuerySet` of published children page"""
191
        key = self.PUB_CHILDREN_KEY % self.id
192
        #children = cache.get(key, None)
193
        # if children is None:
194
        children = Page.objects.filter_published(self.get_children()).all()
195
        #cache.set(key, children)
196
        return children
197
198
    def get_children_for_frontend(self):
199
        """Return a :class:`QuerySet` of published children page"""
200
        return self.published_children()
201
202
    def get_date_ordered_children_for_frontend(self):
203
        """Return a :class:`QuerySet` of published children page ordered
204
        by publication date."""
205
        return self.published_children().order_by('-publication_date')
206
207
    def move_to(self, target, position='first-child'):
208
        """Invalidate cache when moving"""
209
210
        # Invalidate both in case position matters,
211
        # otherwise only target is needed.
212
        self.invalidate()
213
        target.invalidate()
214
        super(Page, self).move_to(target, position=position)
215
216
    def invalidate(self):
217
        """Invalidate cached data for this page."""
218
219
        cache.delete(self.PAGE_LANGUAGES_KEY % (self.id))
220
        cache.delete('PAGE_FIRST_ROOT_ID')
221
        cache.delete(self.CHILDREN_KEY % self.id)
222
        cache.delete(self.PUB_CHILDREN_KEY % self.id)
223
        # XXX: Should this have a depth limit?
224
        if self.parent_id:
225
            self.parent.invalidate()
226
        self._languages = None
227
        self._complete_slug = None
228
        self._content_dict = dict()
229
230
        placeholders = get_placeholders(self.get_template())
231
232
        p_names = [p.ctype for p in placeholders]
233
        if 'slug' not in p_names:
234
            p_names.append('slug')
235
        if 'title' not in p_names:
236
            p_names.append('title')
237
238
        from pages.managers import fake_page
239
        shared = [p for p in placeholders if p.shared]
240
        for share in shared:
241
            print(share.ctype)
242
            fake_page.invalidate(share.ctype)
243
244
        # delete content cache, frozen or not
245
        for name in p_names:
246
            # frozen
247
            cache.delete(
248
                PAGE_CONTENT_DICT_KEY %
249
                (self.id, name, 1))
250
            # not frozen
251
            cache.delete(
252
                PAGE_CONTENT_DICT_KEY %
253
                (self.id, name, 0))
254
255
        cache.delete(self.PAGE_URL_KEY % (self.id))
256
257
    def get_languages(self):
258
        """
259
        Return a list of all used languages for this page.
260
        """
261
        if self._languages:
262
            return self._languages
263
        self._languages = cache.get(self.PAGE_LANGUAGES_KEY % (self.id))
264
        if self._languages is not None:
265
            return self._languages
266
267
        languages = [c['language'] for
268
            c in Content.objects.filter(page=self,
269
            type="slug").values('language')]
270
        # remove duplicates
271
        languages = sorted(set(languages))
272
        cache.set(self.PAGE_LANGUAGES_KEY % (self.id), languages)
273
        self._languages = languages
274
        return languages
275
276
    def is_first_root(self):
277
        """Return ``True`` if this page is the first root pages."""
278
        parent_cache_key = 'PARENT_FOR_%d' % self.id
279
        has_parent = cache.get(parent_cache_key, None)
280
        if has_parent is None:
281
            has_parent = not not self.parent
282
            cache.set(parent_cache_key, has_parent)
283
284
        if has_parent:
285
            return False
286
        if self._is_first_root is not None:
287
            return self._is_first_root
288
        first_root_id = cache.get('PAGE_FIRST_ROOT_ID')
289
        if first_root_id is not None:
290
            self._is_first_root = first_root_id == self.id
291
            return self._is_first_root
292
        try:
293
            first_root_id = Page.objects.root().values('id')[0]['id']
294
        except IndexError:
295
            first_root_id = None
296
        if first_root_id is not None:
297
            cache.set('PAGE_FIRST_ROOT_ID', first_root_id)
298
        self._is_first_root = self.id == first_root_id
299
        return self._is_first_root
300
301
    def get_template(self):
302
        """
303
        Get the :attr:`template <Page.template>` of this page if
304
        defined or the closer parent's one if defined
305
        or :attr:`pages.settings.PAGE_DEFAULT_TEMPLATE` otherwise.
306
        """
307
        if self.template:
308
            return self.template
309
310
        template = None
311
        for p in self.get_ancestors(ascending=True):
312
            if p.template:
313
                template = p.template
314
                break
315
316
        if not template:
317
            template = settings.PAGE_DEFAULT_TEMPLATE
318
319
        return template
320
321
    def get_template_name(self):
322
        """
323
        Get the template name of this page if defined or if a closer
324
        parent has a defined template or
325
        :data:`pages.settings.PAGE_DEFAULT_TEMPLATE` otherwise.
326
        """
327
        template = self.get_template()
328
        page_templates = settings.get_page_templates()
329
        for t in page_templates:
330
            if t[0] == template:
331
                return t[1]
332
        return template
333
334
    def valid_targets(self):
335
        """Return a :class:`QuerySet` of valid targets for moving a page
336
        into the tree.
337
338
        :param perms: the level of permission of the concerned user.
339
        """
340
        exclude_list = [self.id]
341
        for p in self.get_descendants():
342
            exclude_list.append(p.id)
343
        return Page.objects.exclude(id__in=exclude_list)
344
345
    # Content methods
346
347
    def get_content(self, language, ctype, language_fallback=False):
348
        """Shortcut method for retrieving a piece of page content
349
350
        :param language: wanted language, if not defined default is used.
351
        :param ctype: the type of content.
352
        :param fallback: if ``True``, the content will also be searched in \
353
        other languages.
354
        """
355
        return Content.objects.get_content(self, language, ctype,
356
            language_fallback)
357
358
    def expose_content(self):
359
        """Return all the current content of this page into a `string`.
360
361
        This is used by the haystack framework to build the search index."""
362
        placeholders = get_placeholders(self.get_template())
363
        exposed_content = []
364
        for lang in self.get_languages():
365
            for p in placeholders:
366
                content = self.get_content(lang, p.ctype, False)
367
                if content:
368
                    exposed_content.append(content)
369
        return "\r\n".join(exposed_content)
370
371
    def content_by_language(self, language):
372
        """
373
        Return a list of latest published
374
        :class:`Content <pages.models.Content>`
375
        for a particluar language.
376
377
        :param language: wanted language,
378
        """
379
        placeholders = get_placeholders(self.get_template())
380
        content_list = []
381
        for p in placeholders:
382
            try:
383
                content = Content.objects.get_content_object(self,
384
                    language, p.ctype)
385
                content_list.append(content)
386
            except Content.DoesNotExist:
387
                pass
388
        return content_list
389
390
    ### Title and slug
391
392
    def get_url_path(self, language=None):
393
        """Return the URL's path component. Add the language prefix if
394
        ``PAGE_USE_LANGUAGE_PREFIX`` setting is set to ``True``.
395
396
        :param language: the wanted url language.
397
        """
398
        if self.is_first_root():
399
            # this is used to allow users to change URL of the root
400
            # page. The language prefix is not usable here.
401
            try:
402
                return reverse('pages-root')
403
            except Exception:
404
                pass
405
        url = self.get_complete_slug(language)
406
        if not language:
407
            language = settings.PAGE_DEFAULT_LANGUAGE
408
        if settings.PAGE_USE_LANGUAGE_PREFIX:
409
            return reverse('pages-details-by-path',
410
                args=[language, url])
411
        else:
412
            return reverse('pages-details-by-path', args=[url])
413
414
    def get_absolute_url(self, language=None):
415
        """Alias for `get_url_path`.
416
417
        :param language: the wanted url language.
418
        """
419
        return self.get_url_path(language=language)
420
421
    def get_complete_slug(self, language=None, hideroot=True):
422
        """Return the complete slug of this page by concatenating
423
        all parent's slugs.
424
425
        :param language: the wanted slug language."""
426
        if not language:
427
            language = settings.PAGE_DEFAULT_LANGUAGE
428
429
        if self._complete_slug and language in self._complete_slug:
430
            return self._complete_slug[language]
431
432
        self._complete_slug = cache.get(self.PAGE_URL_KEY % (self.id))
433
        if self._complete_slug is None:
434
            self._complete_slug = {}
435
        elif language in self._complete_slug:
436
            return self._complete_slug[language]
437
438
        if hideroot and settings.PAGE_HIDE_ROOT_SLUG and self.is_first_root():
439
            url = ''
440
        else:
441
            url = '%s' % self.slug(language)
442
443
        key = self.ANCESTORS_KEY % self.id
444
        ancestors = cache.get(key, None)
445
        if ancestors is None:
446
            ancestors = self.get_ancestors(ascending=True)
447
            cache.set(key, ancestors)
448
449
        for ancestor in ancestors:
450
            url = ancestor.slug(language) + '/' + url
451
452
        self._complete_slug[language] = url
453
        cache.set(self.PAGE_URL_KEY % (self.id), self._complete_slug)
454
        return url
455
456
    def slug_with_level(self, language=None):
457
        """Display the slug of the page prepended with insecable
458
        spaces to simluate the level of page in the hierarchy."""
459
        level = ''
460
        if self.level:
461
            for n in range(0, self.level):
462
                level += '&nbsp;&nbsp;&nbsp;'
463
        return mark_safe(level + self.slug(language))
464
465
    def slug(self, language=None, fallback=True):
466
        """
467
        Return the slug of the page depending on the given language.
468
469
        :param language: wanted language, if not defined default is used.
470
        :param fallback: if ``True``, the slug will also be searched in other \
471
        languages.
472
        """
473
474
        slug = self.get_content(language, 'slug', language_fallback=fallback)
475
        if slug == '':
476
            return "Page {0}".format(self.id)
477
478
        return slug
479
480
    def title(self, language=None, fallback=True):
481
        """
482
        Return the title of the page depending on the given language.
483
484
        :param language: wanted language, if not defined default is used.
485
        :param fallback: if ``True``, the slug will also be searched in \
486
        other languages.
487
        """
488
        if not language:
489
            language = settings.PAGE_DEFAULT_LANGUAGE
490
491
        return self.get_content(language, 'title', language_fallback=fallback)
492
493
    # Formating methods
494
495
    def margin_level(self):
496
        """Used in the admin menu to create the left margin."""
497
        return self.level * 2
498
499
    def __str__(self):
500
        """Representation of the page, saved or not."""
501
        if self.id:
502
            # without ID a slug cannot be retrieved
503
            return self.slug()
504
        return "Page without id"
505
506
507
@python_2_unicode_compatible
508
class Content(models.Model):
509
    """A block of content, tied to a :class:`Page <pages.models.Page>`,
510
    for a particular language"""
511
512
    # languages could have five characters : Brazilian Portuguese is pt-br
513
    language = models.CharField(_('language'), max_length=5, blank=False)
514
    body = models.TextField(_('body'), blank=True)
515
    type = models.CharField(
516
        _('type'), max_length=100, blank=False,
517
        db_index=True)
518
    page = models.ForeignKey(Page, verbose_name=_('page'), null=True)
519
520
    creation_date = models.DateTimeField(
521
        _('creation date'), editable=False,
522
        default=get_now)
523
    objects = ContentManager()
524
525
    class Meta:
526
        get_latest_by = 'creation_date'
527
        verbose_name = _('content')
528
        verbose_name_plural = _('contents')
529
530
    def __str__(self):
531
        return u"{0} :: {1}".format(self.page.slug(), self.body[0:15])
532
533
534
@python_2_unicode_compatible
535
class PageAlias(models.Model):
536
    """URL alias for a :class:`Page <pages.models.Page>`"""
537
    page = models.ForeignKey(Page, null=True, blank=True,
538
        verbose_name=_('page'))
539
    url = models.CharField(max_length=255, unique=True)
540
    objects = PageAliasManager()
541
542
    class Meta:
543
        verbose_name_plural = _('Aliases')
544
545
    def save(self, *args, **kwargs):
546
        # normalize url
547
        self.url = normalize_url(self.url)
548
        super(PageAlias, self).save(*args, **kwargs)
549
550
    def __str__(self):
551
        return "{0} :: {1}".format(self.url, self.page.get_complete_slug())
552