Question

I may be completely off the reservation here. (Feel free to tell me if I am.)

My use case is that I have a list of schools. The school model is pretty simple:

class School(models.Model):
    name = models.CharField(max_length=100)
    mascot = models.CharField(max_length=100, null=True, blank=True)

When my user wants to edit one of these schools, I don't want them editing the master copy. Instead, I want to give them their own copy which they can play with. When they are done editing their copy, they can submit their change, and someone else will approve it. So I have another class for the user's copy of the school:

class UserSchool(models.Model):
    name = models.CharField(max_length=100)
    mascot = models.CharField(max_length=100, null=True, blank=True)
    master_school = models.ForeignKey(School)
    user = models.ForeignKey(settings.AUTH_USER_MODEL)

So I set up a form to handle the editing of the UserSchool:

class UserSchoolForm(forms.ModelForm):
    class Meta:
        model = UserSchool
        fields = ['name','mascot']

And now I have my EditSchool form:

class EditSchool(UpdateView):
    model = School
    success_url = reverse_lazy('list_schools')
    form_class = UserSchoolForm

    def get(self, request, *args, **kwargs):
        school = self.get_object()

        # make a copy of the school for this user
        user_school, created = UserSchool.objects.get_or_create(
            master_school=school, user=request.user,
            defaults={'name' : school.name, 'mascot' : school.mascot})

        self.object = user_school
        form = UserSchoolForm()
        context = self.get_context_data(form=form)
        return self.render_to_response(context)

I know that get() is making the copy correctly, but when the form displays, there are no values listed in the "name" or "default" fields. My suspicion is that the problem is with the fact that cls.model = School, but self.object is an instance of UserSchool.

Am I close but missing something? Am I completely on the wrong path? Is there a better model for this (like having a single School instance with a special user for "master")?

(And one small complication -- since I'm an old hand at Django, but new a class-based views, I'm trying to use Vanilla Views because I find it easier to figure out what's going on.)

Was it helpful?

Solution

Just to rule out the obvious - you're not passing anything to the form constructor. Have you tried it with instance=user_school? There might be more that needs work but I'd start there.

To expand on this a bit - in your view, you're completely overriding the built in get method. That's fine, but it means that you're bypassing some of the automated behavior of your view superclass. Specifically, the get method of ProcessFormView (one of your ancestor classes) instantiates the form using the get_form method of the view class. FormMixin, another ancestor, defines get_form:

return form_class(**self.get_form_kwargs())

And get_form_kwargs on ModelFormMixin adds self.object to the form's kwargs:

kwargs.update({'instance': self.object})

Because your overridden get method does not call get_form, it also doesn't call get_form_kwargs and therefore doesn't go through the whole path that provides an initial binding for the form.

I personally would try to handle this by modifying the get_object method of your custom view and leaving the rest alone:

class EditSchool(UpdateView):
    model = School
    success_url = reverse_lazy('list_schools')
    form_class = UserSchoolForm

    def get_object(self, queryset=None):
        school = super(EditSchool, self).get_object(queryset=queryset)
        user_school, created = UserSchool.objects.get_or_create(
            master_school=school, user=self.request.user,
            defaults={'name' : school.name, 'mascot' : school.mascot})
        return user_school

There may be more changes needed - I haven't tested this - but both the get and set methods use get_object, and bind it to the form as appropriate.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top