Skip to content

Commit

Permalink
Update README
Browse files Browse the repository at this point in the history
  • Loading branch information
fsbraun committed Jun 11, 2024
1 parent 1d5f0c5 commit 1562858
Show file tree
Hide file tree
Showing 10 changed files with 287 additions and 13 deletions.
118 changes: 105 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
@@ -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`:
```
<h{{ instance.size }} class="custom-header">{{ instance.heading_text }}</h{{ instance.size }}>
```


## 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:<uid>`, 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

Expand Down
26 changes: 26 additions & 0 deletions tests/test_app/admin.py
Original file line number Diff line number Diff line change
@@ -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)
67 changes: 67 additions & 0 deletions tests/test_app/cms_plugins.py
Original file line number Diff line number Diff line change
@@ -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 = '<span>Preview is disabled for this plugin</span>'
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
7 changes: 7 additions & 0 deletions tests/test_app/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from django import forms

from djangocms_text.fields import HTMLFormField


class SimpleTextForm(forms.Form):
text = HTMLFormField()
38 changes: 38 additions & 0 deletions tests/test_app/models.py
Original file line number Diff line number Diff line change
@@ -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)
24 changes: 24 additions & 0 deletions tests/test_app/templates/test_app/base.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{% load cms_tags static menu_tags sekizai_tags %}
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}This is my new project home page{% endblock title %}</title>
{% render_block "css" %}
<style type="text/css">
.nav {
padding-left: 0;
}
.nav li {
display: inline;
list-style-type: none;
padding-right: 20px;
}
</style>
</head>
<body>
{% cms_toolbar %}
<div style="width: 940px; margin:0 auto">
<ul class="nav">
{% show_menu 0 100 100 100 %}
</ul>
{% block content %}
5 changes: 5 additions & 0 deletions tests/test_app/templates/test_app/dummy_child_plugin.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{% load cms_tags %}

{% for plugin in instance.child_plugin_instances %}
{% render_plugin plugin %}
{% endfor %}
5 changes: 5 additions & 0 deletions tests/test_app/templates/test_app/dummy_parent_plugin.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{% load cms_tags %}

{% for plugin in instance.child_plugin_instances %}
{% render_plugin plugin %}
{% endfor %}
8 changes: 8 additions & 0 deletions tests/test_app/templates/test_app/page.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{% extends "base.html" %}
{% load cms_tags %}

{% block title %}{% page_attribute 'title' %}{% endblock title %}

{% block content %}
{% placeholder "content" %}
{% endblock content %}
2 changes: 2 additions & 0 deletions tests/test_app/templates/test_app/plugin_with_sekizai.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{% load sekizai_tags %}
{% addtoblock "css" %}<link rel="stylesheet" href="/static/css/sekizai.css">{% endaddtoblock %}

0 comments on commit 1562858

Please sign in to comment.