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 += ' ' |
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 seven characters : Simplified Chinese is zh-hans |
514
|
|
|
language = models.CharField(_('language'), max_length=7, 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
|
|
View Code Duplication |
def media_filename(instance, filename): |
|
|
|
|
556
|
|
|
avoid_collision = uuid.uuid4().hex[:8] |
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
|
|
|
|