forked from feincms/feincms
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmodels.py
More file actions
397 lines (311 loc) · 12.3 KB
/
Copy pathmodels.py
File metadata and controls
397 lines (311 loc) · 12.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
# ------------------------------------------------------------------------
# ------------------------------------------------------------------------
from django.core.exceptions import PermissionDenied
from django.db import models, transaction
from django.db.models import Q
from django.http import Http404
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from mptt.models import MPTTModel, TreeManager
from feincms import settings
from feincms.models import create_base_model
from feincms.module.mixins import ContentModelMixin
from feincms.module.page import processors
from feincms.utils import get_model_instance, match_model_string, shorten_string
from feincms.utils.managers import ActiveAwareContentManagerMixin
# ------------------------------------------------------------------------
class BasePageManager(ActiveAwareContentManagerMixin, TreeManager):
"""
The page manager. Only adds new methods, does not modify standard Django
manager behavior in any way.
"""
# The fields which should be excluded when creating a copy.
exclude_from_copy = ["id", "tree_id", "lft", "rght", "level", "redirect_to"]
def page_for_path(self, path, raise404=False):
"""
Return a page for a path. Optionally raises a 404 error if requested.
Example::
Page.objects.page_for_path(request.path)
"""
stripped = path.strip("/")
try:
page = self.active().get(_cached_url="/%s/" % stripped if stripped else "/")
if not page.are_ancestors_active():
raise self.model.DoesNotExist("Parents are inactive.")
return page
except self.model.DoesNotExist:
if raise404:
raise Http404()
raise
def best_match_for_path(self, path, raise404=False):
"""
Return the best match for a path. If the path as given is unavailable,
continues to search by chopping path components off the end.
Tries hard to avoid unnecessary database lookups by generating all
possible matching URL prefixes and choosing the longest match.
Page.best_match_for_path('/photos/album/2008/09') might return the
page with url '/photos/album/'.
"""
paths = ["/"]
path = path.strip("/")
if path:
tokens = path.split("/")
paths += ["/%s/" % "/".join(tokens[:i]) for i in range(1, len(tokens) + 1)]
try:
page = (
self.active()
.filter(_cached_url__in=paths)
.extra(select={"_url_length": "LENGTH(_cached_url)"})
.order_by("-_url_length")[0]
)
if not page.are_ancestors_active():
raise IndexError("Parents are inactive.")
return page
except IndexError:
if raise404:
raise Http404()
raise self.model.DoesNotExist
def in_navigation(self):
"""
Returns active pages which have the ``in_navigation`` flag set.
"""
return self.active().filter(in_navigation=True)
def toplevel_navigation(self):
"""
Returns top-level navigation entries.
"""
return self.in_navigation().filter(parent__isnull=True)
def for_request(self, request, raise404=False, best_match=False, path=None):
"""
Return a page for the request
Does not hit the database more than once for the same request.
Examples::
Page.objects.for_request(request, raise404=True, best_match=False)
Defaults to raising a ``DoesNotExist`` exception if no exact match
could be determined.
"""
if not hasattr(request, "_feincms_page"):
path = path or request.path_info or request.path
if best_match:
request._feincms_page = self.best_match_for_path(
path, raise404=raise404
)
else:
request._feincms_page = self.page_for_path(path, raise404=raise404)
return request._feincms_page
# ------------------------------------------------------------------------
class PageManager(BasePageManager):
pass
PageManager.add_to_active_filters(Q(active=True), key="is_active")
# ------------------------------------------------------------------------
class BasePage(create_base_model(MPTTModel), ContentModelMixin):
active = models.BooleanField(_("active"), default=True)
# structure and navigation
title = models.CharField(
_("title"),
max_length=200,
help_text=_("This title is also used for navigation menu items."),
)
slug = models.SlugField(
_("slug"),
max_length=150,
help_text=_("This is used to build the URL for this page"),
)
parent = models.ForeignKey(
"self",
verbose_name=_("Parent"),
blank=True,
on_delete=models.CASCADE,
null=True,
related_name="children",
)
# Custom list_filter - see admin/filterspecs.py
parent.parent_filter = True
in_navigation = models.BooleanField(_("in navigation"), default=False)
override_url = models.CharField(
_("override URL"),
max_length=255,
blank=True,
help_text=_(
"Override the target URL. Be sure to include slashes at the "
"beginning and at the end if it is a local URL. This "
"affects both the navigation and subpages' URLs."
),
)
redirect_to = models.CharField(
_("redirect to"),
max_length=255,
blank=True,
help_text=_(
"Target URL for automatic redirects" " or the primary key of a page."
),
)
_cached_url = models.CharField(
_("Cached URL"),
max_length=255,
blank=True,
editable=False,
default="",
db_index=True,
)
class Meta:
ordering = ["tree_id", "lft"]
abstract = True
objects = PageManager()
def __str__(self):
return self.short_title()
def is_active(self):
"""
Check whether this page and all its ancestors are active
"""
if not self.pk:
return False
# No need to hit DB if page itself is inactive
if not self.active:
return False
pages = self.__class__.objects.active().filter(
tree_id=self.tree_id, lft__lte=self.lft, rght__gte=self.rght
)
return pages.count() > self.level
is_active.short_description = _("is active")
def are_ancestors_active(self):
"""
Check whether all ancestors of this page are active
"""
if self.is_root_node():
return True
queryset = PageManager.apply_active_filters(self.get_ancestors())
return queryset.count() >= self.level
def short_title(self):
"""
Title shortened for display.
"""
return shorten_string(self.title)
short_title.admin_order_field = "title"
short_title.short_description = _("title")
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Cache a copy of the loaded _cached_url value so we can reliably
# determine whether it has been changed in the save handler:
self._original_cached_url = self._cached_url
def save(self, *args, **kwargs):
"""
Overridden save method which updates the ``_cached_url`` attribute of
this page and all subpages. Quite expensive when called with a page
high up in the tree.
"""
cached_page_urls = {}
# determine own URL
if self.override_url:
self._cached_url = self.override_url
elif self.is_root_node():
self._cached_url = "/%s/" % self.slug
else:
self._cached_url = f"{self.parent._cached_url}{self.slug}/"
cached_page_urls[self.id] = self._cached_url
with transaction.atomic():
super().save(*args, **kwargs)
# If our cached URL changed we need to update all descendants to
# reflect the changes. Since this is a very expensive operation
# on large sites we'll check whether our _cached_url actually changed
# or if the updates weren't navigation related:
if self._cached_url != self._original_cached_url:
pages = self.get_descendants().order_by("lft")
for page in pages:
if page.override_url:
page._cached_url = page.override_url
else:
# cannot be root node by definition
page._cached_url = "{}{}/".format(
cached_page_urls[page.parent_id],
page.slug,
)
cached_page_urls[page.id] = page._cached_url
super(BasePage, page).save() # do not recurse
save.alters_data = True
def delete(self, *args, **kwargs):
if not settings.FEINCMS_SINGLETON_TEMPLATE_DELETION_ALLOWED:
if self.template.singleton:
raise PermissionDenied(
_(
"This %(page_class)s uses a singleton template, and "
"FEINCMS_SINGLETON_TEMPLATE_DELETION_ALLOWED=False"
% {"page_class": self._meta.verbose_name}
)
)
super().delete(*args, **kwargs)
delete.alters_data = True
def get_absolute_url(self):
"""
Return the absolute URL of this page.
"""
# result url never begins or ends with a slash
url = self._cached_url.strip("/")
if url:
return reverse("feincms_handler", args=(url,))
return reverse("feincms_home")
def get_navigation_url(self):
"""
Return either ``redirect_to`` if it is set, or the URL of this page.
"""
if self.redirect_to:
return self.get_redirect_to_target()
return self._cached_url
def etag(self, request):
"""
Generate an etag for this page.
An etag should be unique and unchanging for as long as the page
content does not change. Since we have no means to determine whether
rendering the page now (as opposed to a minute ago) will actually
give the same result, this default implementation returns None, which
means "No etag please, thanks for asking".
"""
return None
def last_modified(self, request):
"""
Generate a last modified date for this page.
Since a standard page has no way of knowing this, we always return
"no date" -- this is overridden by the changedate extension.
"""
return None
def get_redirect_to_page(self):
"""
This might be overriden/extended by extension modules.
"""
if not self.redirect_to:
return None
# It might be an identifier for a different object
whereto = match_model_string(self.redirect_to)
if not whereto:
return None
return get_model_instance(*whereto)
def get_redirect_to_target(self, request=None):
"""
This might be overriden/extended by extension modules.
"""
target_page = self.get_redirect_to_page()
if target_page is None:
return self.redirect_to
return target_page.get_absolute_url()
@classmethod
def register_default_processors(cls):
"""
Register our default request processors for the out-of-the-box
Page experience.
"""
cls.register_request_processor(
processors.redirect_request_processor, key="redirect"
)
cls.register_request_processor(
processors.extra_context_request_processor, key="extra_context"
)
# ------------------------------------------------------------------------
class Page(BasePage):
class Meta:
ordering = ["tree_id", "lft"]
verbose_name = _("page")
verbose_name_plural = _("pages")
app_label = "page"
# not yet # permissions = (("edit_page", _("Can edit page metadata")),)
Page.register_default_processors()
# ------------------------------------------------------------------------