diff --git a/README.md b/README.md index 477ee1e..1c50533 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,117 @@ [![codecov](https://codecov.io/gh/fsbraun/djangocms-rest/graph/badge.svg?token=RKQJL8L8BT)](https://codecov.io/gh/fsbraun/djangocms-rest) [![djangocms4]( https://img.shields.io/badge/django%20CMS-4-blue.svg)](https://www.django-cms.org/en/) -# Django CMS REST +# django CMS Headless Mode -This is a demo project that provides RESTful APIs for Django CMS. Currently, it offers public read-only access to pages and placeholders. This allows for the retrieval of structured and nested content from your Django CMS instance in a programmatic way. +## What is djangocms-rest? -Please note that this project is in its early stages and more features will be added in the future. For now, it provides basic functionality for placeholders. +djangocms-rest enables frontend projects to consume django CMS content through a browseable +read-only, REST/JSON API. It is based on the django rest framework (DRF). -## Features +For the following topics please see the ../README.md +- installation and setup instructions +- questions and support options +- collaboration and contributions to this project -- Basic functionality for reading public pages and placeholders -- Optional HTML rendering for placeholder content (including sekizai blocks) +## What is headless mode? -## To dos +A Headless CMS (Content Management System) is a backend-only content management system that provides +content through APIs, making it decoupled from the front-end presentation layer. This allows +developers to deliver content to any device or platform, such as websites, mobile apps, or IoT +devices, using any technology stack. By separating content management from content presentation, +a Headless CMS offers greater flexibility and scalability in delivering content. -- Full language fallback support on page level -- Menu api -- Admin api for editing content - - Placeholder API (adding, deleting, plugins) - - Plugin API (changing, rendering, getting options) -- Tests +## What are the main benefits of running a CMS in headless mode? + +Running a CMS in headless mode offers several benefits, including greater flexibility in delivering +content to multiple platforms and devices through APIs, enabling consistent and efficient +multi-channel experiences. It enhances performance and scalability by allowing frontend and backend +development to progress independently using the best-suited technologies. Additionally, it +streamlines content management, making it easier to update and maintain content across various +applications without needing to alter the underlying infrastructure. + +## Are there js packages for drop-in support of frontend editing in the javascript framework of my choice? + +The good news first: django CMS headless mode is fully backend supported and works independently +of the javascript framework. It is fully compatible with the javascript framework of your choosing. + +## How can I implement a plugin for headless mode? + +It's pretty much the same as for a traditional django CMS project, see +[here for instructions on how to create django CMS plugins](https://docs.django-cms.org/en/latest/how_to/09-custom_plugins.html). + +Let's have an example. Here is a simple plugin with two fields to render a custom header. Please +note that the template included is just a simple visual helper to support editors to manage +content in the django CMS backend. Also, backend developers can now toy around and test their +django CMS code independently of a frontend project. + +After setting up djangocms-rest and creating such a plugin you can now run the project and see a +REST/JSON representation of your content in your browser, ready for consumption by a decoupled +frontend. + +`cms_plugins.py`: +``` +# -*- coding: utf-8 -*- +from cms.plugin_base import CMSPluginBase +from cms.plugin_pool import plugin_pool + +from . import models + + +class CustomHeadingPlugin(CMSPluginBase): + model = models.CustomHeadingPluginModel + module = 'Layout Helpers' + name = "My Custom Heading" + + # this is just a simple, unstyled helper rendering so editors can manage content + render_template = 'custom_heading_plugin/plugins/custom-heading.html' + + allow_children = False + + +plugin_pool.register_plugin(CustomHeadingPlugin) +``` + +`models.py`: +``` +from cms.models.pluginmodel import CMSPlugin +from django.db import models + + +class CustomHeadingPluginModel(CMSPlugin): + + heading_text = models.CharField( + max_length=256, + ) + + size = models.PositiveIntegerField(default=1) +``` + +`templates/custom_heading_plugin/plugins/custom-heading.html`: +``` +{{ instance.heading_text }} +``` + + +## Do default plugins support headless mode out of the box? + +Yes, djangocms-rest provides out of the box support for any and all django CMS plugins whose content +can be serialized. + + +## Does the TextPlugin (Rich Text Editor, RTE) provide a json representation of the rich text? + +Yes, djangocms-text has both HTML blob and structured JSON support for rich text. + +URLs to other CMS objects are dynamic, in the form of `cms.object-name:`, for example +`cms.page:2`. The frontend can then use this to resolve the object and create the appropriate URLs +to the object's frontend representation. + +## I don't need pages, I just have a fixed number of content areas in my frontend application for which I need CMS support. + +Absolutely, you can use the djangocms-aliases package. It allows you to define custom _placeholders_ +that are not linked to any pages. djangocms-rest will then make a list of those aliases and their +content available via the REST API. ## Requirements diff --git a/tests/test_app/admin.py b/tests/test_app/admin.py new file mode 100644 index 0000000..32ccadf --- /dev/null +++ b/tests/test_app/admin.py @@ -0,0 +1,26 @@ +from django.contrib import admin + +from .models import Pizza, Topping + + +class ToppingInlineAdmin(admin.TabularInline): + model = Topping + extra = 1 + + +class PizzaAdmin(admin.ModelAdmin): + fieldsets = ( + ('', { + 'fields': ('description',), + }), + ('Advanced', { + # NOTE: Disabled because when PizzaAdmin uses a collapsed + # class then the order of javascript libs is incorrect. + # 'classes': ('collapse',), + 'fields': ('allergens',), + }), + ) + inlines = [ToppingInlineAdmin] + + +admin.site.register(Pizza, PizzaAdmin) diff --git a/tests/test_app/cms_plugins.py b/tests/test_app/cms_plugins.py new file mode 100644 index 0000000..030bcb7 --- /dev/null +++ b/tests/test_app/cms_plugins.py @@ -0,0 +1,67 @@ +from django.template import engines + +from cms.models import CMSPlugin +from cms.plugin_base import CMSPluginBase +from cms.plugin_pool import plugin_pool +from cms.utils.plugins import get_plugin_model + +from djangocms_text.cms_plugins import TextPlugin +from tests.test_app.models import DummyLink, DummySpacer + + +@plugin_pool.register_plugin +class PreviewDisabledPlugin(CMSPluginBase): + text_editor_preview = False + + def get_render_template(self, context, instance, placeholder): + template = 'Preview is disabled for this plugin' + return engines['django'].from_string(template) + + +@plugin_pool.register_plugin +class SekizaiPlugin(CMSPluginBase): + name = 'Sekizai' + render_template = 'test_app/plugin_with_sekizai.html' + + +@plugin_pool.register_plugin +class ExtendedTextPlugin(TextPlugin): + name = 'Extended' + + +@plugin_pool.register_plugin +class DummyLinkPlugin(CMSPluginBase): + render_plugin = False + model = DummyLink + + +@plugin_pool.register_plugin +class DummySpacerPlugin(CMSPluginBase): + render_plugin = False + model = DummySpacer + + +@plugin_pool.register_plugin +class DummyParentPlugin(CMSPluginBase): + render_template = 'test_app/dummy_parent_plugin.html' + model = DummyLink + allow_children = True + + _ckeditor_body_class = 'parent-plugin-css-class' + _ckeditor_body_class_label_trigger = 'parent link label' + + @classmethod + def get_child_ckeditor_body_css_class(cls, plugin: CMSPlugin) -> str: + plugin_model = get_plugin_model(plugin.plugin_type) + plugin_instance = plugin_model.objects.get(pk=plugin.pk) + if plugin_instance.label == cls._ckeditor_body_class_label_trigger: + return cls._ckeditor_body_class + else: + return '' + + +@plugin_pool.register_plugin +class DummyChildPlugin(CMSPluginBase): + render_template = 'test_app/dummy_child_plugin.html' + child_ckeditor_body_css_class = 'child-plugin-css-class' + allow_children = True diff --git a/tests/test_app/forms.py b/tests/test_app/forms.py new file mode 100644 index 0000000..d9181c4 --- /dev/null +++ b/tests/test_app/forms.py @@ -0,0 +1,7 @@ +from django import forms + +from djangocms_text.fields import HTMLFormField + + +class SimpleTextForm(forms.Form): + text = HTMLFormField() diff --git a/tests/test_app/models.py b/tests/test_app/models.py new file mode 100644 index 0000000..ae940cc --- /dev/null +++ b/tests/test_app/models.py @@ -0,0 +1,38 @@ +from django.db import models + +from cms.models import CMSPlugin + +from djangocms_text.fields import HTMLField + + +class SimpleText(models.Model): + text = HTMLField(blank=True) + + +class DummyLink(CMSPlugin): + label = models.TextField() + + class Meta: + abstract = False + + def __str__(self): + return 'dummy link object' + + +class DummySpacer(CMSPlugin): + class Meta: + abstract = False + + def __str__(self): + return 'dummy spacer object' + + +class Pizza(models.Model): + description = HTMLField() + allergens = HTMLField(blank=True) + + +class Topping(models.Model): + name = models.CharField(max_length=255) + description = HTMLField() + pizza = models.ForeignKey(Pizza, on_delete=models.CASCADE) diff --git a/tests/test_app/templates/test_app/base.html b/tests/test_app/templates/test_app/base.html new file mode 100644 index 0000000..db9d806 --- /dev/null +++ b/tests/test_app/templates/test_app/base.html @@ -0,0 +1,24 @@ +{% load cms_tags static menu_tags sekizai_tags %} + + + + {% block title %}This is my new project home page{% endblock title %} + {% render_block "css" %} + + + +{% cms_toolbar %} +
+ + {% block content %} diff --git a/tests/test_app/templates/test_app/dummy_child_plugin.html b/tests/test_app/templates/test_app/dummy_child_plugin.html new file mode 100644 index 0000000..19db48f --- /dev/null +++ b/tests/test_app/templates/test_app/dummy_child_plugin.html @@ -0,0 +1,5 @@ +{% load cms_tags %} + +{% for plugin in instance.child_plugin_instances %} + {% render_plugin plugin %} +{% endfor %} diff --git a/tests/test_app/templates/test_app/dummy_parent_plugin.html b/tests/test_app/templates/test_app/dummy_parent_plugin.html new file mode 100644 index 0000000..19db48f --- /dev/null +++ b/tests/test_app/templates/test_app/dummy_parent_plugin.html @@ -0,0 +1,5 @@ +{% load cms_tags %} + +{% for plugin in instance.child_plugin_instances %} + {% render_plugin plugin %} +{% endfor %} diff --git a/tests/test_app/templates/test_app/page.html b/tests/test_app/templates/test_app/page.html new file mode 100644 index 0000000..e96a8f2 --- /dev/null +++ b/tests/test_app/templates/test_app/page.html @@ -0,0 +1,8 @@ +{% extends "base.html" %} +{% load cms_tags %} + +{% block title %}{% page_attribute 'title' %}{% endblock title %} + +{% block content %} + {% placeholder "content" %} +{% endblock content %} diff --git a/tests/test_app/templates/test_app/plugin_with_sekizai.html b/tests/test_app/templates/test_app/plugin_with_sekizai.html new file mode 100644 index 0000000..65199b3 --- /dev/null +++ b/tests/test_app/templates/test_app/plugin_with_sekizai.html @@ -0,0 +1,2 @@ +{% load sekizai_tags %} +{% addtoblock "css" %}{% endaddtoblock %}