Add structured content editing in Django admin
This commit is contained in:
167
apps/content/forms.py
Normal file
167
apps/content/forms.py
Normal file
@@ -0,0 +1,167 @@
|
||||
from django import forms
|
||||
|
||||
from .models import ABOUT_KEYS, HOME_KEYS, MediaAsset
|
||||
|
||||
|
||||
ROUTE_CHOICES = (("", "—"), ("about", "About"), ("events", "Events"), ("blog", "Blog"))
|
||||
CONTACT_KINDS = ("email", "phone", "telegram", "instagram", "website")
|
||||
|
||||
|
||||
class PageDraftForm(forms.Form):
|
||||
title = forms.CharField(max_length=180)
|
||||
intro = forms.CharField(max_length=600, required=False, widget=forms.Textarea(attrs={"rows": 3}))
|
||||
seo_title = forms.CharField(max_length=120)
|
||||
seo_description = forms.CharField(max_length=300, widget=forms.Textarea(attrs={"rows": 2}))
|
||||
seo_image = forms.ModelChoiceField(queryset=MediaAsset.objects.all(), required=False)
|
||||
|
||||
def payload(self):
|
||||
return {field: self.cleaned_data[field] for field in ("title", "intro", "seo_title", "seo_description", "seo_image")}
|
||||
|
||||
|
||||
class SiteDraftForm(forms.Form):
|
||||
official_name = forms.CharField(max_length=180)
|
||||
short_name = forms.CharField(max_length=80)
|
||||
faculty_subline = forms.CharField(max_length=140, required=False)
|
||||
description = forms.CharField(max_length=500, required=False, widget=forms.Textarea(attrs={"rows": 3}))
|
||||
logo = forms.ModelChoiceField(queryset=MediaAsset.objects.all(), required=False)
|
||||
logo_alt = forms.CharField(max_length=180, required=False)
|
||||
footer_copy = forms.CharField(max_length=300, required=False)
|
||||
seo_title = forms.CharField(max_length=120, required=False)
|
||||
seo_description = forms.CharField(max_length=300, required=False, widget=forms.Textarea(attrs={"rows": 2}))
|
||||
seo_image = forms.ModelChoiceField(queryset=MediaAsset.objects.all(), required=False)
|
||||
|
||||
def __init__(self, *args, revision=None, **kwargs):
|
||||
initial = kwargs.setdefault("initial", {})
|
||||
if revision:
|
||||
for field in ("official_name", "short_name", "faculty_subline", "description", "logo", "logo_alt", "footer_copy", "seo_title", "seo_description", "seo_image"):
|
||||
initial[field] = getattr(revision, field)
|
||||
super().__init__(*args, **kwargs)
|
||||
navigation = {item["route_id"]: item for item in revision.navigation} if revision else {}
|
||||
contacts = {item["kind"]: item for item in revision.contacts} if revision else {}
|
||||
for position, route in enumerate(("home", "about", "events", "blog")):
|
||||
item = navigation.get(route, {})
|
||||
self.fields[f"nav_{route}_label"] = forms.CharField(max_length=80, initial=item.get("label", route.title()))
|
||||
self.fields[f"nav_{route}_position"] = forms.IntegerField(min_value=0, initial=item.get("position", position))
|
||||
self.fields[f"nav_{route}_visible"] = forms.BooleanField(required=False, initial=item.get("visible", True))
|
||||
for kind in CONTACT_KINDS:
|
||||
item = contacts.get(kind, {})
|
||||
self.fields[f"contact_{kind}_label"] = forms.CharField(max_length=80, required=False, initial=item.get("label", ""))
|
||||
self.fields[f"contact_{kind}_url"] = forms.CharField(max_length=500, required=False, initial=item.get("url", ""))
|
||||
self.fields[f"contact_{kind}_position"] = forms.IntegerField(min_value=0, required=False, initial=item.get("position"))
|
||||
self.fields[f"contact_{kind}_visible"] = forms.BooleanField(required=False, initial=item.get("visible", True))
|
||||
|
||||
def clean(self):
|
||||
cleaned = super().clean()
|
||||
for kind in CONTACT_KINDS:
|
||||
label = cleaned.get(f"contact_{kind}_label")
|
||||
url = cleaned.get(f"contact_{kind}_url")
|
||||
if bool(label) != bool(url):
|
||||
self.add_error(f"contact_{kind}_url", "Contact label and URL must be filled together.")
|
||||
return cleaned
|
||||
|
||||
def payload(self):
|
||||
data = {field: self.cleaned_data[field] for field in ("official_name", "short_name", "faculty_subline", "description", "logo", "logo_alt", "footer_copy", "seo_title", "seo_description", "seo_image")}
|
||||
data["navigation"] = [
|
||||
{"route_id": route, "label": self.cleaned_data[f"nav_{route}_label"], "position": self.cleaned_data[f"nav_{route}_position"], "visible": self.cleaned_data[f"nav_{route}_visible"]}
|
||||
for route in ("home", "about", "events", "blog")
|
||||
]
|
||||
data["contacts"] = []
|
||||
for kind in CONTACT_KINDS:
|
||||
url = self.cleaned_data[f"contact_{kind}_url"]
|
||||
if url:
|
||||
data["contacts"].append({
|
||||
"kind": kind, "label": self.cleaned_data[f"contact_{kind}_label"], "url": url,
|
||||
"position": self.cleaned_data[f"contact_{kind}_position"] if self.cleaned_data[f"contact_{kind}_position"] is not None else len(data["contacts"]),
|
||||
"visible": self.cleaned_data[f"contact_{kind}_visible"],
|
||||
})
|
||||
return data
|
||||
|
||||
|
||||
class EntryForm(forms.Form):
|
||||
title = forms.CharField(max_length=120, required=False)
|
||||
text = forms.CharField(max_length=600, required=False, widget=forms.Textarea(attrs={"rows": 2}))
|
||||
|
||||
def clean(self):
|
||||
cleaned = super().clean()
|
||||
if bool(cleaned.get("title")) != bool(cleaned.get("text")):
|
||||
raise forms.ValidationError("Each entry needs both a title and text.")
|
||||
return cleaned
|
||||
|
||||
|
||||
EntryFormSet = forms.formset_factory(EntryForm, extra=1, max_num=20, validate_max=True, can_delete=True)
|
||||
|
||||
|
||||
class SectionForm(forms.Form):
|
||||
heading = forms.CharField(max_length=120, required=False)
|
||||
body = forms.CharField(max_length=1200, required=False, widget=forms.Textarea(attrs={"rows": 3}))
|
||||
intro = forms.CharField(max_length=300, required=False, widget=forms.Textarea(attrs={"rows": 2}))
|
||||
all_link_label = forms.CharField(max_length=40, required=False)
|
||||
position = forms.IntegerField(min_value=0, required=False)
|
||||
visible = forms.BooleanField(required=False)
|
||||
image = forms.ModelChoiceField(queryset=MediaAsset.objects.all(), required=False)
|
||||
image_alt = forms.CharField(max_length=180, required=False)
|
||||
primary_label = forms.CharField(max_length=40, required=False)
|
||||
primary_route = forms.ChoiceField(choices=ROUTE_CHOICES, required=False)
|
||||
secondary_label = forms.CharField(max_length=40, required=False)
|
||||
secondary_route = forms.ChoiceField(choices=ROUTE_CHOICES, required=False)
|
||||
|
||||
def __init__(self, *args, key, slug, section=None, **kwargs):
|
||||
initial = kwargs.setdefault("initial", {})
|
||||
if section:
|
||||
for field in ("heading", "body", "intro", "all_link_label", "position", "visible", "image", "image_alt"):
|
||||
initial[field] = getattr(section, field)
|
||||
for side in ("primary", "secondary"):
|
||||
action = getattr(section, f"{side}_action") or {}
|
||||
initial[f"{side}_label"] = action.get("label", "")
|
||||
initial[f"{side}_route"] = action.get("route_id", "")
|
||||
else:
|
||||
initial["visible"] = True
|
||||
if slug == "home":
|
||||
initial["position"] = HOME_KEYS.index(key)
|
||||
elif key == "identity":
|
||||
initial["position"] = 0
|
||||
elif slug == "about":
|
||||
initial["position"] = ABOUT_KEYS.index(key)
|
||||
super().__init__(*args, **kwargs)
|
||||
self.key = key
|
||||
self.slug = slug
|
||||
self.title = key.replace("_", " ").title()
|
||||
if key in {"hero", "identity", "contact"} or slug == "home":
|
||||
self.fields["heading"].required = True
|
||||
if slug == "home" or key in {"identity", "contact"}:
|
||||
self.fields["position"].disabled = True
|
||||
if key in {"hero", "identity", "contact"} or slug == "home":
|
||||
self.fields["visible"].disabled = True
|
||||
allowed = {"heading", "position", "visible"}
|
||||
if key in {"hero", "participation"}:
|
||||
allowed |= {"body", "image", "image_alt", "primary_label", "primary_route", "secondary_label", "secondary_route"}
|
||||
self.fields["body"].max_length = 600 if key == "participation" else 500
|
||||
elif key in {"featured_events", "featured_posts"}:
|
||||
allowed |= {"intro", "all_link_label"}
|
||||
elif key != "contact":
|
||||
allowed.add("body")
|
||||
self.fields["body"].max_length = 600 if key == "identity" else 1200
|
||||
for field in tuple(self.fields):
|
||||
if field not in allowed:
|
||||
del self.fields[field]
|
||||
|
||||
def clean(self):
|
||||
cleaned = super().clean()
|
||||
for side in ("primary", "secondary"):
|
||||
if bool(cleaned.get(f"{side}_label")) != bool(cleaned.get(f"{side}_route")):
|
||||
self.add_error(f"{side}_label", "Action label and route must be filled together.")
|
||||
return cleaned
|
||||
|
||||
def payload(self, entries=(), *, contact_position=None):
|
||||
data = self.cleaned_data
|
||||
position = contact_position if contact_position is not None else data["position"]
|
||||
section = {
|
||||
"key": self.key, "section_type": self.key, "position": position,
|
||||
"visible": True if self.slug == "home" or self.key in {"identity", "contact"} else data["visible"],
|
||||
"heading": data["heading"], "body": data.get("body", ""), "intro": data.get("intro", ""),
|
||||
"all_link_label": data.get("all_link_label", ""), "image": data.get("image"), "image_alt": data.get("image_alt", ""),
|
||||
"entries": list(entries),
|
||||
}
|
||||
for side in ("primary", "secondary"):
|
||||
section[f"{side}_action"] = {"label": data[f"{side}_label"], "route_id": data[f"{side}_route"]} if data.get(f"{side}_label") else None
|
||||
return section
|
||||
Reference in New Issue
Block a user