Completed
Push — master ( 6123e6...109967 )
by Batiste
01:08
created

  A

Complexity

Total Complexity 9

Size/Duplication

Total Lines 475
Duplicated Lines 0 %

Importance

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

25 Methods

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