kopia lustrzana https://github.com/rtts/django-simplecms
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
rodzic
25350b4706
commit
e0dddeda08
|
@ -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')
|
||||
|
|
|
@ -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
|
21
cms/forms.py
21
cms/forms.py
|
@ -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
|
||||
|
|
|
@ -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')),
|
||||
|
|
|
@ -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')
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
@ -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 %}
|
144
cms/views.py
144
cms/views.py
|
@ -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'
|
||||
|
||||
|
|
|
@ -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=[
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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/'
|
|
@ -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'
|
||||
|
|
2
setup.py
2
setup.py
|
@ -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',
|
||||
|
|
Ładowanie…
Reference in New Issue