django-camomilla-cms 6.0.0b18__py2.py3-none-any.whl → 6.1.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 CHANGED
@@ -1,4 +1,4 @@
1
- __version__ = "6.0.0-beta.18"
1
+ __version__ = "6.1.0"
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,17 +23,13 @@ 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
31
29
  from camomilla.templates_context.rendering import ctx_registry
32
30
  from django.conf import settings as django_settings
33
31
  from modeltranslation.utils import build_localized_fieldname
34
-
35
-
36
- def GET_TEMPLATE_CHOICES():
37
- return [(t, t) for t in get_all_templates_files()]
32
+ from django.utils.module_loading import import_string
38
33
 
39
34
 
40
35
  class UrlRedirect(models.Model):
@@ -233,7 +228,7 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
233
228
  date_created = models.DateTimeField(auto_now_add=True)
234
229
  date_updated_at = models.DateTimeField(auto_now=True)
235
230
  breadcrumbs_title = models.CharField(max_length=128, null=True, blank=True)
236
- template = models.CharField(max_length=500, null=True, blank=True, choices=[])
231
+ template = models.CharField(max_length=500, null=True, blank=True)
237
232
  template_data = models.JSONField(default=dict, null=False, blank=True)
238
233
  ordering = models.PositiveIntegerField(default=0, blank=False, null=False)
239
234
  parent_page = models.ForeignKey(
@@ -276,7 +271,6 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
276
271
 
277
272
  def __init__(self, *args, **kwargs):
278
273
  super(AbstractPage, self).__init__(*args, **kwargs)
279
- self._meta.get_field("template").choices = lazy(GET_TEMPLATE_CHOICES, list)()
280
274
 
281
275
  def __str__(self) -> str:
282
276
  return "(%s) %s" % (self.__class__.__name__, self.title or self.permalink)
@@ -294,6 +288,22 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
294
288
  context.update(new_ctx)
295
289
  return ctx_registry.get_context_for_page(self, request, super_ctx=context)
296
290
 
291
+ @classmethod
292
+ def get_serializer(cls):
293
+ from camomilla.serializers.mixins import AbstractPageMixin
294
+
295
+ standard_serializer = (
296
+ pointed_getter(cls, "_page_meta.standard_serializer")
297
+ or settings.PAGES_DEFAULT_SERIALIZER
298
+ )
299
+ if isinstance(standard_serializer, str):
300
+ standard_serializer = import_string(standard_serializer)
301
+ if not issubclass(standard_serializer, AbstractPageMixin):
302
+ raise ValueError(
303
+ f"Standard serializer {standard_serializer} must be a subclass of AbstractPageMixin"
304
+ )
305
+ return standard_serializer
306
+
297
307
  @property
298
308
  def model_name(self) -> str:
299
309
  return self._meta.app_label + "." + self._meta.model_name
@@ -363,7 +373,8 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
363
373
  def generate_permalink(self, safe: bool = True) -> str:
364
374
  permalink = f"/{slugify(self.title or '', allow_unicode=True)}"
365
375
  if self.parent:
366
- permalink = f"/{self.parent.permalink}{permalink}"
376
+ parent_permalink = (self.parent.permalink or "").lstrip("/")
377
+ permalink = f"/{parent_permalink}{permalink}"
367
378
  set_nofallbacks(self, "permalink", permalink)
368
379
  qs = UrlNode.objects.exclude(pk=getattr(self.url_node or object, "pk", None))
369
380
  if safe and qs.filter(permalink=permalink).exists():
@@ -462,7 +473,7 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
462
473
  preview = request and getattr(request, "GET", {}).get("preview", False)
463
474
  permalinks = get_field_translations(self.url_node or object, "permalink", None)
464
475
  for lang in activate_languages():
465
- if lang in permalinks:
476
+ if lang in permalinks and permalinks[lang]:
466
477
  permalinks[lang] = (
467
478
  UrlNode.reverse_url(permalinks[lang])
468
479
  if preview or self.is_public
@@ -482,6 +493,7 @@ class AbstractPage(SeoMixin, MetaMixin, models.Model, metaclass=PageBase):
482
493
  parent_page_field = "parent_page"
483
494
  default_template = settings.PAGE_DEFAULT_TEMPLATE
484
495
  inject_context_func = settings.PAGE_INJECT_CONTEXT_FUNC
496
+ standard_serializer = settings.PAGES_DEFAULT_SERIALIZER
485
497
 
486
498
 
487
499
  class Page(AbstractPage):
@@ -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:
@@ -22,7 +22,7 @@ class PageSerializer(AbstractPageMixin, BaseModelSerializer):
22
22
  fields = "__all__"
23
23
 
24
24
 
25
- class BasicUrlNodeSerializer(BaseModelSerializer):
25
+ class UrlNodeSerializer(BaseModelSerializer):
26
26
  is_public = serializers.SerializerMethodField()
27
27
  status = serializers.SerializerMethodField()
28
28
  indexable = serializers.SerializerMethodField()
@@ -41,17 +41,18 @@ class BasicUrlNodeSerializer(BaseModelSerializer):
41
41
  return instance.page.indexable
42
42
 
43
43
 
44
- class UrlNodeSerializer(BasicUrlNodeSerializer):
44
+ class RouteSerializer(UrlNodeSerializer):
45
45
  alternates = serializers.SerializerMethodField()
46
46
 
47
47
  def get_alternates(self, instance: UrlNode):
48
48
  return instance.page.alternate_urls()
49
49
 
50
50
  def to_representation(self, instance: UrlNode):
51
+ standard_serializer = instance.page.get_serializer()
51
52
  model_serializer = build_standard_model_serializer(
52
53
  instance.page.__class__,
53
54
  depth=10,
54
- bases=(AbstractPageMixin,) + get_standard_bases(),
55
+ bases=(standard_serializer,) + get_standard_bases(),
55
56
  )
56
57
  return {
57
58
  **super().to_representation(instance),
camomilla/settings.py CHANGED
@@ -49,6 +49,12 @@ PAGE_INJECT_CONTEXT_FUNC = pointed_getter(
49
49
  django_settings, "CAMOMILLA.RENDER.PAGE.INJECT_CONTEXT", None
50
50
  )
51
51
 
52
+ PAGES_DEFAULT_SERIALIZER = pointed_getter(
53
+ django_settings,
54
+ "CAMOMILLA.API.PAGES.DEFAULT_SERIALIZER",
55
+ "camomilla.serializers.mixins.AbstractPageMixin",
56
+ )
57
+
52
58
  ENABLE_TRANSLATIONS = (
53
59
  ENABLE_REGISTRATIONS and "modeltranslation" in django_settings.INSTALLED_APPS
54
60
  )
@@ -88,8 +94,15 @@ API_TRANSLATION_ACCESSOR = pointed_getter(
88
94
  )
89
95
 
90
96
  REGISTERED_TEMPLATES_APPS = pointed_getter(
91
- django_settings,
92
- "CAMOMILLA.RENDER.REGISTERED_TEMPLATES_APPS", None
97
+ django_settings, "CAMOMILLA.RENDER.REGISTERED_TEMPLATES_APPS", None
98
+ )
99
+
100
+ INTEGRATIONS_ASTRO_ENABLE = pointed_getter(
101
+ django_settings, "CAMOMILLA.INTEGRATIONS.ASTRO.ENABLE", False
102
+ )
103
+
104
+ INTEGRATIONS_ASTRO_URL = pointed_getter(
105
+ django_settings, "CAMOMILLA.INTEGRATIONS.ASTRO.URL", ""
93
106
  )
94
107
 
95
108
  DEBUG = pointed_getter(django_settings, "CAMOMILLA.DEBUG", django_settings.DEBUG)
@@ -114,6 +127,12 @@ DEBUG = pointed_getter(django_settings, "CAMOMILLA.DEBUG", django_settings.DEBUG
114
127
  # "STRUCTURED_FIELD": {
115
128
  # "CACHE_ENABLED": True
116
129
  # }
117
- # "API": {"NESTING_DEPTH": 10, "TRANSLATION_ACCESSOR": "translations"},
130
+ # "INTEGRATIONS": {
131
+ # "ASTRO": {
132
+ # "ENABLE": True,
133
+ # "URL": "http://localhost:4321"
134
+ # }
135
+ # }
136
+ # "API": {"NESTING_DEPTH": 10, "TRANSLATION_ACCESSOR": "translations", "PAGES": {"DEFAULT_SERIALIZER": "camomilla.serializers.page.RouteSerializer"}},
118
137
  # "DEBUG": False
119
138
  # }
@@ -42,13 +42,17 @@ def pretty_dict(data, indent_level=0):
42
42
  if isinstance(item, dict):
43
43
  result.append(pretty_dict(item, indent_level + 1))
44
44
  else:
45
- result.append(f"{indent} {json.dumps(item, default=custom_json_serializer)},")
45
+ result.append(
46
+ f"{indent} {json.dumps(item, default=custom_json_serializer)},"
47
+ )
46
48
  result.append(f"{indent}],")
47
49
 
48
50
  else:
49
- result.append(f"{indent}'{key}': {json.dumps(value, default=custom_json_serializer)},")
51
+ result.append(
52
+ f"{indent}'{key}': {json.dumps(value, default=custom_json_serializer)},"
53
+ )
50
54
 
51
- return "\n".join(result).rstrip(',')
55
+ return "\n".join(result).rstrip(",")
52
56
 
53
57
 
54
58
  @register.filter
@@ -67,7 +71,7 @@ def to_pretty_dict(instance):
67
71
  highlighted = re.sub(
68
72
  r"(')([^&#]+?)('):",
69
73
  r"<span style='color:#df3079'>'\2'</span>:",
70
- escaped
74
+ escaped,
71
75
  )
72
76
 
73
77
  return mark_safe(f"<pre>{highlighted}</pre>")
@@ -1 +1 @@
1
- __version__ = "6.0.0-beta.18"
1
+ __version__ = "6.1.0"
@@ -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/urls.py CHANGED
@@ -19,7 +19,7 @@ from camomilla.views import (
19
19
  UserViewSet,
20
20
  MenuViewSet,
21
21
  )
22
- from camomilla.views.pages import fetch_page
22
+ from camomilla.views.pages import pages_router
23
23
  from camomilla.redirects import url_patterns as old_redirects
24
24
 
25
25
  router = routers.DefaultRouter()
@@ -37,8 +37,8 @@ router.register(r"menus", MenuViewSet, "camomilla-menus")
37
37
  urlpatterns = [
38
38
  *old_redirects,
39
39
  path("", include(router.urls)),
40
- path("pages-router/", fetch_page),
41
- path("pages-router/<path:permalink>", fetch_page),
40
+ path("pages-router/", pages_router),
41
+ path("pages-router/<path:permalink>", pages_router),
42
42
  path("token-auth/", CamomillaObtainAuthToken.as_view(), name="api_token"),
43
43
  path("auth/login/", CamomillaAuthLogin.as_view(), name="login"),
44
44
  path("auth/logout/", CamomillaAuthLogout.as_view(), name="logout"),
@@ -1,33 +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
6
- from camomilla.settings import REGISTERED_TEMPLATES_APPS
7
+ from camomilla.settings import (
8
+ REGISTERED_TEMPLATES_APPS,
9
+ INTEGRATIONS_ASTRO_ENABLE,
10
+ INTEGRATIONS_ASTRO_URL,
11
+ )
7
12
 
8
13
 
9
- def get_all_templates_files() -> Sequence[str]:
14
+ def get_templates(request=None) -> Sequence[str]:
10
15
  files = []
11
16
 
12
17
  for engine in django_template.loader.engines.all():
13
18
 
14
19
  if REGISTERED_TEMPLATES_APPS:
15
20
  dirs = [
16
- d for d in engine.template_dirs
21
+ d
22
+ for d in engine.template_dirs
17
23
  if any(app in str(d) for app in REGISTERED_TEMPLATES_APPS)
18
24
  ]
19
25
  else:
20
26
  # Exclude pip installed site package template dirs
21
27
  dirs = [
22
- d for d in engine.template_dirs
28
+ d
29
+ for d in engine.template_dirs
23
30
  if "site-packages" not in str(d) or "camomilla" in str(d)
24
31
  ]
25
32
 
26
33
  for d in dirs:
27
34
  base = Path(d)
28
- files.extend(
29
- relpath(f, d)
30
- for f in base.rglob("*.html")
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
+ },
31
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
32
52
 
33
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
 
camomilla/views/menus.py CHANGED
@@ -9,7 +9,7 @@ from camomilla.models import AbstractPage, Menu
9
9
  from camomilla.models.page import UrlNode
10
10
  from camomilla.permissions import CamomillaBasePermissions
11
11
  from camomilla.serializers import ContentTypeSerializer, MenuSerializer
12
- from camomilla.serializers.page import BasicUrlNodeSerializer
12
+ from camomilla.serializers.page import UrlNodeSerializer
13
13
  from camomilla.views.base import BaseModelViewset
14
14
  from camomilla.views.decorators import active_lang
15
15
 
@@ -66,7 +66,12 @@ class MenuViewSet(BaseModelViewset):
66
66
  raise Http404("No object matches the given query.")
67
67
  return Response(
68
68
  [
69
- {"id": obj.pk, "name": str(obj), "url_node_id": obj.url_node.pk}
69
+ {
70
+ "id": obj.pk,
71
+ "name": str(obj),
72
+ "url_node_id": obj.url_node.pk,
73
+ "model": f"{content_type.app_label}.{content_type.model}",
74
+ }
70
75
  for obj in content_type.model_class().objects.exclude(
71
76
  url_node__isnull=True
72
77
  )
@@ -78,4 +83,4 @@ class MenuViewSet(BaseModelViewset):
78
83
  def search_urlnode(self, request, *args, **kwargs):
79
84
  url_node = request.GET.get("q", "")
80
85
  qs = UrlNode.objects.filter(permalink__icontains=url_node).order_by("permalink")
81
- return Response(BasicUrlNodeSerializer(qs, many=True).data)
86
+ return Response(UrlNodeSerializer(qs, many=True).data)
@@ -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(
camomilla/views/pages.py CHANGED
@@ -2,7 +2,7 @@ from camomilla.models import Page
2
2
  from camomilla.models.page import UrlNode, UrlRedirect
3
3
  from camomilla.permissions import CamomillaBasePermissions
4
4
  from camomilla.serializers import PageSerializer
5
- from camomilla.serializers.page import UrlNodeSerializer
5
+ from camomilla.serializers.page import RouteSerializer
6
6
  from camomilla.views.base import BaseModelViewset
7
7
  from camomilla.views.decorators import active_lang
8
8
  from camomilla.views.mixins import BulkDeleteMixin, GetUserLanguageMixin
@@ -26,10 +26,10 @@ class PageViewSet(GetUserLanguageMixin, BulkDeleteMixin, BaseModelViewset):
26
26
  permissions.AllowAny,
27
27
  ]
28
28
  )
29
- def fetch_page(request, permalink=""):
29
+ def pages_router(request, permalink=""):
30
30
  redirect = UrlRedirect.find_redirect_from_url(f"/{permalink}")
31
31
  if redirect:
32
32
  redirect = redirect.redirect()
33
33
  return Response({"redirect": redirect.url, "status": redirect.status_code})
34
- node = get_object_or_404(UrlNode, permalink=f"/{permalink}")
35
- return Response(UrlNodeSerializer(node, context={"request": request}).data)
34
+ node: UrlNode = get_object_or_404(UrlNode, permalink=f"/{permalink}")
35
+ return Response(RouteSerializer(node, context={"request": request}).data)
@@ -0,0 +1,133 @@
1
+ Metadata-Version: 2.4
2
+ Name: django-camomilla-cms
3
+ Version: 6.1.0
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.