diff --git a/CHANGELOG.md b/CHANGELOG.md index 584d85993..939d08a21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ v0.9.14-dev (January xx, 2022) **Developer changes** * Add support for OIDC SSO configuration separate from OKTA SSO configuration. +* Add support for soft delete of components/elements by adding `deleted` field to `controls.Elements` model. * Update Django, libraries. * Remove debug-toolbar. @@ -25,6 +26,10 @@ v0.9.13 (January 23, 2022) **UI changes** +* Add button to mark component as deleted. + +**Developer changes** + * Add sign-in warning message to which users need to agree. * Reduce number of Group Django messages from question actions into single message for adding actions. * Simplify new authoring tool. Move prompt from right to left. Only show first line of question prompt. diff --git a/controls/admin.py b/controls/admin.py index 09134f3b0..0eca152ed 100644 --- a/controls/admin.py +++ b/controls/admin.py @@ -48,7 +48,7 @@ class StatementRemoteAdmin(admin.ModelAdmin): readonly_fields = ('created', 'updated', 'uuid') class ElementAdmin(GuardedModelAdmin, ExportCsvMixin): - list_display = ('name', 'full_name', 'element_type', 'id', 'uuid') + list_display = ('name', 'full_name', 'element_type', 'id', 'uuid', 'deleted') search_fields = ('name', 'full_name', 'uuid', 'id') actions = ["export_as_csv"] diff --git a/controls/migrations/0066_element_deleted.py b/controls/migrations/0066_element_deleted.py new file mode 100644 index 000000000..14930a211 --- /dev/null +++ b/controls/migrations/0066_element_deleted.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.10 on 2022-01-06 16:36 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('controls', '0065_auto_20211006_0240'), + ] + + operations = [ + migrations.AddField( + model_name='element', + name='deleted', + field=models.BooleanField(default=False, help_text='Mark Component as deleted'), + ), + ] diff --git a/controls/models.py b/controls/models.py index 4d4ee55c4..bb5668daf 100644 --- a/controls/models.py +++ b/controls/models.py @@ -263,7 +263,7 @@ class Element(auto_prefetch.Model, TagModelMixin): unique=False, blank=True, null=True, help_text="The Import Record which created this Element.") component_type = models.CharField(default="software", max_length=50, help_text="OSCAL Component Type.", unique=False, blank=True, null=True, choices=ComponentTypeEnum.choices()) component_state = models.CharField(default="operational", max_length=50, help_text="OSCAL Component State.", unique=False, blank=True, null=True, choices=ComponentStateEnum.choices()) - + deleted = models.BooleanField(default=False, help_text="Mark Component as deleted") # Notes # Retrieve Element controls where element is e to answer "What controls selected for a system?" (System is an element.) # element_id = 8 @@ -847,6 +847,7 @@ class CommonControl(auto_prefetch.Model, BaseModel): null=True) legacy_imp_smt = models.TextField(help_text="Legacy large implementation statement", unique=False, blank=True, null=True, ) + common_control_provider = auto_prefetch.ForeignKey(CommonControlProvider, on_delete=models.CASCADE) def __str__(self): diff --git a/controls/tests.py b/controls/tests.py index a5a138341..759b1d424 100644 --- a/controls/tests.py +++ b/controls/tests.py @@ -462,6 +462,18 @@ def test_element_create(self): e.delete() self.assertTrue(e.id is None) + + def test_soft_delete(self): + e = Element.objects.create(name="New Element", full_name="New Element Full Name", element_type="system_element") + self.assertEqual(e.name, "New Element") + self.assertTrue(e.id is not None) + self.assertTrue(e.deleted == False) + e.deleted = True + e.save() + e2 = Element.objects.get(pk=e.id) + self.assertEqual(e2.deleted, True) + + def test_element_assign_owner_permissions(self): e = Element.objects.create(name="New Element", full_name="New Element Full Name", element_type="system") e.save() diff --git a/controls/urls.py b/controls/urls.py index 7820f5e21..915bf0deb 100644 --- a/controls/urls.py +++ b/controls/urls.py @@ -4,6 +4,7 @@ from controls.models import Element from siteapp.model_mixins.tags import TagView, build_tag_urls +# delete_tag_urls admin.autodiscover() @@ -89,10 +90,14 @@ url(r'^elements/(\d+)/__edit$', views.edit_element, name="edit_element"), *build_tag_urls(r"^elements/(\d+)/", model=Element), # Tag Urls + url(r'^elements/(\d+)/__delete$', views.delete_element, name="delete_element"), + # *delete_component(r"^elements/(\d+)/", model=Element), # Tag Urls + + # Controls url(r'^catalogs/(?P.*)/group/(?P.*)', views.group, name="control_group"), url(r'^catalogs/(?P.*)/control/(?P.*)', views.control, name="control_info"), - url(r'^api/controlsselect/', views.api_controls_select, name="api_controls_select"), + url(r'^api/controls/select/', views.api_controls_select, name="api_controls_select"), # System Security plan url(r'^(?P.*)/export/oscal', views.OSCAL_ssp_export, name="ssp_export_oscal"), diff --git a/controls/views.py b/controls/views.py index d1afe9d4b..2d488a950 100644 --- a/controls/views.py +++ b/controls/views.py @@ -366,6 +366,16 @@ def edit_element(request, element_id): msg_list = [f"{e.title()} - {errors[e][0]['message']}" for e in errors.keys()] return JsonResponse({"status": "err", "message": "Please fix the following problems:
"+"
".join(msg_list)}) +@login_required +def delete_element(request, element_id): + + if request.method == 'POST': + Element.objects.filter(pk=element_id).update(deleted = True) + messages.add_message(request, messages.INFO, f"Element has been marked deleted.") + return HttpResponseRedirect('/controls/components') + else: + return HttpResponseRedirect('/controls/components') + class SelectedComponentsList(ListView): """ Display System's selected components view diff --git a/siteapp/model_mixins/tags.py b/siteapp/model_mixins/tags.py index 87610cc6b..88a017436 100644 --- a/siteapp/model_mixins/tags.py +++ b/siteapp/model_mixins/tags.py @@ -63,3 +63,4 @@ def build_tag_urls(path_prefix, model): url(rf'{path_prefix}tags/$', lambda *args, **kwargs: TagView.list_tags(*args, model, **kwargs), name=f"list_element_{model.__name__.lower()}"), ] + diff --git a/templates/components/element_detail_tabs.html b/templates/components/element_detail_tabs.html index 0ed8bd147..940500d6c 100644 --- a/templates/components/element_detail_tabs.html +++ b/templates/components/element_detail_tabs.html @@ -64,6 +64,8 @@ .control-id-text { font-weight: bold; } + .cmpt-deleted { color:red; font-weight: bold; } + {% endblock %} @@ -110,7 +112,11 @@

{% endif %} @@ -122,6 +128,9 @@

About

+ {% if element.deleted %} +

DELETED This component has been marked as deleted.

+ {% endif %} {% if element.description %} {{ element.description }} {% else %} @@ -354,6 +363,7 @@

Systems

{% include "edit-component-modal.html" %} +{% include "delete-component-modal.html" %} {{ block.super }} {% endblock %} @@ -515,6 +525,28 @@

Systems

}); }); } + + function delete_component() { + show_delete_component_modal("{{element.name}}","{{element.description}}",(componentName, reason)=>{ + ajax_with_indicator({ + url: '{% url "delete_element" element.id %}', + method: "POST", + data: {name: componentName,description: reason}, + keep_indicator_forever: true, + success: function(res) { + + if(res["status"]=="ok"){ + hide_edit_component_modal(); + window.location.reload(); + } + if(res["status"]=="err"){ + show_delete_component_modal_error(res["message"]) + } + } + }); + }); + } + {% endblock %} diff --git a/templates/delete-component-modal.html b/templates/delete-component-modal.html new file mode 100644 index 000000000..7485b4fa2 --- /dev/null +++ b/templates/delete-component-modal.html @@ -0,0 +1,49 @@ + + + + {% block scripts %} + + {% endblock %} \ No newline at end of file