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