Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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.

like image 530
8one6 Avatar asked Jun 19 '13 12:06

8one6


1 Answers

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.

like image 51
Berislav Lopac Avatar answered Nov 12 '22 12:11

Berislav Lopac