nautobot 3.0.0rc1__py3-none-any.whl → 3.0.1__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.
- nautobot/apps/forms.py +8 -0
- nautobot/apps/templatetags.py +231 -0
- nautobot/apps/testing.py +11 -1
- nautobot/apps/ui.py +21 -1
- nautobot/apps/utils.py +26 -1
- nautobot/core/celery/__init__.py +46 -1
- nautobot/core/cli/bootstrap_v3_to_v5.py +185 -44
- nautobot/core/cli/bootstrap_v3_to_v5_changes.yaml +314 -0
- nautobot/core/graphql/generators.py +2 -2
- nautobot/core/jobs/bulk_actions.py +12 -6
- nautobot/core/jobs/cleanup.py +13 -1
- nautobot/core/settings.py +13 -0
- nautobot/core/settings.yaml +22 -0
- nautobot/core/settings_funcs.py +11 -1
- nautobot/core/tables.py +19 -1
- nautobot/core/templates/components/panel/body_wrapper_generic_table.html +1 -1
- nautobot/core/templates/components/panel/header_extra_content_table.html +9 -3
- nautobot/core/templates/generic/object_create.html +1 -1
- nautobot/core/templates/inc/header.html +9 -10
- nautobot/core/templates/login.html +16 -1
- nautobot/core/templates/nautobot_config.py.j2 +14 -1
- nautobot/core/templates/redoc_ui.html +3 -0
- nautobot/core/templatetags/helpers.py +3 -3
- nautobot/core/testing/views.py +3 -1
- nautobot/core/tests/test_graphql.py +13 -0
- nautobot/core/tests/test_jobs.py +118 -0
- nautobot/core/tests/test_views.py +24 -0
- nautobot/core/ui/bulk_buttons.py +2 -3
- nautobot/core/utils/lookup.py +2 -3
- nautobot/core/utils/permissions.py +1 -1
- nautobot/core/views/generic.py +1 -0
- nautobot/core/views/mixins.py +37 -10
- nautobot/core/views/renderers.py +1 -0
- nautobot/core/views/utils.py +3 -3
- nautobot/data_validation/views.py +1 -9
- nautobot/dcim/forms.py +9 -9
- nautobot/dcim/models/devices.py +3 -3
- nautobot/dcim/tables/power.py +3 -0
- nautobot/dcim/templates/dcim/controllermanageddevicegroup_create.html +1 -1
- nautobot/dcim/views.py +30 -44
- nautobot/extras/api/views.py +14 -3
- nautobot/extras/choices.py +3 -0
- nautobot/extras/jobs.py +48 -2
- nautobot/extras/migrations/0132_approval_workflow_seed_data.py +127 -0
- nautobot/extras/models/approvals.py +11 -1
- nautobot/extras/models/models.py +19 -0
- nautobot/extras/models/relationships.py +3 -1
- nautobot/extras/tables.py +35 -18
- nautobot/extras/templates/extras/approval_workflow/approve.html +9 -2
- nautobot/extras/templates/extras/approval_workflow/deny.html +9 -3
- nautobot/extras/templates/extras/approvalworkflowdefinition_update.html +1 -1
- nautobot/extras/templates/extras/customfield_update.html +1 -1
- nautobot/extras/templates/extras/dynamicgroup_update.html +2 -2
- nautobot/extras/templates/extras/inc/approval_buttons_column.html +10 -2
- nautobot/extras/templates/extras/inc/job_tiles.html +2 -2
- nautobot/extras/templates/extras/inc/jobresult.html +1 -1
- nautobot/extras/templates/extras/metadatatype_create.html +1 -1
- nautobot/extras/templates/extras/object_approvalworkflow.html +2 -3
- nautobot/extras/templates/extras/secretsgroup_update.html +1 -1
- nautobot/extras/tests/test_api.py +57 -3
- nautobot/extras/tests/test_customfields_filters.py +84 -4
- nautobot/extras/tests/test_views.py +323 -6
- nautobot/extras/views.py +114 -39
- nautobot/ipam/constants.py +2 -2
- nautobot/ipam/tables.py +7 -6
- nautobot/load_balancers/constants.py +6 -0
- nautobot/load_balancers/migrations/0001_initial.py +14 -3
- nautobot/load_balancers/models.py +5 -4
- nautobot/load_balancers/tables.py +5 -0
- nautobot/project-static/dist/css/nautobot.css +1 -1
- nautobot/project-static/dist/css/nautobot.css.map +1 -1
- nautobot/project-static/dist/js/graphql-libraries.js +1 -1
- nautobot/project-static/dist/js/graphql-libraries.js.map +1 -1
- nautobot/project-static/dist/js/libraries.js +1 -1
- nautobot/project-static/dist/js/libraries.js.LICENSE.txt +38 -2
- nautobot/project-static/dist/js/libraries.js.map +1 -1
- nautobot/project-static/dist/js/nautobot-graphiql.js +1 -1
- nautobot/project-static/dist/js/nautobot-graphiql.js.map +1 -1
- nautobot/project-static/dist/js/nautobot.js +1 -1
- nautobot/project-static/dist/js/nautobot.js.map +1 -1
- nautobot/project-static/img/dark-theme.png +0 -0
- nautobot/project-static/img/light-theme.png +0 -0
- nautobot/project-static/img/system-theme.png +0 -0
- nautobot/project-static/js/forms.js +1 -85
- nautobot/tenancy/tables.py +3 -2
- nautobot/tenancy/views.py +3 -2
- nautobot/ui/package-lock.json +553 -569
- nautobot/ui/package.json +10 -10
- nautobot/ui/src/js/checkbox.js +132 -0
- nautobot/ui/src/js/nautobot.js +6 -0
- nautobot/ui/src/js/select2.js +69 -73
- nautobot/ui/src/js/theme.js +129 -39
- nautobot/ui/src/scss/nautobot.scss +11 -1
- nautobot/vpn/templates/vpn/vpnprofile_create.html +2 -2
- nautobot/wireless/filters.py +15 -1
- nautobot/wireless/tables.py +18 -14
- nautobot/wireless/templates/wireless/wirelessnetwork_create.html +1 -1
- {nautobot-3.0.0rc1.dist-info → nautobot-3.0.1.dist-info}/METADATA +2 -2
- {nautobot-3.0.0rc1.dist-info → nautobot-3.0.1.dist-info}/RECORD +103 -98
- {nautobot-3.0.0rc1.dist-info → nautobot-3.0.1.dist-info}/LICENSE.txt +0 -0
- {nautobot-3.0.0rc1.dist-info → nautobot-3.0.1.dist-info}/NOTICE +0 -0
- {nautobot-3.0.0rc1.dist-info → nautobot-3.0.1.dist-info}/WHEEL +0 -0
- {nautobot-3.0.0rc1.dist-info → nautobot-3.0.1.dist-info}/entry_points.txt +0 -0
|
@@ -598,10 +598,10 @@ class ApprovalWorkflowStageTest(
|
|
|
598
598
|
response_obj = stage.approval_workflow_stage_responses.first()
|
|
599
599
|
self.assertEqual(response_obj.comments, "This is a test comment.")
|
|
600
600
|
self.assertEqual(response_obj.user, self.user)
|
|
601
|
-
self.assertEqual(response_obj.state,
|
|
601
|
+
self.assertEqual(response_obj.state, ApprovalWorkflowStateChoices.COMMENT)
|
|
602
602
|
|
|
603
603
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
604
|
-
def
|
|
604
|
+
def test_approval_workflow_stage_edit_comment(self):
|
|
605
605
|
for case in self.approval_workflow_content_type_cases:
|
|
606
606
|
content_type = case["content_type"]
|
|
607
607
|
with self.subTest(case=content_type):
|
|
@@ -618,7 +618,61 @@ class ApprovalWorkflowStageTest(
|
|
|
618
618
|
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
619
619
|
|
|
620
620
|
stage.refresh_from_db()
|
|
621
|
-
self.assertEqual(stage.approval_workflow_stage_responses.count(),
|
|
621
|
+
self.assertEqual(stage.approval_workflow_stage_responses.count(), 1)
|
|
622
|
+
self.assertEqual(stage.approval_workflow_stage_responses.first().comments, "This is a test comment.")
|
|
623
|
+
|
|
624
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
625
|
+
def test_approval_workflow_stage_not_allow_add_comment_to_approved_denied_stage(self):
|
|
626
|
+
for case in self.approval_workflow_content_type_cases:
|
|
627
|
+
content_type = case["content_type"]
|
|
628
|
+
with self.subTest(case=content_type):
|
|
629
|
+
stage = case["stage"]
|
|
630
|
+
# set state to approved so we can ensure comments *do not* work.
|
|
631
|
+
stage.state = ApprovalWorkflowStateChoices.APPROVED
|
|
632
|
+
stage.save()
|
|
633
|
+
url = reverse("extras-api:approvalworkflowstage-comment", kwargs={"pk": stage.pk})
|
|
634
|
+
self.add_permissions("extras.change_approvalworkflowstage")
|
|
635
|
+
|
|
636
|
+
data = {"comments": "This is a test comment."}
|
|
637
|
+
response = self.client.post(url, data=data, format="json", **self.header)
|
|
638
|
+
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
|
639
|
+
self.assertEqual(
|
|
640
|
+
response.data["detail"],
|
|
641
|
+
f"This stage is in {stage.state} state. Can't comment approved or denied stage.",
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
stage.refresh_from_db()
|
|
645
|
+
self.assertEqual(stage.approval_workflow_stage_responses.count(), 0)
|
|
646
|
+
|
|
647
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
648
|
+
def test_state_unchanged_when_comment_added_to_approved_stage(self):
|
|
649
|
+
for case in self.approval_workflow_content_type_cases:
|
|
650
|
+
content_type = case["content_type"]
|
|
651
|
+
with self.subTest(case=content_type):
|
|
652
|
+
stage = case["stage"]
|
|
653
|
+
# set min 2 approvers
|
|
654
|
+
stage.approval_workflow_stage_definition.min_approvers = 2
|
|
655
|
+
stage.approval_workflow_stage_definition.save()
|
|
656
|
+
url = reverse("extras-api:approvalworkflowstage-comment", kwargs={"pk": stage.pk})
|
|
657
|
+
self.add_permissions("extras.change_approvalworkflowstage")
|
|
658
|
+
|
|
659
|
+
ApprovalWorkflowStageResponse.objects.create(
|
|
660
|
+
approval_workflow_stage=stage,
|
|
661
|
+
user=self.user,
|
|
662
|
+
state=ApprovalWorkflowStateChoices.APPROVED,
|
|
663
|
+
comments="Approved comment",
|
|
664
|
+
)
|
|
665
|
+
|
|
666
|
+
data = {"comments": "Edit approved comment."}
|
|
667
|
+
response = self.client.post(url, data=data, format="json", **self.header)
|
|
668
|
+
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
669
|
+
|
|
670
|
+
stage.refresh_from_db()
|
|
671
|
+
self.assertEqual(stage.approval_workflow_stage_responses.count(), 1)
|
|
672
|
+
self.assertEqual(stage.approval_workflow_stage_responses.first().comments, "Edit approved comment.")
|
|
673
|
+
self.assertEqual(
|
|
674
|
+
stage.approval_workflow_stage_responses.first().state, ApprovalWorkflowStateChoices.APPROVED
|
|
675
|
+
)
|
|
622
676
|
|
|
623
677
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
624
678
|
def test_approval_workflow_stage_pending_my_approvals(self):
|
|
@@ -3,8 +3,8 @@ from django.db.models import Model
|
|
|
3
3
|
from django.test import tag
|
|
4
4
|
|
|
5
5
|
from nautobot.core.testing import views
|
|
6
|
-
from nautobot.extras.choices import CustomFieldFilterLogicChoices, CustomFieldTypeChoices
|
|
7
|
-
from nautobot.extras.models import CustomField
|
|
6
|
+
from nautobot.extras.choices import CustomFieldFilterLogicChoices, CustomFieldTypeChoices, DynamicGroupOperatorChoices
|
|
7
|
+
from nautobot.extras.models import CustomField, DynamicGroup, DynamicGroupMembership
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
@tag("unit")
|
|
@@ -368,6 +368,82 @@ class CustomFieldsFilters:
|
|
|
368
368
|
instances, filtered, test_case["expected"], assert_in_msg, assert_not_in_msg
|
|
369
369
|
)
|
|
370
370
|
|
|
371
|
+
def test_str_custom_field_with_dynamic_groups(self):
|
|
372
|
+
cf_label = "test_dgs_label_str"
|
|
373
|
+
cf_label_ic = "test_dgs_label_str_ic"
|
|
374
|
+
test_data = self.filter_matrix[CustomFieldTypeChoices.TYPE_TEXT]
|
|
375
|
+
model = self.filterset.Meta.model
|
|
376
|
+
self.create_custom_field(model, cf_label)
|
|
377
|
+
self.create_custom_field(model, cf_label_ic, filter_logic=CustomFieldFilterLogicChoices.FILTER_LOOSE)
|
|
378
|
+
|
|
379
|
+
instances = self.queryset.all()[:5]
|
|
380
|
+
self.prepare_custom_fields_values(cf_label, instances, test_data["value"], "not-matched")
|
|
381
|
+
self.prepare_custom_fields_values(cf_label_ic, instances, test_data["value"], "not-matched")
|
|
382
|
+
|
|
383
|
+
ct = ContentType.objects.get_for_model(model)
|
|
384
|
+
|
|
385
|
+
filter_group = DynamicGroup.objects.create(
|
|
386
|
+
name="CustomField DynamicGroup",
|
|
387
|
+
content_type=ct,
|
|
388
|
+
filter={},
|
|
389
|
+
)
|
|
390
|
+
parent_group = DynamicGroup.objects.create(
|
|
391
|
+
name="Parent CustomField DynamicGroup",
|
|
392
|
+
content_type=ct,
|
|
393
|
+
filter={},
|
|
394
|
+
)
|
|
395
|
+
group_membership = DynamicGroupMembership.objects.create(
|
|
396
|
+
parent_group=parent_group,
|
|
397
|
+
group=filter_group,
|
|
398
|
+
operator=DynamicGroupOperatorChoices.OPERATOR_INTERSECTION,
|
|
399
|
+
weight=10,
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
# Prepare test cases
|
|
403
|
+
# For dynamic group we're supporting only exact or icontains for now
|
|
404
|
+
# Depending on the custom field filter logic
|
|
405
|
+
for lookup_data in test_data["lookups"]:
|
|
406
|
+
if lookup_data["lookup"] not in ["", "ic"]:
|
|
407
|
+
continue
|
|
408
|
+
|
|
409
|
+
test_cases = [(lookup_data["lookup"], test_case) for test_case in lookup_data["test_cases"]]
|
|
410
|
+
group_membership.operator = DynamicGroupOperatorChoices.OPERATOR_INTERSECTION
|
|
411
|
+
group_membership.save()
|
|
412
|
+
|
|
413
|
+
for lookup, test_case in test_cases:
|
|
414
|
+
assert_in_msg = f'object expected to be found for searching `{lookup}` ({lookup_data["name"]}) = "{test_case["search"]}"'
|
|
415
|
+
assert_not_in_msg = f'object expected to be filtered out for searching `{lookup}` ({lookup_data["name"]}) = "{test_case["search"]}"'
|
|
416
|
+
group_filter = {f"cf_{cf_label}": test_case["search"]}
|
|
417
|
+
if lookup == "ic":
|
|
418
|
+
group_filter = {f"cf_{cf_label_ic}": test_case["search"]}
|
|
419
|
+
|
|
420
|
+
with self.subTest(f"Test filtering {group_filter}"):
|
|
421
|
+
filter_group.set_filter(group_filter)
|
|
422
|
+
filter_group.save()
|
|
423
|
+
members = parent_group.update_cached_members()
|
|
424
|
+
self.assertProperInstancesReturned(
|
|
425
|
+
instances, members, test_case["expected"], assert_in_msg, assert_not_in_msg
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
negated_test_cases = self.get_negated_test_cases(lookup_data["lookup"], lookup_data["test_cases"])
|
|
429
|
+
group_membership.operator = DynamicGroupOperatorChoices.OPERATOR_DIFFERENCE
|
|
430
|
+
group_membership.save()
|
|
431
|
+
|
|
432
|
+
for lookup, test_case in negated_test_cases:
|
|
433
|
+
assert_in_msg = f'object expected to be found for searching `{lookup}` ({lookup_data["name"]}) = "{test_case["search"]}"'
|
|
434
|
+
assert_not_in_msg = f'object expected to be filtered out for searching `{lookup}` ({lookup_data["name"]}) = "{test_case["search"]}"'
|
|
435
|
+
group_filter = {f"cf_{cf_label}": test_case["search"]}
|
|
436
|
+
if lookup == "ic":
|
|
437
|
+
group_filter = {f"cf_{cf_label_ic}": test_case["search"]}
|
|
438
|
+
|
|
439
|
+
with self.subTest(f"Test negated filtering {group_filter}"):
|
|
440
|
+
filter_group.set_filter(group_filter)
|
|
441
|
+
filter_group.save()
|
|
442
|
+
members = parent_group.update_cached_members()
|
|
443
|
+
self.assertProperInstancesReturned(
|
|
444
|
+
instances, members, test_case["expected"], assert_in_msg, assert_not_in_msg
|
|
445
|
+
)
|
|
446
|
+
|
|
371
447
|
def test_str_custom_field_filters(self):
|
|
372
448
|
cf_label = "test_fs_label_str"
|
|
373
449
|
test_data = self.filter_matrix[CustomFieldTypeChoices.TYPE_TEXT]
|
|
@@ -411,11 +487,15 @@ class CustomFieldsFilters:
|
|
|
411
487
|
)
|
|
412
488
|
|
|
413
489
|
@staticmethod
|
|
414
|
-
def create_custom_field(
|
|
490
|
+
def create_custom_field(
|
|
491
|
+
model: Model,
|
|
492
|
+
label: str,
|
|
493
|
+
filter_logic: CustomFieldFilterLogicChoices = CustomFieldFilterLogicChoices.FILTER_EXACT,
|
|
494
|
+
) -> CustomField:
|
|
415
495
|
cf = CustomField.objects.create(
|
|
416
496
|
type=CustomFieldTypeChoices.TYPE_TEXT,
|
|
417
497
|
label=label,
|
|
418
|
-
filter_logic=
|
|
498
|
+
filter_logic=filter_logic,
|
|
419
499
|
)
|
|
420
500
|
cf.content_types.set([ContentType.objects.get_for_model(model)])
|
|
421
501
|
return cf
|
|
@@ -133,12 +133,13 @@ class ApprovalWorkflowDefinitionViewTestCase(
|
|
|
133
133
|
name=f"Test Approval Workflow {i}",
|
|
134
134
|
model_content_type=cls.scheduledjob_ct,
|
|
135
135
|
weight=i,
|
|
136
|
+
model_constraints={"job_model__name": "NoSuchJob"},
|
|
136
137
|
)
|
|
137
138
|
|
|
138
139
|
cls.form_data = {
|
|
139
140
|
"name": "Test Approval Workflow Definition 5",
|
|
140
141
|
"model_content_type": cls.scheduledjob_ct.pk,
|
|
141
|
-
"model_constraints": '{"
|
|
142
|
+
"model_constraints": '{"job_model__name": "Bulk Delete Objects"}',
|
|
142
143
|
"weight": 5,
|
|
143
144
|
# These are the "management_form" fields required by the dynamic CustomFieldChoice formsets.
|
|
144
145
|
"approval_workflow_stage_definitions-TOTAL_FORMS": "0", # Set to 0 so validation succeeds until we need it
|
|
@@ -162,6 +163,7 @@ class ApprovalWorkflowStageDefinitionViewTestCase(ViewTestCases.PrimaryObjectVie
|
|
|
162
163
|
name="Test Approval Workflow Definition 1",
|
|
163
164
|
model_content_type=cls.scheduledjob_ct,
|
|
164
165
|
weight=10,
|
|
166
|
+
model_constraints={"job_model__name": "NoSuchJob"},
|
|
165
167
|
)
|
|
166
168
|
cls.approver_group = Group.objects.create(name="Test Group 1")
|
|
167
169
|
cls.updated_approver_group = Group.objects.create(name="Test Group 2")
|
|
@@ -264,7 +266,10 @@ class ApprovalWorkflowViewTestCase(
|
|
|
264
266
|
]
|
|
265
267
|
approval_workflow_definitions = [
|
|
266
268
|
ApprovalWorkflowDefinition.objects.create(
|
|
267
|
-
name=f"Test Approval Workflow {i}",
|
|
269
|
+
name=f"Test Approval Workflow {i}",
|
|
270
|
+
model_content_type=cls.scheduledjob_ct,
|
|
271
|
+
weight=i,
|
|
272
|
+
model_constraints={"job_model__name": "NoSuchJob"},
|
|
268
273
|
)
|
|
269
274
|
for i in range(5)
|
|
270
275
|
]
|
|
@@ -333,6 +338,7 @@ class ApprovalWorkflowStageViewTestCase(
|
|
|
333
338
|
name=f"Test Approval Workflow {i}",
|
|
334
339
|
model_content_type=cls.scheduledjob_ct,
|
|
335
340
|
weight=i,
|
|
341
|
+
model_constraints={"job_model__name": "NoSuchJob"},
|
|
336
342
|
)
|
|
337
343
|
for i in range(5)
|
|
338
344
|
]
|
|
@@ -413,19 +419,33 @@ class ApprovalWorkflowStageViewTestCase(
|
|
|
413
419
|
self.client.force_login(self.user)
|
|
414
420
|
self.add_permissions("extras.change_approvalworkflowstage", "extras.view_approvalworkflowstage")
|
|
415
421
|
|
|
416
|
-
# Try GET with model-level permission
|
|
422
|
+
# Try GET with model-level permission but not belonging to the approver group
|
|
417
423
|
url = reverse("extras:approvalworkflowstage_approve", args=[approval_workflow_stage.pk])
|
|
424
|
+
response = self.client.get(url, follow=True)
|
|
425
|
+
self.assertHttpStatus(response, 200)
|
|
426
|
+
self.assertBodyContains(response, "You are not permitted to approve this")
|
|
427
|
+
|
|
428
|
+
# Try GET with belonging to the approver group
|
|
429
|
+
approval_workflow_stage.approval_workflow_stage_definition.approver_group.user_set.add(self.user)
|
|
418
430
|
response = self.client.get(url)
|
|
431
|
+
approval_workflow_stage.approval_workflow_stage_definition.approver_group.user_set.remove(self.user)
|
|
419
432
|
self.assertHttpStatus(response, 200)
|
|
420
433
|
self.assertBodyContains(response, '<div class="card border-success">') # Assert the success panel is present
|
|
421
434
|
|
|
422
|
-
# Try POST with model-level permission
|
|
435
|
+
# Try POST with model-level permission but not belonging to the approver group
|
|
423
436
|
request = {
|
|
424
437
|
"path": url,
|
|
425
438
|
"data": post_data({"comments": "Approved!"}),
|
|
426
439
|
}
|
|
427
440
|
response = self.client.post(**request, follow=True)
|
|
428
441
|
self.assertHttpStatus(response, 200)
|
|
442
|
+
self.assertBodyContains(response, "You are not permitted to approve this")
|
|
443
|
+
|
|
444
|
+
# Try POST with belonging to the approver group
|
|
445
|
+
approval_workflow_stage.approval_workflow_stage_definition.approver_group.user_set.add(self.user)
|
|
446
|
+
response = self.client.post(**request, follow=True)
|
|
447
|
+
approval_workflow_stage.approval_workflow_stage_definition.approver_group.user_set.remove(self.user)
|
|
448
|
+
self.assertHttpStatus(response, 200)
|
|
429
449
|
approval_workflow_stage.refresh_from_db()
|
|
430
450
|
# New response should be created
|
|
431
451
|
new_response = ApprovalWorkflowStageResponse.objects.get(
|
|
@@ -443,25 +463,88 @@ class ApprovalWorkflowStageViewTestCase(
|
|
|
443
463
|
self.assertHttpStatus(response, 200)
|
|
444
464
|
self.assertBodyContains(response, "Approval Date") # Assert the approval date is present
|
|
445
465
|
|
|
466
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
467
|
+
def test_approve_stage_with_existing_comment_endpoint(self):
|
|
468
|
+
"""Test the approve stage with existing comment endpoint."""
|
|
469
|
+
approval_workflow_stage = ApprovalWorkflowStage.objects.first()
|
|
470
|
+
new_response = ApprovalWorkflowStageResponse.objects.create(
|
|
471
|
+
approval_workflow_stage=approval_workflow_stage,
|
|
472
|
+
user=self.user,
|
|
473
|
+
comments="existing comment",
|
|
474
|
+
state=ApprovalWorkflowStateChoices.COMMENT,
|
|
475
|
+
)
|
|
476
|
+
self.assertEqual(
|
|
477
|
+
ApprovalWorkflowStageResponse.objects.filter(
|
|
478
|
+
approval_workflow_stage=approval_workflow_stage, user=self.user
|
|
479
|
+
).count(),
|
|
480
|
+
1,
|
|
481
|
+
)
|
|
482
|
+
self.client.force_login(self.user)
|
|
483
|
+
self.add_permissions("extras.change_approvalworkflowstage", "extras.view_approvalworkflowstage")
|
|
484
|
+
|
|
485
|
+
# Try GET with model-level permission
|
|
486
|
+
approval_workflow_stage.approval_workflow_stage_definition.approver_group.user_set.add(self.user)
|
|
487
|
+
url = reverse("extras:approvalworkflowstage_approve", args=[approval_workflow_stage.pk])
|
|
488
|
+
response = self.client.get(url)
|
|
489
|
+
approval_workflow_stage.approval_workflow_stage_definition.approver_group.user_set.remove(self.user)
|
|
490
|
+
self.assertHttpStatus(response, 200)
|
|
491
|
+
self.assertBodyContains(response, '<div class="card border-success">') # Assert the success panel is present
|
|
492
|
+
self.assertBodyContains(response, "existing comment")
|
|
493
|
+
|
|
494
|
+
# Try POST with model-level permission
|
|
495
|
+
request = {
|
|
496
|
+
"path": url,
|
|
497
|
+
"data": post_data({"comments": "Approved!"}),
|
|
498
|
+
}
|
|
499
|
+
approval_workflow_stage.approval_workflow_stage_definition.approver_group.user_set.add(self.user)
|
|
500
|
+
response = self.client.post(**request, follow=True)
|
|
501
|
+
approval_workflow_stage.approval_workflow_stage_definition.approver_group.user_set.remove(self.user)
|
|
502
|
+
self.assertHttpStatus(response, 200)
|
|
503
|
+
approval_workflow_stage.refresh_from_db()
|
|
504
|
+
# Response should be updated
|
|
505
|
+
new_response.refresh_from_db()
|
|
506
|
+
self.assertEqual(
|
|
507
|
+
ApprovalWorkflowStageResponse.objects.filter(
|
|
508
|
+
approval_workflow_stage=approval_workflow_stage, user=self.user
|
|
509
|
+
).count(),
|
|
510
|
+
1,
|
|
511
|
+
)
|
|
512
|
+
self.assertEqual(new_response.state, ApprovalWorkflowStateChoices.APPROVED)
|
|
513
|
+
self.assertEqual(new_response.comments, "Approved!")
|
|
514
|
+
|
|
446
515
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
447
516
|
def test_deny_endpoint(self):
|
|
448
517
|
"""Test the deny endpoint."""
|
|
449
518
|
approval_workflow_stage = ApprovalWorkflowStage.objects.first()
|
|
450
519
|
self.add_permissions("extras.change_approvalworkflowstage", "extras.view_approvalworkflowstage")
|
|
451
520
|
|
|
452
|
-
# Try GET with model-level permission
|
|
521
|
+
# Try GET with model-level permission but not belonging to the approver group
|
|
453
522
|
url = reverse("extras:approvalworkflowstage_deny", args=[approval_workflow_stage.pk])
|
|
523
|
+
response = self.client.get(url, follow=True)
|
|
524
|
+
self.assertHttpStatus(response, 200)
|
|
525
|
+
self.assertBodyContains(response, "You are not permitted to deny this")
|
|
526
|
+
|
|
527
|
+
# Try GET with belonging to the approver group
|
|
528
|
+
approval_workflow_stage.approval_workflow_stage_definition.approver_group.user_set.add(self.user)
|
|
454
529
|
response = self.client.get(url)
|
|
530
|
+
approval_workflow_stage.approval_workflow_stage_definition.approver_group.user_set.remove(self.user)
|
|
455
531
|
self.assertHttpStatus(response, 200)
|
|
456
532
|
self.assertBodyContains(response, '<div class="card border-danger">') # Assert the danger panel is present
|
|
457
533
|
|
|
458
|
-
# Try POST with model-level permission
|
|
534
|
+
# Try POST with model-level permission but not belonging to the approver group
|
|
459
535
|
request = {
|
|
460
536
|
"path": url,
|
|
461
537
|
"data": post_data({"comments": "Denied!"}),
|
|
462
538
|
}
|
|
463
539
|
response = self.client.post(**request, follow=True)
|
|
464
540
|
self.assertHttpStatus(response, 200)
|
|
541
|
+
self.assertBodyContains(response, "You are not permitted to deny this")
|
|
542
|
+
|
|
543
|
+
# Try POST with belonging to the approver group
|
|
544
|
+
approval_workflow_stage.approval_workflow_stage_definition.approver_group.user_set.add(self.user)
|
|
545
|
+
response = self.client.post(**request, follow=True)
|
|
546
|
+
approval_workflow_stage.approval_workflow_stage_definition.approver_group.user_set.remove(self.user)
|
|
547
|
+
self.assertHttpStatus(response, 200)
|
|
465
548
|
approval_workflow_stage.refresh_from_db()
|
|
466
549
|
# New response should be created
|
|
467
550
|
new_response = ApprovalWorkflowStageResponse.objects.get(
|
|
@@ -479,6 +562,239 @@ class ApprovalWorkflowStageViewTestCase(
|
|
|
479
562
|
self.assertHttpStatus(response, 200)
|
|
480
563
|
self.assertBodyContains(response, "Denial Date") # Assert the denial date is present
|
|
481
564
|
|
|
565
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
566
|
+
def test_deny_stage_with_existing_comment_endpoint(self):
|
|
567
|
+
"""Test the deny stage with existing comment endpoint."""
|
|
568
|
+
approval_workflow_stage = ApprovalWorkflowStage.objects.first()
|
|
569
|
+
new_response = ApprovalWorkflowStageResponse.objects.create(
|
|
570
|
+
approval_workflow_stage=approval_workflow_stage,
|
|
571
|
+
user=self.user,
|
|
572
|
+
comments="existing comment",
|
|
573
|
+
state=ApprovalWorkflowStateChoices.COMMENT,
|
|
574
|
+
)
|
|
575
|
+
self.assertEqual(
|
|
576
|
+
ApprovalWorkflowStageResponse.objects.filter(
|
|
577
|
+
approval_workflow_stage=approval_workflow_stage, user=self.user
|
|
578
|
+
).count(),
|
|
579
|
+
1,
|
|
580
|
+
)
|
|
581
|
+
self.client.force_login(self.user)
|
|
582
|
+
self.add_permissions("extras.change_approvalworkflowstage", "extras.view_approvalworkflowstage")
|
|
583
|
+
|
|
584
|
+
# Try GET with model-level permission
|
|
585
|
+
approval_workflow_stage.approval_workflow_stage_definition.approver_group.user_set.add(self.user)
|
|
586
|
+
url = reverse("extras:approvalworkflowstage_deny", args=[approval_workflow_stage.pk])
|
|
587
|
+
response = self.client.get(url)
|
|
588
|
+
approval_workflow_stage.approval_workflow_stage_definition.approver_group.user_set.remove(self.user)
|
|
589
|
+
self.assertHttpStatus(response, 200)
|
|
590
|
+
self.assertBodyContains(response, '<div class="card border-danger">') # Assert the danger panel is present
|
|
591
|
+
self.assertBodyContains(response, "existing comment")
|
|
592
|
+
|
|
593
|
+
# Try POST with model-level permission
|
|
594
|
+
approval_workflow_stage.approval_workflow_stage_definition.approver_group.user_set.add(self.user)
|
|
595
|
+
request = {
|
|
596
|
+
"path": url,
|
|
597
|
+
"data": post_data({"comments": "Denied!"}),
|
|
598
|
+
}
|
|
599
|
+
response = self.client.post(**request, follow=True)
|
|
600
|
+
approval_workflow_stage.approval_workflow_stage_definition.approver_group.user_set.remove(self.user)
|
|
601
|
+
self.assertHttpStatus(response, 200)
|
|
602
|
+
approval_workflow_stage.refresh_from_db()
|
|
603
|
+
# Response should be updated
|
|
604
|
+
new_response.refresh_from_db()
|
|
605
|
+
self.assertEqual(
|
|
606
|
+
ApprovalWorkflowStageResponse.objects.filter(
|
|
607
|
+
approval_workflow_stage=approval_workflow_stage, user=self.user
|
|
608
|
+
).count(),
|
|
609
|
+
1,
|
|
610
|
+
)
|
|
611
|
+
self.assertEqual(new_response.state, ApprovalWorkflowStateChoices.DENIED)
|
|
612
|
+
self.assertEqual(new_response.comments, "Denied!")
|
|
613
|
+
self.assertBodyContains(
|
|
614
|
+
response, f"You denied {approval_workflow_stage}."
|
|
615
|
+
) # Assert the denial message is present
|
|
616
|
+
|
|
617
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
618
|
+
def test_comment_endpoint(self):
|
|
619
|
+
"""Test the comment endpoint."""
|
|
620
|
+
approval_workflow_stage = ApprovalWorkflowStage.objects.first()
|
|
621
|
+
self.client.force_login(self.user)
|
|
622
|
+
self.add_permissions("extras.change_approvalworkflowstage", "extras.view_approvalworkflowstage")
|
|
623
|
+
|
|
624
|
+
# Try GET with model-level permission
|
|
625
|
+
url = reverse("extras:approvalworkflowstage_comment", args=[approval_workflow_stage.pk])
|
|
626
|
+
response = self.client.get(url)
|
|
627
|
+
self.assertHttpStatus(response, 200)
|
|
628
|
+
expected_object_button = '<button type="submit" name="_confirm" class="btn btn-info"><span aria-hidden="true" class="mdi mdi-check me-4"></span><!---->Comment</button>'
|
|
629
|
+
self.assertContains(response, expected_object_button, html=True) # Assert button Comment
|
|
630
|
+
# Try POST with model-level permission
|
|
631
|
+
request = {
|
|
632
|
+
"path": url,
|
|
633
|
+
"data": post_data({"comments": "It is just a comment"}),
|
|
634
|
+
}
|
|
635
|
+
response = self.client.post(**request, follow=True)
|
|
636
|
+
self.assertHttpStatus(response, 200)
|
|
637
|
+
approval_workflow_stage.refresh_from_db()
|
|
638
|
+
# New response should be created
|
|
639
|
+
new_response = ApprovalWorkflowStageResponse.objects.get(
|
|
640
|
+
approval_workflow_stage=approval_workflow_stage, user=self.user
|
|
641
|
+
)
|
|
642
|
+
self.assertEqual(new_response.state, ApprovalWorkflowStateChoices.COMMENT)
|
|
643
|
+
self.assertEqual(new_response.comments, "It is just a comment")
|
|
644
|
+
self.assertBodyContains(
|
|
645
|
+
response, f"You commented {approval_workflow_stage}."
|
|
646
|
+
) # Assert the comment message is present
|
|
647
|
+
|
|
648
|
+
# Try GET again in form should be previous message
|
|
649
|
+
url = reverse("extras:approvalworkflowstage_comment", args=[approval_workflow_stage.pk])
|
|
650
|
+
response = self.client.get(url)
|
|
651
|
+
self.assertHttpStatus(response, 200)
|
|
652
|
+
self.assertBodyContains(response, "It is just a comment")
|
|
653
|
+
|
|
654
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
655
|
+
def test_edit_comment_endpoint(self):
|
|
656
|
+
"""Test the edit comment endpoint."""
|
|
657
|
+
approval_workflow_stage = ApprovalWorkflowStage.objects.first()
|
|
658
|
+
# create new response with a comment
|
|
659
|
+
new_response = ApprovalWorkflowStageResponse.objects.create(
|
|
660
|
+
approval_workflow_stage=approval_workflow_stage,
|
|
661
|
+
user=self.user,
|
|
662
|
+
comments="existing comment",
|
|
663
|
+
state=ApprovalWorkflowStateChoices.COMMENT,
|
|
664
|
+
)
|
|
665
|
+
self.assertEqual(
|
|
666
|
+
ApprovalWorkflowStageResponse.objects.filter(
|
|
667
|
+
approval_workflow_stage=approval_workflow_stage, user=self.user
|
|
668
|
+
).count(),
|
|
669
|
+
1,
|
|
670
|
+
)
|
|
671
|
+
self.client.force_login(self.user)
|
|
672
|
+
self.add_permissions("extras.change_approvalworkflowstage", "extras.view_approvalworkflowstage")
|
|
673
|
+
|
|
674
|
+
# Try GET again in form should be previous message
|
|
675
|
+
url = reverse("extras:approvalworkflowstage_comment", args=[approval_workflow_stage.pk])
|
|
676
|
+
response = self.client.get(url)
|
|
677
|
+
self.assertHttpStatus(response, 200)
|
|
678
|
+
self.assertBodyContains(response, new_response.comments)
|
|
679
|
+
|
|
680
|
+
request = {
|
|
681
|
+
"path": url,
|
|
682
|
+
"data": post_data({"comments": "Edit existing comment"}),
|
|
683
|
+
}
|
|
684
|
+
response = self.client.post(**request, follow=True)
|
|
685
|
+
self.assertHttpStatus(response, 200)
|
|
686
|
+
approval_workflow_stage.refresh_from_db()
|
|
687
|
+
self.assertEqual(
|
|
688
|
+
ApprovalWorkflowStageResponse.objects.filter(
|
|
689
|
+
approval_workflow_stage=approval_workflow_stage, user=self.user
|
|
690
|
+
).count(),
|
|
691
|
+
1,
|
|
692
|
+
)
|
|
693
|
+
edited_response = ApprovalWorkflowStageResponse.objects.get(
|
|
694
|
+
approval_workflow_stage=approval_workflow_stage, user=self.user
|
|
695
|
+
)
|
|
696
|
+
self.assertEqual(edited_response.state, ApprovalWorkflowStateChoices.COMMENT)
|
|
697
|
+
self.assertEqual(edited_response.comments, "Edit existing comment")
|
|
698
|
+
|
|
699
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
700
|
+
def test_add_new_comment_from_different_user(self):
|
|
701
|
+
"""Test the add comment if different user add it."""
|
|
702
|
+
approval_workflow_stage = ApprovalWorkflowStage.objects.first()
|
|
703
|
+
# create new response with a comment
|
|
704
|
+
second_user = User.objects.last()
|
|
705
|
+
new_response = ApprovalWorkflowStageResponse.objects.create(
|
|
706
|
+
approval_workflow_stage=approval_workflow_stage,
|
|
707
|
+
user=second_user,
|
|
708
|
+
comments="existing comment",
|
|
709
|
+
state=ApprovalWorkflowStateChoices.COMMENT,
|
|
710
|
+
)
|
|
711
|
+
self.assertEqual(
|
|
712
|
+
ApprovalWorkflowStageResponse.objects.filter(
|
|
713
|
+
approval_workflow_stage=approval_workflow_stage, user=second_user
|
|
714
|
+
).count(),
|
|
715
|
+
1,
|
|
716
|
+
)
|
|
717
|
+
self.client.force_login(self.user)
|
|
718
|
+
self.add_permissions("extras.change_approvalworkflowstage", "extras.view_approvalworkflowstage")
|
|
719
|
+
|
|
720
|
+
# Try GET again in form should be previous message
|
|
721
|
+
url = reverse("extras:approvalworkflowstage_comment", args=[approval_workflow_stage.pk])
|
|
722
|
+
response = self.client.get(url)
|
|
723
|
+
self.assertHttpStatus(response, 200)
|
|
724
|
+
expected_object_comment = '<textarea name="comments" cols="40" rows="10" class="form-control" placeholder="Comments" id="id_comments"></textarea>'
|
|
725
|
+
self.assertContains(response, expected_object_comment, html=True) # Assert empty textarea
|
|
726
|
+
|
|
727
|
+
request = {
|
|
728
|
+
"path": url,
|
|
729
|
+
"data": post_data({"comments": "New comment"}),
|
|
730
|
+
}
|
|
731
|
+
response = self.client.post(**request, follow=True)
|
|
732
|
+
self.assertHttpStatus(response, 200)
|
|
733
|
+
self.assertEqual(ApprovalWorkflowStageResponse.objects.all().count(), 2)
|
|
734
|
+
|
|
735
|
+
old_response = ApprovalWorkflowStageResponse.objects.get(
|
|
736
|
+
approval_workflow_stage=approval_workflow_stage, user=second_user
|
|
737
|
+
)
|
|
738
|
+
self.assertEqual(old_response.state, ApprovalWorkflowStateChoices.COMMENT)
|
|
739
|
+
self.assertEqual(old_response.comments, "existing comment")
|
|
740
|
+
new_response = ApprovalWorkflowStageResponse.objects.get(
|
|
741
|
+
approval_workflow_stage=approval_workflow_stage, user=self.user
|
|
742
|
+
)
|
|
743
|
+
self.assertEqual(new_response.state, ApprovalWorkflowStateChoices.COMMENT)
|
|
744
|
+
self.assertEqual(new_response.comments, "New comment")
|
|
745
|
+
|
|
746
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
747
|
+
def test_add_comment_to_pending_approval(self):
|
|
748
|
+
"""
|
|
749
|
+
Test editing or adding a comment in the approval stage that already has one approval,
|
|
750
|
+
but needs two. Only edit the comment don't change the state from APPROVED to COMMENT.
|
|
751
|
+
"""
|
|
752
|
+
approval_workflow_stage = ApprovalWorkflowStage.objects.first()
|
|
753
|
+
# check min_approver to 2
|
|
754
|
+
approval_workflow_stage.approval_workflow_stage_definition.min_approvers = 2
|
|
755
|
+
approval_workflow_stage.approval_workflow_stage_definition.save()
|
|
756
|
+
# create new response with a comment
|
|
757
|
+
new_response = ApprovalWorkflowStageResponse.objects.create(
|
|
758
|
+
approval_workflow_stage=approval_workflow_stage,
|
|
759
|
+
user=self.user,
|
|
760
|
+
comments="approved comment",
|
|
761
|
+
state=ApprovalWorkflowStateChoices.APPROVED,
|
|
762
|
+
)
|
|
763
|
+
self.assertEqual(
|
|
764
|
+
ApprovalWorkflowStageResponse.objects.filter(
|
|
765
|
+
approval_workflow_stage=approval_workflow_stage, user=self.user
|
|
766
|
+
).count(),
|
|
767
|
+
1,
|
|
768
|
+
)
|
|
769
|
+
self.client.force_login(self.user)
|
|
770
|
+
self.add_permissions("extras.change_approvalworkflowstage", "extras.view_approvalworkflowstage")
|
|
771
|
+
|
|
772
|
+
# Try GET again in form should be previous message
|
|
773
|
+
url = reverse("extras:approvalworkflowstage_comment", args=[approval_workflow_stage.pk])
|
|
774
|
+
response = self.client.get(url)
|
|
775
|
+
self.assertHttpStatus(response, 200)
|
|
776
|
+
self.assertBodyContains(response, new_response.comments)
|
|
777
|
+
|
|
778
|
+
request = {
|
|
779
|
+
"path": url,
|
|
780
|
+
"data": post_data({"comments": "Edit approved comment"}),
|
|
781
|
+
}
|
|
782
|
+
response = self.client.post(**request, follow=True)
|
|
783
|
+
self.assertHttpStatus(response, 200)
|
|
784
|
+
approval_workflow_stage.refresh_from_db()
|
|
785
|
+
self.assertEqual(
|
|
786
|
+
ApprovalWorkflowStageResponse.objects.filter(
|
|
787
|
+
approval_workflow_stage=approval_workflow_stage, user=self.user
|
|
788
|
+
).count(),
|
|
789
|
+
1,
|
|
790
|
+
)
|
|
791
|
+
edited_response = ApprovalWorkflowStageResponse.objects.get(
|
|
792
|
+
approval_workflow_stage=approval_workflow_stage, user=self.user
|
|
793
|
+
)
|
|
794
|
+
# assert state is still APPROVED
|
|
795
|
+
self.assertEqual(edited_response.state, ApprovalWorkflowStateChoices.APPROVED)
|
|
796
|
+
self.assertEqual(edited_response.comments, "Edit approved comment")
|
|
797
|
+
|
|
482
798
|
|
|
483
799
|
class ApprovalWorkflowStageResponseViewTestCase(
|
|
484
800
|
ViewTestCases.DeleteObjectViewTestCase,
|
|
@@ -517,6 +833,7 @@ class ApprovalWorkflowStageResponseViewTestCase(
|
|
|
517
833
|
name=f"Test Approval Workflow {i} Definition",
|
|
518
834
|
model_content_type=cls.scheduledjob_ct,
|
|
519
835
|
weight=i,
|
|
836
|
+
model_constraints={"job_model__name": "NoSuchJob"},
|
|
520
837
|
)
|
|
521
838
|
for i in range(5)
|
|
522
839
|
]
|