#16418: Class based generic DetailView tries to access non-existant _meta field
when get_object has been modified to return a ModelForm
------------------------------------+-------------------------------
 Reporter:  kd4ttc                  |          Owner:  kd4ttc
     Type:  Bug                     |         Status:  new
Milestone:                          |      Component:  Generic views
  Version:  1.3                     |       Severity:  Normal
 Keywords:  genericviews modelform  |   Triage Stage:  Unreviewed
Has patch:  0                       |  Easy pickings:  0
    UI/UX:  0                       |
------------------------------------+-------------------------------
 = Summary =
  I used DetailView and subclassed it, then modified get_object to return a
 ModelForm instance. When it executes an eeror message is generated due to
 the Meta class in ModelForm clashing with the expected Meta attributes in
 the usual object that is passed.  This was initially posted to
 stackoverflow http://stackoverflow.com/q/6564068/820321 where the
 formatting is pleasant to read. This is my first bug report, so the
 formatting of the bug report may not be that good. Apologies in advance.

 = Report Details =
 I've been impressed how rapidly a functional website can go together with
 generic views in the tutorials. Also, the workflow for form processing is
 nice. I used the ModelForm helper class to create a form from a model I
 made and was delighted to see that so much functionality came together.
 When I used the generic list_detail.object_detail I was disappointed that
 all that I could display were fields individually. I knew the ModelForm
 class contained information for rendering, so I wanted to use the
 ModelForm with a generic view.

 I was asking around on stackoverflow to get some direction, and appreciate
 the answers and comments from several posters. I've figured out how to get
 this to work, but there is a bug in DetailView. The solution includes a
 workaround.

 To use a ModelView with the generic view and get all the fields to render
 automatically the following works:

 Create a project, and in it create application inpatients.

 If you have


 {{{
 # inpatients/models.py

 class Inpatient(models.Model):
     last_name = models.CharField(max_length=30)
     first_name = models.CharField(max_length=30,blank=True)
     address = models.CharField(max_length=50,blank=True)
     city = models.CharField(max_length=60,blank=True)
     state = models.CharField(max_length=30,blank=True)
     DOB = models.DateField(blank=True,null=True)
     notes = models.TextField(blank=True)

 def  __unicode__(self):
         return u'%s, %s %s' % (self.last_name, self.first_name, self.DOB)

 class InpatientForm(ModelForm):
     class Meta:
         model = Inpatient
 }}}

 and

 {{{
 # inpatients/views.py

 from django.http import HttpResponse, HttpResponseRedirect
 from django.shortcuts import render_to_response
 from django.views.generic import DetailView
 from portal.inpatients.models import *

 def formtest(request):
     if request.method == 'POST':
         form = InpatientForm(request.POST)
         if form.is_valid():
             form.save()
             return HttpResponseRedirect('/inpatients')
     else:
         form = InpatientForm()
     return render_to_response("formtest.html", {'form': form})

 class FormDetailView(DetailView):
     model=Inpatient
     context_object_name='inpatient'   # defines the name in the template
     template_name_field='inpatient_list_page.html'

     def get_object(self):
         inpatient=super(FormDetailView,self).get_object()
         form=InpatientForm(instance=inpatient)
         return form

     def get_template_names(self):
         return ['inpatient_list_page.html',]
 }}}
 and

 {{{
 #urls.py

 from django.conf.urls.defaults import patterns, include, url
 from django.views.generic import ListView
 from portal.inpatients.models import Inpatient, InpatientForm
 from portal.inpatients.views import FormDetailView

 urlpatterns = patterns('',
     (r'^formtest/$','portal.inpatients.views.formtest'),
     (r'^inpatients/$', ListView.as_view(
         model=Inpatient, template_name='inpatient_list_page.html')),
     (r'^inpatient-detail/(?P<pk>\d+)/$', FormDetailView.as_view()),
 )

 # with a template containing

 {% block content %}
     <h2>Inpatients</h2>
     <ul>
         {% for aninpatient in object_list %}
             <li><a href='/inpatient-detail/{{ aninpatient.id }}/'>
             {{ aninpatient }}, id={{ aninpatient.id }}</a></li>
         {% endfor %}
     </ul>
     {{ inpatient.as_p }}
 {% endblock %}
 # Yeah, kind of hokey. The template is for both the list view and detail
 view.
 # Note how the form is rendered with one line - {{ inpatient.as_p }}
 }}}

 t works. The instructions for using class based generic views lives at
 https://docs.djangoproject.com/en/1.3/topics/class-based-views/
 Instructions there are pretty clear. The key to making things work is to
 redefine get_object. In the documentation under the section "Performing
 extra work" it nicely describes how to do this, the steps being to call
 the original version of get_object, and then to the extra work. The bit
 that I realized is that the return object can be a ModelForm object. The
 object that get_object returns goes straight into the template in a
 render. By taking the retrieved inpatient object and running it through
 InpatientForm it can be passed to a view as a form which then renders
 itself.

 As to the bug: The bug in DetailView is that the get_template_names
 function tries to make a template name from a structure that does not
 exist. In
 
https://code.djangoproject.com/browser/django/trunk/django/views/generic/detail.py
 on lines 127 to 140 we have within
 SingleObjectTemplateResponseMixin.get_template_names:


 {{{
 127        # The least-specific option is the default
 <app>/<model>_detail.html;
 128         # only use this if the object in question is a model.
 129         if hasattr(self.object, '_meta'):
 130             names.append("%s/%s%s.html" % (
 131                 self.object._meta.app_label,
 132                 self.object._meta.object_name.lower(),
 133                 self.template_name_suffix
 134             ))
 135         elif hasattr(self, 'model') and hasattr(self.model, '_meta'):
 136             names.append("%s/%s%s.html" % (
 137                 self.model._meta.app_label,
 138                 self.model._meta.object_name.lower(),
 139                 self.template_name_suffix
 140             ))
 }}}

 The error is that the code on line 131 is executed and dies with error
 message <'ModelFormOptions' object has no attribute 'app_label'>. I
 conclude that the _meta object is defined. I suppose that the problem is
 that in a ModelForm the class Meta is defined. That Meta probably doesn't
 have the fields set that are expected. The workaround is just to rewrite
 get_template_names and return the correct template.

 I suppose a try statement could just go around where the assignments are
 done. The other issue is to decide what the best way is to define the
 template when a ModelForm is used.

 == Some Feedback and Reply on StackOverflow ==

 I don't think this is a bug, and I do think get_object should always
 return model instance not ModelForm instance. Try using editing CBV. –
 rebus

 I think it is a bug for several reasons. The documentation does not say it
 is invalid. The test for valid data before the assignment tests for the
 existence of _meta rather than the actual fields. The routine that is
 looking for the template didn't find the template. Additionally, on the
 principal of Don't Repeat Yourself, the ModelForm should be able to be
 delivered to a template for rendering. – kd4ttc

 == Another User Had Additional Insight ==

 You are right I believe. This is a bug which stems from the fact that both
 ModelForm and Models have a _meta attribute. This same bug would exhibit
 itself anytime an object is returned from get_object() that contains a
 _meta attribute.

 get_object does not have to return a Model instance. You can confirm this
 by looking at the source for DetailView and reading it's docstring:

 {{{
 class DetailView(SingleObjectTemplateResponseMixin, BaseDetailView):
     """
     Render a "detail" view of an object.

     By default this is a model instance looked up from `self.queryset`,
 but the
     view will support display of *any* object by overriding
 `self.get_object()`.
     """
 }}}

 Notice that the doc string explicitly says that any object is supported by
 overriding self.get_object().

 Another piece of corroborating evidence is from the location where this
 bug itself occurs which is the get_template_names method of
 SingleObjectTemplateResponseMixin.

 {{{
     # The least-specific option is the default <app>/<model>_detail.html;
     # only use this if the object in question is a model.
     if hasattr(self.object, '_meta'):
         names.append("%s/%s%s.html" % (
             self.object._meta.app_label,
             self.object._meta.object_name.lower(),
             self.template_name_suffix
         ))
     elif hasattr(self, 'model') and hasattr(self.model, '_meta'):
         names.append("%s/%s%s.html" % (
             self.model._meta.app_label,
             self.model._meta.object_name.lower(),
             self.template_name_suffix
         ))
 }}}

 Again looking at this code, the comment itself say "If the object in
 question is a model". From this comment we can infer that the object
 doesn't always have to be a model.

 -- Donald Stufft

-- 
Ticket URL: <https://code.djangoproject.com/ticket/16418>
Django <https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.

-- 
You received this message because you are subscribed to the Google Groups 
"Django updates" group.
To post to this group, send email to django-updates@googlegroups.com.
To unsubscribe from this group, send email to 
django-updates+unsubscr...@googlegroups.com.
For more options, visit this group at 
http://groups.google.com/group/django-updates?hl=en.

Reply via email to