New version! Modelled after Django's admin application, simplecms now allows

you to define everything related to it in the file 'cms.py'. No more
inherited proxy models and no more migrations every time you add/remove a
section type! In fact, the dependency on django-polymorphic has completely
been removed!

The example project has been updated and should get you started.
Documentation will be coming soon!
main
Jaap Joris Vens 2020-03-21 18:49:41 +01:00
rodzic 09f03b6866
commit 3cc1f9ec08
37 zmienionych plików z 734 dodań i 703 usunięć

Wyświetl plik

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

25
cms/cms.py 100644
Wyświetl plik

@ -0,0 +1,25 @@
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,14 +1,25 @@
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
def register(verbose_name):
import swapper
Section = swapper.load_model('cms', 'Section')
'''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 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 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 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,6 +1,7 @@
import swapper
from django import forms
from django.conf import settings
from django.db.models import Prefetch
from django.contrib.contenttypes.models import ContentType
from django.core.mail import EmailMessage
from django.utils.translation import gettext_lazy as _
@ -13,7 +14,6 @@ class PageForm(forms.ModelForm):
super().__init__(*args, **kwargs)
self.label_suffix = ''
extra = 1 if self.instance.pk else 2
self.formsets = [forms.inlineformset_factory(
parent_model = Page,
model = Section,
@ -37,7 +37,7 @@ class PageForm(forms.ModelForm):
def clean(self):
super().clean()
if not self.formsets[0].is_valid():
self.add_error(None, _('Theres a problem saving one of the sections'))
self.add_error(None, repr(self.formsets[0].errors))
def save(self, *args, **kwargs):
page = super().save()
@ -60,21 +60,21 @@ class SectionForm(forms.ModelForm):
super().__init__(*args, **kwargs)
self.label_suffix = ''
self.fields['DELETE'] = forms.BooleanField(label=_('Delete'), required=False)
extra = 1 if self.instance.pk else 2
# Repopulate the 'choices' attribute of the type field from
# the child model.
self.fields['type'].choices = self._meta.model.TYPES
self.fields['type'].initial = self._meta.model.TYPES[0][0]
# Populate the 'formsets' attribute if the Section was
# extendend with one_to_many fields
self.formsets = []
for field in self.instance._meta.get_fields():
if field.one_to_many:
extra = 1 if getattr(self.instance, field.name).exists() else 2
formset = forms.inlineformset_factory(
Section, field.related_model,
parent_model=Section,
model=field.related_model,
fields='__all__',
extra=extra,
)(
@ -107,19 +107,9 @@ class SectionForm(forms.ModelForm):
if section.page.slug and not section.page.sections.exists():
section.page.delete()
return
# Explanation: get the content type of the model that the user
# supplied when filling in this form, and save it's id to the
# 'polymorphic_ctype_id' field. The next time the object is
# requested from the database, django-polymorphic will convert
# it to the correct subclass.
section.polymorphic_ctype = ContentType.objects.get(
app_label=section._meta.app_label,
model=section.type.lower(),
)
if commit:
elif commit:
section.save()
for formset in self.formsets:
formset.save(commit=commit)

Wyświetl plik

@ -0,0 +1,17 @@
# 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

@ -2,11 +2,12 @@ import swapper
from django.db import models
from django.urls import reverse
from django.forms import TextInput, Select
from django.utils.text import slugify
from django.forms import TextInput, Select
from django.utils.translation import gettext_lazy as _
from django.core.exceptions import ImproperlyConfigured
from embed_video.fields import EmbedVideoField
from polymorphic.models import PolymorphicModel
class VarCharField(models.TextField):
'''Variable width CharField'''
@ -86,9 +87,13 @@ class BasePage(Numbered, models.Model):
verbose_name_plural = _('Pages')
ordering = ['number']
class BaseSection(Numbered, PolymorphicModel):
class BaseSection(Numbered, models.Model):
'''Abstract base model for sections'''
TYPES = [] # Will be populated by @register_model()
# 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'))
@ -113,6 +118,21 @@ class BaseSection(Numbered, PolymorphicModel):
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')

Wyświetl plik

@ -7,6 +7,24 @@ html, body {
padding: 0;
}
div.edit {
position: fixed;
right: 1em;
bottom: 1em;
z-index: 1000;
button {
padding: 0;
outline: none;
background: none;
border: none;
}
img {
width: 75px !important;
height: auto;
}
}
/* Form elements */
form.cms {

Wyświetl plik

@ -4,6 +4,20 @@ html, body {
margin: 0;
padding: 0; }
div.edit {
position: fixed;
right: 1em;
bottom: 1em;
z-index: 1000; }
div.edit button {
padding: 0;
outline: none;
background: none;
border: none; }
div.edit img {
width: 75px !important;
height: auto; }
/* Form elements */
form.cms {
background: #bfb;

Wyświetl plik

@ -5,5 +5,5 @@
"admin.scss"
],
"names": [],
"mappings": "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"
"mappings": "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"
}

Wyświetl plik

@ -1,246 +0,0 @@
$font: sans-serif;
$small: 500px;
$medium: 800px;
$blue: #3573a8;
html, body {
font-family: $font;
line-height: 1.33;
margin: 0;
padding: 0;
}
a {
color: $blue;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
table {
border-collapse: collapse;
th, td {
padding: 1em;
}
th {
border-bottom: 2px solid black;
}
}
div.wrapper {
box-sizing: border-box;
max-width: 700px;
margin: auto;
padding: 0 1rem;
}
div.spacer {
height: 1rem;
clear: both;
}
nav {
padding: 0;
button#hamburger {
position: absolute;
z-index: 1;
top: 0;
right: 0;
.hamburger-inner, .hamburger-inner:before, .hamburger-inner:after {
background: $blue;
}
&.is-active {
position: fixed;
}
&:hover {
opacity: 1 !important;
}
&:focus {
outline: none !important;
}
}
ul {
border-top: 2px solid $blue;
border-bottom: 2px solid $blue;
list-style: none;
margin: 0;
padding: 0;
text-align: center;
overflow: hidden;
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
align-content: start;
li {
margin: 0;
padding: 0;
display: inline-block;
a {
font-size: 1.25rem;
padding: 5px .5em;
transition: .1s ease;
display: inline-block;
font-weight: bold;
}
}
}
@media(min-width: $small) {
a:hover:not(.edit), a.current {
text-decoration: none;
color: white;
background: $blue;
}
button#hamburger {
display: none;
}
}
@media(max-width: $small) {
padding: 0;
button#hamburger {
display: block;
}
ul#menu {
position: fixed;
overflow-y: auto;
z-index: 1;
margin: 0;
padding: 0;
padding-top: 2em;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: white;
list-style: none;
li {
width: 100%;
box-sizing: border-box;
padding: 1em;
margin: 0 1em;
border-bottom: 1px solid #ddd;
line-height: 1.5;
text-align: center;
a {
text-decoration: none;
}
}
transition: .5s ease;
transform: translatex(100%);
&.visible {
transform: translatex(0);
}
}
}
}
div.edit {
text-align: center;
&.page {
position: fixed;
right: 1em;
bottom: 1em;
z-index: 1000;
button {
padding: 0;
outline: none;
}
img {
width: 75px;
height: auto;
}
a:before, button:before, a:after, button:after {
display: none;
}
}
}
div.edit a, div.edit button, a.edit{
font-family: inherit;
font-size: 1rem !important;
font-weight: normal !important;
color: red !important;
text-transform: none !important;
text-decoration: none;
color: inherit;
border: none;
display: inline;
background: none;
cursor: pointer;
&:before {
content: '[ ';
}
&:after {
content: ' ]';
}
}
section {
clear: both;
div.image {
img {
width: 100%;
}
}
div.title {
text-align: center;
}
div.content {
}
div.video {
div.iframe {
width: 100%;
padding-bottom: 56%;
position: relative;
iframe {
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
}
}
}
div.button {
text-align: center;
padding: 1em 0;
}
}
section.contact {
div.message {
display: none;
}
div.formfield {
padding: 0.5em 0;
}
form input, form textarea {
box-sizing: border-box;
font-family: inherit;
font-size: inherit;
display: block;
width: 100%;
padding: 0.5em;
margin: 0;
}
}

Wyświetl plik

@ -1,175 +1,2 @@
html, body {
font-family: sans-serif;
line-height: 1.33;
margin: 0;
padding: 0; }
a {
color: #3573a8;
text-decoration: none; }
a:hover {
text-decoration: underline; }
table {
border-collapse: collapse; }
table th, table td {
padding: 1em; }
table th {
border-bottom: 2px solid black; }
div.wrapper {
box-sizing: border-box;
max-width: 700px;
margin: auto;
padding: 0 1rem; }
div.spacer {
height: 1rem;
clear: both; }
nav {
padding: 0; }
nav button#hamburger {
position: absolute;
z-index: 1;
top: 0;
right: 0; }
nav button#hamburger .hamburger-inner, nav button#hamburger .hamburger-inner:before, nav button#hamburger .hamburger-inner:after {
background: #3573a8; }
nav button#hamburger.is-active {
position: fixed; }
nav button#hamburger:hover {
opacity: 1 !important; }
nav button#hamburger:focus {
outline: none !important; }
nav ul {
border-top: 2px solid #3573a8;
border-bottom: 2px solid #3573a8;
list-style: none;
margin: 0;
padding: 0;
text-align: center;
overflow: hidden;
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
align-content: start; }
nav ul li {
margin: 0;
padding: 0;
display: inline-block; }
nav ul li a {
font-size: 1.25rem;
padding: 5px .5em;
transition: .1s ease;
display: inline-block;
font-weight: bold; }
@media (min-width: 500px) {
nav a:hover:not(.edit), nav a.current {
text-decoration: none;
color: white;
background: #3573a8; }
nav button#hamburger {
display: none; } }
@media (max-width: 500px) {
nav {
padding: 0; }
nav button#hamburger {
display: block; }
nav ul#menu {
position: fixed;
overflow-y: auto;
z-index: 1;
margin: 0;
padding: 0;
padding-top: 2em;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: white;
list-style: none;
transition: .5s ease;
transform: translatex(100%); }
nav ul#menu li {
width: 100%;
box-sizing: border-box;
padding: 1em;
margin: 0 1em;
border-bottom: 1px solid #ddd;
line-height: 1.5;
text-align: center; }
nav ul#menu li a {
text-decoration: none; }
nav ul#menu.visible {
transform: translatex(0); } }
div.edit {
text-align: center; }
div.edit.page {
position: fixed;
right: 1em;
bottom: 1em;
z-index: 1000; }
div.edit.page button {
padding: 0;
outline: none; }
div.edit.page img {
width: 75px;
height: auto; }
div.edit.page a:before, div.edit.page button:before, div.edit.page a:after, div.edit.page button:after {
display: none; }
div.edit a, div.edit button, a.edit {
font-family: inherit;
font-size: 1rem !important;
font-weight: normal !important;
color: red !important;
text-transform: none !important;
text-decoration: none;
color: inherit;
border: none;
display: inline;
background: none;
cursor: pointer; }
div.edit a:before, div.edit button:before, a.edit:before {
content: '[ '; }
div.edit a:after, div.edit button:after, a.edit:after {
content: ' ]'; }
section {
clear: both; }
section div.image img {
width: 100%; }
section div.title {
text-align: center; }
section div.video div.iframe {
width: 100%;
padding-bottom: 56%;
position: relative; }
section div.video div.iframe iframe {
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0; }
section div.button {
text-align: center;
padding: 1em 0; }
section.contact div.message {
display: none; }
section.contact div.formfield {
padding: 0.5em 0; }
section.contact form input, section.contact form textarea {
box-sizing: border-box;
font-family: inherit;
font-size: inherit;
display: block;
width: 100%;
padding: 0.5em;
margin: 0; }
/*# sourceMappingURL=cms.scss.css.map */

File diff suppressed because one or more lines are too long

Wyświetl plik

@ -1,8 +1,8 @@
{% load static %}
{% load i18n %}
{% load static i18n %}
{% get_current_language as lang%}
<!DOCTYPE html>
<html lang="{% get_current_language as lang%}{{lang}}">
<html lang="{{lang}}">
<head>
<title>{% block title %}{% endblock %}</title>
<meta charset="utf-8"/>
@ -49,7 +49,6 @@
<footer>
{% block footer %}
{{footer|safe}}
{% endblock %}
</footer>

Wyświetl plik

@ -1,17 +1,15 @@
{% extends 'base.html' %}
{% load static %}
{% load cms %}
{% load static cms %}
{% block title %}{{block.super}} - {{page.title}}{% endblock %}
{% block content %}
{% for section in sections %}
<div id="{{section.title|slugify}}"></div>
{% include_section section %}
{% endfor %}
{% if perms.cms_page.change %}
<div class="edit page">
<div class="edit">
<a href="{% if page.slug %}{% url 'cms:updatepage' page.slug %}{% else %}{% url 'cms:updatepage' %}{% endif %}"><img src="{% static 'cms/edit.png' %}"></a>
</div>
{% endif %}

Wyświetl plik

@ -14,17 +14,21 @@ class IncludeSectionNode(template.Node):
def __init__(self, section):
self.section = template.Variable(section)
self.csrf_token = template.Variable('csrf_token')
self.request = template.Variable('request')
self.perms = template.Variable('perms')
super().__init__()
def render(self, context):
section = self.section.resolve(context)
template_name = section.view.template_name
csrf_token = self.csrf_token.resolve(context)
request = self.request.resolve(context)
perms = self.perms.resolve(context)
section.context.update({
'csrf_token': csrf_token,
'perms': perms,
})
t = context.template.engine.get_template(template_name)
return t.render(template.Context(section.context))
view = section.get_view(request)
section_context = view.get_context_data(
csrf_token=csrf_token,
section=section,
request=request,
perms=perms,
)
t = context.template.engine.get_template(view.template_name)
return t.render(template.Context(section_context))

Wyświetl plik

@ -0,0 +1,20 @@
from markdown import markdown as md
from django import template
from django.utils.safestring import mark_safe
MARKDOWN_EXTENSIONS = ['extra', 'smarty']
register = template.Library()
@register.simple_tag(takes_context=True)
def eval(context, expr):
'''USE WITH CAUTION!!!
This template tag runs its argument through Django's templating
system using the current context, placing all power into the
hands of the content editors.
Also, it applies Markdown.
'''
result = template.Template(expr).render(context)
return mark_safe(md(result, extensions=MARKDOWN_EXTENSIONS))

Wyświetl plik

@ -1,15 +0,0 @@
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

@ -3,45 +3,16 @@ import swapper
from django.shortcuts import redirect
from django.views.generic import base, detail, edit
from django.http import Http404, HttpResponse, HttpResponseRedirect
from django.utils.translation import gettext_lazy as _
from django.core.exceptions import ImproperlyConfigured
from django.contrib.contenttypes.models import ContentType
from django.contrib.auth.mixins import UserPassesTestMixin
from django.http import Http404, HttpResponse, HttpResponseRedirect, HttpResponseBadRequest
from .decorators import register_view
from .forms import PageForm, SectionForm
Page = swapper.load_model('cms', 'Page')
Section = swapper.load_model('cms', 'Section')
@register_view(Section)
class SectionView:
'''Generic section view'''
template_name = 'cms/sections/section.html'
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(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 (polymorphic) sections'''
'''View of a page with heterogeneous sections'''
model = Page
template_name = 'cms/page.html'
@ -49,27 +20,16 @@ class PageView(detail.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):
'''Initialize sections and render final response'''
'''Instantiate section views and render final response'''
try:
page = self.object = self.get_object()
except Http404:
if self.request.user.has_perm('cms_page_create'):
return redirect('cms:updatepage', self.kwargs['slug'])
else:
raise
raise
context = self.get_context_data(**kwargs)
sections = page.sections.all()
for section in sections:
self.initialize_section(section)
context.update({
'page': page,
'sections': sections,
@ -77,8 +37,7 @@ class PageView(detail.DetailView):
return self.render_to_response(context)
def post(self, request, **kwargs):
'''Initialize sections and call the post() function of the correct
section view'''
'''Call the post() method of the correct section view'''
try:
pk = int(self.request.POST.get('section'))
except:
@ -88,11 +47,9 @@ class PageView(detail.DetailView):
context = self.get_context_data(**kwargs)
sections = page.sections.all()
for section in sections:
self.initialize_section(section)
if section.pk == pk:
result = section.view.post(request)
if isinstance(result, HttpResponseRedirect):
return result
view = section.get_view(request)
result = view.post(request)
if isinstance(result, HttpResponse):
return result
section.context['form'] = result
@ -112,33 +69,26 @@ class PageView(detail.DetailView):
return context
class EditPage(UserPassesTestMixin, edit.ModelFormMixin, base.TemplateResponseMixin, base.View):
'''Base view with nested forms for editing the page and all its sections'''
model = Page
form_class = PageForm
template_name = 'cms/edit.html'
def test_func(self):
app, model = swapper.get_model_name('cms', 'page').lower().split('.')
return self.request.user.has_perm('f{app}_{model}_change')
'''Only allow users with the correct permissions'''
return self.request.user.has_perm('cms_page_change')
def get_form_kwargs(self):
'''Set the default slug to the current URL for new pages'''
kwargs = super().get_form_kwargs()
if 'slug' in self.kwargs:
kwargs.update({'initial': {'slug': self.kwargs['slug']}})
return kwargs
def get_context_data(self, **kwargs):
'''Populate the fields_per_type dict for use in javascript'''
context = super().get_context_data(**kwargs)
fields_per_type = {}
for model, _ in Section.TYPES:
ctype = ContentType.objects.get(
app_label = Section._meta.app_label,
model = model.lower(),
)
fields_per_type[ctype.model] = ['type', 'number'] + ctype.model_class().fields
context.update({
'fields_per_type': json.dumps(fields_per_type),
})
context['fields_per_type'] = json.dumps(Section.get_fields_per_type())
return context
def get_object(self):
@ -148,11 +98,13 @@ class EditPage(UserPassesTestMixin, edit.ModelFormMixin, base.TemplateResponseMi
except Http404:
return None
def get(self, request, *args, **kwargs):
def get(self, *args, **kwargs):
'''Handle GET requests'''
self.object = self.get_object()
return self.render_to_response(self.get_context_data())
return self.render_to_response(self.get_context_data(**kwargs))
def post(self, request, *args, **kwargs):
def post(self, *args, **kwargs):
'''Handle POST requests'''
self.object = self.get_object()
form = self.get_form()
@ -161,14 +113,15 @@ class EditPage(UserPassesTestMixin, edit.ModelFormMixin, base.TemplateResponseMi
if page:
return HttpResponseRedirect(page.get_absolute_url())
return HttpResponseRedirect('/')
return self.render_to_response(self.get_context_data(form=form))
return self.render_to_response(self.get_context_data(form=form, **kwargs))
class CreatePage(EditPage):
'''View for creating new pages'''
def get_object(self):
return Page()
class UpdatePage(EditPage):
pass
'''View for editing existing pages'''
class EditSection(UserPassesTestMixin, edit.ModelFormMixin, base.TemplateResponseMixin, base.View):
model = Section
@ -176,8 +129,7 @@ class EditSection(UserPassesTestMixin, edit.ModelFormMixin, base.TemplateRespons
template_name = 'cms/edit.html'
def test_func(self):
app, model = swapper.get_model_name('cms', 'section').lower().split('.')
return self.request.user.has_perm('f{app}_{model}_change')
return self.request.user.has_perm('cms_section_change')
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
@ -188,17 +140,7 @@ class EditSection(UserPassesTestMixin, edit.ModelFormMixin, base.TemplateRespons
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
fields_per_type = {}
for model, _ in Section.TYPES:
ctype = ContentType.objects.get(
app_label = Section._meta.app_label,
model = model.lower(),
)
fields_per_type[ctype.model] = ['type', 'number'] + ctype.model_class().fields
context.update({
'fields_per_type': json.dumps(fields_per_type),
})
context['fields_per_type'] = json.dumps(Section.get_fields_per_type())
return context
def get_object(self, queryset=None):
@ -215,11 +157,11 @@ class EditSection(UserPassesTestMixin, edit.ModelFormMixin, base.TemplateRespons
raise Http404()
return section
def get(self, request, *args, **kwargs):
def get(self, *args, **kwargs):
self.object = self.get_object()
return self.render_to_response(self.get_context_data())
return self.render_to_response(self.get_context_data(**kwargs))
def post(self, request, *args, **kwargs):
def post(self, *args, **kwargs):
self.object = self.get_object()
form = self.get_form()
@ -231,7 +173,7 @@ class EditSection(UserPassesTestMixin, edit.ModelFormMixin, base.TemplateRespons
return HttpResponseRedirect(self.page.get_absolute_url())
else:
return HttpResponseRedirect('/')
return self.render_to_response(self.get_context_data(form=form))
return self.render_to_response(self.get_context_data(form=form, **kwargs))
class CreateSection(EditSection):
def get_section(self):

Wyświetl plik

@ -1 +0,0 @@
default_app_config = 'app.apps.Config'

Wyświetl plik

@ -1,4 +0,0 @@
from django.apps import AppConfig
class Config(AppConfig):
name = 'app'
verbose_name = 'app'

25
example/app/cms.py 100644
Wyświetl plik

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

Wyświetl plik

@ -1,4 +1,4 @@
# Generated by Django 3.0.2 on 2020-02-16 14:27
# Generated by Django 3.0.2 on 2020-03-21 16:44
import cms.models
from django.conf import settings
@ -12,7 +12,6 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
]
operations = [
@ -20,9 +19,9 @@ class Migration(migrations.Migration):
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='title')),
('title', cms.models.VarCharField(verbose_name='page')),
('slug', models.SlugField(blank=True, unique=True, verbose_name='slug')),
('number', models.PositiveIntegerField(blank=True, verbose_name='number')),
('menu', models.BooleanField(default=True, verbose_name='visible in menu')),
],
options={
@ -37,15 +36,14 @@ class Migration(migrations.Migration):
name='Section',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('type', cms.models.VarCharField(blank=True, verbose_name='type')),
('title', cms.models.VarCharField(verbose_name='section')),
('type', cms.models.VarCharField(verbose_name='type')),
('number', models.PositiveIntegerField(blank=True, verbose_name='number')),
('title', cms.models.VarCharField(blank=True, verbose_name='title')),
('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='button link')),
('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_app.section_set+', to='contenttypes.ContentType')),
],
options={
'verbose_name': 'section',

Wyświetl plik

@ -0,0 +1,26 @@
# 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,46 +1,22 @@
from django.db import models
from django.conf import settings
from cms.models import BasePage, BaseSection
from cms.decorators import register_model
from django.utils.translation import gettext_lazy as _
class Page(BasePage):
'''Add custom fields here. Already existing fields: position, title,
slug, menu
'''Add custom fields here. Already existing fields: title, slug,
number, menu
'''
class Section(BaseSection):
'''Add custom fields here. Already existing fields: type, position,
title, content, image, video, href
'''Add custom fields here. Already existing fields: title, type,
number, content, image, video, href
'''
@register_model('Tekst')
class TextSection(Section):
fields = ['title', 'content']
class Meta:
proxy = True
class SectionImage(models.Model):
section = models.ForeignKey(Section, related_name='images', on_delete=models.CASCADE)
image = models.ImageField(_('Image'))
@register_model('Button')
class ButtonSection(Section):
fields = ['title', 'href']
class Meta:
proxy = True
@register_model('Afbeelding')
class ImageSection(Section):
fields = ['title', 'image']
class Meta:
proxy = True
@register_model('Video')
class VideoSection(Section):
fields = ['title', 'video']
class Meta:
proxy = True
@register_model('Contact')
class ContactSection(Section):
fields = ['title']
class Meta:
proxy = True
ordering = ['pk']

Wyświetl plik

@ -1,10 +1,43 @@
$small: 500px;
$medium: 800px;
$font: sans-serif;
$titlefont: sans-serif;
$small: 500px;
$medium: 800px;
$max-width: 700px;
$color1: #3573a8;
body {
html, body {
font-family: $font;
line-height: 1.5;
margin: 0;
padding: 0;
}
a {
color: $color1;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
a.button, button.button {
cursor: pointer;
font-family: $titlefont
font-size: 1.5em;
line-height: 1.25;
border-radius: 5px;
display: inline-block;
text-decoration: none;
border: none;
padding: 5px 20px;
background: $color1;
color: white;
box-sizing: border-box;
&:hover {
color: $color1;
background: white;
box-shadow: inset 0 0 0 2px $color1;
}
}
h1, h2, h3, h4, h5, h6 {
@ -16,32 +49,232 @@ h2 { font-size: 1.5em }
h3 { font-size: 1.25em }
h4, h5, h6 { font-size: 1em }
a {
&:hover {
a.edit {
color: red;
text-decoration: none;
font-size: 1rem;
font-weight: normal;
&:before { content: '[ ' }
&:after { content: ' ]' }
}
section a.edit {
position: absolute;
bottom: 0;
right: 1em;
}
table {
border-collapse: collapse;
th, td {
padding: 1em;
}
th {
border-bottom: 2px solid $color1;
}
}
a.button {
&:hover {
}
div.wrapper {
box-sizing: border-box;
max-width: $max-width;
margin: auto;
padding: 0 1rem;
}
div.spacer {
height: 1rem;
clear: both;
}
header {
h1 {
text-align: center;
}
img {
display: block;
width: 100%;
max-width: 400px;
margin: auto;
}
text-align: center;
}
nav {
button#hamburger {
position: absolute;
z-index: 1;
top: 0;
right: 0;
.hamburger-inner, .hamburger-inner:before, .hamburger-inner:after {
background: $color1;
}
&.is-active {
position: fixed;
}
&:hover {
opacity: 1 !important;
}
&:focus {
outline: none !important;
}
}
ul {
border-top: 2px solid $color1;
border-bottom: 2px solid $color1;
list-style: none;
margin: 0;
padding: 0;
text-align: center;
overflow: hidden;
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
align-content: start;
li {
margin: 0;
padding: 0;
display: inline-block;
a {
font-size: 1.25rem;
padding: 5px .5em;
transition: .1s ease;
display: inline-block;
font-weight: bold;
}
}
}
@media(min-width: $small) {
a:hover:not(.edit), a.current {
text-decoration: none;
color: white;
background: $color1;
}
button#hamburger {
display: none;
}
}
@media(max-width: $small) {
padding: 0;
button#hamburger {
display: block;
}
ul#menu {
position: fixed;
overflow-y: auto;
z-index: 1;
margin: 0;
padding: 0;
padding-top: 2em;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: white;
list-style: none;
li {
width: 100%;
box-sizing: border-box;
padding: 1em;
margin: 0 1em;
border-bottom: 1px solid #ddd;
line-height: 1.5;
text-align: center;
a {
text-decoration: none;
}
}
transition: .5s ease;
transform: translatex(100%);
&.visible {
transform: translatex(0);
}
}
}
}
div.edit {
position: fixed;
right: 1em;
bottom: 1em;
z-index: 1000;
button {
padding: 0;
outline: none;
background: none;
border: none;
}
img {
width: 75px;
height: auto;
}
}
section {
clear: both;
position: relative;
div.title {
text-align: center;
}
div.video {
div.iframe {
width: 100%;
padding-bottom: 56%;
position: relative;
iframe {
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
}
}
}
}
section.images {
div.images {
display: flex;
flex-wrap: wrap;
margin: 0.25em;
justify-content: center;
div.image {
flex: 1 1 100px;
max-width: $max-width;
margin: 0.5em;
img {
display: block;
width: 100%;
}
}
}
}
section.contact {
div.message {
display: none;
}
div.formfield {
padding: 0.5em 0;
}
form input, form textarea {
box-sizing: border-box;
font-family: inherit;
font-size: inherit;
display: block;
width: 100%;
padding: 0.5em;
margin: 0;
}
}
footer {
margin-top: 4em;
min-height: 400px;
min-height: 10em;
}

Wyświetl plik

@ -1,5 +1,14 @@
body {
font-family: sans-serif; }
html, body {
font-family: sans-serif;
line-height: 1.5;
margin: 0;
padding: 0; }
a {
color: #3573a8;
text-decoration: none; }
a:hover {
text-decoration: underline; }
h1, h2, h3, h4, h5, h6 {
margin: .5em 0;
@ -17,17 +26,179 @@ h3 {
h4, h5, h6 {
font-size: 1em; }
header h1 {
a.edit {
color: red;
text-decoration: none;
font-size: 1rem;
font-weight: normal; }
a.edit:before {
content: '[ '; }
a.edit:after {
content: ' ]'; }
section a.edit {
position: absolute;
bottom: 0;
right: 1em; }
table {
border-collapse: collapse; }
table th, table td {
padding: 1em; }
table th {
border-bottom: 2px solid #3573a8; }
div.wrapper {
box-sizing: border-box;
max-width: 700px;
margin: auto;
padding: 0 1rem; }
div.spacer {
height: 1rem;
clear: both; }
header {
text-align: center; }
header img {
nav button#hamburger {
position: absolute;
z-index: 1;
top: 0;
right: 0; }
nav button#hamburger .hamburger-inner, nav button#hamburger .hamburger-inner:before, nav button#hamburger .hamburger-inner:after {
background: #3573a8; }
nav button#hamburger.is-active {
position: fixed; }
nav button#hamburger:hover {
opacity: 1 !important; }
nav button#hamburger:focus {
outline: none !important; }
nav ul {
border-top: 2px solid #3573a8;
border-bottom: 2px solid #3573a8;
list-style: none;
margin: 0;
padding: 0;
text-align: center;
overflow: hidden;
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
align-content: start; }
nav ul li {
margin: 0;
padding: 0;
display: inline-block; }
nav ul li a {
font-size: 1.25rem;
padding: 5px .5em;
transition: .1s ease;
display: inline-block;
font-weight: bold; }
@media (min-width: 500px) {
nav a:hover:not(.edit), nav a.current {
text-decoration: none;
color: white;
background: #3573a8; }
nav button#hamburger {
display: none; } }
@media (max-width: 500px) {
nav {
padding: 0; }
nav button#hamburger {
display: block; }
nav ul#menu {
position: fixed;
overflow-y: auto;
z-index: 1;
margin: 0;
padding: 0;
padding-top: 2em;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: white;
list-style: none;
transition: .5s ease;
transform: translatex(100%); }
nav ul#menu li {
width: 100%;
box-sizing: border-box;
padding: 1em;
margin: 0 1em;
border-bottom: 1px solid #ddd;
line-height: 1.5;
text-align: center; }
nav ul#menu li a {
text-decoration: none; }
nav ul#menu.visible {
transform: translatex(0); } }
div.edit {
position: fixed;
right: 1em;
bottom: 1em;
z-index: 1000; }
div.edit button {
padding: 0;
outline: none;
background: none;
border: none; }
div.edit img {
width: 75px;
height: auto; }
section {
clear: both;
position: relative; }
section div.title {
text-align: center; }
section div.video div.iframe {
width: 100%;
padding-bottom: 56%;
position: relative; }
section div.video div.iframe iframe {
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0; }
section.images div.images {
display: flex;
flex-wrap: wrap;
margin: 0.25em;
justify-content: center; }
section.images div.images div.image {
flex: 1 1 100px;
max-width: 700px;
margin: 0.5em; }
section.images div.images div.image img {
display: block;
width: 100%; }
section.contact div.message {
display: none; }
section.contact div.formfield {
padding: 0.5em 0; }
section.contact form input, section.contact form textarea {
box-sizing: border-box;
font-family: inherit;
font-size: inherit;
display: block;
width: 100%;
max-width: 400px;
margin: auto; }
padding: 0.5em;
margin: 0; }
footer {
margin-top: 4em;
min-height: 400px; }
min-height: 10em; }
/*# sourceMappingURL=main1.scss.css.map */

File diff suppressed because one or more lines are too long

Wyświetl plik

@ -1,7 +0,0 @@
<section class="button">
<div class="wrapper">
<div class="button">
<a class="button" href="{{section.href}}">{{section.title}}</a>
</div>
</div>
</section>

Wyświetl plik

@ -15,4 +15,8 @@
</form>
</div>
</div>
{% if perms.cms_section_change %}
<a class="edit" href="{% url 'cms:updatesection' section.number %}">{% trans 'edit' %}</a><br>
{% endif %}
</section>

Wyświetl plik

@ -1,11 +0,0 @@
{% load thumbnail %}
<section class="image">
<div class="wrapper">
{% if section.image %}
<div class="image">
<img alt="{{section.title}}" src="{% thumbnail section.image 800x800 %}">
</div>
{% endif %}
</div>
</section>

Wyświetl plik

@ -0,0 +1,15 @@
{% load thumbnail i18n %}
<section class="images">
<div class="images">
{% for image in section.images.all %}
<div class="image">
<img src="{% thumbnail image.image 700x700 %}">
</div>
{% endfor %}
</div>
{% if perms.cms_section_change %}
<a class="edit" href="{% url 'cms:updatesection' section.number %}">{% trans 'edit' %}</a><br>
{% endif %}
</section>

Wyświetl plik

@ -1,4 +1,4 @@
{% load markdown %}
{% load eval i18n %}
<section class="text">
<div class="wrapper">
@ -9,7 +9,11 @@
</div>
<div class="content">
{{section.content|markdown}}
{% eval section.content %}
</div>
</div>
{% if perms.cms_section_change %}
<a class="edit" href="{% url 'cms:updatesection' section.number %}">{% trans 'edit' %}</a><br>
{% endif %}
</section>

Wyświetl plik

@ -1,13 +1,15 @@
{% load embed_video_tags %}
{% load embed_video_tags i18n %}
<section class="video">
<div class="wrapper">
{% if section.video %}
<div class="video">
<div class="iframe">
{% video section.video '800x600' %}
</div>
{% if section.video %}
<div class="video">
<div class="iframe">
{% video section.video '800x600' %}
</div>
{% endif %}
</div>
</div>
{% endif %}
{% if perms.cms_section_change %}
<a class="edit" href="{% url 'cms:updatesection' section.number %}">{% trans 'edit' %}</a><br>
{% endif %}
</section>

Wyświetl plik

@ -1,6 +1,7 @@
{% extends 'cms/base.html' %}
{% load static %}
{% block title %}app{% endblock %}
{% block title %}Awesome Website{% endblock %}
{% block extrahead %}
<link rel="stylesheet" href="{% static 'app/main1.scss.css' %}">

Wyświetl plik

@ -1,27 +0,0 @@
from cms.forms import ContactForm
from cms.views import SectionView, SectionFormView
from cms.decorators import register_view
from .models import *
@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(SectionFormView):
form_class = ContactForm
success_url = '/thanks/'
template_name = 'app/sections/contact.html'

Wyświetl plik

@ -9,6 +9,7 @@ except ImportError:
PROJECT_NAME = 'example'
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'
@ -24,8 +25,7 @@ MEDIA_URL = '/media/'
MEDIA_ROOT = '/srv/' + PROJECT_NAME + '/media'
LOGIN_REDIRECT_URL = '/'
CMS_SECTION_MODEL = 'app.Section'
CMS_PAGE_MODEL = 'app.Page' # https://github.com/wq/django-swappable-models/issues/18#issuecomment-514039164
MARKDOWN_EXTENSIONS = ['extra', 'smarty']
CMS_PAGE_MODEL = 'app.Page'
def read(file):
with open(file) as f:
@ -48,7 +48,6 @@ INSTALLED_APPS = [
'django.contrib.messages',
'django.contrib.staticfiles',
'cms',
'polymorphic',
'embed_video',
'easy_thumbnails',
'django_extensions',