django-camomilla-cms 6.0.0b17__py2.py3-none-any.whl → 6.0.1__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 CHANGED
@@ -1,4 +1,4 @@
1
- __version__ = "6.0.0-beta.17"
1
+ __version__ = "6.0.1"
2
2
 
3
3
 
4
4
  def get_core_apps():
camomilla/models/page.py CHANGED
@@ -10,7 +10,6 @@ from django.http import Http404, HttpRequest
10
10
  from django.shortcuts import redirect
11
11
  from django.urls import NoReverseMatch, reverse
12
12
  from django.utils import timezone
13
- from django.utils.functional import lazy
14
13
  from django.utils.text import slugify
15
14
  from django.utils.translation import gettext_lazy as _
16
15
  from django.utils.translation import get_language
@@ -24,7 +23,6 @@ from camomilla.utils import (
24
23
  lang_fallback_query,
25
24
  set_nofallbacks,
26
25
  url_lang_decompose,
27
- get_all_templates_files,
28
26
  )
29
27
  from camomilla.utils.getters import pointed_getter
30
28
  from camomilla import settings
@@ -33,10 +31,6 @@ from django.conf import settings as django_settings
33
31
  from modeltranslation.utils import build_localized_fieldname
34
32
 
35
33
 
36
- def GET_TEMPLATE_CHOICES():
37
- return [(t, t) for t in get_all_templates_files()]
38
-
39
-
40
34
  class UrlRedirect(models.Model):
41
35
  language_code = models.CharField(max_length=10, null=True)
42
36
  from_url = models.CharField(max_length=400)
@@ -233,7 +227,7 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
233
227
  date_created = models.DateTimeField(auto_now_add=True)
234
228
  date_updated_at = models.DateTimeField(auto_now=True)
235
229
  breadcrumbs_title = models.CharField(max_length=128, null=True, blank=True)
236
- template = models.CharField(max_length=500, null=True, blank=True, choices=[])
230
+ template = models.CharField(max_length=500, null=True, blank=True)
237
231
  template_data = models.JSONField(default=dict, null=False, blank=True)
238
232
  ordering = models.PositiveIntegerField(default=0, blank=False, null=False)
239
233
  parent_page = models.ForeignKey(
@@ -276,7 +270,6 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
276
270
 
277
271
  def __init__(self, *args, **kwargs):
278
272
  super(AbstractPage, self).__init__(*args, **kwargs)
279
- self._meta.get_field("template").choices = lazy(GET_TEMPLATE_CHOICES, list)()
280
273
 
281
274
  def __str__(self) -> str:
282
275
  return "(%s) %s" % (self.__class__.__name__, self.title or self.permalink)
@@ -9,18 +9,22 @@ from camomilla.serializers.mixins import (
9
9
  FieldsOverrideMixin,
10
10
  TranslationsMixin,
11
11
  )
12
+ from camomilla.settings import ENABLE_TRANSLATIONS
12
13
 
13
-
14
- class BaseModelSerializer(
14
+ bases = (
15
15
  SetupEagerLoadingMixin,
16
16
  NestMixin,
17
17
  FilterFieldsMixin,
18
18
  FieldsOverrideMixin,
19
19
  JSONFieldPatchMixin,
20
20
  OrderingMixin,
21
- TranslationsMixin,
22
- serializers.ModelSerializer,
23
- ):
21
+ )
22
+
23
+ if ENABLE_TRANSLATIONS:
24
+ bases += (TranslationsMixin,)
25
+
26
+
27
+ class BaseModelSerializer(*bases, serializers.ModelSerializer):
24
28
  """
25
29
  This is the base serializer for all the models.
26
30
  It adds support for:
camomilla/settings.py CHANGED
@@ -87,6 +87,18 @@ API_TRANSLATION_ACCESSOR = pointed_getter(
87
87
  django_settings, "CAMOMILLA.API.TRANSLATION_ACCESSOR", "translations"
88
88
  )
89
89
 
90
+ REGISTERED_TEMPLATES_APPS = pointed_getter(
91
+ django_settings, "CAMOMILLA.RENDER.REGISTERED_TEMPLATES_APPS", None
92
+ )
93
+
94
+ INTEGRATIONS_ASTRO_ENABLE = pointed_getter(
95
+ django_settings, "CAMOMILLA.INTEGRATIONS.ASTRO.ENABLE", False
96
+ )
97
+
98
+ INTEGRATIONS_ASTRO_URL = pointed_getter(
99
+ django_settings, "CAMOMILLA.INTEGRATIONS.ASTRO.URL", ""
100
+ )
101
+
90
102
  DEBUG = pointed_getter(django_settings, "CAMOMILLA.DEBUG", django_settings.DEBUG)
91
103
 
92
104
  # camomilla settings example
@@ -104,10 +116,17 @@ DEBUG = pointed_getter(django_settings, "CAMOMILLA.DEBUG", django_settings.DEBUG
104
116
  # "AUTO_CREATE_HOMEPAGE": True,
105
117
  # "ARTICLE": {"DEFAULT_TEMPLATE": "", "INJECT_CONTEXT": None },
106
118
  # "PAGE": {"DEFAULT_TEMPLATE": "", "INJECT_CONTEXT": None }
119
+ # "REGISTERED_TEMPLATE_APPS": []
107
120
  # },
108
121
  # "STRUCTURED_FIELD": {
109
122
  # "CACHE_ENABLED": True
110
123
  # }
124
+ # "INTEGRATIONS": {
125
+ # "ASTRO": {
126
+ # "ENABLE": True,
127
+ # "URL": "http://localhost:4321"
128
+ # }
129
+ # }
111
130
  # "API": {"NESTING_DEPTH": 10, "TRANSLATION_ACCESSOR": "translations"},
112
131
  # "DEBUG": False
113
132
  # }
@@ -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>
@@ -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"(&#x27;)([^&#]+?)(&#x27;):",
73
+ r"<span style='color:#df3079'>'\2'</span>:",
74
+ escaped,
75
+ )
76
+
77
+ return mark_safe(f"<pre>{highlighted}</pre>")
@@ -1 +1 @@
1
- __version__ = "6.0.0-beta.17"
1
+ __version__ = "6.0.1"
@@ -3,6 +3,8 @@ from camomilla import settings
3
3
  from .translations import TranslationAwareModelAdmin
4
4
  from camomilla.models import UrlNode
5
5
 
6
+ from camomilla.utils import get_templates
7
+
6
8
 
7
9
  class AbstractPageModelFormMeta(forms.models.ModelFormMetaclass):
8
10
  def __new__(mcs, name, bases, attrs):
@@ -22,6 +24,12 @@ class AbstractPageModelFormMeta(forms.models.ModelFormMetaclass):
22
24
  class AbstractPageModelForm(
23
25
  forms.models.BaseModelForm, metaclass=AbstractPageModelFormMeta
24
26
  ):
27
+ def __init__(self, *args, **kwargs):
28
+ request = kwargs.pop("request", None)
29
+ super().__init__(*args, **kwargs)
30
+ templates = [(t, t) for t in get_templates(request)]
31
+ templates.insert(0, ("", "---------"))
32
+ self.fields["template"] = forms.ChoiceField(choices=templates)
25
33
 
26
34
  def get_initial_for_field(self, field, field_name):
27
35
  if field_name in UrlNode.LANG_PERMALINK_FIELDS:
@@ -43,4 +51,16 @@ class AbstractPageModelForm(
43
51
 
44
52
  class AbstractPageAdmin(TranslationAwareModelAdmin):
45
53
  form = AbstractPageModelForm
54
+
55
+ def get_form(self, request, obj=None, **kwargs):
56
+ kwargs["form"] = self.form
57
+ form = super().get_form(request, obj, **kwargs)
58
+
59
+ class FormWithRequest(form):
60
+ def __new__(cls, *args, **kwargs_):
61
+ kwargs_["request"] = request
62
+ return form(*args, **kwargs_)
63
+
64
+ return FormWithRequest
65
+
46
66
  change_form_template = "admin/camomilla/page/change_form.html"
camomilla/theme/apps.py CHANGED
@@ -34,4 +34,5 @@ class CamomillaThemeConfig(AppConfig):
34
34
  "django_jsonform",
35
35
  "admin_interface",
36
36
  "colorfield",
37
+ "structured",
37
38
  )
@@ -1,20 +1,53 @@
1
1
  from pathlib import Path
2
2
  from typing import Sequence
3
+ import requests
3
4
 
4
5
  from django import template as django_template
5
6
  from os.path import relpath
7
+ from camomilla.settings import (
8
+ REGISTERED_TEMPLATES_APPS,
9
+ INTEGRATIONS_ASTRO_ENABLE,
10
+ INTEGRATIONS_ASTRO_URL,
11
+ )
6
12
 
7
13
 
8
- def get_all_templates_files() -> Sequence[str]:
9
- dirs = []
10
- for engine in django_template.loader.engines.all():
11
- # Exclude pip installed site package template dirs
12
- dirs.extend(
13
- x
14
- for x in engine.template_dirs
15
- if "site-packages" not in str(x) or "camomilla" in str(x)
16
- )
14
+ def get_templates(request=None) -> Sequence[str]:
17
15
  files = []
18
- for dir in dirs:
19
- files.extend(relpath(x, dir) for x in Path(dir).glob("**/*.html") if x)
16
+
17
+ for engine in django_template.loader.engines.all():
18
+
19
+ if REGISTERED_TEMPLATES_APPS:
20
+ dirs = [
21
+ d
22
+ for d in engine.template_dirs
23
+ if any(app in str(d) for app in REGISTERED_TEMPLATES_APPS)
24
+ ]
25
+ else:
26
+ # Exclude pip installed site package template dirs
27
+ dirs = [
28
+ d
29
+ for d in engine.template_dirs
30
+ if "site-packages" not in str(d) or "camomilla" in str(d)
31
+ ]
32
+
33
+ for d in dirs:
34
+ base = Path(d)
35
+ files.extend(relpath(f, d) for f in base.rglob("*.html"))
36
+
37
+ if INTEGRATIONS_ASTRO_ENABLE and request is not None:
38
+ try:
39
+ response = requests.get(
40
+ INTEGRATIONS_ASTRO_URL + "/api/templates",
41
+ cookies={
42
+ "sessionid": request.COOKIES.get("sessionid"),
43
+ "csrftoken": request.COOKIES.get("csrftoken"),
44
+ },
45
+ )
46
+ if response.status_code == 200:
47
+ astro_templates = response.json()
48
+ for template in astro_templates:
49
+ files.append(template)
50
+ except:
51
+ pass
52
+
20
53
  return files
@@ -4,6 +4,8 @@ from ..mixins import (
4
4
  OrderingMixin,
5
5
  CamomillaBasePermissionMixin,
6
6
  )
7
+ from camomilla.serializers.mixins import TranslationsMixin
8
+ from camomilla.utils.translation import plain_to_nest
7
9
  from rest_framework import viewsets
8
10
  from rest_framework.metadata import SimpleMetadata
9
11
  from structured.contrib.restframework import StructuredJSONField
@@ -29,8 +31,8 @@ class BaseViewMetadata(SimpleMetadata):
29
31
 
30
32
  def get_serializer_info(self, serializer):
31
33
  info = super().get_serializer_info(serializer)
32
- if hasattr(serializer, "plain_to_nest"):
33
- info.update(serializer.plain_to_nest(info))
34
+ if isinstance(serializer, TranslationsMixin) and serializer.is_translatable:
35
+ info.update(plain_to_nest(info, serializer.translation_fields))
34
36
  return info
35
37
 
36
38
 
@@ -86,7 +86,7 @@ class PaginateStackMixin:
86
86
  if "sqlite" in settings.DATABASES["default"]["ENGINE"]:
87
87
  filter_statement = Q()
88
88
  for field in search_fields:
89
- filter_statement |= Q(**{field + '__icontains': search_string})
89
+ filter_statement |= Q(**{field + "__icontains": search_string})
90
90
  return list_handler.filter(filter_statement)
91
91
  else:
92
92
  return list_handler.annotate(
@@ -0,0 +1,133 @@
1
+ Metadata-Version: 2.4
2
+ Name: django-camomilla-cms
3
+ Version: 6.0.1
4
+ Summary: Django powered cms
5
+ Author-email: Lotrèk <dimmitutto@lotrek.it>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/camomillacms/camomilla-core
8
+ Keywords: cms,django,api cms
9
+ Classifier: Environment :: Web Environment
10
+ Classifier: Framework :: Django
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python
13
+ Classifier: Programming Language :: Python :: 3
14
+ Requires-Python: <=3.13,>=3.8
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: django-modeltranslation<=0.18.12,>=0.18.7
18
+ Requires-Dist: djsuperadmin<1.0.0,>=0.9
19
+ Requires-Dist: djangorestframework<=3.14.0,>=3.10.0
20
+ Requires-Dist: django-structured-json-field>=0.4.2
21
+ Requires-Dist: Pillow>=10.0.0
22
+ Requires-Dist: django-admin-interface<1.0.0,>=0.26.0
23
+ Requires-Dist: django-ckeditor<7.0.0,>=5.7.1
24
+ Requires-Dist: django-tinymce<5.0.0,>=4.1.0
25
+ Requires-Dist: python-magic<0.5,>=0.4
26
+ Requires-Dist: Django<6,>=3.2
27
+ Requires-Dist: django_jsonform>=2.23
28
+ Requires-Dist: inflection>=0.5.1
29
+ Requires-Dist: uritemplate>=4.1.0
30
+ Dynamic: license-file
31
+
32
+ [![PyPI](https://img.shields.io/pypi/v/django-camomilla-cms?style=flat-square)](https://pypi.org/project/django-camomilla-cms)
33
+ [![Django Versions](https://img.shields.io/badge/django-3.2%20%7C%204.2%20%7C%205.1-blue?style=flat-square)](https://www.djangoproject.com/)
34
+ [![Build](https://img.shields.io/github/actions/workflow/status/camomillacms/camomilla-core/ci.yml?branch=master&style=flat-square)](https://github.com/camomillacms/camomilla-core/actions)
35
+ [![Last Commit](https://img.shields.io/github/last-commit/camomillacms/camomilla-core?style=flat-square)](https://github.com/camomillacms/camomilla-core/commits/master)
36
+ [![Contributors](https://img.shields.io/github/contributors/camomillacms/camomilla-core?style=flat-square)](https://github.com/camomillacms/camomilla-core/graphs/contributors)
37
+ [![Open Issues](https://img.shields.io/github/issues/camomillacms/camomilla-core?style=flat-square)](https://github.com/camomillacms/camomilla-core/issues)
38
+ [![Codecov](https://img.shields.io/codecov/c/github/camomillacms/camomilla-core?style=flat-square)](https://app.codecov.io/gh/camomillacms/camomilla-core/tree/master/camomilla)
39
+ [![License](https://img.shields.io/github/license/camomillacms/camomilla-core?style=flat-square)](./LICENSE)
40
+
41
+
42
+ <br>
43
+ <br>
44
+ <br>
45
+ <br>
46
+ <div align="center">
47
+ <picture>
48
+ <source media="(prefers-color-scheme: dark)" srcset="https://camomillacms.github.io/camomilla-core/images/camomilla-logo-dark.svg?v=1">
49
+ <source media="(prefers-color-scheme: light)" srcset="https://camomillacms.github.io/camomilla-core/images/camomilla-logo-light.svg?v=1">
50
+ <img alt="Fallback image description" src="https://camomillacms.github.io/camomilla-core/images/camomilla-logo-light.svg?v=1" style="width: 250px; height: auto;">
51
+ </picture>
52
+ </div>
53
+ <h3 align="center"">Our beloved Django CMS</h3>
54
+ <br>
55
+
56
+ ## ⭐️ Features
57
+
58
+ <!-- Highlight some of the features your module provide here -->
59
+
60
+ - 🧘‍♀️ &nbsp;Built on top of the django framework
61
+ - 🥨 &nbsp;Beaked page abstract model to let you manage everything you need as a page.
62
+ - 🏞️ &nbsp;Optimized media management with autoresize
63
+ - 👯 &nbsp;Enable relations inside django JSONFields
64
+ - ⚡️ &nbsp;AutoCreate api endpoints from models
65
+ - 🚧 &nbsp;Enable JsonSchema directly in models endpoints
66
+
67
+ Camomilla is a Django CMS that allows you to create and manage your website's content with ease. It provides a simple and intuitive interface for managing pages, media, and other content types. Camomilla is built on top of the Django framework, which means it inherits all the features and benefits of Django framework.
68
+ We try to continuously improve Camomilla by adding new features and fixing bugs. You can check the [CHANGELOG](./CHANGELOG.md) to see what has been added in the latest releases.
69
+
70
+ ## 📦 Quick Start
71
+
72
+ Here you can find some quick setup instructions to get started with Camomilla. For more detailed information, please refer to the [documentation](https://camomillacms.github.io/camomilla-core/).
73
+
74
+ > [!TIP]
75
+ >
76
+ > #### Env Virtualization 👾
77
+ >
78
+ > Use a virtualenv to isolate your project's dependencies from the system's python installation before starting. Check out [virtualenvwrapper](https://virtualenvwrapper.readthedocs.io/en/latest/) for more information.
79
+
80
+ Install django-camomilla-cms and django from pip
81
+
82
+ ```bash
83
+ $ pip install django
84
+ $ pip install django-camomilla-cms==6.0.0
85
+ ```
86
+
87
+ Create a new django project
88
+
89
+ ```bash
90
+ $ django-admin startproject <project_name>
91
+ $ cd <project_name>
92
+ ```
93
+
94
+ Create a dedicated folder for camomilla migrations
95
+
96
+ ```bash
97
+ $ mkdir -p camomilla_migrations
98
+ $ touch camomilla_migrations.__init__.py
99
+ ```
100
+
101
+ Create migrations and prepare the database
102
+
103
+ ```bash
104
+ $ python manage.py makemigrations camomilla
105
+ $ python manage.py migrate
106
+ ```
107
+
108
+ Add camomilla and camomilla dependencies to your project's INSTALLED_APPS
109
+
110
+ ```python
111
+ # <project_name>/settings.py
112
+
113
+ INSTALLED_APPS = [
114
+ ...
115
+ 'camomilla', # always needed
116
+ 'camomilla.theme', # needed to customize admin interface
117
+ 'djsuperadmin', # needed if you whant to use djsuperadmin for contents
118
+ 'modeltranslation', # needed if your website is multilanguage (can be added later)
119
+ 'rest_framework', # always needed
120
+ 'rest_framework.authtoken', # always needed
121
+ ...
122
+ ]
123
+ ```
124
+
125
+ Run the server
126
+
127
+ ```bash
128
+ $ python manage.py runserver
129
+ ```
130
+
131
+ ## 🧑‍💻 How to Contribute
132
+
133
+ We welcome contributions to Camomilla! If you want to contribute, please read our [contributing guide](./CONTRIBUTING.md) for more information on how to get started.