django 1.7 - help with change in "Save as new" behavior when related inlines are marked to be deleted

424 views
Skip to first unread message

Jonathan Morgan

unread,
Nov 7, 2014, 10:00:28 AM11/7/14
to django...@googlegroups.com
Hello,

I have an Event model (for an event like a concert or a play) that can have many Event_Dates (different showing dates and times of the concert of play).  The model relation is implemented with a ForeignKey from the Event_Date model to the Event.

In the django admins, I have the Event_Dates included as an inline in the Event admin page.  The inline code:

class EventDateInline( admin.StackedInline ):

    model
= Event_Date
    extra
= 2
    fk_name
= 'event'
    fieldsets
= [
       
( None,
           
{
               
'fields' : [ 'start_date', 'start_time', 'end_time' ]
           
}
       
),
       
( "Optional End Date and Description",
           
{
               
'fields' : [ 'end_date', 'description' ],
               
'classes' : ( "collapse", )
           
}
       
),
   
]

#-- END EventDateInline StackedInline class --#

And the code where the inline is included in the Event:

    inlines = [
       
#EventPresenterInline,
       
EventDateInline,
       
EventImageInline,
   
]

Pretty standard stuff based on what I know of django.

When the users of this application need to make a new Event that is an annual update of an event that is put on once each year (the Nutcracker, for instance - multiple performances every December), they go to last year's Event in the admin, check the "delete" checkbox next to all but one of the dates, then use the "Save as new" button to make a new Event.

This worked fine in django versions 1.2 through 1.6.  A new event was created with one new date.  In 1.7, when one does this, one gets the following error:

TypeError at /path/to/event/edit/

Model instances without primary key value are unhashable

Request Method:     POST
Django Version:     1.7.1
Exception Type:     TypeError
Exception Value:    

Model instances without primary key value are unhashable

Exception Location:    /path/to/virtualenv/lib/python2.7/site-packages/django/db/models/base.py in __hash__, line 485
Python Executable:  /usr/bin/python
Python Version:     2.7.8 

In looking online, the one post I found that might be related is:


But in my case, while there is a signal associated with Event (on save, it looks up related Event Presenters, creates a string list of their names, then places that string in a field in the Event), it isn't anywhere in the stack trace for the error (below).

Initially, if anyone could help me figure out if this is a behavior that has changed in django versus one that might be related to my code, I'd appreciate it.  I tried disabling all the signals in my application (there are 4 - I removed them from my code-base entirely, then rebooted the server to make sure apache and wsgi were looking at the current code) and the problem persisted.

I honestly was surprised that it used to work, am trying to figure out what to tell the user.  For now, "Save as new" where one does not remove any Event_Dates works, so I advised the user to just do that, then update dates as needed in the new record.

Any help will be greatly appreciated.

Thanks,

Jon

======================================
stack trace:
======================================

Traceback:
File "/path/to/virtualenv/lib/python2.7/site-packages/django/core/handlers/base.py" in get_response
  111.                     response = wrapped_callback(request, *callback_args, **callback_kwargs)
File "/path/to/virtualenv/lib/python2.7/site-packages/django/contrib/admin/options.py" in wrapper
  584.                 return self.admin_site.admin_view(view)(*args, **kwargs)
File "/path/to/virtualenv/lib/python2.7/site-packages/django/utils/decorators.py" in _wrapped_view
  105.                     response = view_func(request, *args, **kwargs)
File "/path/to/virtualenv/lib/python2.7/site-packages/django/views/decorators/cache.py" in _wrapped_view_func
  52.         response = view_func(request, *args, **kwargs)
File "/path/to/virtualenv/lib/python2.7/site-packages/django/contrib/admin/sites.py" in inner
  204.             return view(request, *args, **kwargs)
File "/path/to/virtualenv/lib/python2.7/site-packages/django/contrib/admin/options.py" in change_view
  1457.         return self.changeform_view(request, object_id, form_url, extra_context)
File "/path/to/virtualenv/lib/python2.7/site-packages/django/utils/decorators.py" in _wrapper
  29.             return bound_func(*args, **kwargs)
File "/path/to/virtualenv/lib/python2.7/site-packages/django/utils/decorators.py" in _wrapped_view
  105.                     response = view_func(request, *args, **kwargs)
File "/path/to/virtualenv/lib/python2.7/site-packages/django/utils/decorators.py" in bound_func
  25.                 return func.__get__(self, type(self))(*args2, **kwargs2)
File "/path/to/virtualenv/lib/python2.7/site-packages/django/db/transaction.py" in inner
  394.                 return func(*args, **kwargs)
File "/path/to/virtualenv/lib/python2.7/site-packages/django/contrib/admin/options.py" in changeform_view
  1392.                     current_app=self.admin_site.name))
File "/path/to/virtualenv/lib/python2.7/site-packages/django/contrib/admin/options.py" in add_view
  1454.         return self.changeform_view(request, None, form_url, extra_context)
File "/path/to/virtualenv/lib/python2.7/site-packages/django/utils/decorators.py" in _wrapper
  29.             return bound_func(*args, **kwargs)
File "/path/to/virtualenv/lib/python2.7/site-packages/django/utils/decorators.py" in _wrapped_view
  105.                     response = view_func(request, *args, **kwargs)
File "/path/to/virtualenv/lib/python2.7/site-packages/django/utils/decorators.py" in bound_func
  25.                 return func.__get__(self, type(self))(*args2, **kwargs2)
File "/path/to/virtualenv/lib/python2.7/site-packages/django/db/transaction.py" in inner
  394.                 return func(*args, **kwargs)
File "/path/to/virtualenv/lib/python2.7/site-packages/django/contrib/admin/options.py" in changeform_view
  1404.             if all_valid(formsets) and form_validated:
File "/path/to/virtualenv/lib/python2.7/site-packages/django/forms/formsets.py" in all_valid
  438.         if not formset.is_valid():
File "/path/to/virtualenv/lib/python2.7/site-packages/django/forms/formsets.py" in is_valid
  303.         self.errors
File "/path/to/virtualenv/lib/python2.7/site-packages/django/forms/formsets.py" in errors
  277.             self.full_clean()
File "/path/to/virtualenv/lib/python2.7/site-packages/django/forms/formsets.py" in full_clean
  343.             self.clean()
File "/path/to/virtualenv/lib/python2.7/site-packages/django/forms/models.py" in clean
  641.         self.validate_unique()
File "/path/to/virtualenv/lib/python2.7/site-packages/django/forms/models.py" in validate_unique
  648.         valid_forms = [form for form in self.forms if form.is_valid() and form not in forms_to_delete]
File "/path/to/virtualenv/lib/python2.7/site-packages/django/contrib/admin/options.py" in is_valid
  1853.                 self.hand_clean_DELETE()
File "/path/to/virtualenv/lib/python2.7/site-packages/django/contrib/admin/options.py" in hand_clean_DELETE
  1833.                     collector.collect([self.instance])
File "/path/to/virtualenv/lib/python2.7/site-packages/django/contrib/admin/utils.py" in collect
  180.             return super(NestedObjects, self).collect(objs, source_attr=source_attr, **kwargs)
File "/path/to/virtualenv/lib/python2.7/site-packages/django/db/models/deletion.py" in collect
  168.                             reverse_dependency=reverse_dependency)
File "/path/to/virtualenv/lib/python2.7/site-packages/django/db/models/deletion.py" in add
  85.             if obj not in instances:
File "/path/to/virtualenv/lib/python2.7/site-packages/django/db/models/base.py" in __hash__
  485.             raise TypeError("Model instances without primary key value are unhashable")

Exception Type: TypeError at /path/to/event/edit/
Exception Value: Model instances without primary key value are unhashable


Collin Anderson

unread,
Nov 12, 2014, 3:42:43 PM11/12/14
to django...@googlegroups.com
Hello,

That does seem odd. Are you using a custom form or custom template at all?

There shouldn't be a delete box for unsaved items, right?

Collin

Alex Marandon

unread,
Nov 14, 2014, 4:57:54 AM11/14/14
to django...@googlegroups.com
On Wednesday, 12 November 2014 21:42:43 UTC+1, Collin Anderson wrote:
Hello,

That does seem odd. Are you using a custom form or custom template at all?

I bumped into this issue too. I made a minimal project that triggers the issue :

# model.py
from django.db import models


class Author(models.Model):
    name
= models.CharField(max_length=100)

   
def __unicode__(self):
       
return self.name


class Book(models.Model):
    title
= models.CharField(max_length=100)
    authors
= models.ManyToManyField(Author)

   
def __unicode__(self):
       
return self.title




# admin.py
from django.contrib import admin
from .models import Author, Book


class AuthorshipInline(admin.TabularInline):
    model
= Book.authors.through


@admin.register(Author)
class AuthorAdmin(admin.ModelAdmin):
    inlines
= [AuthorshipInline]
    save_as
= True


@admin.register(Book)
class BookAdmin(admin.ModelAdmin):
    inlines
= [AuthorshipInline]
    save_as
= True




How to reproduce the problem using the admin :
 - create two authors
 - create a book with those two authors
 - edit the book
 - click the check box in the "Delete?" column for one of the authors
 - click "Save as new"

End of stack trace :

File "/home/synext/lib/python2.7/site-packages/Django-1.7.1-py2.7.egg/django/db/models/deletion.py" in collect
  168.                             reverse_dependency=reverse_dependency)
File "/home/synext/lib/python2.7/site-packages/Django-1.7.1-py2.7.egg/django/db/models/deletion.py" in add

  85.             if obj not in instances:
File "/home/synext/lib/python2.7/site-packages/Django-1.7.1-py2.7.egg/django/db/models/base.py" in __hash__

  485.             raise TypeError("Model instances without primary key value are unhashable")

Exception Type: TypeError at /admin/myapp/book/1/

Jonathan Morgan

unread,
Nov 14, 2014, 9:22:14 AM11/14/14
to django...@googlegroups.com
Hello,

Thanks for replying!  Sorry for delayed response.

I am not using custom forms or custom templates.

It looks like, with "Save as new", django first makes a full copy of the base record and all the child records, including those that are marked for deletion, and then it subsequently tries to delete the ones that are marked for deletion before it saves everything, to avoid creating-then-deleting the records marked for deletion.  This makes sense.

In 1.7, deletion of child records fails because it appears to hash the child records as part of this processing, but the child records have not been saved to the database, so do not have an ID, so cannot be hashed.

This behavior of throwing an exception on attempts to hash objects with no ID is a relatively recent change, documented in this commit from 8/14/2013:

https://github.com/django/django/commit/6af05e7a0f0e4604d6a67899acaa99d73ec0dfaa

I am not sure what release this commit is first in, and I don't know too much about the core django code at work here.  If that commit was included in a version earlier than 1.7, then it might be a combination of it and changes to the save functionality to add hashing that are causing this problem.  If it was only introduced in 1.7, then it could be that this change is the cause of this problem, and the higher-level code that implements "Save as new" might not have been changed to deal with this case.

I'm happy to help as I can, but not sure how to proceed from here.

Thanks,

Jon

Jonathan Morgan

unread,
Nov 14, 2014, 9:28:56 AM11/14/14
to django...@googlegroups.com
re: "There shouldn't be a delete box for unsaved items, right?"

This is a special case - you are taking an existing record and "Save as new"-ing (or however you say that), so there are existing associated records that you are copying as well.  But, in this case, I think the user would change a few of those items, then would get rid of any subsequent ones that are not needed by clicking the "Delete" checkbox next to them.  In the original record, all items are saved.  In the new record, however, after "Save as new", none of the child records are saved, and only some are intended to be kept, while some are intended to be discarded.

As far as I know, if you just do "Save as new", then delete un-wanted records in a subsequent edit of the new record, everything works fine.


On Wednesday, November 12, 2014 3:42:43 PM UTC-5, Collin Anderson wrote:

Alex Marandon

unread,
Nov 17, 2014, 9:34:05 AM11/17/14
to django...@googlegroups.com
I've created a ticket for this issue: https://code.djangoproject.com/ticket/23857
Reply all
Reply to author
Forward
0 new messages