Class Based Views

While django-polymorphic provides full admin integration, you might want to build front-end views that allow users to create polymorphic objects. Since a single URL cannot easily handle different form fields for different models, the best approach is a two-step process:

  1. Step 1: Let the user choose the desired type.

  2. Step 2: Display the form for that specific type.

Tip

The code for this example can be found here.

This example uses model labels (e.g., app.ModelName) to identify the selected type. Assume we have the following models:

 1from django.db import models
 2from polymorphic.models import PolymorphicModel
 3
 4
 5class Project(PolymorphicModel):
 6    topic = models.CharField(max_length=30)
 7
 8
 9class ArtProject(Project):
10    artist = models.CharField(max_length=30)
11
12
13class ResearchProject(Project):
14    supervisor = models.CharField(max_length=30)

Step 1: Selecting the Type

Create a form that allows users select the desired model type. You can use a simple choice field for this.

 1from django.apps import apps
 2from django.shortcuts import redirect
 3from django.urls import reverse
 4from django.views.generic import FormView, CreateView
 5from .models import Project, ArtProject, ResearchProject
 6
 7from django import forms
 8from django.utils.translation import gettext_lazy as _
 9
10
11class ProjectTypeChoiceForm(forms.Form):
12    model_type = forms.ChoiceField(
13        label=_("Project Type"),
14        widget=forms.RadioSelect(attrs={"class": "radiolist"}),
15    )
16
17
18class ProjectTypeSelectView(FormView):
19    form_class = ProjectTypeChoiceForm
20    template_name = "project_type_select.html"
21
22    def get_form_kwargs(self):
23        kwargs = super().get_form_kwargs()
24        # Build choices using model labels: [(model_label, verbose_name), ...]
25        choices = [
26            (model._meta.label, model._meta.verbose_name)
27            for model in [ArtProject, ResearchProject]
28        ]
29        kwargs["initial"] = {"model_type": choices[0][0] if choices else None}
30        return kwargs
31
32    def get_form(self, form_class=None):
33        form = super().get_form(form_class)
34        # Populate the choices for the form using model labels
35        choices = [
36            (model._meta.label, model._meta.verbose_name)
37            for model in [ArtProject, ResearchProject]
38        ]
39        form.fields["model_type"].choices = choices
40        return form
41
42    def form_valid(self, form):
43        model_label = form.cleaned_data["model_type"]
44        return redirect(f"{reverse('project-create')}?model={model_label}")
45

Your template project_type_select.html, might look like this:

<form method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">Next</button>
</form>

Step 2: Displaying the Form

The creation view needs to dynamically select the correct form class based on the chosen model label.

 1class ProjectCreateView(CreateView):
 2    model = Project
 3    template_name = "project_form.html"
 4
 5    def get_success_url(self):
 6        return reverse("project-select")
 7
 8    def get_form_class(self):
 9        # Get the requested model label from query parameter
10        model_label = self.request.GET.get("model")
11        if not model_label:
12            # Fallback or redirect to selection view
13            return super().get_form_class()
14
15        # Get the model class using the app registry
16        model_class = apps.get_model(model_label)
17
18        # Create a form for this model
19        # You can also use a factory or a dict mapping if you have custom forms
20        class SpecificForm(forms.ModelForm):
21            class Meta:
22                model = model_class
23                fields = "__all__"  # Or specify fields
24
25        return SpecificForm
26
27    def get_context_data(self, **kwargs):
28        context = super().get_context_data(**kwargs)
29        # Pass the model label to the template so it can be preserved
30        # in the form action
31        context["model_label"] = self.request.GET.get("model")
32        return context

In your template project_form.html, make sure to preserve the model parameter:

<form method="post" action=".?model={{ model_label }}">
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">Save</button>
</form>

And our urls might look like this:

1from django.urls import path
2from .views import ProjectTypeSelectView, ProjectCreateView
3
4urlpatterns = [
5    path("select/", ProjectTypeSelectView.as_view(), name="project-select"),
6    path("create/", ProjectCreateView.as_view(), name="project-create"),
7]

Using extra_views

If you are using django-extra-views, django-polymorphic provides mixins to help with formsets. See polymorphic.contrib.extra_views for more details.