Opened 17 years ago

Closed 17 years ago

Last modified 6 years ago

#6445 closed Uncategorized (fixed)

models.ForeignKey should accept instances as default value

Reported by: eikke@… Owned by: Philippe Raoult
Component: Core (Other) Version: dev
Severity: Normal Keywords: foreignkey default
Cc: Triage Stage: Ready for checkin
Has patch: yes Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

It looks like the 'default' parameter of a models.ForeignKey field requires one to pass an id, passing an instance doesn't work. This is rather contra-intuitive as setting a ForeignKey field of an instance of a model to an instance of the foreign model is supported.

Here's a sample:

#Models
class Foo(models.Model):
    a = models.CharField(max_length=10)

def get_foo():
    return Foo.objects.get(id=1)

class Bar(models.Model):
    b = models.CharField(max_length=10)
    a = models.ForeignKey(Foo, default=get_foo)

#Testing
In [8]: f = Foo(a='abc')

In [9]: f.save()

# Forgot to do a print here, but f.id == 1

In [10]: b = Bar(b='def')

In [11]: b.a = f

# Works fine, as expected
In [12]: b.save()

In [13]: b.a
Out[13]: <Foo: Foo object>

In [14]: c = Bar(b='ghi')

# Using 'default', callable returns f, doesn't work, unlike expected
In [15]: c.save()                        
---------------------------------------------------------------------------
pysqlite2.dbapi2.InterfaceError                            Traceback (most recent call last)

/home/nicolas/Projects/django/app/<ipython console> 

/home/nicolas/Projects/django/django-trunk/django/db/models/base.py in save(self, raw)
    259                 cursor.execute("INSERT INTO %s (%s) VALUES (%s)" % \
    260                     (qn(self._meta.db_table), ','.join(field_names),
--> 261                     ','.join(placeholders)), db_values)
    262             else:
    263                 # Create a new record with defaults for everything.

/home/nicolas/Projects/django/django-trunk/django/db/backends/util.py in execute(self, sql, params)
     16         start = time()
     17         try:
---> 18             return self.cursor.execute(sql, params)
     19         finally:
     20             stop = time()

/home/nicolas/Projects/django/django-trunk/django/db/backends/sqlite3/base.py in execute(self, query, params)
    131     def execute(self, query, params=()):
    132         query = self.convert_query(query, len(params))
--> 133         return Database.Cursor.execute(self, query, params)
    134 
    135     def executemany(self, query, param_list):

InterfaceError: Error binding parameter 1 - probably unsupported type.

Running this same query in a web view shows 'params' is [id, <Foo instance>], where the second expected value should be the id of the used Foo instance.

Attachments (1)

6645.diff (2.0 KB ) - added by Philippe Raoult 17 years ago.
fix + tests + doc

Download all attachments as: .zip

Change History (7)

comment:1 by Nicolas Trangez <eikke@…>, 17 years ago

Forgot something: this does work, but it's a rather ugly workaround:

a = models.ForeignKey(Foo, default=lambda: get_foo().id)

comment:2 by Philippe Raoult, 17 years ago

Has patch: set
Owner: changed from nobody to Philippe Raoult
Status: newassigned
Triage Stage: UnreviewedDesign decision needed

My patch fixes the issue by overriding get_default in ForeignKey to handle that case. I threw some tests in for good measure.

by Philippe Raoult, 17 years ago

Attachment: 6645.diff added

fix + tests + doc

comment:3 by Jacob, 17 years ago

Triage Stage: Design decision neededReady for checkin

comment:4 by Malcolm Tredinnick, 17 years ago

Resolution: fixed
Status: assignedclosed

(In [7331]) Fixed #6445 -- Allow model instances to be used as a default for ForeignKeys
(via a callable). Also updates the documentation of the "default" attribute to
indicate a callable can be used. Thanks, Philipe Raoult.

comment:5 by Clayton Daley, 6 years ago

Easy pickings: unset
UI/UX: unset

Was this behavior intentionally reverted? In Django 2.1.7, I'm getting the error:

TypeError: int() argument must be a string, a bytes-like object or a number, not 'Group'

It's coming from this code (because I can fix it by adding .pk to the get call):

# Can't serialize lambdas in migrations, but functions are fine
def const_my_group():
    return Group.objects.get(name=settings.MY_GROUP)

class Task(models.Model):
    group = models.ForeignKey(Group, default=const_my_group, on_delete=models.PROTECT)

EDIT: I don't want to spam a bunch of tickets with one issue, but it may actually be a reversion of this behavior:

Last edited 6 years ago by Clayton Daley (previous) (diff)

comment:6 by Tim Graham, 6 years ago

Severity: Normal
Type: Uncategorized

Yes, you should use .pk -- see #25129.

Note: See TracTickets for help on using tickets.
Back to Top