#12420 closed (fixed)
"OneToOneField doesn't allow assignment of None"
Reported by: | Kieran Brownlees | Owned by: | Andrii Kurinnyi |
---|---|---|---|
Component: | Database layer (models, ORM) | Version: | dev |
Severity: | Keywords: | ||
Cc: | Ryan Nowakowski, mortas.11@…, George Sakkis | 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
In http://code.djangoproject.com/browser/django/trunk/django/forms/models.py (R:11874)
line 53 it states:
53 # OneToOneField doesn't allow assignment of None. Guard against that 54 # instead of allowing it and throwing an error. 55 if isinstance(f, models.OneToOneField) and cleaned_data [f.name] is None: 56 continue
Further reading of the function SingleRelatedObjectDescriptor which
seems to implement most of the OTOF functionality actually states:
http://code.djangoproject.com/browser/django/trunk/django/db/models/fields/related.py (R:11878)
207 # If null=True, we can assign null here, but otherwise the value needs 208 # to be an instance of the related class. 209 if value is None and self.related.field.null == False:
It seems that when the code in models.py was written it was by someone who simply had null=False set, which meant that the OTOF (correctly) could not have a None value.
I found the issue when trying to set a OTOF as null in an admin form it says "saved successfully" but never changes the value.
Attachments (6)
Change History (22)
comment:1 by , 15 years ago
Cc: | added |
---|
comment:2 by , 15 years ago
Cc: | added |
---|---|
Triage Stage: | Unreviewed → Accepted |
comment:3 by , 15 years ago
Version: | 1.1 → SVN |
---|
by , 15 years ago
Attachment: | t12420.diff added |
---|
comment:5 by , 15 years ago
Has patch: | set |
---|---|
milestone: | → 1.2 |
Needs tests: | set |
Patch needs improvement: | set |
comment:6 by , 15 years ago
Owner: | changed from | to
---|
by , 15 years ago
Attachment: | t12420_2.diff added |
---|
Changed name of the test method to be more descriptive
by , 15 years ago
Attachment: | t12420_4.diff added |
---|
comment:7 by , 15 years ago
Needs tests: | unset |
---|---|
Patch needs improvement: | unset |
Triage Stage: | Accepted → Ready for checkin |
comment:8 by , 15 years ago
Resolution: | → fixed |
---|---|
Status: | new → closed |
comment:9 by , 15 years ago
In case anyone wonders, I didn't backport this because the tests fail on 1.1.X. I'm guessing that the change to OneToOneField that made this possible on trunk isn't on that branch.
comment:10 by , 14 years ago
Resolution: | fixed |
---|---|
Status: | closed → reopened |
Lines 258 and 264 of http://code.djangoproject.com/browser/django/trunk/django/db/models/fields/related.py still fail if value is None.
comment:11 by , 14 years ago
Cc: | added |
---|
comment:13 by , 14 years ago
I don't think it's related, this dies fast and hard; I'm surprised it hasn't been noticed before. Using the models of #14043:
In [6]: bob = Person.objects.create(age=34) In [7]: bobs_soul = Soul.objects.create(person=bob) In [8]: bob.soul == bobs_soul Out[8]: True In [9]: bob.soul = None --------------------------------------------------------------------------- AttributeError Traceback (most recent call last) --> 258 setattr(value, self.related.field.attname, getattr(instance, self.related.field.rel.get_related_field().attname)) 259 260 # Since we already know what the related object is, seed the related AttributeError: 'NoneType' object has no attribute 'person_id'
comment:14 by , 14 years ago
Resolution: | → fixed |
---|---|
Status: | reopened → closed |
@gsakkis - bob.soul from your example is a reverse relation.
Person table doesn't actually have soul_id. So, the way Django deals with it,
if you assign an instance of Soul (i.e. bobs_soul) to bob.soul, it modifies bobs_soul.person_id,
so it will be equal to bob.id (line 258). But, then you still need to save "bobs_soul" object, not
"bob". Which might be a bit counter intuitive, because it looks like we are trying to modify "bob"
object.
Now in case when we are trying to assign None, to bob.soul, it fails to modify assigned value, obviously,
because it is not an instance of a Soul.
If our intention is to severe the relation between objects, we can speculate about two approaches here:
- Check for the fact that the value is None, and don't do anything. But in this case assignment of None wouldn't change anything after saving any of our object to db. (So we didn't reach our goal)
- Retrieve previous value of "bobs_soul" and modify it's place_id to be None. The problem here is that we will be creating new instance of "bobs_soul", then modifying it, but it wouldn't be available outside of the descriptor. So, there is no way to save this newly created instance. (Unless we will be in fact modifying object from related object's cache, and this object is still referenced by user. Like in your example, you have bobs_soul created before, so with the second approach user will be able to do bobs_soul.save() after bob.soul=None, and it will break the relation)
Both of these approaches doesn't allow us to break the relation between objects. So, maybe the best way is not to allow assignment
on the reverse relation attribute for OneToOneField, or do not allow assignment of None, with proper error message, telling that
an assignment should be performed on the other end.
Anyways. The original intention of this ticket was to fix the bug which didn't allow to assign None to OneToOneFields in model forms.
In my opinion, the issue with reverse relation attribute should be moved to a separate ticket.
I've just run into the issue. Doesn't work properly in SVN too.