diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 4bd8966..e4c05be 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -17,6 +17,5 @@ jobs: run: pip install --upgrade ruff - name: Run ruff run: | - ruff djangocms_text - ruff djangocms_text_ckeditor + ruff djangocms_rest ruff tests diff --git a/.github/workflows/publish-to-live-pypi.yml b/.github/workflows/publish-to-live-pypi.yml index b2da945..d2f8e94 100644 --- a/.github/workflows/publish-to-live-pypi.yml +++ b/.github/workflows/publish-to-live-pypi.yml @@ -27,13 +27,6 @@ jobs: pip install build --user - - uses: actions/setup-node@v4 - with: - node-version-file: '.nvmrc' - - name: Install dependencies - run: npm install - - name: Build client - run: webpack --mode production - name: Build a binary wheel and a source tarball run: >- python -m diff --git a/README.md b/README.md index 805e380..6de659f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,3 @@ -Sure, here's a basic `README.md` file for your Django CMS REST project: # Django CMS REST This is a demo project that provides RESTful APIs for Django CMS. diff --git a/djangocms_rest/serializers/pageserializer.py b/djangocms_rest/serializers/pageserializer.py index b84d294..adae8d9 100644 --- a/djangocms_rest/serializers/pageserializer.py +++ b/djangocms_rest/serializers/pageserializer.py @@ -8,37 +8,72 @@ class RESTPage: - def __init__(self, request: Request, page: Page, language: str | None = None) -> None: - host = f"{request.scheme}://{request.get_host()}/" + def __init__( + self, request: Request, page: Page, language: str | None = None + ) -> None: + host = f"{request.scheme}://{request.get_host()}" page_content = page.get_content_obj(language, fallback=True) - self.title: str = page_content.title if page_content else None - self.page_title: str = page_content.page_title or self.title if page_content else None - self.menu_title: str = page_content.menu_title or self.title if page_content else None - self.meta_description: str = page_content.meta_description if page_content else None - self.redirect: str = page_content.redirect if page_content else None - self.absolute_url: str = page.get_absolute_url(language) + if page_content: + self.title: str = page_content.title + self.page_title: str = page_content.page_title or self.title + self.menu_title: str = page_content.menu_title or self.title + self.meta_description: str = page_content.meta_description + self.redirect: str = page_content.redirect + placeholders = page.get_placeholders(language) + declared_slots = [ + placeholder.slot for placeholder in page.get_declared_placeholders() + ] + content_type_id = ContentType.objects.get_for_model( + page_content.__class__ + ).pk + self.placeholders: dict[str, str] = ( + { + placeholder.slot: host + + reverse( + "placeholder-detail", + args=( + language, + content_type_id, + page_content.pk, + placeholder.slot, + ), + ) + for placeholder in placeholders + if placeholder.slot in declared_slots + } + if page_content + else {} + ) + self.in_navigation: bool = page_content.in_navigation + self.soft_root: bool = page_content.soft_root + self.template: str = page_content.template + self.xframe_options: str = page_content.xframe_options + self.limit_visibility_in_menu: int = page_content.limit_visibility_in_menu + self.language: str = page_content.language + self.creation_date: datetime.datetime = page_content.creation_date + self.changed_date: datetime.datetime = page_content.changed_date + else: + self.title: str = "" + self.page_title: str = "" + self.menu_title: str = "" + self.meta_description: str = "" + self.redirect: str = "" + self.placeholders: dict[str, str] = {} - placeholders = page.get_placeholders(language) - declared_slots = [placeholder.slot for placeholder in page.get_declared_placeholders()] - content_type_id = ContentType.objects.get_for_model(page_content.__class__).pk - self.placeholders: dict[str, str] = { - placeholder.slot: host + reverse("placeholder-detail", args=( - language, content_type_id, page_content.pk, placeholder.slot - )) for placeholder in placeholders if placeholder.slot in declared_slots - } if page_content else {} - self.path:str = page.get_path(language) + self.in_navigation: bool = False + self.soft_root: bool = False + self.template: str = "" + self.xframe_options: str = "" + self.limit_visibility_in_menu: int = 0 + self.language: str = page_content.language + self.creation_date: datetime.datetime = page_content.creation_date + self.changed_date: datetime.datetime = page_content.changed_date - self.is_home: bool = page.is_home - self.in_navigation: bool = page_content.in_navigation if page_content else None - self.soft_root: bool = page_content.soft_root if page_content else None - self.template: bool = page_content.template if page_content else None - self.xframe_options: bool = page_content.xframe_options if page_content else None - self.limit_visibility_in_menu: bool = page_content.limit_visibility_in_menu if page_content else None + self.absolute_url: str = page.get_absolute_url(language) + self.path: str = page.get_path(language) - self.language: str = page_content.language if page_content else None + self.is_home: bool = page.is_home self.languages: list[str] = page.languages.split(",") - self.creation_date: datetime.datetime = page_content.creation_date if page_content else None - self.changed_date: datetime.datetime = page_content.changed_date if page_content else None super().__init__() @@ -63,4 +98,6 @@ class PageSerializer(serializers.Serializer): language = serializers.CharField(max_length=10) creation_date = serializers.DateTimeField() changed_date = serializers.DateTimeField() - languages = serializers.ListSerializer(child=serializers.CharField(), allow_empty=True, required=False) + languages = serializers.ListSerializer( + child=serializers.CharField(), allow_empty=True, required=False + ) diff --git a/djangocms_rest/serializers/placeholder.py b/djangocms_rest/serializers/placeholder.py index 3b10874..53bc70a 100644 --- a/djangocms_rest/serializers/placeholder.py +++ b/djangocms_rest/serializers/placeholder.py @@ -1,8 +1,11 @@ import time -from cms.cache.placeholder import _get_placeholder_cache_key, _get_placeholder_cache_version_key -from cms.models import Placeholder, CMSPlugin -from cms.plugin_rendering import ContentRenderer, BaseRenderer +from cms.cache.placeholder import ( + _get_placeholder_cache_key, + _get_placeholder_cache_version_key, +) +from cms.models import Placeholder +from cms.plugin_rendering import BaseRenderer from cms.utils.conf import get_cms_setting from cms.utils.plugins import get_plugins from django.contrib.sites.shortcuts import get_current_site @@ -25,11 +28,15 @@ def _get_placeholder_cache_version(placeholder, lang, site_id): else: version = int(time.time() * 1000000) vary_on_list = [] - _set_placeholder_cache_version(placeholder, lang, site_id, version, vary_on_list) + _set_placeholder_cache_version( + placeholder, lang, site_id, version, vary_on_list + ) return version, vary_on_list -def _set_placeholder_cache_version(placeholder, lang, site_id, version, vary_on_list=None, duration=None): +def _set_placeholder_cache_version( + placeholder, lang, site_id, version, vary_on_list=None, duration=None +): """ Sets the (placeholder x lang)'s version and vary-on header-names list. """ @@ -55,8 +62,8 @@ def set_placeholder_rest_cache(placeholder, lang, site_id, content, request): key = _get_placeholder_cache_key(placeholder, lang, site_id, request) + ":rest" duration = min( - get_cms_setting('CACHE_DURATIONS')['content'], - placeholder.get_cache_expiration(request, now()) + get_cms_setting("CACHE_DURATIONS")["content"], + placeholder.get_cache_expiration(request, now()), ) cache.set(key, content, duration) # "touch" the cache-version, so that it stays as fresh as this content. @@ -73,7 +80,10 @@ def get_placeholder_rest_cache(placeholder, lang, site_id, request): """ from django.core.cache import cache - key = _get_placeholder_cache_key(placeholder, lang, site_id, request, soft=True) + ":rest" + key = ( + _get_placeholder_cache_key(placeholder, lang, site_id, request, soft=True) + + ":rest" + ) content = cache.get(key) return content @@ -84,13 +94,14 @@ class PlaceholderRenderer(BaseRenderer): """ def placeholder_cache_is_enabled(self): - if not get_cms_setting('PLACEHOLDER_CACHE'): + if not get_cms_setting("PLACEHOLDER_CACHE"): return False if self.request.user.is_staff: return False return True def render_placeholder(self, placeholder, context, language, use_cache=False): + context.update({"request": self.request}) if use_cache and placeholder.cache_placeholder: use_cache = self.placeholder_cache_is_enabled() else: @@ -100,7 +111,7 @@ def render_placeholder(self, placeholder, context, language, use_cache=False): cached_value = get_placeholder_rest_cache( placeholder, lang=language, - site_id=get_current_site().pk, + site_id=get_current_site(self.request).pk, request=self.request, ) else: @@ -109,7 +120,7 @@ def render_placeholder(self, placeholder, context, language, use_cache=False): if cached_value is not None: # User has opted to use the cache # and there is something in the cache - return cached_value['content'] + return cached_value["content"] plugin_content = self.render_plugins( placeholder, @@ -121,7 +132,7 @@ def render_placeholder(self, placeholder, context, language, use_cache=False): set_placeholder_rest_cache( placeholder, lang=language, - site_id=get_current_site().pk, + site_id=get_current_site(self.request).pk, content=plugin_content, request=self.request, ) @@ -132,7 +143,9 @@ def render_placeholder(self, placeholder, context, language, use_cache=False): return plugin_content - def render_plugins(self, placeholder: Placeholder, language: str, context: dict) -> dict: + def render_plugins( + self, placeholder: Placeholder, language: str, context: dict + ) -> list: plugins = get_plugins( self.request, placeholder=placeholder, @@ -144,17 +157,21 @@ def render_children(plugins): for plugin in plugins: plugin_content = self.render_plugin(plugin, context) if getattr(plugin, "child_plugin_instances", None): - plugin_content["children"] = render_children(plugin.child_plugin_instances) + plugin_content["children"] = render_children( + plugin.child_plugin_instances + ) if plugin_content: yield plugin_content - return render_children(plugins) + return list(render_children(plugins)) def render_plugin(self, instance, context): instance, plugin = instance.get_plugin_instance() if not instance: return None - if hasattr(instance, "serializer") and issubclass(instance.serializer, serializers.Serializer): + if hasattr(instance, "serializer") and issubclass( + instance.serializer, serializers.Serializer + ): json = instance.serializer(instance, context=context).data if hasattr(instance, "serialize"): json = instance.serialize(context=context) @@ -162,7 +179,14 @@ def render_plugin(self, instance, context): class PluginSerializer(serializers.ModelSerializer): class Meta: model = instance.__class__ - fields = '__all__' + exclude = ( + "id", + "placeholder", + "position", + "creation_date", + "changed_date", + "parent", + ) json = PluginSerializer(instance, context=context).data return json @@ -170,9 +194,15 @@ class Meta: class PlaceholderSerializer(serializers.Serializer): slot = serializers.CharField() - content = serializers.ListSerializer(child=serializers.JSONField(), allow_empty=True, required=False) + label = serializers.CharField() + language = serializers.CharField() + content = serializers.ListSerializer( + child=serializers.JSONField(), allow_empty=True, required=False + ) - def __init__(self, request: Request, placeholder: Placeholder, language: str, *args, **kwargs): + def __init__( + self, request: Request, placeholder: Placeholder, language: str, *args, **kwargs + ): renderer = PlaceholderRenderer(request) placeholder.content = renderer.render_placeholder( placeholder, @@ -180,4 +210,6 @@ def __init__(self, request: Request, placeholder: Placeholder, language: str, *a language=language, use_cache=True, ) + placeholder.label = placeholder.get_label() + placeholder.language = language super().__init__(placeholder, *args, **kwargs) diff --git a/djangocms_rest/serializers/site.py b/djangocms_rest/serializers/site.py deleted file mode 100644 index ce42484..0000000 --- a/djangocms_rest/serializers/site.py +++ /dev/null @@ -1,9 +0,0 @@ -from django.contrib.sites.models import Site - -from rest_framework import serializers - - -class SiteSerializer(serializers.ModelSerializer): - class Meta: - model = Site - fields = '__all__' diff --git a/djangocms_rest/urls.py b/djangocms_rest/urls.py index 51b2824..cc08b3b 100644 --- a/djangocms_rest/urls.py +++ b/djangocms_rest/urls.py @@ -3,15 +3,14 @@ from . import views urlpatterns = [ - path('', views.SiteList.as_view()), - path('/languages/', views.LanguageList.as_view()), - path('//pages', views.PageList.as_view()), - path('//pages/', views.PageDetail.as_view()), - path('//pages//', views.PageDetail.as_view()), + path("", views.LanguageList.as_view()), + path("/pages", views.PageList.as_view(), name="pages-list"), + path("/pages/", views.PageDetail.as_view(), name="pages-root"), + path("/pages//", views.PageDetail.as_view()), path( - 'placeholders/////', + "/placeholders////", views.PlaceholderDetail.as_view(), - name='placeholder-detail', + name="placeholder-detail", ), ] diff --git a/djangocms_rest/views.py b/djangocms_rest/views.py index dc43764..b0f27df 100644 --- a/djangocms_rest/views.py +++ b/djangocms_rest/views.py @@ -1,36 +1,29 @@ from cms.models import Page, PageUrl, Placeholder from cms.utils.conf import get_languages from cms.utils.i18n import get_language_tuple -from django.contrib.sites.models import Site +from django.contrib.sites.shortcuts import get_current_site from django.http import Http404 +from django.urls import reverse from rest_framework.response import Response from rest_framework.views import APIView as DRFAPIView from djangocms_rest.serializers.pageserializer import PageSerializer, RESTPage from djangocms_rest.serializers.placeholder import PlaceholderSerializer -from djangocms_rest.serializers.site import SiteSerializer class APIView(DRFAPIView): - http_method_names = ('get', 'options') - - -class SiteList(APIView): - """ - List all sites. - """ - - def get(self, request, format=None): - pages = Site.objects.all() - serializer = SiteSerializer(pages, many=True, read_only=True) - return Response(serializer.data) + http_method_names = ("get", "options") class LanguageList(APIView): - def get(self, request, site_id, format=None): - languages = get_languages().get(site_id, None) + def get(self, request, format=None): + languages = get_languages().get(get_current_site(request).id, None) if languages is None: raise Http404 + for conf in languages: + conf["pages"] = f"{request.scheme}://{request.get_host()}" + reverse( + "pages-root", args=(conf["code"],) + ) return Response(languages) @@ -38,11 +31,16 @@ class PageList(APIView): """ List all pages, or create a new page. """ - def get(self, request, site_id, language, format=None): - allowed_languages = [lang[0] for lang in get_language_tuple(site_id)] + + def get(self, request, language, format=None): + site = get_current_site(request) + allowed_languages = [lang[0] for lang in get_language_tuple(site.id)] if language not in allowed_languages: raise Http404 - pages = (RESTPage(request, page, language=language) for page in Page.objects.all()) + qs = Page.objects.filter(node__site=site) + if request.user.is_anonymous: + qs = qs.filter(login_required=False) + pages = (RESTPage(request, page, language=language) for page in qs) serializer = PageSerializer(pages, many=True, read_only=True) return Response(serializer.data) @@ -51,13 +49,12 @@ class PageDetail(APIView): """ Retrieve, update or delete a page instance. """ - def get_object(self, site_id, path): + + def get_object(self, site, path): page_urls = ( - PageUrl - .objects - .get_for_site(Site.objects.get(pk=site_id)) + PageUrl.objects.get_for_site(site) .filter(path=path) - .select_related('page__node') + .select_related("page__node") ) page_urls = list(page_urls) # force queryset evaluation to save 1 query try: @@ -68,12 +65,15 @@ def get_object(self, site_id, path): page.urls_cache = {url.language: url for url in page_urls} return page - def get(self, request, site_id, language, path="", format=None): - allowed_languages = [lang[0] for lang in get_language_tuple(site_id)] + def get(self, request, language, path="", format=None): + site = get_current_site(request) + allowed_languages = [lang[0] for lang in get_language_tuple(site.pk)] if language not in allowed_languages: raise Http404 - page = self.get_object(site_id, path) - serializer = PageSerializer(RESTPage(request, page, language=language), read_only=True) + page = self.get_object(site, path) + serializer = PageSerializer( + RESTPage(request, page, language=language), read_only=True + ) return Response(serializer.data) @@ -81,9 +81,7 @@ class PlaceholderDetail(APIView): def get_placeholder(self, content_type_id, object_id, slot): try: placeholder = Placeholder.objects.get( - content_type_id=content_type_id, - object_id=object_id, - slot=slot + content_type_id=content_type_id, object_id=object_id, slot=slot ) except Placeholder.DoesNotExist: raise Http404 @@ -93,5 +91,14 @@ def get(self, request, language, content_type_id, object_id, slot, format=None): placeholder = self.get_placeholder(content_type_id, object_id, slot) if placeholder is None: raise Http404 - serializer = PlaceholderSerializer(request, placeholder, language, read_only=True) + source = ( + placeholder.content_type.model_class() + .objects.filter(pk=placeholder.object_id) + .first() + ) + if source is None: + raise Http404 + serializer = PlaceholderSerializer( + request, placeholder, language, read_only=True + ) return Response(serializer.data)