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.
- 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.5.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.5.dist-info/METADATA +0 -62
- django_camomilla_cms-5.8.5.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.5.dist-info → django_camomilla_cms-6.0.0.dist-info/licenses}/LICENSE +0 -0
- {django_camomilla_cms-5.8.5.dist-info → django_camomilla_cms-6.0.0.dist-info}/top_level.txt +0 -0
camomilla/__init__.py
CHANGED
@@ -1,5 +1,11 @@
|
|
1
|
-
__version__ = "
|
1
|
+
__version__ = "6.0.0"
|
2
2
|
|
3
3
|
|
4
4
|
def get_core_apps():
|
5
|
-
return ["rest_framework", "rest_framework.authtoken"
|
5
|
+
return ["rest_framework", "rest_framework.authtoken"]
|
6
|
+
|
7
|
+
|
8
|
+
def autodiscover():
|
9
|
+
from camomilla.templates_context.autodiscover import autodiscover_context_files
|
10
|
+
|
11
|
+
autodiscover_context_files()
|
camomilla/apps.py
CHANGED
@@ -1,8 +1,16 @@
|
|
1
1
|
from __future__ import unicode_literals
|
2
2
|
|
3
3
|
from django.apps import AppConfig
|
4
|
+
from django.conf import settings
|
4
5
|
|
5
6
|
|
6
7
|
class CamomillaConfig(AppConfig):
|
7
|
-
default_auto_field =
|
8
|
+
default_auto_field = "django.db.models.AutoField"
|
8
9
|
name = "camomilla"
|
10
|
+
|
11
|
+
def ready(self):
|
12
|
+
migration_modules = getattr(settings, "MIGRATION_MODULES", {})
|
13
|
+
if "camomilla" not in migration_modules:
|
14
|
+
migration_modules["camomilla"] = "camomilla_migrations"
|
15
|
+
setattr(settings, "MIGRATION_MODULES", migration_modules)
|
16
|
+
self.module.autodiscover()
|
File without changes
|
@@ -0,0 +1,126 @@
|
|
1
|
+
from django.conf import settings
|
2
|
+
from django.db import migrations, connection
|
3
|
+
|
4
|
+
|
5
|
+
class KeepTranslationsMixin:
|
6
|
+
"""
|
7
|
+
This mixin make it possible to keep translations when migrating from django-hvad to modeltranslation and viceversa.
|
8
|
+
To use it, you have to add a dictionary to your migration class called "keep_translations".
|
9
|
+
The dictionary must have model paths as keys and a list of fields to keep as values.
|
10
|
+
|
11
|
+
|
12
|
+
Example:
|
13
|
+
```python
|
14
|
+
class Migration(KeepTranslationsMixin, migrations.Migration):
|
15
|
+
keep_translations = {
|
16
|
+
"app.Model": ("field1", "field2", "field3")
|
17
|
+
}
|
18
|
+
```
|
19
|
+
"""
|
20
|
+
|
21
|
+
_saved_data_from_plain = {}
|
22
|
+
language_codes = dict(getattr(settings, "LANGUAGES", {})).keys()
|
23
|
+
|
24
|
+
def is_operation_legit(self, o):
|
25
|
+
return not (
|
26
|
+
isinstance(o, migrations.RemoveField)
|
27
|
+
and o.name == "master"
|
28
|
+
and o.model_name.endswith("translation")
|
29
|
+
)
|
30
|
+
|
31
|
+
def __init__(self, *args, **kwargs):
|
32
|
+
super().__init__(*args, **kwargs)
|
33
|
+
self.operations = [o for o in self.operations if self.is_operation_legit(o)]
|
34
|
+
self.operations.insert(
|
35
|
+
0, migrations.RunPython(self._getDataFromHvad, self._restoreDataToHvad)
|
36
|
+
)
|
37
|
+
self.operations.append(
|
38
|
+
migrations.RunPython(
|
39
|
+
self._restoreDataToModelTranslation, self._getDataFromModelTranslation
|
40
|
+
)
|
41
|
+
)
|
42
|
+
|
43
|
+
def _getDataFromHvad(self, apps, schemaeditor):
|
44
|
+
for modelPath, fields in self.keep_translations.items():
|
45
|
+
Model = apps.get_model(*modelPath.split("."))
|
46
|
+
table = Model._meta.db_table + "_translation"
|
47
|
+
if "language_code" not in fields:
|
48
|
+
fields = ("language_code",) + fields
|
49
|
+
with connection.cursor() as cursor:
|
50
|
+
cursor.execute("SELECT master_id FROM {0};".format(table))
|
51
|
+
masters = list(set(cursor.fetchall()))
|
52
|
+
for master in masters:
|
53
|
+
cursor.execute(
|
54
|
+
"SELECT {0} FROM {1} WHERE master_id={2};".format(
|
55
|
+
",".join(fields), table, master[0]
|
56
|
+
)
|
57
|
+
)
|
58
|
+
rows = cursor.fetchall()
|
59
|
+
self._saved_data_from_plain[modelPath] = (
|
60
|
+
self._saved_data_from_plain.get(modelPath, {})
|
61
|
+
)
|
62
|
+
self._saved_data_from_plain[modelPath][master[0]] = (
|
63
|
+
self._saved_data_from_plain[modelPath].get(master[0], [])
|
64
|
+
)
|
65
|
+
for row in rows:
|
66
|
+
self._saved_data_from_plain[modelPath][master[0]].append(
|
67
|
+
dict(zip(fields, row))
|
68
|
+
)
|
69
|
+
|
70
|
+
def _getDataFromModelTranslation(self, apps, schemaeditor):
|
71
|
+
for modelPath, fields in self.keep_translations.items():
|
72
|
+
Model = apps.get_model(*modelPath.split("."))
|
73
|
+
table = Model._meta.db_table
|
74
|
+
for lang in self.language_codes:
|
75
|
+
t_fields = ("id",) + tuple(
|
76
|
+
"{0}_{1}".format(f, lang) for f in fields if f != "id"
|
77
|
+
)
|
78
|
+
with connection.cursor() as cursor:
|
79
|
+
cursor.execute(
|
80
|
+
"SELECT {0} FROM {1}".format(
|
81
|
+
",".join(t_fields),
|
82
|
+
table,
|
83
|
+
)
|
84
|
+
)
|
85
|
+
rows = cursor.fetchall()
|
86
|
+
self._saved_data_from_plain[modelPath] = (
|
87
|
+
self._saved_data_from_plain.get(modelPath, [])
|
88
|
+
)
|
89
|
+
for row in rows:
|
90
|
+
row_data = dict(zip(("master_id", *fields), row))
|
91
|
+
row_data.update({"language_code": lang})
|
92
|
+
self._saved_data_from_plain[modelPath].append(row_data)
|
93
|
+
|
94
|
+
def _restoreDataToModelTranslation(self, apps, schemaeditor):
|
95
|
+
for key, master_dict in self._saved_data_from_plain.items():
|
96
|
+
Model = apps.get_model(*key.split("."))
|
97
|
+
for pk, translations in master_dict.items():
|
98
|
+
try:
|
99
|
+
obj = Model.objects.get(pk=pk)
|
100
|
+
except Model.DoesNotExist:
|
101
|
+
continue
|
102
|
+
for translation in translations:
|
103
|
+
lang = translation.pop("language_code")
|
104
|
+
for attr, value in translation.items():
|
105
|
+
setattr(obj, "{0}_{1}".format(attr, lang), value)
|
106
|
+
obj.save()
|
107
|
+
|
108
|
+
def _restoreDataToHvad(self, apps, schemaeditor):
|
109
|
+
for key, rows in self._saved_data_from_plain.items():
|
110
|
+
Model = apps.get_model(*key.split("."))
|
111
|
+
table = Model._meta.db_table + "_translation"
|
112
|
+
for row in rows:
|
113
|
+
with connection.cursor() as cursor:
|
114
|
+
print(row)
|
115
|
+
cursor.execute(
|
116
|
+
"INSERT INTO {0} ({1}) VALUES ({2});".format(
|
117
|
+
table,
|
118
|
+
",".join(row.keys()),
|
119
|
+
",".join(
|
120
|
+
[
|
121
|
+
"'{}'".format(v).replace("'None'", "NULL")
|
122
|
+
for v in row.values()
|
123
|
+
]
|
124
|
+
),
|
125
|
+
)
|
126
|
+
)
|
@@ -0,0 +1,33 @@
|
|
1
|
+
from django.shortcuts import redirect, render
|
2
|
+
from django.urls import path
|
3
|
+
|
4
|
+
from camomilla import settings
|
5
|
+
from django.conf import settings as django_settings
|
6
|
+
from .models import Page, UrlRedirect
|
7
|
+
|
8
|
+
|
9
|
+
def fetch(request, *args, **kwargs):
|
10
|
+
can_preview = request.user.is_staff or settings.DEBUG
|
11
|
+
preview = can_preview and request.GET.get("preview", False)
|
12
|
+
append_slash = getattr(django_settings, "APPEND_SLASH", True)
|
13
|
+
redirect_obj = UrlRedirect.find_redirect(request)
|
14
|
+
if redirect_obj:
|
15
|
+
return redirect_obj.redirect()
|
16
|
+
if append_slash and not request.path.endswith("/"):
|
17
|
+
q_string = request.META.get("QUERY_STRING", "")
|
18
|
+
return redirect(request.path + "/" + ("?" + q_string if q_string else ""))
|
19
|
+
if "permalink" in kwargs:
|
20
|
+
page = Page.get_or_404(
|
21
|
+
request, bypass_public_check=preview, bypass_type_check=True
|
22
|
+
)
|
23
|
+
elif settings.AUTO_CREATE_HOMEPAGE is False:
|
24
|
+
page, _ = Page.get_or_404(permalink="/", bypass_type_check=True)
|
25
|
+
else:
|
26
|
+
page, _ = Page.get_or_create_homepage()
|
27
|
+
return render(request, page.get_template_path(request), page.get_context(request))
|
28
|
+
|
29
|
+
|
30
|
+
urlpatterns = [
|
31
|
+
path("", fetch, name="camomilla-homepage"),
|
32
|
+
path("<path:permalink>", fetch, name="camomilla-permalink"),
|
33
|
+
]
|
@@ -0,0 +1,13 @@
|
|
1
|
+
from django.db import models
|
2
|
+
|
3
|
+
from .json import ArrayField, JSONField
|
4
|
+
|
5
|
+
ORDERING_ACCEPTED_FIELDS = (
|
6
|
+
models.BigIntegerField,
|
7
|
+
models.IntegerField,
|
8
|
+
models.PositiveIntegerField,
|
9
|
+
models.PositiveSmallIntegerField,
|
10
|
+
models.SmallIntegerField,
|
11
|
+
)
|
12
|
+
|
13
|
+
__all__ = ["JSONField", "ArrayField", "ORDERING_ACCEPTED_FIELDS"]
|
@@ -1,32 +1,18 @@
|
|
1
1
|
import json
|
2
|
+
|
2
3
|
import django
|
3
4
|
from django.conf import settings
|
5
|
+
|
4
6
|
if django.VERSION >= (4, 0):
|
5
7
|
from django.db.models import JSONField as DjangoJSONField
|
6
8
|
else:
|
7
9
|
from django.contrib.postgres.fields import JSONField as DjangoJSONField
|
10
|
+
|
8
11
|
from django.contrib.postgres.fields import ArrayField as DjangoArrayField
|
9
12
|
from django.db import models
|
10
13
|
|
11
14
|
|
12
|
-
|
13
|
-
models.BigIntegerField,
|
14
|
-
models.IntegerField,
|
15
|
-
models.PositiveIntegerField,
|
16
|
-
models.PositiveSmallIntegerField,
|
17
|
-
models.SmallIntegerField,
|
18
|
-
)
|
19
|
-
|
20
|
-
|
21
|
-
class JSONField(DjangoJSONField):
|
22
|
-
pass
|
23
|
-
|
24
|
-
|
25
|
-
class ArrayField(DjangoArrayField):
|
26
|
-
pass
|
27
|
-
|
28
|
-
|
29
|
-
if "sqlite" in settings.DATABASES["default"]["ENGINE"]:
|
15
|
+
if "sqlite" in settings.DATABASES["default"]["ENGINE"]: # noqa: C901
|
30
16
|
|
31
17
|
class JSONField(models.Field):
|
32
18
|
def db_type(self, connection):
|
@@ -70,3 +56,14 @@ if "sqlite" in settings.DATABASES["default"]["ENGINE"]:
|
|
70
56
|
}
|
71
57
|
)
|
72
58
|
return name, path, args, kwargs
|
59
|
+
|
60
|
+
else:
|
61
|
+
|
62
|
+
class JSONField(DjangoJSONField):
|
63
|
+
pass
|
64
|
+
|
65
|
+
class ArrayField(DjangoArrayField):
|
66
|
+
pass
|
67
|
+
|
68
|
+
|
69
|
+
__all__ = [JSONField, ArrayField]
|
@@ -0,0 +1,116 @@
|
|
1
|
+
from django.db.models.query import QuerySet
|
2
|
+
from django.core.exceptions import ObjectDoesNotExist
|
3
|
+
from django.apps import apps
|
4
|
+
from django.db import models
|
5
|
+
from django.utils import timezone
|
6
|
+
from django.db.utils import ProgrammingError, OperationalError
|
7
|
+
from typing import Sequence, Tuple
|
8
|
+
|
9
|
+
URL_NODE_RELATED_NAME = "%(app_label)s_%(class)s"
|
10
|
+
|
11
|
+
|
12
|
+
class PageQuerySet(QuerySet):
|
13
|
+
|
14
|
+
__UrlNodeModel = None
|
15
|
+
|
16
|
+
@property
|
17
|
+
def UrlNodeModel(self):
|
18
|
+
if not self.__UrlNodeModel:
|
19
|
+
self.__UrlNodeModel = apps.get_model("camomilla", "UrlNode")
|
20
|
+
return self.__UrlNodeModel
|
21
|
+
|
22
|
+
def get_permalink_kwargs(self, kwargs):
|
23
|
+
return list(
|
24
|
+
set(kwargs.keys()).intersection(
|
25
|
+
set(self.UrlNodeModel.LANG_PERMALINK_FIELDS + ["permalink"])
|
26
|
+
)
|
27
|
+
)
|
28
|
+
|
29
|
+
def get(self, *args, **kwargs):
|
30
|
+
permalink_args = self.get_permalink_kwargs(kwargs)
|
31
|
+
if len(permalink_args):
|
32
|
+
try:
|
33
|
+
node = self.UrlNodeModel.objects.get(
|
34
|
+
**{arg: kwargs.pop(arg) for arg in permalink_args}
|
35
|
+
)
|
36
|
+
kwargs["url_node"] = node
|
37
|
+
except ObjectDoesNotExist:
|
38
|
+
raise self.model.DoesNotExist(
|
39
|
+
"%s matching query does not exist." % self.model._meta.object_name
|
40
|
+
)
|
41
|
+
return super(PageQuerySet, self).get(*args, **kwargs)
|
42
|
+
|
43
|
+
|
44
|
+
class UrlNodeManager(models.Manager):
|
45
|
+
@property
|
46
|
+
def related_names(self):
|
47
|
+
self._related_names = getattr(
|
48
|
+
self,
|
49
|
+
"_related_names",
|
50
|
+
super().get_queryset().values_list("related_name", flat=True).distinct(),
|
51
|
+
)
|
52
|
+
return self._related_names
|
53
|
+
|
54
|
+
def _annotate_fields(
|
55
|
+
self,
|
56
|
+
qs: models.QuerySet,
|
57
|
+
field_names: Sequence[Tuple[str, models.Field, models.Value]],
|
58
|
+
):
|
59
|
+
for field_name, output_field, default in field_names:
|
60
|
+
whens = [
|
61
|
+
models.When(
|
62
|
+
related_name=related_name,
|
63
|
+
then=models.F("__".join([related_name, field_name])),
|
64
|
+
)
|
65
|
+
for related_name in self.related_names
|
66
|
+
]
|
67
|
+
qs = qs.annotate(
|
68
|
+
**{
|
69
|
+
field_name: models.Case(
|
70
|
+
*whens, output_field=output_field, default=default
|
71
|
+
)
|
72
|
+
}
|
73
|
+
)
|
74
|
+
return self._annotate_is_public(qs)
|
75
|
+
|
76
|
+
def _annotate_is_public(self, qs: models.QuerySet):
|
77
|
+
return qs.annotate(
|
78
|
+
is_public=models.Case(
|
79
|
+
models.When(status="PUB", then=True),
|
80
|
+
models.When(
|
81
|
+
status="PLA", publication_date__lte=timezone.now(), then=True
|
82
|
+
),
|
83
|
+
default=False,
|
84
|
+
output_field=models.BooleanField(default=False),
|
85
|
+
)
|
86
|
+
)
|
87
|
+
|
88
|
+
def get_queryset(self):
|
89
|
+
try:
|
90
|
+
return self._annotate_fields(
|
91
|
+
super().get_queryset(),
|
92
|
+
[
|
93
|
+
(
|
94
|
+
"indexable",
|
95
|
+
models.BooleanField(),
|
96
|
+
models.Value(None, models.BooleanField()),
|
97
|
+
),
|
98
|
+
(
|
99
|
+
"status",
|
100
|
+
models.CharField(),
|
101
|
+
models.Value("DRF", models.CharField()),
|
102
|
+
),
|
103
|
+
(
|
104
|
+
"publication_date",
|
105
|
+
models.DateTimeField(),
|
106
|
+
models.Value(timezone.now(), models.DateTimeField()),
|
107
|
+
),
|
108
|
+
(
|
109
|
+
"date_updated_at",
|
110
|
+
models.DateTimeField(),
|
111
|
+
models.Value(timezone.now(), models.DateTimeField()),
|
112
|
+
),
|
113
|
+
],
|
114
|
+
)
|
115
|
+
except (ProgrammingError, OperationalError):
|
116
|
+
return super().get_queryset()
|
camomilla/model_api.py
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
from rest_framework import routers
|
2
|
+
|
3
|
+
from django.urls import path, include
|
4
|
+
from camomilla.views.base import BaseModelViewset
|
5
|
+
from camomilla.serializers.base import BaseModelSerializer
|
6
|
+
|
7
|
+
router = routers.DefaultRouter()
|
8
|
+
urlpatterns = []
|
9
|
+
|
10
|
+
|
11
|
+
def register(
|
12
|
+
base_serializer=BaseModelSerializer,
|
13
|
+
base_viewset=BaseModelViewset,
|
14
|
+
serializer_meta={},
|
15
|
+
viewset_attrs={},
|
16
|
+
filters=None,
|
17
|
+
):
|
18
|
+
"""
|
19
|
+
Register a model to the API.
|
20
|
+
:param base_serializer: The base serializer to use for the model.
|
21
|
+
:param base_viewset: The base viewset to use for the model.
|
22
|
+
:param serializer_meta: The meta class to use for the serializer.
|
23
|
+
:param viewset_attrs: The attributes to add to the viewset.
|
24
|
+
:param filters: The filters to apply to the queryset.
|
25
|
+
:return: The model.
|
26
|
+
"""
|
27
|
+
|
28
|
+
def inner(model):
|
29
|
+
global urlpatterns
|
30
|
+
base_meta = {
|
31
|
+
"model": model,
|
32
|
+
"fields": "__all__",
|
33
|
+
}
|
34
|
+
if "exclude" in serializer_meta:
|
35
|
+
base_meta.pop("fields")
|
36
|
+
serializer = type(
|
37
|
+
f"{model.__name__}Serializer",
|
38
|
+
(base_serializer,),
|
39
|
+
{
|
40
|
+
"Meta": type(
|
41
|
+
"Meta",
|
42
|
+
(),
|
43
|
+
{
|
44
|
+
**base_meta,
|
45
|
+
**serializer_meta,
|
46
|
+
},
|
47
|
+
)
|
48
|
+
},
|
49
|
+
)
|
50
|
+
|
51
|
+
def get_queryset(self, *args, **kwargs):
|
52
|
+
qs = super(base_viewset, self).get_queryset(*args, **kwargs)
|
53
|
+
return qs if filters is None else qs.filter(**filters)
|
54
|
+
|
55
|
+
viewset = type(
|
56
|
+
f"{model.__name__}ViewSet",
|
57
|
+
(base_viewset,),
|
58
|
+
{
|
59
|
+
"queryset": model.objects.all(),
|
60
|
+
"model": model,
|
61
|
+
"get_queryset": get_queryset,
|
62
|
+
"serializer_class": serializer,
|
63
|
+
**viewset_attrs,
|
64
|
+
},
|
65
|
+
)
|
66
|
+
|
67
|
+
model_path = "".join(
|
68
|
+
[
|
69
|
+
(
|
70
|
+
"-" + character.lower()
|
71
|
+
if character.isupper() and index > 0
|
72
|
+
else character
|
73
|
+
)
|
74
|
+
for index, character in enumerate(model.__name__)
|
75
|
+
]
|
76
|
+
).lstrip("-")
|
77
|
+
|
78
|
+
router.register(
|
79
|
+
f"{model_path.replace(' ', '_').lower().lower()}",
|
80
|
+
viewset,
|
81
|
+
f"{model.__name__.lower()}_api",
|
82
|
+
)
|
83
|
+
urlpatterns = [path("", include(router.urls))]
|
84
|
+
return model
|
85
|
+
|
86
|
+
return inner
|
camomilla/models/__init__.py
CHANGED
@@ -1,6 +1,5 @@
|
|
1
|
-
from .article import *
|
2
|
-
from .
|
3
|
-
from .
|
4
|
-
from .
|
5
|
-
from .
|
6
|
-
from .tag import *
|
1
|
+
from .article import * # NOQA
|
2
|
+
from .content import * # NOQA
|
3
|
+
from .media import * # NOQA
|
4
|
+
from .page import * # NOQA
|
5
|
+
from .menu import * # NOQA
|
camomilla/models/article.py
CHANGED
@@ -1,62 +1,44 @@
|
|
1
|
-
from django.conf import settings
|
1
|
+
from django.conf import settings as dj_settings
|
2
2
|
from django.db import models
|
3
3
|
|
4
|
-
from
|
4
|
+
from camomilla.models.page import AbstractPage
|
5
|
+
from camomilla import settings
|
5
6
|
|
6
|
-
from hvad.models import TranslatedFields
|
7
7
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
CONTENT_STATUS = (
|
12
|
-
("PUB", _("Published")),
|
13
|
-
("DRF", _("Draft")),
|
14
|
-
("TRS", _("Trash")),
|
15
|
-
("PLA", _("Planned")),
|
16
|
-
)
|
17
|
-
|
18
|
-
|
19
|
-
class BaseArticle(SeoMixin, MetaMixin):
|
20
|
-
|
21
|
-
seo_attr = "permalink"
|
22
|
-
|
23
|
-
identifier = models.CharField(max_length=200, unique=True)
|
24
|
-
translations = TranslatedFields(
|
25
|
-
content=models.TextField(default=""),
|
26
|
-
permalink=models.SlugField(max_length=200, blank=False),
|
27
|
-
)
|
8
|
+
class AbstractArticle(AbstractPage):
|
9
|
+
content = models.TextField(default="")
|
28
10
|
author = models.ForeignKey(
|
29
|
-
|
30
|
-
)
|
31
|
-
status = models.CharField(
|
32
|
-
max_length=3,
|
33
|
-
choices=CONTENT_STATUS,
|
34
|
-
default="DRF",
|
11
|
+
dj_settings.AUTH_USER_MODEL, blank=True, null=True, on_delete=models.SET_NULL
|
35
12
|
)
|
36
13
|
highlight_image = models.ForeignKey(
|
37
|
-
"camomilla.Media",
|
14
|
+
"camomilla.Media",
|
15
|
+
blank=True,
|
16
|
+
null=True,
|
17
|
+
on_delete=models.SET_NULL,
|
18
|
+
related_name="%(app_label)s_%(class)s_highlight_images",
|
38
19
|
)
|
39
|
-
|
40
|
-
pubblication_date = models.DateTimeField(null=True, blank=True)
|
41
|
-
tags = models.ManyToManyField("camomilla.Tag", blank=True)
|
42
|
-
categories = models.ManyToManyField("camomilla.Category", blank=True)
|
43
|
-
ordering = models.PositiveIntegerField(default=0, blank=False, null=False)
|
20
|
+
tags = models.ManyToManyField("Tag", blank=True)
|
44
21
|
|
45
22
|
class Meta:
|
46
23
|
abstract = True
|
47
|
-
unique_together = [("permalink", "language_code")]
|
48
24
|
ordering = ["ordering"]
|
49
25
|
|
50
|
-
def save(self, *args, **kwargs):
|
51
|
-
import uuid
|
52
26
|
|
53
|
-
|
54
|
-
|
55
|
-
|
27
|
+
class Article(AbstractArticle):
|
28
|
+
class PageMeta:
|
29
|
+
default_template = settings.ARTICLE_DEFAULT_TEMPLATE
|
30
|
+
inject_context_func = settings.ARTICLE_INJECT_CONTEXT_FUNC
|
31
|
+
|
32
|
+
|
33
|
+
class AbstractTag(models.Model):
|
34
|
+
name = models.CharField(max_length=200, unique=True)
|
35
|
+
|
36
|
+
class Meta:
|
37
|
+
abstract = True
|
56
38
|
|
57
39
|
def __str__(self):
|
58
|
-
return self.
|
40
|
+
return "(%s) %s" % (self.__class__.__name__, self.name)
|
59
41
|
|
60
42
|
|
61
|
-
class
|
62
|
-
|
43
|
+
class Tag(AbstractTag):
|
44
|
+
pass
|
camomilla/models/content.py
CHANGED
@@ -1,20 +1,11 @@
|
|
1
1
|
from django.db import models
|
2
|
-
|
3
2
|
from django.urls import reverse
|
4
|
-
|
5
|
-
from hvad.models import TranslatableModel, TranslatedFields
|
6
|
-
|
7
3
|
from djsuperadmin.mixins import DjSuperAdminMixin
|
8
4
|
|
9
5
|
|
10
|
-
class
|
11
|
-
identifier = models.
|
12
|
-
|
13
|
-
title=models.CharField(max_length=200),
|
14
|
-
subtitle=models.CharField(max_length=200, blank=True, null=True, default=""),
|
15
|
-
permalink=models.CharField(max_length=200, blank=False, null=True),
|
16
|
-
content=models.TextField(default=""),
|
17
|
-
)
|
6
|
+
class AbstractContent(DjSuperAdminMixin, models.Model):
|
7
|
+
identifier = models.TextField()
|
8
|
+
content = models.TextField(default="")
|
18
9
|
page = models.ForeignKey(
|
19
10
|
"camomilla.Page",
|
20
11
|
blank=False,
|
@@ -33,11 +24,13 @@ class BaseContent(DjSuperAdminMixin, TranslatableModel):
|
|
33
24
|
|
34
25
|
class Meta:
|
35
26
|
abstract = True
|
36
|
-
unique_together = [
|
27
|
+
unique_together = ["identifier", "page"]
|
37
28
|
|
38
29
|
def __str__(self):
|
30
|
+
if len(self.identifier) > 40:
|
31
|
+
return "%s..." % self.identifier[:40]
|
39
32
|
return self.identifier
|
40
33
|
|
41
34
|
|
42
|
-
class Content(
|
43
|
-
|
35
|
+
class Content(AbstractContent):
|
36
|
+
pass
|