django-camomilla-cms 5.8.5__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.
Files changed (143) hide show
  1. camomilla/__init__.py +8 -2
  2. camomilla/apps.py +9 -1
  3. camomilla/context_processors.py +6 -0
  4. camomilla/contrib/modeltranslation/__init__.py +0 -0
  5. camomilla/contrib/modeltranslation/hvad_migration.py +126 -0
  6. camomilla/dynamic_pages_urls.py +33 -0
  7. camomilla/fields/__init__.py +13 -0
  8. camomilla/{fields.py → fields/json.py} +15 -18
  9. camomilla/management/commands/regenerate_thumbnails.py +0 -1
  10. camomilla/managers/__init__.py +3 -0
  11. camomilla/managers/pages.py +116 -0
  12. camomilla/model_api.py +86 -0
  13. camomilla/models/__init__.py +5 -6
  14. camomilla/models/article.py +26 -44
  15. camomilla/models/content.py +8 -15
  16. camomilla/models/media.py +70 -97
  17. camomilla/models/menu.py +106 -0
  18. camomilla/models/mixins/__init__.py +10 -48
  19. camomilla/models/page.py +521 -20
  20. camomilla/openapi/__init__.py +0 -0
  21. camomilla/openapi/schema.py +67 -0
  22. camomilla/parsers.py +0 -1
  23. camomilla/redirects.py +10 -0
  24. camomilla/serializers/__init__.py +2 -0
  25. camomilla/serializers/article.py +5 -10
  26. camomilla/serializers/base/__init__.py +21 -17
  27. camomilla/serializers/content_type.py +17 -0
  28. camomilla/serializers/fields/__init__.py +6 -20
  29. camomilla/serializers/fields/file.py +5 -0
  30. camomilla/serializers/fields/related.py +24 -4
  31. camomilla/serializers/media.py +6 -8
  32. camomilla/serializers/menu.py +17 -0
  33. camomilla/serializers/mixins/__init__.py +23 -187
  34. camomilla/serializers/mixins/fields.py +20 -0
  35. camomilla/serializers/mixins/filter_fields.py +57 -0
  36. camomilla/serializers/mixins/json.py +34 -0
  37. camomilla/serializers/mixins/language.py +32 -0
  38. camomilla/serializers/mixins/nesting.py +35 -0
  39. camomilla/serializers/mixins/optimize.py +91 -0
  40. camomilla/serializers/mixins/ordering.py +34 -0
  41. camomilla/serializers/mixins/page.py +58 -0
  42. camomilla/serializers/mixins/translation.py +103 -0
  43. camomilla/serializers/page.py +53 -4
  44. camomilla/serializers/user.py +5 -4
  45. camomilla/serializers/utils.py +38 -0
  46. camomilla/serializers/validators.py +51 -0
  47. camomilla/settings.py +118 -0
  48. camomilla/sitemap.py +30 -0
  49. camomilla/storages/__init__.py +4 -0
  50. camomilla/storages/default.py +12 -0
  51. camomilla/storages/optimize.py +71 -0
  52. camomilla/{storages.py → storages/overwrite.py} +2 -2
  53. camomilla/templates/admin/camomilla/page/change_form.html +10 -0
  54. camomilla/templates/defaults/articles/default.html +7 -0
  55. camomilla/templates/defaults/base.html +170 -0
  56. camomilla/templates/defaults/pages/default.html +3 -0
  57. camomilla/templates/defaults/parts/langswitch.html +83 -0
  58. camomilla/templates/defaults/parts/menu.html +15 -0
  59. camomilla/templates_context/__init__.py +0 -0
  60. camomilla/templates_context/autodiscover.py +51 -0
  61. camomilla/templates_context/rendering.py +89 -0
  62. camomilla/templatetags/camomilla_filters.py +6 -5
  63. camomilla/templatetags/menus.py +37 -0
  64. camomilla/templatetags/model_extras.py +77 -0
  65. camomilla/theme/__init__.py +1 -1
  66. camomilla/theme/admin/__init__.py +99 -0
  67. camomilla/theme/admin/pages.py +46 -0
  68. camomilla/theme/admin/translations.py +13 -0
  69. camomilla/theme/apps.py +38 -0
  70. camomilla/theme/static/admin/css/responsive.css +5 -1021
  71. camomilla/theme/static/admin/img/favicon.ico +0 -0
  72. camomilla/theme/static/admin/img/logo.svg +31 -0
  73. camomilla/theme/templates/admin/base.html +7 -0
  74. camomilla/theme/templates/rosetta/base.html +196 -0
  75. camomilla/translation.py +61 -0
  76. camomilla/urls.py +38 -17
  77. camomilla/utils/__init__.py +4 -0
  78. camomilla/utils/getters.py +27 -0
  79. camomilla/utils/normalization.py +7 -0
  80. camomilla/utils/query_parser.py +167 -0
  81. camomilla/{utils.py → utils/seo.py} +13 -15
  82. camomilla/utils/setters.py +37 -0
  83. camomilla/utils/templates.py +32 -0
  84. camomilla/utils/translation.py +114 -0
  85. camomilla/views/__init__.py +1 -1
  86. camomilla/views/articles.py +5 -7
  87. camomilla/views/base/__init__.py +35 -5
  88. camomilla/views/contents.py +6 -11
  89. camomilla/views/decorators.py +26 -0
  90. camomilla/views/medias.py +24 -19
  91. camomilla/views/menus.py +81 -0
  92. camomilla/views/mixins/__init__.py +17 -73
  93. camomilla/views/mixins/bulk_actions.py +22 -0
  94. camomilla/views/mixins/language.py +33 -0
  95. camomilla/views/mixins/optimize.py +18 -0
  96. camomilla/views/mixins/ordering.py +2 -2
  97. camomilla/views/mixins/pagination.py +12 -18
  98. camomilla/views/mixins/permissions.py +6 -0
  99. camomilla/views/pages.py +28 -6
  100. camomilla/views/tags.py +5 -6
  101. camomilla/views/users.py +7 -12
  102. django_camomilla_cms-6.0.0.dist-info/METADATA +123 -0
  103. django_camomilla_cms-6.0.0.dist-info/RECORD +133 -0
  104. {django_camomilla_cms-5.8.5.dist-info → django_camomilla_cms-6.0.0.dist-info}/WHEEL +1 -1
  105. tests/fixtures/__init__.py +14 -0
  106. tests/test_api.py +22 -39
  107. tests/test_camomilla_filters.py +11 -13
  108. tests/test_media.py +152 -0
  109. tests/test_menu.py +112 -0
  110. tests/test_model_api.py +113 -0
  111. tests/test_model_api_permissions.py +44 -0
  112. tests/test_model_api_register.py +355 -0
  113. tests/test_pages.py +351 -0
  114. tests/test_query_parser.py +58 -0
  115. tests/test_templates_context.py +149 -0
  116. tests/test_utils.py +64 -64
  117. tests/utils/__init__.py +0 -0
  118. tests/utils/api.py +28 -0
  119. tests/utils/media.py +10 -0
  120. camomilla/admin.py +0 -98
  121. camomilla/migrations/0001_initial.py +0 -577
  122. camomilla/migrations/0002_auto_20200214_1127.py +0 -33
  123. camomilla/migrations/0003_auto_20210130_1610.py +0 -30
  124. camomilla/migrations/0004_auto_20210511_0937.py +0 -25
  125. camomilla/migrations/0005_media_image_props.py +0 -19
  126. camomilla/migrations/0006_auto_20220103_1845.py +0 -35
  127. camomilla/migrations/0007_auto_20220211_1622.py +0 -18
  128. camomilla/migrations/0008_auto_20220309_1616.py +0 -60
  129. camomilla/migrations/0009_article__hvad_query_category__hvad_query_and_more.py +0 -165
  130. camomilla/migrations/0010_auto_20220802_1406.py +0 -83
  131. camomilla/migrations/0011_auto_20220902_1000.py +0 -15
  132. camomilla/models/category.py +0 -25
  133. camomilla/models/tag.py +0 -19
  134. camomilla/theme/static/admin/img/logo.png +0 -0
  135. camomilla/theme/templates/admin/base_site.html +0 -18
  136. camomilla/views/categories.py +0 -13
  137. django_camomilla_cms-5.8.5.dist-info/METADATA +0 -62
  138. django_camomilla_cms-5.8.5.dist-info/RECORD +0 -76
  139. tests/urls.py +0 -21
  140. /camomilla/{migrations → contrib}/__init__.py +0 -0
  141. /camomilla/templates/{camomilla → defaults}/widgets/media_select_multiple.html +0 -0
  142. {django_camomilla_cms-5.8.5.dist-info → django_camomilla_cms-6.0.0.dist-info/licenses}/LICENSE +0 -0
  143. {django_camomilla_cms-5.8.5.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
- from django.conf import settings
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
- class BaseMediaFolder(TranslatableModel):
22
- translations = TranslatedFields(
23
- description=models.CharField(max_length=200, blank=True, null=True),
24
- title=models.CharField(max_length=200, blank=True, null=True),
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
- icon = models.ForeignKey(
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(BaseMediaFolder, self).save(*args, **kwargs)
52
+ super().save(*args, **kwargs)
62
53
  self.update_childs()
63
54
 
64
55
  def __str__(self):
65
- to_string = self.slug
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(BaseMediaFolder):
72
- translations = TranslatedFields()
59
+ class MediaFolder(AbstractMediaFolder):
60
+ pass
73
61
 
74
62
 
75
- class Media(TranslatableModel):
76
- translations = TranslatedFields(
77
- alt_text=models.CharField(max_length=200, blank=True, null=True),
78
- title=models.CharField(max_length=200, blank=True, null=True),
79
- description=models.TextField(blank=True, null=True),
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=getattr(settings, "THUMB_FOLDER", "thumbnails"),
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
- name = models.CharField(max_length=200, blank=True, null=True)
90
- size = models.IntegerField(default=0, blank=True, null=True)
91
- mime_type = models.CharField(max_length=128, blank=True, null=True)
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 _make_thumbnail(self):
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
- orig_image = Image.open(fh)
154
- image = orig_image.copy()
155
- self.image_props = {
156
- "width": orig_image.width,
157
- "height": orig_image.height,
158
- "format": orig_image.format,
159
- "mode": orig_image.mode,
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
- image.thumbnail(
167
- (
168
- getattr(settings, "CAMOMILLA_THUMBNAIL_WIDTH", 50),
169
- getattr(settings, "CAMOMILLA_THUMBNAIL_HEIGHT", 50),
170
- ),
171
- Image.ANTIALIAS,
172
- )
173
- fh.close()
174
-
175
- # Path to save to, name, and extension
176
- thumb_name, thumb_extension = os.path.splitext(self.file.name)
177
- thumb_extension = thumb_extension.lower()
178
-
179
- thumb_filename = thumb_name + "_thumb" + thumb_extension
180
-
181
- temp_thumb = BytesIO()
182
- image.save(temp_thumb, "PNG", optimize=True)
183
- temp_thumb.seek(0)
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
- file_to_remove = os.path.join(settings.MEDIA_ROOT, self.file.name)
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
- file_to_remove = os.path.join(settings.MEDIA_ROOT, self.thumbnail.name)
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
- if self.file:
209
- file_to_calc = os.path.join(settings.MEDIA_ROOT, self.file.name)
210
- if os.path.isfile(file_to_calc):
211
- return self.file.size
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.name:
217
- return self.name
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._make_thumbnail()
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()
@@ -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
- from ...utils import get_seo_model
9
-
10
- from djlotrek.utils import alternate_seo_url_with_object
11
-
12
-
13
- class SeoMixin(TranslatableModel):
14
-
15
- seo_attr = "identifier"
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)s_related",
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)