A new templatetag 'includesection' now renders a section with its own

context, as provided by the polymorphic subsection's registered view.

Also, I'm trying to move all the website-related cruft from cms into the
example project, so that only the Page and Section models with their own
"admin" views will remain.
readwriteweb
Jaap Joris Vens 2020-01-05 03:36:23 +01:00
rodzic 9e1baf6ee1
commit 5f5f303187
23 zmienionych plików z 254 dodań i 130 usunięć

Wyświetl plik

@ -1,5 +1,4 @@
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
@ -7,25 +6,6 @@ import swapper
Page = swapper.load_model('cms', 'Page')
Section = swapper.load_model('cms', 'Section')
class ContactForm(forms.Form):
sender = forms.EmailField(label=_('Your email address'))
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,11 +1,10 @@
# Generated by Django 3.0.1 on 2020-01-02 20:41
# Generated by Django 3.0.2 on 2020-01-04 22:55
import cms.models
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import embed_video.fields
import markdownfield.models
class Migration(migrations.Migration):
@ -21,9 +20,9 @@ class Migration(migrations.Migration):
name='Page',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('position', models.PositiveIntegerField(blank=True, verbose_name='position')),
('title', cms.models.VarCharField(verbose_name='title')),
('slug', models.SlugField(blank=True, help_text='A short identifier to use in URLs', unique=True, verbose_name='slug')),
('title', cms.models.VarCharField(verbose_name='title')),
('position', models.PositiveIntegerField(blank=True, verbose_name='position')),
('menu', models.BooleanField(default=True, verbose_name='visible in menu')),
],
options={
@ -38,12 +37,10 @@ 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'), ('contactsection', 'Contact')], default='', verbose_name='section type')),
('position', models.PositiveIntegerField(blank=True, verbose_name='position')),
('type', cms.models.VarCharChoiceField(choices=[('textsection', 'Tekst'), ('imagesection', 'Afbeelding'), ('contactsection', 'Contact')], default='', verbose_name='type')),
('title', cms.models.VarCharField(blank=True, verbose_name='title')),
('color', models.PositiveIntegerField(choices=[(1, 'Wit')], default=1, verbose_name='color')),
('content', markdownfield.models.MarkdownField(blank=True, verbose_name='content')),
('content_rendered', markdownfield.models.RenderedMarkdownField(editable=False)),
('position', models.PositiveIntegerField(blank=True, verbose_name='position')),
('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')),
('button_text', cms.models.VarCharField(blank=True, verbose_name='button text')),

Wyświetl plik

@ -4,13 +4,10 @@ import swapper
from django.db import models
from django.urls import reverse
from django.conf import settings
from django.forms import TextInput, Select
from django.utils.translation import gettext_lazy as _
from embed_video.fields import EmbedVideoField
from polymorphic.models import PolymorphicModel
from markdownfield.models import MarkdownField, RenderedMarkdownField
from markdownfield.validators import VALIDATOR_NULL
from numberedmodel.models import NumberedModel
@ -56,9 +53,7 @@ class BaseSection(NumberedModel, PolymorphicModel):
type = VarCharChoiceField(_('type'), default='', choices=TYPES)
title = VarCharField(_('title'), blank=True)
position = models.PositiveIntegerField(_('position'), blank=True)
color = models.PositiveIntegerField(_('color'), default=1, choices=settings.SECTION_COLORS)
content = MarkdownField(_('content'), rendered_field='content_rendered', validator=VALIDATOR_NULL, use_admin_editor=False, blank=True)
content_rendered = RenderedMarkdownField()
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'))
button_text = VarCharField(_('button text'), blank=True)

Wyświetl plik

@ -0,0 +1,7 @@
{% load i18n %}
{% if request.user.is_staff %}
<div class="edit">
<a href="{% url 'cms:updatesection' section.pk %}">{% trans 'edit this section' %}</a>
</div>
{% endif %}

Wyświetl plik

@ -1,13 +1,12 @@
{% extends 'base.html' %}
{% load i18n %}
{% load cms %}
{% block title %}{{block.super}} - {{page.title}}{% endblock %}
{% block content %}
{% for section in sections %}
<section class="{{section.type}} color{{section.color}}">
{% include 'cms/sections/'|add:section.type|lower|add:'.html' %}
</section>
{% include_section section %}
{% endfor %}
<div class="edit page">

Wyświetl plik

@ -1,6 +1,7 @@
{% load i18n thumbnail embed_video_tags %}
{% load markdown %}
{% block section %}
<section class="{{section.type}} color{{section.color}}">
<div class="wrapper">
{% if section.image %}
@ -17,9 +18,9 @@
</div>
{% endif %}
{% if section.content_rendered %}
{% if section.content %}
<div class="content">
{{section.content_rendered|safe}}
{{section.content|markdown}}
</div>
{% endif %}
@ -31,32 +32,12 @@
</div>
{% endif %}
{% if section.subsections.exists %}
<div class="subsections">
{% for sub in section.subsections.all %}
<div class="subsection color{{sub.color}}">
{% with section=sub user=None %}
{% include 'cms/sections/base.html' %}
{% endwith %}
</div>
{% endfor %}
</div>
{% endif %}
{% if section.button_text %}
<div class="button">
<a class="button" href="{{section.button_link}}">{{section.button_text}}</a>
</div>
{% endif %}
</div>
{% endblock %}
{% block extracontent %}{% endblock %}
{% if user.is_staff %}
<div class="wrapper">
<div class="edit">
<a href="{% url 'cms:updatesection' section.pk %}">{% trans 'edit this section' %}</a>
</div>
{% include 'cms/editlink.html' %}
</div>
{% endif %}
</section>

Wyświetl plik

@ -0,0 +1,29 @@
from django import template
register = template.Library()
@register.tag('include_section')
def do_include(parser, token):
'''Renders the section with its own context
'''
_, section = token.split_contents()
return IncludeSectionNode(section)
class IncludeSectionNode(template.Node):
def __init__(self, section):
self.section = template.Variable(section)
self.csrf_token = template.Variable('csrf_token')
super().__init__()
def render(self, context):
section = self.section.resolve(context)
template_name = section.view.template_name
if template_name is None:
raise ValueError(f'{section} view has no template_name attribute')
csrf_token = self.csrf_token.resolve(context)
if not hasattr(section, 'context'):
raise ValueError(dir(section))
section.context.update({'csrf_token': csrf_token})
t = context.template.engine.get_template(template_name)
return t.render(template.Context(section.context))

Wyświetl plik

@ -0,0 +1,15 @@
from markdown import markdown as md
from django import template
from django.conf import settings
from django.utils.safestring import mark_safe
EXTENSIONS = getattr(settings, 'MARKDOWN_EXTENSIONS', [])
register = template.Library()
@register.filter(is_safe=True)
def markdown(value):
'''Runs Markdown over a given value
'''
return mark_safe(md(value, extensions=EXTENSIONS))

Wyświetl plik

@ -9,8 +9,6 @@ urlpatterns = [
path('updatesection/<int:pk>/', UpdateSection.as_view(), name='updatesection'),
path('createpage/', CreatePage.as_view(), name='createpage'),
path('createsection/<int:pk>', CreateSection.as_view(), name='createsection'),
# Feel free to copy the following into your root URL conf!
path('', PageView.as_view(), name='page'),
path('<slug:slug>/', PageView.as_view(), name='page'),
]

Wyświetl plik

@ -4,6 +4,7 @@ import json
import swapper
from django.views import generic
from django.views.generic.edit import FormMixin
from django.shortcuts import redirect
from django.core.exceptions import ImproperlyConfigured
from django.contrib.contenttypes.models import ContentType
@ -19,43 +20,27 @@ Section = swapper.load_model('cms', 'Section')
@register_view(Section)
class SectionView:
'''Generic section view'''
def get(self, request, section):
'''Override this to add custom attributes to a section'''
return section
template_name = 'cms/sections/section.html'
class SectionWithFormView(SectionView):
def setup(self, request, section):
'''Initialize request and section attributes'''
self.request = request
self.section = section
def get_context_data(self, **kwargs):
'''Override this to customize a section's context'''
return kwargs
class SectionFormView(FormMixin, 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):
def post(self, request):
'''Process form'''
form = self.get_form_class()(request.POST, request.FILES)
form = self.get_form()
if form.is_valid():
form.save(request)
return redirect(self.get_success_url())
section.form = form
return section
return form
class MenuMixin:
'''Add pages to template context'''
@ -83,14 +68,21 @@ class PageView(MenuMixin, MemoryMixin, generic.DetailView):
'''Supply a default argument for slug'''
super().setup(*args, slug=slug, **kwargs)
def initialize_section(self, section):
section.view = section.__class__.view_class()
section.view.setup(self.request, section)
section.context = section.view.get_context_data(
request = self.request,
section = section,
)
def get(self, request, *args, **kwargs):
'''Call each sections's get() view before rendering final response'''
'''Initialize sections and render 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)
self.initialize_section(section)
context.update({
'page': page,
'sections': sections,
@ -98,7 +90,8 @@ class PageView(MenuMixin, MemoryMixin, generic.DetailView):
return self.render_to_response(context)
def post(self, request, **kwargs):
'''Call the post() function of the correct section view'''
'''Initialize sections and call the post() function of the correct
section view'''
try:
pk = int(self.request.POST.get('section'))
except:
@ -108,20 +101,20 @@ class PageView(MenuMixin, MemoryMixin, generic.DetailView):
context = self.get_context_data(**kwargs)
sections = page.sections.all()
for section in sections:
view = section.__class__.view_class()
self.initialize_section(section)
if section.pk == pk:
result = view.post(request, section)
result = section.view.post(request)
if isinstance(result, HttpResponseRedirect):
return result
else:
view.get(request, section)
section.context['form'] = result
context.update({
'page': page,
'sections': sections,
})
return self.render_to_response(context)
# The following views all require a logged-in staff member
# The following views require a logged-in staff member
class StaffRequiredMixin(UserPassesTestMixin):
def test_func(self):

Wyświetl plik

@ -0,0 +1,22 @@
from django import forms
from django.core.mail import EmailMessage
from django.utils.translation import gettext_lazy as _
class ContactForm(forms.Form):
sender = forms.EmailField(label=_('Your email address'))
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()

Wyświetl plik

@ -1,11 +1,10 @@
# Generated by Django 3.0.1 on 2020-01-02 20:42
# Generated by Django 3.0.2 on 2020-01-05 02:29
import cms.models
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import embed_video.fields
import markdownfield.models
class Migration(migrations.Migration):
@ -13,8 +12,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 = [
@ -22,9 +21,9 @@ class Migration(migrations.Migration):
name='Page',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('position', models.PositiveIntegerField(blank=True, verbose_name='position')),
('title', cms.models.VarCharField(verbose_name='title')),
('slug', models.SlugField(blank=True, help_text='A short identifier to use in URLs', unique=True, verbose_name='slug')),
('title', cms.models.VarCharField(verbose_name='title')),
('position', models.PositiveIntegerField(blank=True, verbose_name='position')),
('menu', models.BooleanField(default=True, verbose_name='visible in menu')),
],
options={
@ -38,16 +37,15 @@ 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'), ('contactsection', 'Contact')], default='', verbose_name='section type')),
('position', models.PositiveIntegerField(blank=True, verbose_name='position')),
('type', cms.models.VarCharChoiceField(choices=[('textsection', 'Tekst'), ('buttonsection', 'Button'), ('imagesection', 'Afbeelding'), ('videosection', 'Video'), ('contactsection', 'Contact')], default='', verbose_name='type')),
('title', cms.models.VarCharField(blank=True, verbose_name='title')),
('color', models.PositiveIntegerField(choices=[(1, 'Wit')], default=1, verbose_name='color')),
('content', markdownfield.models.MarkdownField(blank=True, verbose_name='content')),
('content_rendered', markdownfield.models.RenderedMarkdownField(editable=False)),
('position', models.PositiveIntegerField(blank=True, verbose_name='position')),
('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')),
('button_text', cms.models.VarCharField(blank=True, verbose_name='button text')),
('button_link', cms.models.VarCharField(blank=True, verbose_name='button link')),
('color', models.PositiveIntegerField(choices=[(1, 'Wit')], default=1, verbose_name='kleur')),
('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_app.section_set+', to='contenttypes.ContentType')),
],
@ -58,6 +56,17 @@ class Migration(migrations.Migration):
'abstract': False,
},
),
migrations.CreateModel(
name='ButtonSection',
fields=[
],
options={
'proxy': True,
'indexes': [],
'constraints': [],
},
bases=('app.section',),
),
migrations.CreateModel(
name='ContactSection',
fields=[
@ -91,4 +100,15 @@ class Migration(migrations.Migration):
},
bases=('app.section',),
),
migrations.CreateModel(
name='VideoSection',
fields=[
],
options={
'proxy': True,
'indexes': [],
'constraints': [],
},
bases=('app.section',),
),
]

Wyświetl plik

@ -1,3 +1,5 @@
from django.db import models
from django.conf import settings
from cms.models import BasePage, BaseSection
from cms.decorators import register_model
@ -9,13 +11,20 @@ class Page(BasePage):
class Section(BaseSection):
'''Add custom fields here. Already existing fields: type, position,
title, color, content, image, video, button_text, button_link
title, content, image, video, button_text, button_link
'''
color = models.PositiveIntegerField('kleur', default=1, choices=settings.SECTION_COLORS)
@register_model('Tekst')
class TextSection(Section):
fields = ['type', 'position', 'title', 'content']
fields = ['type', 'position', 'title', 'color', 'content']
class Meta:
proxy = True
@register_model('Button')
class ButtonSection(Section):
fields = ['type', 'position', 'title', 'button_text', 'button_link']
class Meta:
proxy = True
@ -25,6 +34,12 @@ class ImageSection(Section):
class Meta:
proxy = True
@register_model('Video')
class VideoSection(Section):
fields = ['type', 'position', 'title', 'video']
class Meta:
proxy = True
@register_model('Contact')
class ContactSection(Section):
fields = ['type', 'position', 'title']

Wyświetl plik

@ -0,0 +1,11 @@
<section class="{{section.type}} color{{section.color}}">
<div class="wrapper">
{% if section.button_text %}
<div class="button">
<a class="button" href="{{section.button_link}}">{{section.button_text}}</a>
</div>
{% endif %}
{% include 'cms/editlink.html' %}
</div>
</section>

Wyświetl plik

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

Wyświetl plik

@ -0,0 +1,14 @@
{% load thumbnail %}
<section class="{{section.type}} color{{section.color}}">
<div class="wrapper">
{% if section.image %}
<div class="image">
<img alt="{{section.title}}" src="{% thumbnail section.image 800x800 %}">
</div>
{% endif %}
{% include 'cms/editlink.html' %}
</div>
</section>

Wyświetl plik

@ -0,0 +1,21 @@
{% load markdown %}
<section class="{{section.type}} color{{section.color}}">
<div class="wrapper">
{% if section.title %}
<div class="title">
<h1>
{{section.title}}
</h1>
</div>
{% endif %}
{% if section.content %}
<div class="content">
{{section.content|markdown}}
</div>
{% endif %}
{% include 'cms/editlink.html' %}
</div>
</section>

Wyświetl plik

@ -0,0 +1,16 @@
{% load embed_video_tags %}
<section class="{{section.type}} color{{section.color}}">
<div class="wrapper">
{% if section.video %}
<div class="video">
<div class="iframe">
{% video section.video '800x600' %}
</div>
</div>
{% endif %}
{% include 'cms/editlink.html' %}
</div>
</section>

Wyświetl plik

@ -1 +0,0 @@
{% extends 'cms/sections/base.html' %}

Wyświetl plik

@ -1 +0,0 @@
{% extends 'cms/sections/base.html' %}

Wyświetl plik

@ -1,10 +1,27 @@
from cms.forms import ContactForm
from cms.views import SectionWithFormView
from cms.views import SectionView, SectionFormView
from cms.decorators import register_view
from .models import *
from .forms import ContactForm
@register_view(TextSection)
class TextView(SectionView):
template_name = 'app/sections/text.html'
@register_view(ButtonSection)
class ButtonView(SectionView):
template_name = 'app/sections/button.html'
@register_view(ImageSection)
class ImageView(SectionView):
template_name = 'app/sections/image.html'
@register_view(VideoSection)
class VideoView(SectionView):
template_name = 'app/sections/video.html'
@register_view(ContactSection)
class ContactFormView(SectionWithFormView):
class ContactFormView(SectionFormView):
form_class = ContactForm
success_url = '/thanks/'
template_name = 'app/sections/contact.html'

Wyświetl plik

@ -7,7 +7,6 @@ except ImportError:
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
PROJECT_NAME = 'example'
SITE_URL = 'https://example.com/'
KEYFILE = f'/tmp/{PROJECT_NAME}.secret'
ADMINS = [('JJ Vens', 'jj@rtts.eu')]
ALLOWED_HOSTS = ['*']
@ -25,7 +24,6 @@ MEDIA_ROOT = '/srv/' + PROJECT_NAME + '/media'
LOGIN_REDIRECT_URL = '/'
CMS_SECTION_MODEL = 'app.Section'
MARKDOWN_EXTENSIONS = ['extra', 'smarty']
MARKDOWN_EXTENSION_CONFIGS = {'extra': {}, 'smarty': {}}
def read(file):
with open(file) as f:

Wyświetl plik

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