Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

multi step form and model inheritance in django

I have seen this approach in many web applications (e.g. when you subscribe for an insurance), but I can't find a good way to implement it in django. I have several classes in my model which inherit from a base class, and so they have several fields in common. In the create-view I want to use that inheritance, so first ask for the common fields and then ask for the specific fields, depending on the choices of the user.

Naive example, suppose I want to fill a database of places

class Place(Model):
    name = models.CharField(max_length=40)
    address = models.CharField(max_length=100)

class Restaurant(Place):
    cuisine = models.CharField(max_length=40)
    website = models.CharField(max_length=40)

class SportField(Place):
    sport = models.CharField(max_length=40)

Now I would like to have a create view when there are the common fields (name and address) and then the possibility to choose the type of place (Restaurant / SportField). Once the kind of place is selected (or the user press a "Continue" button) new fields appear (I guess to make it simple the page need to reload) and the old one are still visible, already filled.

I have seen this approach many times, so I am surprised there is no standard way, or some extensions already helping with that (I have looked at Form Wizard from django-formtools, but not really linked to inheritance), also doing more complicated stuff, as having more depth in inheritance.

like image 868
Ruggero Turra Avatar asked Dec 22 '22 15:12

Ruggero Turra


2 Answers

models.py

class Place(models.Model):
    name = models.CharField(max_length=40)
    address = models.CharField(max_length=100)

    
class Restaurant(Place):
    cuisine = models.CharField(max_length=40)
    website = models.CharField(max_length=40)


class SportField(Place):
    sport = models.CharField(max_length=40)

forms.py

from django.db import models
from django import forms

class CustomForm(forms.Form):
    CHOICES = (('restaurant', 'Restaurant'), ('sport', 'Sport'),)
    name = forms.CharField(widget=forms.TextInput(attrs={'placeholder': 'Name'}))
    address = forms.CharField(widget=forms.TextInput(attrs={'placeholder': 'Address'}))
    type = forms.ChoiceField(
        choices=CHOICES,
        widget=forms.Select(attrs={'onChange':'renderForm();'}))
    cuisine = forms.CharField(required=False, widget=forms.TextInput(attrs={'placeholder': 'Cuisine'}))
    website = forms.CharField(required=False, widget=forms.TextInput(attrs={'placeholder': 'Website'}))
    sport = forms.CharField(required=False, widget=forms.TextInput(attrs={'placeholder': 'Sport'}))

views.py

from django.http.response import HttpResponse
from .models import Restaurant, SportField
from .forms import CustomForm
from django.shortcuts import render
from django.views import View


class CustomView(View):

    def get(self, request,):
        form = CustomForm()
        return render(request, 'home.html', {'form':form})

    def post(self, request,):
        data = request.POST
        name = data['name']
        address = data['address']
        type = data['type']
        if(type == 'restaurant'):
            website = data['website']
            cuisine = data['cuisine']
            Restaurant.objects.create(
                name=name, address=address, website=website, cuisine=cuisine
            )
        else:
            sport = data['sport']
            SportField.objects.create(name=name, address=address, sport=sport)
        return HttpResponse("Success")

templates/home.html

<html>

<head>
    <script type="text/javascript">
        function renderForm() {
            var type =
                document.getElementById("{{form.type.auto_id}}").value;
            if (type == 'restaurant') {
                document.getElementById("{{form.website.auto_id}}").style.display = 'block';
                document.getElementById("{{form.cuisine.auto_id}}").style.display = 'block';
                document.getElementById("{{form.sport.auto_id}}").style.display = 'none';
            } else {
                document.getElementById("{{form.website.auto_id}}").style.display = 'none';
                document.getElementById("{{form.cuisine.auto_id}}").style.display = 'none';
                document.getElementById("{{form.sport.auto_id}}").style.display = 'block';
            }

        }
    </script>
</head>

<body onload="renderForm()">
    <form method="post" action="/">
        {% csrf_token %}
        {{form.name}}<br>
        {{form.address}}<br>
        {{form.type}}<br>
        {{form.website}}
        {{form.cuisine}}
        {{form.sport}}
        <input type="submit">
    </form>
</body>

</html>

Add templates folder in settings.py

TEMPLATES = [
    {
        ...
        'DIRS': [os.path.join(BASE_DIR, 'templates')],
        ...
]
like image 200
Abhijith Konnayil Avatar answered Dec 28 '22 07:12

Abhijith Konnayil


I've created a 2-page working example using modified Class Based Views.

When the form is submitted on the first page, an object of place_type is created. The user is then redirected to the second page where they can update existing details and add additional information.

No separate ModelForms are needed because the CreateView and UpdateView automatically generate the forms from the relevant object's model class.

A single template named place_form.html is required. It should render the {{ form }} tag.

# models.py
from django.db import models
from django.urls import reverse

class Place(models.Model):
    """
    Each tuple in TYPE_CHOICES contains a child class name
    as the first element.

    """
    TYPE_CHOICES = (
        ('Restaurant', 'Restaurant'),
        ('SportField', 'Sport Field'),
    )
    name = models.CharField(max_length=40)
    address = models.CharField(max_length=100)
    place_type = models.CharField(max_length=40, blank=True, choices=TYPE_CHOICES)

    def __str__(self):
        return self.name

    def get_absolute_url(self):
        return reverse('place_update', args=[self.pk])

# Child models go here...
# urls.py
from django.urls import path
from . import views

urlpatterns = [
    path('create/', views.PlaceCreateView.as_view(), name='place_create'),
    path('<pk>/', views.PlaceUpdateView.as_view(), name='place_update'),
]
# views.py
from django.http import HttpResponseRedirect
from django.forms.models import construct_instance, modelform_factory
from django.views.generic.edit import CreateView, UpdateView
from django.urls import reverse_lazy

from . import models

class PlaceCreateView(CreateView):
    model = models.Place
    fields = '__all__'

    def form_valid(self, form):
        """
        If a `place_type` is selected, it is used to create an 
        instance of that Model and return the url.

        """
        place_type = form.cleaned_data['place_type']
        if place_type:
            klass = getattr(models, place_type)
            instance = klass()
            obj = construct_instance(form, instance)
            obj.save()
            return HttpResponseRedirect(obj.get_absolute_url())
        return super().form_valid(form)

class PlaceUpdateView(UpdateView):
    fields = '__all__'
    success_url = reverse_lazy('place_create')
    template_name = 'place_form.html'

    def get_object(self, queryset=None):
        """
        If the place has a `place_type`, get that object instead.

        """
        pk = self.kwargs.get(self.pk_url_kwarg)
        if pk is not None:
            obj = models.Place.objects.get(pk=pk)
            if obj.place_type:
                klass = getattr(models, obj.place_type)
                obj = klass.objects.get(pk=pk)
        else:
            raise AttributeError(
                "PlaceUpdateView must be called with an object pk in the URLconf."
            )
        return obj

    def get_form_class(self):
        """
        Remove the `place_type` field.

        """
        model = self.object.__class__
        return modelform_factory(model, exclude=['place_type',])
like image 22
MattRowbum Avatar answered Dec 28 '22 06:12

MattRowbum