Code

Opened 6 years ago

Closed 6 years ago

Last modified 3 years ago

#8719 closed (fixed)

KeyError on save of model with inline-edited ForeignKey with non-standard primary key

Reported by: jonloyens Owned by: nobody
Component: contrib.admin Version: master
Severity: Keywords:
Cc: Triage Stage: Unreviewed
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: UI/UX:

Description

When editing a ForeignKey relationship inline in admin, if the model representing the foreign key has a non-standard primary key, a KeyError will be thrown when editing an existing model.

Consider the following models and admin classes:

class ModelA( models.Model )
    some_data = models.CharField(max_length=255, blank=True)

class ModelB( models.Model ):
    mypk_id = models.AutoField(primary_key=True)
    modela = models.ForeignKey(ModelA)
    more_data = models.CharField(max_length=255, blank=True)

class ModelBAdmin( admin.StackedInline ):
    model = ModelB

class ModelAAdmin( admin.ModelAdmin ):
     inlines = [ModelBAdmin]

When attempting to save an existing ModelA in admin with an existing ModelB. Admin will throw a KeyError due to there not being a 'mypk_id' in the cleaned_data of the FormSet here:

Environment:

Request Method: POST
Request URL: http://localhost:8000/admin/auth/user/12922/
Django Version: 1.0-beta_2-SVN-8706
Python Version: 2.5.1
Installed Applications:
['django.contrib.auth',
 'django.contrib.contenttypes',
 'django.contrib.sessions',
 'django.contrib.sites',
 'mc_billing_site.billing',
 'django.contrib.admin']
Installed Middleware:
('django.middleware.common.CommonMiddleware',
 'django.contrib.sessions.middleware.SessionMiddleware',
 'django.contrib.auth.middleware.AuthenticationMiddleware',
 'django.middleware.doc.XViewMiddleware')


Traceback:
File "/Library/Python/2.5/site-packages/django/core/handlers/base.py" in get_response
  86.                 response = callback(request, *callback_args, **callback_kwargs)
File "/Library/Python/2.5/site-packages/django/contrib/admin/sites.py" in root
  173.                 return self.model_page(request, *url.split('/', 2))
File "/Library/Python/2.5/site-packages/django/views/decorators/cache.py" in _wrapped_view_func
  44.         response = view_func(request, *args, **kwargs)
File "/Library/Python/2.5/site-packages/django/contrib/admin/sites.py" in model_page
  192.         return admin_obj(request, rest_of_url)
File "/Library/Python/2.5/site-packages/django/contrib/auth/admin.py" in __call__
  42.         return super(UserAdmin, self).__call__(request, url)
File "/Library/Python/2.5/site-packages/django/contrib/admin/options.py" in __call__
  196.             return self.change_view(request, unquote(url))
File "/Library/Python/2.5/site-packages/django/db/transaction.py" in _commit_on_success
  238.                 res = func(*args, **kw)
File "/Library/Python/2.5/site-packages/django/contrib/admin/options.py" in change_view
  588.                     self.save_formset(request, form, formset, change=True)
File "/Library/Python/2.5/site-packages/django/contrib/admin/options.py" in save_formset
  378.         formset.save()
File "/Library/Python/2.5/site-packages/django/forms/models.py" in save
  280.         return self.save_existing_objects(commit) + self.save_new_objects(commit)
File "/Library/Python/2.5/site-packages/django/forms/models.py" in save_existing_objects
  294.             obj = existing_objects[form.cleaned_data[self.model._meta.pk.attname]]

Exception Type: KeyError at /admin/testapp/modela/12922/
Exception Value: 'mypk_id'

I am absolutely conviced this is related to this other ticket: 8695 but want to make sure this gets tested as well.

Attachments (2)

primary_key_formset_fixes.diff (10.5 KB) - added by brosner 6 years ago.
rough patch.
primary_key_formset_fixes.2.diff (12.3 KB) - added by brosner 6 years ago.
improved

Download all attachments as: .zip

Change History (10)

comment:1 Changed 6 years ago by kmtracey

  • Needs documentation unset
  • Needs tests unset
  • Patch needs improvement unset

Interesting. This case I know did once work, because all my models have non-standard naming and I was once able to save models with inline-edited objects. I've isolated when this failure was introduced to [8528]. Specifically I'm sure it's the change from if self.model._meta.has_auto_field: to if self.model._meta.pk.auto_created: in add_fields. For the old way non-standard-named PK fields were getting added, now they are not.

It is quite similar to #8695 in that the problem is the PK value is not added to the form data. However, the #8695 case was not working before [8528] either. In that case the PK field is not an auto field at all.

It seems add_fields needs to consider more cases where the PK field needs to be hidden in the form.

[Oddly enough the #8694 was also introduced by [8528], however it is a different part of [8528] that causes the problem in #8694.)

comment:2 Changed 6 years ago by kmtracey

  • milestone set to 1.0

Changed 6 years ago by brosner

rough patch.

comment:3 Changed 6 years ago by brosner

I've uploaded a patch that should fix this whole mess, but attached here to see if it fixes this case too. I need to clean it up and get things in the right place. Let me know if it works.

comment:4 Changed 6 years ago by kmtracey

Hi Brian, tried it for this case and it's got a problem. I can't even get to the change page for a model with explicitly specified PK fields. I get an AttributeError 'CluesFormFormSet' object has no attribute '_pk_field', traceback is:

Environment:

Request Method: GET
Request URL: http://lbox:8000/admin/crossword/entries/125408/
Django Version: 1.0-beta_2-SVN-8752
Python Version: 2.5.1
Installed Applications:
['django.contrib.auth',
 'django.contrib.contenttypes',
 'django.contrib.sessions',
 'django.contrib.admin',
 'django.contrib.admindocs',
 'django.contrib.sites',
 'django.contrib.humanize',
 'xword.crossword']
Installed Middleware:
('django.middleware.common.CommonMiddleware',
 'django.contrib.sessions.middleware.SessionMiddleware',
 'django.contrib.auth.middleware.AuthenticationMiddleware',
 'django.middleware.doc.XViewMiddleware')


Traceback:
File "/home/kmt/tmp/django/trunk/django/core/handlers/base.py" in get_response
  86.                 response = callback(request, *callback_args, **callback_kwargs)
File "/home/kmt/tmp/django/trunk/django/contrib/admin/sites.py" in root
  173.                 return self.model_page(request, *url.split('/', 2))
File "/home/kmt/tmp/django/trunk/django/views/decorators/cache.py" in _wrapped_view_func
  44.         response = view_func(request, *args, **kwargs)
File "/home/kmt/tmp/django/trunk/django/contrib/admin/sites.py" in model_page
  192.         return admin_obj(request, rest_of_url)
File "/home/kmt/tmp/django/trunk/django/contrib/admin/options.py" in __call__
  196.             return self.change_view(request, unquote(url))
File "/home/kmt/tmp/django/trunk/django/db/transaction.py" in _commit_on_success
  238.                 res = func(*args, **kw)
File "/home/kmt/tmp/django/trunk/django/contrib/admin/options.py" in change_view
  596.                 formset = FormSet(instance=obj)
File "/home/kmt/tmp/django/trunk/django/forms/models.py" in __init__
  362.         super(BaseInlineFormSet, self).__init__(data, files, prefix=prefix or self.rel_name)
File "/home/kmt/tmp/django/trunk/django/forms/models.py" in __init__
  256.         super(BaseModelFormSet, self).__init__(**defaults)
File "/home/kmt/tmp/django/trunk/django/forms/formsets.py" in __init__
  67.         self._construct_forms()
File "/home/kmt/tmp/django/trunk/django/forms/models.py" in _construct_forms
  368.         super(BaseInlineFormSet, self)._construct_forms()
File "/home/kmt/tmp/django/trunk/django/forms/formsets.py" in _construct_forms
  76.             self.forms.append(self._construct_form(i))
File "/home/kmt/tmp/django/trunk/django/forms/formsets.py" in _construct_form
  96.         self.add_fields(form, i)
File "/home/kmt/tmp/django/trunk/django/forms/models.py" in add_fields
  389.         if isinstance(self._pk_field, OneToOneField):

Exception Type: AttributeError at /admin/crossword/entries/125408/
Exception Value: 'CluesFormFormSet' object has no attribute '_pk_field'

My Clues model has primary key defined like so:

class Clues(models.Model):
    ID = models.AutoField(primary_key=True)
    etc...

Verified the patch does fix #8694 and #8695 though.

Changed 6 years ago by brosner

improved

comment:5 Changed 6 years ago by brosner

Ah, ok, thanks Karen. The patch is improved and I added a test case for this ticket. Can you double check it?

comment:6 Changed 6 years ago by kmtracey

Change page still won't display, it's back to KeyError, but during rendering of the form vs. attempt to save it:

Environment:

Request Method: GET
Request URL: http://lbox:8000/admin/crossword/entries/33616/
Django Version: 1.0-beta_2-SVN-8752
Python Version: 2.5.1
Installed Applications:
['django.contrib.auth',
 'django.contrib.contenttypes',
 'django.contrib.sessions',
 'django.contrib.admin',
 'django.contrib.admindocs',
 'django.contrib.sites',
 'django.contrib.humanize',
 'xword.crossword']
Installed Middleware:
('django.middleware.common.CommonMiddleware',
 'django.contrib.sessions.middleware.SessionMiddleware',
 'django.contrib.auth.middleware.AuthenticationMiddleware',
 'django.middleware.doc.XViewMiddleware')


Template error:
In template /home/kmt/tmp/django/trunk/django/contrib/admin/templates/admin/edit_inline/tabular.html, error at line 18
   Caught an exception while rendering: "Key 'ID' not found in Form"
   8 :    <table>
   9 :      <thead><tr>
   10 :      {% for field in inline_admin_formset.fields %}
   11 :        {% if not field.is_hidden %}
   12 :          <th {% if forloop.first %}colspan="2"{% endif %}>{{ field.label|capfirst|escape }}</th>
   13 :         {% endif %}
   14 :      {% endfor %}
   15 :      {% if inline_admin_formset.formset.can_delete %}<th>{% trans "Delete?" %}</th>{% endif %}
   16 :      </tr></thead>
   17 :    
   18 :       {% for inline_admin_form in inline_admin_formset %} 
   19 :                    
   20 :         <tr class="{% cycle row1,row2 %} {% if inline_admin_form.original or inline_admin_form.show_url %}has_original{% endif %}">
   21 : 
   22 :         <td class="original">
   23 :           {% if inline_admin_form.original or inline_admin_form.show_url %}<p>
   24 :           {% if inline_admin_form.original %} {{ inline_admin_form.original }}{% endif %}
   25 :           {% if inline_admin_form.show_url %}<a href="../../../r/{{ inline_admin_form.original.content_type_id }}/{{ inline_admin_form.original.id }}/">{% trans "View on site" %}</a>{% endif %}
   26 :             </p>{% endif %}
   27 :           {{ inline_admin_form.pk_field.field }}
   28 :           {% spaceless %}

Traceback:
File "/home/kmt/tmp/django/trunk/django/core/handlers/base.py" in get_response
  86.                 response = callback(request, *callback_args, **callback_kwargs)
File "/home/kmt/tmp/django/trunk/django/contrib/admin/sites.py" in root
  173.                 return self.model_page(request, *url.split('/', 2))
File "/home/kmt/tmp/django/trunk/django/views/decorators/cache.py" in _wrapped_view_func
  44.         response = view_func(request, *args, **kwargs)
File "/home/kmt/tmp/django/trunk/django/contrib/admin/sites.py" in model_page
  192.         return admin_obj(request, rest_of_url)
File "/home/kmt/tmp/django/trunk/django/contrib/admin/options.py" in __call__
  196.             return self.change_view(request, unquote(url))
File "/home/kmt/tmp/django/trunk/django/db/transaction.py" in _commit_on_success
  238.                 res = func(*args, **kw)
File "/home/kmt/tmp/django/trunk/django/contrib/admin/options.py" in change_view
  622.         return self.render_change_form(request, context, change=True, obj=obj)
File "/home/kmt/tmp/django/trunk/django/contrib/admin/options.py" in render_change_form
  404.         ], context, context_instance=template.RequestContext(request))
File "/home/kmt/tmp/django/trunk/django/shortcuts/__init__.py" in render_to_response
  18.     return HttpResponse(loader.render_to_string(*args, **kwargs), **httpresponse_kwargs)
File "/home/kmt/tmp/django/trunk/django/template/loader.py" in render_to_string
  107.     return t.render(context_instance)
File "/home/kmt/tmp/django/trunk/django/template/__init__.py" in render
  176.         return self.nodelist.render(context)
File "/home/kmt/tmp/django/trunk/django/template/__init__.py" in render
  756.                 bits.append(self.render_node(node, context))
File "/home/kmt/tmp/django/trunk/django/template/debug.py" in render_node
  71.             result = node.render(context)
File "/home/kmt/tmp/django/trunk/django/template/loader_tags.py" in render
  97.         return compiled_parent.render(context)
File "/home/kmt/tmp/django/trunk/django/template/__init__.py" in render
  176.         return self.nodelist.render(context)
File "/home/kmt/tmp/django/trunk/django/template/__init__.py" in render
  756.                 bits.append(self.render_node(node, context))
File "/home/kmt/tmp/django/trunk/django/template/debug.py" in render_node
  71.             result = node.render(context)
File "/home/kmt/tmp/django/trunk/django/template/loader_tags.py" in render
  97.         return compiled_parent.render(context)
File "/home/kmt/tmp/django/trunk/django/template/__init__.py" in render
  176.         return self.nodelist.render(context)
File "/home/kmt/tmp/django/trunk/django/template/__init__.py" in render
  756.                 bits.append(self.render_node(node, context))
File "/home/kmt/tmp/django/trunk/django/template/debug.py" in render_node
  71.             result = node.render(context)
File "/home/kmt/tmp/django/trunk/django/template/loader_tags.py" in render
  24.         result = self.nodelist.render(context)
File "/home/kmt/tmp/django/trunk/django/template/__init__.py" in render
  756.                 bits.append(self.render_node(node, context))
File "/home/kmt/tmp/django/trunk/django/template/debug.py" in render_node
  71.             result = node.render(context)
File "/home/kmt/tmp/django/trunk/django/template/defaulttags.py" in render
  148.                 nodelist.append(node.render(context))
File "/home/kmt/tmp/django/trunk/django/template/loader_tags.py" in render
  123.             return t.render(context)
File "/home/kmt/tmp/django/trunk/django/template/__init__.py" in render
  176.         return self.nodelist.render(context)
File "/home/kmt/tmp/django/trunk/django/template/__init__.py" in render
  756.                 bits.append(self.render_node(node, context))
File "/home/kmt/tmp/django/trunk/django/template/debug.py" in render_node
  81.             raise wrapped

Exception Type: TemplateSyntaxError at /admin/crossword/entries/33616/
Exception Value: Caught an exception while rendering: "Key 'ID' not found in Form"

Original Traceback (most recent call last):
  File "/home/kmt/tmp/django/trunk/django/template/debug.py", line 71, in render_node
    result = node.render(context)
  File "/home/kmt/tmp/django/trunk/django/template/defaulttags.py", line 148, in render
    nodelist.append(node.render(context))
  File "/home/kmt/tmp/django/trunk/django/template/debug.py", line 87, in render
    output = force_unicode(self.filter_expression.resolve(context))
  File "/home/kmt/tmp/django/trunk/django/template/__init__.py", line 523, in resolve
    obj = self.var.resolve(context)
  File "/home/kmt/tmp/django/trunk/django/template/__init__.py", line 664, in resolve
    value = self._resolve_lookup(context)
  File "/home/kmt/tmp/django/trunk/django/template/__init__.py", line 699, in _resolve_lookup
    current = current()
  File "/home/kmt/tmp/django/trunk/django/contrib/admin/helpers.py", line 129, in pk_field
    return AdminField(self.form, self.formset._pk_field.name, False)
  File "/home/kmt/tmp/django/trunk/django/contrib/admin/helpers.py", line 72, in __init__
    self.field = form[field] # A django.forms.BoundField instance
  File "/home/kmt/tmp/django/trunk/django/forms/forms.py", line 105, in __getitem__
    raise KeyError('Key %r not found in Form' % name)
KeyError: "Key 'ID' not found in Form"

comment:7 Changed 6 years ago by brosner

  • Resolution set to fixed
  • Status changed from new to closed

Fixed in [8756].

comment:8 Changed 3 years ago by jacob

  • milestone 1.0 deleted

Milestone 1.0 deleted

Add Comment

Modify Ticket

Change Properties
<Author field>
Action
as closed
as The resolution will be set. Next status will be 'closed'
The resolution will be deleted. Next status will be 'new'
Author


E-mail address and user name can be saved in the Preferences.

 
Note: See TracTickets for help on using tickets.