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' 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): def register(verbose_name):
'''Decorator to register a section subclass''' import swapper
def wrapper(model): Section = swapper.load_model('cms', 'Section')
parent_model = model.__bases__[-1]
parent_model.TYPES.append((model.__name__.lower(), verbose_name)) '''Decorator to register a specific section type'''
return model def wrapper(view):
Section._cms_views[view.__name__.lower()] = view
Section.TYPES.append((view.__name__.lower(), verbose_name))
return view
return wrapper return wrapper
def register_view(section_class): # def register_model(verbose_name):
'''Decorator to connect a section model to a view class''' # '''Decorator to register a section subclass'''
def wrapper(model): # def wrapper(model):
section_class.view_class = model # parent_model = model.__bases__[-1]
return model # parent_model.TYPES.append((model.__name__.lower(), verbose_name))
return wrapper # 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 import swapper
from django import forms from django import forms
from django.conf import settings from django.conf import settings
from django.db.models import Prefetch
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.mail import EmailMessage from django.core.mail import EmailMessage
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -13,7 +14,6 @@ class PageForm(forms.ModelForm):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.label_suffix = '' self.label_suffix = ''
extra = 1 if self.instance.pk else 2 extra = 1 if self.instance.pk else 2
self.formsets = [forms.inlineformset_factory( self.formsets = [forms.inlineformset_factory(
parent_model = Page, parent_model = Page,
model = Section, model = Section,
@ -37,7 +37,7 @@ class PageForm(forms.ModelForm):
def clean(self): def clean(self):
super().clean() super().clean()
if not self.formsets[0].is_valid(): 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): def save(self, *args, **kwargs):
page = super().save() page = super().save()
@ -60,21 +60,21 @@ class SectionForm(forms.ModelForm):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.label_suffix = '' self.label_suffix = ''
self.fields['DELETE'] = forms.BooleanField(label=_('Delete'), required=False) 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 # Repopulate the 'choices' attribute of the type field from
# the child model. # the child model.
self.fields['type'].choices = self._meta.model.TYPES self.fields['type'].choices = self._meta.model.TYPES
self.fields['type'].initial = self._meta.model.TYPES[0][0] self.fields['type'].initial = self._meta.model.TYPES[0][0]
# Populate the 'formsets' attribute if the Section was # Populate the 'formsets' attribute if the Section was
# extendend with one_to_many fields # extendend with one_to_many fields
self.formsets = [] self.formsets = []
for field in self.instance._meta.get_fields(): for field in self.instance._meta.get_fields():
if field.one_to_many: if field.one_to_many:
extra = 1 if getattr(self.instance, field.name).exists() else 2
formset = forms.inlineformset_factory( formset = forms.inlineformset_factory(
Section, field.related_model, parent_model=Section,
model=field.related_model,
fields='__all__', fields='__all__',
extra=extra, extra=extra,
)( )(
@ -107,19 +107,9 @@ class SectionForm(forms.ModelForm):
if section.page.slug and not section.page.sections.exists(): if section.page.slug and not section.page.sections.exists():
section.page.delete() section.page.delete()
return return
elif commit:
# 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:
section.save() section.save()
for formset in self.formsets: for formset in self.formsets:
formset.save(commit=commit) 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.db import models
from django.urls import reverse from django.urls import reverse
from django.forms import TextInput, Select
from django.utils.text import slugify from django.utils.text import slugify
from django.forms import TextInput, Select
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.core.exceptions import ImproperlyConfigured
from embed_video.fields import EmbedVideoField from embed_video.fields import EmbedVideoField
from polymorphic.models import PolymorphicModel
class VarCharField(models.TextField): class VarCharField(models.TextField):
'''Variable width CharField''' '''Variable width CharField'''
@ -86,9 +87,13 @@ class BasePage(Numbered, models.Model):
verbose_name_plural = _('Pages') verbose_name_plural = _('Pages')
ordering = ['number'] ordering = ['number']
class BaseSection(Numbered, PolymorphicModel): class BaseSection(Numbered, models.Model):
'''Abstract base model for sections''' '''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) page = models.ForeignKey(swapper.get_model_name('cms', 'Page'), verbose_name=_('page'), related_name='sections', on_delete=models.PROTECT)
title = VarCharField(_('section')) title = VarCharField(_('section'))
type = VarCharField(_('type')) type = VarCharField(_('type'))
@ -113,6 +118,21 @@ class BaseSection(Numbered, PolymorphicModel):
else: else:
return self.title 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: class Meta:
abstract = True abstract = True
verbose_name = _('section') verbose_name = _('section')

Wyświetl plik

@ -7,6 +7,24 @@ html, body {
padding: 0; 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 elements */
form.cms { form.cms {

Wyświetl plik

@ -4,6 +4,20 @@ html, body {
margin: 0; margin: 0;
padding: 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 elements */
form.cms { form.cms {
background: #bfb; background: #bfb;

Wyświetl plik

@ -5,5 +5,5 @@
"admin.scss" "admin.scss"
], ],
"names": [], "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 */ /*# 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 static i18n %}
{% load i18n %} {% get_current_language as lang%}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="{% get_current_language as lang%}{{lang}}"> <html lang="{{lang}}">
<head> <head>
<title>{% block title %}{% endblock %}</title> <title>{% block title %}{% endblock %}</title>
<meta charset="utf-8"/> <meta charset="utf-8"/>
@ -49,7 +49,6 @@
<footer> <footer>
{% block footer %} {% block footer %}
{{footer|safe}}
{% endblock %} {% endblock %}
</footer> </footer>

Wyświetl plik

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

Wyświetl plik

@ -14,17 +14,21 @@ class IncludeSectionNode(template.Node):
def __init__(self, section): def __init__(self, section):
self.section = template.Variable(section) self.section = template.Variable(section)
self.csrf_token = template.Variable('csrf_token') self.csrf_token = template.Variable('csrf_token')
self.request = template.Variable('request')
self.perms = template.Variable('perms') self.perms = template.Variable('perms')
super().__init__() super().__init__()
def render(self, context): def render(self, context):
section = self.section.resolve(context) section = self.section.resolve(context)
template_name = section.view.template_name
csrf_token = self.csrf_token.resolve(context) csrf_token = self.csrf_token.resolve(context)
request = self.request.resolve(context)
perms = self.perms.resolve(context) perms = self.perms.resolve(context)
section.context.update({ view = section.get_view(request)
'csrf_token': csrf_token, section_context = view.get_context_data(
'perms': perms, csrf_token=csrf_token,
}) section=section,
t = context.template.engine.get_template(template_name) request=request,
return t.render(template.Context(section.context)) 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.shortcuts import redirect
from django.views.generic import base, detail, edit 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.contrib.auth.mixins import UserPassesTestMixin
from django.http import Http404, HttpResponse, HttpResponseRedirect, HttpResponseBadRequest
from .decorators import register_view
from .forms import PageForm, SectionForm from .forms import PageForm, SectionForm
Page = swapper.load_model('cms', 'Page') Page = swapper.load_model('cms', 'Page')
Section = swapper.load_model('cms', 'Section') 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): class PageView(detail.DetailView):
'''View of a page with heterogeneous (polymorphic) sections''' '''View of a page with heterogeneous sections'''
model = Page model = Page
template_name = 'cms/page.html' template_name = 'cms/page.html'
@ -49,27 +20,16 @@ class PageView(detail.DetailView):
'''Supply a default argument for slug''' '''Supply a default argument for slug'''
super().setup(*args, slug=slug, **kwargs) 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): def get(self, request, *args, **kwargs):
'''Initialize sections and render final response''' '''Instantiate section views and render final response'''
try: try:
page = self.object = self.get_object() page = self.object = self.get_object()
except Http404: except Http404:
if self.request.user.has_perm('cms_page_create'): if self.request.user.has_perm('cms_page_create'):
return redirect('cms:updatepage', self.kwargs['slug']) return redirect('cms:updatepage', self.kwargs['slug'])
else: raise
raise
context = self.get_context_data(**kwargs) context = self.get_context_data(**kwargs)
sections = page.sections.all() sections = page.sections.all()
for section in sections:
self.initialize_section(section)
context.update({ context.update({
'page': page, 'page': page,
'sections': sections, 'sections': sections,
@ -77,8 +37,7 @@ class PageView(detail.DetailView):
return self.render_to_response(context) return self.render_to_response(context)
def post(self, request, **kwargs): def post(self, request, **kwargs):
'''Initialize sections and call the post() function of the correct '''Call the post() method of the correct section view'''
section view'''
try: try:
pk = int(self.request.POST.get('section')) pk = int(self.request.POST.get('section'))
except: except:
@ -88,11 +47,9 @@ class PageView(detail.DetailView):
context = self.get_context_data(**kwargs) context = self.get_context_data(**kwargs)
sections = page.sections.all() sections = page.sections.all()
for section in sections: for section in sections:
self.initialize_section(section)
if section.pk == pk: if section.pk == pk:
result = section.view.post(request) view = section.get_view(request)
if isinstance(result, HttpResponseRedirect): result = view.post(request)
return result
if isinstance(result, HttpResponse): if isinstance(result, HttpResponse):
return result return result
section.context['form'] = result section.context['form'] = result
@ -112,33 +69,26 @@ class PageView(detail.DetailView):
return context return context
class EditPage(UserPassesTestMixin, edit.ModelFormMixin, base.TemplateResponseMixin, base.View): class EditPage(UserPassesTestMixin, edit.ModelFormMixin, base.TemplateResponseMixin, base.View):
'''Base view with nested forms for editing the page and all its sections'''
model = Page model = Page
form_class = PageForm form_class = PageForm
template_name = 'cms/edit.html' template_name = 'cms/edit.html'
def test_func(self): def test_func(self):
app, model = swapper.get_model_name('cms', 'page').lower().split('.') '''Only allow users with the correct permissions'''
return self.request.user.has_perm('f{app}_{model}_change') return self.request.user.has_perm('cms_page_change')
def get_form_kwargs(self): def get_form_kwargs(self):
'''Set the default slug to the current URL for new pages'''
kwargs = super().get_form_kwargs() kwargs = super().get_form_kwargs()
if 'slug' in self.kwargs: if 'slug' in self.kwargs:
kwargs.update({'initial': {'slug': self.kwargs['slug']}}) kwargs.update({'initial': {'slug': self.kwargs['slug']}})
return kwargs return kwargs
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
'''Populate the fields_per_type dict for use in javascript'''
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
fields_per_type = {} context['fields_per_type'] = json.dumps(Section.get_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),
})
return context return context
def get_object(self): def get_object(self):
@ -148,11 +98,13 @@ class EditPage(UserPassesTestMixin, edit.ModelFormMixin, base.TemplateResponseMi
except Http404: except Http404:
return None return None
def get(self, request, *args, **kwargs): def get(self, *args, **kwargs):
'''Handle GET requests'''
self.object = self.get_object() 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() self.object = self.get_object()
form = self.get_form() form = self.get_form()
@ -161,14 +113,15 @@ class EditPage(UserPassesTestMixin, edit.ModelFormMixin, base.TemplateResponseMi
if page: if page:
return HttpResponseRedirect(page.get_absolute_url()) return HttpResponseRedirect(page.get_absolute_url())
return HttpResponseRedirect('/') 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): class CreatePage(EditPage):
'''View for creating new pages'''
def get_object(self): def get_object(self):
return Page() return Page()
class UpdatePage(EditPage): class UpdatePage(EditPage):
pass '''View for editing existing pages'''
class EditSection(UserPassesTestMixin, edit.ModelFormMixin, base.TemplateResponseMixin, base.View): class EditSection(UserPassesTestMixin, edit.ModelFormMixin, base.TemplateResponseMixin, base.View):
model = Section model = Section
@ -176,8 +129,7 @@ class EditSection(UserPassesTestMixin, edit.ModelFormMixin, base.TemplateRespons
template_name = 'cms/edit.html' template_name = 'cms/edit.html'
def test_func(self): def test_func(self):
app, model = swapper.get_model_name('cms', 'section').lower().split('.') return self.request.user.has_perm('cms_section_change')
return self.request.user.has_perm('f{app}_{model}_change')
def get_form_kwargs(self): def get_form_kwargs(self):
kwargs = super().get_form_kwargs() kwargs = super().get_form_kwargs()
@ -188,17 +140,7 @@ class EditSection(UserPassesTestMixin, edit.ModelFormMixin, base.TemplateRespons
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
fields_per_type = {} context['fields_per_type'] = json.dumps(Section.get_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),
})
return context return context
def get_object(self, queryset=None): def get_object(self, queryset=None):
@ -215,11 +157,11 @@ class EditSection(UserPassesTestMixin, edit.ModelFormMixin, base.TemplateRespons
raise Http404() raise Http404()
return section return section
def get(self, request, *args, **kwargs): def get(self, *args, **kwargs):
self.object = self.get_object() 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() self.object = self.get_object()
form = self.get_form() form = self.get_form()
@ -231,7 +173,7 @@ class EditSection(UserPassesTestMixin, edit.ModelFormMixin, base.TemplateRespons
return HttpResponseRedirect(self.page.get_absolute_url()) return HttpResponseRedirect(self.page.get_absolute_url())
else: else:
return HttpResponseRedirect('/') 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): class CreateSection(EditSection):
def get_section(self): 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 import cms.models
from django.conf import settings from django.conf import settings
@ -12,7 +12,6 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
('contenttypes', '0002_remove_content_type_name'),
] ]
operations = [ operations = [
@ -20,9 +19,9 @@ class Migration(migrations.Migration):
name='Page', name='Page',
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('number', models.PositiveIntegerField(blank=True, verbose_name='number')), ('title', cms.models.VarCharField(verbose_name='page')),
('title', cms.models.VarCharField(verbose_name='title')),
('slug', models.SlugField(blank=True, unique=True, verbose_name='slug')), ('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')), ('menu', models.BooleanField(default=True, verbose_name='visible in menu')),
], ],
options={ options={
@ -37,15 +36,14 @@ class Migration(migrations.Migration):
name='Section', name='Section',
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('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')), ('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')), ('content', models.TextField(blank=True, verbose_name='content')),
('image', models.ImageField(blank=True, upload_to='', verbose_name='image')), ('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')), ('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')), ('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={ options={
'verbose_name': 'section', '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.db import models
from django.conf import settings
from cms.models import BasePage, BaseSection from cms.models import BasePage, BaseSection
from cms.decorators import register_model from django.utils.translation import gettext_lazy as _
class Page(BasePage): class Page(BasePage):
'''Add custom fields here. Already existing fields: position, title, '''Add custom fields here. Already existing fields: title, slug,
slug, menu number, menu
''' '''
class Section(BaseSection): class Section(BaseSection):
'''Add custom fields here. Already existing fields: type, position, '''Add custom fields here. Already existing fields: title, type,
title, content, image, video, href number, content, image, video, href
''' '''
@register_model('Tekst') class SectionImage(models.Model):
class TextSection(Section): section = models.ForeignKey(Section, related_name='images', on_delete=models.CASCADE)
fields = ['title', 'content'] image = models.ImageField(_('Image'))
class Meta:
proxy = True
@register_model('Button')
class ButtonSection(Section):
fields = ['title', 'href']
class Meta: class Meta:
proxy = True ordering = ['pk']
@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

Wyświetl plik

@ -1,10 +1,43 @@
$small: 500px;
$medium: 800px;
$font: sans-serif; $font: sans-serif;
$titlefont: sans-serif; $titlefont: sans-serif;
$small: 500px;
$medium: 800px;
$max-width: 700px;
$color1: #3573a8;
body { html, body {
font-family: $font; 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 { h1, h2, h3, h4, h5, h6 {
@ -16,32 +49,232 @@ h2 { font-size: 1.5em }
h3 { font-size: 1.25em } h3 { font-size: 1.25em }
h4, h5, h6 { font-size: 1em } h4, h5, h6 { font-size: 1em }
a { a.edit {
&:hover { 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 { div.wrapper {
&:hover { box-sizing: border-box;
} max-width: $max-width;
margin: auto;
padding: 0 1rem;
}
div.spacer {
height: 1rem;
clear: both;
} }
header { header {
h1 { text-align: center;
text-align: center;
}
img {
display: block;
width: 100%;
max-width: 400px;
margin: auto;
}
} }
nav { 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 { footer {
margin-top: 4em; min-height: 10em;
min-height: 400px;
} }

Wyświetl plik

@ -1,5 +1,14 @@
body { html, body {
font-family: sans-serif; } 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 { h1, h2, h3, h4, h5, h6 {
margin: .5em 0; margin: .5em 0;
@ -17,17 +26,179 @@ h3 {
h4, h5, h6 { h4, h5, h6 {
font-size: 1em; } 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; } 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; display: block;
width: 100%; width: 100%;
max-width: 400px; padding: 0.5em;
margin: auto; } margin: 0; }
footer { footer {
margin-top: 4em; min-height: 10em; }
min-height: 400px; }
/*# sourceMappingURL=main1.scss.css.map */ /*# 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> </form>
</div> </div>
</div> </div>
{% if perms.cms_section_change %}
<a class="edit" href="{% url 'cms:updatesection' section.number %}">{% trans 'edit' %}</a><br>
{% endif %}
</section> </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"> <section class="text">
<div class="wrapper"> <div class="wrapper">
@ -9,7 +9,11 @@
</div> </div>
<div class="content"> <div class="content">
{{section.content|markdown}} {% eval section.content %}
</div> </div>
</div> </div>
{% if perms.cms_section_change %}
<a class="edit" href="{% url 'cms:updatesection' section.number %}">{% trans 'edit' %}</a><br>
{% endif %}
</section> </section>

Wyświetl plik

@ -1,13 +1,15 @@
{% load embed_video_tags %} {% load embed_video_tags i18n %}
<section class="video"> <section class="video">
<div class="wrapper"> {% if section.video %}
{% if section.video %} <div class="video">
<div class="video"> <div class="iframe">
<div class="iframe"> {% video section.video '800x600' %}
{% video section.video '800x600' %}
</div>
</div> </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> </section>

Wyświetl plik

@ -1,6 +1,7 @@
{% extends 'cms/base.html' %} {% extends 'cms/base.html' %}
{% load static %} {% load static %}
{% block title %}app{% endblock %}
{% block title %}Awesome Website{% endblock %}
{% block extrahead %} {% block extrahead %}
<link rel="stylesheet" href="{% static 'app/main1.scss.css' %}"> <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' PROJECT_NAME = 'example'
KEYFILE = f'/tmp/{PROJECT_NAME}.secret' KEYFILE = f'/tmp/{PROJECT_NAME}.secret'
ADMINS = [('JJ Vens', 'jj@rtts.eu')] ADMINS = [('JJ Vens', 'jj@rtts.eu')]
DEFAULT_FROM_EMAIL = 'noreply@rtts.eu'
DEFAULT_TO_EMAIL = 'jj@rtts.eu' DEFAULT_TO_EMAIL = 'jj@rtts.eu'
ALLOWED_HOSTS = ['*'] ALLOWED_HOSTS = ['*']
ROOT_URLCONF = 'project.urls' ROOT_URLCONF = 'project.urls'
@ -24,8 +25,7 @@ MEDIA_URL = '/media/'
MEDIA_ROOT = '/srv/' + PROJECT_NAME + '/media' MEDIA_ROOT = '/srv/' + PROJECT_NAME + '/media'
LOGIN_REDIRECT_URL = '/' LOGIN_REDIRECT_URL = '/'
CMS_SECTION_MODEL = 'app.Section' CMS_SECTION_MODEL = 'app.Section'
CMS_PAGE_MODEL = 'app.Page' # https://github.com/wq/django-swappable-models/issues/18#issuecomment-514039164 CMS_PAGE_MODEL = 'app.Page'
MARKDOWN_EXTENSIONS = ['extra', 'smarty']
def read(file): def read(file):
with open(file) as f: with open(file) as f:
@ -48,7 +48,6 @@ INSTALLED_APPS = [
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'cms', 'cms',
'polymorphic',
'embed_video', 'embed_video',
'easy_thumbnails', 'easy_thumbnails',
'django_extensions', 'django_extensions',