First draft of version 2.0.0

Oh boy! This is a big one. Two new dependencies: swapper and
django-polymorphic will now allow any project that uses cms to elegantly
extend the default Section model with custom fields and custom subclasses.
This is still a work in progress.
readwriteweb
Jaap Joris Vens 2019-12-31 13:05:12 +01:00
rodzic 4004643dea
commit d166e10b05
29 zmienionych plików z 264 dodań i 170 usunięć

Wyświetl plik

@ -2,36 +2,18 @@ from django.contrib import admin
from django.utils.text import Truncator
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
from .models import Page, Section, SubSection, Config
class InlineSectionAdmin(admin.StackedInline):
model = Section
extra = 0
class InlineSubSectionAdmin(admin.StackedInline):
model = SubSection
extra = 0
from .models import Page, Config
@admin.register(Page)
class PageAdmin(admin.ModelAdmin):
prepopulated_fields = {'slug': ('title',)}
inlines = [InlineSectionAdmin]
@admin.register(Section)
class SectionAdmin(admin.ModelAdmin):
inlines = [InlineSubSectionAdmin]
class BaseSectionAdmin(admin.ModelAdmin):
list_filter = [
('page', admin.RelatedOnlyFieldListFilter),
]
list_display = ['__str__', 'get_type_display']
@admin.register(SubSection)
class SubSectionAdmin(admin.ModelAdmin):
list_filter = [
('section', admin.RelatedOnlyFieldListFilter),
('section__page', admin.RelatedOnlyFieldListFilter),
]
@admin.register(Config)
class ConfigAdmin(admin.ModelAdmin):
list_display = ['__str__', 'get_content']

Wyświetl plik

@ -1,5 +1,9 @@
from django import forms
from .models import Page, Section, SubSection
from django.contrib.contenttypes.models import ContentType
from .models import Page
import swapper
Section = swapper.load_model('cms', 'Section')
class PageForm(forms.ModelForm):
class Meta:
@ -7,14 +11,25 @@ class PageForm(forms.ModelForm):
fields = '__all__'
class SectionForm(forms.ModelForm):
def save(self):
section = super().save()
app_label = section._meta.app_label
model = section.type
# Explanation: we'll 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. This way, the next
# time the object is requested from the database,
# django-polymorphic will automatically convert it to the
# correct subclass. Brilliant!
section.polymorphic_ctype = ContentType.objects.get(
app_label=section._meta.app_label,
model=section.type.lower(),
)
section.save()
return section
class Meta:
model = Section
exclude = ['page']
class SubSectionForm(forms.ModelForm):
class Meta:
model = SubSection
exclude = ['section']
SectionFormSet = forms.inlineformset_factory(Page, Section, exclude='__all__', extra=0)
SubSectionFormSet = forms.inlineformset_factory(Section, SubSection, exclude='__all__', extra=0)

Wyświetl plik

@ -1,13 +1,17 @@
# Generated by Django 3.0.1 on 2019-12-31 11:15
import ckeditor.fields
from django.db import migrations, models
import django.db.models.deletion
import embed_video.fields
class Migration(migrations.Migration):
initial = True
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
]
operations = [
@ -16,7 +20,7 @@ class Migration(migrations.Migration):
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('parameter', models.PositiveIntegerField(choices=[(10, 'Footer')], unique=True)),
('content', ckeditor.fields.RichTextField(blank=True, verbose_name='Inhoud')),
('content', ckeditor.fields.RichTextField(blank=True, verbose_name='content')),
],
options={
'verbose_name': 'configuration parameter',
@ -44,20 +48,19 @@ class Migration(migrations.Migration):
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('position', models.PositiveIntegerField(blank=True, verbose_name='position')),
('title', models.CharField(max_length=255, verbose_name='title')),
('title', models.CharField(blank=True, max_length=255, verbose_name='title')),
('type', models.CharField(choices=[('normal', 'Normaal')], default='normal', max_length=16, verbose_name='section type')),
('color', models.PositiveIntegerField(choices=[(1, 'Licht'), (2, 'Donker')], default=1, verbose_name='color')),
('color', models.PositiveIntegerField(choices=[(1, 'Wit')], default=1, verbose_name='color')),
('content', ckeditor.fields.RichTextField(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', models.CharField(blank=True, max_length=255, verbose_name='button text')),
('button_link', models.CharField(blank=True, max_length=255, verbose_name='button link')),
('page', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='sections', to='cms.Page', verbose_name='page')),
('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_cms.section_set+', to='contenttypes.ContentType')),
],
options={
'verbose_name': 'section',
'verbose_name_plural': 'sections',
'ordering': ['position'],
'swappable': 'CMS_SECTION_MODEL',
},
),
]

Wyświetl plik

@ -8,7 +8,7 @@ def add_homepage(apps, schema_editor):
class Migration(migrations.Migration):
dependencies = [
('cms', '0003_subsection'),
('cms', '0001_initial'),
]
operations = [

Wyświetl plik

@ -1,15 +0,0 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cms', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='section',
name='title',
field=models.CharField(blank=True, max_length=255, verbose_name='title'),
),
]

Wyświetl plik

@ -1,32 +0,0 @@
import ckeditor.fields
import cms.models
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('cms', '0002_title'),
]
operations = [
migrations.CreateModel(
name='SubSection',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('position', models.PositiveIntegerField(blank=True, verbose_name='position')),
('title', models.CharField(blank=True, max_length=255, verbose_name='title')),
('color', models.PositiveIntegerField(choices=[(1, 'Licht'), (2, 'Donker')], default=1, verbose_name='color')),
('content', ckeditor.fields.RichTextField(blank=True, verbose_name='content')),
('image', models.ImageField(blank=True, upload_to='', verbose_name='image')),
('button_text', cms.models.VarCharField(blank=True, verbose_name='button text')),
('button_link', cms.models.VarCharField(blank=True, verbose_name='button link')),
('section', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='subsections', to='cms.Section', verbose_name='section')),
],
options={
'verbose_name': 'subsection',
'verbose_name_plural': 'subsections',
'ordering': ['position'],
},
),
]

Wyświetl plik

@ -1,3 +1,4 @@
import swapper
from django.db import models
from django.urls import reverse
from django.conf import settings
@ -5,6 +6,7 @@ from django.forms import TextInput
from django.utils.translation import gettext_lazy as _
from ckeditor.fields import RichTextField
from embed_video.fields import EmbedVideoField
from polymorphic.models import PolymorphicModel
from numberedmodel.models import NumberedModel
@ -36,11 +38,12 @@ class Page(NumberedModel):
verbose_name_plural = _('Pages')
ordering = ['position']
class Section(NumberedModel):
choices = settings.SECTION_TYPES
class BaseSection(NumberedModel, PolymorphicModel):
page = models.ForeignKey(Page, verbose_name=_('page'), related_name='sections', on_delete=models.PROTECT)
position = models.PositiveIntegerField(_('position'), blank=True)
title = models.CharField(_('title'), max_length=255, blank=True)
type = models.CharField(_('section type'), max_length=16, default=settings.SECTION_TYPES[0][0], choices=settings.SECTION_TYPES)
type = models.CharField(_('section type'), max_length=16, default=choices[0][0], choices=choices)
color = models.PositiveIntegerField(_('color'), default=1, choices=settings.SECTION_COLORS)
content = RichTextField(_('content'), blank=True)
@ -61,36 +64,15 @@ class Section(NumberedModel):
return self.title
class Meta:
abstract = True
verbose_name = _('section')
verbose_name_plural = _('sections')
ordering = ['position']
#app_label = 'cms'
class SubSection(NumberedModel):
section = models.ForeignKey(Section, verbose_name=_('section'), related_name='subsections', on_delete=models.CASCADE)
position = models.PositiveIntegerField(_('position'), blank=True)
title = models.CharField(_('title'), max_length=255, blank=True)
color = models.PositiveIntegerField(_('color'), default=1, choices=settings.SECTION_COLORS)
content = RichTextField(_('content'), blank=True)
image = models.ImageField(_('image'), blank=True)
button_text = VarCharField(_('button text'), blank=True)
button_link = VarCharField(_('button link'), blank=True)
def number_with_respect_to(self):
return self.section.subsections.all()
def __str__(self):
if not self.pk:
return str(_('New subsection'))
elif not self.title:
return str(_('Untitled'))
else:
return self.title
class Section(BaseSection):
class Meta:
verbose_name = _('subsection')
verbose_name_plural = _('subsections')
ordering = ['position']
swappable = swapper.swappable_setting('cms', 'Section')
class Config(models.Model):
TYPES = [

Wyświetl plik

@ -188,8 +188,6 @@ div.edit a, div.edit button, a.edit{
display: inline;
background: none;
cursor: pointer;
margin: 0;
padding: 0;
&:before {
content: '[ ';

Wyświetl plik

@ -140,9 +140,7 @@ div.edit a, div.edit button, a.edit {
border: none;
display: inline;
background: none;
cursor: pointer;
margin: 0;
padding: 0; }
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 {

File diff suppressed because one or more lines are too long

Wyświetl plik

@ -27,7 +27,7 @@
{% if pages %}
<ul id="menu">
{% for p in pages %}
<li><a href="{% if p.slug %}{% url page_url_pattern p.slug %}{% else %}{% url page_url_pattern %}{% endif %}" {% if p.pk == object.pk %}class="current"{% endif %}>{{p.title}}</a></li>
<li><a href="{% if p.slug %}{% url page_url_pattern p.slug %}{% else %}{% url page_url_pattern %}{% endif %}" {% if p.pk == page.pk %}class="current"{% endif %}>{{p.title}}</a></li>
{% endfor %}
{% if user.is_staff %}
<li><a class="edit" href="{% url 'cms:createpage' %}">+ {% trans 'new page' %}</a></li>

Wyświetl plik

@ -6,7 +6,8 @@
{% block content %}
{% for section in sections %}
<section class="{{section.type}} color{{section.color}}">
{% include 'cms/sections/'|add:section.type|add:'.html' %}
DIT IS EEN SECTIE MET FIELDS: {{section.fields}}
{% include 'cms/sections/'|add:section.type|lower|add:'.html' %}
</section>
{% endfor %}

Wyświetl plik

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

Wyświetl plik

@ -1,5 +1,5 @@
from django.urls import path
from .views import PageView, UpdatePage, CreatePage, UpdateSection, CreateSection, CreateSubSection
from .views import PageView, UpdatePage, CreatePage, UpdateSection, CreateSection
app_name = 'cms'
@ -9,7 +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'),
path('createsubsection/<int:pk>/', CreateSubSection.as_view(), name='createsubsection'),
# Feel free to copy the following into your root URL conf!
path('', PageView.as_view(), name='page'),

Wyświetl plik

@ -4,10 +4,13 @@ from django.shortcuts import redirect
from django.contrib.auth.mixins import UserPassesTestMixin
from django.views.generic import DetailView, UpdateView, CreateView
from .models import Page, Section, SubSection
from .forms import PageForm, SectionFormSet, SectionForm, SubSectionFormSet, SubSectionForm
from .models import Page
from .forms import PageForm, SectionForm
from .utils import get_config
import swapper
Section = swapper.load_model('cms', 'Section')
class StaffRequiredMixin(UserPassesTestMixin):
def test_func(self):
return self.request.user.is_staff
@ -68,59 +71,17 @@ class CreateSection(StaffRequiredMixin, MenuMixin, CreateView):
form.save()
return redirect(self.request.session.get('previous_url'))
class CreateSubSection(StaffRequiredMixin, MenuMixin, CreateView):
model = SubSection
form_class = SubSectionForm
template_name = 'cms/new.html'
def form_valid(self, form):
form.instance.section = Section.objects.get(pk=self.kwargs.get('pk'))
form.save()
return redirect(self.request.session.get('previous_url'))
class BaseUpdateView(StaffRequiredMixin, MenuMixin, UpdateView):
template_name = 'cms/edit.html'
def post(self, request, *args, **kwargs):
self.object = self.get_object()
form = self.get_form()
formset = self.formset_class(request.POST, request.FILES, instance=self.object)
if form.is_valid() and formset.is_valid():
return self.form_valid(form, formset)
else:
return self.form_invalid(form, formset)
def form_valid(self, form, formset):
def form_valid(self, form):
form.save()
formset.save()
return redirect(self.request.session.get('previous_url'))
def form_invalid(self, form, formset):
return self.render_to_response(self.get_context_data(form=form, formset=formset))
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
if 'formset' not in context:
formset = self.formset_class(instance=self.object)
context.update({
'formset': formset,
'formset_form_url': self.get_formset_form_url(self.object),
'formset_description': self.formset_class.model._meta.verbose_name,
})
return context
class UpdatePage(BaseUpdateView):
model = Page
form_class = PageForm
formset_class = SectionFormSet
def get_formset_form_url(self, page):
return reverse('cms:createsection', args=[page.pk])
class UpdateSection(BaseUpdateView):
model = Section
form_class = SectionForm
formset_class = SubSectionFormSet
def get_formset_form_url(self, page):
return reverse('cms:createsubsection', args=[page.pk])

Wyświetl plik

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

Wyświetl plik

@ -0,0 +1,7 @@
from django.contrib import admin
from cms.admin import BaseSectionAdmin
from .models import Section
@admin.register(Section)
class SectionAdmin(BaseSectionAdmin):
pass

Wyświetl plik

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

Wyświetl plik

@ -0,0 +1,64 @@
# Generated by Django 3.0.1 on 2019-12-31 11:16
import ckeditor.fields
from django.db import migrations, models
import django.db.models.deletion
import embed_video.fields
class Migration(migrations.Migration):
initial = True
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('cms', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Section',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('position', models.PositiveIntegerField(blank=True, verbose_name='position')),
('title', models.CharField(blank=True, max_length=255, verbose_name='title')),
('type', models.CharField(choices=[('normal', 'Normaal')], default='normal', max_length=16, verbose_name='section type')),
('color', models.PositiveIntegerField(choices=[(1, 'Wit')], default=1, verbose_name='color')),
('content', ckeditor.fields.RichTextField(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', models.CharField(blank=True, max_length=255, verbose_name='button text')),
('button_link', models.CharField(blank=True, max_length=255, verbose_name='button link')),
('page', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='sections', to='cms.Page', 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',
'verbose_name_plural': 'sections',
'ordering': ['position'],
'abstract': False,
},
),
migrations.CreateModel(
name='ImageSection',
fields=[
],
options={
'proxy': True,
'indexes': [],
'constraints': [],
},
bases=('app.section',),
),
migrations.CreateModel(
name='TextSection',
fields=[
],
options={
'proxy': True,
'indexes': [],
'constraints': [],
},
bases=('app.section',),
),
]

Wyświetl plik

@ -0,0 +1,18 @@
from cms.models import BaseSection
class Section(BaseSection):
'''Add custom fields here. Already existing fields: title, color,
content, image, video, button_text, button_link
'''
class TextSection(Section):
fields = ['title', 'content']
class Meta:
proxy = True
class ImageSection(Section):
fields = ['title', 'image']
class Meta:
proxy = True

Wyświetl plik

@ -0,0 +1,47 @@
$small: 500px;
$medium: 800px;
$font: sans-serif;
$titlefont: sans-serif;
body {
font-family: $font;
}
h1, h2, h3, h4, h5, h6 {
margin: .5em 0;
font-family: $titlefont;
}
h1 { font-size: 2em }
h2 { font-size: 1.5em }
h3 { font-size: 1.25em }
h4, h5, h6 { font-size: 1em }
a {
&:hover {
}
}
a.button {
&:hover {
}
}
header {
h1 {
text-align: center;
}
img {
display: block;
width: 100%;
max-width: 400px;
margin: auto;
}
}
nav {
}
footer {
margin-top: 4em;
min-height: 400px;
}

Wyświetl plik

@ -0,0 +1,33 @@
body {
font-family: sans-serif; }
h1, h2, h3, h4, h5, h6 {
margin: .5em 0;
font-family: sans-serif; }
h1 {
font-size: 2em; }
h2 {
font-size: 1.5em; }
h3 {
font-size: 1.25em; }
h4, h5, h6 {
font-size: 1em; }
header h1 {
text-align: center; }
header img {
display: block;
width: 100%;
max-width: 400px;
margin: auto; }
footer {
margin-top: 4em;
min-height: 400px; }
/*# sourceMappingURL=main1.scss.css.map */

Wyświetl plik

@ -0,0 +1,9 @@
{
"version": 3,
"file": "main1.css",
"sources": [
"main1.scss"
],
"names": [],
"mappings": "AAKA,AAAA,IAAI,CAAC;EACH,WAAW,EAJN,UAAU,GAKhB;;AAED,AAAA,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC;EACrB,MAAM,EAAE,MAAM;EACd,WAAW,EARD,UAAU,GASrB;;AACD,AAAA,EAAE,CAAC;EAAE,SAAS,EAAE,GAAI,GAAE;;AACtB,AAAA,EAAE,CAAC;EAAE,SAAS,EAAE,KAAM,GAAE;;AACxB,AAAA,EAAE,CAAC;EAAE,SAAS,EAAE,MAAO,GAAE;;AACzB,AAAA,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC;EAAE,SAAS,EAAE,GAAI,GAAE;;AAY9B,AACE,MADI,CACJ,EAAE,CAAC;EACD,UAAU,EAAE,MAAM,GACnB;;AAHH,AAIE,MAJI,CAIJ,GAAG,CAAC;EACF,OAAO,EAAE,KAAK;EACd,KAAK,EAAE,IAAI;EACX,SAAS,EAAE,KAAK;EAChB,MAAM,EAAE,IAAI,GACb;;AAMH,AAAA,MAAM,CAAC;EACL,UAAU,EAAE,GAAG;EACf,UAAU,EAAE,KAAK,GAClB"
}

Wyświetl plik

@ -0,0 +1,13 @@
{% extends 'cms/base.html' %}
{% load static %}
{% block title %}app{% endblock %}
{% block extrahead %}
<link rel="stylesheet" href="{% static 'app/main1.scss.css' %}">
{% endblock %}
{% block header %}
<div class="wrapper">
<h1><a href="/">Awesome Website</a></h1>
</div>
{% endblock %}

Wyświetl plik

@ -5,7 +5,7 @@ try:
except ImportError:
DEBUG = True
PROJECT_NAME = INSERT_PROJECT_NAME_HERE
PROJECT_NAME = 'example'
KEYFILE = f'/tmp/{PROJECT_NAME}.secret'
ADMINS = [('JJ Vens', 'jj@rtts.eu')]
ALLOWED_HOSTS = ['*']
@ -22,6 +22,7 @@ MEDIA_URL = '/media/'
MEDIA_ROOT = '/srv/' + PROJECT_NAME + '/media'
LOGIN_REDIRECT_URL = '/'
PAGE_URL_PATTERN = 'cms:page'
CMS_SECTION_MODEL = 'app.Section'
def read(file):
with open(file) as f:
@ -36,7 +37,8 @@ except IOError:
write(KEYFILE, SECRET_KEY)
SECTION_TYPES = [
('normal', 'Normaal'),
('TextSection', 'Tekst'),
('ImageSection', 'Afbeelding'),
]
SECTION_COLORS = [
@ -63,8 +65,7 @@ CKEDITOR_CONFIGS = {
}
INSTALLED_APPS = [
'simplesass',
PROJECT_NAME,
'app',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
@ -72,7 +73,9 @@ INSTALLED_APPS = [
'django.contrib.messages',
'django.contrib.staticfiles',
'cms',
'simplesass',
'ckeditor',
'polymorphic',
'embed_video',
'easy_thumbnails',
'django_extensions',

Wyświetl plik

@ -3,6 +3,8 @@ Django
django-ckeditor
django-extensions
django-embed-video
django-polymorphic
easy-thumbnails
psycopg2
libsass
swapper

Wyświetl plik

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