#34816: GenericForeignKey crashes if content_type_id is changed and object_id is
type incompatible with old object
-------------------------------------+-------------------------------------
Reporter: Richard Laager | Owner: nobody
Type: Uncategorized | Status: new
Component: | Version: 4.2
contrib.contenttypes |
Severity: Normal | Resolution:
Keywords: | Triage Stage:
| Unreviewed
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
Changes (by Richard Laager):
* status: closed => new
* resolution: invalid =>
Old description:
> Steps to reproduce:
>
> 1. Create a model ("A") with a GenericForeignKey.
> 2. Create an instance of A ("a") referencing an instance of model "B"
> with a PK that is an integer type.
> 3. Change the instance of "A" to reference an instance of model "C" with
> a PK that is incompatible with int(). But make this change using
> `content_type_id` and `object_id` properties, not `content_object`, i.e.:
> {{{#!python
> a.content_type_id = ContentType.objects.get_for_model(B)
> a.object_id = "foo"
> }}}
> 4. Try to access `a.content_object`.
>
> Expected result:
>
> This looks up and returns an instance of "b" (the B with a PK of "foo").
>
> Actual result:
>
> This crashes (I'm using 3.2, but the code is unchanged in master):
> {{{#!python
> File "django/db/models/fields/__init__.py", line 1836, in to_python
> return int(value)
>
> ValueError: invalid literal for int() with base 10: 'foo'
> }}}
>
> This happens because it tries to `to_python()` the new key on the old
> model's PK field.
>
> One possible fix would be to make the logic short-circuit, like this
> (also attached):
> {{{#!diff
> diff --git a/django/contrib/contenttypes/fields.py
> b/django/contrib/contenttypes/fields.py
> index 35fcd0d908..e984fb5375 100644
> --- a/django/contrib/contenttypes/fields.py
> +++ b/django/contrib/contenttypes/fields.py
> @@ -242,11 +242,11 @@ class GenericForeignKey(FieldCacheMixin):
> ct_match = (
> ct_id == self.get_content_type(obj=rel_obj,
> using=instance._state.db).id
> )
> - pk_match = rel_obj._meta.pk.to_python(pk_val) == rel_obj.pk
> - if ct_match and pk_match:
> - return rel_obj
> - else:
> - rel_obj = None
> + if ct_match:
> + pk_match = rel_obj._meta.pk.to_python(pk_val) ==
> rel_obj.pk
> + if pk_match:
> + return rel_obj
> + rel_obj = None
> if ct_id is not None:
> ct = self.get_content_type(id=ct_id,
> using=instance._state.db)
> try:
> }}}
New description:
Steps to reproduce:
1. Create a model ("A") with a GenericForeignKey.
2. Create an instance of A ("a") referencing an instance of model "B" with
a PK that is an integer type.
3. Change the instance of "A" to reference an instance of model "C" with a
PK that is incompatible with int(). But make this change using
`content_type_id` and `object_id` properties, not `content_object`, i.e.:
{{{#!python
a.content_type_id = ContentType.objects.get_for_model(B).pk
a.object_id = "foo"
}}}
4. Try to access `a.content_object`.
Expected result:
This looks up and returns an instance of "b" (the B with a PK of "foo").
Actual result:
This crashes (I'm using 3.2, but the code is unchanged in master):
{{{#!python
File "django/db/models/fields/__init__.py", line 1836, in to_python
return int(value)
ValueError: invalid literal for int() with base 10: 'foo'
}}}
This happens because it tries to `to_python()` the new key on the old
model's PK field.
One possible fix would be to make the logic short-circuit, like this (also
attached):
{{{#!diff
diff --git a/django/contrib/contenttypes/fields.py
b/django/contrib/contenttypes/fields.py
index 35fcd0d908..e984fb5375 100644
--- a/django/contrib/contenttypes/fields.py
+++ b/django/contrib/contenttypes/fields.py
@@ -242,11 +242,11 @@ class GenericForeignKey(FieldCacheMixin):
ct_match = (
ct_id == self.get_content_type(obj=rel_obj,
using=instance._state.db).id
)
- pk_match = rel_obj._meta.pk.to_python(pk_val) == rel_obj.pk
- if ct_match and pk_match:
- return rel_obj
- else:
- rel_obj = None
+ if ct_match:
+ pk_match = rel_obj._meta.pk.to_python(pk_val) ==
rel_obj.pk
+ if pk_match:
+ return rel_obj
+ rel_obj = None
if ct_id is not None:
ct = self.get_content_type(id=ct_id,
using=instance._state.db)
try:
}}}
--
Comment:
Sorry, that's just a red herring from writing up the bug report. Here is a
real example from right now:
{{{
In [1]: from case.models import Case
In [2]: case = Case.objects.get(id=REDACTED)
In [3]: case.content_object
Out[3]: <REDACTED: REDACTED>
In [4]: case.content_type_id = 175
In [5]: case.object_id = '218-555-1212'
In [6]: case.content_object
---------------------------------------------------------------------------
ValueError Traceback (most recent call
last)
File /srv/www/testing/lib/.venv/lib/python3.10/site-
packages/django/db/models/fields/__init__.py:1836, in
IntegerField.to_python(self, value)
1835 try:
-> 1836 return int(value)
1837 except (TypeError, ValueError):
ValueError: invalid literal for int() with base 10: '218-555-1212'
During handling of the above exception, another exception occurred:
ValidationError Traceback (most recent call
last)
Cell In[6], line 1
----> 1 case.content_object
File /srv/www/testing/lib/.venv/lib/python3.10/site-
packages/django/contrib/contenttypes/fields.py:233, in
GenericForeignKey.__get__(self, instance, cls)
231 if rel_obj is not None:
232 ct_match = ct_id == self.get_content_type(obj=rel_obj,
using=instance._state.db).id
--> 233 pk_match = rel_obj._meta.pk.to_python(pk_val) == rel_obj.pk
234 if ct_match and pk_match:
235 return rel_obj
File /srv/www/testing/lib/.venv/lib/python3.10/site-
packages/django/db/models/fields/__init__.py:1838, in
IntegerField.to_python(self, value)
1836 return int(value)
1837 except (TypeError, ValueError):
-> 1838 raise exceptions.ValidationError(
1839 self.error_messages['invalid'],
1840 code='invalid',
1841 params={'value': value},
1842 )
ValidationError: ['“218-555-1212” value must be an integer.']
}}}
If step 3 (accessing `case.content_object`) is omitted, the problem does
not occur because there is no cached content_object. Alternatively, if
between 3 and 4 one does `case.content_object = None` to clear the cache,
the problem does not occur.
--
Ticket URL: <https://code.djangoproject.com/ticket/34816#comment:2>
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 unsubscribe from this group and stop receiving emails from it, send an email
to [email protected].
To view this discussion on the web visit
https://groups.google.com/d/msgid/django-updates/0107018a6e1a6f00-ae65d49e-95e5-4ef5-b2b4-0fb3df518df3-000000%40eu-central-1.amazonses.com.