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
@@ -0,0 +1,71 @@
|
|
1
|
+
import traceback
|
2
|
+
from io import BytesIO
|
3
|
+
|
4
|
+
from django.core.files.base import ContentFile
|
5
|
+
from PIL import Image
|
6
|
+
|
7
|
+
from camomilla import settings
|
8
|
+
from camomilla.storages.default import get_default_storage_class
|
9
|
+
|
10
|
+
|
11
|
+
class OptimizedStorage(get_default_storage_class()):
|
12
|
+
MEDIA_MAX_WIDTH = settings.MEDIA_OPTIMIZE_MAX_WIDTH
|
13
|
+
MEDIA_MAX_HEIGHT = settings.MEDIA_OPTIMIZE_MAX_HEIGHT
|
14
|
+
MEDIA_DPI = settings.MEDIA_OPTIMIZE_DPI
|
15
|
+
MEDIA_OPTIMIZE_JPEG_QUALITY = settings.MEDIA_OPTIMIZE_JPEG_QUALITY
|
16
|
+
|
17
|
+
def __init__(self, *args, **kwargs) -> None:
|
18
|
+
self.MEDIA_MAX_WIDTH = int(kwargs.pop("max_width", self.MEDIA_MAX_WIDTH))
|
19
|
+
self.MEDIA_MAX_HEIGHT = int(kwargs.pop("max_height", self.MEDIA_MAX_HEIGHT))
|
20
|
+
self.MEDIA_DPI = int(kwargs.pop("dpi", self.MEDIA_DPI))
|
21
|
+
self.MEDIA_OPTIMIZE_JPEG_QUALITY = int(
|
22
|
+
kwargs.pop("jpeg_quality", self.MEDIA_OPTIMIZE_JPEG_QUALITY)
|
23
|
+
)
|
24
|
+
super().__init__(*args, **kwargs)
|
25
|
+
|
26
|
+
def _save(self, name: str, content: ContentFile):
|
27
|
+
if settings.ENABLE_MEDIA_OPTIMIZATION:
|
28
|
+
content, _ = self._optimize(name, content)
|
29
|
+
return super(OptimizedStorage, self)._save(name, content)
|
30
|
+
|
31
|
+
def _optimize(self, name: str, content: ContentFile):
|
32
|
+
try:
|
33
|
+
image = Image.open(content)
|
34
|
+
original_size = content.size
|
35
|
+
width, height = image.size
|
36
|
+
|
37
|
+
if width > self.MEDIA_MAX_WIDTH or height > self.MEDIA_MAX_HEIGHT:
|
38
|
+
if width <= height:
|
39
|
+
selected_width = int((self.MEDIA_MAX_HEIGHT / height) * width)
|
40
|
+
selected_height = self.MEDIA_MAX_HEIGHT
|
41
|
+
else:
|
42
|
+
selected_height = int((self.MEDIA_MAX_WIDTH / width) * height)
|
43
|
+
selected_width = self.MEDIA_MAX_WIDTH
|
44
|
+
|
45
|
+
image = image.resize(
|
46
|
+
[selected_width, selected_height], resample=Image.LANCZOS
|
47
|
+
)
|
48
|
+
dpi = (self.MEDIA_DPI, self.MEDIA_DPI)
|
49
|
+
image.info["dpi"] = dpi
|
50
|
+
tmp = BytesIO()
|
51
|
+
ext = name.split(".")[-1].lower().replace("jpg", "jpeg")
|
52
|
+
image.save(
|
53
|
+
tmp,
|
54
|
+
ext,
|
55
|
+
dpi=dpi,
|
56
|
+
optimize=True,
|
57
|
+
quality=self.MEDIA_OPTIMIZE_JPEG_QUALITY,
|
58
|
+
)
|
59
|
+
tmp_size = len(tmp.getvalue())
|
60
|
+
|
61
|
+
if tmp_size > original_size:
|
62
|
+
tmp.close()
|
63
|
+
return content, False
|
64
|
+
optimized_content = ContentFile(tmp.getvalue())
|
65
|
+
tmp.close()
|
66
|
+
content.close()
|
67
|
+
return optimized_content, True
|
68
|
+
except Exception as e:
|
69
|
+
traceback.print_exc()
|
70
|
+
print(f"Error optimizing image {name}: {str(e)}")
|
71
|
+
return content, False
|
@@ -1,7 +1,7 @@
|
|
1
|
-
from
|
1
|
+
from camomilla.storages.default import get_default_storage_class
|
2
2
|
|
3
3
|
|
4
|
-
class OverwriteStorage(
|
4
|
+
class OverwriteStorage(get_default_storage_class()):
|
5
5
|
def _save(self, name, content):
|
6
6
|
if self.exists(name):
|
7
7
|
self.delete(name)
|
@@ -0,0 +1,10 @@
|
|
1
|
+
{% extends "admin/change_form.html" %}
|
2
|
+
{% load i18n %}
|
3
|
+
{% block object-tools-items %}
|
4
|
+
{{block.super}}
|
5
|
+
<li>
|
6
|
+
<a href="{{original.routerlink}}?preview=true" target="_blank" class="previewlink">
|
7
|
+
{% translate "Preview" %}
|
8
|
+
</a>
|
9
|
+
</li>
|
10
|
+
{% endblock %}
|
@@ -0,0 +1,170 @@
|
|
1
|
+
{% load menus %}
|
2
|
+
{% load i18n %}
|
3
|
+
{% load model_extras %}
|
4
|
+
|
5
|
+
{% get_current_language as current_lang %}
|
6
|
+
|
7
|
+
|
8
|
+
<html>
|
9
|
+
|
10
|
+
<head>
|
11
|
+
<title>Camomilla CMS</title>
|
12
|
+
<style>
|
13
|
+
html,
|
14
|
+
|
15
|
+
body {
|
16
|
+
background-color: #f7fafc;
|
17
|
+
color: black;
|
18
|
+
display: flex;
|
19
|
+
flex-direction: column;
|
20
|
+
align-items: center;
|
21
|
+
justify-content: center;
|
22
|
+
font-family: sans-serif;
|
23
|
+
margin: 0;
|
24
|
+
}
|
25
|
+
|
26
|
+
.letter {
|
27
|
+
fill: black;
|
28
|
+
}
|
29
|
+
|
30
|
+
.language-switch {
|
31
|
+
position: absolute;
|
32
|
+
top: 6px;
|
33
|
+
right: 6px;
|
34
|
+
}
|
35
|
+
|
36
|
+
|
37
|
+
@media (prefers-color-scheme: dark) {
|
38
|
+
body {
|
39
|
+
background-color: #0d1117;
|
40
|
+
color: white;
|
41
|
+
}
|
42
|
+
|
43
|
+
.letter {
|
44
|
+
fill: #ebebeb;
|
45
|
+
}
|
46
|
+
}
|
47
|
+
.temp_type{
|
48
|
+
font-size: 18px;
|
49
|
+
font-style: italic;
|
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
|
+
}
|
90
|
+
</style>
|
91
|
+
</head>
|
92
|
+
|
93
|
+
<body>
|
94
|
+
<header >
|
95
|
+
{% block main_menu %}
|
96
|
+
{% render_menu "main_menu" %}
|
97
|
+
{% endblock %}
|
98
|
+
{% block language_switch %}
|
99
|
+
{% include "defaults/parts/langswitch.html" %}
|
100
|
+
{% endblock %}
|
101
|
+
</header>
|
102
|
+
|
103
|
+
<svg width="300" height="300" viewBox="0 0 180 180" fill="none" xmlns="http://www.w3.org/2000/svg">
|
104
|
+
<path fill-rule="evenodd" clip-rule="evenodd"
|
105
|
+
d="M130.84 104.52L135.262 102.465C135.262 102.465 133.122 100.134 133.122 96.8994C133.122 93.8551 135.215 91.5718 139.163 91.5718C143.539 91.5718 145.394 94.3308 145.394 97.3751C145.394 100.039 143.444 102.275 143.444 102.275L148.294 104.52L139.567 108.656L130.84 104.52Z"
|
106
|
+
fill="#FCB92C" />
|
107
|
+
<path
|
108
|
+
d="M138.023 65C126.947 65 117.965 73.4591 117.965 87.3252C117.965 97.9645 123.023 102.499 123.023 102.499L131.657 98.4877C131.657 98.4877 127.732 94.2146 127.732 88.2844C127.732 82.7032 131.569 78.5172 138.808 78.5172C146.831 78.5172 150.232 83.5752 150.232 89.1565C150.232 94.0402 146.656 98.1389 146.656 98.1389L156.075 102.499C156.075 102.499 159.999 96.7436 159.999 87.5868C159.999 74.244 149.709 65 138.023 65Z"
|
109
|
+
fill="#6AB946" />
|
110
|
+
<path
|
111
|
+
d="M58.0257 84.4732L59.1476 79.5448C57.024 80.0256 56.9439 78.9839 56.9439 76.9404V72.5329C56.9439 67.364 54.8202 65 49.0905 65C43.0001 65 40.1151 67.6445 40.5158 70.8901L46.7264 72.052C46.5261 70.249 47.2072 69.5678 48.6497 69.5678C50.1723 69.5678 50.6932 70.3291 50.6932 72.4527V73.0137C44.162 73.0537 39.8747 75.0171 39.8747 79.3445C39.8747 82.9106 42.7997 84.8338 46.2055 84.8338C49.1305 84.8338 50.573 83.3914 51.5346 82.0291C52.0555 83.4314 53.7384 85.1544 58.0257 84.4732ZM50.6932 78.0222C50.5329 79.7452 49.0905 80.3863 47.8884 80.3863C46.8466 80.3863 46.1655 79.9054 46.1655 78.8637C46.1655 77.0205 48.249 76.5798 50.6932 76.5397V78.0222Z"
|
112
|
+
class="letter" />
|
113
|
+
<path
|
114
|
+
d="M103.675 84.8338C111.048 84.8338 113.332 80.3462 113.332 74.8568C113.332 69.688 111.288 65 103.555 65C95.982 65 93.8183 69.5277 93.8183 74.7767C93.8183 80.1458 96.1022 84.8338 103.675 84.8338ZM103.595 80.1859C100.83 80.1859 100.309 77.5414 100.309 74.8568C100.309 72.2123 100.83 69.6479 103.555 69.6479C106.32 69.6479 106.841 72.2123 106.841 74.937C106.841 77.6616 106.32 80.1859 103.595 80.1859Z"
|
115
|
+
class="letter" />
|
116
|
+
<path
|
117
|
+
d="M31.9003 77.6215C31.9003 79.2243 30.9787 80.1859 29.4561 80.1859C27.2924 80.1859 26.4911 78.5431 26.4911 75.2174C26.4911 72.5329 26.7716 69.6479 29.4561 69.6479C30.578 69.6479 31.8202 70.4092 31.4596 72.9335L37.59 71.6914C38.0308 67.4442 34.665 65 29.5363 65C24.087 65 20 67.4442 20 74.8568C20 81.1876 22.8849 84.8338 29.376 84.8338C34.5448 84.8338 36.6284 82.3496 37.7102 78.7835L31.9003 77.6215Z"
|
118
|
+
class="letter" />
|
119
|
+
<path
|
120
|
+
d="M112.21 106.783L113.332 101.854C111.208 102.335 111.128 101.293 111.128 99.2499V94.8424C111.128 89.6736 109.004 87.3095 103.275 87.3095C97.1843 87.3095 94.2994 89.9541 94.7001 93.1996L100.911 94.3616C100.71 92.5585 101.391 91.8773 102.834 91.8773C104.357 91.8773 104.877 92.6386 104.877 94.7623V95.3232C98.3463 95.3633 94.059 97.3266 94.059 101.654C94.059 105.22 96.984 107.143 100.39 107.143C103.315 107.143 104.757 105.701 105.719 104.339C106.24 105.741 107.923 107.464 112.21 106.783ZM104.877 100.332C104.717 102.055 103.275 102.696 102.073 102.696C101.031 102.696 100.35 102.215 100.35 101.173C100.35 99.3301 102.433 98.8893 104.877 98.8492V100.332Z"
|
121
|
+
class="letter" />
|
122
|
+
<path d="M61.3508 106.581H67.6015V87.8691H61.3508V106.581Z" class="letter" />
|
123
|
+
<path
|
124
|
+
d="M64.4758 115.882C62.5525 115.882 60.9498 114.279 60.9498 112.356C60.9498 110.432 62.5525 108.87 64.4758 108.87C66.4391 108.87 68.0018 110.432 68.0018 112.356C68.0018 114.279 66.4391 115.882 64.4758 115.882Z"
|
125
|
+
class="letter" />
|
126
|
+
<path
|
127
|
+
d="M50.3532 106.582H56.6038V94.2814C56.6038 88.4315 53.3984 87.3095 50.6336 87.3095C46.3063 87.3095 44.7837 89.9541 44.3429 90.9558C43.5415 88.2311 41.2576 87.3095 38.8535 87.3095C34.8066 87.3095 33.244 89.7136 32.7231 90.8356V87.8705H26.9132V106.582H33.1638V97.2465C33.4844 96.0044 34.6063 92.5986 36.8501 92.5986C38.1323 92.5986 38.6532 93.7205 38.6532 95.8842V106.582H44.9039V97.1263C45.2645 95.7239 46.3864 92.5986 48.5501 92.5986C49.8323 92.5986 50.3532 93.6804 50.3532 95.8842V106.582Z"
|
128
|
+
class="letter" />
|
129
|
+
<path
|
130
|
+
d="M84.7527 80.6771H91.0033V71.9719C91.0033 66.1219 87.7979 65 85.0331 65C80.7058 65 79.1832 67.6445 78.7424 68.6462C77.941 65.9216 75.6571 65 73.253 65C69.2061 65 67.6435 67.4041 67.1226 68.526V65.561H61.3127V84.2729H67.5633V74.937C67.8839 73.6948 69.0058 70.289 71.2496 70.289C72.5318 70.289 73.0527 71.4109 73.0527 73.5746V80.6771H79.3034V74.8168C79.664 73.4144 80.7859 70.289 82.9496 70.289C84.2318 70.289 84.7527 71.3709 84.7527 73.5746V80.6771Z"
|
131
|
+
class="letter" />
|
132
|
+
<path d="M73.07 84.2731V106.582H79.3207V84.2731H73.07Z" class="letter" />
|
133
|
+
<path d="M84.7531 84.2731V106.582H91.0037V84.2731H84.7531Z" class="letter" />
|
134
|
+
</svg>
|
135
|
+
|
136
|
+
{% block template_info_title %}
|
137
|
+
<h1><span class="temp_type">PAGE</span> DEFAULT TEMPLATE</h1>
|
138
|
+
{% endblock %}
|
139
|
+
{% block template_info %}
|
140
|
+
<code>
|
141
|
+
- <b>Class:</b> {{page_model.class}} <br>
|
142
|
+
- <b>Module:</b> {{page_model.module}} <br>
|
143
|
+
- <b>Identifier:</b> {{page.pk}} <br>
|
144
|
+
- <b>Permalink:</b> {{page.permalink}} <br>
|
145
|
+
- <b>Language:</b> {{current_lang}} <br>
|
146
|
+
</code>
|
147
|
+
|
148
|
+
{% endblock %}
|
149
|
+
|
150
|
+
{% block content %}
|
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
|
+
|
168
|
+
</body>
|
169
|
+
|
170
|
+
</html>
|
@@ -0,0 +1,83 @@
|
|
1
|
+
{% load i18n camomilla_filters %}
|
2
|
+
|
3
|
+
<div>
|
4
|
+
<style>
|
5
|
+
.language-switch {
|
6
|
+
font-family: monospace;
|
7
|
+
border-top: solid 1px;
|
8
|
+
border-right: solid 1px;
|
9
|
+
border-left: solid 1px;
|
10
|
+
}
|
11
|
+
.language-switch div {
|
12
|
+
border-bottom: solid 1px;
|
13
|
+
padding: 0.3rem 1rem;
|
14
|
+
cursor: pointer;
|
15
|
+
}
|
16
|
+
</style>
|
17
|
+
<form id="form_change_language" action="{% url 'set_language' %}" method="post" style='display: none;'>
|
18
|
+
{% csrf_token %}
|
19
|
+
<input id="next_form_change_language" name="next" type="hidden" value="{{ request.path }}" />
|
20
|
+
<select id="select_change_language" name="language">
|
21
|
+
{% get_current_language as LANGUAGE_CODE %}
|
22
|
+
{% get_available_languages as LANGUAGES %}
|
23
|
+
{% get_language_info_list for LANGUAGES as languages %}
|
24
|
+
{% for language in languages %}
|
25
|
+
<option value="{{ language.code }}" {% if language.code == LANGUAGE_CODE %} selected="selected" {% endif %}>
|
26
|
+
{{ language.name_local }} ({{ language.code }})
|
27
|
+
</option>
|
28
|
+
{% endfor %}
|
29
|
+
</select>
|
30
|
+
<input type="submit" value="Go" style='display: none;' />
|
31
|
+
</form>
|
32
|
+
|
33
|
+
<div class="language-switch">
|
34
|
+
{% get_current_language as LANGUAGE_CODE %}
|
35
|
+
{% get_available_languages as LANGUAGES %}
|
36
|
+
{% get_language_info_list for LANGUAGES as languages %}
|
37
|
+
{% get_language_info for LANGUAGE_CODE as current_lang %}
|
38
|
+
|
39
|
+
{% for lang_code, redirect in page.alternate_urls.items %}
|
40
|
+
<div class="language-switch--btn" data-lang="{{lang_code}}"
|
41
|
+
{% if current_lang.code != lang_code %}
|
42
|
+
{% if redirect is None %}
|
43
|
+
onclick="submitLanguageHomeRedirect('{{lang_code}}');"
|
44
|
+
{% else %}
|
45
|
+
onclick="submitLanguage('{{lang_code}}', '{{redirect|strip_lang:lang_code}}');"
|
46
|
+
{% endif %}
|
47
|
+
{% endif %}
|
48
|
+
>
|
49
|
+
{% get_language_info for lang_code as lang %}
|
50
|
+
<a>{{ lang.name_translated }} {% if current_lang.code == lang_code %} 👾 {% endif %}</a>
|
51
|
+
</div>
|
52
|
+
{% endfor %}
|
53
|
+
</div>
|
54
|
+
</div>
|
55
|
+
|
56
|
+
<script>
|
57
|
+
|
58
|
+
window.submitLanguage = function (langCode, redirect) {
|
59
|
+
document.getElementById('select_change_language').value = langCode;
|
60
|
+
document.getElementById('next_form_change_language').value = redirect;
|
61
|
+
document.getElementById('next_form_change_language').disabled = false;
|
62
|
+
setTimeout(function () {
|
63
|
+
document.getElementById('form_change_language').submit();
|
64
|
+
}, 300);
|
65
|
+
};
|
66
|
+
|
67
|
+
window.submitLanguageWithoutRedirect = function (langCode) {
|
68
|
+
document.getElementById('select_change_language').value = langCode;
|
69
|
+
document.getElementById('next_form_change_language').disabled = true;
|
70
|
+
setTimeout(function () {
|
71
|
+
document.getElementById('form_change_language').submit();
|
72
|
+
}, 300);
|
73
|
+
};
|
74
|
+
window.submitLanguageHomeRedirect = function (langCode) {
|
75
|
+
document.getElementById('select_change_language').value = langCode;
|
76
|
+
document.getElementById('next_form_change_language').value = "/";
|
77
|
+
document.getElementById('next_form_change_language').disabled = false;
|
78
|
+
setTimeout(function () {
|
79
|
+
document.getElementById('form_change_language').submit();
|
80
|
+
}, 300);
|
81
|
+
};
|
82
|
+
|
83
|
+
</script>
|
@@ -0,0 +1,15 @@
|
|
1
|
+
{% load menus %}
|
2
|
+
{% if menu.nodes|length %}
|
3
|
+
<ul>
|
4
|
+
{% for item in menu.nodes %}
|
5
|
+
<li>
|
6
|
+
{% if item.link.url %}
|
7
|
+
<a href="{{ item|node_url }}{% if is_preview %}?preview=true{% endif %}">{{ item.title }}</a>
|
8
|
+
{% else %}
|
9
|
+
<span>{{item.title}}</span>
|
10
|
+
{% endif %}
|
11
|
+
{% include 'defaults/parts/menu.html' with menu=item %}
|
12
|
+
</li>
|
13
|
+
{% endfor %}
|
14
|
+
</ul>
|
15
|
+
{% endif %}
|
File without changes
|
@@ -0,0 +1,51 @@
|
|
1
|
+
def autodiscover_context_files():
|
2
|
+
"""
|
3
|
+
Auto-discover INSTALLED_APPS template_context.py modules and fail silently when
|
4
|
+
not present. This forces an import on them to register.
|
5
|
+
Also import explicit modules.
|
6
|
+
"""
|
7
|
+
import copy
|
8
|
+
import sys
|
9
|
+
from django.utils.module_loading import module_has_submodule
|
10
|
+
from camomilla.templates_context.rendering import ctx_registry
|
11
|
+
from importlib import import_module
|
12
|
+
from django.apps import apps
|
13
|
+
from camomilla.settings import TEMPLATE_CONTEXT_FILES, DEBUG
|
14
|
+
|
15
|
+
mods = [
|
16
|
+
(app_config.name, app_config.module) for app_config in apps.get_app_configs()
|
17
|
+
]
|
18
|
+
|
19
|
+
for app, mod in mods:
|
20
|
+
module = "%s.template_context" % app
|
21
|
+
_model_registry_bkp = copy.copy(ctx_registry._model_registry)
|
22
|
+
_template_registry_bkp = copy.copy(ctx_registry._template_registry)
|
23
|
+
|
24
|
+
try:
|
25
|
+
import_module(module)
|
26
|
+
except Exception:
|
27
|
+
ctx_registry._model_registry = _model_registry_bkp
|
28
|
+
ctx_registry._template_registry = _template_registry_bkp
|
29
|
+
if module_has_submodule(mod, "template_context"):
|
30
|
+
raise
|
31
|
+
|
32
|
+
for module in TEMPLATE_CONTEXT_FILES:
|
33
|
+
import_module(module)
|
34
|
+
|
35
|
+
if DEBUG:
|
36
|
+
try:
|
37
|
+
if sys.argv[1] in ("runserver", "runserver_plus"):
|
38
|
+
if not ctx_registry.get_registered_info().items():
|
39
|
+
return
|
40
|
+
print("Camomilla context files:")
|
41
|
+
for k, v in ctx_registry.get_registered_info().items():
|
42
|
+
print(f"{k}:")
|
43
|
+
models = v.get("models")
|
44
|
+
templates = v.get("templates")
|
45
|
+
if models:
|
46
|
+
print(f" models: {models}")
|
47
|
+
if templates:
|
48
|
+
print(f" templates: {templates}")
|
49
|
+
print("\n")
|
50
|
+
except IndexError:
|
51
|
+
pass
|
@@ -0,0 +1,89 @@
|
|
1
|
+
from collections import defaultdict
|
2
|
+
from typing import Callable, Optional, TYPE_CHECKING, Type, Dict
|
3
|
+
import inspect
|
4
|
+
from django.http import HttpRequest
|
5
|
+
|
6
|
+
if TYPE_CHECKING:
|
7
|
+
from camomilla.models.page import AbstractPage
|
8
|
+
|
9
|
+
|
10
|
+
class PagesContextRegistry:
|
11
|
+
def __init__(self) -> None:
|
12
|
+
self._model_registry = defaultdict(set)
|
13
|
+
self._template_registry = defaultdict(set)
|
14
|
+
|
15
|
+
def register(
|
16
|
+
self,
|
17
|
+
func: Callable,
|
18
|
+
template_path: Optional[str],
|
19
|
+
page_model: Optional[Type["AbstractPage"]],
|
20
|
+
):
|
21
|
+
assert (
|
22
|
+
template_path is not None or page_model is not None
|
23
|
+
), "You must provide at least one between template_path and page_model"
|
24
|
+
if template_path is not None:
|
25
|
+
self._template_registry[template_path].add(func)
|
26
|
+
if page_model is not None:
|
27
|
+
self._model_registry[page_model].add(func)
|
28
|
+
|
29
|
+
def get_registered_info(self) -> Dict[str, dict]:
|
30
|
+
info = defaultdict(dict)
|
31
|
+
for k, v in self._template_registry.items():
|
32
|
+
for f in v:
|
33
|
+
key = f"{f.__module__}.{f.__name__}"
|
34
|
+
info[key]["templates"] = info[key].get("templates", set())
|
35
|
+
info[key]["templates"].add(k)
|
36
|
+
info[key]["templates_count"] = len(info[key]["templates"])
|
37
|
+
for k, v in self._model_registry.items():
|
38
|
+
key = f"{f.__module__}.{f.__name__}"
|
39
|
+
for f in v:
|
40
|
+
info[key]["models"] = info[key].get("models", set())
|
41
|
+
info[key]["models"].add(k)
|
42
|
+
info[key]["models_count"] = len(info[key]["models"])
|
43
|
+
return info
|
44
|
+
|
45
|
+
def get_wrapper_context_func(
|
46
|
+
self, template_path: str, page_model: Type["AbstractPage"]
|
47
|
+
) -> Callable:
|
48
|
+
all_funcs = set()
|
49
|
+
for cls in self._model_registry.keys():
|
50
|
+
if issubclass(page_model, cls):
|
51
|
+
all_funcs.update(self._model_registry[cls])
|
52
|
+
all_funcs.update(self._template_registry[template_path])
|
53
|
+
|
54
|
+
def context_func(request: HttpRequest, super_ctx: dict = {}):
|
55
|
+
context = super_ctx.copy()
|
56
|
+
for func in all_funcs:
|
57
|
+
arg_spec = inspect.getfullargspec(func)
|
58
|
+
kwargs = {}
|
59
|
+
if "request" in arg_spec.args:
|
60
|
+
kwargs["request"] = request
|
61
|
+
if "super_ctx" in arg_spec.args:
|
62
|
+
kwargs["super_ctx"] = context
|
63
|
+
context.update(func(**kwargs) or {})
|
64
|
+
return context
|
65
|
+
|
66
|
+
return context_func
|
67
|
+
|
68
|
+
def get_context_for_page(self, page: "AbstractPage", request, super_ctx: dict = {}):
|
69
|
+
return self.get_wrapper_context_func(
|
70
|
+
page.get_template_path(request), page.__class__
|
71
|
+
)(request, super_ctx=super_ctx)
|
72
|
+
|
73
|
+
|
74
|
+
def register(
|
75
|
+
template_path: Optional[str] = None,
|
76
|
+
page_model: Optional[Type["AbstractPage"]] = None,
|
77
|
+
):
|
78
|
+
assert (
|
79
|
+
template_path is not None or page_model is not None
|
80
|
+
), "You must provide at least one between template_path and page_model"
|
81
|
+
|
82
|
+
def inner(func: Callable):
|
83
|
+
ctx_registry.register(func, template_path, page_model)
|
84
|
+
return func
|
85
|
+
|
86
|
+
return inner
|
87
|
+
|
88
|
+
|
89
|
+
ctx_registry = PagesContextRegistry()
|
@@ -7,14 +7,10 @@ register = template.Library()
|
|
7
7
|
|
8
8
|
@register.filter(name="filter_content")
|
9
9
|
def filter_content(page, args):
|
10
|
-
curr_lang = get_language()
|
11
10
|
try:
|
12
|
-
content = page.contents.
|
11
|
+
content = page.contents.get(identifier=args)
|
13
12
|
except page.contents.model.DoesNotExist:
|
14
13
|
content, _ = page.contents.get_or_create(identifier=args)
|
15
|
-
if curr_lang not in content.translations.all_languages():
|
16
|
-
content.translate(curr_lang)
|
17
|
-
content.save()
|
18
14
|
return content
|
19
15
|
|
20
16
|
|
@@ -22,3 +18,8 @@ def filter_content(page, args):
|
|
22
18
|
def alternate_urls(page, request):
|
23
19
|
alternates = page.alternate_urls(request)
|
24
20
|
return alternates.get("alternate_urls", alternates).items()
|
21
|
+
|
22
|
+
|
23
|
+
@register.filter(name="strip_lang")
|
24
|
+
def strip_lang(value, lang=get_language()):
|
25
|
+
return "/%s" % value.lstrip("/%s/" % lang)
|
@@ -0,0 +1,37 @@
|
|
1
|
+
from django import template
|
2
|
+
from camomilla.models import Menu
|
3
|
+
|
4
|
+
from camomilla.models.menu import MenuNode
|
5
|
+
|
6
|
+
register = template.Library()
|
7
|
+
|
8
|
+
|
9
|
+
@register.simple_tag(takes_context=True)
|
10
|
+
def get_menus(context, *args):
|
11
|
+
if "menus" in context:
|
12
|
+
return context["menus"]
|
13
|
+
qs = Menu.objects.all()
|
14
|
+
if len(args):
|
15
|
+
qs = qs.filter(key__in=args)
|
16
|
+
return Menu.defaultdict({m.key: m for m in qs})
|
17
|
+
|
18
|
+
|
19
|
+
@register.simple_tag(takes_context=True)
|
20
|
+
def render_menu(
|
21
|
+
context: template.RequestContext,
|
22
|
+
menu_key: str,
|
23
|
+
template_path: str = "defaults/parts/menu.html",
|
24
|
+
):
|
25
|
+
if context is not None and not isinstance(context, dict):
|
26
|
+
context = context.__dict__
|
27
|
+
|
28
|
+
return context.get("menus", Menu.defaultdict())[menu_key].render(
|
29
|
+
template_path=template_path,
|
30
|
+
context=context,
|
31
|
+
request=context.get("request", None),
|
32
|
+
)
|
33
|
+
|
34
|
+
|
35
|
+
@register.filter(name="node_url")
|
36
|
+
def get_menu_node_url(node: MenuNode):
|
37
|
+
return node.link.url
|
@@ -0,0 +1,77 @@
|
|
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(
|
46
|
+
f"{indent} {json.dumps(item, default=custom_json_serializer)},"
|
47
|
+
)
|
48
|
+
result.append(f"{indent}],")
|
49
|
+
|
50
|
+
else:
|
51
|
+
result.append(
|
52
|
+
f"{indent}'{key}': {json.dumps(value, default=custom_json_serializer)},"
|
53
|
+
)
|
54
|
+
|
55
|
+
return "\n".join(result).rstrip(",")
|
56
|
+
|
57
|
+
|
58
|
+
@register.filter
|
59
|
+
def to_pretty_dict(instance):
|
60
|
+
data = model_to_dict(instance)
|
61
|
+
|
62
|
+
for key, value in data.items():
|
63
|
+
if isinstance(value, BaseModel):
|
64
|
+
data[key] = value.model_dump()
|
65
|
+
|
66
|
+
formatted_dict = "{\n" + pretty_dict(data, indent_level=1) + "\n}"
|
67
|
+
|
68
|
+
escaped = html.escape(formatted_dict)
|
69
|
+
|
70
|
+
# This to highlight the keys in the JSON
|
71
|
+
highlighted = re.sub(
|
72
|
+
r"(')([^&#]+?)('):",
|
73
|
+
r"<span style='color:#df3079'>'\2'</span>:",
|
74
|
+
escaped,
|
75
|
+
)
|
76
|
+
|
77
|
+
return mark_safe(f"<pre>{highlighted}</pre>")
|
camomilla/theme/__init__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__ = "
|
1
|
+
__version__ = "6.0.0"
|