#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.