kopia lustrzana https://github.com/rtts/django-simplecms
add support for subsections
rodzic
caa04922b7
commit
7997d7af0f
15
cms/forms.py
15
cms/forms.py
|
@ -1,9 +1,20 @@
|
|||
from django import forms
|
||||
from .models import Page, Section
|
||||
from .models import Page, Section, SubSection
|
||||
|
||||
class PageForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Page
|
||||
fields = '__all__'
|
||||
|
||||
SectionFormSet = forms.inlineformset_factory(Page, Section, exclude='__all__', extra=1)
|
||||
class SectionForm(forms.ModelForm):
|
||||
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)
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
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'],
|
||||
},
|
||||
),
|
||||
]
|
|
@ -1,11 +1,17 @@
|
|||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.conf import settings
|
||||
from django.forms import TextInput
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from ckeditor.fields import RichTextField
|
||||
from embed_video.fields import EmbedVideoField
|
||||
|
||||
class VarCharField(models.TextField):
|
||||
def formfield(self, **kwargs):
|
||||
kwargs.update({'widget': TextInput})
|
||||
return super().formfield(**kwargs)
|
||||
|
||||
# From https://github.com/JaapJoris/django-numberedmodel.git
|
||||
class NumberedModel(models.Model):
|
||||
def number_with_respect_to(self):
|
||||
|
@ -57,7 +63,7 @@ class Page(NumberedModel):
|
|||
menu = models.BooleanField(_('visible in menu'), default=True)
|
||||
|
||||
def __str__(self):
|
||||
return '{}. {}'.format(self.position, self.title)
|
||||
return '{}: {}. {}'.format(self._meta.verbose_name.title(), self.position, self.title)
|
||||
|
||||
def get_absolute_url(self):
|
||||
if self.slug:
|
||||
|
@ -87,19 +93,37 @@ class Section(NumberedModel):
|
|||
return self.page.sections.all()
|
||||
|
||||
def __str__(self):
|
||||
if not self.position:
|
||||
title = _('New section')
|
||||
elif not self.title:
|
||||
title = '{}. {}'.format(self.position, _('Untitled'))
|
||||
else:
|
||||
title = '{}. {}'.format(self.position, self.title)
|
||||
return str(title)
|
||||
title = self.title if self.title else _('Untitled')
|
||||
return '{}: {}. {}'.format(self._meta.verbose_name.title(), self.position, title)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('section')
|
||||
verbose_name_plural = _('sections')
|
||||
ordering = ['position']
|
||||
|
||||
class SubSection(NumberedModel):
|
||||
section = models.ForeignKey(Section, verbose_name=_('section'), related_name='subsections', on_delete=models.PROTECT)
|
||||
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):
|
||||
title = self.title if self.title else _('Untitled')
|
||||
return '{}: {}. {}'.format(self._meta.verbose_name.title(), self.position, title)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('subsection')
|
||||
verbose_name_plural = _('subsections')
|
||||
ordering = ['position']
|
||||
|
||||
class Config(models.Model):
|
||||
TYPES = [
|
||||
(10, _('Footer')),
|
||||
|
|
|
@ -113,12 +113,15 @@ nav {
|
|||
}
|
||||
|
||||
div.edit {
|
||||
position: fixed;
|
||||
left: 1em;
|
||||
top: 1em;
|
||||
font-size: 0.8em;
|
||||
color: red;
|
||||
|
||||
&.page {
|
||||
position: fixed;
|
||||
left: 1em;
|
||||
top: 1em;
|
||||
}
|
||||
|
||||
a, button {
|
||||
color: inherit;
|
||||
border: none;
|
||||
|
|
|
@ -37,6 +37,7 @@ header, section, footer {
|
|||
nav ul#menu li a:hover, nav ul#menu li a.current {
|
||||
transform: scale(1.2);
|
||||
transform-origin: bottom; } }
|
||||
|
||||
@media (max-width: 500px) {
|
||||
nav button#hamburger {
|
||||
position: absolute;
|
||||
|
@ -79,11 +80,12 @@ header, section, footer {
|
|||
transform: translatex(0); } }
|
||||
|
||||
div.edit {
|
||||
position: fixed;
|
||||
left: 1em;
|
||||
top: 1em;
|
||||
font-size: 0.8em;
|
||||
color: red; }
|
||||
div.edit.page {
|
||||
position: fixed;
|
||||
left: 1em;
|
||||
top: 1em; }
|
||||
div.edit a, div.edit button {
|
||||
color: inherit;
|
||||
border: none;
|
||||
|
@ -102,9 +104,11 @@ a.edit {
|
|||
|
||||
article section div.image img {
|
||||
width: 100%; }
|
||||
|
||||
article section div.title {
|
||||
font-size: 2em;
|
||||
text-align: center; }
|
||||
|
||||
article section div.video div.iframe {
|
||||
width: 100%;
|
||||
padding-bottom: 56%;
|
||||
|
@ -115,6 +119,7 @@ article section div.video div.iframe {
|
|||
height: 100%;
|
||||
left: 0;
|
||||
top: 0; }
|
||||
|
||||
article section div.button {
|
||||
text-align: center; }
|
||||
article section div.button a {
|
||||
|
@ -134,6 +139,7 @@ form div.global_error {
|
|||
background: #f001;
|
||||
color: red;
|
||||
font-weight: bold; }
|
||||
|
||||
form fieldset {
|
||||
padding: 2em;
|
||||
margin-bottom: 2em;
|
||||
|
@ -141,33 +147,43 @@ form fieldset {
|
|||
border-radius: 3px; }
|
||||
form fieldset legend {
|
||||
font-size: 1.15em; }
|
||||
|
||||
form div.formfield {
|
||||
margin: 5px 0;
|
||||
padding: 10px 0;
|
||||
font-size: 0; }
|
||||
|
||||
form div.formfield > * {
|
||||
font-size: 1rem; }
|
||||
|
||||
form div.formfield.error {
|
||||
border: 2px dotted red;
|
||||
padding: 10px;
|
||||
margin: 0 -10px;
|
||||
background: #f001; }
|
||||
|
||||
form div.formfield.required div.label {
|
||||
font-weight: 700; }
|
||||
|
||||
form div.formfield.required input, form div.formfield.required select, form div.formfield.required textarea {
|
||||
border: 1px solid black; }
|
||||
|
||||
form div.label {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 400; }
|
||||
|
||||
form div.input {
|
||||
overflow: hidden;
|
||||
margin: 5px 0; }
|
||||
|
||||
form div.helptext, form span.required {
|
||||
color: #666;
|
||||
font-size: 12px !important;
|
||||
font-weight: 400 !important; }
|
||||
|
||||
form span.required {
|
||||
font-style: italic; }
|
||||
|
||||
form input, form select, form textarea {
|
||||
background: white;
|
||||
color: black;
|
||||
|
@ -178,21 +194,27 @@ form input, form select, form textarea {
|
|||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 5px; }
|
||||
|
||||
form div.django-ckeditor-widget {
|
||||
display: block !important; }
|
||||
|
||||
form div.cke_chrome {
|
||||
box-sizing: border-box !important;
|
||||
border: 1px solid #aaa !important; }
|
||||
|
||||
form input[type=checkbox] {
|
||||
width: auto; }
|
||||
|
||||
form select {
|
||||
background: white; }
|
||||
|
||||
form div.filefield {
|
||||
border: 1px solid #aaa;
|
||||
background: white;
|
||||
padding: 4px; }
|
||||
form div.filefield input {
|
||||
border: none; }
|
||||
|
||||
form ul.errorlist {
|
||||
margin: 0;
|
||||
margin-bottom: 1em;
|
||||
|
@ -203,8 +225,9 @@ form ul.errorlist {
|
|||
form ul.errorlist li {
|
||||
margin: 0;
|
||||
padding: 0; }
|
||||
|
||||
form .errors {
|
||||
color: red;
|
||||
font-weight: bold; }
|
||||
|
||||
/*# sourceMappingURL=cms.scss.css.map */
|
||||
/*# sourceMappingURL=cms.scss.css.map */
|
File diff suppressed because one or more lines are too long
|
@ -1,23 +1,29 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block header %}{% endblock %}
|
||||
{% block nav %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form method="POST" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
{{form.media}}
|
||||
|
||||
<div class="wrapper">
|
||||
{% if form.errors or formset.errors %}
|
||||
<div class="global_error">
|
||||
{% trans 'Please correct the error(s) below and save again' %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% for field in form %}
|
||||
{% include 'cms/formfield.html' with field=field %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<section>
|
||||
<div class="wrapper">
|
||||
<h1>{{form.instance}}</h1>
|
||||
{% if form.errors or formset.errors %}
|
||||
<div class="global_error">
|
||||
{% trans 'Please correct the error(s) below and save again' %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% for field in form %}
|
||||
{% include 'cms/formfield.html' with field=field %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% block sections %}
|
||||
{% block formset %}
|
||||
{{formset.management_form}}
|
||||
{% for form in formset %}
|
||||
{{form.media}}
|
||||
|
@ -26,16 +32,26 @@
|
|||
{% endfor %}
|
||||
<section>
|
||||
<div class="wrapper">
|
||||
<h1>{% trans 'Section' %}: {{form.instance}}</h1>
|
||||
<h1>{{form.instance}}</h1>
|
||||
{% for field in form.visible_fields %}
|
||||
{% include 'cms/formfield.html' with field=field %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
{% endfor %}
|
||||
|
||||
<section>
|
||||
<div class="wrapper">
|
||||
<div class="button">
|
||||
<a href="{{formset_form_url}}">
|
||||
Add a new {{formset_description}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
<div class="edit">
|
||||
<div class="edit page">
|
||||
<button>{% trans 'save' %}</button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<form method="POST" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
{{form.media}}
|
||||
|
||||
<div class="wrapper">
|
||||
{% if form.errors or formset.errors %}
|
||||
<div class="global_error">
|
||||
{% trans 'Please correct the error(s) below and save again' %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% for field in form %}
|
||||
{% include 'cms/formfield.html' with field=field %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% block sections %}
|
||||
{{formset.management_form}}
|
||||
{% for form in formset %}
|
||||
{{form.media}}
|
||||
{% for field in form.hidden_fields %}
|
||||
{{field}}
|
||||
{% endfor %}
|
||||
<section>
|
||||
<div class="wrapper">
|
||||
<h1>{% trans 'Section' %}: {{form.instance}}</h1>
|
||||
{% for field in form.visible_fields %}
|
||||
{% include 'cms/formfield.html' with field=field %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
<div class="edit">
|
||||
<button>{% trans 'save' %}</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
|
@ -0,0 +1,43 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<form method="POST" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
{{form.media}}
|
||||
|
||||
<section>
|
||||
<div class="wrapper">
|
||||
<h1>{{form.model}}: {{form.object}}</h1>
|
||||
{% if form.errors or formset.errors %}
|
||||
<div class="global_error">
|
||||
{% trans 'Please correct the error(s) below and save again' %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% for field in form %}
|
||||
{% include 'cms/formfield.html' with field=field %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{{formset.management_form}}
|
||||
{% for form in formset %}
|
||||
{{form.media}}
|
||||
{% for field in form.hidden_fields %}
|
||||
{{field}}
|
||||
{% endfor %}
|
||||
<section>
|
||||
<div class="wrapper">
|
||||
<h1>{% trans 'Subsection' %}: {{form.instance}}</h1>
|
||||
{% for field in form.visible_fields %}
|
||||
{% include 'cms/formfield.html' with field=field %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
{% endfor %}
|
||||
|
||||
<div class="edit">
|
||||
<button>{% trans 'save' %}</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
|
@ -1,3 +1,3 @@
|
|||
{% extends 'cms/edit.html' %}
|
||||
|
||||
{% block sections %}{% endblock %}
|
||||
{% block formset %}{% endblock %}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
{% block title %}{{block.super}} - {{object.title}}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="edit">
|
||||
<div class="edit page">
|
||||
{% if user.is_staff %}
|
||||
{% if object.slug %}
|
||||
<a href="{% url 'cms:updatepage' object.slug %}">{% trans 'edit this page' %}</a>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
{% load thumbnail embed_video_tags %}
|
||||
{% load i18n thumbnail embed_video_tags %}
|
||||
|
||||
<div class="wrapper">
|
||||
|
||||
|
@ -41,4 +41,10 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if user.is_staff %}
|
||||
<div class="edit">
|
||||
<a href="{% url 'cms:updatesection' section.pk %}">{% trans 'edit this section' %}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from django.urls import path
|
||||
from .views import PageView, UpdatePage, CreatePage
|
||||
from .views import PageView, UpdatePage, CreatePage, UpdateSection, CreateSection, CreateSubSection
|
||||
|
||||
app_name = 'cms'
|
||||
|
||||
|
@ -7,6 +7,9 @@ urlpatterns = [
|
|||
path('', PageView.as_view(), {'slug': ''}, name='homepage'),
|
||||
path('<slug:slug>/', PageView.as_view(), name='page'),
|
||||
path('cms/homepage/', UpdatePage.as_view(), {'slug': ''}, name='updatehomepage'),
|
||||
path('cms/page/<slug:slug>/', UpdatePage.as_view(), name='updatepage'),
|
||||
path('cms/page/<int:pk>/', UpdatePage.as_view(), name='updatepage'),
|
||||
path('cms/section/<int:pk>/', UpdateSection.as_view(), name='updatesection'),
|
||||
path('cms/newpage/', CreatePage.as_view(), name='createpage'),
|
||||
path('cms/page/<int:pk>/newsection/', CreateSection.as_view(), name='createsection'),
|
||||
path('cms/section/<int:pk>/newsubsection/', CreateSubSection.as_view(), name='createsubsection'),
|
||||
]
|
||||
|
|
61
cms/views.py
61
cms/views.py
|
@ -1,9 +1,10 @@
|
|||
from django.urls import reverse
|
||||
from django.shortcuts import redirect
|
||||
from django.contrib.auth.mixins import UserPassesTestMixin
|
||||
from django.views.generic import DetailView, UpdateView, CreateView
|
||||
|
||||
from .models import Page
|
||||
from .forms import PageForm, SectionFormSet
|
||||
from .models import Page, Section, SubSection
|
||||
from .forms import PageForm, SectionFormSet, SectionForm, SubSectionFormSet, SubSectionForm
|
||||
from .utils import get_config
|
||||
|
||||
class StaffRequiredMixin(UserPassesTestMixin):
|
||||
|
@ -25,20 +26,38 @@ class PageView(MenuMixin, DetailView):
|
|||
model = Page
|
||||
template_name = 'cms/page.html'
|
||||
|
||||
class CreatePage(StaffRequiredMixin, MenuMixin, CreateView):
|
||||
class CreatePage(StaffRequiredMixin, CreateView):
|
||||
model = Page
|
||||
form_class = PageForm
|
||||
template_name = 'cms/new.html'
|
||||
|
||||
class UpdatePage(StaffRequiredMixin, MenuMixin, UpdateView):
|
||||
model = Page
|
||||
form_class = PageForm
|
||||
class CreateSection(StaffRequiredMixin, 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(reverse('cms:updatepage', args=[form.instance.page.pk]))
|
||||
|
||||
class CreateSubSection(StaffRequiredMixin, 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(reverse('cms:updatesection', args=[form.instance.section.pk]))
|
||||
|
||||
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 = SectionFormSet(request.POST, request.FILES, instance=self.object)
|
||||
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:
|
||||
|
@ -47,7 +66,7 @@ class UpdatePage(StaffRequiredMixin, MenuMixin, UpdateView):
|
|||
def form_valid(self, form, formset):
|
||||
form.save()
|
||||
formset.save()
|
||||
return redirect(form.instance.get_absolute_url())
|
||||
return redirect(self.get_absolute_url(form.instance))
|
||||
|
||||
def form_invalid(self, form, formset):
|
||||
return self.render_to_response(self.get_context_data(form=form, formset=formset))
|
||||
|
@ -55,8 +74,32 @@ class UpdatePage(StaffRequiredMixin, MenuMixin, UpdateView):
|
|||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
if 'formset' not in context:
|
||||
formset = SectionFormSet(instance=self.object)
|
||||
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.title(),
|
||||
})
|
||||
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])
|
||||
|
||||
def get_absolute_url(self, instance):
|
||||
return instance.get_absolute_url()
|
||||
|
||||
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])
|
||||
|
||||
def get_absolute_url(self, instance):
|
||||
return instance.page.get_absolute_url()
|
||||
|
|
Ładowanie…
Reference in New Issue