1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198 |
- import decimal
- import enum
- import json
- import unittest
- import uuid
- from django import forms
- from django.core import checks, exceptions, serializers, validators
- from django.core.exceptions import FieldError
- from django.core.management import call_command
- from django.db import IntegrityError, connection, models
- from django.db.models.expressions import Exists, OuterRef, RawSQL, Value
- from django.db.models.functions import Cast, Upper
- from django.test import TransactionTestCase, modify_settings, override_settings
- from django.test.utils import isolate_apps
- from django.utils import timezone
- from . import (
- PostgreSQLSimpleTestCase, PostgreSQLTestCase, PostgreSQLWidgetTestCase,
- )
- from .models import (
- ArrayEnumModel, ArrayFieldSubclass, CharArrayModel, DateTimeArrayModel,
- IntegerArrayModel, NestedIntegerArrayModel, NullableIntegerArrayModel,
- OtherTypesArrayModel, PostgreSQLModel, Tag,
- )
- try:
- from psycopg2.extras import NumericRange
- from django.contrib.postgres.aggregates import ArrayAgg
- from django.contrib.postgres.fields import ArrayField
- from django.contrib.postgres.fields.array import (
- IndexTransform, SliceTransform,
- )
- from django.contrib.postgres.forms import (
- SimpleArrayField, SplitArrayField, SplitArrayWidget,
- )
- from django.db.backends.postgresql.base import PSYCOPG2_VERSION
- except ImportError:
- pass
- @isolate_apps('postgres_tests')
- class BasicTests(PostgreSQLSimpleTestCase):
- def test_get_field_display(self):
- class MyModel(PostgreSQLModel):
- field = ArrayField(
- models.CharField(max_length=16),
- choices=[
- ['Media', [(['vinyl', 'cd'], 'Audio')]],
- (('mp3', 'mp4'), 'Digital'),
- ],
- )
- tests = (
- (['vinyl', 'cd'], 'Audio'),
- (('mp3', 'mp4'), 'Digital'),
- (('a', 'b'), "('a', 'b')"),
- (['c', 'd'], "['c', 'd']"),
- )
- for value, display in tests:
- with self.subTest(value=value, display=display):
- instance = MyModel(field=value)
- self.assertEqual(instance.get_field_display(), display)
- def test_get_field_display_nested_array(self):
- class MyModel(PostgreSQLModel):
- field = ArrayField(
- ArrayField(models.CharField(max_length=16)),
- choices=[
- [
- 'Media',
- [([['vinyl', 'cd'], ('x',)], 'Audio')],
- ],
- ((['mp3'], ('mp4',)), 'Digital'),
- ],
- )
- tests = (
- ([['vinyl', 'cd'], ('x',)], 'Audio'),
- ((['mp3'], ('mp4',)), 'Digital'),
- ((('a', 'b'), ('c',)), "(('a', 'b'), ('c',))"),
- ([['a', 'b'], ['c']], "[['a', 'b'], ['c']]"),
- )
- for value, display in tests:
- with self.subTest(value=value, display=display):
- instance = MyModel(field=value)
- self.assertEqual(instance.get_field_display(), display)
- class TestSaveLoad(PostgreSQLTestCase):
- def test_integer(self):
- instance = IntegerArrayModel(field=[1, 2, 3])
- instance.save()
- loaded = IntegerArrayModel.objects.get()
- self.assertEqual(instance.field, loaded.field)
- def test_char(self):
- instance = CharArrayModel(field=['hello', 'goodbye'])
- instance.save()
- loaded = CharArrayModel.objects.get()
- self.assertEqual(instance.field, loaded.field)
- def test_dates(self):
- instance = DateTimeArrayModel(
- datetimes=[timezone.now()],
- dates=[timezone.now().date()],
- times=[timezone.now().time()],
- )
- instance.save()
- loaded = DateTimeArrayModel.objects.get()
- self.assertEqual(instance.datetimes, loaded.datetimes)
- self.assertEqual(instance.dates, loaded.dates)
- self.assertEqual(instance.times, loaded.times)
- def test_tuples(self):
- instance = IntegerArrayModel(field=(1,))
- instance.save()
- loaded = IntegerArrayModel.objects.get()
- self.assertSequenceEqual(instance.field, loaded.field)
- def test_integers_passed_as_strings(self):
- # This checks that get_prep_value is deferred properly
- instance = IntegerArrayModel(field=['1'])
- instance.save()
- loaded = IntegerArrayModel.objects.get()
- self.assertEqual(loaded.field, [1])
- def test_default_null(self):
- instance = NullableIntegerArrayModel()
- instance.save()
- loaded = NullableIntegerArrayModel.objects.get(pk=instance.pk)
- self.assertIsNone(loaded.field)
- self.assertEqual(instance.field, loaded.field)
- def test_null_handling(self):
- instance = NullableIntegerArrayModel(field=None)
- instance.save()
- loaded = NullableIntegerArrayModel.objects.get()
- self.assertEqual(instance.field, loaded.field)
- instance = IntegerArrayModel(field=None)
- with self.assertRaises(IntegrityError):
- instance.save()
- def test_nested(self):
- instance = NestedIntegerArrayModel(field=[[1, 2], [3, 4]])
- instance.save()
- loaded = NestedIntegerArrayModel.objects.get()
- self.assertEqual(instance.field, loaded.field)
- def test_other_array_types(self):
- instance = OtherTypesArrayModel(
- ips=['192.168.0.1', '::1'],
- uuids=[uuid.uuid4()],
- decimals=[decimal.Decimal(1.25), 1.75],
- tags=[Tag(1), Tag(2), Tag(3)],
- json=[{'a': 1}, {'b': 2}],
- int_ranges=[NumericRange(10, 20), NumericRange(30, 40)],
- bigint_ranges=[
- NumericRange(7000000000, 10000000000),
- NumericRange(50000000000, 70000000000),
- ]
- )
- instance.save()
- loaded = OtherTypesArrayModel.objects.get()
- self.assertEqual(instance.ips, loaded.ips)
- self.assertEqual(instance.uuids, loaded.uuids)
- self.assertEqual(instance.decimals, loaded.decimals)
- self.assertEqual(instance.tags, loaded.tags)
- self.assertEqual(instance.json, loaded.json)
- self.assertEqual(instance.int_ranges, loaded.int_ranges)
- self.assertEqual(instance.bigint_ranges, loaded.bigint_ranges)
- def test_null_from_db_value_handling(self):
- instance = OtherTypesArrayModel.objects.create(
- ips=['192.168.0.1', '::1'],
- uuids=[uuid.uuid4()],
- decimals=[decimal.Decimal(1.25), 1.75],
- tags=None,
- )
- instance.refresh_from_db()
- self.assertIsNone(instance.tags)
- self.assertEqual(instance.json, [])
- self.assertIsNone(instance.int_ranges)
- self.assertIsNone(instance.bigint_ranges)
- def test_model_set_on_base_field(self):
- instance = IntegerArrayModel()
- field = instance._meta.get_field('field')
- self.assertEqual(field.model, IntegerArrayModel)
- self.assertEqual(field.base_field.model, IntegerArrayModel)
- def test_nested_nullable_base_field(self):
- if PSYCOPG2_VERSION < (2, 7, 5):
- self.skipTest('See https://github.com/psycopg/psycopg2/issues/325')
- instance = NullableIntegerArrayModel.objects.create(
- field_nested=[[None, None], [None, None]],
- )
- self.assertEqual(instance.field_nested, [[None, None], [None, None]])
- class TestQuerying(PostgreSQLTestCase):
- @classmethod
- def setUpTestData(cls):
- cls.objs = NullableIntegerArrayModel.objects.bulk_create([
- NullableIntegerArrayModel(field=[1]),
- NullableIntegerArrayModel(field=[2]),
- NullableIntegerArrayModel(field=[2, 3]),
- NullableIntegerArrayModel(field=[20, 30, 40]),
- NullableIntegerArrayModel(field=None),
- ])
- def test_empty_list(self):
- NullableIntegerArrayModel.objects.create(field=[])
- obj = NullableIntegerArrayModel.objects.annotate(
- empty_array=models.Value([], output_field=ArrayField(models.IntegerField())),
- ).filter(field=models.F('empty_array')).get()
- self.assertEqual(obj.field, [])
- self.assertEqual(obj.empty_array, [])
- def test_exact(self):
- self.assertSequenceEqual(
- NullableIntegerArrayModel.objects.filter(field__exact=[1]),
- self.objs[:1]
- )
- def test_exact_with_expression(self):
- self.assertSequenceEqual(
- NullableIntegerArrayModel.objects.filter(field__exact=[Value(1)]),
- self.objs[:1],
- )
- def test_exact_charfield(self):
- instance = CharArrayModel.objects.create(field=['text'])
- self.assertSequenceEqual(
- CharArrayModel.objects.filter(field=['text']),
- [instance]
- )
- def test_exact_nested(self):
- instance = NestedIntegerArrayModel.objects.create(field=[[1, 2], [3, 4]])
- self.assertSequenceEqual(
- NestedIntegerArrayModel.objects.filter(field=[[1, 2], [3, 4]]),
- [instance]
- )
- def test_isnull(self):
- self.assertSequenceEqual(
- NullableIntegerArrayModel.objects.filter(field__isnull=True),
- self.objs[-1:]
- )
- def test_gt(self):
- self.assertSequenceEqual(
- NullableIntegerArrayModel.objects.filter(field__gt=[0]),
- self.objs[:4]
- )
- def test_lt(self):
- self.assertSequenceEqual(
- NullableIntegerArrayModel.objects.filter(field__lt=[2]),
- self.objs[:1]
- )
- def test_in(self):
- self.assertSequenceEqual(
- NullableIntegerArrayModel.objects.filter(field__in=[[1], [2]]),
- self.objs[:2]
- )
- def test_in_subquery(self):
- IntegerArrayModel.objects.create(field=[2, 3])
- self.assertSequenceEqual(
- NullableIntegerArrayModel.objects.filter(
- field__in=IntegerArrayModel.objects.all().values_list('field', flat=True)
- ),
- self.objs[2:3]
- )
- @unittest.expectedFailure
- def test_in_including_F_object(self):
- # This test asserts that Array objects passed to filters can be
- # constructed to contain F objects. This currently doesn't work as the
- # psycopg2 mogrify method that generates the ARRAY() syntax is
- # expecting literals, not column references (#27095).
- self.assertSequenceEqual(
- NullableIntegerArrayModel.objects.filter(field__in=[[models.F('id')]]),
- self.objs[:2]
- )
- def test_in_as_F_object(self):
- self.assertSequenceEqual(
- NullableIntegerArrayModel.objects.filter(field__in=[models.F('field')]),
- self.objs[:4]
- )
- def test_contained_by(self):
- self.assertSequenceEqual(
- NullableIntegerArrayModel.objects.filter(field__contained_by=[1, 2]),
- self.objs[:2]
- )
- def test_contained_by_including_F_object(self):
- self.assertSequenceEqual(
- NullableIntegerArrayModel.objects.filter(field__contained_by=[models.F('id'), 2]),
- self.objs[:3],
- )
- def test_contains(self):
- self.assertSequenceEqual(
- NullableIntegerArrayModel.objects.filter(field__contains=[2]),
- self.objs[1:3]
- )
- def test_contains_subquery(self):
- IntegerArrayModel.objects.create(field=[2, 3])
- inner_qs = IntegerArrayModel.objects.values_list('field', flat=True)
- self.assertSequenceEqual(
- NullableIntegerArrayModel.objects.filter(field__contains=inner_qs[:1]),
- self.objs[2:3],
- )
- inner_qs = IntegerArrayModel.objects.filter(field__contains=OuterRef('field'))
- self.assertSequenceEqual(
- NullableIntegerArrayModel.objects.filter(Exists(inner_qs)),
- self.objs[1:3],
- )
- def test_contains_including_expression(self):
- self.assertSequenceEqual(
- NullableIntegerArrayModel.objects.filter(
- field__contains=[2, Value(6) / Value(2)],
- ),
- self.objs[2:3],
- )
- def test_icontains(self):
- # Using the __icontains lookup with ArrayField is inefficient.
- instance = CharArrayModel.objects.create(field=['FoO'])
- self.assertSequenceEqual(
- CharArrayModel.objects.filter(field__icontains='foo'),
- [instance]
- )
- def test_contains_charfield(self):
- # Regression for #22907
- self.assertSequenceEqual(
- CharArrayModel.objects.filter(field__contains=['text']),
- []
- )
- def test_contained_by_charfield(self):
- self.assertSequenceEqual(
- CharArrayModel.objects.filter(field__contained_by=['text']),
- []
- )
- def test_overlap_charfield(self):
- self.assertSequenceEqual(
- CharArrayModel.objects.filter(field__overlap=['text']),
- []
- )
- def test_overlap_charfield_including_expression(self):
- obj_1 = CharArrayModel.objects.create(field=['TEXT', 'lower text'])
- obj_2 = CharArrayModel.objects.create(field=['lower text', 'TEXT'])
- CharArrayModel.objects.create(field=['lower text', 'text'])
- self.assertSequenceEqual(
- CharArrayModel.objects.filter(field__overlap=[
- Upper(Value('text')),
- 'other',
- ]),
- [obj_1, obj_2],
- )
- def test_lookups_autofield_array(self):
- qs = NullableIntegerArrayModel.objects.filter(
- field__0__isnull=False,
- ).values('field__0').annotate(
- arrayagg=ArrayAgg('id'),
- ).order_by('field__0')
- tests = (
- ('contained_by', [self.objs[1].pk, self.objs[2].pk, 0], [2]),
- ('contains', [self.objs[2].pk], [2]),
- ('exact', [self.objs[3].pk], [20]),
- ('overlap', [self.objs[1].pk, self.objs[3].pk], [2, 20]),
- )
- for lookup, value, expected in tests:
- with self.subTest(lookup=lookup):
- self.assertSequenceEqual(
- qs.filter(
- **{'arrayagg__' + lookup: value},
- ).values_list('field__0', flat=True),
- expected,
- )
- def test_index(self):
- self.assertSequenceEqual(
- NullableIntegerArrayModel.objects.filter(field__0=2),
- self.objs[1:3]
- )
- def test_index_chained(self):
- self.assertSequenceEqual(
- NullableIntegerArrayModel.objects.filter(field__0__lt=3),
- self.objs[0:3]
- )
- def test_index_nested(self):
- instance = NestedIntegerArrayModel.objects.create(field=[[1, 2], [3, 4]])
- self.assertSequenceEqual(
- NestedIntegerArrayModel.objects.filter(field__0__0=1),
- [instance]
- )
- @unittest.expectedFailure
- def test_index_used_on_nested_data(self):
- instance = NestedIntegerArrayModel.objects.create(field=[[1, 2], [3, 4]])
- self.assertSequenceEqual(
- NestedIntegerArrayModel.objects.filter(field__0=[1, 2]),
- [instance]
- )
- def test_index_transform_expression(self):
- expr = RawSQL("string_to_array(%s, ';')", ['1;2'])
- self.assertSequenceEqual(
- NullableIntegerArrayModel.objects.filter(
- field__0=Cast(
- IndexTransform(1, models.IntegerField, expr),
- output_field=models.IntegerField(),
- ),
- ),
- self.objs[:1],
- )
- def test_index_annotation(self):
- qs = NullableIntegerArrayModel.objects.annotate(second=models.F('field__1'))
- self.assertCountEqual(
- qs.values_list('second', flat=True),
- [None, None, None, 3, 30],
- )
- def test_overlap(self):
- self.assertSequenceEqual(
- NullableIntegerArrayModel.objects.filter(field__overlap=[1, 2]),
- self.objs[0:3]
- )
- def test_len(self):
- self.assertSequenceEqual(
- NullableIntegerArrayModel.objects.filter(field__len__lte=2),
- self.objs[0:3]
- )
- def test_len_empty_array(self):
- obj = NullableIntegerArrayModel.objects.create(field=[])
- self.assertSequenceEqual(
- NullableIntegerArrayModel.objects.filter(field__len=0),
- [obj]
- )
- def test_slice(self):
- self.assertSequenceEqual(
- NullableIntegerArrayModel.objects.filter(field__0_1=[2]),
- self.objs[1:3]
- )
- self.assertSequenceEqual(
- NullableIntegerArrayModel.objects.filter(field__0_2=[2, 3]),
- self.objs[2:3]
- )
- def test_order_by_slice(self):
- more_objs = (
- NullableIntegerArrayModel.objects.create(field=[1, 637]),
- NullableIntegerArrayModel.objects.create(field=[2, 1]),
- NullableIntegerArrayModel.objects.create(field=[3, -98123]),
- NullableIntegerArrayModel.objects.create(field=[4, 2]),
- )
- self.assertSequenceEqual(
- NullableIntegerArrayModel.objects.order_by('field__1'),
- [
- more_objs[2], more_objs[1], more_objs[3], self.objs[2],
- self.objs[3], more_objs[0], self.objs[4], self.objs[1],
- self.objs[0],
- ]
- )
- @unittest.expectedFailure
- def test_slice_nested(self):
- instance = NestedIntegerArrayModel.objects.create(field=[[1, 2], [3, 4]])
- self.assertSequenceEqual(
- NestedIntegerArrayModel.objects.filter(field__0__0_1=[1]),
- [instance]
- )
- def test_slice_transform_expression(self):
- expr = RawSQL("string_to_array(%s, ';')", ['9;2;3'])
- self.assertSequenceEqual(
- NullableIntegerArrayModel.objects.filter(field__0_2=SliceTransform(2, 3, expr)),
- self.objs[2:3],
- )
- def test_slice_annotation(self):
- qs = NullableIntegerArrayModel.objects.annotate(
- first_two=models.F('field__0_2'),
- )
- self.assertCountEqual(
- qs.values_list('first_two', flat=True),
- [None, [1], [2], [2, 3], [20, 30]],
- )
- def test_usage_in_subquery(self):
- self.assertSequenceEqual(
- NullableIntegerArrayModel.objects.filter(
- id__in=NullableIntegerArrayModel.objects.filter(field__len=3)
- ),
- [self.objs[3]]
- )
- def test_enum_lookup(self):
- class TestEnum(enum.Enum):
- VALUE_1 = 'value_1'
- instance = ArrayEnumModel.objects.create(array_of_enums=[TestEnum.VALUE_1])
- self.assertSequenceEqual(
- ArrayEnumModel.objects.filter(array_of_enums__contains=[TestEnum.VALUE_1]),
- [instance]
- )
- def test_unsupported_lookup(self):
- msg = "Unsupported lookup '0_bar' for ArrayField or join on the field not permitted."
- with self.assertRaisesMessage(FieldError, msg):
- list(NullableIntegerArrayModel.objects.filter(field__0_bar=[2]))
- msg = "Unsupported lookup '0bar' for ArrayField or join on the field not permitted."
- with self.assertRaisesMessage(FieldError, msg):
- list(NullableIntegerArrayModel.objects.filter(field__0bar=[2]))
- def test_grouping_by_annotations_with_array_field_param(self):
- value = models.Value([1], output_field=ArrayField(models.IntegerField()))
- self.assertEqual(
- NullableIntegerArrayModel.objects.annotate(
- array_length=models.Func(
- value, 1, function='ARRAY_LENGTH', output_field=models.IntegerField(),
- ),
- ).values('array_length').annotate(
- count=models.Count('pk'),
- ).get()['array_length'],
- 1,
- )
- class TestDateTimeExactQuerying(PostgreSQLTestCase):
- @classmethod
- def setUpTestData(cls):
- now = timezone.now()
- cls.datetimes = [now]
- cls.dates = [now.date()]
- cls.times = [now.time()]
- cls.objs = [
- DateTimeArrayModel.objects.create(datetimes=cls.datetimes, dates=cls.dates, times=cls.times),
- ]
- def test_exact_datetimes(self):
- self.assertSequenceEqual(
- DateTimeArrayModel.objects.filter(datetimes=self.datetimes),
- self.objs
- )
- def test_exact_dates(self):
- self.assertSequenceEqual(
- DateTimeArrayModel.objects.filter(dates=self.dates),
- self.objs
- )
- def test_exact_times(self):
- self.assertSequenceEqual(
- DateTimeArrayModel.objects.filter(times=self.times),
- self.objs
- )
- class TestOtherTypesExactQuerying(PostgreSQLTestCase):
- @classmethod
- def setUpTestData(cls):
- cls.ips = ['192.168.0.1', '::1']
- cls.uuids = [uuid.uuid4()]
- cls.decimals = [decimal.Decimal(1.25), 1.75]
- cls.tags = [Tag(1), Tag(2), Tag(3)]
- cls.objs = [
- OtherTypesArrayModel.objects.create(
- ips=cls.ips,
- uuids=cls.uuids,
- decimals=cls.decimals,
- tags=cls.tags,
- )
- ]
- def test_exact_ip_addresses(self):
- self.assertSequenceEqual(
- OtherTypesArrayModel.objects.filter(ips=self.ips),
- self.objs
- )
- def test_exact_uuids(self):
- self.assertSequenceEqual(
- OtherTypesArrayModel.objects.filter(uuids=self.uuids),
- self.objs
- )
- def test_exact_decimals(self):
- self.assertSequenceEqual(
- OtherTypesArrayModel.objects.filter(decimals=self.decimals),
- self.objs
- )
- def test_exact_tags(self):
- self.assertSequenceEqual(
- OtherTypesArrayModel.objects.filter(tags=self.tags),
- self.objs
- )
- @isolate_apps('postgres_tests')
- class TestChecks(PostgreSQLSimpleTestCase):
- def test_field_checks(self):
- class MyModel(PostgreSQLModel):
- field = ArrayField(models.CharField())
- model = MyModel()
- errors = model.check()
- self.assertEqual(len(errors), 1)
- # The inner CharField is missing a max_length.
- self.assertEqual(errors[0].id, 'postgres.E001')
- self.assertIn('max_length', errors[0].msg)
- def test_invalid_base_fields(self):
- class MyModel(PostgreSQLModel):
- field = ArrayField(models.ManyToManyField('postgres_tests.IntegerArrayModel'))
- model = MyModel()
- errors = model.check()
- self.assertEqual(len(errors), 1)
- self.assertEqual(errors[0].id, 'postgres.E002')
- def test_invalid_default(self):
- class MyModel(PostgreSQLModel):
- field = ArrayField(models.IntegerField(), default=[])
- model = MyModel()
- self.assertEqual(model.check(), [
- checks.Warning(
- msg=(
- "ArrayField default should be a callable instead of an "
- "instance so that it's not shared between all field "
- "instances."
- ),
- hint='Use a callable instead, e.g., use `list` instead of `[]`.',
- obj=MyModel._meta.get_field('field'),
- id='fields.E010',
- )
- ])
- def test_valid_default(self):
- class MyModel(PostgreSQLModel):
- field = ArrayField(models.IntegerField(), default=list)
- model = MyModel()
- self.assertEqual(model.check(), [])
- def test_valid_default_none(self):
- class MyModel(PostgreSQLModel):
- field = ArrayField(models.IntegerField(), default=None)
- model = MyModel()
- self.assertEqual(model.check(), [])
- def test_nested_field_checks(self):
- """
- Nested ArrayFields are permitted.
- """
- class MyModel(PostgreSQLModel):
- field = ArrayField(ArrayField(models.CharField()))
- model = MyModel()
- errors = model.check()
- self.assertEqual(len(errors), 1)
- # The inner CharField is missing a max_length.
- self.assertEqual(errors[0].id, 'postgres.E001')
- self.assertIn('max_length', errors[0].msg)
- def test_choices_tuple_list(self):
- class MyModel(PostgreSQLModel):
- field = ArrayField(
- models.CharField(max_length=16),
- choices=[
- [
- 'Media',
- [(['vinyl', 'cd'], 'Audio'), (('vhs', 'dvd'), 'Video')],
- ],
- (['mp3', 'mp4'], 'Digital'),
- ],
- )
- self.assertEqual(MyModel._meta.get_field('field').check(), [])
- @unittest.skipUnless(connection.vendor == 'postgresql', "PostgreSQL specific tests")
- class TestMigrations(TransactionTestCase):
- available_apps = ['postgres_tests']
- def test_deconstruct(self):
- field = ArrayField(models.IntegerField())
- name, path, args, kwargs = field.deconstruct()
- new = ArrayField(*args, **kwargs)
- self.assertEqual(type(new.base_field), type(field.base_field))
- self.assertIsNot(new.base_field, field.base_field)
- def test_deconstruct_with_size(self):
- field = ArrayField(models.IntegerField(), size=3)
- name, path, args, kwargs = field.deconstruct()
- new = ArrayField(*args, **kwargs)
- self.assertEqual(new.size, field.size)
- def test_deconstruct_args(self):
- field = ArrayField(models.CharField(max_length=20))
- name, path, args, kwargs = field.deconstruct()
- new = ArrayField(*args, **kwargs)
- self.assertEqual(new.base_field.max_length, field.base_field.max_length)
- def test_subclass_deconstruct(self):
- field = ArrayField(models.IntegerField())
- name, path, args, kwargs = field.deconstruct()
- self.assertEqual(path, 'django.contrib.postgres.fields.ArrayField')
- field = ArrayFieldSubclass()
- name, path, args, kwargs = field.deconstruct()
- self.assertEqual(path, 'postgres_tests.models.ArrayFieldSubclass')
- @override_settings(MIGRATION_MODULES={
- "postgres_tests": "postgres_tests.array_default_migrations",
- })
- def test_adding_field_with_default(self):
- # See #22962
- table_name = 'postgres_tests_integerarraydefaultmodel'
- with connection.cursor() as cursor:
- self.assertNotIn(table_name, connection.introspection.table_names(cursor))
- call_command('migrate', 'postgres_tests', verbosity=0)
- with connection.cursor() as cursor:
- self.assertIn(table_name, connection.introspection.table_names(cursor))
- call_command('migrate', 'postgres_tests', 'zero', verbosity=0)
- with connection.cursor() as cursor:
- self.assertNotIn(table_name, connection.introspection.table_names(cursor))
- @override_settings(MIGRATION_MODULES={
- "postgres_tests": "postgres_tests.array_index_migrations",
- })
- def test_adding_arrayfield_with_index(self):
- """
- ArrayField shouldn't have varchar_patterns_ops or text_patterns_ops indexes.
- """
- table_name = 'postgres_tests_chartextarrayindexmodel'
- call_command('migrate', 'postgres_tests', verbosity=0)
- with connection.cursor() as cursor:
- like_constraint_columns_list = [
- v['columns']
- for k, v in list(connection.introspection.get_constraints(cursor, table_name).items())
- if k.endswith('_like')
- ]
- # Only the CharField should have a LIKE index.
- self.assertEqual(like_constraint_columns_list, [['char2']])
- # All fields should have regular indexes.
- with connection.cursor() as cursor:
- indexes = [
- c['columns'][0]
- for c in connection.introspection.get_constraints(cursor, table_name).values()
- if c['index'] and len(c['columns']) == 1
- ]
- self.assertIn('char', indexes)
- self.assertIn('char2', indexes)
- self.assertIn('text', indexes)
- call_command('migrate', 'postgres_tests', 'zero', verbosity=0)
- with connection.cursor() as cursor:
- self.assertNotIn(table_name, connection.introspection.table_names(cursor))
- class TestSerialization(PostgreSQLSimpleTestCase):
- test_data = (
- '[{"fields": {"field": "[\\"1\\", \\"2\\", null]"}, "model": "postgres_tests.integerarraymodel", "pk": null}]'
- )
- def test_dumping(self):
- instance = IntegerArrayModel(field=[1, 2, None])
- data = serializers.serialize('json', [instance])
- self.assertEqual(json.loads(data), json.loads(self.test_data))
- def test_loading(self):
- instance = list(serializers.deserialize('json', self.test_data))[0].object
- self.assertEqual(instance.field, [1, 2, None])
- class TestValidation(PostgreSQLSimpleTestCase):
- def test_unbounded(self):
- field = ArrayField(models.IntegerField())
- with self.assertRaises(exceptions.ValidationError) as cm:
- field.clean([1, None], None)
- self.assertEqual(cm.exception.code, 'item_invalid')
- self.assertEqual(
- cm.exception.message % cm.exception.params,
- 'Item 2 in the array did not validate: This field cannot be null.'
- )
- def test_blank_true(self):
- field = ArrayField(models.IntegerField(blank=True, null=True))
- # This should not raise a validation error
- field.clean([1, None], None)
- def test_with_size(self):
- field = ArrayField(models.IntegerField(), size=3)
- field.clean([1, 2, 3], None)
- with self.assertRaises(exceptions.ValidationError) as cm:
- field.clean([1, 2, 3, 4], None)
- self.assertEqual(cm.exception.messages[0], 'List contains 4 items, it should contain no more than 3.')
- def test_nested_array_mismatch(self):
- field = ArrayField(ArrayField(models.IntegerField()))
- field.clean([[1, 2], [3, 4]], None)
- with self.assertRaises(exceptions.ValidationError) as cm:
- field.clean([[1, 2], [3, 4, 5]], None)
- self.assertEqual(cm.exception.code, 'nested_array_mismatch')
- self.assertEqual(cm.exception.messages[0], 'Nested arrays must have the same length.')
- def test_with_base_field_error_params(self):
- field = ArrayField(models.CharField(max_length=2))
- with self.assertRaises(exceptions.ValidationError) as cm:
- field.clean(['abc'], None)
- self.assertEqual(len(cm.exception.error_list), 1)
- exception = cm.exception.error_list[0]
- self.assertEqual(
- exception.message,
- 'Item 1 in the array did not validate: Ensure this value has at most 2 characters (it has 3).'
- )
- self.assertEqual(exception.code, 'item_invalid')
- self.assertEqual(exception.params, {'nth': 1, 'value': 'abc', 'limit_value': 2, 'show_value': 3})
- def test_with_validators(self):
- field = ArrayField(models.IntegerField(validators=[validators.MinValueValidator(1)]))
- field.clean([1, 2], None)
- with self.assertRaises(exceptions.ValidationError) as cm:
- field.clean([0], None)
- self.assertEqual(len(cm.exception.error_list), 1)
- exception = cm.exception.error_list[0]
- self.assertEqual(
- exception.message,
- 'Item 1 in the array did not validate: Ensure this value is greater than or equal to 1.'
- )
- self.assertEqual(exception.code, 'item_invalid')
- self.assertEqual(exception.params, {'nth': 1, 'value': 0, 'limit_value': 1, 'show_value': 0})
- class TestSimpleFormField(PostgreSQLSimpleTestCase):
- def test_valid(self):
- field = SimpleArrayField(forms.CharField())
- value = field.clean('a,b,c')
- self.assertEqual(value, ['a', 'b', 'c'])
- def test_to_python_fail(self):
- field = SimpleArrayField(forms.IntegerField())
- with self.assertRaises(exceptions.ValidationError) as cm:
- field.clean('a,b,9')
- self.assertEqual(cm.exception.messages[0], 'Item 1 in the array did not validate: Enter a whole number.')
- def test_validate_fail(self):
- field = SimpleArrayField(forms.CharField(required=True))
- with self.assertRaises(exceptions.ValidationError) as cm:
- field.clean('a,b,')
- self.assertEqual(cm.exception.messages[0], 'Item 3 in the array did not validate: This field is required.')
- def test_validate_fail_base_field_error_params(self):
- field = SimpleArrayField(forms.CharField(max_length=2))
- with self.assertRaises(exceptions.ValidationError) as cm:
- field.clean('abc,c,defg')
- errors = cm.exception.error_list
- self.assertEqual(len(errors), 2)
- first_error = errors[0]
- self.assertEqual(
- first_error.message,
- 'Item 1 in the array did not validate: Ensure this value has at most 2 characters (it has 3).'
- )
- self.assertEqual(first_error.code, 'item_invalid')
- self.assertEqual(first_error.params, {'nth': 1, 'value': 'abc', 'limit_value': 2, 'show_value': 3})
- second_error = errors[1]
- self.assertEqual(
- second_error.message,
- 'Item 3 in the array did not validate: Ensure this value has at most 2 characters (it has 4).'
- )
- self.assertEqual(second_error.code, 'item_invalid')
- self.assertEqual(second_error.params, {'nth': 3, 'value': 'defg', 'limit_value': 2, 'show_value': 4})
- def test_validators_fail(self):
- field = SimpleArrayField(forms.RegexField('[a-e]{2}'))
- with self.assertRaises(exceptions.ValidationError) as cm:
- field.clean('a,bc,de')
- self.assertEqual(cm.exception.messages[0], 'Item 1 in the array did not validate: Enter a valid value.')
- def test_delimiter(self):
- field = SimpleArrayField(forms.CharField(), delimiter='|')
- value = field.clean('a|b|c')
- self.assertEqual(value, ['a', 'b', 'c'])
- def test_delimiter_with_nesting(self):
- field = SimpleArrayField(SimpleArrayField(forms.CharField()), delimiter='|')
- value = field.clean('a,b|c,d')
- self.assertEqual(value, [['a', 'b'], ['c', 'd']])
- def test_prepare_value(self):
- field = SimpleArrayField(forms.CharField())
- value = field.prepare_value(['a', 'b', 'c'])
- self.assertEqual(value, 'a,b,c')
- def test_max_length(self):
- field = SimpleArrayField(forms.CharField(), max_length=2)
- with self.assertRaises(exceptions.ValidationError) as cm:
- field.clean('a,b,c')
- self.assertEqual(cm.exception.messages[0], 'List contains 3 items, it should contain no more than 2.')
- def test_min_length(self):
- field = SimpleArrayField(forms.CharField(), min_length=4)
- with self.assertRaises(exceptions.ValidationError) as cm:
- field.clean('a,b,c')
- self.assertEqual(cm.exception.messages[0], 'List contains 3 items, it should contain no fewer than 4.')
- def test_required(self):
- field = SimpleArrayField(forms.CharField(), required=True)
- with self.assertRaises(exceptions.ValidationError) as cm:
- field.clean('')
- self.assertEqual(cm.exception.messages[0], 'This field is required.')
- def test_model_field_formfield(self):
- model_field = ArrayField(models.CharField(max_length=27))
- form_field = model_field.formfield()
- self.assertIsInstance(form_field, SimpleArrayField)
- self.assertIsInstance(form_field.base_field, forms.CharField)
- self.assertEqual(form_field.base_field.max_length, 27)
- def test_model_field_formfield_size(self):
- model_field = ArrayField(models.CharField(max_length=27), size=4)
- form_field = model_field.formfield()
- self.assertIsInstance(form_field, SimpleArrayField)
- self.assertEqual(form_field.max_length, 4)
- def test_model_field_choices(self):
- model_field = ArrayField(models.IntegerField(choices=((1, 'A'), (2, 'B'))))
- form_field = model_field.formfield()
- self.assertEqual(form_field.clean('1,2'), [1, 2])
- def test_already_converted_value(self):
- field = SimpleArrayField(forms.CharField())
- vals = ['a', 'b', 'c']
- self.assertEqual(field.clean(vals), vals)
- def test_has_changed(self):
- field = SimpleArrayField(forms.IntegerField())
- self.assertIs(field.has_changed([1, 2], [1, 2]), False)
- self.assertIs(field.has_changed([1, 2], '1,2'), False)
- self.assertIs(field.has_changed([1, 2], '1,2,3'), True)
- self.assertIs(field.has_changed([1, 2], 'a,b'), True)
- def test_has_changed_empty(self):
- field = SimpleArrayField(forms.CharField())
- self.assertIs(field.has_changed(None, None), False)
- self.assertIs(field.has_changed(None, ''), False)
- self.assertIs(field.has_changed(None, []), False)
- self.assertIs(field.has_changed([], None), False)
- self.assertIs(field.has_changed([], ''), False)
- class TestSplitFormField(PostgreSQLSimpleTestCase):
- def test_valid(self):
- class SplitForm(forms.Form):
- array = SplitArrayField(forms.CharField(), size=3)
- data = {'array_0': 'a', 'array_1': 'b', 'array_2': 'c'}
- form = SplitForm(data)
- self.assertTrue(form.is_valid())
- self.assertEqual(form.cleaned_data, {'array': ['a', 'b', 'c']})
- def test_required(self):
- class SplitForm(forms.Form):
- array = SplitArrayField(forms.CharField(), required=True, size=3)
- data = {'array_0': '', 'array_1': '', 'array_2': ''}
- form = SplitForm(data)
- self.assertFalse(form.is_valid())
- self.assertEqual(form.errors, {'array': ['This field is required.']})
- def test_remove_trailing_nulls(self):
- class SplitForm(forms.Form):
- array = SplitArrayField(forms.CharField(required=False), size=5, remove_trailing_nulls=True)
- data = {'array_0': 'a', 'array_1': '', 'array_2': 'b', 'array_3': '', 'array_4': ''}
- form = SplitForm(data)
- self.assertTrue(form.is_valid(), form.errors)
- self.assertEqual(form.cleaned_data, {'array': ['a', '', 'b']})
- def test_remove_trailing_nulls_not_required(self):
- class SplitForm(forms.Form):
- array = SplitArrayField(
- forms.CharField(required=False),
- size=2,
- remove_trailing_nulls=True,
- required=False,
- )
- data = {'array_0': '', 'array_1': ''}
- form = SplitForm(data)
- self.assertTrue(form.is_valid())
- self.assertEqual(form.cleaned_data, {'array': []})
- def test_required_field(self):
- class SplitForm(forms.Form):
- array = SplitArrayField(forms.CharField(), size=3)
- data = {'array_0': 'a', 'array_1': 'b', 'array_2': ''}
- form = SplitForm(data)
- self.assertFalse(form.is_valid())
- self.assertEqual(form.errors, {'array': ['Item 3 in the array did not validate: This field is required.']})
- def test_invalid_integer(self):
- msg = 'Item 2 in the array did not validate: Ensure this value is less than or equal to 100.'
- with self.assertRaisesMessage(exceptions.ValidationError, msg):
- SplitArrayField(forms.IntegerField(max_value=100), size=2).clean([0, 101])
- # To locate the widget's template.
- @modify_settings(INSTALLED_APPS={'append': 'django.contrib.postgres'})
- def test_rendering(self):
- class SplitForm(forms.Form):
- array = SplitArrayField(forms.CharField(), size=3)
- self.assertHTMLEqual(str(SplitForm()), '''
- <tr>
- <th><label for="id_array_0">Array:</label></th>
- <td>
- <input id="id_array_0" name="array_0" type="text" required>
- <input id="id_array_1" name="array_1" type="text" required>
- <input id="id_array_2" name="array_2" type="text" required>
- </td>
- </tr>
- ''')
- def test_invalid_char_length(self):
- field = SplitArrayField(forms.CharField(max_length=2), size=3)
- with self.assertRaises(exceptions.ValidationError) as cm:
- field.clean(['abc', 'c', 'defg'])
- self.assertEqual(cm.exception.messages, [
- 'Item 1 in the array did not validate: Ensure this value has at most 2 characters (it has 3).',
- 'Item 3 in the array did not validate: Ensure this value has at most 2 characters (it has 4).',
- ])
- def test_splitarraywidget_value_omitted_from_data(self):
- class Form(forms.ModelForm):
- field = SplitArrayField(forms.IntegerField(), required=False, size=2)
- class Meta:
- model = IntegerArrayModel
- fields = ('field',)
- form = Form({'field_0': '1', 'field_1': '2'})
- self.assertEqual(form.errors, {})
- obj = form.save(commit=False)
- self.assertEqual(obj.field, [1, 2])
- def test_splitarrayfield_has_changed(self):
- class Form(forms.ModelForm):
- field = SplitArrayField(forms.IntegerField(), required=False, size=2)
- class Meta:
- model = IntegerArrayModel
- fields = ('field',)
- tests = [
- ({}, {'field_0': '', 'field_1': ''}, True),
- ({'field': None}, {'field_0': '', 'field_1': ''}, True),
- ({'field': [1]}, {'field_0': '', 'field_1': ''}, True),
- ({'field': [1]}, {'field_0': '1', 'field_1': '0'}, True),
- ({'field': [1, 2]}, {'field_0': '1', 'field_1': '2'}, False),
- ({'field': [1, 2]}, {'field_0': 'a', 'field_1': 'b'}, True),
- ]
- for initial, data, expected_result in tests:
- with self.subTest(initial=initial, data=data):
- obj = IntegerArrayModel(**initial)
- form = Form(data, instance=obj)
- self.assertIs(form.has_changed(), expected_result)
- def test_splitarrayfield_remove_trailing_nulls_has_changed(self):
- class Form(forms.ModelForm):
- field = SplitArrayField(forms.IntegerField(), required=False, size=2, remove_trailing_nulls=True)
- class Meta:
- model = IntegerArrayModel
- fields = ('field',)
- tests = [
- ({}, {'field_0': '', 'field_1': ''}, False),
- ({'field': None}, {'field_0': '', 'field_1': ''}, False),
- ({'field': []}, {'field_0': '', 'field_1': ''}, False),
- ({'field': [1]}, {'field_0': '1', 'field_1': ''}, False),
- ]
- for initial, data, expected_result in tests:
- with self.subTest(initial=initial, data=data):
- obj = IntegerArrayModel(**initial)
- form = Form(data, instance=obj)
- self.assertIs(form.has_changed(), expected_result)
- class TestSplitFormWidget(PostgreSQLWidgetTestCase):
- def test_get_context(self):
- self.assertEqual(
- SplitArrayWidget(forms.TextInput(), size=2).get_context('name', ['val1', 'val2']),
- {
- 'widget': {
- 'name': 'name',
- 'is_hidden': False,
- 'required': False,
- 'value': "['val1', 'val2']",
- 'attrs': {},
- 'template_name': 'postgres/widgets/split_array.html',
- 'subwidgets': [
- {
- 'name': 'name_0',
- 'is_hidden': False,
- 'required': False,
- 'value': 'val1',
- 'attrs': {},
- 'template_name': 'django/forms/widgets/text.html',
- 'type': 'text',
- },
- {
- 'name': 'name_1',
- 'is_hidden': False,
- 'required': False,
- 'value': 'val2',
- 'attrs': {},
- 'template_name': 'django/forms/widgets/text.html',
- 'type': 'text',
- },
- ]
- }
- }
- )
- 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,
- """
- <input name="array_0" type="text">
- <input name="array_1" type="text">
- """
- )
- def test_render_attrs(self):
- self.check_html(
- SplitArrayWidget(forms.TextInput(), size=2),
- 'array', ['val1', 'val2'], attrs={'id': 'foo'},
- html=(
- """
- <input id="foo_0" name="array_0" type="text" value="val1">
- <input id="foo_1" name="array_1" type="text" value="val2">
- """
- )
- )
- def test_value_omitted_from_data(self):
- widget = SplitArrayWidget(forms.TextInput(), size=2)
- self.assertIs(widget.value_omitted_from_data({}, {}, 'field'), True)
- self.assertIs(widget.value_omitted_from_data({'field_0': 'value'}, {}, 'field'), False)
- self.assertIs(widget.value_omitted_from_data({'field_1': 'value'}, {}, 'field'), False)
- self.assertIs(widget.value_omitted_from_data({'field_0': 'value', 'field_1': 'value'}, {}, 'field'), False)
|