Refactor forms.py: quite elegant and reusable Page _and_ Section forms. Both

work with the same edit.html template. Finally you can edit sections
individually!
main
Jaap Joris Vens 2020-03-13 23:08:21 +01:00
rodzic 1989615e60
commit d8d54ea4c4
5 zmienionych plików z 125 dodań i 81 usunięć

Wyświetl plik

@ -10,12 +10,13 @@ Section = swapper.load_model('cms', 'Section')
class PageForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.label_suffix = ''
extra = 1 if self.instance.pk else 2
self.formsets = [forms.inlineformset_factory(
parent_model = Page,
model = Section,
form = SectionForm,
formset = BaseSectionFormSet,
extra=extra,
)(
data=self.data if self.is_bound else None,
@ -33,8 +34,6 @@ class PageForm(forms.ModelForm):
super().clean()
if not self.formsets[0].is_valid():
self.add_error(None, _('Theres a problem saving one of the sections'))
if not self.instance and not self.formsets[0].has_changed():
self.add_error(None, _('You cant save a new page without adding any sections!'))
def save(self, *args, **kwargs):
page = super().save()
@ -55,18 +54,48 @@ class SectionForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.label_suffix = ''
self.fields['DELETE'] = forms.BooleanField(label=_('Delete'), required=False)
# Repopulate the 'choices' attribute of the type field from
# the child model.
self.fields['type'].choices = self._meta.model.TYPES
self.fields['type'].initial = self._meta.model.TYPES[0][0]
def delete(self):
instance = super().save()
instance.delete()
section = self.instance
self.formsets = []
for field in section._meta.get_fields():
if field.one_to_many:
extra = 1 if getattr(section, field.name).exists() else 2
formset = forms.inlineformset_factory(
Section, field.related_model,
fields='__all__',
extra=extra,
)(
instance=section,
data=self.data if self.is_bound else None,
files=self.files if self.is_bound else None,
prefix=f'{self.prefix}-{field.name}',
form_kwargs={'label_suffix': self.label_suffix},
)
formset.name = field.name
self.formsets.append(formset)
def is_valid(self):
result = super().is_valid()
if self.is_bound:
for formset in self.formsets:
result = result and formset.is_valid()
return result
def save(self, commit=True):
section = super().save()
section = super().save(commit=commit)
if self.cleaned_data['DELETE']:
section.delete()
if section.page.slug and not section.page.sections.exists():
section.page.delete()
return
# Explanation: get the content type of the model that the user
# supplied when filling in this form, and save it's id to the
@ -80,72 +109,15 @@ class SectionForm(forms.ModelForm):
if commit:
section.save()
for formset in self.formsets:
formset.save(commit=commit)
return section
core_field_names = ['title', 'type', 'number', 'content', 'image', 'video', 'href']
def core_fields(self):
return [field for field in self.visible_fields() if field.name in self.core_field_names]
def extra_fields(self):
return [field for field in self.visible_fields() if field.name not in self.core_field_names]
class Meta:
model = Section
exclude = ['page']
class BaseSectionFormSet(forms.BaseInlineFormSet):
'''If a swappable Section model defines one-to-many fields, (i.e. has
foreign keys pointing to it) formsets will be generated for the
related models and stored in the form.formsets array.
Based on this logic for nested formsets:
https://www.yergler.net/2013/09/03/nested-formsets-redux/
Typical usecases could be:
- an images section that displays multiple images
- a column section that displays separate colums
- a calendar section that displays calendar events
- etc...
'''
def add_fields(self, form, index):
super().add_fields(form, index)
section = form.instance
form.formsets = []
for field in section._meta.get_fields():
if field.one_to_many:
extra = 1 if getattr(section, field.name).exists() else 2
formset = forms.inlineformset_factory(
Section, field.related_model,
fields='__all__',
extra=extra,
)(
instance=section,
data=form.data if self.is_bound else None,
files=form.files if self.is_bound else None,
prefix=f'{form.prefix}-{field.name}',
form_kwargs=self.form_kwargs,
)
formset.name = field.name
form.formsets.append(formset)
def is_valid(self):
result = super().is_valid()
if self.is_bound:
for form in self.forms:
for formset in form.formsets:
result = result and formset.is_valid()
return result
def save(self, commit=True):
result = super().save(commit=commit)
for form in self:
for formset in form.formsets:
formset.save(commit=commit)
return result
class ContactForm(forms.Form):
sender = forms.EmailField(label=_('Your email address'))
spam_protection = forms.CharField(label=_('Your message'), widget=forms.Textarea())

Wyświetl plik

@ -22,11 +22,14 @@ class Numbered:
def number_with_respect_to(self):
return self.__class__.objects.all()
def get_field_name(self):
return self.__class__._meta.ordering[-1].lstrip('-')
def _renumber(self):
'''Renumbers the queryset while preserving the instance's number'''
queryset = self.number_with_respect_to()
field_name = self.__class__._meta.ordering[-1].lstrip('-')
field_name = self.get_field_name()
this_nr = getattr(self, field_name)
if this_nr is None:
this_nr = len(queryset) + 1
@ -55,8 +58,9 @@ class Numbered:
super().save(*args, **kwargs)
def delete(self, *args, **kwargs):
super().delete(*args, **kwargs)
setattr(self, self.get_field_name(), 9999) # hack
self._renumber()
super().delete(*args, **kwargs)
class BasePage(Numbered, models.Model):
'''Abstract base model for pages'''

Wyświetl plik

@ -98,6 +98,7 @@
function setEventHandlers() {
for (let typefield of document.querySelectorAll('select[name$=-type]')) {
let formId = typefield.name.replace(/-type$/, '');
console.log(formId + 'huh');
let form = document.getElementById(formId);
let type = typefield.value.toLowerCase();
showRelevantFields(form, type);

Wyświetl plik

@ -1,12 +1,15 @@
from django.urls import path
from .views import PageView, CreatePage, UpdatePage
from .views import PageView, CreatePage, UpdatePage, CreateSection, UpdateSection
app_name = 'cms'
urlpatterns = [
path('new/', CreatePage.as_view(), name='createpage'),
path('edit/', UpdatePage.as_view(), name='updatepage'),
path('edit/', UpdatePage.as_view(), kwargs={'slug': ''}, name='updatepage'),
path('edit/<int:number>/', UpdateSection.as_view(), kwargs={'slug': ''}, name='updatesection'),
path('<slug:slug>/edit/', UpdatePage.as_view(), name='updatepage'),
path('<slug:slug>/edit/new/', CreateSection.as_view(), name='createsection'),
path('<slug:slug>/edit/<int:number>/', UpdateSection.as_view(), name='updatesection'),
path('', PageView.as_view(), name='page'),
path('<slug:slug>/', PageView.as_view(), name='page'),
]

Wyświetl plik

@ -10,7 +10,7 @@ from django.contrib.contenttypes.models import ContentType
from django.contrib.auth.mixins import UserPassesTestMixin
from .decorators import register_view
from .forms import PageForm
from .forms import PageForm, SectionForm
Page = swapper.load_model('cms', 'Page')
Section = swapper.load_model('cms', 'Section')
@ -143,16 +143,10 @@ class EditPage(UserPassesTestMixin, edit.ModelFormMixin, base.TemplateResponseMi
app, model = swapper.get_model_name('cms', 'page').lower().split('.')
return self.request.user.has_perm('f{app}_{model}_change')
def setup(self, *args, slug='', **kwargs):
'''Supply a default argument for slug'''
super().setup(*args, slug=slug, **kwargs)
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs.update({
'label_suffix': '',
'initial': {'slug': self.kwargs['slug']},
})
if 'slug' in self.kwargs:
kwargs.update({'initial': {'slug': self.kwargs['slug']}})
return kwargs
def get_context_data(self, **kwargs):
@ -194,7 +188,77 @@ class EditPage(UserPassesTestMixin, edit.ModelFormMixin, base.TemplateResponseMi
class CreatePage(EditPage):
def get_object(self):
pass
return Page()
class UpdatePage(EditPage):
pass
class EditSection(UserPassesTestMixin, edit.ModelFormMixin, base.TemplateResponseMixin, base.View):
model = Section
form_class = SectionForm
template_name = 'cms/edit.html'
def test_func(self):
app, model = swapper.get_model_name('cms', 'section').lower().split('.')
return self.request.user.has_perm('f{app}_{model}_change')
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs.update({
'prefix': 'section',
})
return kwargs
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
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
def get_object(self, queryset=None):
try:
self.page = Page.objects.get(slug=self.kwargs['slug'])
except Page.DoesNotExist:
raise Http404()
return self.get_section()
def get_section(self):
try:
section = self.page.sections.get(number=self.kwargs['number'])
except Section.DoesNotExist:
raise Http404()
return section
def get(self, request, *args, **kwargs):
self.object = self.get_object()
return self.render_to_response(self.get_context_data())
def post(self, request, *args, **kwargs):
self.object = self.get_object()
form = self.get_form()
if form.is_valid():
section = form.save()
if section:
return HttpResponseRedirect(section.page.get_absolute_url())
elif self.page.sections.exists():
return HttpResponseRedirect(self.page.get_absolute_url())
else:
return HttpResponseRedirect('/')
return self.render_to_response(self.get_context_data(form=form))
class CreateSection(EditSection):
def get_section(self):
return Section(page=self.page)
class UpdateSection(EditSection):
pass