Re-introduce formsets

No more clicking individual sections to edit them. From now on, there will
only be one edit button! The biggest challenge was getting the javascript
closures to understand.
readwriteweb
Jaap Joris Vens 2020-02-14 17:20:41 +01:00
rodzic ddbd220527
commit 3f900617c8
13 zmienionych plików z 135 dodań i 56 usunięć

Wyświetl plik

@ -2,6 +2,7 @@ import swapper
from django import forms from django import forms
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.forms import inlineformset_factory
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
Page = swapper.load_model('cms', 'Page') Page = swapper.load_model('cms', 'Page')
@ -42,8 +43,13 @@ class SectionForm(forms.ModelForm):
# 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]
def save(self): def delete(self):
instance = super().save()
instance.delete()
def save(self, commit=True):
section = super().save() section = super().save()
# Explanation: get the content type of the model that the user # Explanation: get the content type of the model that the user
@ -56,7 +62,8 @@ class SectionForm(forms.ModelForm):
model=section.type.lower(), model=section.type.lower(),
) )
section.save() if commit:
section.save()
return section return section
class Meta: class Meta:
@ -69,3 +76,5 @@ class SectionForm(forms.ModelForm):
# There is definitely a bug in Django, since the above 'field_classes' gets # There is definitely a bug in Django, since the above 'field_classes' gets
# ignored entirely. Workaround to force a ChoiceField anyway: # ignored entirely. Workaround to force a ChoiceField anyway:
type = forms.ChoiceField() type = forms.ChoiceField()
SectionFormSet = inlineformset_factory(Page, Section, form=SectionForm, extra=1)

Wyświetl plik

@ -289,12 +289,12 @@ form.cms {
clear: both; clear: both;
box-sizing: border-box; box-sizing: border-box;
&#type, &#number { &.type, &.number {
width: 75%; width: 75%;
clear: none; clear: none;
float: left; float: left;
} }
&#number { &.number {
width: 20%; width: 20%;
float: right; float: right;
} }

Wyświetl plik

@ -206,11 +206,11 @@ form.cms div.formfield {
padding: 10px 0; padding: 10px 0;
clear: both; clear: both;
box-sizing: border-box; } box-sizing: border-box; }
form.cms div.formfield#type, form.cms div.formfield#number { form.cms div.formfield.type, form.cms div.formfield.number {
width: 75%; width: 75%;
clear: none; clear: none;
float: left; } float: left; }
form.cms div.formfield#number { form.cms div.formfield.number {
width: 20%; width: 20%;
float: right; } float: right; }

Wyświetl plik

@ -0,0 +1,39 @@
{% extends 'base.html' %}
{% load i18n %}
{% block content %}
<form method="post" class="cms">
{% csrf_token %}
{{form.media}}
<section>
<div class="wrapper">
{% if protected %}
<div class="title">
<h1>You can't do that</h1>
</div>
In order to delete this page, you will first need to delete the following sections:
<ul>
{{protected|unordered_list}}
</ul>
{% else %}
<div class="title">
<h1>Are you sure?</h1>
</div>
The following objects will be deleted:
<ul>
{{deleted|unordered_list}}
</ul>
<button name="confirm">{% trans 'delete' %}</button>
{% endif %}
</div>
</section>
</form>
{% endblock %}

Wyświetl plik

@ -5,6 +5,9 @@
<form method="POST" enctype="multipart/form-data" class="cms"> <form method="POST" enctype="multipart/form-data" class="cms">
{% csrf_token %} {% csrf_token %}
{{form.media}} {{form.media}}
<div class="edit page">
<button>{% trans 'save' %}</button>
</div>
<section> <section>
<div class="wrapper"> <div class="wrapper">
@ -19,6 +22,17 @@
{% for field in form %} {% for field in form %}
{% include 'cms/formfield.html' with field=field %} {% include 'cms/formfield.html' with field=field %}
{% endfor %} {% endfor %}
{% if form.instance.pk %}
<div class="formfield">
<div class="label">
<label for="id_delete">{% trans 'Delete' %}:</label>
</div>
<div class="input">
<input type="checkbox" name="delete" id="id_delete">
</div>
</div>
{% endif %}
</div> </div>
</section> </section>
@ -32,27 +46,19 @@
<section> <section>
<div class="wrapper"> <div class="wrapper">
<h1>{{form.instance}}</h1> <h1>{{form.instance}}</h1>
{% for field in form.visible_fields %} <div class="subform">
{% include 'cms/formfield.html' with field=field %} {% for field in form.visible_fields %}
{% endfor %} {% if field.name == 'DELETE' and not form.instance.pk %}
{% else %}
{% include 'cms/formfield.html' with field=field counter=forloop.parentloop.counter0 %}
{% endif %}
{% endfor %}
</div>
</div> </div>
</section> </section>
{% endfor %} {% endfor %}
<section>
<div class="wrapper">
<div class="edit">
<a href="{{formset_form_url}}">
+ {% trans 'new' %} {{formset_description}}
</a>
</div>
</div>
</section>
{% endif %} {% endif %}
<div class="edit page">
<button>{% trans 'save' %}</button>
</div>
</form> </form>
{% endblock %} {% endblock %}
@ -61,7 +67,6 @@
<script> <script>
var slugfield = document.getElementById('id_slug'); var slugfield = document.getElementById('id_slug');
var titlefield = document.getElementById('id_title'); var titlefield = document.getElementById('id_title');
var typefield = document.getElementById('id_type');
/* My own implementation of Django's prepopulate.js */ /* My own implementation of Django's prepopulate.js */
@ -81,29 +86,52 @@
} }
{% if fields_per_type %} {% if fields_per_type %}
/* Only show relevant fields */
var typefield = document.getElementById('id_type');
fields_per_type = {{fields_per_type|safe}};
if (typefield) { if (typefield) {
function show_relevant_fields(type) { function show_relevant_fields(type) {
fields_per_type = {{fields_per_type|safe}};
for (el of document.querySelectorAll('div.formfield')) { for (el of document.querySelectorAll('div.formfield')) {
el.style.display = 'none'; el.style.display = 'none';
} }
for (field of fields_per_type[type]) { for (field of fields_per_type[type]) {
el = document.getElementById(field); el = document.getElementById(field);
console.log('showing ' + field)
el.style.display = 'block'; el.style.display = 'block';
} }
} }
typefield.addEventListener('input', function(event) { typefield.addEventListener('input', function(event) {
console.log(typefield.value);
show_relevant_fields(typefield.value.toLowerCase()); show_relevant_fields(typefield.value.toLowerCase());
}); });
show_relevant_fields(typefield.value.toLowerCase()); show_relevant_fields(typefield.value.toLowerCase());
} }
subforms = document.querySelectorAll('div.subform');
for (let i = 0; i < subforms.length; ++i) {
let typefield = document.getElementById('id_sections-' + i + '-type');
function show_relevant_fields(type) {
for (field of subforms[i].querySelectorAll('div.formfield')) {
field.style.display = 'none';
}
delete_checkbox = document.getElementById('DELETE' + i);
if (delete_checkbox) {
delete_checkbox.style.display = 'block';
}
for (field of fields_per_type[type]) {
el = document.getElementById(field + i);
el.style.display = 'block';
}
}
typefield.addEventListener('input', function(event) {
(show_relevant_fields(typefield.value.toLowerCase()));
});
show_relevant_fields(typefield.value.toLowerCase());
}
{% endif %} {% endif %}
</script> </script>
{% endblock %} {% endblock %}

Wyświetl plik

@ -1,4 +1,4 @@
<div class="formfield{% if field.errors %} error{% endif %}{% if field.field.required %} required{% endif %}" id="{{field.name}}"> <div class="formfield{% if field.errors %} error{% endif %}{% if field.field.required %} required{% endif %} {{field.name}}" id="{{field.name}}{{counter}}">
<div class="errors"> <div class="errors">
{{field.errors}} {{field.errors}}
</div> </div>

Wyświetl plik

@ -21,14 +21,4 @@
{% endif %} {% endif %}
</div> </div>
{% if user.is_staff %}
<section>
<div class="wrapper">
<div class="edit">
<a href="{% url 'cms:createsection' page.pk %}">+ {% trans 'new section' %}</a>
</div>
</div>
</section>
{% endif %}
{% endblock %} {% endblock %}

Wyświetl plik

@ -4,13 +4,14 @@ import swapper
from django.views import generic from django.views import generic
from django.shortcuts import redirect from django.shortcuts import redirect
from django.views.generic.edit import FormMixin from django.views.generic.edit import FormMixin
from django.contrib.admin.utils import NestedObjects
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.contrib.contenttypes.models import ContentType 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 HttpResponseRedirect, HttpResponseBadRequest from django.http import HttpResponseRedirect, HttpResponseBadRequest
from .decorators import register_view from .decorators import register_view
from .forms import PageForm, SectionForm from .forms import PageForm, SectionForm, SectionFormSet
Page = swapper.load_model('cms', 'Page') Page = swapper.load_model('cms', 'Page')
Section = swapper.load_model('cms', 'Section') Section = swapper.load_model('cms', 'Section')
@ -138,13 +139,38 @@ class BaseUpdateView(generic.UpdateView):
template_name = 'cms/edit.html' template_name = 'cms/edit.html'
def form_valid(self, form): def form_valid(self, form):
form.save() if 'delete' in self.request.POST:
collector = NestedObjects(using='default')
collector.collect([self.object])
self.template_name = 'cms/confirm.html'
return self.render_to_response(self.get_context_data(deleted=collector.nested(), protected=collector.protected))
else:
form.save()
return redirect(self.request.session.get('previous_url')) return redirect(self.request.session.get('previous_url'))
class UpdatePage(StaffRequiredMixin, MenuMixin, BaseUpdateView): class UpdatePage(StaffRequiredMixin, TypeMixin, BaseUpdateView):
model = Page model = Page
form_class = PageForm form_class = PageForm
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
if 'formset' not in context:
context['formset'] = SectionFormSet(instance=self.object)
return context
def post(self, request, *args, **kwargs):
self.object = self.get_object()
form = self.get_form()
formset = SectionFormSet(request.POST, request.FILES, instance=self.object)
if form.is_valid() and formset.is_valid():
formset.save()
return self.form_valid(form)
else:
return self.form_invalid(form, formset)
def form_invalid(self, form, formset):
return self.render_to_response(self.get_context_data(form=form, formset=formset))
class UpdateSection(StaffRequiredMixin, TypeMixin, BaseUpdateView): class UpdateSection(StaffRequiredMixin, TypeMixin, BaseUpdateView):
model = Section model = Section
form_class = SectionForm form_class = SectionForm

Wyświetl plik

@ -5,7 +5,5 @@
<a class="button" href="{{section.button_link}}">{{section.button_text}}</a> <a class="button" href="{{section.button_link}}">{{section.button_text}}</a>
</div> </div>
{% endif %} {% endif %}
{% include 'cms/editlink.html' %}
</div> </div>
</section> </section>

Wyświetl plik

@ -1,5 +1,4 @@
{% load i18n %} {% load i18n %}
<section class="{{section.type}} color{{section.color}}"> <section class="{{section.type}} color{{section.color}}">
<div class="wrapper"> <div class="wrapper">
<div class="title"> <div class="title">
@ -15,7 +14,5 @@
<button class="button" name="section" value="{{section.pk}}">{% trans 'Send' %}</button> <button class="button" name="section" value="{{section.pk}}">{% trans 'Send' %}</button>
</form> </form>
</div> </div>
{% include 'cms/editlink.html' %}
</div> </div>
</section> </section>

Wyświetl plik

@ -2,13 +2,10 @@
<section class="{{section.type}} color{{section.color}}"> <section class="{{section.type}} color{{section.color}}">
<div class="wrapper"> <div class="wrapper">
{% if section.image %} {% if section.image %}
<div class="image"> <div class="image">
<img alt="{{section.title}}" src="{% thumbnail section.image 800x800 %}"> <img alt="{{section.title}}" src="{% thumbnail section.image 800x800 %}">
</div> </div>
{% endif %} {% endif %}
{% include 'cms/editlink.html' %}
</div> </div>
</section> </section>

Wyświetl plik

@ -15,7 +15,5 @@
{{section.content|markdown}} {{section.content|markdown}}
</div> </div>
{% endif %} {% endif %}
{% include 'cms/editlink.html' %}
</div> </div>
</section> </section>

Wyświetl plik

@ -2,7 +2,6 @@
<section class="{{section.type}} color{{section.color}}"> <section class="{{section.type}} color{{section.color}}">
<div class="wrapper"> <div class="wrapper">
{% if section.video %} {% if section.video %}
<div class="video"> <div class="video">
<div class="iframe"> <div class="iframe">
@ -10,7 +9,5 @@
</div> </div>
</div> </div>
{% endif %} {% endif %}
{% include 'cms/editlink.html' %}
</div> </div>
</section> </section>