Coverage for utilities/testing/views.py: 0%
631 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 18:35 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 18:35 +0000
1import csv
3from django.conf import settings
4from django.contrib.contenttypes.models import ContentType
5from django.core.exceptions import ObjectDoesNotExist
6from django.http import QueryDict
7from django.test import override_settings
8from django.urls import reverse
9from django.utils.translation import gettext as _
11from core.choices import ObjectChangeActionChoices
12from core.models import ObjectChange, ObjectType
13from netbox.choices import CSVDelimiterChoices, ImportFormatChoices
14from netbox.models.features import ChangeLoggingMixin, CustomFieldsMixin
15from users.models import ObjectPermission
17from .base import ModelTestCase
18from .query_counts import assert_expected_query_count
19from .utils import add_custom_field_data, disable_warnings, get_random_string, post_data
21__all__ = (
22 'ModelViewTestCase',
23 'ViewTestCases',
24)
27#
28# UI Tests
29#
31class ModelViewTestCase(ModelTestCase):
32 """
33 Base TestCase for model views. Subclass to test individual views.
34 """
35 def _get_base_url(self):
36 """
37 Return the base format for a URL for the test's model. Override this to test for a model which belongs
38 to a different app (e.g. testing Interfaces within the virtualization app).
39 """
40 return '{}:{}_{{}}'.format(
41 self.model._meta.app_label,
42 self.model._meta.model_name
43 )
45 def _get_url(self, action, instance=None):
46 """
47 Return the URL name for a specific action and optionally a specific instance
48 """
49 url_format = self._get_base_url()
51 # If no instance was provided, assume we don't need a unique identifier
52 if instance is None:
53 return reverse(url_format.format(action))
55 return reverse(url_format.format(action), kwargs={'pk': instance.pk})
58class ViewTestCases:
59 """
60 We keep any TestCases with test_* methods inside a class to prevent unittest from trying to run them.
61 """
62 class GetObjectViewTestCase(ModelViewTestCase):
63 """
64 Retrieve a single instance.
65 """
66 @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'], LOGIN_REQUIRED=False)
67 def test_get_object_anonymous(self):
68 # Make the request as an unauthenticated user
69 self.client.logout()
70 ct = ContentType.objects.get_for_model(self.model)
71 if (ct.app_label, ct.model) in settings.EXEMPT_EXCLUDE_MODELS:
72 # Models listed in EXEMPT_EXCLUDE_MODELS should not be accessible to anonymous users
73 with disable_warnings('django.request'):
74 response = self.client.get(self._get_queryset().first().get_absolute_url())
75 self.assertHttpStatus(response, 302)
76 else:
77 response = self.client.get(self._get_queryset().first().get_absolute_url())
78 self.assertHttpStatus(response, 200)
80 def test_get_object_without_permission(self):
81 instance = self._get_queryset().first()
83 # Try GET without permission
84 with disable_warnings('django.request'):
85 self.assertHttpStatus(self.client.get(instance.get_absolute_url()), 403)
87 def test_get_object_with_permission(self):
88 instance = self._get_queryset().first()
90 # Add model-level permission
91 obj_perm = ObjectPermission(
92 name='Test permission',
93 actions=['view']
94 )
95 obj_perm.save()
96 obj_perm.users.add(self.user)
97 obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model))
99 # Try GET with model-level permission
100 self.assertHttpStatus(self.client.get(instance.get_absolute_url()), 200)
102 def test_get_object_with_constrained_permission(self):
103 instance1, instance2 = self._get_queryset().all()[:2]
105 # Add object-level permission
106 obj_perm = ObjectPermission(
107 name='Test permission',
108 constraints={'pk': instance1.pk},
109 actions=['view']
110 )
111 obj_perm.save()
112 obj_perm.users.add(self.user)
113 obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model))
115 # Try GET to permitted object
116 self.assertHttpStatus(self.client.get(instance1.get_absolute_url()), 200)
118 # Try GET to non-permitted object
119 self.assertHttpStatus(self.client.get(instance2.get_absolute_url()), 404)
121 class GetObjectChangelogViewTestCase(ModelViewTestCase):
122 """
123 View the changelog for an instance.
124 """
125 @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
126 def test_get_object_changelog(self):
127 url = self._get_url('changelog', self._get_queryset().first())
128 response = self.client.get(url)
129 self.assertHttpStatus(response, 200)
131 class CreateObjectViewTestCase(ModelViewTestCase):
132 """
133 Create a single new instance.
135 :form_data: Data to be used when creating a new object.
136 """
137 form_data = {}
138 validation_excluded_fields = []
140 def test_create_object_without_permission(self):
142 # Try GET without permission
143 with disable_warnings('django.request'):
144 self.assertHttpStatus(self.client.get(self._get_url('add')), 403)
146 # Try POST without permission
147 request = {
148 'path': self._get_url('add'),
149 'data': post_data(self.form_data),
150 }
151 response = self.client.post(**request)
152 with disable_warnings('django.request'):
153 self.assertHttpStatus(response, 403)
155 @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'], EXEMPT_EXCLUDE_MODELS=[])
156 def test_create_object_with_permission(self):
157 # Assign unconstrained permission
158 obj_perm = ObjectPermission(
159 name='Test permission',
160 actions=['add']
161 )
162 obj_perm.save()
163 obj_perm.users.add(self.user)
164 obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model))
166 # Try GET with model-level permission
167 self.assertHttpStatus(self.client.get(self._get_url('add')), 200)
169 # Add custom field data if the model supports it
170 if issubclass(self.model, CustomFieldsMixin):
171 add_custom_field_data(self.form_data, self.model)
173 # If supported, add a changelog message
174 if issubclass(self.model, ChangeLoggingMixin):
175 if 'changelog_message' not in self.form_data:
176 self.form_data['changelog_message'] = get_random_string(10)
178 # Try POST with model-level permission
179 initial_count = self._get_queryset().count()
180 request = {
181 'path': self._get_url('add'),
182 'data': post_data(self.form_data),
183 }
184 self.assertHttpStatus(self.client.post(**request), 302)
185 self.assertEqual(initial_count + 1, self._get_queryset().count())
186 instance = self._get_queryset().order_by('pk').last()
187 self.assertInstanceEqual(instance, self.form_data, exclude=self.validation_excluded_fields)
189 # Verify ObjectChange creation
190 if issubclass(self.model, ChangeLoggingMixin):
191 objectchanges = ObjectChange.objects.filter(
192 changed_object_type=ContentType.objects.get_for_model(instance),
193 changed_object_id=instance.pk
194 )
195 self.assertEqual(len(objectchanges), 1)
196 self.assertObjectChange(objectchanges[0], action=ObjectChangeActionChoices.ACTION_CREATE,
197 message=self.form_data['changelog_message'])
199 @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'], EXEMPT_EXCLUDE_MODELS=[])
200 def test_create_object_with_constrained_permission(self):
202 # Assign constrained permission
203 obj_perm = ObjectPermission(
204 name='Test permission',
205 constraints={'pk': 0}, # Dummy permission to deny all
206 actions=['add']
207 )
208 obj_perm.save()
209 obj_perm.users.add(self.user)
210 obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model))
212 # Try GET with object-level permission
213 self.assertHttpStatus(self.client.get(self._get_url('add')), 200)
215 # Try to create an object (not permitted)
216 initial_count = self._get_queryset().count()
217 request = {
218 'path': self._get_url('add'),
219 'data': post_data(self.form_data),
220 }
221 self.assertHttpStatus(self.client.post(**request), 200)
222 self.assertEqual(initial_count, self._get_queryset().count()) # Check that no object was created
224 # Update the ObjectPermission to allow creation
225 obj_perm.constraints = {'pk__gt': 0}
226 obj_perm.save()
228 # Try to create an object (permitted)
229 request = {
230 'path': self._get_url('add'),
231 'data': post_data(self.form_data),
232 }
233 self.assertHttpStatus(self.client.post(**request), 302)
234 self.assertEqual(initial_count + 1, self._get_queryset().count())
235 instance = self._get_queryset().order_by('pk').last()
236 self.assertInstanceEqual(instance, self.form_data, exclude=self.validation_excluded_fields)
238 class EditObjectViewTestCase(ModelViewTestCase):
239 """
240 Edit a single existing instance.
242 :form_data: Data to be used when updating the first existing object.
243 """
244 form_data = {}
245 validation_excluded_fields = []
247 def test_edit_object_without_permission(self):
248 instance = self._get_queryset().first()
250 # Try GET without permission
251 with disable_warnings('django.request'):
252 self.assertHttpStatus(self.client.get(self._get_url('edit', instance)), 403)
254 # Try POST without permission
255 request = {
256 'path': self._get_url('edit', instance),
257 'data': post_data(self.form_data),
258 }
259 with disable_warnings('django.request'):
260 self.assertHttpStatus(self.client.post(**request), 403)
262 @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'], EXEMPT_EXCLUDE_MODELS=[])
263 def test_edit_object_with_permission(self):
264 instance = self._get_queryset().first()
266 # Assign model-level permission
267 obj_perm = ObjectPermission(
268 name='Test permission',
269 actions=['change']
270 )
271 obj_perm.save()
272 obj_perm.users.add(self.user)
273 obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model))
275 # Try GET with model-level permission
276 self.assertHttpStatus(self.client.get(self._get_url('edit', instance)), 200)
278 # Add custom field data if the model supports it
279 if issubclass(self.model, CustomFieldsMixin):
280 add_custom_field_data(self.form_data, self.model)
282 # If supported, add a changelog message
283 if issubclass(self.model, ChangeLoggingMixin):
284 if 'changelog_message' not in self.form_data:
285 self.form_data['changelog_message'] = get_random_string(10)
287 # Try POST with model-level permission
288 request = {
289 'path': self._get_url('edit', instance),
290 'data': post_data(self.form_data),
291 }
292 self.assertHttpStatus(self.client.post(**request), 302)
293 instance = self._get_queryset().get(pk=instance.pk)
294 self.assertInstanceEqual(instance, self.form_data, exclude=self.validation_excluded_fields)
296 # Verify ObjectChange creation
297 if issubclass(self.model, ChangeLoggingMixin):
298 objectchanges = ObjectChange.objects.filter(
299 changed_object_type=ContentType.objects.get_for_model(instance),
300 changed_object_id=instance.pk
301 )
302 self.assertEqual(len(objectchanges), 1)
303 self.assertObjectChange(objectchanges[0], action=ObjectChangeActionChoices.ACTION_UPDATE,
304 message=self.form_data['changelog_message'])
306 @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'], EXEMPT_EXCLUDE_MODELS=[])
307 def test_edit_object_with_constrained_permission(self):
308 instance1, instance2 = self._get_queryset().all()[:2]
310 # Assign constrained permission
311 obj_perm = ObjectPermission(
312 name='Test permission',
313 constraints={'pk': instance1.pk},
314 actions=['change']
315 )
316 obj_perm.save()
317 obj_perm.users.add(self.user)
318 obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model))
320 # Try GET with a permitted object
321 self.assertHttpStatus(self.client.get(self._get_url('edit', instance1)), 200)
323 # Try GET with a non-permitted object
324 self.assertHttpStatus(self.client.get(self._get_url('edit', instance2)), 404)
326 # Try to edit a permitted object
327 request = {
328 'path': self._get_url('edit', instance1),
329 'data': post_data(self.form_data),
330 }
331 self.assertHttpStatus(self.client.post(**request), 302)
332 instance = self._get_queryset().get(pk=instance1.pk)
333 self.assertInstanceEqual(instance, self.form_data, exclude=self.validation_excluded_fields)
335 # Try to edit a non-permitted object
336 request = {
337 'path': self._get_url('edit', instance2),
338 'data': post_data(self.form_data),
339 }
340 self.assertHttpStatus(self.client.post(**request), 404)
342 class DeleteObjectViewTestCase(ModelViewTestCase):
343 """
344 Delete a single instance.
345 """
346 def test_delete_object_without_permission(self):
347 instance = self._get_queryset().first()
349 # Try GET without permission
350 with disable_warnings('django.request'):
351 self.assertHttpStatus(self.client.get(self._get_url('delete', instance)), 403)
353 # Try POST without permission
354 request = {
355 'path': self._get_url('delete', instance),
356 'data': post_data({'confirm': True}),
357 }
358 with disable_warnings('django.request'):
359 self.assertHttpStatus(self.client.post(**request), 403)
361 @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
362 def test_delete_object_with_permission(self):
363 instance = self._get_queryset().first()
364 form_data = {'confirm': True}
366 # Assign model-level permission
367 obj_perm = ObjectPermission(
368 name='Test permission',
369 actions=['delete']
370 )
371 obj_perm.save()
372 obj_perm.users.add(self.user)
373 obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model))
375 # Try GET with model-level permission
376 self.assertHttpStatus(self.client.get(self._get_url('delete', instance)), 200)
378 # If supported, add a changelog message
379 if issubclass(self.model, ChangeLoggingMixin):
380 form_data['changelog_message'] = get_random_string(10)
382 # Try POST with model-level permission
383 request = {
384 'path': self._get_url('delete', instance),
385 'data': post_data(form_data),
386 }
387 self.assertHttpStatus(self.client.post(**request), 302)
388 with self.assertRaises(ObjectDoesNotExist):
389 self._get_queryset().get(pk=instance.pk)
391 # Verify ObjectChange creation
392 if issubclass(self.model, ChangeLoggingMixin):
393 objectchanges = ObjectChange.objects.filter(
394 changed_object_type=ContentType.objects.get_for_model(instance),
395 changed_object_id=instance.pk
396 )
397 self.assertEqual(len(objectchanges), 1)
398 self.assertObjectChange(objectchanges[0], action=ObjectChangeActionChoices.ACTION_DELETE,
399 message=form_data['changelog_message'])
401 @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
402 def test_delete_object_with_constrained_permission(self):
403 instance1, instance2 = self._get_queryset().all()[:2]
405 # Assign object-level permission
406 obj_perm = ObjectPermission(
407 name='Test permission',
408 constraints={'pk': instance1.pk},
409 actions=['delete']
410 )
411 obj_perm.save()
412 obj_perm.users.add(self.user)
413 obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model))
415 # Try GET with a permitted object
416 self.assertHttpStatus(self.client.get(self._get_url('delete', instance1)), 200)
418 # Try GET with a non-permitted object
419 self.assertHttpStatus(self.client.get(self._get_url('delete', instance2)), 404)
421 # Try to delete a permitted object
422 request = {
423 'path': self._get_url('delete', instance1),
424 'data': post_data({'confirm': True}),
425 }
426 self.assertHttpStatus(self.client.post(**request), 302)
427 with self.assertRaises(ObjectDoesNotExist):
428 self._get_queryset().get(pk=instance1.pk)
430 # Try to delete a non-permitted object
431 request = {
432 'path': self._get_url('delete', instance2),
433 'data': post_data({'confirm': True}),
434 }
435 self.assertHttpStatus(self.client.post(**request), 404)
436 self.assertTrue(self._get_queryset().filter(pk=instance2.pk).exists())
438 class ListObjectsViewTestCase(ModelViewTestCase):
439 """
440 Retrieve multiple instances.
441 """
442 @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'], LOGIN_REQUIRED=False)
443 def test_list_objects_anonymous(self):
444 # Make the request as an unauthenticated user
445 self.client.logout()
446 ct = ContentType.objects.get_for_model(self.model)
447 if (ct.app_label, ct.model) in settings.EXEMPT_EXCLUDE_MODELS:
448 # Models listed in EXEMPT_EXCLUDE_MODELS should not be accessible to anonymous users
449 with disable_warnings('django.request'):
450 response = self.client.get(self._get_url('list'))
451 self.assertHttpStatus(response, 302)
452 else:
453 response = self.client.get(self._get_url('list'))
454 self.assertHttpStatus(response, 200)
456 def test_list_objects_without_permission(self):
458 # Try GET without permission
459 with disable_warnings('django.request'):
460 self.assertHttpStatus(self.client.get(self._get_url('list')), 403)
462 def test_list_objects_with_permission(self):
464 # Add model-level permission
465 obj_perm = ObjectPermission(
466 name='Test permission',
467 actions=['view']
468 )
469 obj_perm.save()
470 obj_perm.users.add(self.user)
471 obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model))
473 # Try GET with model-level permission
474 with assert_expected_query_count(self, 'list_objects_with_permission'):
475 response = self.client.get(self._get_url('list'))
476 self.assertHttpStatus(response, 200)
478 def test_list_objects_with_constrained_permission(self):
479 instance1, instance2 = self._get_queryset().all()[:2]
481 # Add object-level permission
482 obj_perm = ObjectPermission(
483 name='Test permission',
484 constraints={'pk': instance1.pk},
485 actions=['view']
486 )
487 obj_perm.save()
488 obj_perm.users.add(self.user)
489 obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model))
491 # Try GET with object-level permission
492 response = self.client.get(self._get_url('list'))
493 self.assertHttpStatus(response, 200)
494 content = str(response.content)
495 self.assertIn(instance1.get_absolute_url(), content)
496 self.assertNotIn(instance2.get_absolute_url(), content)
498 def test_export_objects(self):
499 url = self._get_url('list')
501 # Add model-level permission
502 obj_perm = ObjectPermission(
503 name='Test permission',
504 actions=['view']
505 )
506 obj_perm.save()
507 obj_perm.users.add(self.user)
508 obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model))
510 # Test default CSV export
511 response = self.client.get(f'{url}?export')
512 self.assertHttpStatus(response, 200)
513 self.assertEqual(response.get('Content-Type'), 'text/csv; charset=utf-8')
515 # Test table-based export
516 response = self.client.get(f'{url}?export=table')
517 self.assertHttpStatus(response, 200)
518 self.assertEqual(response.get('Content-Type'), 'text/csv; charset=utf-8')
520 @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'], LOGIN_REQUIRED=False)
521 def test_export_objects_anonymous(self):
522 # Ensure we are logged out.
523 self.client.logout()
525 # Some models (e.g. the users model) always require to be logged in, so we skip them here.
526 ct = ContentType.objects.get_for_model(self.model)
527 if (ct.app_label, ct.model) in settings.EXEMPT_EXCLUDE_MODELS:
528 return
530 url = self._get_url('list')
532 # Test default CSV (or sometimes YAML) export
533 response = self.client.get(f'{url}?export')
534 self.assertHttpStatus(response, 200)
535 if hasattr(self.model, 'to_yaml'):
536 self.assertEqual(response.get('Content-Type'), 'text/yaml')
537 else:
538 self.assertEqual(response.get('Content-Type'), 'text/csv; charset=utf-8')
540 # Test table-based export
541 response = self.client.get(f'{url}?export=table')
542 self.assertHttpStatus(response, 200)
543 self.assertEqual(response.get('Content-Type'), 'text/csv; charset=utf-8')
545 class CreateMultipleObjectsViewTestCase(ModelViewTestCase):
546 """
547 Create multiple instances using a single form. Expects the creation of three new instances by default.
549 :bulk_create_count: The number of objects expected to be created (default: 3).
550 :bulk_create_data: A dictionary of data to be used for bulk object creation.
551 """
552 bulk_create_count = 3
553 bulk_create_data = {}
554 validation_excluded_fields = []
556 def test_create_multiple_objects_without_permission(self):
557 request = {
558 'path': self._get_url('add'),
559 'data': post_data(self.bulk_create_data),
560 }
562 # Try POST without permission
563 with disable_warnings('django.request'):
564 self.assertHttpStatus(self.client.post(**request), 403)
566 def test_create_multiple_objects_with_permission(self):
567 initial_count = self._get_queryset().count()
568 request = {
569 'path': self._get_url('add'),
570 'data': post_data(self.bulk_create_data),
571 }
573 # Assign non-constrained permission
574 obj_perm = ObjectPermission(
575 name='Test permission',
576 actions=['add'],
577 )
578 obj_perm.save()
579 obj_perm.users.add(self.user)
580 obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model))
582 # Bulk create objects
583 response = self.client.post(**request)
584 self.assertHttpStatus(response, 302)
585 self.assertEqual(initial_count + self.bulk_create_count, self._get_queryset().count())
586 for instance in self._get_queryset().order_by('-pk')[:self.bulk_create_count]:
587 self.assertInstanceEqual(instance, self.bulk_create_data, exclude=self.validation_excluded_fields)
589 def test_create_multiple_objects_with_constrained_permission(self):
590 initial_count = self._get_queryset().count()
591 request = {
592 'path': self._get_url('add'),
593 'data': post_data(self.bulk_create_data),
594 }
596 # Assign constrained permission
597 obj_perm = ObjectPermission(
598 name='Test permission',
599 actions=['add'],
600 constraints={'pk': 0} # Dummy constraint to deny all
601 )
602 obj_perm.save()
603 obj_perm.users.add(self.user)
604 obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model))
606 # Attempt to make the request with unmet constraints
607 self.assertHttpStatus(self.client.post(**request), 200)
608 self.assertEqual(self._get_queryset().count(), initial_count)
610 # Update the ObjectPermission to allow creation
611 obj_perm.constraints = {'pk__gt': 0} # Dummy constraint to allow all
612 obj_perm.save()
614 response = self.client.post(**request)
615 self.assertHttpStatus(response, 302)
616 self.assertEqual(initial_count + self.bulk_create_count, self._get_queryset().count())
617 for instance in self._get_queryset().order_by('-pk')[: self.bulk_create_count]:
618 self.assertInstanceEqual(instance, self.bulk_create_data, exclude=self.validation_excluded_fields)
620 def test_create_multiple_objects_addanother(self):
621 """The "Create & Add Another" redirect retains the parent the objects were created under."""
622 parent_fields = ('device', 'module', 'device_type', 'module_type', 'virtual_machine')
623 parents = {
624 field: getattr(value, 'pk', value) for field, value in self.bulk_create_data.items()
625 if field in parent_fields and value
626 }
627 if not parents:
628 self.skipTest("bulk_create_data declares no parent assignment")
630 # Assign non-constrained permission
631 obj_perm = ObjectPermission(
632 name='Test permission',
633 actions=['add'],
634 )
635 obj_perm.save()
636 obj_perm.users.add(self.user)
637 obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model))
639 response = self.client.post(
640 path=self._get_url('add'),
641 data=post_data({**self.bulk_create_data, '_addanother': True}),
642 )
643 self.assertHttpStatus(response, 302)
645 path, _sep, query = response['Location'].partition('?')
646 self.assertEqual(path, self._get_url('add'))
647 params = QueryDict(query)
648 for field, value in parents.items():
649 self.assertEqual(params.getlist(field), [str(value)])
651 class BulkImportObjectsViewTestCase(ModelViewTestCase):
652 """
653 Create multiple instances from imported data.
655 :csv_data: CSV data for bulk import testing. Supports two formats:
657 1. Tuple/list format (backwards compatible):
658 csv_data = (
659 "name,slug,description",
660 "Object 1,object-1,First object",
661 "Object 2,object-2,Second object",
662 )
664 2. Dictionary format for multiple scenarios:
665 csv_data = {
666 'default': (
667 "name,slug,description",
668 "Object 1,object-1,First object",
669 ),
670 'with_optional_fields': (
671 "name,slug,description,comments",
672 "Object 2,object-2,Second object,With comments",
673 )
674 }
676 When using dictionary format, test_bulk_import_objects_with_permission()
677 runs each scenario as a separate subtest with clear output:
679 test_bulk_import_objects_with_permission (scenario=default) ... ok
680 test_bulk_import_objects_with_permission (scenario=with_optional_fields) ... ok
681 """
683 csv_data = ()
685 def get_scenarios(self):
686 return self.csv_data.keys() if isinstance(self.csv_data, dict) else ['default']
688 def _get_csv_data(self, scenario_name='default'):
689 """
690 Get CSV data for testing. Supports both tuple/list and dictionary formats.
691 """
692 if isinstance(self.csv_data, dict):
693 if scenario_name not in self.csv_data:
694 available = ', '.join(self.csv_data.keys())
695 raise ValueError(f"Scenario '{scenario_name}' not found in csv_data. Available: {available}")
696 return '\n'.join(self.csv_data[scenario_name])
697 if isinstance(self.csv_data, (tuple, list)):
698 return '\n'.join(self.csv_data)
699 raise TypeError(f'csv_data must be a tuple, list, or dictionary, got {type(self.csv_data)}')
701 def _get_update_csv_data(self):
702 return self.csv_update_data, '\n'.join(self.csv_update_data)
704 def test_bulk_import_objects_without_permission(self):
705 data = {
706 'data': self._get_csv_data(),
707 'format': ImportFormatChoices.CSV,
708 'csv_delimiter': CSVDelimiterChoices.AUTO,
709 }
711 # Test GET without permission
712 with disable_warnings('django.request'):
713 self.assertHttpStatus(self.client.get(self._get_url('bulk_import')), 403)
715 # Try POST without permission
716 response = self.client.post(self._get_url('bulk_import'), data)
717 with disable_warnings('django.request'):
718 self.assertHttpStatus(response, 403)
720 @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'], EXEMPT_EXCLUDE_MODELS=[])
721 def test_bulk_import_objects_with_permission(self, post_import_callback=None):
722 # Assign model-level permission once for all scenarios
723 obj_perm = ObjectPermission(name='Test permission', actions=['add'])
724 obj_perm.save()
725 obj_perm.users.add(self.user)
726 obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model))
728 # Try GET with model-level permission (only once)
729 self.assertHttpStatus(self.client.get(self._get_url('bulk_import')), 200)
731 # Test each scenario
732 for scenario_name in self.get_scenarios():
733 with self.cleanupSubTest(scenario=scenario_name):
734 self._test_bulk_import_with_permission_scenario(scenario_name)
736 if post_import_callback:
737 post_import_callback(scenario_name)
739 def _test_bulk_import_with_permission_scenario(self, scenario_name):
740 """
741 Helper method to test a single bulk import scenario.
742 """
743 initial_count = self._get_queryset().count()
745 # Get CSV data for this scenario
746 scenario_data = self._get_csv_data(scenario_name)
747 expected_new_objects = len(scenario_data.splitlines()) - 1
749 data = {
750 'data': scenario_data,
751 'format': ImportFormatChoices.CSV,
752 'csv_delimiter': CSVDelimiterChoices.AUTO,
753 }
755 # If supported, add a changelog message
756 if issubclass(self.model, ChangeLoggingMixin):
757 data['changelog_message'] = get_random_string(10)
759 # Test POST with permission
760 response = self.client.post(self._get_url('bulk_import'), data)
761 self.assertHttpStatus(response, 302)
763 # Verify object count increase
764 self.assertEqual(self._get_queryset().count(), initial_count + expected_new_objects)
766 # Verify ObjectChange creation
767 if issubclass(self.model, ChangeLoggingMixin):
768 request_id = response.headers.get('X-Request-ID')
769 self.assertIsNotNone(request_id, 'Unable to determine request ID from response')
770 objectchanges = ObjectChange.objects.filter(
771 changed_object_type=ContentType.objects.get_for_model(self.model),
772 request_id=request_id,
773 action=ObjectChangeActionChoices.ACTION_CREATE,
774 )
775 self.assertEqual(len(objectchanges), expected_new_objects)
777 for oc in objectchanges:
778 self.assertObjectChange(oc, action=ObjectChangeActionChoices.ACTION_CREATE,
779 message=data['changelog_message'])
781 @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
782 def test_bulk_update_objects_without_change_permission(self):
783 # Bulk import rows carrying an object ID update existing objects. This must require the 'change'
784 # permission, matching the REST API; the 'add' permission alone must not permit updates.
785 if not hasattr(self, 'csv_update_data'):
786 raise NotImplementedError(_("The test must define csv_update_data."))
788 initial_count = self._get_queryset().count()
789 array, csv_data = self._get_update_csv_data()
790 data = {
791 'format': ImportFormatChoices.CSV,
792 'data': csv_data,
793 'csv_delimiter': CSVDelimiterChoices.AUTO,
794 }
796 # Assign only the 'add' permission
797 obj_perm = ObjectPermission(
798 name='Test permission',
799 actions=['add']
800 )
801 obj_perm.save()
802 obj_perm.users.add(self.user)
803 obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model))
805 # Take a snapshot of the objects targeted for update
806 reader = csv.DictReader(array, delimiter=',')
807 check_data = list(reader)
808 before = {
809 line['id']: self.model.objects.get(id=line['id'])
810 for line in check_data
811 }
813 # The import must be rejected with a permissions error (form re-rendered) and no object modified
814 response = self.client.post(self._get_url('bulk_import'), data)
815 self.assertHttpStatus(response, 200)
816 self.assertContains(response, 'Remove the ID column to create new objects instead.')
817 self.assertEqual(initial_count, self._get_queryset().count())
818 for line in check_data:
819 obj = self.model.objects.get(id=line['id'])
820 for attr in line:
821 if attr == 'id':
822 continue
823 # Skip relational fields (FK/M2M), consistent with test_bulk_update_objects_with_permission
824 if self.model._meta.get_field(attr).is_relation:
825 continue
826 self.assertEqual(getattr(obj, attr), getattr(before[line['id']], attr))
828 @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
829 def test_bulk_update_objects_with_permission(self):
830 if not hasattr(self, 'csv_update_data'):
831 raise NotImplementedError(_("The test must define csv_update_data."))
833 initial_count = self._get_queryset().count()
834 array, csv_data = self._get_update_csv_data()
835 data = {
836 'format': ImportFormatChoices.CSV,
837 'data': csv_data,
838 'csv_delimiter': CSVDelimiterChoices.AUTO,
839 }
841 # Updating existing objects requires both 'add' (to reach the view) and 'change' (to update)
842 obj_perm = ObjectPermission(
843 name='Test permission',
844 actions=['add', 'change']
845 )
846 obj_perm.save()
847 obj_perm.users.add(self.user)
848 obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model))
850 # Test POST with permission
851 self.assertHttpStatus(self.client.post(self._get_url('bulk_import'), data), 302)
852 self.assertEqual(initial_count, self._get_queryset().count())
854 # Verify that each object was actually updated to match the value specified in the CSV
855 reader = csv.DictReader(array, delimiter=',')
856 check_data = list(reader)
857 for line in check_data:
858 obj = self.model.objects.get(id=line["id"])
859 for attr, expected in line.items():
860 if attr == "id":
861 continue
862 field = self.model._meta.get_field(attr)
863 # Skip relational fields (FK/M2M): the CSV value can't be mapped to a comparable attribute
864 if field.is_relation:
865 continue
866 actual = getattr(obj, attr)
867 # Only verify simple scalar values against the raw CSV string; skip lists and other complex
868 # representations that the import form transforms (e.g. choice-set extra_choices).
869 if not isinstance(actual, (str, int, float)):
870 continue
871 # Compare case-insensitively to tolerate values normalized on save (e.g. MAC addresses)
872 self.assertEqual(str(actual).lower(), str(expected).lower())
874 @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'], EXEMPT_EXCLUDE_MODELS=[])
875 def test_bulk_import_objects_with_constrained_permission(self, post_import_callback=None):
876 # Assign constrained permission (deny all initially)
877 obj_perm = ObjectPermission(
878 name='Test permission',
879 constraints={'pk': 0}, # Dummy permission to deny all
880 actions=['add'],
881 )
882 obj_perm.save()
883 obj_perm.users.add(self.user)
884 obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model))
886 # Test each scenario with constrained permissions
887 for scenario_name in self.get_scenarios():
888 with self.cleanupSubTest(scenario=scenario_name):
889 self._test_bulk_import_constrained_scenario(scenario_name, obj_perm)
891 if post_import_callback:
892 post_import_callback(scenario_name)
894 def _test_bulk_import_constrained_scenario(self, scenario_name, obj_perm):
895 """
896 Helper method to test a single bulk import scenario with constrained permissions.
897 """
898 initial_count = self._get_queryset().count()
900 # Get CSV data for this scenario
901 scenario_data = self._get_csv_data(scenario_name)
902 expected_new_objects = len(scenario_data.splitlines()) - 1
904 data = {
905 'data': scenario_data,
906 'format': ImportFormatChoices.CSV,
907 'csv_delimiter': CSVDelimiterChoices.AUTO,
908 }
910 # Attempt to import non-permitted objects (should fail)
911 self.assertHttpStatus(self.client.post(self._get_url('bulk_import'), data), 200)
912 self.assertEqual(self._get_queryset().count(), initial_count)
914 # Update permission constraints to allow all
915 obj_perm.constraints = {'pk__gt': 0} # Dummy permission to allow all
916 obj_perm.save()
918 # Import permitted objects (should succeed)
919 self.assertHttpStatus(self.client.post(self._get_url('bulk_import'), data), 302)
920 self.assertEqual(self._get_queryset().count(), initial_count + expected_new_objects)
922 class BulkEditObjectsViewTestCase(ModelViewTestCase):
923 """
924 Edit multiple instances.
926 :bulk_edit_data: A dictionary of data to be used when bulk editing a set of objects. This data should differ
927 from that used for initial object creation within setUpTestData().
928 """
929 bulk_edit_data = {}
931 def test_bulk_edit_objects_without_permission(self):
932 pk_list = self._get_queryset().values_list('pk', flat=True)[:3]
933 data = {
934 'pk': pk_list,
935 '_apply': True, # Form button
936 }
938 # Test GET without permission
939 with disable_warnings('django.request'):
940 self.assertHttpStatus(self.client.get(self._get_url('bulk_edit')), 403)
942 # Try POST without permission
943 with disable_warnings('django.request'):
944 self.assertHttpStatus(self.client.post(self._get_url('bulk_edit'), data), 403)
946 @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'], EXEMPT_EXCLUDE_MODELS=[])
947 def test_bulk_edit_objects_with_permission(self):
948 pk_list = list(self._get_queryset().values_list('pk', flat=True)[:3])
949 data = {
950 'pk': pk_list,
951 '_apply': True, # Form button
952 }
954 # If supported, add a changelog message
955 if issubclass(self.model, ChangeLoggingMixin):
956 data['changelog_message'] = get_random_string(10)
958 # Append the form data to the request
959 data.update(post_data(self.bulk_edit_data))
961 # Assign model-level permission
962 obj_perm = ObjectPermission(
963 name='Test permission',
964 actions=['view', 'change']
965 )
966 obj_perm.save()
967 obj_perm.users.add(self.user)
968 obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model))
970 # Try POST with model-level permission
971 response = self.client.post(self._get_url('bulk_edit'), data)
972 self.assertHttpStatus(response, 302)
973 for i, instance in enumerate(self._get_queryset().filter(pk__in=pk_list)):
974 self.assertInstanceEqual(instance, self.bulk_edit_data)
976 # Verify ObjectChange creation
977 if issubclass(self.model, ChangeLoggingMixin):
978 request_id = response.headers.get('X-Request-ID')
979 self.assertIsNotNone(request_id, "Unable to determine request ID from response")
980 objectchanges = ObjectChange.objects.filter(
981 changed_object_type=ContentType.objects.get_for_model(self.model),
982 changed_object_id__in=pk_list
983 )
984 self.assertEqual(len(objectchanges), len(pk_list))
985 for oc in objectchanges:
986 self.assertObjectChange(oc, action=ObjectChangeActionChoices.ACTION_UPDATE,
987 message=data['changelog_message'])
989 @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'], EXEMPT_EXCLUDE_MODELS=[])
990 def test_bulk_edit_objects_with_constrained_permission(self):
991 pk_list = list(self._get_queryset().values_list('pk', flat=True)[:3])
992 data = {
993 'pk': pk_list,
994 '_apply': True, # Form button
995 }
997 # Append the form data to the request
998 data.update(post_data(self.bulk_edit_data))
1000 # Dynamically determine a constraint that will *not* be matched by the updated objects.
1001 attr_name = list(self.bulk_edit_data.keys())[0]
1002 field = self.model._meta.get_field(attr_name)
1003 value = field.value_from_object(self._get_queryset().first())
1005 # Assign constrained permission
1006 obj_perm = ObjectPermission(
1007 name='Test permission',
1008 constraints={attr_name: value},
1009 actions=['view', 'change']
1010 )
1011 obj_perm.save()
1012 obj_perm.users.add(self.user)
1013 obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model))
1015 # Attempt to bulk edit permitted objects into a non-permitted state
1016 response = self.client.post(self._get_url('bulk_edit'), data)
1017 self.assertHttpStatus(response, 200)
1019 # Update permission constraints
1020 obj_perm.constraints = {'pk__gt': 0}
1021 obj_perm.save()
1023 # Bulk edit permitted objects
1024 self.assertHttpStatus(self.client.post(self._get_url('bulk_edit'), data), 302)
1025 for i, instance in enumerate(self._get_queryset().filter(pk__in=pk_list)):
1026 self.assertInstanceEqual(instance, self.bulk_edit_data)
1028 class BulkDeleteObjectsViewTestCase(ModelViewTestCase):
1029 """
1030 Delete multiple instances.
1031 """
1032 def test_bulk_delete_objects_without_permission(self):
1033 pk_list = self._get_queryset().values_list('pk', flat=True)[:3]
1034 data = {
1035 'pk': pk_list,
1036 'confirm': True,
1037 '_confirm': True, # Form button
1038 }
1040 # Test GET without permission
1041 with disable_warnings('django.request'):
1042 self.assertHttpStatus(self.client.get(self._get_url('bulk_delete')), 403)
1044 # Try POST without permission
1045 with disable_warnings('django.request'):
1046 self.assertHttpStatus(self.client.post(self._get_url('bulk_delete'), data), 403)
1048 def test_bulk_delete_objects_with_permission(self):
1049 pk_list = list(self._get_queryset().values_list('pk', flat=True))[:3]
1050 data = {
1051 'pk': pk_list,
1052 'confirm': True,
1053 '_confirm': True, # Form button
1054 }
1056 # If supported, add a changelog message
1057 if issubclass(self.model, ChangeLoggingMixin):
1058 data['changelog_message'] = get_random_string(10)
1060 # Assign unconstrained permission
1061 obj_perm = ObjectPermission(
1062 name='Test permission',
1063 actions=['delete']
1064 )
1065 obj_perm.save()
1066 obj_perm.users.add(self.user)
1067 obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model))
1069 # Try POST with model-level permission
1070 response = self.client.post(self._get_url('bulk_delete'), data)
1071 self.assertHttpStatus(response, 302)
1072 self.assertFalse(self._get_queryset().filter(pk__in=pk_list).exists())
1074 # Verify ObjectChange creation
1075 if issubclass(self.model, ChangeLoggingMixin):
1076 objectchanges = ObjectChange.objects.filter(
1077 changed_object_type=ContentType.objects.get_for_model(self.model),
1078 changed_object_id__in=pk_list
1079 )
1080 self.assertEqual(len(objectchanges), len(pk_list))
1081 for oc in objectchanges:
1082 self.assertObjectChange(oc, action=ObjectChangeActionChoices.ACTION_DELETE,
1083 message=data['changelog_message'])
1085 def test_bulk_delete_objects_with_constrained_permission(self):
1086 pk_list = self._get_queryset().values_list('pk', flat=True)
1087 data = {
1088 'pk': pk_list,
1089 'confirm': True,
1090 '_confirm': True, # Form button
1091 }
1093 # Assign constrained permission
1094 obj_perm = ObjectPermission(
1095 name='Test permission',
1096 constraints={'pk': 0}, # Dummy permission to deny all
1097 actions=['delete']
1098 )
1099 obj_perm.save()
1100 obj_perm.users.add(self.user)
1101 obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model))
1103 # Attempt to bulk delete non-permitted objects
1104 initial_count = self._get_queryset().count()
1105 self.assertHttpStatus(self.client.post(self._get_url('bulk_delete'), data), 302)
1106 self.assertEqual(self._get_queryset().count(), initial_count)
1108 # Update permission constraints
1109 obj_perm.constraints = {'pk__gt': 0} # Dummy permission to allow all
1110 obj_perm.save()
1112 # Bulk delete permitted objects
1113 self.assertHttpStatus(self.client.post(self._get_url('bulk_delete'), data), 302)
1114 self.assertEqual(self._get_queryset().count(), 0)
1116 class BulkRenameObjectsViewTestCase(ModelViewTestCase):
1117 """
1118 Rename multiple instances.
1119 """
1120 rename_data = {
1121 'find': '^(.*)$',
1122 'replace': '\\1X', # Append an X to the original value
1123 'use_regex': True,
1124 }
1126 def test_bulk_rename_objects_without_permission(self):
1127 pk_list = self._get_queryset().values_list('pk', flat=True)[:3]
1128 data = {
1129 'pk': pk_list,
1130 '_apply': True, # Form button
1131 }
1132 data.update(self.rename_data)
1134 # Test GET without permission
1135 with disable_warnings('django.request'):
1136 self.assertHttpStatus(self.client.get(self._get_url('bulk_rename')), 403)
1138 # Try POST without permission
1139 with disable_warnings('django.request'):
1140 self.assertHttpStatus(self.client.post(self._get_url('bulk_rename'), data), 403)
1142 @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
1143 def test_bulk_rename_objects_with_permission(self):
1144 objects = self._get_queryset().all()[:3]
1145 pk_list = [obj.pk for obj in objects]
1146 data = {
1147 'pk': pk_list,
1148 '_apply': True, # Form button
1149 'field_names': ['name'],
1150 }
1151 data.update(self.rename_data)
1153 # Assign model-level permission
1154 obj_perm = ObjectPermission(
1155 name='Test permission',
1156 actions=['change']
1157 )
1158 obj_perm.save()
1159 obj_perm.users.add(self.user)
1160 obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model))
1162 # Try POST with model-level permission
1163 self.assertHttpStatus(self.client.post(self._get_url('bulk_rename'), data), 302)
1164 for i, instance in enumerate(self._get_queryset().filter(pk__in=pk_list)):
1165 self.assertEqual(instance.name, f'{objects[i].name}X')
1167 @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
1168 def test_bulk_rename_objects_with_changelog_message(self):
1169 if not issubclass(self.model, ChangeLoggingMixin):
1170 self.skipTest("Model does not support change logging")
1171 objects = self._get_queryset().all()[:3]
1172 pk_list = [obj.pk for obj in objects]
1173 data = {
1174 'pk': pk_list,
1175 '_apply': True,
1176 'changelog_message': 'Bulk rename test message',
1177 'field_names': ['name'],
1178 }
1179 data.update(self.rename_data)
1181 # Assign model-level permission
1182 obj_perm = ObjectPermission(
1183 name='Test permission',
1184 actions=['change']
1185 )
1186 obj_perm.save()
1187 obj_perm.users.add(self.user)
1188 obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model))
1190 self.assertHttpStatus(self.client.post(self._get_url('bulk_rename'), data), 302)
1192 # Verify changelog message was recorded on each renamed object
1193 object_type = ObjectType.objects.get_for_model(self.model)
1194 for pk in pk_list:
1195 oc = ObjectChange.objects.filter(
1196 changed_object_type=object_type,
1197 changed_object_id=pk,
1198 action=ObjectChangeActionChoices.ACTION_UPDATE,
1199 ).order_by('-time').first()
1200 self.assertIsNotNone(oc)
1201 self.assertEqual(oc.message, 'Bulk rename test message')
1203 @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
1204 def test_bulk_rename_objects_with_constrained_permission(self):
1205 objects = self._get_queryset().all()[:3]
1206 pk_list = [obj.pk for obj in objects]
1207 data = {
1208 'pk': pk_list,
1209 '_apply': True, # Form button
1210 'field_names': ['name'],
1211 }
1212 data.update(self.rename_data)
1214 # Assign constrained permission
1215 obj_perm = ObjectPermission(
1216 name='Test permission',
1217 constraints={'name__regex': '[^X]$'},
1218 actions=['change']
1219 )
1220 obj_perm.save()
1221 obj_perm.users.add(self.user)
1222 obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model))
1224 # Attempt to bulk edit permitted objects into a non-permitted state
1225 response = self.client.post(self._get_url('bulk_rename'), data)
1226 self.assertHttpStatus(response, 200)
1228 # Update permission constraints
1229 obj_perm.constraints = {'pk__gt': 0}
1230 obj_perm.save()
1232 # Bulk rename permitted objects
1233 self.assertHttpStatus(self.client.post(self._get_url('bulk_rename'), data), 302)
1234 for i, instance in enumerate(self._get_queryset().filter(pk__in=pk_list)):
1235 self.assertEqual(instance.name, f'{objects[i].name}X')
1237 @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
1238 def test_bulk_rename_label_field(self):
1239 """When field_names=['label'] is submitted, labels (not names) are updated."""
1240 if 'label' not in {f.name for f in self.model._meta.fields}:
1241 self.skipTest("Model does not have a label field")
1243 objects = self._get_queryset().all()[:3]
1244 pk_list = [obj.pk for obj in objects]
1245 original_labels = [obj.label for obj in objects]
1246 data = {
1247 'pk': pk_list,
1248 'field_names': ['label'],
1249 '_apply': True,
1250 }
1251 data.update(self.rename_data)
1253 obj_perm = ObjectPermission(name='Test permission', actions=['change'])
1254 obj_perm.save()
1255 obj_perm.users.add(self.user)
1256 obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model))
1258 self.assertHttpStatus(self.client.post(self._get_url('bulk_rename'), data), 302)
1259 for i, instance in enumerate(self._get_queryset().filter(pk__in=pk_list)):
1260 self.assertEqual(instance.label, f'{original_labels[i]}X')
1261 self.assertEqual(instance.name, objects[i].name)
1263 @override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
1264 def test_bulk_rename_name_and_label_fields(self):
1265 """When field_names=['name', 'label'] is submitted, both fields are updated simultaneously."""
1266 if 'label' not in {f.name for f in self.model._meta.fields}:
1267 self.skipTest("Model does not have a label field")
1269 objects = self._get_queryset().all()[:3]
1270 pk_list = [obj.pk for obj in objects]
1271 original_names = [obj.name for obj in objects]
1272 original_labels = [obj.label for obj in objects]
1273 data = {
1274 'pk': pk_list,
1275 'field_names': ['name', 'label'],
1276 '_apply': True,
1277 }
1278 data.update(self.rename_data)
1280 obj_perm = ObjectPermission(name='Test permission', actions=['change'])
1281 obj_perm.save()
1282 obj_perm.users.add(self.user)
1283 obj_perm.object_types.add(ObjectType.objects.get_for_model(self.model))
1285 self.assertHttpStatus(self.client.post(self._get_url('bulk_rename'), data), 302)
1286 for i, instance in enumerate(self._get_queryset().filter(pk__in=pk_list)):
1287 self.assertEqual(instance.name, f'{original_names[i]}X')
1288 self.assertEqual(instance.label, f'{original_labels[i]}X')
1290 class PrimaryObjectViewTestCase(
1291 GetObjectViewTestCase,
1292 GetObjectChangelogViewTestCase,
1293 CreateObjectViewTestCase,
1294 EditObjectViewTestCase,
1295 DeleteObjectViewTestCase,
1296 ListObjectsViewTestCase,
1297 BulkImportObjectsViewTestCase,
1298 BulkEditObjectsViewTestCase,
1299 BulkDeleteObjectsViewTestCase,
1300 ):
1301 """
1302 TestCase suitable for testing all standard View functions for primary objects
1303 """
1304 maxDiff = None
1306 class OrganizationalObjectViewTestCase(
1307 GetObjectViewTestCase,
1308 GetObjectChangelogViewTestCase,
1309 CreateObjectViewTestCase,
1310 EditObjectViewTestCase,
1311 DeleteObjectViewTestCase,
1312 ListObjectsViewTestCase,
1313 BulkImportObjectsViewTestCase,
1314 BulkEditObjectsViewTestCase,
1315 BulkDeleteObjectsViewTestCase,
1316 ):
1317 """
1318 TestCase suitable for all organizational objects
1319 """
1320 maxDiff = None
1322 class AdminModelViewTestCase(
1323 GetObjectViewTestCase,
1324 CreateObjectViewTestCase,
1325 EditObjectViewTestCase,
1326 DeleteObjectViewTestCase,
1327 ListObjectsViewTestCase,
1328 BulkImportObjectsViewTestCase,
1329 BulkEditObjectsViewTestCase,
1330 BulkDeleteObjectsViewTestCase,
1331 ):
1332 """
1333 TestCase suitable for testing all standard View functions for objects which inherit from AdminModel.
1334 """
1335 maxDiff = None
1337 class DeviceComponentTemplateViewTestCase(
1338 EditObjectViewTestCase,
1339 DeleteObjectViewTestCase,
1340 CreateMultipleObjectsViewTestCase,
1341 BulkEditObjectsViewTestCase,
1342 BulkRenameObjectsViewTestCase,
1343 BulkDeleteObjectsViewTestCase,
1344 ):
1345 """
1346 TestCase suitable for testing device component template models (ConsolePortTemplates, InterfaceTemplates, etc.)
1347 """
1348 maxDiff = None
1350 class DeviceComponentViewTestCase(
1351 GetObjectViewTestCase,
1352 GetObjectChangelogViewTestCase,
1353 EditObjectViewTestCase,
1354 DeleteObjectViewTestCase,
1355 ListObjectsViewTestCase,
1356 CreateMultipleObjectsViewTestCase,
1357 BulkImportObjectsViewTestCase,
1358 BulkEditObjectsViewTestCase,
1359 BulkRenameObjectsViewTestCase,
1360 BulkDeleteObjectsViewTestCase,
1361 ):
1362 """
1363 TestCase suitable for testing device component models (ConsolePorts, Interfaces, etc.)
1364 """
1365 maxDiff = None