Automatic detection of available section types

Use the decorator @register to register your own Section child models, and
use their fields attribute to specify which fields it uses.
readwriteweb
Jaap Joris Vens 2020-01-02 01:56:15 +01:00
rodzic 646311335b
commit a69d51a0dc
12 zmienionych plików z 163 dodań i 119 usunięć

Wyświetl plik

@ -2,10 +2,9 @@ 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, Config
from .models import Config
@admin.register(Page)
class PageAdmin(admin.ModelAdmin):
class BasePageAdmin(admin.ModelAdmin):
prepopulated_fields = {'slug': ('title',)}
class BaseSectionAdmin(admin.ModelAdmin):

Wyświetl plik

@ -1,8 +1,8 @@
from django import forms
from django.contrib.contenttypes.models import ContentType
from .models import Page
import swapper
Page = swapper.load_model('cms', 'Page')
Section = swapper.load_model('cms', 'Section')
class PageForm(forms.ModelForm):
@ -11,6 +11,13 @@ class PageForm(forms.ModelForm):
fields = '__all__'
class SectionForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Repopulate the 'choices' attribute of the type field from
# the child model.
self.fields['type'].choices = self._meta.model.TYPES
def save(self):
section = super().save()
app_label = section._meta.app_label
@ -21,7 +28,7 @@ class SectionForm(forms.ModelForm):
# 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!
# correct subclass.
section.polymorphic_ctype = ContentType.objects.get(
app_label=section._meta.app_label,
model=section.type.lower(),

Wyświetl plik

@ -1,6 +1,8 @@
# Generated by Django 3.0.1 on 2019-12-31 11:15
# Generated by Django 3.0.1 on 2020-01-02 00:14
import ckeditor.fields
import cms.models
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import embed_video.fields
@ -15,6 +17,23 @@ class Migration(migrations.Migration):
]
operations = [
migrations.CreateModel(
name='Page',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('position', models.PositiveIntegerField(blank=True, verbose_name='position')),
('title', cms.models.VarCharField(verbose_name='title')),
('slug', models.SlugField(blank=True, help_text='A short identifier to use in URLs', unique=True, verbose_name='slug')),
('menu', models.BooleanField(default=True, verbose_name='visible in menu')),
],
options={
'verbose_name': 'Page',
'verbose_name_plural': 'Pages',
'ordering': ['position'],
'abstract': False,
'swappable': 'CMS_PAGE_MODEL',
},
),
migrations.CreateModel(
name='Config',
fields=[
@ -28,38 +47,27 @@ class Migration(migrations.Migration):
'ordering': ['parameter'],
},
),
migrations.CreateModel(
name='Page',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('position', models.PositiveIntegerField(blank=True, verbose_name='position')),
('title', models.CharField(max_length=255, verbose_name='title')),
('slug', models.SlugField(blank=True, help_text='A short identifier to use in URLs', unique=True, verbose_name='slug')),
('menu', models.BooleanField(default=True, verbose_name='visible in menu')),
],
options={
'verbose_name': 'Page',
'verbose_name_plural': 'Pages',
'ordering': ['position'],
},
),
migrations.CreateModel(
name='Section',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('type', cms.models.VarCharChoiceField(choices=[('textsection', 'Tekst'), ('stepsection', 'Stappenplan'), ('uploadsection', 'Upload')], default='', verbose_name='section type')),
('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')),
('title', cms.models.VarCharField(blank=True, verbose_name='title')),
('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')),
('button_text', cms.models.VarCharField(blank=True, verbose_name='button text')),
('button_link', cms.models.VarCharField(blank=True, verbose_name='button 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_cms.section_set+', to='contenttypes.ContentType')),
],
options={
'verbose_name': 'section',
'verbose_name_plural': 'sections',
'ordering': ['position'],
'abstract': False,
'swappable': 'CMS_SECTION_MODEL',
},
),

Wyświetl plik

@ -1,12 +1,12 @@
import swapper
from django.db import migrations
Page = swapper.load_model('cms', 'Page')
def add_homepage(apps, schema_editor):
Page = apps.get_model('cms', 'Page')
if not Page.objects.exists():
Page(slug='', title='Homepage', position=1).save()
class Migration(migrations.Migration):
dependencies = [
('cms', '0001_initial'),
]

Wyświetl plik

@ -2,7 +2,7 @@ import swapper
from django.db import models
from django.urls import reverse
from django.conf import settings
from django.forms import TextInput
from django.forms import TextInput, Select
from django.utils.translation import gettext_lazy as _
from ckeditor.fields import RichTextField
from embed_video.fields import EmbedVideoField
@ -10,14 +10,25 @@ from polymorphic.models import PolymorphicModel
from numberedmodel.models import NumberedModel
def register(verbose_name):
def wrapper(model):
model.__bases__[-1].TYPES.append((model.__name__.lower(), verbose_name))
return model
return wrapper
class VarCharField(models.TextField):
def formfield(self, **kwargs):
kwargs.update({'widget': TextInput})
return super().formfield(**kwargs)
class Page(NumberedModel):
class VarCharChoiceField(models.TextField):
def formfield(self, **kwargs):
kwargs.update({'widget': Select})
return super().formfield(**kwargs)
class BasePage(NumberedModel):
position = models.PositiveIntegerField(_('position'), blank=True)
title = models.CharField(_('title'), max_length=255)
title = VarCharField(_('title'))
slug = models.SlugField(_('slug'), help_text=_('A short identifier to use in URLs'), blank=True, unique=True)
menu = models.BooleanField(_('visible in menu'), default=True)
@ -29,29 +40,28 @@ class Page(NumberedModel):
def get_absolute_url(self):
if self.slug:
return reverse(settings.PAGE_URL_PATTERN, args=[self.slug])
return reverse('cms:page', args=[self.slug])
else:
return reverse(settings.PAGE_URL_PATTERN)
return reverse('cms:page')
class Meta:
abstract = True
verbose_name = _('Page')
verbose_name_plural = _('Pages')
ordering = ['position']
choices = settings.SECTION_TYPES
class BaseSection(NumberedModel, PolymorphicModel):
page = models.ForeignKey(Page, verbose_name=_('page'), related_name='sections', on_delete=models.PROTECT)
type = models.CharField(_('section type'), max_length=16, default=choices[0][0], choices=choices)
TYPES = []
page = models.ForeignKey(swapper.get_model_name('cms', 'Page'), verbose_name=_('page'), related_name='sections', on_delete=models.PROTECT)
type = VarCharChoiceField(_('section type'), default='', choices=TYPES)
position = models.PositiveIntegerField(_('position'), blank=True)
title = models.CharField(_('title'), max_length=255, blank=True)
title = VarCharField(_('title'), blank=True)
color = models.PositiveIntegerField(_('color'), default=1, choices=settings.SECTION_COLORS)
content = RichTextField(_('content'), blank=True)
image = models.ImageField(_('image'), blank=True)
video = EmbedVideoField(_('video'), blank=True, help_text=_('Paste a YouTube, Vimeo, or SoundCloud link'))
button_text = models.CharField(_('button text'), max_length=255, blank=True)
button_link = models.CharField(_('button link'), max_length=255, blank=True)
button_text = VarCharField(_('button text'), blank=True)
button_link = VarCharField(_('button link'), blank=True)
def number_with_respect_to(self):
return self.page.sections.all()
@ -69,10 +79,13 @@ class BaseSection(NumberedModel, PolymorphicModel):
verbose_name = _('section')
verbose_name_plural = _('sections')
ordering = ['position']
#app_label = 'cms'
class Page(BasePage):
class Meta(BasePage.Meta):
swappable = swapper.swappable_setting('cms', 'Page')
class Section(BaseSection):
class Meta:
class Meta(BaseSection.Meta):
swappable = swapper.swappable_setting('cms', 'Section')
class Config(models.Model):

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 == page.pk %}class="current"{% endif %}>{{p.title}}</a></li>
<li><a href="{% if p.slug %}{% url 'cms:page' p.slug %}{% else %}{% url 'cms:page' %}{% 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

@ -77,6 +77,7 @@
}
}
{% if fields_per_type %}
/* Only show relevant fields */
if (typefield) {
@ -99,5 +100,6 @@
});
show_relevant_fields(typefield.value.toLowerCase());
}
{% endif %}
</script>
{% endblock %}

Wyświetl plik

@ -1,87 +1,38 @@
import json
from django.conf import settings
from django.urls import reverse
from django.views import generic
from django.shortcuts import redirect
from django.contrib.contenttypes.models import ContentType
from django.contrib.auth.mixins import UserPassesTestMixin
from .models import Page
from .forms import PageForm, SectionForm
from .utils import get_config
import swapper
Page = swapper.load_model('cms', 'Page')
Section = swapper.load_model('cms', 'Section')
class StaffRequiredMixin(UserPassesTestMixin):
def test_func(self):
return self.request.user.is_staff
class MenuMixin(object):
class MenuMixin:
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
pages = Page.objects.filter(menu=True)
footer = get_config(10)
context.update({
'page_url_pattern': settings.PAGE_URL_PATTERN,
'pages': pages,
'footer': footer,
})
return context
class MemoryMixin(object):
def dispatch(self, request, *args, **kwargs):
if request.user.is_authenticated:
request.session['previous_url'] = request.path
return super().dispatch(request, *args, **kwargs)
class BasePageView(MenuMixin, MemoryMixin, generic.DetailView):
model = Page
template_name = 'cms/page.html'
def setup(self, request, *args, slug='', **kwargs):
self.request = request
self.args = args
self.kwargs = kwargs
self.kwargs['slug'] = slug
class TypeMixin(MenuMixin):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
page = self.object
sections = page.sections.all()
context.update({
'page': page,
'sections': sections,
})
return context
class PageView(BasePageView):
pass
class CreatePage(StaffRequiredMixin, MenuMixin, generic.CreateView):
model = Page
form_class = PageForm
template_name = 'cms/new.html'
class CreateSection(StaffRequiredMixin, MenuMixin, generic.CreateView):
model = Section
form_class = SectionForm
template_name = 'cms/new.html'
def form_valid(self, form):
form.instance.page = Page.objects.get(pk=self.kwargs.get('pk'))
form.save()
return redirect(self.request.session.get('previous_url'))
class BaseUpdateView(StaffRequiredMixin, MenuMixin, generic.UpdateView):
template_name = 'cms/edit.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
section_types = settings.SECTION_TYPES
fields_per_type = {}
for model, desc in section_types:
for model, desc in Section.TYPES:
ctype = ContentType.objects.get(
app_label=Section._meta.app_label,
model=model.lower(),
@ -93,14 +44,60 @@ class BaseUpdateView(StaffRequiredMixin, MenuMixin, generic.UpdateView):
})
return context
class MemoryMixin:
def dispatch(self, request, *args, **kwargs):
if request.user.is_authenticated:
request.session['previous_url'] = request.path
return super().dispatch(request, *args, **kwargs)
class PageView(MenuMixin, MemoryMixin, generic.DetailView):
model = Page
template_name = 'cms/page.html'
# Supplies a default argument for slug
def setup(self, *args, slug='', **kwargs):
super().setup(*args, slug=slug, **kwargs)
#self.request = request
#self.args = args
#self.kwargs = kwargs
#self.kwargs['slug'] = slug
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
page = self.object
sections = page.sections.all()
context.update({
'page': page,
'sections': sections,
})
return context
class BaseUpdateView(generic.UpdateView):
template_name = 'cms/edit.html'
def form_valid(self, form):
form.save()
return redirect(self.request.session.get('previous_url'))
class UpdatePage(BaseUpdateView):
class UpdatePage(StaffRequiredMixin, MenuMixin, BaseUpdateView):
model = Page
form_class = PageForm
class UpdateSection(BaseUpdateView):
class UpdateSection(StaffRequiredMixin, TypeMixin, BaseUpdateView):
model = Section
form_class = SectionForm
class CreatePage(StaffRequiredMixin, MenuMixin, generic.CreateView):
model = Page
form_class = PageForm
template_name = 'cms/new.html'
class CreateSection(StaffRequiredMixin, TypeMixin, generic.CreateView):
model = Section
form_class = SectionForm
template_name = 'cms/new.html'
def form_valid(self, form):
form.instance.page = Page.objects.get(pk=self.kwargs.get('pk'))
form.save()
return redirect(self.request.session.get('previous_url'))

Wyświetl plik

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

Wyświetl plik

@ -1,6 +1,8 @@
# Generated by Django 3.0.1 on 2019-12-31 11:16
# Generated by Django 3.0.1 on 2020-01-02 00:06
import ckeditor.fields
import cms.models
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import embed_video.fields
@ -11,25 +13,41 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.CMS_PAGE_MODEL),
('contenttypes', '0002_remove_content_type_name'),
('cms', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Section',
name='Page',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('position', models.PositiveIntegerField(blank=True, verbose_name='position')),
('title', 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')),
('title', cms.models.VarCharField(verbose_name='title')),
('slug', models.SlugField(blank=True, help_text='A short identifier to use in URLs', unique=True, verbose_name='slug')),
('menu', models.BooleanField(default=True, verbose_name='visible in menu')),
],
options={
'verbose_name': 'Page',
'verbose_name_plural': 'Pages',
'ordering': ['position'],
'abstract': False,
},
),
migrations.CreateModel(
name='Section',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('type', cms.models.VarCharChoiceField(choices=[('textsection', 'Tekst'), ('imagesection', 'Afbeelding')], default='', verbose_name='section type')),
('position', models.PositiveIntegerField(blank=True, verbose_name='position')),
('title', cms.models.VarCharField(blank=True, verbose_name='title')),
('color', models.PositiveIntegerField(choices=[(1, 'Wit')], default=1, verbose_name='color')),
('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')),
('button_text', cms.models.VarCharField(blank=True, verbose_name='button text')),
('button_link', cms.models.VarCharField(blank=True, verbose_name='button 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={

Wyświetl plik

@ -1,16 +1,24 @@
from cms.models import BaseSection
from cms.models import *
class Section(BaseSection):
'''Add custom fields here. Already existing fields: title, color,
content, image, video, button_text, button_link
class Page(BasePage):
'''Add custom fields here. Already existing fields: position, title,
slug, menu
'''
class Section(BaseSection):
'''Add custom fields here. Already existing fields: type, position,
title, color, content, image, video, button_text, button_link
'''
@register('Tekst')
class TextSection(Section):
fields = ['type', 'position', 'title', 'content']
class Meta:
proxy = True
@register('Afbeelding')
class ImageSection(Section):
fields = ['type', 'position', 'title', 'image']
class Meta:

Wyświetl plik

@ -21,7 +21,6 @@ STATIC_ROOT = '/srv/' + PROJECT_NAME + '/static'
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):