django-camomilla-cms 5.8.6__py2.py3-none-any.whl → 6.0.0__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- camomilla/__init__.py +8 -2
- camomilla/apps.py +9 -1
- camomilla/context_processors.py +6 -0
- camomilla/contrib/modeltranslation/__init__.py +0 -0
- camomilla/contrib/modeltranslation/hvad_migration.py +126 -0
- camomilla/dynamic_pages_urls.py +33 -0
- camomilla/fields/__init__.py +13 -0
- camomilla/{fields.py → fields/json.py} +15 -18
- camomilla/management/commands/regenerate_thumbnails.py +0 -1
- camomilla/managers/__init__.py +3 -0
- camomilla/managers/pages.py +116 -0
- camomilla/model_api.py +86 -0
- camomilla/models/__init__.py +5 -6
- camomilla/models/article.py +26 -44
- camomilla/models/content.py +8 -15
- camomilla/models/media.py +70 -97
- camomilla/models/menu.py +106 -0
- camomilla/models/mixins/__init__.py +10 -48
- camomilla/models/page.py +521 -20
- camomilla/openapi/__init__.py +0 -0
- camomilla/openapi/schema.py +67 -0
- camomilla/parsers.py +0 -1
- camomilla/redirects.py +10 -0
- camomilla/serializers/__init__.py +2 -0
- camomilla/serializers/article.py +5 -10
- camomilla/serializers/base/__init__.py +21 -17
- camomilla/serializers/content_type.py +17 -0
- camomilla/serializers/fields/__init__.py +6 -20
- camomilla/serializers/fields/file.py +5 -0
- camomilla/serializers/fields/related.py +24 -4
- camomilla/serializers/media.py +6 -8
- camomilla/serializers/menu.py +17 -0
- camomilla/serializers/mixins/__init__.py +23 -187
- camomilla/serializers/mixins/fields.py +20 -0
- camomilla/serializers/mixins/filter_fields.py +57 -0
- camomilla/serializers/mixins/json.py +34 -0
- camomilla/serializers/mixins/language.py +32 -0
- camomilla/serializers/mixins/nesting.py +35 -0
- camomilla/serializers/mixins/optimize.py +91 -0
- camomilla/serializers/mixins/ordering.py +34 -0
- camomilla/serializers/mixins/page.py +58 -0
- camomilla/serializers/mixins/translation.py +103 -0
- camomilla/serializers/page.py +53 -4
- camomilla/serializers/user.py +5 -4
- camomilla/serializers/utils.py +38 -0
- camomilla/serializers/validators.py +51 -0
- camomilla/settings.py +118 -0
- camomilla/sitemap.py +30 -0
- camomilla/storages/__init__.py +4 -0
- camomilla/storages/default.py +12 -0
- camomilla/storages/optimize.py +71 -0
- camomilla/{storages.py → storages/overwrite.py} +2 -2
- camomilla/templates/admin/camomilla/page/change_form.html +10 -0
- camomilla/templates/defaults/articles/default.html +7 -0
- camomilla/templates/defaults/base.html +170 -0
- camomilla/templates/defaults/pages/default.html +3 -0
- camomilla/templates/defaults/parts/langswitch.html +83 -0
- camomilla/templates/defaults/parts/menu.html +15 -0
- camomilla/templates_context/__init__.py +0 -0
- camomilla/templates_context/autodiscover.py +51 -0
- camomilla/templates_context/rendering.py +89 -0
- camomilla/templatetags/camomilla_filters.py +6 -5
- camomilla/templatetags/menus.py +37 -0
- camomilla/templatetags/model_extras.py +77 -0
- camomilla/theme/__init__.py +1 -1
- camomilla/theme/admin/__init__.py +99 -0
- camomilla/theme/admin/pages.py +46 -0
- camomilla/theme/admin/translations.py +13 -0
- camomilla/theme/apps.py +38 -0
- camomilla/theme/static/admin/css/responsive.css +5 -1021
- camomilla/theme/static/admin/img/favicon.ico +0 -0
- camomilla/theme/static/admin/img/logo.svg +31 -0
- camomilla/theme/templates/admin/base.html +7 -0
- camomilla/theme/templates/rosetta/base.html +196 -0
- camomilla/translation.py +61 -0
- camomilla/urls.py +38 -17
- camomilla/utils/__init__.py +4 -0
- camomilla/utils/getters.py +27 -0
- camomilla/utils/normalization.py +7 -0
- camomilla/utils/query_parser.py +167 -0
- camomilla/{utils.py → utils/seo.py} +13 -15
- camomilla/utils/setters.py +37 -0
- camomilla/utils/templates.py +32 -0
- camomilla/utils/translation.py +114 -0
- camomilla/views/__init__.py +1 -1
- camomilla/views/articles.py +5 -7
- camomilla/views/base/__init__.py +35 -5
- camomilla/views/contents.py +6 -11
- camomilla/views/decorators.py +26 -0
- camomilla/views/medias.py +24 -19
- camomilla/views/menus.py +81 -0
- camomilla/views/mixins/__init__.py +17 -73
- camomilla/views/mixins/bulk_actions.py +22 -0
- camomilla/views/mixins/language.py +33 -0
- camomilla/views/mixins/optimize.py +18 -0
- camomilla/views/mixins/ordering.py +2 -2
- camomilla/views/mixins/pagination.py +12 -18
- camomilla/views/mixins/permissions.py +6 -0
- camomilla/views/pages.py +28 -6
- camomilla/views/tags.py +5 -6
- camomilla/views/users.py +7 -12
- django_camomilla_cms-6.0.0.dist-info/METADATA +123 -0
- django_camomilla_cms-6.0.0.dist-info/RECORD +133 -0
- {django_camomilla_cms-5.8.6.dist-info → django_camomilla_cms-6.0.0.dist-info}/WHEEL +1 -1
- tests/fixtures/__init__.py +14 -0
- tests/test_api.py +22 -39
- tests/test_camomilla_filters.py +11 -13
- tests/test_media.py +152 -0
- tests/test_menu.py +112 -0
- tests/test_model_api.py +113 -0
- tests/test_model_api_permissions.py +44 -0
- tests/test_model_api_register.py +355 -0
- tests/test_pages.py +351 -0
- tests/test_query_parser.py +58 -0
- tests/test_templates_context.py +149 -0
- tests/test_utils.py +64 -64
- tests/utils/__init__.py +0 -0
- tests/utils/api.py +28 -0
- tests/utils/media.py +10 -0
- camomilla/admin.py +0 -98
- camomilla/migrations/0001_initial.py +0 -577
- camomilla/migrations/0002_auto_20200214_1127.py +0 -33
- camomilla/migrations/0003_auto_20210130_1610.py +0 -30
- camomilla/migrations/0004_auto_20210511_0937.py +0 -25
- camomilla/migrations/0005_media_image_props.py +0 -19
- camomilla/migrations/0006_auto_20220103_1845.py +0 -35
- camomilla/migrations/0007_auto_20220211_1622.py +0 -18
- camomilla/migrations/0008_auto_20220309_1616.py +0 -60
- camomilla/migrations/0009_article__hvad_query_category__hvad_query_and_more.py +0 -165
- camomilla/migrations/0010_auto_20220802_1406.py +0 -83
- camomilla/migrations/0011_auto_20220902_1000.py +0 -15
- camomilla/models/category.py +0 -25
- camomilla/models/tag.py +0 -19
- camomilla/theme/static/admin/img/logo.png +0 -0
- camomilla/theme/templates/admin/base_site.html +0 -18
- camomilla/views/categories.py +0 -13
- django_camomilla_cms-5.8.6.dist-info/METADATA +0 -63
- django_camomilla_cms-5.8.6.dist-info/RECORD +0 -76
- tests/urls.py +0 -21
- /camomilla/{migrations → contrib}/__init__.py +0 -0
- /camomilla/templates/{camomilla → defaults}/widgets/media_select_multiple.html +0 -0
- {django_camomilla_cms-5.8.6.dist-info → django_camomilla_cms-6.0.0.dist-info/licenses}/LICENSE +0 -0
- {django_camomilla_cms-5.8.6.dist-info → django_camomilla_cms-6.0.0.dist-info}/top_level.txt +0 -0
camomilla/models/media.py
CHANGED
@@ -1,39 +1,30 @@
|
|
1
1
|
import json
|
2
2
|
import os
|
3
|
-
import magic
|
4
3
|
from io import BytesIO
|
5
4
|
|
6
|
-
|
5
|
+
import magic
|
7
6
|
from django.core.exceptions import ValidationError
|
8
7
|
from django.core.files.base import ContentFile
|
9
|
-
from django.core.files.storage import default_storage as storage
|
10
8
|
from django.db import models
|
11
9
|
from django.db.models.fields.related import ForeignObjectRel
|
12
|
-
from ..fields import JSONField
|
13
10
|
from django.db.models.signals import post_save, pre_delete
|
14
11
|
from django.dispatch import receiver
|
15
12
|
from django.utils.safestring import mark_safe
|
13
|
+
from django.utils.text import slugify
|
16
14
|
from django.utils.translation import gettext_lazy as _
|
17
|
-
from hvad.models import TranslatableModel, TranslatedFields
|
18
15
|
from PIL import Image
|
19
16
|
|
17
|
+
from camomilla.fields import JSONField
|
18
|
+
from camomilla.settings import THUMBNAIL_FOLDER, THUMBNAIL_HEIGHT, THUMBNAIL_WIDTH
|
19
|
+
from camomilla.storages.optimize import OptimizedStorage
|
20
20
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
)
|
26
|
-
slug = models.SlugField()
|
21
|
+
|
22
|
+
class AbstractMediaFolder(models.Model):
|
23
|
+
title = models.CharField(max_length=200)
|
24
|
+
slug = models.SlugField(editable=False, max_length=200, blank=True, null=True)
|
27
25
|
creation_date = models.DateTimeField(auto_now_add=True)
|
28
26
|
last_modified = models.DateTimeField(auto_now=True)
|
29
|
-
|
30
|
-
"camomilla.Media",
|
31
|
-
on_delete=models.SET_NULL,
|
32
|
-
null=True,
|
33
|
-
blank=True,
|
34
|
-
verbose_name=_("Image cover"),
|
35
|
-
)
|
36
|
-
path = models.TextField(blank=True, null=True)
|
27
|
+
path = models.TextField(blank=True, null=True, editable=False)
|
37
28
|
updir = models.ForeignKey(
|
38
29
|
"self",
|
39
30
|
on_delete=models.CASCADE,
|
@@ -50,46 +41,42 @@ class BaseMediaFolder(TranslatableModel):
|
|
50
41
|
folder.save()
|
51
42
|
|
52
43
|
def save(self, *args, **kwargs):
|
44
|
+
self.slug = slugify(self.title)
|
53
45
|
if self.updir:
|
54
46
|
if self.updir.id == self.id:
|
55
47
|
raise ValidationError({"updir": "Unvalid parent"})
|
56
48
|
self.path = "{0}/{1}".format(self.updir.path, self.slug)
|
57
|
-
|
58
49
|
else:
|
59
50
|
self.path = "/{0}".format(self.slug)
|
60
51
|
|
61
|
-
super(
|
52
|
+
super().save(*args, **kwargs)
|
62
53
|
self.update_childs()
|
63
54
|
|
64
55
|
def __str__(self):
|
65
|
-
|
66
|
-
if self.title:
|
67
|
-
to_string += " - " + self.title
|
68
|
-
return to_string
|
56
|
+
return "[%s] %s" % (self.__class__.__name__, self.title)
|
69
57
|
|
70
58
|
|
71
|
-
class MediaFolder(
|
72
|
-
|
59
|
+
class MediaFolder(AbstractMediaFolder):
|
60
|
+
pass
|
73
61
|
|
74
62
|
|
75
|
-
class Media(
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
file = models.FileField()
|
63
|
+
class Media(models.Model):
|
64
|
+
# Seo Attributes
|
65
|
+
alt_text = models.CharField(max_length=200, blank=True, null=True)
|
66
|
+
title = models.CharField(max_length=200, blank=True, null=True)
|
67
|
+
description = models.TextField(blank=True, null=True)
|
68
|
+
|
69
|
+
file = models.FileField(storage=OptimizedStorage())
|
82
70
|
thumbnail = models.ImageField(
|
83
|
-
upload_to=
|
71
|
+
upload_to=THUMBNAIL_FOLDER,
|
84
72
|
max_length=500,
|
85
73
|
null=True,
|
86
74
|
blank=True,
|
87
75
|
)
|
88
76
|
created = models.DateTimeField(auto_now=True)
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
image_props = JSONField(default=dict, blank=True)
|
77
|
+
size = models.IntegerField(default=0, blank=True, null=True, editable=False)
|
78
|
+
mime_type = models.CharField(max_length=128, blank=True, null=True, editable=False)
|
79
|
+
image_props = JSONField(default=dict, blank=True, editable=False)
|
93
80
|
folder = models.ForeignKey(
|
94
81
|
MediaFolder,
|
95
82
|
null=True,
|
@@ -121,6 +108,7 @@ class Media(TranslatableModel):
|
|
121
108
|
ordering = ["-pk"]
|
122
109
|
|
123
110
|
def regenerate_thumbnail(self):
|
111
|
+
self._remove_thumbnail()
|
124
112
|
if self.file:
|
125
113
|
self._make_thumbnail()
|
126
114
|
|
@@ -140,53 +128,42 @@ class Media(TranslatableModel):
|
|
140
128
|
}
|
141
129
|
return json.dumps(json_r)
|
142
130
|
|
143
|
-
def
|
144
|
-
try:
|
145
|
-
fh = storage.open(self.file.name, "rb")
|
146
|
-
self.mime_type = magic.from_buffer(fh.read(2048), mime=True)
|
147
|
-
except FileNotFoundError as ex:
|
148
|
-
print(ex)
|
149
|
-
self.image_props = {}
|
150
|
-
self.mime_type = ""
|
151
|
-
return False
|
131
|
+
def _update_file_info(self, img_bytes=None):
|
152
132
|
try:
|
153
|
-
|
154
|
-
|
155
|
-
self.
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
133
|
+
if not img_bytes:
|
134
|
+
img_bytes = self.file.storage.open(self.file.name, "rb")
|
135
|
+
self.mime_type = magic.from_buffer(img_bytes.read(2048), mime=True)
|
136
|
+
with Image.open(img_bytes) as image:
|
137
|
+
self.image_props = {
|
138
|
+
"width": image.width,
|
139
|
+
"height": image.height,
|
140
|
+
"format": image.format,
|
141
|
+
"mode": image.mode,
|
142
|
+
}
|
161
143
|
except Exception as ex:
|
162
144
|
print(ex)
|
163
145
|
return False
|
164
146
|
|
147
|
+
def _make_thumbnail(self, img_bytes=None):
|
165
148
|
try:
|
166
|
-
|
167
|
-
(
|
168
|
-
|
169
|
-
|
170
|
-
),
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
# Load a ContentFile into the thumbnail field so it gets saved
|
186
|
-
self.thumbnail.save(
|
187
|
-
thumb_filename, ContentFile(temp_thumb.read()), save=False
|
188
|
-
)
|
189
|
-
temp_thumb.close()
|
149
|
+
if not img_bytes:
|
150
|
+
img_bytes = self.file.storage.open(self.file.name, "rb")
|
151
|
+
with Image.open(img_bytes) as orig_image:
|
152
|
+
image = orig_image.copy()
|
153
|
+
image.thumbnail((THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT), Image.LANCZOS)
|
154
|
+
|
155
|
+
# Path to save to, name, and extension
|
156
|
+
thumb_name, thumb_extension = os.path.splitext(self.file.name)
|
157
|
+
thumb_extension = thumb_extension.lower()
|
158
|
+
thumb_filename = thumb_name + "_thumb" + thumb_extension
|
159
|
+
temp_thumb = BytesIO()
|
160
|
+
image.save(temp_thumb, "PNG", optimize=True)
|
161
|
+
temp_thumb.seek(0)
|
162
|
+
# Load a ContentFile into the thumbnail field so it gets saved
|
163
|
+
self.thumbnail.save(
|
164
|
+
thumb_filename, ContentFile(temp_thumb.read()), save=False
|
165
|
+
)
|
166
|
+
temp_thumb.close()
|
190
167
|
except Exception:
|
191
168
|
return False
|
192
169
|
|
@@ -194,34 +171,30 @@ class Media(TranslatableModel):
|
|
194
171
|
|
195
172
|
def _remove_file(self):
|
196
173
|
if self.file:
|
197
|
-
|
198
|
-
if os.path.isfile(file_to_remove):
|
199
|
-
os.remove(file_to_remove)
|
174
|
+
self.file.storage.delete(self.file.name)
|
200
175
|
|
201
176
|
def _remove_thumbnail(self):
|
202
177
|
if self.thumbnail:
|
203
|
-
|
204
|
-
if os.path.isfile(file_to_remove):
|
205
|
-
os.remove(file_to_remove)
|
178
|
+
self.thumbnail.storage.delete(self.thumbnail.name)
|
206
179
|
|
207
180
|
def _get_file_size(self):
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
else:
|
213
|
-
return 0
|
181
|
+
try:
|
182
|
+
return self.file.storage.size(self.file.name)
|
183
|
+
except Exception:
|
184
|
+
return 0
|
214
185
|
|
215
186
|
def __str__(self):
|
216
|
-
if self.
|
217
|
-
return self.
|
187
|
+
if self.title:
|
188
|
+
return self.title
|
218
189
|
return self.file.name
|
219
190
|
|
220
191
|
|
221
192
|
@receiver(post_save, sender=Media, dispatch_uid="make thumbnails")
|
222
|
-
def update_media(sender, instance, **kwargs):
|
193
|
+
def update_media(sender, instance: Media, **kwargs):
|
223
194
|
instance._remove_thumbnail()
|
224
|
-
instance.
|
195
|
+
image_bytes = instance.file.storage.open(instance.file.name, "rb")
|
196
|
+
instance._update_file_info(image_bytes)
|
197
|
+
instance._make_thumbnail(image_bytes)
|
225
198
|
Media.objects.filter(pk=instance.pk).update(
|
226
199
|
size=instance._get_file_size(),
|
227
200
|
thumbnail=instance.thumbnail,
|
@@ -231,6 +204,6 @@ def update_media(sender, instance, **kwargs):
|
|
231
204
|
|
232
205
|
|
233
206
|
@receiver(pre_delete, sender=Media, dispatch_uid="make thumbnails")
|
234
|
-
def delete_media_files(sender, instance, **kwargs):
|
207
|
+
def delete_media_files(sender, instance: Media, **kwargs):
|
235
208
|
instance._remove_thumbnail()
|
236
209
|
instance._remove_file()
|
camomilla/models/menu.py
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
from enum import Enum
|
2
|
+
from uuid import uuid4
|
3
|
+
from django.contrib.contenttypes.models import ContentType
|
4
|
+
from django.db import models
|
5
|
+
from django.utils.translation import gettext_lazy as _
|
6
|
+
from django.template.loader import render_to_string
|
7
|
+
from django.template import RequestContext
|
8
|
+
from django.utils.safestring import mark_safe
|
9
|
+
from pydantic import (
|
10
|
+
Field,
|
11
|
+
SerializationInfo,
|
12
|
+
computed_field,
|
13
|
+
model_serializer,
|
14
|
+
)
|
15
|
+
from structured.pydantic.models import BaseModel
|
16
|
+
from structured.fields import StructuredJSONField
|
17
|
+
from camomilla.models.page import UrlNode, AbstractPage
|
18
|
+
from typing import Optional, Union, Callable, List
|
19
|
+
from django.db.models.base import Model as DjangoModel
|
20
|
+
|
21
|
+
|
22
|
+
class LinkTypes(str, Enum):
|
23
|
+
relational = "RE"
|
24
|
+
static = "ST"
|
25
|
+
|
26
|
+
|
27
|
+
class MenuNodeLink(BaseModel):
|
28
|
+
link_type: LinkTypes = LinkTypes.static
|
29
|
+
static: str = None
|
30
|
+
content_type: ContentType = None
|
31
|
+
page: AbstractPage = None
|
32
|
+
url_node: UrlNode = None
|
33
|
+
|
34
|
+
@model_serializer(mode="wrap", when_used="json")
|
35
|
+
def update_relational(self, handler: Callable, info: SerializationInfo):
|
36
|
+
if self.link_type == LinkTypes.relational:
|
37
|
+
if self.content_type and self.page:
|
38
|
+
if isinstance(self.page, DjangoModel) and not self.page._meta.abstract:
|
39
|
+
self.content_type = ContentType.objects.get_for_model(
|
40
|
+
self.page.__class__
|
41
|
+
)
|
42
|
+
ctype_id = getattr(self.content_type, "pk", self.content_type)
|
43
|
+
page_id = getattr(self.page, "pk", self.page)
|
44
|
+
c_type = ContentType.objects.filter(pk=ctype_id).first()
|
45
|
+
model = c_type and c_type.model_class()
|
46
|
+
page = model and model.objects.filter(pk=page_id).first()
|
47
|
+
self.url_node = page and page.url_node
|
48
|
+
elif self.url_node:
|
49
|
+
url_node_id = getattr(self.url_node, "pk", self.url_node)
|
50
|
+
self.page = UrlNode.objects.filter(pk=url_node_id).first().page
|
51
|
+
self.content_type = ContentType.objects.get_for_model(
|
52
|
+
self.page.__class__
|
53
|
+
)
|
54
|
+
return handler(self)
|
55
|
+
|
56
|
+
def get_url(self, request=None):
|
57
|
+
if self.link_type == LinkTypes.relational:
|
58
|
+
return isinstance(self.url_node, UrlNode) and self.url_node.routerlink
|
59
|
+
elif self.link_type == LinkTypes.static:
|
60
|
+
return self.static
|
61
|
+
|
62
|
+
@computed_field
|
63
|
+
@property
|
64
|
+
def url(self) -> Optional[str]:
|
65
|
+
return self.get_url()
|
66
|
+
|
67
|
+
|
68
|
+
class MenuNode(BaseModel):
|
69
|
+
id: str = Field(default_factory=uuid4)
|
70
|
+
meta: dict = {}
|
71
|
+
nodes: List["MenuNode"] = []
|
72
|
+
title: str = ""
|
73
|
+
link: MenuNodeLink
|
74
|
+
|
75
|
+
|
76
|
+
class Menu(models.Model):
|
77
|
+
key = models.CharField(max_length=200, unique=True, editable=False)
|
78
|
+
available_classes = models.JSONField(default=dict, editable=False)
|
79
|
+
enabled = models.BooleanField(default=True)
|
80
|
+
nodes = StructuredJSONField(default=list, schema=MenuNode)
|
81
|
+
|
82
|
+
class Meta:
|
83
|
+
verbose_name = _("menu")
|
84
|
+
verbose_name_plural = _("menus")
|
85
|
+
|
86
|
+
def render(
|
87
|
+
self,
|
88
|
+
template_path: str,
|
89
|
+
request=None,
|
90
|
+
context: Union[dict, RequestContext] = {},
|
91
|
+
):
|
92
|
+
if isinstance(context, RequestContext):
|
93
|
+
context = context.flatten()
|
94
|
+
is_preview = (
|
95
|
+
False if request is None else bool(request.GET.get("preview", False))
|
96
|
+
)
|
97
|
+
context.update({"menu": self, "is_preview": is_preview})
|
98
|
+
return mark_safe(render_to_string(template_path, context, request))
|
99
|
+
|
100
|
+
class defaultdict(dict):
|
101
|
+
def __missing__(self, key):
|
102
|
+
dict.__setitem__(self, key, Menu.objects.get_or_create(key=key)[0])
|
103
|
+
return self[key]
|
104
|
+
|
105
|
+
def __str__(self) -> str:
|
106
|
+
return self.key
|
@@ -1,66 +1,28 @@
|
|
1
1
|
from django.db import models
|
2
|
-
from ...fields import JSONField
|
3
|
-
from hvad.models import TranslatableModel, TranslatedFields
|
4
2
|
|
5
|
-
from django.utils.translation import gettext_lazy as _
|
6
|
-
from django.utils.text import slugify
|
7
3
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
translations = TranslatedFields(
|
18
|
-
title=models.CharField(max_length=200),
|
19
|
-
description=models.TextField(blank=True, null=True, default=""),
|
20
|
-
permalink=models.CharField(max_length=200, blank=True),
|
21
|
-
og_description=models.TextField(blank=True, null=True, default=""),
|
22
|
-
og_title=models.CharField(max_length=200, blank=True, null=True, default=""),
|
23
|
-
og_type=models.CharField(max_length=200, blank=True, null=True, default=""),
|
24
|
-
og_url=models.CharField(max_length=200, blank=True, null=True, default=""),
|
25
|
-
canonical=models.CharField(max_length=200, blank=True, null=True, default=""),
|
26
|
-
)
|
4
|
+
class SeoMixin(models.Model):
|
5
|
+
title = models.CharField(max_length=200, null=True, blank=True)
|
6
|
+
description = models.TextField(null=True, blank=True)
|
7
|
+
og_description = models.TextField(blank=True, null=True)
|
8
|
+
og_title = models.CharField(max_length=200, blank=True, null=True)
|
9
|
+
og_type = models.CharField(max_length=200, blank=True, null=True)
|
10
|
+
og_url = models.CharField(max_length=200, blank=True, null=True)
|
11
|
+
canonical = models.CharField(max_length=200, blank=True, null=True)
|
27
12
|
og_image = models.ForeignKey(
|
28
13
|
"camomilla.Media",
|
29
14
|
blank=True,
|
30
15
|
null=True,
|
31
16
|
on_delete=models.SET_NULL,
|
32
|
-
related_name="%(app_label)s_%(class)
|
17
|
+
related_name="%(app_label)s_%(class)s_og_images",
|
33
18
|
)
|
34
19
|
|
35
|
-
@classmethod
|
36
|
-
def get(model, request, **kwargs):
|
37
|
-
return get_seo_model(request, model, **kwargs)
|
38
|
-
|
39
|
-
def alternate_urls(self, request):
|
40
|
-
return alternate_seo_url_with_object(
|
41
|
-
request, self.__class__, permalink=self.permalink
|
42
|
-
)
|
43
|
-
|
44
20
|
class Meta:
|
45
21
|
abstract = True
|
46
22
|
|
47
23
|
|
48
|
-
class SlugMixin(object):
|
49
|
-
|
50
|
-
slug_attr = "title"
|
51
|
-
|
52
|
-
def get_slug(self):
|
53
|
-
return self.slug
|
54
|
-
|
55
|
-
get_slug.short_description = _("Slug")
|
56
|
-
|
57
|
-
def save(self, *args, **kwargs):
|
58
|
-
self.slug = slugify(getattr(self, self.slug_attr))
|
59
|
-
super(SlugMixin, self).save(*args, **kwargs)
|
60
|
-
|
61
|
-
|
62
24
|
class MetaMixin(models.Model):
|
63
|
-
meta = JSONField(default=dict)
|
25
|
+
meta = models.JSONField(default=dict, null=False, blank=True)
|
64
26
|
|
65
27
|
def get_meta(self, key, default=None):
|
66
28
|
return self.meta.get(key, default)
|