1from django.contrib.contenttypes.models import ContentType
2from django.core.exceptions import ValidationError
3from django.urls import reverse
4from rest_framework import status
5
6from dcim.filtersets import SiteFilterSet
7from dcim.forms import SiteCSVForm
8from dcim.models import Site, Rack
9from extras.choices import *
10from extras.models import CustomField
11from utilities.testing import APITestCase, TestCase
12from virtualization.models import VirtualMachine
13
14
15class CustomFieldTest(TestCase):
16
17    def setUp(self):
18
19        Site.objects.bulk_create([
20            Site(name='Site A', slug='site-a'),
21            Site(name='Site B', slug='site-b'),
22            Site(name='Site C', slug='site-c'),
23        ])
24
25    def test_simple_fields(self):
26        DATA = (
27            {'field_type': CustomFieldTypeChoices.TYPE_TEXT, 'field_value': 'Foobar!', 'empty_value': ''},
28            {'field_type': CustomFieldTypeChoices.TYPE_INTEGER, 'field_value': 0, 'empty_value': None},
29            {'field_type': CustomFieldTypeChoices.TYPE_INTEGER, 'field_value': 42, 'empty_value': None},
30            {'field_type': CustomFieldTypeChoices.TYPE_BOOLEAN, 'field_value': True, 'empty_value': None},
31            {'field_type': CustomFieldTypeChoices.TYPE_BOOLEAN, 'field_value': False, 'empty_value': None},
32            {'field_type': CustomFieldTypeChoices.TYPE_DATE, 'field_value': '2016-06-23', 'empty_value': None},
33            {'field_type': CustomFieldTypeChoices.TYPE_URL, 'field_value': 'http://example.com/', 'empty_value': ''},
34        )
35
36        obj_type = ContentType.objects.get_for_model(Site)
37
38        for data in DATA:
39
40            # Create a custom field
41            cf = CustomField(type=data['field_type'], name='my_field', required=False)
42            cf.save()
43            cf.content_types.set([obj_type])
44
45            # Check that the field has a null initial value
46            site = Site.objects.first()
47            self.assertIsNone(site.custom_field_data[cf.name])
48
49            # Assign a value to the first Site
50            site.custom_field_data[cf.name] = data['field_value']
51            site.save()
52
53            # Retrieve the stored value
54            site.refresh_from_db()
55            self.assertEqual(site.custom_field_data[cf.name], data['field_value'])
56
57            # Delete the stored value
58            site.custom_field_data.pop(cf.name)
59            site.save()
60            site.refresh_from_db()
61            self.assertIsNone(site.custom_field_data.get(cf.name))
62
63            # Delete the custom field
64            cf.delete()
65
66    def test_select_field(self):
67        obj_type = ContentType.objects.get_for_model(Site)
68
69        # Create a custom field
70        cf = CustomField(
71            type=CustomFieldTypeChoices.TYPE_SELECT,
72            name='my_field',
73            required=False,
74            choices=['Option A', 'Option B', 'Option C']
75        )
76        cf.save()
77        cf.content_types.set([obj_type])
78
79        # Check that the field has a null initial value
80        site = Site.objects.first()
81        self.assertIsNone(site.custom_field_data[cf.name])
82
83        # Assign a value to the first Site
84        site.custom_field_data[cf.name] = 'Option A'
85        site.save()
86
87        # Retrieve the stored value
88        site.refresh_from_db()
89        self.assertEqual(site.custom_field_data[cf.name], 'Option A')
90
91        # Delete the stored value
92        site.custom_field_data.pop(cf.name)
93        site.save()
94        site.refresh_from_db()
95        self.assertIsNone(site.custom_field_data.get(cf.name))
96
97        # Delete the custom field
98        cf.delete()
99
100    def test_rename_customfield(self):
101        obj_type = ContentType.objects.get_for_model(Site)
102        FIELD_DATA = 'abc'
103
104        # Create a custom field
105        cf = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='field1')
106        cf.save()
107        cf.content_types.set([obj_type])
108
109        # Assign custom field data to an object
110        site = Site.objects.create(
111            name='Site 1',
112            slug='site-1',
113            custom_field_data={'field1': FIELD_DATA}
114        )
115        site.refresh_from_db()
116        self.assertEqual(site.custom_field_data['field1'], FIELD_DATA)
117
118        # Rename the custom field
119        cf.name = 'field2'
120        cf.save()
121
122        # Check that custom field data on the object has been updated
123        site.refresh_from_db()
124        self.assertNotIn('field1', site.custom_field_data)
125        self.assertEqual(site.custom_field_data['field2'], FIELD_DATA)
126
127
128class CustomFieldManagerTest(TestCase):
129
130    def setUp(self):
131        content_type = ContentType.objects.get_for_model(Site)
132        custom_field = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='text_field', default='foo')
133        custom_field.save()
134        custom_field.content_types.set([content_type])
135
136    def test_get_for_model(self):
137        self.assertEqual(CustomField.objects.get_for_model(Site).count(), 1)
138        self.assertEqual(CustomField.objects.get_for_model(VirtualMachine).count(), 0)
139
140
141class CustomFieldAPITest(APITestCase):
142
143    @classmethod
144    def setUpTestData(cls):
145        content_type = ContentType.objects.get_for_model(Site)
146
147        # Text custom field
148        cls.cf_text = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='text_field', default='foo')
149        cls.cf_text.save()
150        cls.cf_text.content_types.set([content_type])
151
152        # Integer custom field
153        cls.cf_integer = CustomField(type=CustomFieldTypeChoices.TYPE_INTEGER, name='number_field', default=123)
154        cls.cf_integer.save()
155        cls.cf_integer.content_types.set([content_type])
156
157        # Boolean custom field
158        cls.cf_boolean = CustomField(type=CustomFieldTypeChoices.TYPE_BOOLEAN, name='boolean_field', default=False)
159        cls.cf_boolean.save()
160        cls.cf_boolean.content_types.set([content_type])
161
162        # Date custom field
163        cls.cf_date = CustomField(type=CustomFieldTypeChoices.TYPE_DATE, name='date_field', default='2020-01-01')
164        cls.cf_date.save()
165        cls.cf_date.content_types.set([content_type])
166
167        # URL custom field
168        cls.cf_url = CustomField(type=CustomFieldTypeChoices.TYPE_URL, name='url_field', default='http://example.com/1')
169        cls.cf_url.save()
170        cls.cf_url.content_types.set([content_type])
171
172        # Select custom field
173        cls.cf_select = CustomField(type=CustomFieldTypeChoices.TYPE_SELECT, name='choice_field', choices=['Foo', 'Bar', 'Baz'])
174        cls.cf_select.default = 'Foo'
175        cls.cf_select.save()
176        cls.cf_select.content_types.set([content_type])
177
178        # Create some sites
179        cls.sites = (
180            Site(name='Site 1', slug='site-1'),
181            Site(name='Site 2', slug='site-2'),
182        )
183        Site.objects.bulk_create(cls.sites)
184
185        # Assign custom field values for site 2
186        cls.sites[1].custom_field_data = {
187            cls.cf_text.name: 'bar',
188            cls.cf_integer.name: 456,
189            cls.cf_boolean.name: True,
190            cls.cf_date.name: '2020-01-02',
191            cls.cf_url.name: 'http://example.com/2',
192            cls.cf_select.name: 'Bar',
193        }
194        cls.sites[1].save()
195
196    def test_get_single_object_without_custom_field_data(self):
197        """
198        Validate that custom fields are present on an object even if it has no values defined.
199        """
200        url = reverse('dcim-api:site-detail', kwargs={'pk': self.sites[0].pk})
201        self.add_permissions('dcim.view_site')
202
203        response = self.client.get(url, **self.header)
204        self.assertEqual(response.data['name'], self.sites[0].name)
205        self.assertEqual(response.data['custom_fields'], {
206            'text_field': None,
207            'number_field': None,
208            'boolean_field': None,
209            'date_field': None,
210            'url_field': None,
211            'choice_field': None,
212        })
213
214    def test_get_single_object_with_custom_field_data(self):
215        """
216        Validate that custom fields are present and correctly set for an object with values defined.
217        """
218        site2_cfvs = self.sites[1].custom_field_data
219        url = reverse('dcim-api:site-detail', kwargs={'pk': self.sites[1].pk})
220        self.add_permissions('dcim.view_site')
221
222        response = self.client.get(url, **self.header)
223        self.assertEqual(response.data['name'], self.sites[1].name)
224        self.assertEqual(response.data['custom_fields']['text_field'], site2_cfvs['text_field'])
225        self.assertEqual(response.data['custom_fields']['number_field'], site2_cfvs['number_field'])
226        self.assertEqual(response.data['custom_fields']['boolean_field'], site2_cfvs['boolean_field'])
227        self.assertEqual(response.data['custom_fields']['date_field'], site2_cfvs['date_field'])
228        self.assertEqual(response.data['custom_fields']['url_field'], site2_cfvs['url_field'])
229        self.assertEqual(response.data['custom_fields']['choice_field'], site2_cfvs['choice_field'])
230
231    def test_create_single_object_with_defaults(self):
232        """
233        Create a new site with no specified custom field values and check that it received the default values.
234        """
235        data = {
236            'name': 'Site 3',
237            'slug': 'site-3',
238        }
239        url = reverse('dcim-api:site-list')
240        self.add_permissions('dcim.add_site')
241
242        response = self.client.post(url, data, format='json', **self.header)
243        self.assertHttpStatus(response, status.HTTP_201_CREATED)
244
245        # Validate response data
246        response_cf = response.data['custom_fields']
247        self.assertEqual(response_cf['text_field'], self.cf_text.default)
248        self.assertEqual(response_cf['number_field'], self.cf_integer.default)
249        self.assertEqual(response_cf['boolean_field'], self.cf_boolean.default)
250        self.assertEqual(response_cf['date_field'], self.cf_date.default)
251        self.assertEqual(response_cf['url_field'], self.cf_url.default)
252        self.assertEqual(response_cf['choice_field'], self.cf_select.default)
253
254        # Validate database data
255        site = Site.objects.get(pk=response.data['id'])
256        self.assertEqual(site.custom_field_data['text_field'], self.cf_text.default)
257        self.assertEqual(site.custom_field_data['number_field'], self.cf_integer.default)
258        self.assertEqual(site.custom_field_data['boolean_field'], self.cf_boolean.default)
259        self.assertEqual(str(site.custom_field_data['date_field']), self.cf_date.default)
260        self.assertEqual(site.custom_field_data['url_field'], self.cf_url.default)
261        self.assertEqual(site.custom_field_data['choice_field'], self.cf_select.default)
262
263    def test_create_single_object_with_values(self):
264        """
265        Create a single new site with a value for each type of custom field.
266        """
267        data = {
268            'name': 'Site 3',
269            'slug': 'site-3',
270            'custom_fields': {
271                'text_field': 'bar',
272                'number_field': 456,
273                'boolean_field': True,
274                'date_field': '2020-01-02',
275                'url_field': 'http://example.com/2',
276                'choice_field': 'Bar',
277            },
278        }
279        url = reverse('dcim-api:site-list')
280        self.add_permissions('dcim.add_site')
281
282        response = self.client.post(url, data, format='json', **self.header)
283        self.assertHttpStatus(response, status.HTTP_201_CREATED)
284
285        # Validate response data
286        response_cf = response.data['custom_fields']
287        data_cf = data['custom_fields']
288        self.assertEqual(response_cf['text_field'], data_cf['text_field'])
289        self.assertEqual(response_cf['number_field'], data_cf['number_field'])
290        self.assertEqual(response_cf['boolean_field'], data_cf['boolean_field'])
291        self.assertEqual(response_cf['date_field'], data_cf['date_field'])
292        self.assertEqual(response_cf['url_field'], data_cf['url_field'])
293        self.assertEqual(response_cf['choice_field'], data_cf['choice_field'])
294
295        # Validate database data
296        site = Site.objects.get(pk=response.data['id'])
297        self.assertEqual(site.custom_field_data['text_field'], data_cf['text_field'])
298        self.assertEqual(site.custom_field_data['number_field'], data_cf['number_field'])
299        self.assertEqual(site.custom_field_data['boolean_field'], data_cf['boolean_field'])
300        self.assertEqual(str(site.custom_field_data['date_field']), data_cf['date_field'])
301        self.assertEqual(site.custom_field_data['url_field'], data_cf['url_field'])
302        self.assertEqual(site.custom_field_data['choice_field'], data_cf['choice_field'])
303
304    def test_create_multiple_objects_with_defaults(self):
305        """
306        Create three news sites with no specified custom field values and check that each received
307        the default custom field values.
308        """
309        data = (
310            {
311                'name': 'Site 3',
312                'slug': 'site-3',
313            },
314            {
315                'name': 'Site 4',
316                'slug': 'site-4',
317            },
318            {
319                'name': 'Site 5',
320                'slug': 'site-5',
321            },
322        )
323        url = reverse('dcim-api:site-list')
324        self.add_permissions('dcim.add_site')
325
326        response = self.client.post(url, data, format='json', **self.header)
327        self.assertHttpStatus(response, status.HTTP_201_CREATED)
328        self.assertEqual(len(response.data), len(data))
329
330        for i, obj in enumerate(data):
331
332            # Validate response data
333            response_cf = response.data[i]['custom_fields']
334            self.assertEqual(response_cf['text_field'], self.cf_text.default)
335            self.assertEqual(response_cf['number_field'], self.cf_integer.default)
336            self.assertEqual(response_cf['boolean_field'], self.cf_boolean.default)
337            self.assertEqual(response_cf['date_field'], self.cf_date.default)
338            self.assertEqual(response_cf['url_field'], self.cf_url.default)
339            self.assertEqual(response_cf['choice_field'], self.cf_select.default)
340
341            # Validate database data
342            site = Site.objects.get(pk=response.data[i]['id'])
343            self.assertEqual(site.custom_field_data['text_field'], self.cf_text.default)
344            self.assertEqual(site.custom_field_data['number_field'], self.cf_integer.default)
345            self.assertEqual(site.custom_field_data['boolean_field'], self.cf_boolean.default)
346            self.assertEqual(str(site.custom_field_data['date_field']), self.cf_date.default)
347            self.assertEqual(site.custom_field_data['url_field'], self.cf_url.default)
348            self.assertEqual(site.custom_field_data['choice_field'], self.cf_select.default)
349
350    def test_create_multiple_objects_with_values(self):
351        """
352        Create a three new sites, each with custom fields defined.
353        """
354        custom_field_data = {
355            'text_field': 'bar',
356            'number_field': 456,
357            'boolean_field': True,
358            'date_field': '2020-01-02',
359            'url_field': 'http://example.com/2',
360            'choice_field': 'Bar',
361        }
362        data = (
363            {
364                'name': 'Site 3',
365                'slug': 'site-3',
366                'custom_fields': custom_field_data,
367            },
368            {
369                'name': 'Site 4',
370                'slug': 'site-4',
371                'custom_fields': custom_field_data,
372            },
373            {
374                'name': 'Site 5',
375                'slug': 'site-5',
376                'custom_fields': custom_field_data,
377            },
378        )
379        url = reverse('dcim-api:site-list')
380        self.add_permissions('dcim.add_site')
381
382        response = self.client.post(url, data, format='json', **self.header)
383        self.assertHttpStatus(response, status.HTTP_201_CREATED)
384        self.assertEqual(len(response.data), len(data))
385
386        for i, obj in enumerate(data):
387
388            # Validate response data
389            response_cf = response.data[i]['custom_fields']
390            self.assertEqual(response_cf['text_field'], custom_field_data['text_field'])
391            self.assertEqual(response_cf['number_field'], custom_field_data['number_field'])
392            self.assertEqual(response_cf['boolean_field'], custom_field_data['boolean_field'])
393            self.assertEqual(response_cf['date_field'], custom_field_data['date_field'])
394            self.assertEqual(response_cf['url_field'], custom_field_data['url_field'])
395            self.assertEqual(response_cf['choice_field'], custom_field_data['choice_field'])
396
397            # Validate database data
398            site = Site.objects.get(pk=response.data[i]['id'])
399            self.assertEqual(site.custom_field_data['text_field'], custom_field_data['text_field'])
400            self.assertEqual(site.custom_field_data['number_field'], custom_field_data['number_field'])
401            self.assertEqual(site.custom_field_data['boolean_field'], custom_field_data['boolean_field'])
402            self.assertEqual(str(site.custom_field_data['date_field']), custom_field_data['date_field'])
403            self.assertEqual(site.custom_field_data['url_field'], custom_field_data['url_field'])
404            self.assertEqual(site.custom_field_data['choice_field'], custom_field_data['choice_field'])
405
406    def test_update_single_object_with_values(self):
407        """
408        Update an object with existing custom field values. Ensure that only the updated custom field values are
409        modified.
410        """
411        site = self.sites[1]
412        original_cfvs = {**site.custom_field_data}
413        data = {
414            'custom_fields': {
415                'text_field': 'ABCD',
416                'number_field': 1234,
417            },
418        }
419        url = reverse('dcim-api:site-detail', kwargs={'pk': self.sites[1].pk})
420        self.add_permissions('dcim.change_site')
421
422        response = self.client.patch(url, data, format='json', **self.header)
423        self.assertHttpStatus(response, status.HTTP_200_OK)
424
425        # Validate response data
426        response_cf = response.data['custom_fields']
427        self.assertEqual(response_cf['text_field'], data['custom_fields']['text_field'])
428        self.assertEqual(response_cf['number_field'], data['custom_fields']['number_field'])
429        self.assertEqual(response_cf['boolean_field'], original_cfvs['boolean_field'])
430        self.assertEqual(response_cf['date_field'], original_cfvs['date_field'])
431        self.assertEqual(response_cf['url_field'], original_cfvs['url_field'])
432        self.assertEqual(response_cf['choice_field'], original_cfvs['choice_field'])
433
434        # Validate database data
435        site.refresh_from_db()
436        self.assertEqual(site.custom_field_data['text_field'], data['custom_fields']['text_field'])
437        self.assertEqual(site.custom_field_data['number_field'], data['custom_fields']['number_field'])
438        self.assertEqual(site.custom_field_data['boolean_field'], original_cfvs['boolean_field'])
439        self.assertEqual(site.custom_field_data['date_field'], original_cfvs['date_field'])
440        self.assertEqual(site.custom_field_data['url_field'], original_cfvs['url_field'])
441        self.assertEqual(site.custom_field_data['choice_field'], original_cfvs['choice_field'])
442
443    def test_minimum_maximum_values_validation(self):
444        url = reverse('dcim-api:site-detail', kwargs={'pk': self.sites[1].pk})
445        self.add_permissions('dcim.change_site')
446
447        self.cf_integer.validation_minimum = 10
448        self.cf_integer.validation_maximum = 20
449        self.cf_integer.save()
450
451        data = {'custom_fields': {'number_field': 9}}
452        response = self.client.patch(url, data, format='json', **self.header)
453        self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
454
455        data = {'custom_fields': {'number_field': 21}}
456        response = self.client.patch(url, data, format='json', **self.header)
457        self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
458
459        data = {'custom_fields': {'number_field': 15}}
460        response = self.client.patch(url, data, format='json', **self.header)
461        self.assertHttpStatus(response, status.HTTP_200_OK)
462
463    def test_regex_validation(self):
464        url = reverse('dcim-api:site-detail', kwargs={'pk': self.sites[1].pk})
465        self.add_permissions('dcim.change_site')
466
467        self.cf_text.validation_regex = r'^[A-Z]{3}$'  # Three uppercase letters
468        self.cf_text.save()
469
470        data = {'custom_fields': {'text_field': 'ABC123'}}
471        response = self.client.patch(url, data, format='json', **self.header)
472        self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
473
474        data = {'custom_fields': {'text_field': 'abc'}}
475        response = self.client.patch(url, data, format='json', **self.header)
476        self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
477
478        data = {'custom_fields': {'text_field': 'ABC'}}
479        response = self.client.patch(url, data, format='json', **self.header)
480        self.assertHttpStatus(response, status.HTTP_200_OK)
481
482
483class CustomFieldImportTest(TestCase):
484    user_permissions = (
485        'dcim.view_site',
486        'dcim.add_site',
487    )
488
489    @classmethod
490    def setUpTestData(cls):
491
492        custom_fields = (
493            CustomField(name='text', type=CustomFieldTypeChoices.TYPE_TEXT),
494            CustomField(name='integer', type=CustomFieldTypeChoices.TYPE_INTEGER),
495            CustomField(name='boolean', type=CustomFieldTypeChoices.TYPE_BOOLEAN),
496            CustomField(name='date', type=CustomFieldTypeChoices.TYPE_DATE),
497            CustomField(name='url', type=CustomFieldTypeChoices.TYPE_URL),
498            CustomField(name='select', type=CustomFieldTypeChoices.TYPE_SELECT, choices=['Choice A', 'Choice B', 'Choice C']),
499        )
500        for cf in custom_fields:
501            cf.save()
502            cf.content_types.set([ContentType.objects.get_for_model(Site)])
503
504    def test_import(self):
505        """
506        Import a Site in CSV format, including a value for each CustomField.
507        """
508        data = (
509            ('name', 'slug', 'status', 'cf_text', 'cf_integer', 'cf_boolean', 'cf_date', 'cf_url', 'cf_select'),
510            ('Site 1', 'site-1', 'active', 'ABC', '123', 'True', '2020-01-01', 'http://example.com/1', 'Choice A'),
511            ('Site 2', 'site-2', 'active', 'DEF', '456', 'False', '2020-01-02', 'http://example.com/2', 'Choice B'),
512            ('Site 3', 'site-3', 'active', '', '', '', '', '', ''),
513        )
514        csv_data = '\n'.join(','.join(row) for row in data)
515
516        response = self.client.post(reverse('dcim:site_import'), {'csv': csv_data})
517        self.assertEqual(response.status_code, 200)
518
519        # Validate data for site 1
520        site1 = Site.objects.get(name='Site 1')
521        self.assertEqual(len(site1.custom_field_data), 6)
522        self.assertEqual(site1.custom_field_data['text'], 'ABC')
523        self.assertEqual(site1.custom_field_data['integer'], 123)
524        self.assertEqual(site1.custom_field_data['boolean'], True)
525        self.assertEqual(site1.custom_field_data['date'], '2020-01-01')
526        self.assertEqual(site1.custom_field_data['url'], 'http://example.com/1')
527        self.assertEqual(site1.custom_field_data['select'], 'Choice A')
528
529        # Validate data for site 2
530        site2 = Site.objects.get(name='Site 2')
531        self.assertEqual(len(site2.custom_field_data), 6)
532        self.assertEqual(site2.custom_field_data['text'], 'DEF')
533        self.assertEqual(site2.custom_field_data['integer'], 456)
534        self.assertEqual(site2.custom_field_data['boolean'], False)
535        self.assertEqual(site2.custom_field_data['date'], '2020-01-02')
536        self.assertEqual(site2.custom_field_data['url'], 'http://example.com/2')
537        self.assertEqual(site2.custom_field_data['select'], 'Choice B')
538
539        # No custom field data should be set for site 3
540        site3 = Site.objects.get(name='Site 3')
541        self.assertFalse(any(site3.custom_field_data.values()))
542
543    def test_import_missing_required(self):
544        """
545        Attempt to import an object missing a required custom field.
546        """
547        # Set one of our CustomFields to required
548        CustomField.objects.filter(name='text').update(required=True)
549
550        form_data = {
551            'name': 'Site 1',
552            'slug': 'site-1',
553        }
554
555        form = SiteCSVForm(data=form_data)
556        self.assertFalse(form.is_valid())
557        self.assertIn('cf_text', form.errors)
558
559    def test_import_invalid_choice(self):
560        """
561        Attempt to import an object with an invalid choice selection.
562        """
563        form_data = {
564            'name': 'Site 1',
565            'slug': 'site-1',
566            'cf_select': 'Choice X'
567        }
568
569        form = SiteCSVForm(data=form_data)
570        self.assertFalse(form.is_valid())
571        self.assertIn('cf_select', form.errors)
572
573
574class CustomFieldModelTest(TestCase):
575
576    @classmethod
577    def setUpTestData(cls):
578        cf1 = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='foo')
579        cf1.save()
580        cf1.content_types.set([ContentType.objects.get_for_model(Site)])
581
582        cf2 = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='bar')
583        cf2.save()
584        cf2.content_types.set([ContentType.objects.get_for_model(Rack)])
585
586    def test_cf_data(self):
587        """
588        Check that custom field data is present on the instance immediately after being set and after being fetched
589        from the database.
590        """
591        site = Site(name='Test Site', slug='test-site')
592
593        # Check custom field data on new instance
594        site.cf['foo'] = 'abc'
595        self.assertEqual(site.cf['foo'], 'abc')
596
597        # Check custom field data from database
598        site.save()
599        site = Site.objects.get(name='Test Site')
600        self.assertEqual(site.cf['foo'], 'abc')
601
602    def test_invalid_data(self):
603        """
604        Setting custom field data for a non-applicable (or non-existent) CustomField should raise a ValidationError.
605        """
606        site = Site(name='Test Site', slug='test-site')
607
608        # Set custom field data
609        site.cf['foo'] = 'abc'
610        site.cf['bar'] = 'def'
611        with self.assertRaises(ValidationError):
612            site.clean()
613
614        del(site.cf['bar'])
615        site.clean()
616
617    def test_missing_required_field(self):
618        """
619        Check that a ValidationError is raised if any required custom fields are not present.
620        """
621        cf3 = CustomField(type=CustomFieldTypeChoices.TYPE_TEXT, name='baz', required=True)
622        cf3.save()
623        cf3.content_types.set([ContentType.objects.get_for_model(Site)])
624
625        site = Site(name='Test Site', slug='test-site')
626
627        # Set custom field data with a required field omitted
628        site.cf['foo'] = 'abc'
629        with self.assertRaises(ValidationError):
630            site.clean()
631
632        site.cf['baz'] = 'def'
633        site.clean()
634
635
636class CustomFieldFilterTest(TestCase):
637    queryset = Site.objects.all()
638    filterset = SiteFilterSet
639
640    @classmethod
641    def setUpTestData(cls):
642        obj_type = ContentType.objects.get_for_model(Site)
643
644        # Integer filtering
645        cf = CustomField(name='cf1', type=CustomFieldTypeChoices.TYPE_INTEGER)
646        cf.save()
647        cf.content_types.set([obj_type])
648
649        # Boolean filtering
650        cf = CustomField(name='cf2', type=CustomFieldTypeChoices.TYPE_BOOLEAN)
651        cf.save()
652        cf.content_types.set([obj_type])
653
654        # Exact text filtering
655        cf = CustomField(name='cf3', type=CustomFieldTypeChoices.TYPE_TEXT,
656                         filter_logic=CustomFieldFilterLogicChoices.FILTER_EXACT)
657        cf.save()
658        cf.content_types.set([obj_type])
659
660        # Loose text filtering
661        cf = CustomField(name='cf4', type=CustomFieldTypeChoices.TYPE_TEXT,
662                         filter_logic=CustomFieldFilterLogicChoices.FILTER_LOOSE)
663        cf.save()
664        cf.content_types.set([obj_type])
665
666        # Date filtering
667        cf = CustomField(name='cf5', type=CustomFieldTypeChoices.TYPE_DATE)
668        cf.save()
669        cf.content_types.set([obj_type])
670
671        # Exact URL filtering
672        cf = CustomField(name='cf6', type=CustomFieldTypeChoices.TYPE_URL,
673                         filter_logic=CustomFieldFilterLogicChoices.FILTER_EXACT)
674        cf.save()
675        cf.content_types.set([obj_type])
676
677        # Loose URL filtering
678        cf = CustomField(name='cf7', type=CustomFieldTypeChoices.TYPE_URL,
679                         filter_logic=CustomFieldFilterLogicChoices.FILTER_LOOSE)
680        cf.save()
681        cf.content_types.set([obj_type])
682
683        # Selection filtering
684        cf = CustomField(name='cf8', type=CustomFieldTypeChoices.TYPE_SELECT, choices=['Foo', 'Bar', 'Baz'])
685        cf.save()
686        cf.content_types.set([obj_type])
687
688        # Multiselect filtering
689        cf = CustomField(name='cf9', type=CustomFieldTypeChoices.TYPE_MULTISELECT, choices=['A', 'AA', 'B', 'C'])
690        cf.save()
691        cf.content_types.set([obj_type])
692
693        Site.objects.bulk_create([
694            Site(name='Site 1', slug='site-1', custom_field_data={
695                'cf1': 100,
696                'cf2': True,
697                'cf3': 'foo',
698                'cf4': 'foo',
699                'cf5': '2016-06-26',
700                'cf6': 'http://foo.example.com/',
701                'cf7': 'http://foo.example.com/',
702                'cf8': 'Foo',
703                'cf9': ['A', 'B'],
704            }),
705            Site(name='Site 2', slug='site-2', custom_field_data={
706                'cf1': 200,
707                'cf2': False,
708                'cf3': 'foobar',
709                'cf4': 'foobar',
710                'cf5': '2016-06-27',
711                'cf6': 'http://bar.example.com/',
712                'cf7': 'http://bar.example.com/',
713                'cf8': 'Bar',
714                'cf9': ['AA', 'B'],
715            }),
716            Site(name='Site 3', slug='site-3'),
717        ])
718
719    def test_filter_integer(self):
720        self.assertEqual(self.filterset({'cf_cf1': 100}, self.queryset).qs.count(), 1)
721
722    def test_filter_boolean(self):
723        self.assertEqual(self.filterset({'cf_cf2': True}, self.queryset).qs.count(), 1)
724        self.assertEqual(self.filterset({'cf_cf2': False}, self.queryset).qs.count(), 1)
725
726    def test_filter_text(self):
727        self.assertEqual(self.filterset({'cf_cf3': 'foo'}, self.queryset).qs.count(), 1)
728        self.assertEqual(self.filterset({'cf_cf4': 'foo'}, self.queryset).qs.count(), 2)
729
730    def test_filter_date(self):
731        self.assertEqual(self.filterset({'cf_cf5': '2016-06-26'}, self.queryset).qs.count(), 1)
732
733    def test_filter_url(self):
734        self.assertEqual(self.filterset({'cf_cf6': 'http://foo.example.com/'}, self.queryset).qs.count(), 1)
735        self.assertEqual(self.filterset({'cf_cf7': 'example.com'}, self.queryset).qs.count(), 2)
736
737    def test_filter_select(self):
738        self.assertEqual(self.filterset({'cf_cf8': 'Foo'}, self.queryset).qs.count(), 1)
739        self.assertEqual(self.filterset({'cf_cf8': 'Bar'}, self.queryset).qs.count(), 1)
740        self.assertEqual(self.filterset({'cf_cf8': 'Baz'}, self.queryset).qs.count(), 0)
741
742    def test_filter_multiselect(self):
743        self.assertEqual(self.filterset({'cf_cf9': 'A'}, self.queryset).qs.count(), 1)
744        self.assertEqual(self.filterset({'cf_cf9': 'B'}, self.queryset).qs.count(), 2)
745        self.assertEqual(self.filterset({'cf_cf9': 'C'}, self.queryset).qs.count(), 0)
746