nautobot 2.4.4__py3-none-any.whl → 2.4.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of nautobot might be problematic. Click here for more details.

Files changed (42) hide show
  1. nautobot/__init__.py +19 -3
  2. nautobot/core/celery/__init__.py +5 -3
  3. nautobot/core/jobs/__init__.py +3 -2
  4. nautobot/core/testing/__init__.py +2 -0
  5. nautobot/core/testing/mixins.py +9 -0
  6. nautobot/core/tests/test_jobs.py +26 -27
  7. nautobot/dcim/tests/test_jobs.py +4 -6
  8. nautobot/extras/choices.py +8 -3
  9. nautobot/extras/jobs.py +181 -103
  10. nautobot/extras/management/utils.py +13 -2
  11. nautobot/extras/models/datasources.py +4 -1
  12. nautobot/extras/models/jobs.py +20 -17
  13. nautobot/extras/tables.py +25 -29
  14. nautobot/extras/test_jobs/atomic_transaction.py +6 -6
  15. nautobot/extras/test_jobs/fail.py +75 -1
  16. nautobot/extras/tests/test_api.py +1 -1
  17. nautobot/extras/tests/test_datasources.py +64 -54
  18. nautobot/extras/tests/test_jobs.py +69 -62
  19. nautobot/extras/tests/test_models.py +1 -1
  20. nautobot/extras/tests/test_relationships.py +5 -5
  21. nautobot/extras/views.py +7 -1
  22. nautobot/ipam/forms.py +15 -0
  23. nautobot/ipam/querysets.py +6 -0
  24. nautobot/ipam/templates/ipam/rir.html +1 -43
  25. nautobot/ipam/tests/test_models.py +16 -0
  26. nautobot/ipam/urls.py +1 -21
  27. nautobot/ipam/views.py +24 -41
  28. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +43 -5
  29. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +35 -0
  30. nautobot/project-static/docs/development/jobs/index.html +27 -14
  31. nautobot/project-static/docs/objects.inv +0 -0
  32. nautobot/project-static/docs/release-notes/version-2.4.html +183 -0
  33. nautobot/project-static/docs/requirements.txt +1 -1
  34. nautobot/project-static/docs/search/search_index.json +1 -1
  35. nautobot/project-static/docs/sitemap.xml +290 -290
  36. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  37. {nautobot-2.4.4.dist-info → nautobot-2.4.5.dist-info}/METADATA +3 -3
  38. {nautobot-2.4.4.dist-info → nautobot-2.4.5.dist-info}/RECORD +42 -42
  39. {nautobot-2.4.4.dist-info → nautobot-2.4.5.dist-info}/LICENSE.txt +0 -0
  40. {nautobot-2.4.4.dist-info → nautobot-2.4.5.dist-info}/NOTICE +0 -0
  41. {nautobot-2.4.4.dist-info → nautobot-2.4.5.dist-info}/WHEEL +0 -0
  42. {nautobot-2.4.4.dist-info → nautobot-2.4.5.dist-info}/entry_points.txt +0 -0
nautobot/__init__.py CHANGED
@@ -14,18 +14,34 @@ __initialized = False
14
14
 
15
15
  def add_success_logger():
16
16
  """Add a custom log level for success messages."""
17
- SUCCESS = 25
17
+ SUCCESS = 25 # between INFO and WARNING
18
18
  logging.addLevelName(SUCCESS, "SUCCESS")
19
19
 
20
- def success(self, message, *args, **kws):
20
+ def success(self, message, *args, **kwargs):
21
+ kwargs["stacklevel"] = kwargs.get("stacklevel", 1) + 1 # so that funcName is the caller function, not "success"
21
22
  if self.isEnabledFor(SUCCESS):
22
- self._log(SUCCESS, message, args, **kws)
23
+ self._log(SUCCESS, message, args, **kwargs)
23
24
 
24
25
  logging.Logger.success = success
25
26
  return success
26
27
 
27
28
 
29
+ def add_failure_logger():
30
+ """Add a custom log level for failure messages less severe than an ERROR."""
31
+ FAILURE = 35 # between WARNING and ERROR
32
+ logging.addLevelName(FAILURE, "FAILURE")
33
+
34
+ def failure(self, message, *args, **kwargs):
35
+ kwargs["stacklevel"] = kwargs.get("stacklevel", 1) + 1 # so that funcName is the caller function, not "failure"
36
+ if self.isEnabledFor(FAILURE):
37
+ self._log(FAILURE, message, args, **kwargs)
38
+
39
+ logging.Logger.failure = failure
40
+ return failure
41
+
42
+
28
43
  add_success_logger()
44
+ add_failure_logger()
29
45
  logger = logging.getLogger(__name__)
30
46
 
31
47
 
@@ -14,7 +14,7 @@ from django.utils.module_loading import import_string
14
14
  from kombu.serialization import register
15
15
  from prometheus_client import CollectorRegistry, multiprocess, start_http_server
16
16
 
17
- from nautobot import add_success_logger
17
+ from nautobot import add_failure_logger, add_success_logger
18
18
  from nautobot.core.celery.control import discard_git_repository, refresh_git_repository # noqa: F401 # unused-import
19
19
  from nautobot.core.celery.encoders import NautobotKombuJSONEncoder
20
20
  from nautobot.core.celery.log import NautobotDatabaseHandler
@@ -138,14 +138,16 @@ def add_nautobot_log_handler(logger_instance, log_format=None):
138
138
 
139
139
  @signals.after_setup_logger.connect
140
140
  def setup_nautobot_global_logging(logger, **kwargs): # pylint: disable=redefined-outer-name
141
- """Add SUCCESS log to celery global logger."""
141
+ """Add SUCCESS and FAILURE logs to celery global logger."""
142
142
  logger.success = add_success_logger()
143
+ logger.failure = add_failure_logger()
143
144
 
144
145
 
145
146
  @signals.after_setup_task_logger.connect
146
147
  def setup_nautobot_task_logging(logger, **kwargs): # pylint: disable=redefined-outer-name
147
- """Add SUCCESS log to celery task logger."""
148
+ """Add SUCCESS and FAILURE logs to celery task logger."""
148
149
  logger.success = add_success_logger()
150
+ logger.failure = add_failure_logger()
149
151
 
150
152
 
151
153
  @signals.celeryd_after_setup.connect
@@ -295,8 +295,9 @@ class ImportObjects(Job):
295
295
  validation_failed = True
296
296
  else:
297
297
  validation_failed = True
298
- for field, err in serializer.errors.items():
299
- self.logger.error("Row %d: `%s`: `%s`", row, field, err[0])
298
+ for field, errs in serializer.errors.items():
299
+ for err in errs:
300
+ self.logger.error("Row %d: `%s`: `%s`", row, field, err)
300
301
  return new_objs, validation_failed
301
302
 
302
303
  def run(self, *, content_type, csv_data=None, csv_file=None, roll_back_if_error=False): # pylint:disable=arguments-differ
@@ -68,6 +68,8 @@ def run_job_for_testing(job, username="test-user", profile=False, **kwargs):
68
68
  username=username, defaults={"is_superuser": True, "password": "password"}
69
69
  )
70
70
  # Run the job synchronously in the current thread as if it were being executed by a worker
71
+ # TODO: in Nautobot core testing, we set `CELERY_TASK_ALWAYS_EAGER = True`, so we *could* use enqueue_job() instead,
72
+ # but switching now would be a potentially breaking change for apps...
71
73
  job_result = JobResult.execute_job(
72
74
  job_model=job,
73
75
  user=user_instance,
@@ -18,6 +18,7 @@ from nautobot.core.models import fields as core_fields
18
18
  from nautobot.core.testing import utils
19
19
  from nautobot.core.utils import permissions
20
20
  from nautobot.extras import management, models as extras_models
21
+ from nautobot.extras.choices import JobResultStatusChoices
21
22
  from nautobot.users import models as users_models
22
23
 
23
24
  # Use the proper swappable User model
@@ -188,6 +189,14 @@ class NautobotTestCaseMixin:
188
189
  err_message = f"{msg}\n{err_message}"
189
190
  self.assertIn(response.status_code, expected_status, err_message)
190
191
 
192
+ def assertJobResultStatus(self, job_result, expected_status=JobResultStatusChoices.STATUS_SUCCESS):
193
+ """Assert that the given job_result has the expected_status, or print the job logs to aid in debugging."""
194
+ self.assertEqual(
195
+ job_result.status,
196
+ expected_status,
197
+ (job_result.traceback, list(job_result.job_log_entries.values_list("message", flat=True))),
198
+ )
199
+
191
200
  def assertInstanceEqual(self, instance, data, exclude=None, api=False):
192
201
  """
193
202
  Compare a model instance to a dictionary, checking that its attribute values match those specified
@@ -48,7 +48,7 @@ class ExportObjectListTest(TransactionTestCase):
48
48
  username=self.user.username, # otherwise run_job_for_testing defaults to a superuser account
49
49
  content_type=ContentType.objects.get_for_model(Status).pk,
50
50
  )
51
- self.assertEqual(job_result.status, JobResultStatusChoices.STATUS_FAILURE)
51
+ self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_FAILURE)
52
52
  log_error = JobLogEntry.objects.get(job_result=job_result, log_level=LogLevelChoices.LOG_ERROR)
53
53
  self.assertEqual(log_error.message, f'User "{self.user}" does not have permission to view status objects')
54
54
  self.assertFalse(job_result.files.exists())
@@ -70,7 +70,7 @@ class ExportObjectListTest(TransactionTestCase):
70
70
  username=self.user.username, # otherwise run_job_for_testing defaults to a superuser account
71
71
  content_type=ContentType.objects.get_for_model(Status).pk,
72
72
  )
73
- self.assertEqual(job_result.status, JobResultStatusChoices.STATUS_SUCCESS)
73
+ self.assertJobResultStatus(job_result)
74
74
  self.assertTrue(job_result.files.exists())
75
75
  self.assertEqual(Path(job_result.files.first().file.name).name, "nautobot_statuses.csv")
76
76
  csv_bytes = job_result.files.first().file.read()
@@ -86,7 +86,7 @@ class ExportObjectListTest(TransactionTestCase):
86
86
  "ExportObjectList",
87
87
  content_type=ContentType.objects.get_for_model(Status).pk,
88
88
  )
89
- self.assertEqual(job_result.status, JobResultStatusChoices.STATUS_SUCCESS)
89
+ self.assertJobResultStatus(job_result)
90
90
  self.assertTrue(job_result.files.exists())
91
91
  self.assertEqual(Path(job_result.files.first().file.name).name, "nautobot_statuses.csv")
92
92
  csv_data = job_result.files.first().file.read().decode("utf-8")
@@ -107,7 +107,7 @@ class ExportObjectListTest(TransactionTestCase):
107
107
  content_type=ContentType.objects.get_for_model(Status).pk,
108
108
  export_template=et.pk,
109
109
  )
110
- self.assertEqual(job_result.status, JobResultStatusChoices.STATUS_SUCCESS)
110
+ self.assertJobResultStatus(job_result)
111
111
  self.assertTrue(job_result.files.exists())
112
112
  self.assertEqual(Path(job_result.files.first().file.name).name, "nautobot_statuses.txt")
113
113
  text_data = job_result.files.first().file.read().decode("utf-8")
@@ -129,7 +129,7 @@ class ExportObjectListTest(TransactionTestCase):
129
129
  content_type=ContentType.objects.get_for_model(DeviceType).pk,
130
130
  export_format="yaml",
131
131
  )
132
- self.assertEqual(job_result.status, JobResultStatusChoices.STATUS_SUCCESS)
132
+ self.assertJobResultStatus(job_result)
133
133
  self.assertTrue(job_result.files.exists())
134
134
  self.assertEqual(Path(job_result.files.first().file.name).name, "nautobot_device_types.yaml")
135
135
  yaml_data = job_result.files.first().file.read().decode("utf-8")
@@ -159,7 +159,7 @@ class ImportObjectsTestCase(TransactionTestCase):
159
159
  content_type=ContentType.objects.get_for_model(Status).pk,
160
160
  csv_data=self.csv_data,
161
161
  )
162
- self.assertEqual(job_result.status, JobResultStatusChoices.STATUS_FAILURE)
162
+ self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_FAILURE)
163
163
  log_error = JobLogEntry.objects.get(job_result=job_result, log_level=LogLevelChoices.LOG_ERROR)
164
164
  self.assertEqual(log_error.message, f'User "{self.user}" does not have permission to create status objects')
165
165
  self.assertFalse(Status.objects.filter(name__startswith="test_status").exists())
@@ -172,7 +172,7 @@ class ImportObjectsTestCase(TransactionTestCase):
172
172
  username=self.user.username, # otherwise run_job_for_testing defaults to a superuser account
173
173
  content_type=ContentType.objects.get_for_model(Status).pk,
174
174
  )
175
- self.assertEqual(job_result.status, JobResultStatusChoices.STATUS_FAILURE)
175
+ self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_FAILURE)
176
176
 
177
177
  def test_csv_import_with_constrained_permission(self):
178
178
  """Job should only allow the user to import objects they have permission to add."""
@@ -191,7 +191,7 @@ class ImportObjectsTestCase(TransactionTestCase):
191
191
  content_type=ContentType.objects.get_for_model(Status).pk,
192
192
  csv_data=self.csv_data,
193
193
  )
194
- self.assertEqual(job_result.status, JobResultStatusChoices.STATUS_FAILURE)
194
+ self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_FAILURE)
195
195
  log_successes = JobLogEntry.objects.filter(
196
196
  job_result=job_result, log_level=LogLevelChoices.LOG_INFO, message__icontains="created"
197
197
  )
@@ -220,7 +220,7 @@ class ImportObjectsTestCase(TransactionTestCase):
220
220
  content_type=ContentType.objects.get_for_model(Status).pk,
221
221
  csv_data=self.csv_data,
222
222
  )
223
- self.assertEqual(job_result.status, JobResultStatusChoices.STATUS_SUCCESS)
223
+ self.assertJobResultStatus(job_result)
224
224
  self.assertFalse(
225
225
  JobLogEntry.objects.filter(job_result=job_result, log_level=LogLevelChoices.LOG_WARNING).exists()
226
226
  )
@@ -246,7 +246,7 @@ class ImportObjectsTestCase(TransactionTestCase):
246
246
  content_type=ContentType.objects.get_for_model(Prefix).pk,
247
247
  csv_file=csv_file.id,
248
248
  )
249
- self.assertEqual(job_result.status, JobResultStatusChoices.STATUS_SUCCESS)
249
+ self.assertJobResultStatus(job_result)
250
250
  self.assertFalse(
251
251
  JobLogEntry.objects.filter(job_result=job_result, log_level=LogLevelChoices.LOG_WARNING).exists()
252
252
  )
@@ -287,7 +287,7 @@ class ImportObjectsTestCase(TransactionTestCase):
287
287
  content_type=ContentType.objects.get_for_model(Device).pk,
288
288
  csv_file=csv_file.id,
289
289
  )
290
- self.assertEqual(job_result.status, JobResultStatusChoices.STATUS_SUCCESS)
290
+ self.assertJobResultStatus(job_result)
291
291
  self.assertFalse(
292
292
  JobLogEntry.objects.filter(job_result=job_result, log_level=LogLevelChoices.LOG_WARNING).exists()
293
293
  )
@@ -313,7 +313,7 @@ class ImportObjectsTestCase(TransactionTestCase):
313
313
  csv_data=csv_data,
314
314
  roll_back_if_error=True,
315
315
  )
316
- self.assertEqual(job_result.status, JobResultStatusChoices.STATUS_FAILURE)
316
+ self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_FAILURE)
317
317
  log_info = JobLogEntry.objects.filter(
318
318
  job_result=job_result, log_level=LogLevelChoices.LOG_INFO, message__icontains="created"
319
319
  )
@@ -335,7 +335,7 @@ class ImportObjectsTestCase(TransactionTestCase):
335
335
  content_type=ContentType.objects.get_for_model(Status).pk,
336
336
  csv_data=csv_data,
337
337
  )
338
- self.assertEqual(job_result.status, JobResultStatusChoices.STATUS_FAILURE)
338
+ self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_FAILURE)
339
339
  log_errors = JobLogEntry.objects.filter(job_result=job_result, log_level=LogLevelChoices.LOG_ERROR)
340
340
  self.assertEqual(log_errors[0].message, "Row 1: `color`: `Enter a valid hexadecimal RGB color code.`")
341
341
  self.assertFalse(Status.objects.filter(name="test_status0").exists())
@@ -375,7 +375,7 @@ class ImportObjectsTestCase(TransactionTestCase):
375
375
  content_type=ContentType.objects.get_for_model(LocationType).pk,
376
376
  csv_data=location_types_csv,
377
377
  )
378
- self.assertEqual(location_types_job_result.status, JobResultStatusChoices.STATUS_SUCCESS)
378
+ self.assertJobResultStatus(location_types_job_result)
379
379
 
380
380
  location_type_count = LocationType.objects.filter(name="ContactAssignmentImportTestLocationType").count()
381
381
  self.assertEqual(location_type_count, 1, f"Unexpected count of LocationTypes {location_type_count}")
@@ -386,7 +386,7 @@ class ImportObjectsTestCase(TransactionTestCase):
386
386
  content_type=ContentType.objects.get_for_model(Location).pk,
387
387
  csv_data=locations_csv,
388
388
  )
389
- self.assertEqual(locations_job_result.status, JobResultStatusChoices.STATUS_SUCCESS)
389
+ self.assertJobResultStatus(locations_job_result)
390
390
 
391
391
  location_count = Location.objects.filter(location_type__name="ContactAssignmentImportTestLocationType").count()
392
392
  self.assertEqual(location_count, 2, f"Unexpected count of Locations {location_count}")
@@ -397,7 +397,7 @@ class ImportObjectsTestCase(TransactionTestCase):
397
397
  content_type=ContentType.objects.get_for_model(Contact).pk,
398
398
  csv_data=contacts_csv,
399
399
  )
400
- self.assertEqual(contacts_job_result.status, JobResultStatusChoices.STATUS_SUCCESS)
400
+ self.assertJobResultStatus(contacts_job_result)
401
401
 
402
402
  contact_count = Contact.objects.filter(name="Bob-ContactAssignmentImportTestLocation").count()
403
403
  self.assertEqual(contact_count, 1, f"Unexpected number of contacts {contact_count}")
@@ -408,7 +408,7 @@ class ImportObjectsTestCase(TransactionTestCase):
408
408
  content_type=ContentType.objects.get_for_model(Role).pk,
409
409
  csv_data=roles_csv,
410
410
  )
411
- self.assertEqual(roles_job_result.status, JobResultStatusChoices.STATUS_SUCCESS)
411
+ self.assertJobResultStatus(roles_job_result)
412
412
 
413
413
  role_count = Role.objects.filter(name="ContactAssignmentImportTestLocation-On Site").count()
414
414
  self.assertEqual(role_count, 1, f"Unexpected number of role values {role_count}")
@@ -426,8 +426,7 @@ class ImportObjectsTestCase(TransactionTestCase):
426
426
  content_type=ContentType.objects.get_for_model(ContactAssociation).pk,
427
427
  csv_data=associations_csv,
428
428
  )
429
-
430
- self.assertEqual(associations_job_result.status, JobResultStatusChoices.STATUS_SUCCESS)
429
+ self.assertJobResultStatus(associations_job_result)
431
430
 
432
431
 
433
432
  class LogsCleanupTestCase(TransactionTestCase):
@@ -469,7 +468,7 @@ class LogsCleanupTestCase(TransactionTestCase):
469
468
  cleanup_types=[CleanupTypes.JOB_RESULT],
470
469
  max_age=0,
471
470
  )
472
- self.assertEqual(job_result.status, JobResultStatusChoices.STATUS_FAILURE)
471
+ self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_FAILURE)
473
472
  log_error = JobLogEntry.objects.get(job_result=job_result, log_level=LogLevelChoices.LOG_ERROR)
474
473
  self.assertEqual(log_error.message, f'User "{self.user}" does not have permission to delete JobResult records')
475
474
  self.assertEqual(JobResult.objects.count(), job_result_count + 1)
@@ -484,7 +483,7 @@ class LogsCleanupTestCase(TransactionTestCase):
484
483
  cleanup_types=[CleanupTypes.OBJECT_CHANGE],
485
484
  max_age=0,
486
485
  )
487
- self.assertEqual(job_result.status, JobResultStatusChoices.STATUS_FAILURE)
486
+ self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_FAILURE)
488
487
  log_error = JobLogEntry.objects.get(job_result=job_result, log_level=LogLevelChoices.LOG_ERROR)
489
488
  self.assertEqual(
490
489
  log_error.message, f'User "{self.user}" does not have permission to delete ObjectChange records'
@@ -532,7 +531,7 @@ class LogsCleanupTestCase(TransactionTestCase):
532
531
  cleanup_types=[CleanupTypes.JOB_RESULT, CleanupTypes.OBJECT_CHANGE],
533
532
  max_age=0,
534
533
  )
535
- self.assertEqual(job_result.status, JobResultStatusChoices.STATUS_SUCCESS)
534
+ self.assertJobResultStatus(job_result)
536
535
  self.assertEqual(job_result.result["extras.JobResult"], 1)
537
536
  self.assertEqual(job_result.result["extras.ObjectChange"], 1)
538
537
  with self.assertRaises(JobResult.DoesNotExist):
@@ -629,7 +628,7 @@ class BulkEditTestCase(TransactionTestCase):
629
628
  tag.content_types.add(self.namespace_ct)
630
629
 
631
630
  def _common_no_error_test_assertion(self, model, job_result, expected_count, **filter_params):
632
- self.assertEqual(job_result.status, JobResultStatusChoices.STATUS_SUCCESS)
631
+ self.assertJobResultStatus(job_result)
633
632
  self.assertEqual(model.objects.filter(**filter_params).count(), expected_count)
634
633
  self.assertFalse(
635
634
  JobLogEntry.objects.filter(job_result=job_result, log_level=LogLevelChoices.LOG_WARNING).exists()
@@ -650,7 +649,7 @@ class BulkEditTestCase(TransactionTestCase):
650
649
  form_data={"pk": pk_list, "color": "aa1409"},
651
650
  username=self.user.username,
652
651
  )
653
- self.assertEqual(job_result.status, JobResultStatusChoices.STATUS_FAILURE)
652
+ self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_FAILURE)
654
653
  job_log = JobLogEntry.objects.get(job_result=job_result, log_level=LogLevelChoices.LOG_ERROR)
655
654
  self.assertEqual(job_log.message, f'User "{self.user}" does not have permission to update status objects')
656
655
 
@@ -961,7 +960,7 @@ class BulkDeleteTestCase(TransactionTestCase):
961
960
  )
962
961
 
963
962
  def _common_no_error_test_assertion(self, model, job_result, **filter_params):
964
- self.assertEqual(job_result.status, JobResultStatusChoices.STATUS_SUCCESS)
963
+ self.assertJobResultStatus(job_result)
965
964
  self.assertEqual(model.objects.filter(**filter_params).count(), 0)
966
965
  self.assertFalse(
967
966
  JobLogEntry.objects.filter(job_result=job_result, log_level=LogLevelChoices.LOG_WARNING).exists()
@@ -979,7 +978,7 @@ class BulkDeleteTestCase(TransactionTestCase):
979
978
  pk_list=statuses_to_delete,
980
979
  username=self.user.username,
981
980
  )
982
- self.assertEqual(job_result.status, JobResultStatusChoices.STATUS_FAILURE)
981
+ self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_FAILURE)
983
982
  job_log = JobLogEntry.objects.get(job_result=job_result, log_level=LogLevelChoices.LOG_ERROR)
984
983
  self.assertEqual(job_log.message, f'User "{self.user}" does not have permission to delete status objects')
985
984
  self.assertEqual(Status.objects.filter(pk__in=statuses_to_delete).count(), len(statuses_to_delete))
@@ -1002,7 +1001,7 @@ class BulkDeleteTestCase(TransactionTestCase):
1002
1001
  pk_list=statuses_to_delete,
1003
1002
  username=self.user.username,
1004
1003
  )
1005
- self.assertEqual(job_result.status, JobResultStatusChoices.STATUS_FAILURE)
1004
+ self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_FAILURE)
1006
1005
  error_log = JobLogEntry.objects.get(job_result=job_result, log_level=LogLevelChoices.LOG_ERROR)
1007
1006
  self.assertEqual(
1008
1007
  error_log.message, "You do not have permissions to delete some of the objects provided in `pk_list`."
@@ -71,7 +71,7 @@ def create_common_data_for_software_related_test_cases():
71
71
  class TestSoftwareImageFileTestCase(TransactionTestCase):
72
72
  def test_correct_handling_for_model_protected_error(self):
73
73
  create_common_data_for_software_related_test_cases()
74
- software_image_file = SoftwareImageFile.objects.first()
74
+ software_image_file = SoftwareImageFile.objects.get(image_file_name="software_image_file_qs_test_1.bin")
75
75
 
76
76
  self.add_permissions("dcim.delete_softwareimagefile")
77
77
  pk_list = [str(software_image_file.pk)]
@@ -86,9 +86,7 @@ class TestSoftwareImageFileTestCase(TransactionTestCase):
86
86
  pk_list=pk_list,
87
87
  username=self.user.username,
88
88
  )
89
- logs = JobLogEntry.objects.filter(job_result=job_result)
90
- print([(log.message, log.log_level) for log in logs])
91
- self.assertEqual(job_result.status, JobResultStatusChoices.STATUS_FAILURE)
89
+ self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_FAILURE)
92
90
  error_log = JobLogEntry.objects.get(job_result=job_result, log_level=LogLevelChoices.LOG_ERROR)
93
91
  self.assertIn("Caught ProtectedError while attempting to delete objects", error_log.message)
94
92
  self.assertEqual(initial_count, SoftwareImageFile.objects.all().count())
@@ -97,7 +95,7 @@ class TestSoftwareImageFileTestCase(TransactionTestCase):
97
95
  class TestSoftwareVersionTestCase(TransactionTestCase):
98
96
  def test_correct_handling_for_model_protected_error(self):
99
97
  create_common_data_for_software_related_test_cases()
100
- software_version = SoftwareVersion.objects.first()
98
+ software_version = SoftwareVersion.objects.get(version="Test version 1.0.0")
101
99
 
102
100
  initial_count = SoftwareVersion.objects.all().count()
103
101
  self.add_permissions("dcim.delete_softwareversion")
@@ -112,7 +110,7 @@ class TestSoftwareVersionTestCase(TransactionTestCase):
112
110
  pk_list=pk_list,
113
111
  username=self.user.username,
114
112
  )
115
- self.assertEqual(job_result.status, JobResultStatusChoices.STATUS_FAILURE)
113
+ self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_FAILURE)
116
114
  error_log = JobLogEntry.objects.get(job_result=job_result, log_level=LogLevelChoices.LOG_ERROR)
117
115
  self.assertIn("Caught ProtectedError while attempting to delete objects", error_log.message)
118
116
  self.assertEqual(initial_count, SoftwareVersion.objects.all().count())
@@ -251,8 +251,10 @@ class JobResultStatusChoices(ChoiceSet):
251
251
  """
252
252
 
253
253
  STATUS_FAILURE = states.FAILURE
254
+ STATUS_IGNORED = states.IGNORED
254
255
  STATUS_PENDING = states.PENDING
255
256
  STATUS_RECEIVED = states.RECEIVED
257
+ STATUS_REJECTED = states.REJECTED
256
258
  STATUS_RETRY = states.RETRY
257
259
  STATUS_REVOKED = states.REVOKED
258
260
  STATUS_STARTED = states.STARTED
@@ -302,27 +304,30 @@ class JobResultStatusChoices(ChoiceSet):
302
304
  class LogLevelChoices(ChoiceSet):
303
305
  LOG_DEBUG = "debug"
304
306
  LOG_INFO = "info"
307
+ LOG_SUCCESS = "success"
305
308
  LOG_WARNING = "warning"
309
+ LOG_FAILURE = "failure"
306
310
  LOG_ERROR = "error"
307
311
  LOG_CRITICAL = "critical"
308
- LOG_SUCCESS = "success"
309
312
 
310
313
  CHOICES = (
311
314
  (LOG_DEBUG, "Debug"),
312
315
  (LOG_INFO, "Info"),
316
+ (LOG_SUCCESS, "Success"),
313
317
  (LOG_WARNING, "Warning"),
318
+ (LOG_FAILURE, "Failure"),
314
319
  (LOG_ERROR, "Error"),
315
320
  (LOG_CRITICAL, "Critical"),
316
- (LOG_SUCCESS, "Success"),
317
321
  )
318
322
 
319
323
  CSS_CLASSES = {
320
324
  LOG_DEBUG: "debug",
321
325
  LOG_INFO: "info",
326
+ LOG_SUCCESS: "success",
322
327
  LOG_WARNING: "warning",
328
+ LOG_FAILURE: "failure",
323
329
  LOG_ERROR: "error",
324
330
  LOG_CRITICAL: "critical",
325
- LOG_SUCCESS: "success",
326
331
  }
327
332
 
328
333