The dependency on swapper has been replaced with an incredibly simply

"registry". Client projects register their models and views during import
time with a decorator. The cms views use the registered classes to generate
pages and forms.

Furthermore, the example application now combines the app and the project in
one, inspired by this line in the Django documentation:

    There’s no restriction that a project package can’t also be considered
    an application and have models, etc.
main
Jaap Joris Vens 2020-03-22 12:46:10 +01:00
rodzic 98b1a61af1
commit 0e221ccf33
32 zmienionych plików z 123 dodań i 243 usunięć

Wyświetl plik

@ -1,4 +1 @@
from .decorators import register
from .cms import SectionView, SectionFormView
default_app_config = 'cms.apps.CmsConfig'

Wyświetl plik

@ -1,25 +0,0 @@
from django.views.generic import edit
from django.http import HttpResponseRedirect
class SectionView:
'''Generic section view'''
template_name = 'cms/sections/section.html'
def __init__(self, request):
'''Initialize request attribute'''
self.request = request
def get_context_data(self, **kwargs):
'''Override this to customize a section's context'''
return kwargs
class SectionFormView(edit.FormMixin, SectionView):
'''Generic section with associated form'''
def post(self, request):
'''Process form'''
form = self.get_form()
if form.is_valid():
form.save(request)
return HttpResponseRedirect(self.get_success_url())
return form

Wyświetl plik

@ -1,25 +1,17 @@
def register(verbose_name):
import swapper
Section = swapper.load_model('cms', 'Section')
from cms import registry
'''Decorator to register a specific section type'''
def wrapper(view):
Section._cms_views[view.__name__.lower()] = view
Section.TYPES.append((view.__name__.lower(), verbose_name))
return view
return wrapper
def page_model(cls):
'''Decorator to register the Page model'''
registry.page_class = cls
return cls
# def register_model(verbose_name):
# '''Decorator to register a section subclass'''
# def wrapper(model):
# parent_model = model.__bases__[-1]
# parent_model.TYPES.append((model.__name__.lower(), verbose_name))
# return model
# return wrapper
def section_model(cls):
'''Decorator to register the Section model'''
registry.section_class = cls
return cls
# def register_view(section_class):
# '''Decorator to connect a section model to a view class'''
# def wrapper(model):
# section_class.view_class = model
# return model
# return wrapper
def section_view(cls):
'''Decorator to register a view for a specific section'''
registry.views_per_type[cls.__name__.lower()] = cls
registry.section_class.TYPES.append((cls.__name__.lower(), cls.verbose_name))
return cls

Wyświetl plik

@ -1,4 +1,3 @@
import swapper
from django import forms
from django.conf import settings
from django.db.models import Prefetch
@ -6,8 +5,7 @@ from django.contrib.contenttypes.models import ContentType
from django.core.mail import EmailMessage
from django.utils.translation import gettext_lazy as _
Page = swapper.load_model('cms', 'Page')
Section = swapper.load_model('cms', 'Section')
from . import registry
class PageForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
@ -15,8 +13,8 @@ class PageForm(forms.ModelForm):
self.label_suffix = ''
extra = 1 if self.instance.pk else 2
self.formsets = [forms.inlineformset_factory(
parent_model = Page,
model = Section,
parent_model = registry.page_class,
model = registry.section_class,
form = SectionForm,
extra=extra,
)(
@ -50,7 +48,7 @@ class PageForm(forms.ModelForm):
return page
class Meta:
model = Page
model = registry.page_class
fields = '__all__'
class SectionForm(forms.ModelForm):
@ -73,7 +71,7 @@ class SectionForm(forms.ModelForm):
for field in self.instance._meta.get_fields():
if field.one_to_many:
formset = forms.inlineformset_factory(
parent_model=Section,
parent_model=registry.section_class,
model=field.related_model,
fields='__all__',
extra=extra,
@ -116,7 +114,7 @@ class SectionForm(forms.ModelForm):
return section
class Meta:
model = Section
model = registry.section_class
exclude = ['page']
class ContactForm(forms.Form):

Wyświetl plik

@ -1,61 +0,0 @@
# Generated by Django 3.0.2 on 2020-02-16 14:27
import cms.models
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import embed_video.fields
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.CMS_PAGE_MODEL),
('contenttypes', '0002_remove_content_type_name'),
]
operations = [
migrations.CreateModel(
name='Page',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('number', models.PositiveIntegerField(blank=True, verbose_name='number')),
('title', cms.models.VarCharField(verbose_name='page')),
('slug', models.SlugField(blank=True, unique=True, verbose_name='slug')),
('menu', models.BooleanField(default=True, verbose_name='visible in menu')),
],
options={
'verbose_name': 'Page',
'verbose_name_plural': 'Pages',
'ordering': ['number'],
'abstract': False,
'swappable': 'CMS_PAGE_MODEL',
},
bases=(cms.models.Numbered, models.Model),
),
migrations.CreateModel(
name='Section',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('type', cms.models.VarCharField(verbose_name='type')),
('number', models.PositiveIntegerField(blank=True, verbose_name='number')),
('title', cms.models.VarCharField(verbose_name='section')),
('content', models.TextField(blank=True, verbose_name='content')),
('image', models.ImageField(blank=True, upload_to='', verbose_name='image')),
('video', embed_video.fields.EmbedVideoField(blank=True, help_text='Paste a YouTube, Vimeo, or SoundCloud link', verbose_name='video')),
('href', cms.models.VarCharField(blank=True, verbose_name='link')),
('page', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='sections', to=settings.CMS_PAGE_MODEL, verbose_name='page')),
('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_cms.section_set+', to='contenttypes.ContentType')),
],
options={
'verbose_name': 'section',
'verbose_name_plural': 'sections',
'ordering': ['number'],
'abstract': False,
'swappable': 'CMS_SECTION_MODEL',
},
bases=(cms.models.Numbered, models.Model),
),
]

Wyświetl plik

@ -1,17 +0,0 @@
# Generated by Django 3.0.3 on 2020-03-21 10:23
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('cms', '0002_initial_homepage'),
]
operations = [
migrations.RemoveField(
model_name='section',
name='polymorphic_ctype',
),
]

Wyświetl plik

@ -1,5 +1,3 @@
import swapper
from django.db import models
from django.urls import reverse
from django.utils.text import slugify
@ -89,16 +87,10 @@ class BasePage(Numbered, models.Model):
class BaseSection(Numbered, models.Model):
'''Abstract base model for sections'''
# These will be populated by @register
TYPES = []
_cms_views = {}
page = models.ForeignKey(swapper.get_model_name('cms', 'Page'), verbose_name=_('page'), related_name='sections', on_delete=models.PROTECT)
title = VarCharField(_('section'))
type = VarCharField(_('type'))
number = models.PositiveIntegerField(_('number'), blank=True)
content = models.TextField(_('content'), blank=True)
image = models.ImageField(_('image'), blank=True)
video = EmbedVideoField(_('video'), blank=True, help_text=_('Paste a YouTube, Vimeo, or SoundCloud link'))
@ -118,33 +110,8 @@ class BaseSection(Numbered, models.Model):
else:
return self.title
def get_view(self, request):
'''Try to instantiate the registered view for this section'''
try:
return self.__class__._cms_views[self.type](request)
except:
raise ImproperlyConfigured(
f'No view registered for sections of type {self.type}!')
@classmethod
def get_fields_per_type(cls):
fields_per_type = {}
for name, view in cls._cms_views.items():
fields_per_type[name] = ['type', 'number'] + view.fields
return fields_per_type
class Meta:
abstract = True
verbose_name = _('section')
verbose_name_plural = _('sections')
ordering = ['number']
class Page(BasePage):
'''Swappable page model'''
class Meta(BasePage.Meta):
swappable = swapper.swappable_setting('cms', 'Page')
class Section(BaseSection):
'''Swappable section model'''
class Meta(BaseSection.Meta):
swappable = swapper.swappable_setting('cms', 'Section')

13
cms/registry.py 100644
Wyświetl plik

@ -0,0 +1,13 @@
views_per_type = {}
page_class = None
section_class = None
def get_view(section, request):
'''Instantiate the registered view of a section'''
return views_per_type[section.type](request)
def get_fields_per_type():
fields_per_type = {}
for name, view in views_per_type.items():
fields_per_type[name] = ['title', 'type', 'number'] + view.fields
return fields_per_type

Wyświetl plik

@ -5,6 +5,8 @@ from django.shortcuts import reverse
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
from cms import registry
MARKDOWN_EXTENSIONS = ['extra', 'smarty']
register = template.Library()
@ -51,7 +53,7 @@ class IncludeSectionNode(template.Node):
csrf_token = self.csrf_token.resolve(context)
request = self.request.resolve(context)
perms = self.perms.resolve(context)
view = section.get_view(request)
view = registry.get_view(section, request)
section_context = view.get_context_data(
csrf_token=csrf_token,
section=section,

Wyświetl plik

@ -1,19 +1,39 @@
import json
import swapper
from django.shortcuts import redirect
from django.views.generic import base, detail, edit
from django.contrib.auth.mixins import UserPassesTestMixin
from django.http import Http404, HttpResponse, HttpResponseRedirect, HttpResponseBadRequest
from . import registry
from .forms import PageForm, SectionForm
Page = swapper.load_model('cms', 'Page')
Section = swapper.load_model('cms', 'Section')
class SectionView:
'''Generic section view'''
template_name = 'cms/sections/section.html'
def __init__(self, request):
'''Initialize request attribute'''
self.request = request
def get_context_data(self, **kwargs):
'''Override this to customize a section's context'''
return kwargs
class SectionFormView(edit.FormMixin, SectionView):
'''Generic section with associated form'''
def post(self, request):
'''Process form'''
form = self.get_form()
if form.is_valid():
form.save(request)
return HttpResponseRedirect(self.get_success_url())
return form
class PageView(detail.DetailView):
'''View of a page with heterogeneous sections'''
model = Page
model = registry.page_class
template_name = 'cms/page.html'
def setup(self, *args, slug='', **kwargs):
@ -25,9 +45,14 @@ class PageView(detail.DetailView):
try:
page = self.object = self.get_object()
except Http404:
if self.request.user.has_perm('cms_page_create'):
if self.kwargs['slug'] == '':
page = registry.page_class(title='Homepage', slug='')
page.save()
self.object = page
elif self.request.user.has_perm('cms_page_create'):
return redirect('cms:updatepage', self.kwargs['slug'])
raise
else:
raise
context = self.get_context_data(**kwargs)
sections = page.sections.all()
context.update({
@ -48,7 +73,7 @@ class PageView(detail.DetailView):
sections = page.sections.all()
for section in sections:
if section.pk == pk:
view = section.get_view(request)
view = registry.get_view(section, request)
result = view.post(request)
if isinstance(result, HttpResponse):
return result
@ -62,7 +87,7 @@ class PageView(detail.DetailView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
pages = Page.objects.filter(menu=True)
pages = registry.page_class.objects.filter(menu=True)
context.update({
'pages': pages,
})
@ -70,7 +95,7 @@ class PageView(detail.DetailView):
class EditPage(UserPassesTestMixin, edit.ModelFormMixin, base.TemplateResponseMixin, base.View):
'''Base view with nested forms for editing the page and all its sections'''
model = Page
model = registry.page_class
form_class = PageForm
template_name = 'cms/edit.html'
@ -88,7 +113,7 @@ class EditPage(UserPassesTestMixin, edit.ModelFormMixin, base.TemplateResponseMi
def get_context_data(self, **kwargs):
'''Populate the fields_per_type dict for use in javascript'''
context = super().get_context_data(**kwargs)
context['fields_per_type'] = json.dumps(Section.get_fields_per_type())
context['fields_per_type'] = json.dumps(registry.get_fields_per_type())
return context
def get_object(self):
@ -118,13 +143,13 @@ class EditPage(UserPassesTestMixin, edit.ModelFormMixin, base.TemplateResponseMi
class CreatePage(EditPage):
'''View for creating new pages'''
def get_object(self):
return Page()
return registry.page_class()
class UpdatePage(EditPage):
'''View for editing existing pages'''
class EditSection(UserPassesTestMixin, edit.ModelFormMixin, base.TemplateResponseMixin, base.View):
model = Section
model = registry.section_class
form_class = SectionForm
template_name = 'cms/edit.html'
@ -140,20 +165,20 @@ class EditSection(UserPassesTestMixin, edit.ModelFormMixin, base.TemplateRespons
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['fields_per_type'] = json.dumps(Section.get_fields_per_type())
context['fields_per_type'] = json.dumps(registry.get_fields_per_type())
return context
def get_object(self, queryset=None):
try:
self.page = Page.objects.get(slug=self.kwargs['slug'])
except Page.DoesNotExist:
self.page = registry.page_class.objects.get(slug=self.kwargs['slug'])
except registry.page_class.DoesNotExist:
raise Http404()
return self.get_section()
def get_section(self):
try:
section = self.page.sections.get(number=self.kwargs['number'])
except Section.DoesNotExist:
except self.page.sections.DoesNotExist:
raise Http404()
return section
@ -177,7 +202,7 @@ class EditSection(UserPassesTestMixin, edit.ModelFormMixin, base.TemplateRespons
class CreateSection(EditSection):
def get_section(self):
return Section(page=self.page)
return registry.section_class(page=self.page)
class UpdateSection(EditSection):
pass

Wyświetl plik

@ -1,26 +0,0 @@
# Generated by Django 3.0.2 on 2020-03-21 16:58
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('app', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='SectionImage',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('image', models.ImageField(upload_to='', verbose_name='Image')),
('section', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='images', to=settings.CMS_SECTION_MODEL)),
],
options={
'ordering': ['pk'],
},
),
]

Wyświetl plik

@ -1,24 +1,29 @@
import cms
from cms.views import SectionView, SectionFormView
from cms.decorators import section_view
from cms.forms import ContactForm
from django.utils.translation import gettext_lazy as _
@cms.register(_('Text'))
class Text(cms.SectionView):
@section_view
class Text(SectionView):
verbose_name = _('Text')
fields = ['title', 'content']
template_name = 'app/sections/text.html'
@cms.register(_('Images'))
class Images(cms.SectionView):
@section_view
class Images(SectionView):
verbose_name = _('Images')
fields = ['title', 'images']
template_name = 'app/sections/images.html'
@cms.register(_('Video'))
class Video(cms.SectionView):
@section_view
class Video(SectionView):
verbose_name = _('Video')
fields = ['title', 'video']
template_name = 'app/sections/video.html'
@cms.register(_('Contact'))
class Contact(cms.SectionFormView):
@section_view
class Contact(SectionFormView):
verbose_name = _('Contact')
fields = ['title']
form_class = ContactForm
success_url = '/thanks/'

Wyświetl plik

@ -1,7 +1,6 @@
# Generated by Django 3.0.2 on 2020-03-21 16:44
# Generated by Django 3.0.2 on 2020-03-22 11:11
import cms.models
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import embed_video.fields
@ -32,6 +31,16 @@ class Migration(migrations.Migration):
},
bases=(cms.models.Numbered, models.Model),
),
migrations.CreateModel(
name='SectionImage',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('image', models.ImageField(upload_to='', verbose_name='Image')),
],
options={
'ordering': ['pk'],
},
),
migrations.CreateModel(
name='Section',
fields=[
@ -43,7 +52,7 @@ class Migration(migrations.Migration):
('image', models.ImageField(blank=True, upload_to='', verbose_name='image')),
('video', embed_video.fields.EmbedVideoField(blank=True, help_text='Paste a YouTube, Vimeo, or SoundCloud link', verbose_name='video')),
('href', cms.models.VarCharField(blank=True, verbose_name='link')),
('page', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='sections', to=settings.CMS_PAGE_MODEL, verbose_name='page')),
('page', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='sections', to='example.Page', verbose_name='page')),
],
options={
'verbose_name': 'section',

Wyświetl plik

@ -1,21 +1,25 @@
from django.db import models
from cms.models import BasePage, BaseSection
from django.utils.translation import gettext_lazy as _
from cms.models import BasePage, BaseSection
from cms.decorators import page_model, section_model
@page_model
class Page(BasePage):
'''Add custom fields here. Already existing fields: title, slug,
number, menu
'''
@section_model
class Section(BaseSection):
'''Add custom fields here. Already existing fields: title, type,
number, content, image, video, href
'''
page = models.ForeignKey(Page, verbose_name=_('page'), related_name='sections', on_delete=models.PROTECT)
class SectionImage(models.Model):
section = models.ForeignKey(Section, related_name='images', on_delete=models.CASCADE)
#section = models.ForeignKey(Section, related_name='images', on_delete=models.CASCADE)
image = models.ImageField(_('Image'))
class Meta:

Wyświetl plik

@ -1,19 +1,13 @@
import os, random, string
try:
import uwsgi
DEBUG = False
except ImportError:
DEBUG = True
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
PROJECT_NAME = 'example'
PROJECT_NAME = os.path.basename(os.path.dirname(os.path.abspath(__file__)))
KEYFILE = f'/tmp/{PROJECT_NAME}.secret'
ADMINS = [('JJ Vens', 'jj@rtts.eu')]
DEFAULT_FROM_EMAIL = 'noreply@rtts.eu'
DEFAULT_TO_EMAIL = 'jj@rtts.eu'
ALLOWED_HOSTS = ['*']
ROOT_URLCONF = 'project.urls'
WSGI_APPLICATION = 'project.wsgi.application'
ROOT_URLCONF = PROJECT_NAME + '.urls'
WSGI_APPLICATION = PROJECT_NAME + '.wsgi.application'
LANGUAGE_CODE = 'nl'
TIME_ZONE = 'Europe/Amsterdam'
USE_I18N = True
@ -24,8 +18,13 @@ STATIC_ROOT = '/srv/' + PROJECT_NAME + '/static'
MEDIA_URL = '/media/'
MEDIA_ROOT = '/srv/' + PROJECT_NAME + '/media'
LOGIN_REDIRECT_URL = '/'
CMS_SECTION_MODEL = 'app.Section'
CMS_PAGE_MODEL = 'app.Page'
try:
import uwsgi
DEBUG = False
except ImportError:
DEBUG = True
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
def read(file):
with open(file) as f:
@ -40,7 +39,7 @@ except IOError:
write(KEYFILE, SECRET_KEY)
INSTALLED_APPS = [
'app',
PROJECT_NAME,
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',

Wyświetl plik

@ -3,7 +3,7 @@ import os
import sys
if __name__ == '__main__':
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings')
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'example.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:

Wyświetl plik

@ -3,7 +3,7 @@ from setuptools import setup, find_packages
setup(
name = 'django-simplecms',
version = '2.3.1',
version = '3.0.0',
url = 'https://github.com/rtts/django-simplecms',
author = 'Jaap Joris Vens',
author_email = 'jj@rtts.eu',
@ -15,11 +15,9 @@ setup(
'django',
'django-extensions',
'django-embed-video',
'django-polymorphic',
'easy-thumbnails',
'psycopg2',
'markdown',
'libsass',
'swapper',
],
)