django-camomilla-cms 6.0.0b16__py2.py3-none-any.whl → 6.0.0b18__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 +1 -1
- camomilla/contrib/modeltranslation/hvad_migration.py +9 -9
- camomilla/dynamic_pages_urls.py +6 -2
- camomilla/managers/pages.py +87 -2
- camomilla/model_api.py +6 -4
- camomilla/models/menu.py +9 -4
- camomilla/models/page.py +178 -117
- camomilla/openapi/schema.py +15 -10
- camomilla/redirects.py +10 -0
- camomilla/serializers/base/__init__.py +4 -4
- camomilla/serializers/fields/__init__.py +5 -17
- camomilla/serializers/fields/related.py +5 -3
- camomilla/serializers/mixins/__init__.py +23 -240
- camomilla/serializers/mixins/fields.py +20 -0
- camomilla/serializers/mixins/filter_fields.py +9 -8
- 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/{contrib/rest_framework/serializer.py → serializers/mixins/translation.py} +16 -56
- camomilla/serializers/utils.py +3 -3
- camomilla/serializers/validators.py +6 -2
- camomilla/settings.py +16 -2
- camomilla/storages/default.py +7 -1
- camomilla/templates/defaults/base.html +60 -4
- camomilla/templates/defaults/parts/menu.html +1 -1
- camomilla/templatetags/menus.py +3 -0
- camomilla/templatetags/model_extras.py +73 -0
- camomilla/theme/__init__.py +1 -1
- camomilla/theme/{admin.py → admin/__init__.py} +22 -20
- camomilla/theme/admin/pages.py +46 -0
- camomilla/theme/admin/translations.py +13 -0
- camomilla/theme/apps.py +2 -5
- camomilla/translation.py +7 -1
- camomilla/urls.py +2 -5
- camomilla/utils/query_parser.py +42 -23
- camomilla/utils/templates.py +23 -10
- camomilla/utils/translation.py +47 -5
- camomilla/views/base/__init__.py +35 -5
- camomilla/views/medias.py +1 -1
- camomilla/views/mixins/__init__.py +17 -76
- 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/pagination.py +11 -8
- camomilla/views/mixins/permissions.py +6 -0
- camomilla/views/pages.py +12 -2
- {django_camomilla_cms-6.0.0b16.dist-info → django_camomilla_cms-6.0.0b18.dist-info}/METADATA +23 -16
- {django_camomilla_cms-6.0.0b16.dist-info → django_camomilla_cms-6.0.0b18.dist-info}/RECORD +63 -45
- {django_camomilla_cms-6.0.0b16.dist-info → django_camomilla_cms-6.0.0b18.dist-info}/WHEEL +1 -1
- tests/test_camomilla_filters.py +1 -1
- tests/test_media.py +98 -65
- tests/test_menu.py +97 -0
- tests/test_model_api_register.py +393 -0
- tests/test_pages.py +343 -0
- tests/test_query_parser.py +1 -2
- tests/test_templates_context.py +111 -0
- tests/utils/api.py +0 -1
- tests/utils/media.py +10 -0
- camomilla/contrib/rest_framework/__init__.py +0 -0
- camomilla/serializers/fields/json.py +0 -48
- {django_camomilla_cms-6.0.0b16.dist-info → django_camomilla_cms-6.0.0b18.dist-info/licenses}/LICENSE +0 -0
- {django_camomilla_cms-6.0.0b16.dist-info → django_camomilla_cms-6.0.0b18.dist-info}/top_level.txt +0 -0
@@ -1,7 +1,5 @@
|
|
1
1
|
from functools import cached_property
|
2
|
-
from typing import Iterable,
|
3
|
-
from django.http import QueryDict
|
4
|
-
|
2
|
+
from typing import Iterable, List
|
5
3
|
from modeltranslation import settings as mt_settings
|
6
4
|
from modeltranslation.translator import NotRegistered, translator
|
7
5
|
from modeltranslation.utils import build_localized_fieldname
|
@@ -9,50 +7,8 @@ from rest_framework import serializers
|
|
9
7
|
from rest_framework.exceptions import ValidationError
|
10
8
|
from camomilla.utils.getters import pointed_getter
|
11
9
|
from camomilla.utils.translation import is_translatable
|
12
|
-
|
13
|
-
|
14
|
-
TRANS_ACCESSOR = "translations"
|
15
|
-
|
16
|
-
|
17
|
-
def plain_to_nest(data, fields, accessor=TRANS_ACCESSOR):
|
18
|
-
"""
|
19
|
-
This function transforms a plain dictionary with translations fields (es. {"title_en": "Hello"})
|
20
|
-
into a dictionary with nested translations fields (es. {"translations": {"en": {"title": "Hello"}}}).
|
21
|
-
"""
|
22
|
-
trans_data = {}
|
23
|
-
for lang in mt_settings.AVAILABLE_LANGUAGES:
|
24
|
-
lang_data = {}
|
25
|
-
for field in fields:
|
26
|
-
trans_field_name = build_localized_fieldname(field, lang)
|
27
|
-
if trans_field_name in data:
|
28
|
-
lang_data[field] = data.pop(trans_field_name)
|
29
|
-
if lang_data.keys():
|
30
|
-
trans_data[lang] = lang_data
|
31
|
-
if trans_data.keys():
|
32
|
-
data[accessor] = trans_data
|
33
|
-
return data
|
34
|
-
|
35
|
-
|
36
|
-
def nest_to_plain(data: Union[dict, QueryDict], fields: List[str], accessor=TRANS_ACCESSOR):
|
37
|
-
"""
|
38
|
-
This function is the inverse of plain_to_nest.
|
39
|
-
It transforms a dictionary with nested translations fields (es. {"translations": {"en": {"title": "Hello"}}})
|
40
|
-
into a plain dictionary with translations fields (es. {"title_en": "Hello"}).
|
41
|
-
"""
|
42
|
-
if isinstance(data, QueryDict):
|
43
|
-
data = data.dict()
|
44
|
-
translations = data.pop(accessor, {})
|
45
|
-
for lang in mt_settings.AVAILABLE_LANGUAGES:
|
46
|
-
nest_trans = translations.pop(lang, {})
|
47
|
-
for k in fields:
|
48
|
-
data.pop(k, None) # this removes all trans field without lang
|
49
|
-
if k in nest_trans:
|
50
|
-
# this saves on the default field the default language value
|
51
|
-
if lang == mt_settings.DEFAULT_LANGUAGE:
|
52
|
-
data[k] = nest_trans[k]
|
53
|
-
key = build_localized_fieldname(k, lang)
|
54
|
-
data[key] = data.get(key, nest_trans[k])
|
55
|
-
return data
|
10
|
+
from camomilla.utils.translation import nest_to_plain, plain_to_nest
|
11
|
+
from camomilla.settings import API_TRANSLATION_ACCESSOR
|
56
12
|
|
57
13
|
|
58
14
|
class TranslationsMixin(serializers.ModelSerializer):
|
@@ -67,6 +23,16 @@ class TranslationsMixin(serializers.ModelSerializer):
|
|
67
23
|
`{"translations": {"en": {"title": "Hello"}, "it": {"title": "Ciao"}}` -> `{"title_en": "Hello", "title_it": "Ciao"}`
|
68
24
|
"""
|
69
25
|
|
26
|
+
def _transform_input(self, data):
|
27
|
+
return nest_to_plain(
|
28
|
+
data, self.translation_fields or [], API_TRANSLATION_ACCESSOR
|
29
|
+
)
|
30
|
+
|
31
|
+
def _transform_output(self, data):
|
32
|
+
return plain_to_nest(
|
33
|
+
data, self.translation_fields or [], API_TRANSLATION_ACCESSOR
|
34
|
+
)
|
35
|
+
|
70
36
|
@cached_property
|
71
37
|
def translation_fields(self) -> List[str]:
|
72
38
|
try:
|
@@ -81,22 +47,16 @@ class TranslationsMixin(serializers.ModelSerializer):
|
|
81
47
|
yield field
|
82
48
|
|
83
49
|
def to_internal_value(self, data):
|
84
|
-
|
85
|
-
nest_to_plain(data, self.translation_fields)
|
86
|
-
return super().to_internal_value(data)
|
50
|
+
return super().to_internal_value(self._transform_input(data))
|
87
51
|
|
88
52
|
def to_representation(self, instance):
|
89
|
-
|
90
|
-
if self.translation_fields:
|
91
|
-
plain_to_nest(representation, self.translation_fields)
|
92
|
-
return representation
|
53
|
+
return self._transform_output(super().to_representation(instance))
|
93
54
|
|
94
55
|
def run_validation(self, *args, **kwargs):
|
95
56
|
try:
|
96
57
|
return super().run_validation(*args, **kwargs)
|
97
58
|
except ValidationError as ex:
|
98
|
-
|
99
|
-
plain_to_nest(ex.detail, self.translation_fields)
|
59
|
+
ex.detail.update(self._transform_input(ex.detail))
|
100
60
|
raise ValidationError(detail=ex.detail)
|
101
61
|
|
102
62
|
@property
|
camomilla/serializers/utils.py
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
def get_standard_bases() -> tuple:
|
2
2
|
from rest_framework.serializers import ModelSerializer
|
3
|
-
from camomilla.serializers.fields import FieldsOverrideMixin
|
4
|
-
from camomilla.serializers.mixins.filter_fields import FilterFieldsMixin
|
5
|
-
from camomilla.contrib.rest_framework.serializer import RemoveTranslationsMixin
|
6
3
|
from camomilla.serializers.mixins import (
|
7
4
|
JSONFieldPatchMixin,
|
8
5
|
NestMixin,
|
9
6
|
OrderingMixin,
|
10
7
|
SetupEagerLoadingMixin,
|
8
|
+
FieldsOverrideMixin,
|
9
|
+
FilterFieldsMixin,
|
10
|
+
RemoveTranslationsMixin,
|
11
11
|
)
|
12
12
|
|
13
13
|
return (
|
@@ -31,9 +31,13 @@ class UniquePermalinkValidator:
|
|
31
31
|
for language in activate_languages():
|
32
32
|
autopermalink_f = build_localized_fieldname("autopermalink", language)
|
33
33
|
f_name = build_localized_fieldname("permalink", language)
|
34
|
-
permalink = value.get(
|
34
|
+
permalink = value.get(
|
35
|
+
f_name, instance and get_nofallbacks(instance, "permalink")
|
36
|
+
)
|
35
37
|
permalink = UrlNode.sanitize_permalink(permalink)
|
36
|
-
autopermalink = value.get(
|
38
|
+
autopermalink = value.get(
|
39
|
+
autopermalink_f, instance and get_nofallbacks(instance, "autopermalink")
|
40
|
+
)
|
37
41
|
if autopermalink:
|
38
42
|
continue
|
39
43
|
fake_instance = serializer.Meta.model()
|
camomilla/settings.py
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
from django.conf import settings as django_settings
|
2
|
-
from modeltranslation.settings import ENABLE_REGISTRATIONS
|
2
|
+
from modeltranslation.settings import ENABLE_REGISTRATIONS, AVAILABLE_LANGUAGES
|
3
3
|
|
4
4
|
from camomilla.utils.getters import pointed_getter
|
5
5
|
|
@@ -53,6 +53,10 @@ ENABLE_TRANSLATIONS = (
|
|
53
53
|
ENABLE_REGISTRATIONS and "modeltranslation" in django_settings.INSTALLED_APPS
|
54
54
|
)
|
55
55
|
|
56
|
+
DEFAULT_LANGUAGE = pointed_getter(django_settings, "LANGUAGE_CODE", "en")
|
57
|
+
|
58
|
+
LANGUAGE_CODES = AVAILABLE_LANGUAGES
|
59
|
+
|
56
60
|
MEDIA_OPTIMIZE_MAX_WIDTH = pointed_getter(
|
57
61
|
django_settings, "CAMOMILLA.MEDIA.OPTIMIZE.MAX_WIDTH", 1980
|
58
62
|
)
|
@@ -79,6 +83,15 @@ TEMPLATE_CONTEXT_FILES = pointed_getter(
|
|
79
83
|
django_settings, "CAMOMILLA.RENDER.TEMPLATE_CONTEXT_FILES", []
|
80
84
|
)
|
81
85
|
|
86
|
+
API_TRANSLATION_ACCESSOR = pointed_getter(
|
87
|
+
django_settings, "CAMOMILLA.API.TRANSLATION_ACCESSOR", "translations"
|
88
|
+
)
|
89
|
+
|
90
|
+
REGISTERED_TEMPLATES_APPS = pointed_getter(
|
91
|
+
django_settings,
|
92
|
+
"CAMOMILLA.RENDER.REGISTERED_TEMPLATES_APPS", None
|
93
|
+
)
|
94
|
+
|
82
95
|
DEBUG = pointed_getter(django_settings, "CAMOMILLA.DEBUG", django_settings.DEBUG)
|
83
96
|
|
84
97
|
# camomilla settings example
|
@@ -96,10 +109,11 @@ DEBUG = pointed_getter(django_settings, "CAMOMILLA.DEBUG", django_settings.DEBUG
|
|
96
109
|
# "AUTO_CREATE_HOMEPAGE": True,
|
97
110
|
# "ARTICLE": {"DEFAULT_TEMPLATE": "", "INJECT_CONTEXT": None },
|
98
111
|
# "PAGE": {"DEFAULT_TEMPLATE": "", "INJECT_CONTEXT": None }
|
112
|
+
# "REGISTERED_TEMPLATE_APPS": []
|
99
113
|
# },
|
100
114
|
# "STRUCTURED_FIELD": {
|
101
115
|
# "CACHE_ENABLED": True
|
102
116
|
# }
|
103
|
-
# "API": {"NESTING_DEPTH": 10 },
|
117
|
+
# "API": {"NESTING_DEPTH": 10, "TRANSLATION_ACCESSOR": "translations"},
|
104
118
|
# "DEBUG": False
|
105
119
|
# }
|
camomilla/storages/default.py
CHANGED
@@ -1,6 +1,12 @@
|
|
1
1
|
from django.utils.module_loading import import_string
|
2
2
|
from django.conf import settings
|
3
3
|
|
4
|
+
from django import VERSION as DJANGO_VERSION
|
5
|
+
|
4
6
|
|
5
7
|
def get_default_storage_class():
|
6
|
-
|
8
|
+
if DJANGO_VERSION >= (4, 2):
|
9
|
+
storage = settings.STORAGES["default"]["BACKEND"]
|
10
|
+
else:
|
11
|
+
storage = settings.DEFAULT_FILE_STORAGE
|
12
|
+
return import_string(storage)
|
@@ -1,5 +1,7 @@
|
|
1
1
|
{% load menus %}
|
2
2
|
{% load i18n %}
|
3
|
+
{% load model_extras %}
|
4
|
+
|
3
5
|
{% get_current_language as current_lang %}
|
4
6
|
|
5
7
|
|
@@ -9,9 +11,6 @@
|
|
9
11
|
<title>Camomilla CMS</title>
|
10
12
|
<style>
|
11
13
|
html,
|
12
|
-
body {
|
13
|
-
height: 100%;
|
14
|
-
}
|
15
14
|
|
16
15
|
body {
|
17
16
|
background-color: #f7fafc;
|
@@ -49,6 +48,45 @@
|
|
49
48
|
font-size: 18px;
|
50
49
|
font-style: italic;
|
51
50
|
}
|
51
|
+
.accordion {
|
52
|
+
margin: 2rem auto;
|
53
|
+
border: 1px solid #000;
|
54
|
+
font-family: monospace;
|
55
|
+
width: 800px;
|
56
|
+
max-width: 800px;
|
57
|
+
}
|
58
|
+
|
59
|
+
.accordion-header {
|
60
|
+
display: flex;
|
61
|
+
justify-content: space-between;
|
62
|
+
align-items: center;
|
63
|
+
padding: 0.75rem 1rem;
|
64
|
+
cursor: pointer;
|
65
|
+
user-select: none;
|
66
|
+
}
|
67
|
+
|
68
|
+
.accordion-icon {
|
69
|
+
width: 1rem;
|
70
|
+
height: 1rem;
|
71
|
+
transition: transform 0.3s ease;
|
72
|
+
}
|
73
|
+
|
74
|
+
.accordion-toggle:checked + .accordion-header .accordion-icon {
|
75
|
+
transform: rotate(180deg);
|
76
|
+
}
|
77
|
+
|
78
|
+
.accordion-body {
|
79
|
+
max-height: 0;
|
80
|
+
overflow: hidden;
|
81
|
+
transition: max-height 0.3s ease;
|
82
|
+
}
|
83
|
+
|
84
|
+
.accordion-toggle:checked + .accordion-header + .accordion-body {
|
85
|
+
max-height: 30vh;
|
86
|
+
padding: 1rem;
|
87
|
+
border-top: 1px solid #000;
|
88
|
+
overflow-y: auto;
|
89
|
+
}
|
52
90
|
</style>
|
53
91
|
</head>
|
54
92
|
|
@@ -104,11 +142,29 @@
|
|
104
142
|
- <b>Module:</b> {{page_model.module}} <br>
|
105
143
|
- <b>Identifier:</b> {{page.pk}} <br>
|
106
144
|
- <b>Permalink:</b> {{page.permalink}} <br>
|
107
|
-
- <b>Language:</b> {{current_lang}} <br>
|
145
|
+
- <b>Language:</b> {{current_lang}} <br>
|
108
146
|
</code>
|
147
|
+
|
109
148
|
{% endblock %}
|
149
|
+
|
110
150
|
{% block content %}
|
111
151
|
{% endblock %}
|
152
|
+
|
153
|
+
<div class="accordion">
|
154
|
+
<input type="checkbox" id="accordion-toggle" class="accordion-toggle" hidden>
|
155
|
+
<label for="accordion-toggle" class="accordion-header">
|
156
|
+
<span>Page data</span>
|
157
|
+
<svg class="accordion-icon" viewBox="0 0 24 24">
|
158
|
+
<path d="M6 9l6 6 6-6" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/>
|
159
|
+
</svg>
|
160
|
+
</label>
|
161
|
+
<div class="accordion-body">
|
162
|
+
<pre>{{ page|to_pretty_dict|safe }}</pre>
|
163
|
+
</div>
|
164
|
+
</div>
|
165
|
+
|
166
|
+
</div>
|
167
|
+
|
112
168
|
</body>
|
113
169
|
|
114
170
|
</html>
|
@@ -4,7 +4,7 @@
|
|
4
4
|
{% for item in menu.nodes %}
|
5
5
|
<li>
|
6
6
|
{% if item.link.url %}
|
7
|
-
<a href="{{ item
|
7
|
+
<a href="{{ item|node_url }}{% if is_preview %}?preview=true{% endif %}">{{ item.title }}</a>
|
8
8
|
{% else %}
|
9
9
|
<span>{{item.title}}</span>
|
10
10
|
{% endif %}
|
camomilla/templatetags/menus.py
CHANGED
@@ -22,6 +22,9 @@ def render_menu(
|
|
22
22
|
menu_key: str,
|
23
23
|
template_path: str = "defaults/parts/menu.html",
|
24
24
|
):
|
25
|
+
if context is not None and not isinstance(context, dict):
|
26
|
+
context = context.__dict__
|
27
|
+
|
25
28
|
return context.get("menus", Menu.defaultdict())[menu_key].render(
|
26
29
|
template_path=template_path,
|
27
30
|
context=context,
|
@@ -0,0 +1,73 @@
|
|
1
|
+
from django import template
|
2
|
+
from django.utils.safestring import mark_safe
|
3
|
+
from django.forms.models import model_to_dict
|
4
|
+
from structured.pydantic.models import BaseModel
|
5
|
+
import json
|
6
|
+
import re
|
7
|
+
import html
|
8
|
+
from datetime import datetime
|
9
|
+
|
10
|
+
register = template.Library()
|
11
|
+
|
12
|
+
|
13
|
+
def custom_json_serializer(obj):
|
14
|
+
"""Serialize custom objects to JSON."""
|
15
|
+
if isinstance(obj, datetime):
|
16
|
+
return obj.isoformat()
|
17
|
+
raise TypeError(f"Type {type(obj)} not serializable")
|
18
|
+
|
19
|
+
|
20
|
+
def pretty_dict(data, indent_level=0):
|
21
|
+
"""
|
22
|
+
Recursive function to format a dictionary into a pretty string.
|
23
|
+
Args:
|
24
|
+
data (dict): The dictionary to format.
|
25
|
+
indent_level (int): The current indentation level.
|
26
|
+
Returns:
|
27
|
+
str: A pretty-printed string representation of the dictionary.
|
28
|
+
"""
|
29
|
+
indent = " " * indent_level
|
30
|
+
result = []
|
31
|
+
|
32
|
+
for key, value in data.items():
|
33
|
+
|
34
|
+
if isinstance(value, dict):
|
35
|
+
result.append(f"{indent}'{key}': {{")
|
36
|
+
result.append(pretty_dict(value, indent_level + 1))
|
37
|
+
result.append(f"{indent}}},")
|
38
|
+
|
39
|
+
elif isinstance(value, list):
|
40
|
+
result.append(f"{indent}'{key}': [")
|
41
|
+
for item in value:
|
42
|
+
if isinstance(item, dict):
|
43
|
+
result.append(pretty_dict(item, indent_level + 1))
|
44
|
+
else:
|
45
|
+
result.append(f"{indent} {json.dumps(item, default=custom_json_serializer)},")
|
46
|
+
result.append(f"{indent}],")
|
47
|
+
|
48
|
+
else:
|
49
|
+
result.append(f"{indent}'{key}': {json.dumps(value, default=custom_json_serializer)},")
|
50
|
+
|
51
|
+
return "\n".join(result).rstrip(',')
|
52
|
+
|
53
|
+
|
54
|
+
@register.filter
|
55
|
+
def to_pretty_dict(instance):
|
56
|
+
data = model_to_dict(instance)
|
57
|
+
|
58
|
+
for key, value in data.items():
|
59
|
+
if isinstance(value, BaseModel):
|
60
|
+
data[key] = value.model_dump()
|
61
|
+
|
62
|
+
formatted_dict = "{\n" + pretty_dict(data, indent_level=1) + "\n}"
|
63
|
+
|
64
|
+
escaped = html.escape(formatted_dict)
|
65
|
+
|
66
|
+
# This to highlight the keys in the JSON
|
67
|
+
highlighted = re.sub(
|
68
|
+
r"(')([^&#]+?)('):",
|
69
|
+
r"<span style='color:#df3079'>'\2'</span>:",
|
70
|
+
escaped
|
71
|
+
)
|
72
|
+
|
73
|
+
return mark_safe(f"<pre>{highlighted}</pre>")
|
camomilla/theme/__init__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__ = "6.0.0-beta.
|
1
|
+
__version__ = "6.0.0-beta.18"
|
@@ -1,33 +1,30 @@
|
|
1
|
-
from
|
1
|
+
from tinymce.widgets import TinyMCE
|
2
2
|
from django import forms
|
3
3
|
from django.contrib import admin
|
4
4
|
from django.http import HttpResponse
|
5
|
-
|
6
|
-
from
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
class AbstractPageAdmin(TranslationAwareModelAdmin):
|
19
|
-
change_form_template = "admin/camomilla/page/change_form.html"
|
5
|
+
from .pages import AbstractPageModelForm, AbstractPageAdmin
|
6
|
+
from .translations import TranslationAwareModelAdmin
|
7
|
+
from camomilla.models import (
|
8
|
+
Article,
|
9
|
+
Content,
|
10
|
+
Media,
|
11
|
+
MediaFolder,
|
12
|
+
Page,
|
13
|
+
Tag,
|
14
|
+
Menu,
|
15
|
+
UrlRedirect,
|
16
|
+
)
|
20
17
|
|
21
18
|
|
22
19
|
class UserProfileAdmin(admin.ModelAdmin):
|
23
20
|
pass
|
24
21
|
|
25
22
|
|
26
|
-
class ArticleAdminForm(
|
23
|
+
class ArticleAdminForm(AbstractPageModelForm):
|
27
24
|
class Meta:
|
28
25
|
model = Article
|
29
26
|
fields = "__all__"
|
30
|
-
widgets = {"content":
|
27
|
+
widgets = {"content": TinyMCE()}
|
31
28
|
|
32
29
|
|
33
30
|
class ArticleAdmin(AbstractPageAdmin):
|
@@ -47,7 +44,7 @@ class ContentAdminForm(forms.ModelForm):
|
|
47
44
|
class Meta:
|
48
45
|
model = Content
|
49
46
|
fields = "__all__"
|
50
|
-
widgets = {"content":
|
47
|
+
widgets = {"content": TinyMCE()}
|
51
48
|
|
52
49
|
|
53
50
|
class ContentAdmin(TranslationAwareModelAdmin):
|
@@ -81,13 +78,17 @@ class MediaAdmin(TranslationAwareModelAdmin):
|
|
81
78
|
|
82
79
|
|
83
80
|
class PageAdmin(AbstractPageAdmin):
|
84
|
-
|
81
|
+
pass
|
85
82
|
|
86
83
|
|
87
84
|
class MenuAdmin(TranslationAwareModelAdmin):
|
88
85
|
pass
|
89
86
|
|
90
87
|
|
88
|
+
class UrlRedirectAdmin(admin.ModelAdmin):
|
89
|
+
pass
|
90
|
+
|
91
|
+
|
91
92
|
admin.site.register(Article, ArticleAdmin)
|
92
93
|
admin.site.register(MediaFolder, MediaFolderAdmin)
|
93
94
|
admin.site.register(Tag, TagAdmin)
|
@@ -95,3 +96,4 @@ admin.site.register(Content, ContentAdmin)
|
|
95
96
|
admin.site.register(Media, MediaAdmin)
|
96
97
|
admin.site.register(Page, PageAdmin)
|
97
98
|
admin.site.register(Menu, MenuAdmin)
|
99
|
+
admin.site.register(UrlRedirect, UrlRedirectAdmin)
|
@@ -0,0 +1,46 @@
|
|
1
|
+
from django import forms
|
2
|
+
from camomilla import settings
|
3
|
+
from .translations import TranslationAwareModelAdmin
|
4
|
+
from camomilla.models import UrlNode
|
5
|
+
|
6
|
+
|
7
|
+
class AbstractPageModelFormMeta(forms.models.ModelFormMetaclass):
|
8
|
+
def __new__(mcs, name, bases, attrs):
|
9
|
+
new_class = super().__new__(mcs, name, bases, attrs)
|
10
|
+
fields_to_add = forms.fields_for_model(UrlNode, UrlNode.LANG_PERMALINK_FIELDS)
|
11
|
+
if settings.ENABLE_TRANSLATIONS:
|
12
|
+
for i, field_name in enumerate(fields_to_add.keys()):
|
13
|
+
field_classes = ["mt", f"mt-field-{field_name.replace('_', '-')}"]
|
14
|
+
i == 0 and field_classes.append("mt-default")
|
15
|
+
fields_to_add[field_name].widget.attrs.update(
|
16
|
+
{"class": " ".join(field_classes)}
|
17
|
+
)
|
18
|
+
new_class.base_fields.update(fields_to_add)
|
19
|
+
return new_class
|
20
|
+
|
21
|
+
|
22
|
+
class AbstractPageModelForm(
|
23
|
+
forms.models.BaseModelForm, metaclass=AbstractPageModelFormMeta
|
24
|
+
):
|
25
|
+
|
26
|
+
def get_initial_for_field(self, field, field_name):
|
27
|
+
if field_name in UrlNode.LANG_PERMALINK_FIELDS:
|
28
|
+
return getattr(self.instance, field_name)
|
29
|
+
return super().get_initial_for_field(field, field_name)
|
30
|
+
|
31
|
+
def save(self, commit: bool = True):
|
32
|
+
model = super().save(commit=False)
|
33
|
+
for field_name in UrlNode.LANG_PERMALINK_FIELDS:
|
34
|
+
if field_name in self.cleaned_data:
|
35
|
+
if getattr(model, field_name) != self.cleaned_data[field_name]:
|
36
|
+
# sets autopermalink to False if permalink is manually set
|
37
|
+
setattr(model, f"auto{field_name}", False)
|
38
|
+
setattr(model, field_name, self.cleaned_data[field_name])
|
39
|
+
if commit:
|
40
|
+
model.save()
|
41
|
+
return model
|
42
|
+
|
43
|
+
|
44
|
+
class AbstractPageAdmin(TranslationAwareModelAdmin):
|
45
|
+
form = AbstractPageModelForm
|
46
|
+
change_form_template = "admin/camomilla/page/change_form.html"
|
@@ -0,0 +1,13 @@
|
|
1
|
+
from camomilla import settings
|
2
|
+
|
3
|
+
if settings.ENABLE_TRANSLATIONS:
|
4
|
+
from modeltranslation.admin import (
|
5
|
+
TabbedTranslationAdmin as TranslationAwareModelAdmin,
|
6
|
+
)
|
7
|
+
else:
|
8
|
+
from django.contrib.admin import ModelAdmin as TranslationAwareModelAdmin
|
9
|
+
|
10
|
+
|
11
|
+
__all__ = [
|
12
|
+
"TranslationAwareModelAdmin",
|
13
|
+
]
|
camomilla/theme/apps.py
CHANGED
@@ -29,13 +29,10 @@ class CamomillaThemeConfig(AppConfig):
|
|
29
29
|
name = "camomilla.theme"
|
30
30
|
|
31
31
|
def ready(self):
|
32
|
-
set_default_settings(
|
33
|
-
CKEDITOR_UPLOAD_PATH="editor-uploads/", X_FRAME_OPTIONS="SAMEORIGIN"
|
34
|
-
)
|
35
32
|
add_apps(
|
36
|
-
"
|
37
|
-
"ckeditor",
|
33
|
+
"tinymce",
|
38
34
|
"django_jsonform",
|
39
35
|
"admin_interface",
|
40
36
|
"colorfield",
|
37
|
+
"structured",
|
41
38
|
)
|
camomilla/translation.py
CHANGED
@@ -16,7 +16,13 @@ class SeoMixinTranslationOptions(TranslationOptions):
|
|
16
16
|
|
17
17
|
|
18
18
|
class AbstractPageTranslationOptions(SeoMixinTranslationOptions):
|
19
|
-
fields = (
|
19
|
+
fields = (
|
20
|
+
"breadcrumbs_title",
|
21
|
+
"autopermalink",
|
22
|
+
"status",
|
23
|
+
"indexable",
|
24
|
+
"template_data",
|
25
|
+
)
|
20
26
|
|
21
27
|
|
22
28
|
@register(Article)
|
camomilla/urls.py
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
from django.shortcuts import redirect
|
2
1
|
from django.urls import include, path
|
3
2
|
from rest_framework import routers
|
4
3
|
from importlib.util import find_spec
|
@@ -21,6 +20,7 @@ from camomilla.views import (
|
|
21
20
|
MenuViewSet,
|
22
21
|
)
|
23
22
|
from camomilla.views.pages import fetch_page
|
23
|
+
from camomilla.redirects import url_patterns as old_redirects
|
24
24
|
|
25
25
|
router = routers.DefaultRouter()
|
26
26
|
|
@@ -30,18 +30,15 @@ router.register(r"contents", ContentViewSet, "camomilla-content")
|
|
30
30
|
router.register(r"media", MediaViewSet, "camomilla-media")
|
31
31
|
router.register(r"media-folders", MediaFolderViewSet, "camomilla-media_folders")
|
32
32
|
router.register(r"pages", PageViewSet, "camomilla-pages")
|
33
|
-
router.register(r"sitemap", PageViewSet, "camomilla-sitemap")
|
34
33
|
router.register(r"users", UserViewSet, "camomilla-users")
|
35
34
|
router.register(r"permissions", PermissionViewSet, "camomilla-permissions")
|
36
35
|
router.register(r"menus", MenuViewSet, "camomilla-menus")
|
37
36
|
|
38
37
|
urlpatterns = [
|
38
|
+
*old_redirects,
|
39
39
|
path("", include(router.urls)),
|
40
40
|
path("pages-router/", fetch_page),
|
41
41
|
path("pages-router/<path:permalink>", fetch_page),
|
42
|
-
path(
|
43
|
-
"profiles/me/", lambda _: redirect("../../users/current/"), name="profiles-me"
|
44
|
-
),
|
45
42
|
path("token-auth/", CamomillaObtainAuthToken.as_view(), name="api_token"),
|
46
43
|
path("auth/login/", CamomillaAuthLogin.as_view(), name="login"),
|
47
44
|
path("auth/logout/", CamomillaAuthLogout.as_view(), name="logout"),
|