Question

I have a Django app where users submit orders for payment. Clearly, security is important. I want to minimise the amount of code that I have to write, to avoid introducing any security holes, and ease maintenance.

The model is simple:

class Order(models.Model):
    user = models.ForeignKey(User)
    created = models.DateTimeField()
    paid = models.DateTimeField(null=True, blank=True)
    items = models.ManyToManyField(Item)

I'm using a CreateView to create instances of Order:

class OrderView(CreateView):
    model = Order
    form_class = OrderForm

I want to enforce values for certain fields in those instances. For example, I want the instance user field set to the current logged-in user. I don't want any possibility that the user can change the value of this field, so I don't want it to appear in the form at all. Therefore I use a custom ModelForm to remove these fields from the form:

class OrderForm(forms.ModelForm):
    class Meta:
        model = Order
        # For security, we control exactly which fields are placed
        # in the form, rather than excluding some:
        fields = ('items',)

Now I want the newly created Order instances to have the user field set to the current logged-in user. I can't find any documentation about what is the best way to do this.

(A) I can override the form's save() method to modify the object before saving, but it feels like this code doesn't belong in the form, which doesn't know anything about the user field. I also don't have access to the request here, which I'd need to determine the current user. But it might look like this:

class OrderForm(forms.ModelForm):
    def save(self, commit=True):
        instance = super(OrderForm, self).save(commit=False)
        instance.user = get_request_magic().user
        if commit:
            instance.save()
        return instance

(B) I can override the view's form_valid method to save the object with commit=False, like a class-based version of this question. But I can't call the superclass method directly, because it saves the object with no way to disable commit, so I have to manually skip a generation of form_valid which is nasty. Apart from that complaint, this does look like the best way I've found so far:

class OrderView(CreateView):
    def form_valid(self, form):
        self.object = form.save(commit=False)
        self.object.user = self.request.user
        self.object.save()
        return super(ModelFormMixin, self).form_valid(form)

(C) I could write a replacement for CreateView that adds a hook to allow objects to be changed before saving them. But that feels like more boilerplate and duplication.

(D) I can't provide an initial value, because there's no form field to put it in, so it will be ignored.

Any other ideas? If (B) the best option, is there any way around the hacky way of manually specifying which superclass' form_valid method I want to call?

Was it helpful?

Solution

Django user Charettes answered the question for me:

You can achieve this by overriding form_valid:

class OrderCreateViewMixin(CreateView):
    def form_valid(self, form):
        form.instance.user = request.user
        return super(OrderCreateViewMixin, self).form_valid(form)

Which pointed me towards the right part of the documentation:

class AuthorCreate(CreateView):
    form_class = AuthorForm
    model = Author

    def form_valid(self, form):
        form.instance.created_by = self.request.user
        return super(AuthorCreate, self).form_valid(form)

This is definitely the simplest and cleanest answer I've found so far. It doesn't require modifying the form in any way, although it does directly access its instance member which is a bit ugly. However, at least it's officially documented, so it's unlikely to break.

OTHER TIPS

There are probably multiple approaches to this. I would do this:

Create a constructor in your form which takes the request:

def __init__(self, *args, **kwargs):
        request = kwargs.pop('request', None)
        super(OrderForm, self).__init__(*args, **kwargs)
        self.request = request

When creating your form for POST processing, instantiate it as follows:

form = OrderForm(data=request.POST, request=request)

Now, in your save() method, you have access to the user on the request by referencing self.request.user and can set it accordingly on your model.

The way I've gone about handling this situation with CBVs, is to pass in an unsaved instance of the model to the form. This is how I've done it:

class OrderView(CreateView):
    def get_form_kwargs(self):
        self.object = Order(user=self.request.user)
        return super(OrderView, self).get_form_kwargs()

Both CreateView and UpdateView will add instance to the form kwargs, setting it to the value of self.object.

The only other way, besides what you've already mentioned, is to construct your view class from the same elements that CreateView does, and then change the get and post methods to populate self.object there. I've done that when I have needed a lot of create views in my project:

class OrderView(SingleObjectTemplateResponseMixin, ModelFormMixin, ProcessFormView):
    template_name_suffix = '_form'

    def get(self, request, *args, **kwargs):
        self.object = Order(user=request.user)
        return super(OrderView, self).get(request, *args, **kwargs)

    def post(self, request, *args, **kwargs):
        self.object = Order(user=request.user)
        return super(OrderView, self).post(request, *args, **kwargs)

Here is a more generalized version to be reused: https://gist.github.com/4439975

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