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 += ' ' |
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
|
|
|
|