django-camomilla-cms 6.0.0b2__tar.gz → 6.0.0b4__tar.gz
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.
- {django-camomilla-cms-6.0.0b2/django_camomilla_cms.egg-info → django-camomilla-cms-6.0.0b4}/PKG-INFO +1 -1
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/__init__.py +1 -1
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/apps.py +3 -0
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/contrib/modeltranslation/hvad_migration.py +1 -2
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/contrib/rest_framework/serializer.py +31 -1
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/dynamic_pages_urls.py +7 -1
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/fields/json.py +12 -9
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/management/commands/regenerate_thumbnails.py +0 -1
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/model_api.py +6 -4
- django-camomilla-cms-6.0.0b4/camomilla/models/__init__.py +5 -0
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/models/article.py +0 -1
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/models/media.py +1 -2
- django-camomilla-cms-6.0.0b4/camomilla/models/menu.py +89 -0
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/models/mixins/__init__.py +0 -1
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/models/page.py +66 -32
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/openapi/schema.py +27 -0
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/parsers.py +0 -1
- django-camomilla-cms-6.0.0b4/camomilla/serializers/fields/json.py +24 -0
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/serializers/mixins/__init__.py +16 -2
- django-camomilla-cms-6.0.0b4/camomilla/serializers/page.py +63 -0
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/serializers/user.py +2 -3
- django-camomilla-cms-6.0.0b4/camomilla/serializers/utils.py +35 -0
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/settings.py +21 -1
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/storages/optimize.py +1 -1
- django-camomilla-cms-6.0.0b4/camomilla/structured/__init__.py +120 -0
- django-camomilla-cms-6.0.0b4/camomilla/structured/cache.py +193 -0
- django-camomilla-cms-6.0.0b4/camomilla/structured/fields.py +149 -0
- django-camomilla-cms-6.0.0b4/camomilla/structured/models.py +47 -0
- django-camomilla-cms-6.0.0b4/camomilla/structured/utils.py +114 -0
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/templatetags/camomilla_filters.py +0 -1
- django-camomilla-cms-6.0.0b4/camomilla/theme/__init__.py +1 -0
- {django-camomilla-cms-6.0.0b2/camomilla → django-camomilla-cms-6.0.0b4/camomilla/theme}/admin.py +12 -6
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/theme/apps.py +12 -1
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/translation.py +4 -2
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/urls.py +13 -6
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/utils/__init__.py +1 -1
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/utils/getters.py +11 -1
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/utils/templates.py +2 -2
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/utils/translation.py +9 -6
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/views/__init__.py +1 -1
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/views/articles.py +0 -1
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/views/contents.py +0 -1
- django-camomilla-cms-6.0.0b4/camomilla/views/decorators.py +26 -0
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/views/medias.py +1 -2
- django-camomilla-cms-6.0.0b4/camomilla/views/menus.py +86 -0
- django-camomilla-cms-6.0.0b4/camomilla/views/pages.py +25 -0
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/views/tags.py +0 -1
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/views/users.py +0 -2
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4/django_camomilla_cms.egg-info}/PKG-INFO +1 -1
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/django_camomilla_cms.egg-info/SOURCES.txt +4 -1
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/django_camomilla_cms.egg-info/requires.txt +3 -2
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/setup.cfg +3 -2
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/setup.py +1 -1
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/tests/test_api.py +1 -0
- django-camomilla-cms-6.0.0b2/camomilla/models/__init__.py +0 -5
- django-camomilla-cms-6.0.0b2/camomilla/models/menu.py +0 -87
- django-camomilla-cms-6.0.0b2/camomilla/serializers/fields/json.py +0 -92
- django-camomilla-cms-6.0.0b2/camomilla/serializers/page.py +0 -16
- django-camomilla-cms-6.0.0b2/camomilla/serializers/utils.py +0 -30
- django-camomilla-cms-6.0.0b2/camomilla/structured/__init__.py +0 -105
- django-camomilla-cms-6.0.0b2/camomilla/structured/fields.py +0 -292
- django-camomilla-cms-6.0.0b2/camomilla/structured/models.py +0 -140
- django-camomilla-cms-6.0.0b2/camomilla/theme/__init__.py +0 -1
- django-camomilla-cms-6.0.0b2/camomilla/views/menus.py +0 -42
- django-camomilla-cms-6.0.0b2/camomilla/views/pages.py +0 -13
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/LICENSE +0 -0
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/MANIFEST.in +0 -0
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/README.md +0 -0
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/authentication.py +0 -0
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/context_processors.py +0 -0
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/contrib/__init__.py +0 -0
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/contrib/modeltranslation/__init__.py +0 -0
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/contrib/rest_framework/__init__.py +0 -0
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/defaults.py +0 -0
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/exceptions.py +0 -0
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/fields/__init__.py +0 -0
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/management/__init__.py +0 -0
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/management/commands/__init__.py +0 -0
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/models/content.py +0 -0
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/openapi/__init__.py +0 -0
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/permissions.py +0 -0
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/serializers/__init__.py +0 -0
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/serializers/article.py +0 -0
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/serializers/base/__init__.py +0 -0
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/serializers/content_type.py +0 -0
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/serializers/fields/__init__.py +0 -0
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/serializers/fields/file.py +0 -0
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/serializers/fields/related.py +0 -0
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/serializers/media.py +0 -0
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/serializers/menu.py +0 -0
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/serializers/validators.py +0 -0
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/storages/__init__.py +0 -0
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/storages/overwrite.py +0 -0
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/templates/admin/camomilla/page/change_form.html +0 -0
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/templates/defaults/articles/default.html +0 -0
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/templates/defaults/base.html +0 -0
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/templates/defaults/pages/default.html +0 -0
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/templates/defaults/parts/langswitch.html +0 -0
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/templates/defaults/parts/menu.html +0 -0
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/templates/defaults/widgets/media_select_multiple.html +0 -0
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/templatetags/__init__.py +0 -0
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/templatetags/menus.py +0 -0
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/theme/static/admin/css/responsive.css +0 -0
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/theme/static/admin/img/favicon.ico +0 -0
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/theme/static/admin/img/logo.svg +0 -0
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/theme/templates/admin/base.html +0 -0
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/theme/templates/rosetta/base.html +0 -0
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/utils/normalization.py +0 -0
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/utils/seo.py +0 -0
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/views/base/__init__.py +0 -0
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/views/languages.py +0 -0
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/views/mixins/__init__.py +0 -0
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/views/mixins/ordering.py +0 -0
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/views/mixins/pagination.py +0 -0
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/django_camomilla_cms.egg-info/dependency_links.txt +0 -0
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/django_camomilla_cms.egg-info/top_level.txt +0 -0
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/tests/__init__.py +0 -0
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/tests/test_camomilla_filters.py +0 -0
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/tests/test_models.py +0 -0
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/tests/test_utils.py +0 -0
- {django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/tests/urls.py +0 -0
@@ -3,6 +3,8 @@ from __future__ import unicode_literals
|
|
3
3
|
from django.apps import AppConfig
|
4
4
|
from django.conf import settings
|
5
5
|
|
6
|
+
from camomilla.context.autodiscover import autodiscover_context_files
|
7
|
+
|
6
8
|
|
7
9
|
class CamomillaConfig(AppConfig):
|
8
10
|
default_auto_field = "django.db.models.AutoField"
|
@@ -13,3 +15,4 @@ class CamomillaConfig(AppConfig):
|
|
13
15
|
if "camomilla" not in migration_modules:
|
14
16
|
migration_modules["camomilla"] = "camomilla_migrations"
|
15
17
|
setattr(settings, "MIGRATION_MODULES", migration_modules)
|
18
|
+
autodiscover_context_files()
|
@@ -3,7 +3,6 @@ from django.db import migrations, connection
|
|
3
3
|
|
4
4
|
|
5
5
|
class KeepTranslationsMixin:
|
6
|
-
|
7
6
|
_saved_data_from_plain = {}
|
8
7
|
language_codes = dict(getattr(settings, "LANGUAGES", {})).keys()
|
9
8
|
|
@@ -30,7 +29,7 @@ class KeepTranslationsMixin:
|
|
30
29
|
for modelPath, fields in self.keep_translations.items():
|
31
30
|
Model = apps.get_model(*modelPath.split("."))
|
32
31
|
table = Model._meta.db_table + "_translation"
|
33
|
-
if
|
32
|
+
if "language_code" not in fields:
|
34
33
|
fields = ("language_code",) + fields
|
35
34
|
with connection.cursor() as cursor:
|
36
35
|
cursor.execute("SELECT master_id FROM {0};".format(table))
|
@@ -52,7 +52,7 @@ class TranslationsMixin(serializers.ModelSerializer):
|
|
52
52
|
@property
|
53
53
|
def _writable_fields(self):
|
54
54
|
for field in super()._writable_fields:
|
55
|
-
if
|
55
|
+
if field.field_name not in self.translation_fields:
|
56
56
|
yield field
|
57
57
|
|
58
58
|
def to_internal_value(self, data):
|
@@ -77,3 +77,33 @@ class TranslationsMixin(serializers.ModelSerializer):
|
|
77
77
|
@property
|
78
78
|
def is_translatable(self):
|
79
79
|
return is_translatable(pointed_getter(self, "Meta.model"))
|
80
|
+
|
81
|
+
|
82
|
+
class RemoveTranslationsMixin(serializers.ModelSerializer):
|
83
|
+
@cached_property
|
84
|
+
def translation_fields(self):
|
85
|
+
try:
|
86
|
+
return translator.get_options_for_model(self.Meta.model).get_field_names()
|
87
|
+
except NotRegistered:
|
88
|
+
return []
|
89
|
+
|
90
|
+
def get_default_field_names(self, declared_fields, model_info):
|
91
|
+
request = self.context.get("request", False)
|
92
|
+
included_translations = request and request.GET.get(
|
93
|
+
"included_translations", False
|
94
|
+
)
|
95
|
+
if included_translations == "all":
|
96
|
+
return super().get_default_field_names(declared_fields, model_info)
|
97
|
+
elif included_translations is not False:
|
98
|
+
included_translations = included_translations.split(",")
|
99
|
+
else:
|
100
|
+
included_translations = []
|
101
|
+
|
102
|
+
field_names = super().get_default_field_names(declared_fields, model_info)
|
103
|
+
for lang in mt_settings.AVAILABLE_LANGUAGES:
|
104
|
+
if lang not in included_translations:
|
105
|
+
for field in self.translation_fields:
|
106
|
+
localized_fieldname = build_localized_fieldname(field, lang)
|
107
|
+
if localized_fieldname in field_names:
|
108
|
+
field_names.remove(localized_fieldname)
|
109
|
+
return field_names
|
{django-camomilla-cms-6.0.0b2 → django-camomilla-cms-6.0.0b4}/camomilla/dynamic_pages_urls.py
RENAMED
@@ -1,13 +1,19 @@
|
|
1
1
|
from django.shortcuts import render
|
2
2
|
from django.urls import path
|
3
3
|
|
4
|
+
from camomilla import settings
|
5
|
+
|
4
6
|
from .models import Page
|
5
7
|
|
6
8
|
|
7
9
|
def fetch(request, *args, **kwargs):
|
8
10
|
preview = request.user.is_staff and request.GET.get("preview", False)
|
9
11
|
if "permalink" in kwargs:
|
10
|
-
page = Page.get_or_404(
|
12
|
+
page = Page.get_or_404(
|
13
|
+
request, bypass_public_check=preview, bypass_type_check=True
|
14
|
+
)
|
15
|
+
elif settings.AUTO_CREATE_HOMEPAGE is False:
|
16
|
+
page, _ = Page.get_or_404(permalink="/", bypass_type_check=True)
|
11
17
|
else:
|
12
18
|
page, _ = Page.get_or_create_homepage()
|
13
19
|
return render(request, page.get_template_path(request), page.get_context(request))
|
@@ -12,15 +12,7 @@ from django.contrib.postgres.fields import ArrayField as DjangoArrayField
|
|
12
12
|
from django.db import models
|
13
13
|
|
14
14
|
|
15
|
-
|
16
|
-
pass
|
17
|
-
|
18
|
-
|
19
|
-
class ArrayField(DjangoArrayField):
|
20
|
-
pass
|
21
|
-
|
22
|
-
|
23
|
-
if "sqlite" in settings.DATABASES["default"]["ENGINE"]:
|
15
|
+
if "sqlite" in settings.DATABASES["default"]["ENGINE"]: # noqa: C901
|
24
16
|
|
25
17
|
class JSONField(models.Field):
|
26
18
|
def db_type(self, connection):
|
@@ -64,3 +56,14 @@ if "sqlite" in settings.DATABASES["default"]["ENGINE"]:
|
|
64
56
|
}
|
65
57
|
)
|
66
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]
|
@@ -13,7 +13,7 @@ def register(
|
|
13
13
|
base_viewset=BaseModelViewset,
|
14
14
|
serializer_meta={},
|
15
15
|
viewset_attrs={},
|
16
|
-
filters
|
16
|
+
filters=None,
|
17
17
|
):
|
18
18
|
"""
|
19
19
|
Register a model to the API.
|
@@ -30,8 +30,8 @@ def register(
|
|
30
30
|
"model": model,
|
31
31
|
"fields": "__all__",
|
32
32
|
}
|
33
|
-
if
|
34
|
-
base_meta.pop(
|
33
|
+
if "exclude" in serializer_meta:
|
34
|
+
base_meta.pop("fields")
|
35
35
|
serializer = type(
|
36
36
|
f"{model.__name__}Serializer",
|
37
37
|
(base_serializer,),
|
@@ -51,7 +51,9 @@ def register(
|
|
51
51
|
f"{model.__name__}ViewSet",
|
52
52
|
(base_viewset,),
|
53
53
|
{
|
54
|
-
"get_queryset": lambda self: model.objects.all()
|
54
|
+
"get_queryset": lambda self: model.objects.all()
|
55
|
+
if filters is None
|
56
|
+
else model.objects.filter(**filters),
|
55
57
|
"serializer_class": serializer,
|
56
58
|
**viewset_attrs,
|
57
59
|
},
|
@@ -5,7 +5,6 @@ from io import BytesIO
|
|
5
5
|
import magic
|
6
6
|
from django.core.exceptions import ValidationError
|
7
7
|
from django.core.files.base import ContentFile
|
8
|
-
from django.core.files.storage import default_storage as storage
|
9
8
|
from django.db import models
|
10
9
|
from django.db.models.fields.related import ForeignObjectRel
|
11
10
|
from django.db.models.signals import post_save, pre_delete
|
@@ -181,7 +180,7 @@ class Media(models.Model):
|
|
181
180
|
def _get_file_size(self):
|
182
181
|
try:
|
183
182
|
return self.file.storage.size(self.file.name)
|
184
|
-
except:
|
183
|
+
except Exception:
|
185
184
|
return 0
|
186
185
|
|
187
186
|
def __str__(self):
|
@@ -0,0 +1,89 @@
|
|
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 camomilla import structured
|
16
|
+
from camomilla.models.page import UrlNode
|
17
|
+
from typing import Optional, Union, Callable
|
18
|
+
|
19
|
+
|
20
|
+
class LinkTypes(str, Enum):
|
21
|
+
relational = "RE"
|
22
|
+
static = "ST"
|
23
|
+
|
24
|
+
|
25
|
+
class MenuNodeLink(structured.BaseModel):
|
26
|
+
link_type: LinkTypes = LinkTypes.static
|
27
|
+
static: str = None
|
28
|
+
content_type: int = None
|
29
|
+
page_id: int = None
|
30
|
+
url_node: UrlNode = None
|
31
|
+
|
32
|
+
@model_serializer(mode="wrap", when_used="json")
|
33
|
+
def update_relational(self, handler: Callable, info: SerializationInfo):
|
34
|
+
if self.link_type == LinkTypes.relational:
|
35
|
+
if self.content_type and self.page_id:
|
36
|
+
c_type = ContentType.objects.filter(pk=self.content_type).first()
|
37
|
+
model = c_type and c_type.model_class()
|
38
|
+
page = model and model.objects.filter(pk=self.page_id).first()
|
39
|
+
self.url_node = page and page.url_node
|
40
|
+
return handler(self)
|
41
|
+
|
42
|
+
def get_url(self, request=None):
|
43
|
+
if self.link_type == LinkTypes.relational:
|
44
|
+
return isinstance(self.url_node, UrlNode) and self.url_node.routerlink
|
45
|
+
elif self.link_type == LinkTypes.static:
|
46
|
+
return self.static
|
47
|
+
|
48
|
+
@computed_field
|
49
|
+
@property
|
50
|
+
def url(self) -> Optional[str]:
|
51
|
+
return self.get_url()
|
52
|
+
|
53
|
+
|
54
|
+
class MenuNode(structured.BaseModel):
|
55
|
+
id: str = Field(default_factory=uuid4)
|
56
|
+
meta: dict = {}
|
57
|
+
nodes: list["MenuNode"] = []
|
58
|
+
title: str = ""
|
59
|
+
link: MenuNodeLink
|
60
|
+
|
61
|
+
|
62
|
+
class Menu(models.Model):
|
63
|
+
key = models.CharField(max_length=200, unique=True, editable=False)
|
64
|
+
available_classes = models.JSONField(default=dict, editable=False)
|
65
|
+
enabled = models.BooleanField(default=True)
|
66
|
+
nodes = structured.StructuredJSONField(default=list, schema=MenuNode)
|
67
|
+
|
68
|
+
class Meta:
|
69
|
+
verbose_name = _("menu")
|
70
|
+
verbose_name_plural = _("menus")
|
71
|
+
|
72
|
+
def render(
|
73
|
+
self,
|
74
|
+
template_path: str,
|
75
|
+
request=None,
|
76
|
+
context: Union[dict, RequestContext] = {},
|
77
|
+
):
|
78
|
+
if isinstance(context, RequestContext):
|
79
|
+
context = context.flatten()
|
80
|
+
context.update({"menu": self})
|
81
|
+
return mark_safe(render_to_string(template_path, context, request))
|
82
|
+
|
83
|
+
class defaultdict(dict):
|
84
|
+
def __missing__(self, key):
|
85
|
+
dict.__setitem__(self, key, Menu.objects.get_or_create(key=key)[0])
|
86
|
+
return self[key]
|
87
|
+
|
88
|
+
def __str__(self) -> str:
|
89
|
+
return self.key
|
@@ -1,4 +1,4 @@
|
|
1
|
-
from typing import
|
1
|
+
from typing import Sequence, Tuple
|
2
2
|
from uuid import uuid4
|
3
3
|
|
4
4
|
from django.core.exceptions import ObjectDoesNotExist
|
@@ -20,13 +20,15 @@ from camomilla.utils import (
|
|
20
20
|
lang_fallback_query,
|
21
21
|
set_nofallbacks,
|
22
22
|
url_lang_decompose,
|
23
|
-
get_all_templates_files
|
23
|
+
get_all_templates_files,
|
24
24
|
)
|
25
25
|
from camomilla.utils.getters import pointed_getter
|
26
26
|
from camomilla import settings
|
27
|
+
from camomilla.context.rendering import ctx_registry
|
28
|
+
|
27
29
|
|
28
30
|
def GET_TEMPLATE_CHOICES():
|
29
|
-
return [(t,t)for t in get_all_templates_files()]
|
31
|
+
return [(t, t) for t in get_all_templates_files()]
|
30
32
|
|
31
33
|
|
32
34
|
class UrlNodeManager(models.Manager):
|
@@ -42,7 +44,7 @@ class UrlNodeManager(models.Manager):
|
|
42
44
|
def _annotate_fields(
|
43
45
|
self,
|
44
46
|
qs: models.QuerySet,
|
45
|
-
field_names:
|
47
|
+
field_names: Sequence[Tuple[str, models.Field, models.Value]],
|
46
48
|
):
|
47
49
|
for field_name, output_field, default in field_names:
|
48
50
|
whens = [
|
@@ -66,9 +68,21 @@ class UrlNodeManager(models.Manager):
|
|
66
68
|
return self._annotate_fields(
|
67
69
|
super().get_queryset(),
|
68
70
|
[
|
69
|
-
(
|
70
|
-
|
71
|
-
|
71
|
+
(
|
72
|
+
"indexable",
|
73
|
+
models.BooleanField(),
|
74
|
+
models.Value(None, models.BooleanField()),
|
75
|
+
),
|
76
|
+
(
|
77
|
+
"status",
|
78
|
+
models.BooleanField(),
|
79
|
+
models.Value(None, models.BooleanField()),
|
80
|
+
),
|
81
|
+
(
|
82
|
+
"pubblication_date",
|
83
|
+
models.DateTimeField(),
|
84
|
+
models.Value(timezone.now(), models.DateTimeField()),
|
85
|
+
),
|
72
86
|
],
|
73
87
|
)
|
74
88
|
except (ProgrammingError, OperationalError):
|
@@ -83,15 +97,19 @@ class UrlNode(models.Model):
|
|
83
97
|
@property
|
84
98
|
def page(self) -> "AbstractPage":
|
85
99
|
return getattr(self, self.related_name)
|
86
|
-
|
87
|
-
@
|
88
|
-
def
|
100
|
+
|
101
|
+
@staticmethod
|
102
|
+
def reverse_url(permalink: str) -> str:
|
89
103
|
try:
|
90
|
-
if
|
91
|
-
return reverse(
|
92
|
-
return reverse(
|
104
|
+
if permalink == "/":
|
105
|
+
return reverse("camomilla-homepage")
|
106
|
+
return reverse("camomilla-permalink", args=(permalink.lstrip("/"),))
|
93
107
|
except NoReverseMatch:
|
94
|
-
return
|
108
|
+
return None
|
109
|
+
|
110
|
+
@property
|
111
|
+
def routerlink(self) -> str:
|
112
|
+
return self.reverse_url(self.permalink) or self.permalink
|
95
113
|
|
96
114
|
|
97
115
|
PAGE_CHILD_RELATED_NAME = "%(app_label)s_%(class)s_child_pages"
|
@@ -123,7 +141,11 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
|
|
123
141
|
date_created = models.DateTimeField(auto_now_add=True)
|
124
142
|
date_updated_at = models.DateTimeField(auto_now=True)
|
125
143
|
url_node = models.OneToOneField(
|
126
|
-
UrlNode,
|
144
|
+
UrlNode,
|
145
|
+
on_delete=models.CASCADE,
|
146
|
+
related_name=URL_NODE_RELATED_NAME,
|
147
|
+
null=True,
|
148
|
+
editable=False,
|
127
149
|
)
|
128
150
|
breadcrumbs_title = models.CharField(max_length=128, null=True, blank=True)
|
129
151
|
slug = models.SlugField(max_length=150, allow_unicode=True, null=True, blank=True)
|
@@ -145,16 +167,16 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
|
|
145
167
|
blank=True,
|
146
168
|
on_delete=models.CASCADE,
|
147
169
|
)
|
148
|
-
|
170
|
+
|
149
171
|
def __init__(self, *args, **kwargs):
|
150
172
|
super(AbstractPage, self).__init__(*args, **kwargs)
|
151
|
-
self._meta.get_field(
|
173
|
+
self._meta.get_field("template").choices = lazy(GET_TEMPLATE_CHOICES, list)()
|
152
174
|
|
153
175
|
def __str__(self) -> str:
|
154
176
|
return "(%s) %s" % (self.__class__.__name__, self.title or self.permalink)
|
155
177
|
|
156
178
|
def get_context(self, request=None):
|
157
|
-
context={
|
179
|
+
context = {
|
158
180
|
"page": self,
|
159
181
|
"page_model": {"class": self.__class__.__name__, "module": self.__module__},
|
160
182
|
"request": request,
|
@@ -164,7 +186,7 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
|
|
164
186
|
new_ctx = inject_func(request=request, super_ctx=context)
|
165
187
|
if isinstance(new_ctx, dict):
|
166
188
|
context.update(new_ctx)
|
167
|
-
return context
|
189
|
+
return ctx_registry.get_context_for_page(self, request, super_ctx=context)
|
168
190
|
|
169
191
|
@property
|
170
192
|
def model_name(self) -> str:
|
@@ -177,13 +199,13 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
|
|
177
199
|
@property
|
178
200
|
def permalink(self) -> str:
|
179
201
|
return self.url_node and self.url_node.permalink
|
180
|
-
|
202
|
+
|
181
203
|
@property
|
182
204
|
def routerlink(self) -> str:
|
183
205
|
return self.url_node and self.url_node.routerlink
|
184
|
-
|
206
|
+
|
185
207
|
@property
|
186
|
-
def breadcrumbs(self) ->
|
208
|
+
def breadcrumbs(self) -> Sequence[dict]:
|
187
209
|
breadcrumb = {
|
188
210
|
"permalink": self.permalink,
|
189
211
|
"title": self.breadcrumbs_title or self.title or self.slug,
|
@@ -197,7 +219,9 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
|
|
197
219
|
if self.status == "PUB":
|
198
220
|
return True
|
199
221
|
if self.status == "PLA":
|
200
|
-
return
|
222
|
+
return (
|
223
|
+
bool(self.pubblication_date) and timezone.now() > self.pubblication_date
|
224
|
+
)
|
201
225
|
return False
|
202
226
|
|
203
227
|
def get_template_path(self, request=None) -> str:
|
@@ -222,7 +246,7 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
|
|
222
246
|
|
223
247
|
def _update_url_node(self, force: bool = False) -> UrlNode:
|
224
248
|
self.url_node = self._get_or_create_url_node()
|
225
|
-
for
|
249
|
+
for __ in activate_languages():
|
226
250
|
old_permalink = self.permalink
|
227
251
|
new_permalink = self.generate_permalink()
|
228
252
|
force = force or old_permalink != new_permalink
|
@@ -232,7 +256,7 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
|
|
232
256
|
self.update_childs()
|
233
257
|
return self.url_node
|
234
258
|
|
235
|
-
def generate_permalink(self, safe:bool=True) -> str:
|
259
|
+
def generate_permalink(self, safe: bool = True) -> str:
|
236
260
|
slug = get_nofallbacks(self, "slug")
|
237
261
|
if slug is None and not self.permalink:
|
238
262
|
translations = get_field_translations(self, "slug").values()
|
@@ -277,14 +301,20 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
|
|
277
301
|
).first()
|
278
302
|
page = node and node.page
|
279
303
|
type_error = not bypass_type_check and not isinstance(page, cls)
|
280
|
-
public_error = not bypass_public_check and not getattr(
|
304
|
+
public_error = not bypass_public_check and not getattr(
|
305
|
+
page or object, "is_public", False
|
306
|
+
)
|
281
307
|
if not page or type_error or public_error:
|
282
308
|
bases = (UrlNode.DoesNotExist,)
|
283
309
|
if hasattr(cls, "DoesNotExist"):
|
284
310
|
bases += (cls.DoesNotExist,)
|
285
|
-
|
286
|
-
|
287
|
-
|
311
|
+
message = "%s matching query does not exist." % cls._meta.object_name
|
312
|
+
if public_error:
|
313
|
+
message = (
|
314
|
+
"Match found: %s.\nThe page appears not to be public.\nUse ?preview=true in the url to see it."
|
315
|
+
% page
|
316
|
+
)
|
317
|
+
raise type("PageDoesNotExist", bases, {})(message)
|
288
318
|
return page
|
289
319
|
|
290
320
|
@classmethod
|
@@ -311,11 +341,15 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
|
|
311
341
|
def get_or_404(cls, request, *args, **kwargs) -> "AbstractPage":
|
312
342
|
try:
|
313
343
|
return cls.get(request, *args, **kwargs)
|
314
|
-
except ObjectDoesNotExist:
|
315
|
-
raise Http404(
|
344
|
+
except ObjectDoesNotExist as ex:
|
345
|
+
raise Http404(ex)
|
316
346
|
|
317
347
|
def alternate_urls(self, *args, **kwargs) -> dict:
|
318
|
-
|
348
|
+
permalinks = get_field_translations(self.url_node or object, "permalink", None)
|
349
|
+
for lang in activate_languages():
|
350
|
+
if lang in permalinks:
|
351
|
+
permalinks[lang] = UrlNode.reverse_url(permalinks[lang])
|
352
|
+
return permalinks
|
319
353
|
|
320
354
|
class Meta:
|
321
355
|
abstract = True
|
@@ -7,9 +7,13 @@ from camomilla.contrib.rest_framework.serializer import (
|
|
7
7
|
plain_to_nest,
|
8
8
|
TRANS_ACCESSOR,
|
9
9
|
)
|
10
|
+
from camomilla.serializers.fields.json import StructuredJSONField
|
11
|
+
from camomilla.utils.getters import find_and_replace_dict, pointed_getter
|
10
12
|
|
11
13
|
|
12
14
|
class AutoSchema(DRFAutoSchema):
|
15
|
+
extra_components = {}
|
16
|
+
|
13
17
|
def map_serializer(self, serializer):
|
14
18
|
schema = super(AutoSchema, self).map_serializer(serializer)
|
15
19
|
if isinstance(serializer, TranslationsMixin) and serializer.is_translatable:
|
@@ -23,6 +27,29 @@ class AutoSchema(DRFAutoSchema):
|
|
23
27
|
}
|
24
28
|
return schema
|
25
29
|
|
30
|
+
def get_components(self, path, method):
|
31
|
+
components = super().get_components(path, method)
|
32
|
+
if len(self.extra_components.keys()) > 0:
|
33
|
+
components = {**(components or {}), **self.extra_components}
|
34
|
+
return components
|
35
|
+
|
36
|
+
def map_field(self, field):
|
37
|
+
if isinstance(field, StructuredJSONField):
|
38
|
+
|
39
|
+
def replace(key, value):
|
40
|
+
if isinstance(value, str) and value.startswith("#/definitions"):
|
41
|
+
return value.replace("#/definitions", "#/components/schemas")
|
42
|
+
return value
|
43
|
+
|
44
|
+
self.extra_components.update(
|
45
|
+
**find_and_replace_dict(
|
46
|
+
field.json_schema.pop("definitions", {}), replace
|
47
|
+
)
|
48
|
+
)
|
49
|
+
|
50
|
+
return find_and_replace_dict(field.json_schema, replace)
|
51
|
+
return super().map_field(field)
|
52
|
+
|
26
53
|
|
27
54
|
class SchemaGenerator(DRFSchemaGenerator):
|
28
55
|
def create_view(self, callback, method, request=None):
|
@@ -0,0 +1,24 @@
|
|
1
|
+
from rest_framework import serializers
|
2
|
+
from rest_framework.utils import model_meta
|
3
|
+
|
4
|
+
|
5
|
+
class StructuredJSONField(serializers.JSONField):
|
6
|
+
def __init__(self, **kwargs):
|
7
|
+
self.schema = kwargs.pop("schema", None)
|
8
|
+
super().__init__(**kwargs)
|
9
|
+
|
10
|
+
def bind(self, field_name, parent):
|
11
|
+
if self.schema is None and isinstance(parent, serializers.ModelSerializer):
|
12
|
+
info = model_meta.get_field_info(parent.Meta.model)
|
13
|
+
field = info.fields[field_name]
|
14
|
+
self.schema = field.schema
|
15
|
+
self.many = field.many
|
16
|
+
self.json_schema = field.schema.json_schema()
|
17
|
+
super().bind(field_name, parent)
|
18
|
+
|
19
|
+
def to_representation(self, instance):
|
20
|
+
if isinstance(instance, list) and self.many:
|
21
|
+
return super().to_representation(
|
22
|
+
self.schema.dump_python(instance, exclude_unset=True)
|
23
|
+
)
|
24
|
+
return super().to_representation(instance.model_dump(exclude_unset=True))
|
@@ -20,6 +20,9 @@ if django.VERSION >= (4, 0):
|
|
20
20
|
else:
|
21
21
|
from django.contrib.postgres.fields import JSONField as DjangoJSONField
|
22
22
|
|
23
|
+
from typing import TYPE_CHECKING
|
24
|
+
if TYPE_CHECKING:
|
25
|
+
from camomilla.models.page import AbstractPage
|
23
26
|
|
24
27
|
# TODO: decide what to do with LangInfoMixin mixin!
|
25
28
|
class LangInfoMixin(metaclass=serializers.SerializerMetaclass):
|
@@ -121,8 +124,17 @@ class NestMixin:
|
|
121
124
|
return field_class, field_kwargs
|
122
125
|
|
123
126
|
|
124
|
-
|
125
127
|
class AbstractPageMixin(serializers.ModelSerializer):
|
128
|
+
breadcrumbs = serializers.SerializerMethodField()
|
129
|
+
routerlink = serializers.CharField(read_only=True)
|
130
|
+
template = serializers.SerializerMethodField()
|
131
|
+
|
132
|
+
def get_template(self, instance: 'AbstractPage'):
|
133
|
+
return instance.get_template_path()
|
134
|
+
|
135
|
+
def get_breadcrumbs(self, instance: 'AbstractPage'):
|
136
|
+
return instance.breadcrumbs
|
137
|
+
|
126
138
|
LANG_PERMALINK_FIELDS = [
|
127
139
|
build_localized_fieldname("permalink", lang)
|
128
140
|
for lang in AVAILABLE_LANGUAGES
|
@@ -134,6 +146,9 @@ class AbstractPageMixin(serializers.ModelSerializer):
|
|
134
146
|
return super().translation_fields + ["permalink"]
|
135
147
|
|
136
148
|
def get_default_field_names(self, *args):
|
149
|
+
from camomilla.contrib.rest_framework.serializer import RemoveTranslationsMixin
|
150
|
+
if RemoveTranslationsMixin in self.__class__.__bases__: # noqa: E501
|
151
|
+
return super().get_default_field_names(*args)
|
137
152
|
return (
|
138
153
|
[f for f in super().get_default_field_names(*args) if f != "url_node"]
|
139
154
|
+ self.LANG_PERMALINK_FIELDS
|
@@ -150,4 +165,3 @@ class AbstractPageMixin(serializers.ModelSerializer):
|
|
150
165
|
|
151
166
|
def get_validators(self):
|
152
167
|
return super().get_validators() + [UniquePermalinkValidator()]
|
153
|
-
|