开发者

Multiple form classes in django generic (class) views

I'd like to use the class based generic views of django 1.3 for forms, but sometimes have to manage multiple form classes in one form. However, it looks like the existing views based on FormMixin assume a single form class.

Is this possible with generic views and how would I do it?

开发者_如何学运维

EDIT: to clarify, I have one form but more than one (ModelForm based) class. For example in the inline_formset example in the django docs, I would want to present a page where an author and his books can be edited at once, in a single form:

author_form = AuthorForm(request.POST, instance = author)
books_formset = BookInlineFormSet(request.POST, request.FILES, instance=author)


Facing similar problem, I've come to conclusion that it's not possible.

Though having multiple forms per page itself turned out to be a design mistake, presenting all sorts of troubles. E.g., user fills two forms, clicks submit on one of them and loses data from the other. Workaround requires complicated controller that needs to be aware of the state of all forms on the page. (See also here for some discussion on related problem.)

If having multiple forms per page isn't your exact requirement, I'd suggest to look at alternative solutions.

For example, it's usually possible to show user only one editable form at a time.

In my case, I switched to django-formwizard (not a django.contrib one, which is a bit old and seems to be currently under a redesign, but this one Update: Beginning with release 1.4 of Django, django-formwizard app will be available in django.contrib, replacing old formwizard. It's already in trunk, see docs). For the user I made it to look like there are actually multiple forms on the page, but only one is editable. And user had to fill forms in predetermined order. This made dealing with multiple forms much easier.

Otherwise, if forms really need to be presented all at once, it may make sense to combine them into one.


UPDATE (after your clarification):

No, you can't deal with formsets using generic FormView either. Though your example appears to be quite simple to implement: I think it's very similar to this example in Django docs on formsets. It deals with two formsets, and you just need to replace one with the form (I think you still need to specify prefix to avoid possible clashes of elements' id attributes).

In short, in your case I'd subclass django.views.generic.base.View and override get() and post() methods to deal with form and formset similar to above example from Django docs.

In this case, I think it's fine to present both form and formset editable—with a single button to submit them both.

ANOTHER UPDATE:

There's an active recent ticket in Django trac, #16256 More class based views: formsets derived generic views. If all goes well, new generic views will be added to Django: FormSetsView, ModelFormSetsView and InlineFormSetsView. Particularly, the last one ‘provides a way to show and handle a model with it's inline formsets’.


Present fields from two models on a single view page

You have to extend django.views.generic.View class and override get(request) and post(request) methods.

This is how I did that.

I'm using Django 1.11.

This is how my form (consisted of two forms) looks like:

Multiple form classes in django generic (class) views

My View class which renders my two forms:

from django.views.generic import View

class UserRegistrationView(View):
    # Here I say which classes i'm gonna use
    # (It's not mandatory, it's just that I find it easier)
    user_form_class = UserForm
    profile_form_class = ProfileForm
    template_name = 'user/register.html'

    def get(self, request):
        if request.user.is_authenticated():
            return render(request, 'user/already_logged_in.html')
        # Here I make instances of my form classes and pass them None
        # which tells them that there is no additional data to display (errors, for example)
        user_form = self.user_form_class(None)
        profile_form = self.profile_form_class(None)
        # and then just pass them to my template
        return render(request, self.template_name, {'user_form': user_form, 'profile_form': profile_form})

    def post(self, request):
        # Here I also make instances of my form classes but this time I fill
        # them up with data from POST request
        user_form = self.user_form_class(request.POST)
        profile_form = self.profile_form_class(request.POST)

        if user_form.is_valid() and profile_form.is_valid():
            user = user_form.save(commit=False)
            user_profile = profile_form.save(commit=False)

            # form.cleaned_data is a dictionary which contains data validated
            # by fields constraints (Say we have a field which is a number. The cleaning here would 
            # be to just convert a string which came from the browser to an integer.)
            username = user_form.cleaned_data['username']
            password = user_form.cleaned_data['password']

            # This will be clarified later 
            # You can save each object individually if they're not connected, as mines are (see class UserProfile below)
            user.set_password(password)
            user.userprofile = user_profile
            user.save()

            user = authenticate(username=username, password=password)

            if user is not None:
                if user.is_active:
                    login(request, user)
                return redirect('user:private_profile')

        # else: # form not valid - each form will contain errors in form.errors
        return render(request, self.template_name, {
            'user_form': user_form,
            'profile_form': profile_form
        })

I have a User and UserProfile models. User is django.contrib.auth.models.User and UserProfile is as follows:

class UserProfile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    friends = models.ManyToManyField('self', null=True, blank=True)
    address = models.CharField(max_length=100, default='Some address 42')

    def get_absolute_url(self):
        return reverse('user:public_profile', kwargs={'pk': self.pk})

    def __str__(self):
        return 'username: ' + self.user.username + '; address: ' + self.address

    @receiver(post_save, sender=User) # see Clarification 1 below
    def create_user_profile(sender, instance, created, **kwargs):
        if created: # See Clarification 2 below
            UserProfile.objects.create(user=instance, address=instance.userprofile.address)

    @receiver(post_save, sender=User)
    def update_user_profile(sender, instance, **kwargs):
        instance.userprofile.save()

Clarification 1: @receiver(post_save, sender=User)

  • when User is saved (I wrote user.save() somewhere (user is an instance of class User) ) UserProfile will also be saved.

Clarification 2: if created: (clarification from the View class)

  • If User is being CREATED, make a UserProfile where user = User instance which was just now submitted trough a UserForm

  • address is gathered from ProfileForm and added to the user instance before calling user.save()

I have two forms:

UserForm:

class UserForm(forms.ModelForm):
    password = forms.CharField(widget=forms.PasswordInput(render_value=True), required=True)
    password_confirmation = forms.CharField(widget=forms.PasswordInput(render_value=True), required=True)

    first_name = forms.CharField(required=True)
    last_name = forms.CharField(required=True)

    class Meta:
        model = User
        fields = ('email', 'username', 'first_name', 'last_name', 'password', 'password_confirmation')

    def clean(self):
        cleaned_data = super(UserForm, self).clean()
        password = cleaned_data.get("password")
        password_confirmation = cleaned_data.get("password_confirmation")

        if password != password_confirmation:
            self.fields['password'].widget = forms.PasswordInput()
            self.fields['password_confirmation'].widget = forms.PasswordInput()

            self.add_error('password', "Must match with Password confirmation")
            self.add_error('password_confirmation', "Must match with Password")
            raise forms.ValidationError(
                "Password and Password confirmation do not match"
            )

ProfileForm:

class ProfileForm(forms.ModelForm):
    class Meta:
        model = UserProfile
        fields = ('address',)

I hope that I understood your question well and that this will help you (and others to come). :)


One of the principles of django is that you can build up one big form out of several smaller forms, with only one submit-button. That's why the <form>-tags aren't generated by django itself.

The problem with generic views, class based or not, and multiple such forms in the background, is of course that the sky's the limit. The forms may be related somehow: a "mother"-form and and optional extra data that depends on the data in the mother (onetoone, say). Then there's the model that is connected to several other models through foreign keys and/or intermediary tables where you'd use form+formsets. Then there's the all-formset kind of page, like in the admin when you make some fields editable directly in the list view. Each of these are different types of multi form views, and I don't think it would be fruitful to make one generic view that would cover all cases.

If you have a "mother" model though, you can use a standard UpdateView or CreateView and add methods for the extra forms that are called from get() and post(), after the code that deals with the mother model. In form_valid() for instance, if the mother-form is valid you can process the other forms. You'll have the pk of the mother that you then use to connect up the data in the other forms.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜