-
Notifications
You must be signed in to change notification settings - Fork 12
Expand file tree
/
Copy pathmixins.py
More file actions
306 lines (257 loc) · 9.85 KB
/
Copy pathmixins.py
File metadata and controls
306 lines (257 loc) · 9.85 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
from django.conf import settings
from django.core.checks import Warning
from django.db import models
from django.db.models import Q, signals
from django.utils.translation import activate, get_language, gettext_lazy as _
from tree_queries.fields import TreeNodeForeignKey
from feincms3.utils import ChoicesCharField, validation_error
class MenuMixin(models.Model):
"""
The ``MenuMixin`` is most useful on pages where there are menus with
differing content on a single page, for example the main navigation
and a meta navigation (containing contact, imprint etc.)
"""
menu = ChoicesCharField(_("menu"), max_length=100, blank=True)
class Meta:
abstract = True
@staticmethod
def fill_menu_choices(sender, **kwargs):
"""
Fills in the choices for ``menu`` from the ``MENUS`` class variable.
This method is a receiver of Django's ``class_prepared`` signal.
"""
if issubclass(sender, MenuMixin) and not sender._meta.abstract:
field = sender._meta.get_field("menu")
field.choices = sender.MENUS
field.default = field.choices[0][0]
@classmethod
def check(cls, **kwargs):
errors = super().check(**kwargs)
errors.extend(cls._check_feincms3_menu_mixin(**kwargs))
return errors
@classmethod
def _check_feincms3_menu_mixin(cls, **kwargs):
if invalid := [
value
for value, _label in cls.MENUS
if value and (value.startswith("_") or not value.isidentifier())
]:
yield Warning(
"MenuMixin menus should only use valid public Python identifiers"
f" as keys. Invalid: {sorted(invalid)!r}.",
obj=cls,
id="feincms3.W005",
)
signals.class_prepared.connect(MenuMixin.fill_menu_choices)
class TemplateMixin(models.Model):
"""
It is sometimes useful to have different templates for CMS models such
as pages, articles or anything comparable. The ``TemplateMixin``
provides a ready-made solution for selecting django-content-editor
``Template`` instances through Django's administration interface.
.. warning::
You are encouraged to use the PageTypeMixin and TemplateType from
:mod:`feincms3.applications` instead.
"""
template_key = ChoicesCharField(_("template"), max_length=100)
class Meta:
abstract = True
@property
def template(self):
"""
Return the selected template instance if the ``template_key`` field
matches, or falls back to the first template in ``TEMPLATES``.
"""
return self.TEMPLATES_DICT.get(self.template_key, self.TEMPLATES[0])
@property
def regions(self):
"""
Return the selected template instances' ``regions`` attribute, falling
back to an empty list if no template instance could be found.
"""
return self.template.regions
@staticmethod
def fill_template_key_choices(sender, **kwargs):
"""
Fills in the choices for ``menu`` from the ``MENUS`` class variable.
This method is a receiver of Django's ``class_prepared`` signal.
"""
if issubclass(sender, TemplateMixin) and not sender._meta.abstract:
field = sender._meta.get_field("template_key")
field.choices = [(t.key, t.title) for t in sender.TEMPLATES]
field.default = sender.TEMPLATES[0].key
sender.TEMPLATES_DICT = {t.key: t for t in sender.TEMPLATES}
signals.class_prepared.connect(TemplateMixin.fill_template_key_choices)
class LanguageMixin(models.Model):
"""
Pages may come in varying languages. ``LanguageMixin`` helps with that.
"""
language_code = ChoicesCharField(
_("language"),
max_length=10,
choices=settings.LANGUAGES,
default=settings.LANGUAGES[0][0],
)
class Meta:
abstract = True
def activate_language(self, request):
"""
``activate()`` the page's language and set ``request.LANGUAGE_CODE``
"""
# Do what LocaleMiddleware does.
activate(self.language_code)
request.LANGUAGE_CODE = get_language()
class LanguageAndTranslationOfMixin(LanguageMixin):
"""
This object not only has a language, it may also be a translation of
another object.
"""
translation_of = TreeNodeForeignKey(
"self",
on_delete=models.SET_NULL,
blank=True,
null=True,
related_name="+",
verbose_name=_("translation of"),
limit_choices_to={"language_code": settings.LANGUAGES[0][0]},
)
class Meta:
abstract = True
@classmethod
def check(cls, **kwargs):
errors = super().check(**kwargs)
errors.extend(cls._check_feincms3_language_and_translation_of_mixin(**kwargs))
return errors
@classmethod
def _check_feincms3_language_and_translation_of_mixin(cls, **kwargs):
unique_together = [set(fields) for fields in cls._meta.unique_together]
if {"language_code", "translation_of"} not in unique_together:
yield Warning(
"Models using the LanguageAndTranslationOfMixin should ensure"
" that only one translation can be added per language.",
obj=cls,
id="feincms3.W003",
hint='Add ("language_code", "translation_of") to unique_together.',
)
def translations(self):
"""
Return a queryset containing all translations of this object
This method can be called on any object if translations have been
defined at all, you do not have to fetch the object in the primary
language first.
"""
primary = (
self.pk
if self.language_code == settings.LANGUAGES[0][0]
else self.translation_of_id
)
queryset = self.__class__._default_manager
return (
queryset.filter(Q(id=primary) | Q(translation_of=primary))
if primary
else queryset.none()
)
def clean_fields(self, exclude=None):
"""
Implement the following validation rules:
- Objects in the primary language cannot be the translation of another object
- Objects in other languages can only reference objects in the primary
language (this is automatically verified by Django because we're
using ``limit_choices_to``)
"""
super().clean_fields(exclude)
if self.language_code == settings.LANGUAGES[0][0] and self.translation_of:
raise validation_error(
_(
"Objects in the primary language cannot be"
" the translation of another object."
),
field="translation_of",
exclude=exclude,
)
class RedirectMixin(models.Model):
"""
The ``RedirectMixin`` allows adding redirects in the page tree.
"""
redirect_to_url = models.CharField(
_("Redirect to URL"), max_length=1000, blank=True
)
redirect_to_page = TreeNodeForeignKey(
"self",
on_delete=models.SET_NULL,
blank=True,
null=True,
related_name="+",
verbose_name=_("Redirect to page"),
)
class Meta:
abstract = True
def get_redirect_url(self):
"""
Return the URL for the redirect, if a redirect is configured
"""
if self.redirect_to_url:
return self.redirect_to_url
elif self.redirect_to_page:
return self.redirect_to_page.get_absolute_url()
def clean_fields(self, exclude=None):
"""
Ensure that redirects are configured properly.
"""
super().clean_fields(exclude)
if self.redirect_to_url and self.redirect_to_page_id:
raise validation_error(
_("Only set one redirect value."),
field="redirect_to_url",
exclude=exclude,
)
if self.redirect_to_page_id:
if self.redirect_to_page_id == self.pk:
raise validation_error(
_("Cannot redirect to self."),
field="redirect_to_page",
exclude=exclude,
)
if (
self.redirect_to_page.redirect_to_url
or self.redirect_to_page.redirect_to_page
):
raise validation_error(
_(
'Do not chain redirects. The selected page "%(title)s"'
' redirects to "%(path)s".'
)
% {
"title": self.redirect_to_page,
"path": self.redirect_to_page.get_redirect_url(),
},
field="redirect_to_page",
exclude=exclude,
)
if self.pk and (self.redirect_to_url or self.redirect_to_page_id):
# Any page redirects to this page?
other = self.__class__._default_manager.filter(redirect_to_page=self)
if other:
raise validation_error(
_(
"Do not chain redirects. The following pages already"
" redirect to this page: %(pages)s"
)
% {
"pages": ", ".join(
f"{page} ({page.get_absolute_url()})" for page in other
)
},
field="redirect_to_page",
exclude=exclude,
)
@classmethod
def admin_fieldset(cls, **kwargs):
cfg = {
"fields": (
"redirect_to_page",
"redirect_to_url",
),
"classes": ["tabbed"],
} | kwargs
return (_("Redirects"), cfg)