|
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 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] |
|
|
|
|
|
|
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
|
|
|
|