New machinery for registering custom views

Each custom section can now have their own associated custom SectionView.
SectionView subclasses behave just like Django's generic views, except they
return Section objects instead of http responses. The updated PageView takes
care of compositing all rendered sections into the final response. Nice!
readwriteweb
Jaap Joris Vens 2020-01-02 19:32:15 +01:00
rodzic 25350b4706
commit e0dddeda08
15 zmienionych plików z 238 dodań i 132 usunięć

Wyświetl plik

@ -1,6 +1,12 @@
from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _
from django.utils.module_loading import autodiscover_modules
class CmsConfig(AppConfig):
name = 'cms'
verbose_name = _('Content Management System')
def ready(self):
# Need to load view models at startup to make the
# register_view decorator work
autodiscover_modules('views')

17
cms/decorators.py 100644
Wyświetl plik

@ -0,0 +1,17 @@
def register_model(verbose_name):
'''Decorator to register a section subclass.
'''
def wrapper(model):
model.__bases__[-1].TYPES.append((model.__name__.lower(), verbose_name))
return model
return wrapper
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

Wyświetl plik

@ -1,10 +1,31 @@
from django import forms
from django.core.mail import EmailMessage
from django.utils.translation import gettext_lazy as _
from django.contrib.contenttypes.models import ContentType
import swapper
Page = swapper.load_model('cms', 'Page')
Section = swapper.load_model('cms', 'Section')
class ContactForm(forms.Form):
sender = forms.EmailField(label=_('From'))
spam_protection = forms.CharField(label=_('Your message'), widget=forms.Textarea())
message = forms.CharField(label=_('Your message'), widget=forms.Textarea(), initial='Hi there!')
def save(self, request):
hostname = request.get_host()
body = self.cleaned_data.get('spam_protection') # MUHAHA
if len(body.split()) < 7:
return
email = EmailMessage(
to = ['info@' + hostname],
from_email = 'noreply@' + hostname,
body = body,
subject = _('Contact form at %(hostname)s.') % {'hostname': hostname},
headers = {'Reply-To': self.cleaned_data.get('sender')},
)
email.send()
class PageForm(forms.ModelForm):
class Meta:
model = Page

Wyświetl plik

@ -1,4 +1,4 @@
# Generated by Django 3.0.1 on 2020-01-02 00:14
# Generated by Django 3.0.1 on 2020-01-02 18:31
import ckeditor.fields
import cms.models
@ -51,7 +51,7 @@ class Migration(migrations.Migration):
name='Section',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('type', cms.models.VarCharChoiceField(choices=[('textsection', 'Tekst'), ('stepsection', 'Stappenplan'), ('uploadsection', 'Upload')], default='', verbose_name='section type')),
('type', cms.models.VarCharChoiceField(choices=[('textsection', 'Tekst'), ('imagesection', 'Afbeelding'), ('contactsection', 'Contact')], default='', verbose_name='section type')),
('position', models.PositiveIntegerField(blank=True, verbose_name='position')),
('title', cms.models.VarCharField(blank=True, verbose_name='title')),
('color', models.PositiveIntegerField(choices=[(1, 'Wit')], default=1, verbose_name='color')),

Wyświetl plik

@ -1,4 +1,7 @@
'''CMS Models'''
import swapper
from django.db import models
from django.urls import reverse
from django.conf import settings
@ -10,23 +13,20 @@ from polymorphic.models import PolymorphicModel
from numberedmodel.models import NumberedModel
def register(verbose_name):
def wrapper(model):
model.__bases__[-1].TYPES.append((model.__name__.lower(), verbose_name))
return model
return wrapper
class VarCharField(models.TextField):
'''Variable width CharField'''
def formfield(self, **kwargs):
kwargs.update({'widget': TextInput})
return super().formfield(**kwargs)
class VarCharChoiceField(models.TextField):
'''Variable width CharField with choices'''
def formfield(self, **kwargs):
kwargs.update({'widget': Select})
return super().formfield(**kwargs)
class BasePage(NumberedModel):
'''Abstract base model for pages'''
position = models.PositiveIntegerField(_('position'), blank=True)
title = VarCharField(_('title'))
slug = models.SlugField(_('slug'), help_text=_('A short identifier to use in URLs'), blank=True, unique=True)
@ -35,14 +35,12 @@ class BasePage(NumberedModel):
def __str__(self):
if not self.pk:
return str(_('New page'))
else:
return self.title
return self.title
def get_absolute_url(self):
if self.slug:
return reverse('cms:page', args=[self.slug])
else:
return reverse('cms:page')
return reverse('cms:page')
class Meta:
abstract = True
@ -51,6 +49,7 @@ class BasePage(NumberedModel):
ordering = ['position']
class BaseSection(NumberedModel, PolymorphicModel):
'''Abstract base model for sections'''
TYPES = []
page = models.ForeignKey(swapper.get_model_name('cms', 'Page'), verbose_name=_('page'), related_name='sections', on_delete=models.PROTECT)
type = VarCharChoiceField(_('section type'), default='', choices=TYPES)
@ -81,10 +80,12 @@ class BaseSection(NumberedModel, PolymorphicModel):
ordering = ['position']
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')

Wyświetl plik

@ -201,50 +201,6 @@ section {
clear: both;
border-bottom: 2px solid $blue;
div.subsections {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
div.subsection {
div.wrapper {
padding: 0;
}
flex: 0 0 30%;
margin-bottom: 2em;
box-sizing: border-box;
div.title {
font-size: 1.5em;
}
div.image {
padding-top: 56%;
overflow: hidden;
position: relative;
img {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
}
}
@media(max-width: $medium) {
flex-basis: 45%;
}
@media(max-width: $small) {
flex-basis: 100%;
}
}
}
div.image {
img {
width: 100%;
@ -252,7 +208,6 @@ section {
}
div.title {
//font-size: 2em;
text-align: center;
}
@ -281,6 +236,15 @@ section {
}
}
section.contactsection {
div#message {
display: none;
}
textarea {
font-family: inherit;
}
}
/* Form elements */
form.cms {

Wyświetl plik

@ -149,38 +149,6 @@ div.edit a, div.edit button, a.edit {
section {
clear: both;
border-bottom: 2px solid #3573a8; }
section div.subsections {
display: flex;
flex-wrap: wrap;
justify-content: space-between; }
section div.subsections div.subsection {
flex: 0 0 30%;
margin-bottom: 2em;
box-sizing: border-box; }
section div.subsections div.subsection div.wrapper {
padding: 0; }
section div.subsections div.subsection div.title {
font-size: 1.5em; }
section div.subsections div.subsection div.image {
padding-top: 56%;
overflow: hidden;
position: relative; }
section div.subsections div.subsection div.image img {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
object-fit: cover;
object-position: center; }
@media (max-width: 800px) {
section div.subsections div.subsection {
flex-basis: 45%; } }
@media (max-width: 500px) {
section div.subsections div.subsection {
flex-basis: 100%; } }
section div.image img {
width: 100%; }
section div.title {
@ -199,6 +167,12 @@ section {
text-align: center;
padding: 1em 0; }
section.contactsection div#message {
display: none; }
section.contactsection textarea {
font-family: inherit; }
/* Form elements */
form.cms div.global_error {
border: 2px dotted red;

File diff suppressed because one or more lines are too long

Wyświetl plik

@ -0,0 +1,21 @@
{% extends 'cms/sections/base.html' %}
{% load i18n %}
{% block section %}
<div class="wrapper">
<div class="title">
<h1>{{section.title}}</h1>
</div>
<div class="form">
<form method="post" class="cms">
{% csrf_token %}
{% for field in section.form %}
{% include 'cms/formfield.html' with field=field %}
{% endfor %}
<button class="button" name="section" value="{{section.pk}}">{% trans 'Send' %}</button>
</form>
</div>
</div>
{% endblock %}

Wyświetl plik

@ -1,23 +1,65 @@
import json
'''CMS Views'''
import json
import swapper
from django.urls import reverse
from django.views import generic
from django.shortcuts import redirect
from django.core.exceptions import ImproperlyConfigured
from django.contrib.contenttypes.models import ContentType
from django.contrib.auth.mixins import UserPassesTestMixin
from django.http import HttpResponseRedirect, HttpResponseBadRequest
from .decorators import register_view
from .forms import PageForm, SectionForm
from .utils import get_config
import swapper
Page = swapper.load_model('cms', 'Page')
Section = swapper.load_model('cms', 'Section')
class StaffRequiredMixin(UserPassesTestMixin):
def test_func(self):
return self.request.user.is_staff
@register_view(Section)
class SectionView:
'''Generic section view'''
def get(self, request, section):
'''Override this to add custom attributes to a section'''
return section
class SectionWithFormView(SectionView):
'''Generic section with associated form'''
form_class = None
success_url = None
def get_form_class(self):
'''Return the form class to use in this view.'''
if self.form_class:
return self.form_class
raise ImproperlyConfigured(
'Either specify formclass attribute or override get_form_class()')
def get_success_url(self):
'''Return the URL to redirect to after processing a valid form.'''
if self.success_url:
return self.success_url
raise ImproperlyConfigured(
'Either specify success_url attribute or override get_success_url()')
def get(self, request, section):
'''Add form to section'''
form = self.get_form_class()()
section.form = form
return section
def post(self, request, section):
'''Process form'''
form = self.get_form_class()(request.POST, request.FILES)
if form.is_valid():
form.save(request)
return redirect(self.get_success_url())
section.form = form
return section
class MenuMixin:
'''Add pages and footer to template context'''
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
pages = Page.objects.filter(menu=True)
@ -28,11 +70,71 @@ class MenuMixin:
})
return context
class MemoryMixin:
'''Remember the previous page in session'''
def dispatch(self, request, *args, **kwargs):
if request.user.is_authenticated:
request.session['previous_url'] = request.path
return super().dispatch(request, *args, **kwargs)
class PageView(MenuMixin, MemoryMixin, generic.DetailView):
'''View of a page with heterogeneous (polymorphic) sections'''
model = Page
template_name = 'cms/page.html'
def setup(self, *args, slug='', **kwargs):
'''Supply a default argument for slug'''
super().setup(*args, slug=slug, **kwargs)
def get(self, request, *args, **kwargs):
'''Call each sections's get() view before rendering final response'''
page = self.object = self.get_object()
context = self.get_context_data(**kwargs)
sections = page.sections.all()
for section in sections:
view = section.__class__.view_class()
view.get(request, section)
context.update({
'page': page,
'sections': sections,
})
return self.render_to_response(context)
def post(self, request, **kwargs):
'''Call the post() function of the correct section view'''
try:
pk = int(self.request.POST.get('section'))
except:
return HttpResponseBadRequest()
page = self.object = self.get_object()
context = self.get_context_data(**kwargs)
sections = page.sections.all()
for section in sections:
view = section.__class__.view_class()
if section.pk == pk:
result = view.post(request, section)
if isinstance(result, HttpResponseRedirect):
return result
else:
view.get(request, section)
context.update({
'page': page,
'sections': sections,
})
return self.render_to_response(context)
# The following views all require a logged-in staff member
class StaffRequiredMixin(UserPassesTestMixin):
def test_func(self):
return self.request.user.is_staff
class TypeMixin(MenuMixin):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
fields_per_type = {}
for model, desc in Section.TYPES:
for model, _ in Section.TYPES:
ctype = ContentType.objects.get(
app_label=Section._meta.app_label,
model=model.lower(),
@ -44,34 +146,6 @@ class TypeMixin(MenuMixin):
})
return context
class MemoryMixin:
def dispatch(self, request, *args, **kwargs):
if request.user.is_authenticated:
request.session['previous_url'] = request.path
return super().dispatch(request, *args, **kwargs)
class PageView(MenuMixin, MemoryMixin, generic.DetailView):
model = Page
template_name = 'cms/page.html'
# Supplies a default argument for slug
def setup(self, *args, slug='', **kwargs):
super().setup(*args, slug=slug, **kwargs)
#self.request = request
#self.args = args
#self.kwargs = kwargs
#self.kwargs['slug'] = slug
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
page = self.object
sections = page.sections.all()
context.update({
'page': page,
'sections': sections,
})
return context
class BaseUpdateView(generic.UpdateView):
template_name = 'cms/edit.html'

Wyświetl plik

@ -1,4 +1,4 @@
# Generated by Django 3.0.1 on 2020-01-02 00:06
# Generated by Django 3.0.1 on 2020-01-02 18:31
import ckeditor.fields
import cms.models
@ -13,8 +13,8 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.CMS_PAGE_MODEL),
('contenttypes', '0002_remove_content_type_name'),
migrations.swappable_dependency(settings.CMS_PAGE_MODEL),
]
operations = [
@ -38,7 +38,7 @@ class Migration(migrations.Migration):
name='Section',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('type', cms.models.VarCharChoiceField(choices=[('textsection', 'Tekst'), ('imagesection', 'Afbeelding')], default='', verbose_name='section type')),
('type', cms.models.VarCharChoiceField(choices=[('textsection', 'Tekst'), ('imagesection', 'Afbeelding'), ('contactsection', 'Contact')], default='', verbose_name='section type')),
('position', models.PositiveIntegerField(blank=True, verbose_name='position')),
('title', cms.models.VarCharField(blank=True, verbose_name='title')),
('color', models.PositiveIntegerField(choices=[(1, 'Wit')], default=1, verbose_name='color')),
@ -57,6 +57,17 @@ class Migration(migrations.Migration):
'abstract': False,
},
),
migrations.CreateModel(
name='ContactSection',
fields=[
],
options={
'proxy': True,
'indexes': [],
'constraints': [],
},
bases=('app.section',),
),
migrations.CreateModel(
name='ImageSection',
fields=[

Wyświetl plik

@ -1,4 +1,5 @@
from cms.models import *
from cms.models import BasePage, BaseSection
from cms.decorators import register_model
class Page(BasePage):
'''Add custom fields here. Already existing fields: position, title,
@ -12,14 +13,20 @@ class Section(BaseSection):
'''
@register('Tekst')
@register_model('Tekst')
class TextSection(Section):
fields = ['type', 'position', 'title', 'content']
class Meta:
proxy = True
@register('Afbeelding')
@register_model('Afbeelding')
class ImageSection(Section):
fields = ['type', 'position', 'title', 'image']
class Meta:
proxy = True
@register_model('Contact')
class ContactSection(Section):
fields = ['type', 'position', 'title']
class Meta:
proxy = True

Wyświetl plik

@ -0,0 +1,9 @@
from cms.forms import ContactForm
from cms.views import SectionWithFormView, register_view
from .models import *
@register_view(ContactSection)
class ContactFormView(SectionWithFormView):
form_class = ContactForm
success_url = '/thanks/'

Wyświetl plik

@ -4,6 +4,7 @@ try:
DEBUG = False
except ImportError:
DEBUG = True
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
PROJECT_NAME = 'example'
KEYFILE = f'/tmp/{PROJECT_NAME}.secret'

Wyświetl plik

@ -3,7 +3,7 @@ from setuptools import setup, find_packages
setup(
name = 'django-simplecms',
version = '2.0.0',
version = '2.1.0',
url = 'https://github.com/rtts/django-simplecms',
author = 'Jaap Joris Vens',
author_email = 'jj@rtts.eu',