Django: CreateView with pre-populated and uneditable fields specified by query string
Asked Answered
B

1

10

Let's say we have an app called Closet and it has some models:

# closet.models.py
class  Outfit(models.Model):
    shirt   = models.ForeignKey(Shirt)
    pants   = models.ForeignKey(Trouser)

class Shirt(models.Model):
    desc    = models.TextField()

class Trouser(models.Model):
    desc    = models.TextField()

class Footwear(models.Model):
    desc    = models.TextField

Using generic detail view, it's easy to make the URL conf for details on each of those:

#urls.py
urlpatterns = patterns('',
    url(r'^closet/outfit/(?P<pk>\d+)$',     DetailView(model=Outfit),       name='outfit_detail'),
    url(r'^closet/shirt/(?P<pk>\d+)$',          DetailView(model=Shirt),        name='shirt_detail'),
    url(r'^closet/trouser/(?P<pk>\d+)$',        DetailView(model=Trouser),      name='trouser_detail'),
    url(r'^closet/footwear/(?P<pk>\d+)$',       DetailView(model=Footwear),     name='footwear_detail'),
)   

What I'd like to do next is define the views that will create a new object of each type. I would like to do this with an extended version of CreateView which will be able to handle data on pre-populated fields.

Specifically, I want the following behavior:

  1. If I visit /closet/outfit/new I want to get a standard ModelForm for the Outfit model with everything blank and everything editable.
  2. If I visit /closet/outfit/new/?shirt=1 I want to see all the fields I saw in case 1) but I want the shirt field to be pre-populated with the shirt with pk=1. Additionally, I want the shirt field to be displayed as un-editable. If the form is submitted and is deemed to be invalid, when the form is redisplayed I want the shirt field to continue to be un-editable.
  3. If I visit /closet/outfit/new/?shirt=1&trouser=2 I want to see all the fields I saw in case 1) but now both the shirt and trouser fields should be preopoulated and uneditable. (I.e. only the footwear field should be editable.)

In general, is this possible? I.e. can the querystring modify the structure of the displayed form in this way? I want to accomplish this in the DRYest way possible. My gut tells me this should be doable with class based views and perhaps would involve model_form_factory but I can't get the logic straight in my mind. In particular, I wasn't sure whether it was possible to have the class-based-view access the request.REQUEST (i.e. the request.POST or request.GET parameters) at the time that the ModelForm is being constructed.

Perhaps its possible only if I use different querystring keywords for the locked fields. I.e. perhaps the URL's need to be: /closet/outfit/new/?lock_shirt=1 and /closet/outfit/new?lock_shirt=1&lock_trouser=2. Perhaps if its done that way the POST handler would be handed both a list of locked fields (for the purposes of form display in the browser) along with a regular list of all the model fields for the purpose of actually creating the object.

Why do I want this: In the template for the footwear_detail I would want to be able to make a tag like

<a href="{% url outfit_new %}?footwear={{object.pk}}>Click to create a new outfit with this footwear!</a>

In general, it would be really useful to be able to make links to forms whose "structure" (not just values) changes depending on the querystring passed.


Responding to the great suggestion from Berislav Lopac:

So I went ahead and did:

class CreateViewWithPredefined(CreateView):
    def get_initial(self):
    return self.request.GET

This gets me 90% of what I need. But let me flesh out the situation a bit more. Say I add two fields to the Outfit model: headgear = models.ManyToManyField('headgear') and awesomeness_rating = models.FloatField().

Two problems:

  1. If I visit /closet/outfit/new/?awesomeness_rating=10 then my form pre-fills with [u'10'] instead of just filling with 10. Is there a filter I should use in my template or a bit of processing I can add to my view to make the formatting more appropriate?
  2. If I want to pre-specify a few pieces of headwear, what is the right format to pass what feels like a python list in through a query string? I.e. should I do /closet/outfit/new/?headgear=1,2,3? If so, will Django correctly figure out that I'd like to pre-select the 3 pieces of headgear with those ID's?

Continuing to work on this...

class CreateViewWithPredefined(CreateView):
    def get_initial(self):
        initial = super(CreateView, self).get_initial()
        for k, v in self.request.GET.iterlists():
            if len(v) > 1:
                initial.update({ k : v })
            else:
                initial.update({ k : v[0] })
        return initial

This seems to kill 2 birds with one stone: numerical data gets coerced from unicode to numerical and it flattens lists when possible (as intended). Need to check if this works on multi-valued fields.

Beichner answered 19/6, 2013 at 12:56 Comment(0)
H
4

It's self.request, anywhere in a CBV. :-)

OK, let me make this answer more comprehensive. Basically, what you want is the get_initial method, which is contributed by the FormMixin. Override it to populate the initial values for your fields.

Hooded answered 19/6, 2013 at 16:1 Comment(5)
This is a great first step. I have two questions about how to implement this. Please see edits to the original post above. I think for my purposes, I will wind up going this route since it gets the gist. But to the extent that others might care about the original problem: does anyone have a solution to the original question...instead of just pre-filling the form with initial data, is there a way to have the pre-filled fields actually display as locked?Beichner
One way is to add the readonly attribute to the input element, maybe in the get_form method of the view. You could take the request.GET fields (or even better use your get_initial in order to avoid duplication of code) and modify the widget of each field that has a value there.Hooded
Thanks. I'm quite new to this and am still having issues with the unicode formatting. I understand that all entries in a QueryDict that I get from request.GET will be formatted as Unicode strings. Is there a general way to convert a unicode to its native python datatype? So if I do /closet/outfit/new/?awesomeness_level=10 I want to make sure that the field prepopulates with 10 instead of [u'10]...Beichner
Why would you want that? HTML fields always contain text values and are unaware of their values' types.Hooded
Everything is easy if each variable has only 1 value. But if you have a querystring of ?var=a,b that's ambiguous since maybe you want a,b to be the only text value of var, but maybe you want a and b to be two separate values. You can avoid the ambiguity by doing ?var=a&var=b. Multiple selects will give strings like the latter. So I guess I was hoping there would be a nice pair of functions: one to take dictionaries of variables and turn them into querystrings (unambiguously) and the other to take get/post parameters and turn them into dictionaries.Beichner

© 2022 - 2024 — McMap. All rights reserved.