Version in base suite: 1.11.23-1~deb10u1 Base version: python-django_1.11.23-1~deb10u1 Target version: python-django_1.11.27-1~deb10u1 Base file: /srv/ftp-master.debian.org/ftp/pool/main/p/python-django/python-django_1.11.23-1~deb10u1.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/p/python-django/python-django_1.11.27-1~deb10u1.dsc Django.egg-info/PKG-INFO | 4 - Django.egg-info/SOURCES.txt | 4 + PKG-INFO | 4 - debian/changelog | 21 +++++++ django/__init__.py | 2 django/contrib/auth/forms.py | 30 ++++++++-- django/contrib/postgres/fields/hstore.py | 2 django/contrib/postgres/fields/jsonb.py | 2 django/contrib/postgres/lookups.py | 2 django/forms/widgets.py | 2 docs/releases/1.11.24.txt | 15 +++++ docs/releases/1.11.25.txt | 14 ++++ docs/releases/1.11.26.txt | 15 +++++ docs/releases/1.11.27.txt | 31 ++++++++++ docs/releases/index.txt | 4 + docs/releases/security.txt | 55 ++++++++++++++++++ tests/auth_tests/test_forms.py | 42 ++++++++++++++ tests/forms_tests/widget_tests/test_checkboxinput.py | 5 + tests/postgres_tests/test_array.py | 11 +++ tests/postgres_tests/test_hstore.py | 15 +++++ tests/postgres_tests/test_json.py | 56 ++++++++++++++++++- 21 files changed, 323 insertions(+), 13 deletions(-) diff -Nru python-django-1.11.23/Django.egg-info/PKG-INFO python-django-1.11.27/Django.egg-info/PKG-INFO --- python-django-1.11.23/Django.egg-info/PKG-INFO 2019-08-01 08:44:01.000000000 +0000 +++ python-django-1.11.27/Django.egg-info/PKG-INFO 2019-12-18 08:33:12.000000000 +0000 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: Django -Version: 1.11.23 +Version: 1.11.27 Summary: A high-level Python Web framework that encourages rapid development and clean, pragmatic design. Home-page: https://www.djangoproject.com/ Author: Django Software Foundation @@ -27,5 +27,5 @@ Classifier: Topic :: Internet :: WWW/HTTP :: WSGI Classifier: Topic :: Software Development :: Libraries :: Application Frameworks Classifier: Topic :: Software Development :: Libraries :: Python Modules -Provides-Extra: argon2 Provides-Extra: bcrypt +Provides-Extra: argon2 diff -Nru python-django-1.11.23/Django.egg-info/SOURCES.txt python-django-1.11.27/Django.egg-info/SOURCES.txt --- python-django-1.11.23/Django.egg-info/SOURCES.txt 2019-08-01 08:44:02.000000000 +0000 +++ python-django-1.11.27/Django.egg-info/SOURCES.txt 2019-12-18 08:33:13.000000000 +0000 @@ -3551,6 +3551,10 @@ docs/releases/1.11.21.txt docs/releases/1.11.22.txt docs/releases/1.11.23.txt +docs/releases/1.11.24.txt +docs/releases/1.11.25.txt +docs/releases/1.11.26.txt +docs/releases/1.11.27.txt docs/releases/1.11.3.txt docs/releases/1.11.4.txt docs/releases/1.11.5.txt diff -Nru python-django-1.11.23/PKG-INFO python-django-1.11.27/PKG-INFO --- python-django-1.11.23/PKG-INFO 2019-08-01 08:44:08.000000000 +0000 +++ python-django-1.11.27/PKG-INFO 2019-12-18 08:33:15.000000000 +0000 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: Django -Version: 1.11.23 +Version: 1.11.27 Summary: A high-level Python Web framework that encourages rapid development and clean, pragmatic design. Home-page: https://www.djangoproject.com/ Author: Django Software Foundation @@ -27,5 +27,5 @@ Classifier: Topic :: Internet :: WWW/HTTP :: WSGI Classifier: Topic :: Software Development :: Libraries :: Application Frameworks Classifier: Topic :: Software Development :: Libraries :: Python Modules -Provides-Extra: argon2 Provides-Extra: bcrypt +Provides-Extra: argon2 diff -Nru python-django-1.11.23/debian/changelog python-django-1.11.27/debian/changelog --- python-django-1.11.23/debian/changelog 2019-08-08 15:00:04.000000000 +0000 +++ python-django-1.11.27/debian/changelog 2020-01-06 15:35:55.000000000 +0000 @@ -1,3 +1,24 @@ +python-django (1:1.11.27-1~deb10u1) buster-security; urgency=high + + * New upstream security release. (Closes: #946937) + + + - CVE-2019-19844: Potential account hijack via password reset form. + + By submitting a suitably crafted email address making use of Unicode + characters, that compared equal to an existing user email when + lower-cased for comparison, an attacker could be sent a password reset + token for the matched account. + + In order to avoid this vulnerability, password reset requests now compare + the submitted email using the stricter, recommended algorithm for + case-insensitive comparison of two identifiers from Unicode Technical + Report 36, section 2.11.2(B)(2). Upon a match, the email containing the + reset token will be sent to the email address on record rather than the + submitted address. + + -- Chris Lamb Mon, 06 Jan 2020 15:35:55 +0000 + python-django (1:1.11.23-1~deb10u1) buster-security; urgency=high * New upstream security release. diff -Nru python-django-1.11.23/django/__init__.py python-django-1.11.27/django/__init__.py --- python-django-1.11.23/django/__init__.py 2019-08-01 08:43:22.000000000 +0000 +++ python-django-1.11.27/django/__init__.py 2019-12-18 08:32:18.000000000 +0000 @@ -2,7 +2,7 @@ from django.utils.version import get_version -VERSION = (1, 11, 23, 'final', 0) +VERSION = (1, 11, 27, 'final', 0) __version__ = get_version(VERSION) diff -Nru python-django-1.11.23/django/contrib/auth/forms.py python-django-1.11.27/django/contrib/auth/forms.py --- python-django-1.11.23/django/contrib/auth/forms.py 2019-08-01 08:35:35.000000000 +0000 +++ python-django-1.11.27/django/contrib/auth/forms.py 2019-12-18 08:31:53.000000000 +0000 @@ -16,12 +16,27 @@ from django.template import loader from django.utils.encoding import force_bytes from django.utils.http import urlsafe_base64_encode +from django.utils.six import PY3 from django.utils.text import capfirst from django.utils.translation import ugettext, ugettext_lazy as _ UserModel = get_user_model() +def _unicode_ci_compare(s1, s2): + """ + Perform case-insensitive comparison of two identifiers, using the + recommended algorithm from Unicode Technical Report 36, section + 2.11.2(B)(2). + """ + normalized1 = unicodedata.normalize('NFKC', s1) + normalized2 = unicodedata.normalize('NFKC', s2) + if PY3: + return normalized1.casefold() == normalized2.casefold() + # lower() is the best alternative available on Python 2. + return normalized1.lower() == normalized2.lower() + + class ReadOnlyPasswordHashWidget(forms.Widget): template_name = 'auth/widgets/read_only_password_hash.html' @@ -249,11 +264,16 @@ that prevent inactive users and users with unusable passwords from resetting their password. """ + email_field_name = UserModel.get_email_field_name() active_users = UserModel._default_manager.filter(**{ - '%s__iexact' % UserModel.get_email_field_name(): email, + '%s__iexact' % email_field_name: email, 'is_active': True, }) - return (u for u in active_users if u.has_usable_password()) + return ( + u for u in active_users + if u.has_usable_password() and + _unicode_ci_compare(email, getattr(u, email_field_name)) + ) def save(self, domain_override=None, subject_template_name='registration/password_reset_subject.txt', @@ -266,6 +286,7 @@ user. """ email = self.cleaned_data["email"] + email_field_name = UserModel.get_email_field_name() for user in self.get_users(email): if not domain_override: current_site = get_current_site(request) @@ -273,8 +294,9 @@ domain = current_site.domain else: site_name = domain = domain_override + user_email = getattr(user, email_field_name) context = { - 'email': email, + 'email': user_email, 'domain': domain, 'site_name': site_name, 'uid': urlsafe_base64_encode(force_bytes(user.pk)), @@ -286,7 +308,7 @@ context.update(extra_email_context) self.send_mail( subject_template_name, email_template_name, context, from_email, - email, html_email_template_name=html_email_template_name, + user_email, html_email_template_name=html_email_template_name, ) diff -Nru python-django-1.11.23/django/contrib/postgres/fields/hstore.py python-django-1.11.27/django/contrib/postgres/fields/hstore.py --- python-django-1.11.23/django/contrib/postgres/fields/hstore.py 2019-08-01 08:35:36.000000000 +0000 +++ python-django-1.11.27/django/contrib/postgres/fields/hstore.py 2019-12-18 08:31:54.000000000 +0000 @@ -86,7 +86,7 @@ def as_sql(self, compiler, connection): lhs, params = compiler.compile(self.lhs) - return '(%s -> %%s)' % lhs, [self.key_name] + params + return '(%s -> %%s)' % lhs, tuple(params) + (self.key_name,) class KeyTransformFactory(object): diff -Nru python-django-1.11.23/django/contrib/postgres/fields/jsonb.py python-django-1.11.27/django/contrib/postgres/fields/jsonb.py --- python-django-1.11.23/django/contrib/postgres/fields/jsonb.py 2019-08-01 08:35:36.000000000 +0000 +++ python-django-1.11.27/django/contrib/postgres/fields/jsonb.py 2019-12-18 08:31:54.000000000 +0000 @@ -107,7 +107,7 @@ lookup = int(self.key_name) except ValueError: lookup = self.key_name - return '(%s %s %%s)' % (lhs, self.operator), [lookup] + params + return '(%s %s %%s)' % (lhs, self.operator), tuple(params) + (lookup,) class KeyTextTransform(KeyTransform): diff -Nru python-django-1.11.23/django/contrib/postgres/lookups.py python-django-1.11.27/django/contrib/postgres/lookups.py --- python-django-1.11.23/django/contrib/postgres/lookups.py 2019-08-01 08:35:36.000000000 +0000 +++ python-django-1.11.27/django/contrib/postgres/lookups.py 2019-12-18 08:31:54.000000000 +0000 @@ -8,7 +8,7 @@ def as_sql(self, qn, connection): lhs, lhs_params = self.process_lhs(qn, connection) rhs, rhs_params = self.process_rhs(qn, connection) - params = lhs_params + rhs_params + params = tuple(lhs_params) + tuple(rhs_params) return '%s %s %s' % (lhs, self.operator, rhs), params diff -Nru python-django-1.11.23/django/forms/widgets.py python-django-1.11.27/django/forms/widgets.py --- python-django-1.11.23/django/forms/widgets.py 2019-08-01 08:35:36.000000000 +0000 +++ python-django-1.11.27/django/forms/widgets.py 2019-12-18 08:31:54.000000000 +0000 @@ -508,6 +508,8 @@ if self.check_test(value): if attrs is None: attrs = {} + else: + attrs = attrs.copy() attrs['checked'] = True return super(CheckboxInput, self).get_context(name, value, attrs) diff -Nru python-django-1.11.23/docs/releases/1.11.24.txt python-django-1.11.27/docs/releases/1.11.24.txt --- python-django-1.11.23/docs/releases/1.11.24.txt 1970-01-01 00:00:00.000000000 +0000 +++ python-django-1.11.27/docs/releases/1.11.24.txt 2019-11-18 08:15:26.000000000 +0000 @@ -0,0 +1,15 @@ +============================ +Django 1.11.24 release notes +============================ + +*September 2, 2019* + +Django 1.11.24 fixes a regression in 1.11.23. + +Bugfixes +======== + +* Fixed crash of ``KeyTransform()`` for + :class:`~django.contrib.postgres.fields.JSONField` and + :class:`~django.contrib.postgres.fields.HStoreField` when using on + expressions with params (:ticket:`30672`). diff -Nru python-django-1.11.23/docs/releases/1.11.25.txt python-django-1.11.27/docs/releases/1.11.25.txt --- python-django-1.11.23/docs/releases/1.11.25.txt 1970-01-01 00:00:00.000000000 +0000 +++ python-django-1.11.27/docs/releases/1.11.25.txt 2019-11-18 08:15:26.000000000 +0000 @@ -0,0 +1,14 @@ +============================ +Django 1.11.25 release notes +============================ + +*October 1, 2019* + +Django 1.11.25 fixes a regression in 1.11.23. + +Bugfixes +======== + +* Fixed a crash when filtering with a ``Subquery()`` annotation of a queryset + containing :class:`~django.contrib.postgres.fields.JSONField` or + :class:`~django.contrib.postgres.fields.HStoreField` (:ticket:`30769`). diff -Nru python-django-1.11.23/docs/releases/1.11.26.txt python-django-1.11.27/docs/releases/1.11.26.txt --- python-django-1.11.23/docs/releases/1.11.26.txt 1970-01-01 00:00:00.000000000 +0000 +++ python-django-1.11.27/docs/releases/1.11.26.txt 2019-11-18 08:32:10.000000000 +0000 @@ -0,0 +1,15 @@ +============================ +Django 1.11.26 release notes +============================ + +*November 4, 2019* + +Django 1.11.26 fixes a regression in 1.11.25. + +Bugfixes +======== + +* Fixed a crash when using a ``contains``, ``contained_by``, ``has_key``, + ``has_keys``, or ``has_any_keys`` lookup on + :class:`~django.contrib.postgres.fields.JSONField`, if the right or left hand + side of an expression is a key transform (:ticket:`30826`). diff -Nru python-django-1.11.23/docs/releases/1.11.27.txt python-django-1.11.27/docs/releases/1.11.27.txt --- python-django-1.11.23/docs/releases/1.11.27.txt 1970-01-01 00:00:00.000000000 +0000 +++ python-django-1.11.27/docs/releases/1.11.27.txt 2019-12-18 08:17:17.000000000 +0000 @@ -0,0 +1,31 @@ +============================ +Django 1.11.27 release notes +============================ + +*December 18, 2019* + +Django 1.11.27 fixes a security issue and a data loss bug in 1.11.26. + +CVE-2019-19844: Potential account hijack via password reset form +================================================================ + +By submitting a suitably crafted email address making use of Unicode +characters, that compared equal to an existing user email when lower-cased for +comparison, an attacker could be sent a password reset token for the matched +account. + +In order to avoid this vulnerability, password reset requests now compare the +submitted email using the stricter, recommended algorithm for case-insensitive +comparison of two identifiers from `Unicode Technical Report 36, section +2.11.2(B)(2)`__. Upon a match, the email containing the reset token will be +sent to the email address on record rather than the submitted address. + +.. __: https://www.unicode.org/reports/tr36/#Recommendations_General + +Bugfixes +======== + +* Fixed a data loss possibility in + :class:`~django.contrib.postgres.forms.SplitArrayField`. When using with + ``ArrayField(BooleanField())``, all values after the first ``True`` value + were marked as checked instead of preserving passed values (:ticket:`31073`). diff -Nru python-django-1.11.23/docs/releases/index.txt python-django-1.11.27/docs/releases/index.txt --- python-django-1.11.23/docs/releases/index.txt 2019-08-01 08:36:08.000000000 +0000 +++ python-django-1.11.27/docs/releases/index.txt 2019-12-18 08:31:54.000000000 +0000 @@ -26,6 +26,10 @@ .. toctree:: :maxdepth: 1 + 1.11.27 + 1.11.26 + 1.11.25 + 1.11.24 1.11.23 1.11.22 1.11.21 diff -Nru python-django-1.11.23/docs/releases/security.txt python-django-1.11.27/docs/releases/security.txt --- python-django-1.11.23/docs/releases/security.txt 2019-07-29 09:16:02.000000000 +0000 +++ python-django-1.11.27/docs/releases/security.txt 2019-12-18 08:31:54.000000000 +0000 @@ -974,3 +974,58 @@ * Django 2.2 :commit:`(patch) <77706a3e4766da5d5fb75c4db22a0a59a28e6cd6>` * Django 2.1 :commit:`(patch) <1e40f427bb8d0fb37cc9f830096a97c36c97af6f>` * Django 1.11 :commit:`(patch) <32124fc41e75074141b05f10fc55a4f01ff7f050>` + +August 1, 2019 - :cve:`2019-14232` +---------------------------------- + +Denial-of-service possibility in ``django.utils.text.Truncator``. `Full +description `__ + +Versions affected +~~~~~~~~~~~~~~~~~ + +* Django 2.2 :commit:`(patch) ` +* Django 2.1 :commit:`(patch) ` +* Django 1.11 :commit:`(patch) <42a66e969023c00536256469f0e8b8a099ef109d>` + +August 1, 2019 - :cve:`2019-14233` +---------------------------------- + +Denial-of-service possibility in ``strip_tags()``. `Full description +`__ + +Versions affected +~~~~~~~~~~~~~~~~~ + +* Django 2.2 :commit:`(patch) ` +* Django 2.1 :commit:`(patch) <5ff8e791148bd451180124d76a55cb2b2b9556eb>` +* Django 1.11 :commit:`(patch) <52479acce792ad80bb0f915f20b835f919993c72>` + + +August 1, 2019 - :cve:`2019-14234` +---------------------------------- + +SQL injection possibility in key and index lookups for +``JSONField``/``HStoreField``. `Full description +`__ + +Versions affected +~~~~~~~~~~~~~~~~~ + +* Django 2.2 :commit:`(patch) <4f5b58f5cd3c57fee9972ab074f8dc6895d8f387>` +* Django 2.1 :commit:`(patch) ` +* Django 1.11 :commit:`(patch) ` + +August 1, 2019 - :cve:`2019-14235` +---------------------------------- + +Potential memory exhaustion in ``django.utils.encoding.uri_to_iri()``. `Full +description +`__ + +Versions affected +~~~~~~~~~~~~~~~~~ + +* Django 2.2 :commit:`(patch) ` +* Django 2.1 :commit:`(patch) <5d50a2e5fa36ad23ab532fc54cf4073de84b3306>` +* Django 1.11 :commit:`(patch) <869b34e9b3be3a4cfcb3a145f218ffd3f5e3fd79>` diff -Nru python-django-1.11.23/tests/auth_tests/test_forms.py python-django-1.11.27/tests/auth_tests/test_forms.py --- python-django-1.11.23/tests/auth_tests/test_forms.py 2019-08-01 08:35:37.000000000 +0000 +++ python-django-1.11.27/tests/auth_tests/test_forms.py 2019-12-18 08:31:54.000000000 +0000 @@ -694,6 +694,48 @@ self.assertFalse(form.is_valid()) self.assertEqual(form['email'].errors, [_('Enter a valid email address.')]) + def test_user_email_unicode_collision(self): + User.objects.create_user('mike123', 'mike@example.org', 'test123') + User.objects.create_user('mike456', 'mıke@example.org', 'test123') + data = {'email': 'mıke@example.org'} + form = PasswordResetForm(data) + if six.PY2: + self.assertFalse(form.is_valid()) + else: + self.assertTrue(form.is_valid()) + form.save() + self.assertEqual(len(mail.outbox), 1) + self.assertEqual(mail.outbox[0].to, ['mıke@example.org']) + + def test_user_email_domain_unicode_collision(self): + User.objects.create_user('mike123', 'mike@ixample.org', 'test123') + User.objects.create_user('mike456', 'mike@ıxample.org', 'test123') + data = {'email': 'mike@ıxample.org'} + form = PasswordResetForm(data) + self.assertTrue(form.is_valid()) + form.save() + self.assertEqual(len(mail.outbox), 1) + self.assertEqual(mail.outbox[0].to, ['mike@ıxample.org']) + + def test_user_email_unicode_collision_nonexistent(self): + User.objects.create_user('mike123', 'mike@example.org', 'test123') + data = {'email': 'mıke@example.org'} + form = PasswordResetForm(data) + if six.PY2: + self.assertFalse(form.is_valid()) + else: + self.assertTrue(form.is_valid()) + form.save() + self.assertEqual(len(mail.outbox), 0) + + def test_user_email_domain_unicode_collision_nonexistent(self): + User.objects.create_user('mike123', 'mike@ixample.org', 'test123') + data = {'email': 'mike@ıxample.org'} + form = PasswordResetForm(data) + self.assertTrue(form.is_valid()) + form.save() + self.assertEqual(len(mail.outbox), 0) + def test_nonexistent_email(self): """ Test nonexistent email address. This should not fail because it would diff -Nru python-django-1.11.23/tests/forms_tests/widget_tests/test_checkboxinput.py python-django-1.11.27/tests/forms_tests/widget_tests/test_checkboxinput.py --- python-django-1.11.23/tests/forms_tests/widget_tests/test_checkboxinput.py 2019-08-01 08:35:37.000000000 +0000 +++ python-django-1.11.27/tests/forms_tests/widget_tests/test_checkboxinput.py 2019-12-18 08:31:54.000000000 +0000 @@ -89,3 +89,8 @@ def test_value_omitted_from_data(self): self.assertIs(self.widget.value_omitted_from_data({'field': 'value'}, {}, 'field'), False) self.assertIs(self.widget.value_omitted_from_data({}, {}, 'field'), False) + + def test_get_context_does_not_mutate_attrs(self): + attrs = {'checked': False} + self.widget.get_context('name', True, attrs) + self.assertIs(attrs['checked'], False) diff -Nru python-django-1.11.23/tests/postgres_tests/test_array.py python-django-1.11.27/tests/postgres_tests/test_array.py --- python-django-1.11.23/tests/postgres_tests/test_array.py 2019-08-01 08:35:37.000000000 +0000 +++ python-django-1.11.27/tests/postgres_tests/test_array.py 2019-12-18 08:31:54.000000000 +0000 @@ -826,6 +826,17 @@ } ) + def test_checkbox_get_context_attrs(self): + context = SplitArrayWidget( + forms.CheckboxInput(), + size=2, + ).get_context('name', [True, False]) + self.assertEqual(context['widget']['value'], '[True, False]') + self.assertEqual( + [subwidget['attrs'] for subwidget in context['widget']['subwidgets']], + [{'checked': True}, {}] + ) + def test_render(self): self.check_html( SplitArrayWidget(forms.TextInput(), size=2), 'array', None, diff -Nru python-django-1.11.23/tests/postgres_tests/test_hstore.py python-django-1.11.27/tests/postgres_tests/test_hstore.py --- python-django-1.11.23/tests/postgres_tests/test_hstore.py 2019-08-01 08:35:37.000000000 +0000 +++ python-django-1.11.27/tests/postgres_tests/test_hstore.py 2019-12-18 08:31:54.000000000 +0000 @@ -5,6 +5,7 @@ from django.core import exceptions, serializers from django.db import connection +from django.db.models.expressions import OuterRef, RawSQL, Subquery from django.forms import Form from django.test.utils import CaptureQueriesContext, modify_settings @@ -14,6 +15,7 @@ try: from django.contrib.postgres import forms from django.contrib.postgres.fields import HStoreField + from django.contrib.postgres.fields.hstore import KeyTransform from django.contrib.postgres.validators import KeysValidator except ImportError: pass @@ -121,6 +123,13 @@ self.objs[:2] ) + def test_key_transform_raw_expression(self): + expr = RawSQL('%s::hstore', ['x => b, y => c']) + self.assertSequenceEqual( + HStoreModel.objects.filter(field__a=KeyTransform('x', expr)), + self.objs[:2] + ) + def test_keys(self): self.assertSequenceEqual( HStoreModel.objects.filter(field__keys=['a']), @@ -180,6 +189,12 @@ queries[0]['sql'], ) + def test_obj_subquery_lookup(self): + qs = HStoreModel.objects.annotate( + value=Subquery(HStoreModel.objects.filter(pk=OuterRef('pk')).values('field')), + ).filter(value__a='b') + self.assertSequenceEqual(qs, self.objs[:2]) + class TestSerialization(HStoreTestCase): test_data = ('[{"fields": {"field": "{\\"a\\": \\"b\\"}"}, ' diff -Nru python-django-1.11.23/tests/postgres_tests/test_json.py python-django-1.11.27/tests/postgres_tests/test_json.py --- python-django-1.11.23/tests/postgres_tests/test_json.py 2019-08-01 08:35:37.000000000 +0000 +++ python-django-1.11.27/tests/postgres_tests/test_json.py 2019-12-18 08:31:54.000000000 +0000 @@ -7,6 +7,9 @@ from django.core import exceptions, serializers from django.core.serializers.json import DjangoJSONEncoder from django.db import connection +from django.db.models import F, OuterRef, Subquery +from django.db.models.expressions import RawSQL +from django.db.models.functions import Cast from django.forms import CharField, Form, widgets from django.test import skipUnlessDBFeature from django.test.utils import CaptureQueriesContext @@ -18,6 +21,9 @@ try: from django.contrib.postgres import forms from django.contrib.postgres.fields import JSONField + from django.contrib.postgres.fields.jsonb import ( + KeyTextTransform, KeyTransform + ) except ImportError: pass @@ -126,7 +132,12 @@ 'k': True, 'l': False, }), - JSONModel.objects.create(field={'foo': 'bar'}), + JSONModel.objects.create(field={ + 'foo': 'bar', + 'baz': {'a': 'b', 'c': 'd'}, + 'bar': ['foo', 'bar'], + 'bax': {'foo': 'bar'}, + }), ] def test_exact(self): @@ -147,6 +158,24 @@ [self.objs[0]] ) + def test_key_transform_raw_expression(self): + expr = RawSQL('%s::jsonb', ['{"x": "bar"}']) + self.assertSequenceEqual( + JSONModel.objects.filter(field__foo=KeyTransform('x', expr)), + [self.objs[-1]], + ) + + def test_key_transform_expression(self): + self.assertSequenceEqual( + JSONModel.objects.filter(field__d__0__isnull=False).annotate( + key=KeyTransform('d', 'field'), + ).annotate( + chain=KeyTransform('0', 'key'), + expr=KeyTransform('0', Cast('key', JSONField())), + ).filter(chain=F('expr')), + [self.objs[8]], + ) + def test_isnull_key(self): # key__isnull works the same as has_key='key'. self.assertSequenceEqual( @@ -200,6 +229,12 @@ [self.objs[7], self.objs[8]] ) + def test_obj_subquery_lookup(self): + qs = JSONModel.objects.annotate( + value=Subquery(JSONModel.objects.filter(pk=OuterRef('pk')).values('field')), + ).filter(value__a='b') + self.assertSequenceEqual(qs, [self.objs[7], self.objs[8]]) + def test_deep_lookup_objs(self): self.assertSequenceEqual( JSONModel.objects.filter(field__k__l='m'), @@ -277,6 +312,25 @@ queries[0]['sql'], ) + def test_lookups_with_key_transform(self): + tests = ( + ('field__d__contains', 'e'), + ('field__baz__contained_by', {'a': 'b', 'c': 'd', 'e': 'f'}), + ('field__baz__has_key', 'c'), + ('field__baz__has_keys', ['a', 'c']), + ('field__baz__has_any_keys', ['a', 'x']), + ('field__contains', KeyTransform('bax', 'field')), + ( + 'field__contained_by', + KeyTransform('x', RawSQL('%s::jsonb', ['{"x": {"a": "b", "c": 1, "d": "e"}}'])), + ), + ('field__has_key', KeyTextTransform('foo', 'field')), + ) + for lookup, value in tests: + self.assertTrue(JSONModel.objects.filter( + **{lookup: value} + ).exists()) + @skipUnlessDBFeature('has_jsonb_datatype') class TestSerialization(PostgreSQLTestCase):