diff --git a/CHANGELOG.md b/CHANGELOG.md index ba12b7b34..da34a8acb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,23 @@ +# 2.5.1 2024-02-08 + +## Fix + +- CSS issue in documentation rendering for
 block in dark theme
+- Template issue on documentation when applying jinja filters on None
+- Remove superusers from "list_approvers". They still can approve but are not displayed in the list.
+- When editing an ApprovalWorkflow, it was possible to use a scope already assigned to another workflow.
+
+## Enhancement
+
+- List related docs in OperationDetails and ServiceDetails view
+- Add PROCESSING requests in the dashboard of the main page
+- Display only docs that are not linked to services or operations in ListDoc
+
+## Feature
+
+- ApprovalWorkflow preview.
+- Provided field validators (json and public ssh key).
+
 # 2.5.0 2024-01-09
 
 ## Fix 
diff --git a/Squest/utils/squest_model.py b/Squest/utils/squest_model.py
index 8b2baf402..9d676b9a0 100644
--- a/Squest/utils/squest_model.py
+++ b/Squest/utils/squest_model.py
@@ -111,8 +111,7 @@ def get_scopes(self):
         squest_scope = GlobalScope.load()
         return squest_scope.get_scopes()
 
-
-    def who_has_perm(self, permission_str):
+    def who_has_perm(self, permission_str, exclude_superuser=False):
         app_label, codename = permission_str.split(".")
 
         # Global Perm permission for all users
@@ -134,7 +133,11 @@ def who_has_perm(self, permission_str):
         )
 
         rbacs = rbac0 | rbac1
-        return User.objects.filter(Q(groups__in=rbacs) | Q(is_superuser=True)).distinct()
+        if exclude_superuser is True:
+            return User.objects.filter(Q(groups__in=rbacs)).distinct()
+        else:
+            return User.objects.filter(Q(groups__in=rbacs) | Q(is_superuser=True)).distinct()
+
 
 class SquestChangelog(Model):
     class Meta:
diff --git a/Squest/version.py b/Squest/version.py
index c3c61c86f..037904b86 100644
--- a/Squest/version.py
+++ b/Squest/version.py
@@ -1,2 +1,2 @@
-__version__ = "2.5.0"
+__version__ = "2.5.1"
 VERSION = __version__
diff --git a/Squest/views.py b/Squest/views.py
index 70dd48491..a172eab6f 100644
--- a/Squest/views.py
+++ b/Squest/views.py
@@ -80,6 +80,9 @@ def home(request):
         service_dict["hold_requests"] = sum([x["count"] for x in all_requests if
                                                   x["state"] == RequestState.ON_HOLD and x[
                                                       "instance__service"] == service.id])
+        service_dict["processing_requests"] = sum([x["count"] for x in all_requests if
+                                                  x["state"] == RequestState.PROCESSING and x[
+                                                      "instance__service"] == service.id])
 
         service_dict["opened_supports"] = sum([x["count"] for x in all_supports if
                                                x["state"] == SupportState.OPENED and x[
diff --git a/docs/manual/service_catalog/docs.md b/docs/manual/service_catalog/docs.md
index 7461c9758..fa13b59d6 100644
--- a/docs/manual/service_catalog/docs.md
+++ b/docs/manual/service_catalog/docs.md
@@ -4,6 +4,10 @@ Docs section allow administrators to create and link documentation to Squest ser
 
 Documentation are writen with Markdown syntax.
 
+!!!note
+
+    Docs linked to a service or an operation are not listed in the global doc list from the sidebar menu.
+
 ## Linked to services
 
 When linked to one or more service, the documentation is shown in each "instance detail" page that correspond to the type of selected services.
diff --git a/plugins/field_validators/is_json.py b/plugins/field_validators/is_json.py
new file mode 100644
index 000000000..0ddaf80dd
--- /dev/null
+++ b/plugins/field_validators/is_json.py
@@ -0,0 +1,26 @@
+import json
+
+# For testing
+try:
+    from django.core.exceptions import ValidationError as UIValidationError
+    from rest_framework.serializers import ValidationError as APIValidationError
+except ImportError:
+    pass
+
+
+def is_json(json_str):
+    try:
+        json.loads(json_str)
+    except ValueError as e:
+        return False
+    return True
+
+
+def validate_api(value):
+    if not is_json(value):
+        raise APIValidationError("is not JSON")
+
+
+def validate_ui(value):
+    if not is_json(value):
+        raise UIValidationError("is not JSON")
diff --git a/plugins/field_validators/is_public_ssh_key.py b/plugins/field_validators/is_public_ssh_key.py
new file mode 100644
index 000000000..8fb2a61c2
--- /dev/null
+++ b/plugins/field_validators/is_public_ssh_key.py
@@ -0,0 +1,49 @@
+import base64
+import binascii
+import struct
+
+# For testing
+try:
+    from django.core.exceptions import ValidationError as UIValidationError
+    from rest_framework.serializers import ValidationError as APIValidationError
+except ImportError:
+    pass
+
+ERROR_MESSAGE = "Is not a valid public ssh key"
+
+def is_public_ssh_key(ssh_key):
+    array = ssh_key.split()
+    # Each rsa-ssh key has 3 different strings in it, first one being
+    # type_of_key second one being keystring third one being username.
+    if len(array) not in [2, 3]:
+        return False
+    type_of_key = array[0]
+    ssh_key_str = array[1]
+
+    # must have only valid rsa-ssh key characters ie binascii characters
+    try:
+        data = base64.decodebytes(bytes(ssh_key_str, 'utf-8'))
+    except binascii.Error:
+        return False
+    a = 4
+    # unpack the contents of ssh_key, from ssh_key[:4] , it must be equal to 7 , property of ssh key .
+    try:
+        str_len = struct.unpack('>I', data[:a])[0]
+    except struct.error:
+        return False
+    # ssh_key[4:11] must have string which matches with the type_of_key , another ssh key property.
+    print(str_len)
+    if data[a:a + str_len].decode(encoding='utf-8') == type_of_key:
+        return True
+    else:
+        return False
+
+
+def validate_api(value):
+    if not is_public_ssh_key(value):
+        raise APIValidationError(ERROR_MESSAGE)
+
+
+def validate_ui(value):
+    if not is_public_ssh_key(value):
+        raise UIValidationError(ERROR_MESSAGE)
diff --git a/profiles/models/scope.py b/profiles/models/scope.py
index 47ccfe3ff..82d3bc33e 100644
--- a/profiles/models/scope.py
+++ b/profiles/models/scope.py
@@ -123,6 +123,7 @@ def get_queryset(self):
     def expand(self):
         return self.get_queryset().expand()
 
+
 class Scope(AbstractScope):
     class Meta:
         permissions = [
@@ -178,3 +179,24 @@ def get_q_filter(cls, user, perm):
 
     def get_absolute_url(self):
         return self.get_object().get_absolute_url()
+
+    def get_workflows(self):
+        from service_catalog.models import Operation, ApprovalWorkflow
+        operations = Operation.objects.filter(enabled=True)
+
+        # Teams
+        approval_workflow = ApprovalWorkflow.objects.filter(scopes__id=self.id, operation__in=operations, enabled=True)
+        operations = operations.exclude(id__in=approval_workflow.values_list('operation__id', flat=True))
+
+        # Org
+        if self.is_team:
+            approval_workflow = approval_workflow | ApprovalWorkflow.objects.filter(scopes__id=self.get_object().org.id,
+                                                                                    operation__in=operations,
+                                                                                    enabled=True)
+            operations = operations.exclude(id__in=approval_workflow.values_list('operation__id', flat=True))
+
+        # Default
+        approval_workflow = approval_workflow | ApprovalWorkflow.objects.filter(scopes__isnull=True,
+                                                                                operation__in=operations,
+                                                                                enabled=True)
+        return approval_workflow
diff --git a/profiles/tables/__init__.py b/profiles/tables/__init__.py
index a247497b4..7b3680b44 100644
--- a/profiles/tables/__init__.py
+++ b/profiles/tables/__init__.py
@@ -4,3 +4,4 @@
 from .team_table import *
 from .user_table import *
 from .permission_table import *
+from .approval_workflow import *
diff --git a/profiles/tables/approval_workflow.py b/profiles/tables/approval_workflow.py
new file mode 100644
index 000000000..fa5b574d9
--- /dev/null
+++ b/profiles/tables/approval_workflow.py
@@ -0,0 +1,19 @@
+from django.utils.html import format_html
+from django_tables2 import LinkColumn, TemplateColumn
+
+from Squest.utils.squest_table import SquestTable
+from profiles.models import Scope
+
+
+class ApprovalWorkflowPreviewTable(SquestTable):
+
+    name = LinkColumn()
+    preview = TemplateColumn(template_name='profiles/custom_columns/preview_workflow.html', orderable=False)
+
+    class Meta:
+        model = Scope
+        attrs = {"id": "role_table", "class": "table squest-pagination-tables"}
+        fields = ("name", "preview")
+
+    def render_name(self, value, record):
+        return format_html(f'{record}')
diff --git a/profiles/views/organization.py b/profiles/views/organization.py
index 283991122..0c030b4be 100644
--- a/profiles/views/organization.py
+++ b/profiles/views/organization.py
@@ -7,6 +7,7 @@
 from profiles.models import Organization, Team
 from profiles.tables import OrganizationTable, ScopeRoleTable, TeamTable, UserRoleTable
 from profiles.tables.quota_table import QuotaTable
+from service_catalog.tables.approval_workflow_table import ApprovalWorkflowPreviewTable
 
 
 class OrganizationListView(SquestListView):
@@ -38,7 +39,10 @@ def get_context_data(self, **kwargs):
             hide_fields=('org',), prefix="team-"
         )
         config.configure(context['teams'])
-
+        if self.request.user.has_perm("service_catalog.view_approvalworkflow"):
+            context["workflows"] = ApprovalWorkflowPreviewTable(self.get_object().get_workflows(), prefix="workflow-",
+                                                         hide_fields=["enabled", "actions", "scopes"])
+            config.configure(context["workflows"])
         context['roles'] = ScopeRoleTable(self.object.roles.distinct())
         config.configure(context['roles'])
 
diff --git a/profiles/views/team.py b/profiles/views/team.py
index 79e2f2386..22b448d81 100644
--- a/profiles/views/team.py
+++ b/profiles/views/team.py
@@ -7,6 +7,7 @@
 from profiles.models.team import Team
 from profiles.tables import UserRoleTable, ScopeRoleTable, TeamTable
 from profiles.tables.quota_table import QuotaTable
+from service_catalog.tables.approval_workflow_table import ApprovalWorkflowPreviewTable
 
 
 def get_organization_breadcrumbs(team):
@@ -44,6 +45,11 @@ def get_context_data(self, **kwargs):
         context['roles'] = ScopeRoleTable(self.object.roles.distinct(), prefix="role-")
         config.configure(context['roles'])
 
+        if self.request.user.has_perm("service_catalog.view_approvalworkflow"):
+            context["workflows"] = ApprovalWorkflowPreviewTable(self.get_object().get_workflows(), prefix="workflow-",
+                                                         hide_fields=["enabled", "actions", "scopes"])
+            config.configure(context["workflows"])
+
         return context
 
 
diff --git a/project-static/squest/css/squest-dark.css b/project-static/squest/css/squest-dark.css
index ac7e826d2..86dad5aa4 100644
--- a/project-static/squest/css/squest-dark.css
+++ b/project-static/squest/css/squest-dark.css
@@ -7,7 +7,7 @@ footer{
     color: #fff !important;
 }
 
-.bg-dark table tr,.bg-dark blockquote,.bg-dark pre {
+.bg-dark table tr,.bg-dark blockquote,details.bg-dark pre {
     background-color: #343a40 !important;
     color: #fff !important;
 }
diff --git a/project-static/squest/css/squest.css b/project-static/squest/css/squest.css
index 20a93b71f..6aeca8588 100644
--- a/project-static/squest/css/squest.css
+++ b/project-static/squest/css/squest.css
@@ -93,3 +93,7 @@ input.form-control[disabled] {
 .popover {
     max-width: 100%;
 }
+
+.callout a{
+    color: unset;
+}
diff --git a/pyproject.toml b/pyproject.toml
index 0ea49faee..f8994539c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
 [tool.poetry]
 name = "squest"
-version = "2.5.0"
+version = "2.5.1"
 description = "Service catalog on top of Red Hat Ansible Automation Platform(RHAAP)/AWX (formerly known as Ansible Tower)"
 authors = ["Nicolas Marcq ", "Elias Boulharts "]
 license = "MIT"
diff --git a/service_catalog/admin.py b/service_catalog/admin.py
index 478d7c3b8..a175321f9 100644
--- a/service_catalog/admin.py
+++ b/service_catalog/admin.py
@@ -10,5 +10,14 @@ class DocAdmin(admin.ModelAdmin):
         models.TextField: {'widget': AdminMartorWidget},
     }
 
+    list_filter = ['services', 'operations']
+    list_display = ['title', 'linked_services', 'linked_operations']
+
+    def linked_services(self, obj):
+        return ", ".join([str(service) for service in obj.services.all()])
+
+    def linked_operations(self, obj):
+        return ", ".join([str(operation) for operation in obj.operations.all()])
+
 
 admin.site.register(Doc, DocAdmin)
diff --git a/service_catalog/filters/doc_filter.py b/service_catalog/filters/doc_filter.py
index 940c468a9..282e7a3f7 100644
--- a/service_catalog/filters/doc_filter.py
+++ b/service_catalog/filters/doc_filter.py
@@ -13,8 +13,6 @@ def __init__(self, *args, **kwargs):
 
 
 class DocFilter(SquestFilter):
-    services = ServiceFilter(widget=SelectMultiple(attrs={'data-live-search': "true"}))
-
     class Meta:
         model = Doc
-        fields = ['title', 'services']
+        fields = ['title']
diff --git a/service_catalog/forms/approval_workflow_form.py b/service_catalog/forms/approval_workflow_form.py
index 779505fe5..104721e3d 100644
--- a/service_catalog/forms/approval_workflow_form.py
+++ b/service_catalog/forms/approval_workflow_form.py
@@ -10,9 +10,12 @@ class Meta:
         fields = ['name', 'operation', 'scopes', 'enabled']
 
     def clean(self):
-        cleaned_data = super().clean()
+        cleaned_data = super(ApprovalWorkflowForm, self).clean()
         operation = cleaned_data.get("operation")
         scopes = cleaned_data.get("scopes")
+        self.custom_clean_scopes(operation, scopes)
+
+    def custom_clean_scopes(self, operation, scopes):
         # check that selected scopes are not already in use by another approval workflow for the selected operation
         exclude_id = self.instance.id if self.instance else None
         if not scopes.exists():
@@ -29,3 +32,8 @@ class ApprovalWorkflowFormEdit(ApprovalWorkflowForm):
     class Meta:
         model = ApprovalWorkflow
         fields = ['name', 'scopes', 'enabled']
+
+    def clean(self):
+        cleaned_data = super(ApprovalWorkflowForm, self).clean()
+        scopes = cleaned_data.get("scopes")
+        self.custom_clean_scopes(self.instance.operation, scopes)
diff --git a/service_catalog/forms/operation_forms.py b/service_catalog/forms/operation_forms.py
index da3c15c51..ac7625535 100644
--- a/service_catalog/forms/operation_forms.py
+++ b/service_catalog/forms/operation_forms.py
@@ -28,7 +28,7 @@ def __init__(self, *args, **kwargs):
         if self.instance is not None:
             if self.instance.validators is not None:
                 # Converting comma separated string to python list
-                instance_validator_as_list = self.instance.validators.split(",")
+                instance_validator_as_list = self.instance.validators_name
                 # set the current value
                 self.initial["validators"] = instance_validator_as_list
 
diff --git a/service_catalog/models/approval_step_state.py b/service_catalog/models/approval_step_state.py
index 223747c12..da045488a 100644
--- a/service_catalog/models/approval_step_state.py
+++ b/service_catalog/models/approval_step_state.py
@@ -59,12 +59,12 @@ def reset_to_pending(self):
     def get_scopes(self):
         return self.approval_workflow_state.get_scopes()
 
-    def who_can_approve(self):
+    def who_can_approve(self, exclude_superuser=False):
         return self.approval_workflow_state.request.instance.quota_scope.who_has_perm(
-            self.approval_step.permission.permission_str)
+            self.approval_step.permission.permission_str, exclude_superuser=exclude_superuser)
 
-    def who_can_accept(self):
-        return self.who_can_approve()
+    def who_can_accept(self, exclude_superuser=False):
+        return self.who_can_approve(exclude_superuser)
 
     @classmethod
     def get_q_filter(cls, user, perm):
diff --git a/service_catalog/models/approval_workflow_state.py b/service_catalog/models/approval_workflow_state.py
index 8d5c435d8..7d78e5a12 100644
--- a/service_catalog/models/approval_workflow_state.py
+++ b/service_catalog/models/approval_workflow_state.py
@@ -62,7 +62,7 @@ def first_step(self):
             return first_step.first()
         return None
 
-    def who_can_approve(self):
+    def who_can_approve(self, exclude_superuser=False):
         if self.current_step is not None:
-            return self.current_step.who_can_approve()
+            return self.current_step.who_can_approve(exclude_superuser=exclude_superuser)
         return User.objects.none()
diff --git a/service_catalog/models/documentation.py b/service_catalog/models/documentation.py
index 5efdb8b97..e2c150e24 100644
--- a/service_catalog/models/documentation.py
+++ b/service_catalog/models/documentation.py
@@ -36,14 +36,16 @@ def render(self, instance=None):
             return self.content
         try:
             template = Template(self.content)
+            context = {
+                "instance": instance
+            }
+            return template.render(context)
         except UndefinedError as e:
             logger.warning(f"Error: {e.message}, instance: {instance}, doc: {self}")
             raise TemplateError(e)
         except TemplateSyntaxError as e:
             logger.warning(f"Error: {e.message}, instance: {instance}, doc: {self}")
             raise TemplateError(e)
-
-        context = {
-            "instance": instance
-        }
-        return template.render(context)
+        except TypeError as e:
+            logger.warning(f"Error: {e}, instance: {instance}, doc: {self}")
+            raise TemplateError(e)
diff --git a/service_catalog/models/operations.py b/service_catalog/models/operations.py
index cf14b889d..34ea04d97 100644
--- a/service_catalog/models/operations.py
+++ b/service_catalog/models/operations.py
@@ -52,10 +52,14 @@ class Operation(SquestModel):
                                  help_text="Jinja supported. Job template type")
     validators = CharField(null=True, blank=True, max_length=200, verbose_name="Survey validators")
 
+    @property
+    def validators_name(self):
+        return self.validators.split(",") if self.validators else None
+
     def get_validators(self):
         validators = list()
         if self.validators is not None:
-            all_validators = self.validators.split(",")
+            all_validators = self.validators_name
             all_validators.sort()
             for validator_file in all_validators:
                 validator = PluginController.get_survey_validator_def(validator_file)
diff --git a/service_catalog/models/request.py b/service_catalog/models/request.py
index 5ff64c2fb..ea95a1ad0 100644
--- a/service_catalog/models/request.py
+++ b/service_catalog/models/request.py
@@ -104,11 +104,12 @@ def get_scopes(self):
     def __str__(self):
         return f"#{self.id}"
 
-    def who_can_accept(self):
+    def who_can_accept(self, exclude_superuser=False):
         if self.approval_workflow_state is not None:
-            return self.approval_workflow_state.who_can_approve()
+            return self.approval_workflow_state.who_can_approve(exclude_superuser=exclude_superuser)
         else:
-            return self.instance.quota_scope.who_has_perm("service_catalog.accept_request")
+            return self.instance.quota_scope.who_has_perm("service_catalog.accept_request",
+                                                          exclude_superuser=exclude_superuser)
 
     def full_survey_user(self, approval_step_state=None):
 
@@ -167,6 +168,7 @@ def re_submit(self, save=True):
         self.setup_approval_workflow()
         if save:
             self.save()
+
     @transition(field=state, source=[RequestState.SUBMITTED,
                                      RequestState.ON_HOLD,
                                      RequestState.REJECTED,
@@ -470,7 +472,6 @@ def on_change(cls, sender, instance, *args, **kwargs):
                                          *args, **kwargs)
 
 
-
 pre_save.connect(Request.on_change, sender=Request)
 post_save.connect(Request.on_create, sender=Request)
 post_save.connect(Request.auto_accept_and_process_signal, sender=Request)
diff --git a/service_catalog/tables/approval_workflow_table.py b/service_catalog/tables/approval_workflow_table.py
index 4c69afd75..d10644c1f 100644
--- a/service_catalog/tables/approval_workflow_table.py
+++ b/service_catalog/tables/approval_workflow_table.py
@@ -20,3 +20,13 @@ def render_scopes(self, value, record):
         for scope in scopes:
             html += f"{scope}  "
         return format_html(html)
+
+
+class ApprovalWorkflowPreviewTable(SquestTable):
+    class Meta:
+        model = ApprovalWorkflow
+        attrs = {"id": "approval_workflow_table", "class": "table squest-pagination-tables "}
+        fields = ("name", "operation", "preview")
+
+    name = LinkColumn()
+    preview = TemplateColumn(template_name='service_catalog/custom_columns/preview_workflow.html', orderable=False)
diff --git a/service_catalog/tables/doc_tables.py b/service_catalog/tables/doc_tables.py
index 2368920a6..196a79c2f 100644
--- a/service_catalog/tables/doc_tables.py
+++ b/service_catalog/tables/doc_tables.py
@@ -6,12 +6,9 @@
 
 
 class DocTable(SquestTable):
-    actions = TemplateColumn(template_name='service_catalog/custom_columns/doc_actions.html', orderable=False)
-    services = TemplateColumn(template_name='service_catalog/custom_columns/doc_services.html', verbose_name="Linked services")
-    operations = TemplateColumn(template_name='service_catalog/custom_columns/doc_operations.html', verbose_name="Linked operations")
     title = LinkColumn("service_catalog:doc_details", args=[A("id")])
 
     class Meta:
         model = Doc
         attrs = {"id": "doc_table", "class": "table squest-pagination-tables"}
-        fields = ("title", "services", "operations", "actions")
+        fields = ("title",)
diff --git a/service_catalog/tables/instance_tables.py b/service_catalog/tables/instance_tables.py
index bc7c4172b..7b8053120 100644
--- a/service_catalog/tables/instance_tables.py
+++ b/service_catalog/tables/instance_tables.py
@@ -1,4 +1,3 @@
-
 from django.utils.html import format_html
 from django_tables2 import TemplateColumn, LinkColumn, CheckBoxColumn, Column
 
@@ -8,8 +7,10 @@
 
 class InstanceTable(SquestTable):
     selection = CheckBoxColumn(accessor='pk', attrs={"th__input": {"onclick": "toggle(this)"}})
+    id = LinkColumn()
     quota_scope__name = Column(verbose_name='Quota scope')
-    name = LinkColumn()
+    name = LinkColumn(verbose_name="Name")
+    service__name = Column(verbose_name="Service")
     date_available = TemplateColumn(template_name='generics/custom_columns/generic_date_format.html')
     last_updated = TemplateColumn(template_name='generics/custom_columns/generic_date_format.html')
 
@@ -20,8 +21,13 @@ def before_render(self, request):
     class Meta:
         model = Instance
         attrs = {"id": "instance_table", "class": "table squest-pagination-tables"}
-        fields = ("selection", "name", "service__name", "quota_scope__name", "state", "requester", "date_available", "last_updated")
+        fields = (
+            "selection", "id", "name", "service__name", "quota_scope__name", "state", "requester", "date_available",
+            "last_updated")
 
     def render_state(self, record, value):
         from service_catalog.views import map_instance_state
-        return format_html(f' { value } ')
+        return format_html(f' {value} ')
+
+    def render_id(self, value, record):
+        return f"#{value}"
diff --git a/service_catalog/tables/operation_tables.py b/service_catalog/tables/operation_tables.py
index 9fa31c1ae..3c7eb9636 100644
--- a/service_catalog/tables/operation_tables.py
+++ b/service_catalog/tables/operation_tables.py
@@ -26,7 +26,6 @@ class Meta:
         attrs = {"id": "operation_table", "class": "table squest-pagination-tables"}
         fields = ("name", "description", "type", "is_admin_operation", "actions")
 
-    name = LinkColumn()
     type = TemplateColumn(template_name='service_catalog/custom_columns/operation_type.html')
     actions = TemplateColumn(template_name='service_catalog/custom_columns/operation_request.html', orderable=False,
                              verbose_name="")
@@ -39,7 +38,6 @@ class Meta:
         attrs = {"id": "operation_table", "class": "table squest-pagination-tables"}
         fields = ("name", "description", "is_admin_operation", "actions")
 
-    name = LinkColumn()
     actions = TemplateColumn(template_name='service_catalog/custom_columns/create_operation_request.html',
                             orderable=False)
     is_admin_operation = TemplateColumn(template_name='generics/custom_columns/generic_boolean.html')
diff --git a/service_catalog/tables/request_tables.py b/service_catalog/tables/request_tables.py
index 5c0836426..e9c90df14 100644
--- a/service_catalog/tables/request_tables.py
+++ b/service_catalog/tables/request_tables.py
@@ -8,7 +8,8 @@
 
 class RequestTable(SquestTable):
     selection = CheckBoxColumn(accessor='pk', attrs={"th__input": {"onclick": "toggle(this)"}})
-    id = Column(linkify=True, verbose_name="Request")
+    id = Column(linkify=True, verbose_name="ID")
+    user__username = Column(verbose_name="User")
     date_submitted = TemplateColumn(template_name='generics/custom_columns/generic_date_format.html')
     instance = LinkColumn()
     last_updated = TemplateColumn(template_name='generics/custom_columns/generic_date_format.html')
diff --git a/service_catalog/urls.py b/service_catalog/urls.py
index ddbaa7297..579f99e30 100644
--- a/service_catalog/urls.py
+++ b/service_catalog/urls.py
@@ -162,6 +162,7 @@
     # Approval Workflow CRUD
     path('administration/approval/', views.ApprovalWorkflowListView.as_view(), name='approvalworkflow_list'),
     path('administration/approval//', views.ApprovalWorkflowDetailView.as_view(), name='approvalworkflow_details'),
+    path('administration/approval//scope/', views.ApprovalWorkflowPreviewView.as_view(), name='approvalworkflow_preview'),
     path('administration/approval/create/', views.ApprovalWorkflowCreateView.as_view(), name='approvalworkflow_create'),
     path('administration/approval//edit/', views.ApprovalWorkflowEditView.as_view(), name='approvalworkflow_edit'),
     path('administration/approval//delete/', views.ApprovalWorkflowDeleteView.as_view(), name='approvalworkflow_delete'),
diff --git a/service_catalog/views/approval_workflow_views.py b/service_catalog/views/approval_workflow_views.py
index 75314dc57..934380879 100644
--- a/service_catalog/views/approval_workflow_views.py
+++ b/service_catalog/views/approval_workflow_views.py
@@ -1,9 +1,13 @@
+from django.shortcuts import get_object_or_404
+
 from Squest.utils.squest_table import SquestRequestConfig
 from Squest.utils.squest_views import SquestListView, SquestDetailView, SquestCreateView, SquestUpdateView, \
     SquestDeleteView, SquestConfirmView
+from profiles.models import Scope, Organization, Team
+from profiles.tables import ApprovalWorkflowPreviewTable
 from service_catalog.filters.approval_workflow_filter import ApprovalWorkflowFilter
 from service_catalog.forms.approval_workflow_form import ApprovalWorkflowForm, ApprovalWorkflowFormEdit
-from service_catalog.models import ApprovalWorkflow
+from service_catalog.models import ApprovalWorkflow, Request
 from service_catalog.tables.approval_workflow_table import ApprovalWorkflowTable
 from service_catalog.tables.request_tables import RequestTablesForApprovalWorkflow
 
@@ -27,10 +31,52 @@ def get_context_data(self, **kwargs):
                 "operation__service", "approval_workflow_state", "approval_workflow_state__approval_workflow",
                 "approval_workflow_state__current_step",
                 "approval_workflow_state__current_step__approval_step", "approval_workflow_state__approval_step_states"
-            ))
-        context['request_table'].exclude = ("selection","last_updated","date_submitted")
-
+            ).filter(id__in=Request.get_queryset_for_user(self.request.user, "service_catalog.view_request")),
+            hide_fields=["selection", "last_updated", "date_submitted"])
         config.configure(context['request_table'])
+
+        context["scope_table"] = ApprovalWorkflowPreviewTable(
+            Scope.objects.filter(id__in=Organization.get_queryset_for_user(self.request.user, 'profiles.view_organization').values_list("id",flat=True)) |
+            Scope.objects.filter(id__in=Team.get_queryset_for_user(self.request.user, 'profiles.view_team')).values_list("id", flat=True)
+        )
+        config.configure(context['scope_table'])
+
+        return context
+
+
+class ApprovalWorkflowPreviewView(SquestDetailView):
+    model = ApprovalWorkflow
+    template_name = "service_catalog/approvalworkflow_preview.html"
+
+    def get_permission_required(self):
+        return "service_catalog.view_approvalworkflow"
+
+    def get_context_data(self, **kwargs):
+        context = super().get_context_data(**kwargs)
+        scope = get_object_or_404(Scope, id=self.kwargs.get("scope_id"))
+        context["current_workflow"] = scope.get_workflows().filter(operation=self.get_object().operation).first()
+        context['scope'] = scope
+        if scope.is_team:
+            perm_add_approvers = "profiles.add_users_team"
+        if scope.is_org:
+            perm_add_approvers = "profiles.add_users_organization"
+        context["can_add_approvers"] = self.request.user.has_perm(perm_add_approvers, scope)
+        context["can_add_role"] = self.request.user.has_perm("profiles.add_role")
+        context['breadcrumbs'] = [
+            {
+                'text': self.django_content_type.name.capitalize(),
+                'url': self.get_generic_url('list')
+            },
+            {
+                'text': str(self.get_object()),
+                'url': self.get_object().get_absolute_url()
+            },
+            {
+                'text': scope,
+                'url': ''
+            },
+        ]
+
         return context
 
 
diff --git a/service_catalog/views/doc.py b/service_catalog/views/doc.py
index 9446e20e1..878ebd80b 100644
--- a/service_catalog/views/doc.py
+++ b/service_catalog/views/doc.py
@@ -20,6 +20,9 @@ class DocListView(SquestListView):
     model = Doc
     filterset_class = DocFilter
 
+    def get_queryset(self):
+        return Doc.objects.filter(services__isnull=True, operations__isnull=True)
+
     def get_context_data(self, **kwargs):
         context = super().get_context_data(**kwargs)
         context['html_button_path'] = ""
diff --git a/service_catalog/views/filters.py b/service_catalog/views/filters.py
index a4d0671ee..2348b585a 100644
--- a/service_catalog/views/filters.py
+++ b/service_catalog/views/filters.py
@@ -189,6 +189,11 @@ def display_boolean(boolean_value):
         return mark_safe('')
 
 
+@register.simple_tag()
+def who_can_approve_on_scope(step, scope):
+    return scope.who_has_perm(step.permission.permission_str, exclude_superuser=True)
+
+
 @register.simple_tag()
 def who_can_approve(object):
-    return object.who_can_accept()
+    return object.who_can_accept(exclude_superuser=True)
diff --git a/templates/base.html b/templates/base.html
index 2c4a28c0b..65486680a 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -75,7 +75,7 @@
                                 {% include "generics/breadcrumbs.html" %}
                             
                             
-
+
{% block extra_header_button %} {% if extra_html_button_path %} {% include extra_html_button_path %} diff --git a/templates/generics/doc_aside.html b/templates/generics/doc_aside.html index 506b82047..efecd8270 100644 --- a/templates/generics/doc_aside.html +++ b/templates/generics/doc_aside.html @@ -20,7 +20,7 @@
data-parent="#accordion_docs">
- {{ doc.content| safe_markdown }} + {{ doc.content | safe_markdown }}
diff --git a/templates/home/home.html b/templates/home/home.html index f5d839130..de1772ae9 100644 --- a/templates/home/home.html +++ b/templates/home/home.html @@ -36,6 +36,7 @@

Service overview

Submitted Accepted On hold + Processing Failed {% if can_list_support %} Supports @@ -54,8 +55,8 @@

Service overview

> - - {{ service.service.name }} + + {{ service.service.name }} @@ -86,6 +87,13 @@

Service overview

{% endif %} + + {% if service.processing_requests %} + {{ service.processing_requests }} + + {% endif %} + {% if service.failed_requests %} To be reviewed {% if can_list_instance %}
-
+

{{ total_instance }}

Available instances

@@ -140,7 +148,7 @@

{{ total_instance }}

{% endif %} {% if can_list_request %}
-
+
@@ -96,6 +99,11 @@

{{ object.name }}

{% render_table quotas %} {% endif %}
+
+ {% if workflows %} + {% render_table workflows %} + {% endif %} +
diff --git a/templates/service_catalog/approvalworkflow_detail.html b/templates/service_catalog/approvalworkflow_detail.html index cf9cd915e..73fb958ee 100644 --- a/templates/service_catalog/approvalworkflow_detail.html +++ b/templates/service_catalog/approvalworkflow_detail.html @@ -15,148 +15,161 @@ {% endblock %} {% block main %} -
-
-
-
-
-

{{ object.name }}

-
-
-
    -
  • - ID{{ object.id }} -
  • -
  • - Name{{ object.name }} -
  • -
  • - Hash{{ object.hash | to_hexa }} -
  • -
  • - Operation - {{ object.operation.name }} -
  • +
    +
    +
    +
    +
    +

    {{ object.name }}

    +
    +
    +
      +
    • + ID{{ object.id }} +
    • +
    • + Name{{ object.name }} +
    • +
    • + Hash{{ object.hash | to_hexa }} +
    • +
    • + Operation + {{ object.operation.name }} +
    • +
    • + Enabled + {{ object.enabled | display_boolean }} +
    • + {% if object.scopes.all %}
    • - Enabled - {{ object.enabled | display_boolean }} -
    • - {% if object.scopes.all %} -
    • - Restricted scopes - + Restricted scopes + {% for scope in object.scopes.distinct %} {{ scope }} {% endfor %} -
    • - {% endif %} - {% if object.get_unused_fields %} -
    • +
    • + {% endif %} + {% if object.get_unused_fields %} +
    • Unused required fields - + {% for field in object.get_unused_fields %} {{ field }} {% endfor %} -
    • - {% endif %} - -
    -
    -
    -
    -
    -
    -
    - -
    -
    -
    -
    -
    - -
    -

    Drag and drop steps to reorganize the order

    -
    +
+
+
+
+
+
+
+ +
+
+
+
+
- - Add a step - +
+

Drag and drop steps to reorganize the order

-
- {% for approval_step in object.approval_steps.all|dictsort:"position" %} -
-
-

{{ approval_step.name }}

- + + Add a step + +
+ +
+ {% for approval_step in object.approval_steps.all|dictsort:"position" %} +
+
+

{{ approval_step.name }}

+ -
-
-
Permission
-
{{ approval_step.permission }}
+
+
+
+
Permission
+
{{ approval_step.permission }}
-
Read survey fields
-
- {% for field in approval_step.readable_fields.all %} - {{ field.name }} - {% endfor %} -
+
Read survey fields
+
+ {% for field in approval_step.readable_fields.all %} + {{ field.name }} + {% endfor %} +
-
Write survey fields
-
- {% for field in approval_step.editable_fields.all %} - {{ field.name }} - {% endfor %} -
- {% if approval_step.auto_accept_condition %} -
Auto accept condition
-
{{ approval_step.auto_accept_condition }}
- {% endif %} -
-
+
Write survey fields
+
+ {% for field in approval_step.editable_fields.all %} + {{ field.name }} + {% endfor %} +
+ {% if approval_step.auto_accept_condition %} +
Auto accept condition
+
{{ approval_step.auto_accept_condition }}
+ {% endif %} +
- {% endfor %} -
+
+ {% empty %} +
+ No steps in the workflow +
+ {% endfor %}
-
-
- Requests currently using workflow -
- {% render_table request_table %} +
+
+
+ Requests currently using workflow
+ {% render_table request_table %} +
+
+
+ Preview your workflow on your Organization/Team +
+ {% render_table scope_table %}
-
-
-
+ +
+
+
{% endblock %} {% block custom_script %} diff --git a/templates/service_catalog/approvalworkflow_preview.html b/templates/service_catalog/approvalworkflow_preview.html new file mode 100644 index 000000000..a63ca4611 --- /dev/null +++ b/templates/service_catalog/approvalworkflow_preview.html @@ -0,0 +1,129 @@ +{% extends 'base.html' %} +{% block title %} + #{{ object.id }} | Approval Workflow for {{ scope }} +{% endblock %} +{% load render_table from django_tables2 %} +{% load static %} + +{% block main %} +
+
+
+
+
+ {% if current_workflow.id != object.id %} +
+ {% if current_workflow %} + {{ scope }} doesn't use this + workflow. {{ scope }} use {{ current_workflow }} to + handle {{ object.operation }}. + {% else %} + {{ scope }} doesn't use workflow for {{ object.operation }} + {% endif %} +
+ + {% endif %} +
+ Example of workflow {{ object }} when requesting {{ object.operation }} for + {{ scope }}.
+ +
+ +
+ {% for approval_step in object.approval_steps.all %} +
+
+

{{ approval_step.name }}

+
+
+
+
Permission
+
+ {{ approval_step.permission.permission_str }} +
+
Related roles
+
+ {% for role in approval_step.permission.role_set.all %} + {{ role.name }}
+ {% empty %} + No related roles, create a role first. + {% endfor %} +
+
Read survey fields
+
+ {% for field in approval_step.readable_fields.all %} + {{ field.name }} + {% endfor %} +
+
Write survey fields
+
+ {% for field in approval_step.editable_fields.all %} + {{ field.name }} + {% endfor %} +
+ {% if approval_step.auto_accept_condition %} +
Auto accept condition
+
{{ approval_step.auto_accept_condition }}
+ {% endif %} +
Approvers
+ +
+ {% who_can_approve_on_scope approval_step scope as list_who_can_approve %} + {% if list_who_can_approve.all %} + {{ list_who_can_approve|join:", " }} + {% else %} + No + approvers + {% endif %} +
+
+
+ +
+ {% empty %} +
+ No steps in the workflow +
+ + {% endfor %} +
+
+
+
+
+
+{% endblock %} +{% block custom_script %} + + +{% endblock %} diff --git a/templates/service_catalog/custom_columns/doc_actions.html b/templates/service_catalog/custom_columns/doc_actions.html deleted file mode 100644 index 5afac3ee4..000000000 --- a/templates/service_catalog/custom_columns/doc_actions.html +++ /dev/null @@ -1,3 +0,0 @@ - - Open - \ No newline at end of file diff --git a/templates/service_catalog/custom_columns/doc_operations.html b/templates/service_catalog/custom_columns/doc_operations.html deleted file mode 100644 index a9c7496e8..000000000 --- a/templates/service_catalog/custom_columns/doc_operations.html +++ /dev/null @@ -1,3 +0,0 @@ -{% for operation in record.operations.all %} - {{ operation.name }} ({{ operation.service.name }}) -{% endfor %} diff --git a/templates/service_catalog/custom_columns/doc_services.html b/templates/service_catalog/custom_columns/doc_services.html deleted file mode 100644 index d8c32b419..000000000 --- a/templates/service_catalog/custom_columns/doc_services.html +++ /dev/null @@ -1,3 +0,0 @@ -{% for service in record.services.all %} - {{ service.name }} -{% endfor %} \ No newline at end of file diff --git a/templates/service_catalog/custom_columns/preview_workflow.html b/templates/service_catalog/custom_columns/preview_workflow.html new file mode 100644 index 000000000..af21ef15c --- /dev/null +++ b/templates/service_catalog/custom_columns/preview_workflow.html @@ -0,0 +1,3 @@ + + Preview + diff --git a/templates/service_catalog/custom_columns/service_operations.html b/templates/service_catalog/custom_columns/service_operations.html index 1a7c35038..b2181ecb0 100644 --- a/templates/service_catalog/custom_columns/service_operations.html +++ b/templates/service_catalog/custom_columns/service_operations.html @@ -1,3 +1,3 @@ - + {{ record.operations.count }} \ No newline at end of file diff --git a/templates/service_catalog/instance_detail.html b/templates/service_catalog/instance_detail.html index 5a827f82f..371573e1c 100644 --- a/templates/service_catalog/instance_detail.html +++ b/templates/service_catalog/instance_detail.html @@ -35,6 +35,11 @@

{{ object.name }}

alt="Service image">
    +
  • + ID + {{ object.id }} +
  • +
  • Service {% has_perm request.user "service_catalog.change_service" object.service as can_edit_service %} diff --git a/templates/service_catalog/operation_detail.html b/templates/service_catalog/operation_detail.html index 7c21b0a35..b09f108da 100644 --- a/templates/service_catalog/operation_detail.html +++ b/templates/service_catalog/operation_detail.html @@ -42,6 +42,23 @@

    Details

    {{ object.enabled | display_boolean }}
+
  • + Docs + {% for doc in object.docs.all %} + + {{ doc.title }} +
    + {% endfor %} +
  • +
  • + Validators + {% for validator in object.validators_name %} + + {{ validator }} +
    + {% endfor %} +
  • diff --git a/templates/service_catalog/service_detail.html b/templates/service_catalog/service_detail.html index 127addd85..f89a55054 100644 --- a/templates/service_catalog/service_detail.html +++ b/templates/service_catalog/service_detail.html @@ -26,6 +26,7 @@

    {{ object.name }}

    Service image
    @@ -55,6 +56,15 @@

    {{ object.name }}

    {% endfor %} +
  • + Docs + {% for doc in object.docs.all %} + + {{ doc.title }} +
    + {% endfor %} +
  • diff --git a/tests/test_plugins/test_provided_validators/__init__.py b/tests/test_plugins/test_provided_validators/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_plugins/test_provided_validators/test_is_json.py b/tests/test_plugins/test_provided_validators/test_is_json.py new file mode 100644 index 000000000..b9533c3da --- /dev/null +++ b/tests/test_plugins/test_provided_validators/test_is_json.py @@ -0,0 +1,73 @@ +from django.core.exceptions import ValidationError +from django.test import TestCase, override_settings +from rest_framework import serializers +from Squest.utils.plugin_controller import PluginController + + +class TestSSHPublicKeyValidator(TestCase): + def test_with_text(self): + json_text = "aaaabbbbcccc" + loaded_module = PluginController.get_ui_field_validator_def("is_json") + with self.assertRaises(ValidationError): + loaded_module(json_text) + loaded_module = PluginController.get_api_field_validator_def("is_json") + with self.assertRaises(serializers.ValidationError): + loaded_module(json_text) + + def test_with_empty_text(self): + json_text = "" + loaded_module = PluginController.get_ui_field_validator_def("is_json") + with self.assertRaises(ValidationError): + loaded_module(json_text) + loaded_module = PluginController.get_api_field_validator_def("is_json") + with self.assertRaises(serializers.ValidationError): + loaded_module(json_text) + + def test_with_double_json(self): + json_text = '{"my_json": "is_ok", other_json: {"in_side": 13}}{"outside": "is_not_ok"}' + loaded_module = PluginController.get_ui_field_validator_def("is_json") + with self.assertRaises(ValidationError): + loaded_module(json_text) + loaded_module = PluginController.get_api_field_validator_def("is_json") + with self.assertRaises(serializers.ValidationError): + loaded_module(json_text) + + def test_with_single_quote(self): + json_text = "{'my_json': 'is_ok', other_json: {'in_side': 13}}" + loaded_module = PluginController.get_ui_field_validator_def("is_json") + with self.assertRaises(ValidationError): + loaded_module(json_text) + loaded_module = PluginController.get_api_field_validator_def("is_json") + with self.assertRaises(serializers.ValidationError): + loaded_module(json_text) + + def test_with_missing_quote(self): + json_text = '{"my_json": "is_ok", other_json: {"in_side": 13}}' + loaded_module = PluginController.get_ui_field_validator_def("is_json") + with self.assertRaises(ValidationError): + loaded_module(json_text) + loaded_module = PluginController.get_api_field_validator_def("is_json") + with self.assertRaises(serializers.ValidationError): + loaded_module(json_text) + + def test_with_additional_comma(self): + json_text = '{"my_json": "is_ok", "other_json": {"in_side": 13},}' + loaded_module = PluginController.get_ui_field_validator_def("is_json") + with self.assertRaises(ValidationError): + loaded_module(json_text) + loaded_module = PluginController.get_api_field_validator_def("is_json") + with self.assertRaises(serializers.ValidationError): + loaded_module(json_text) + + def test_with_json(self): + json_text = '{"my_json": "is_ok", "other_json": {"in_side": 13}}' + loaded_module = PluginController.get_api_field_validator_def("is_json") + try: + loaded_module(json_text) + except ValidationError: + self.fail("UI validator fail") + loaded_module = PluginController.get_api_field_validator_def("is_json") + try: + loaded_module(json_text) + except serializers.ValidationError: + self.fail("API validator fail") diff --git a/tests/test_plugins/test_provided_validators/test_is_public_ssh_key.py b/tests/test_plugins/test_provided_validators/test_is_public_ssh_key.py new file mode 100644 index 000000000..825f93a0e --- /dev/null +++ b/tests/test_plugins/test_provided_validators/test_is_public_ssh_key.py @@ -0,0 +1,43 @@ +from django.core.exceptions import ValidationError +from django.test import TestCase, override_settings +from rest_framework import serializers +from Squest.utils.plugin_controller import PluginController + + +class TestSSHPublicKeyValidator(TestCase): + def test_with_wrong_ssh_key(self): + public_ssh_key = "aaaa bbbb cccc" + loaded_module = PluginController.get_ui_field_validator_def("is_public_ssh_key") + with self.assertRaises(ValidationError): + loaded_module(public_ssh_key) + loaded_module = PluginController.get_api_field_validator_def("is_public_ssh_key") + with self.assertRaises(serializers.ValidationError): + loaded_module(public_ssh_key) + + def test_with_rsa(self): + public_ssh_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDeJvMJmt63nSi7sO6tKGHxnmvOckWaypcZKaPl5KLDCL5RsiaZMi/NrniiLsgv8FX5txQmsU2e1W7Ozh6VBGVErwV7S/GCkcYqO91lhZ+jVA7QHsK7hQWubq5zJzBm+p2HFSQk3ch0tXxhMbGivL1AkqziABgKLzhFeLIWzG3OCy8kj/F9b8doRiQ2FkMCT3sCGEts7nXQJ/0WyMj0FwyNr/R93+P/57M2QG49Wr7iFInzr6+BDcClmGMNHXNepvLQFHuW4suVU3Q7UaHFp9b1kDGbcbiw/w4JEutxuKv+DLOfm4pd8YhQsB8begILTwi5PJpvqfCSuE1jMN054t7xVssJU1Po5jMcSWfN8Q+/l7SSFgkCRH4Ul7LF+bMM8z3FejMp96CFmFagIePzqpnwjy4NLSEeESQhVCosPXpwCuwvZuebEgeptV8mK3TnVUksZmwnQrqwDqa6s9nZCjf/UZ03b73eFPcRwO0dgC2aWXvjzCB9SimdIBaMYMjtnuZJxADrb1AG9MTaBIj4bvn3phsx8OyZpc0c7mdyp1Quh5jBOwNJX7wU716o5lu5cMHTE29VFMIGz3yT+/ETxpC3/9CWuKLQoJFcV9PNzBScIIB2m7A+zkaEFbCDoCJqX8R5fs6Vsnq3vyEDu2VCGw3NUW+lQ+Di4y4+pAkhTaI4Mw== squest@fake" + loaded_module = PluginController.get_api_field_validator_def("is_public_ssh_key") + try: + loaded_module(public_ssh_key) + except ValidationError: + self.fail("UI validator fail") + loaded_module = PluginController.get_api_field_validator_def("is_public_ssh_key") + try: + loaded_module(public_ssh_key) + except serializers.ValidationError: + self.fail("API validator fail") + + def test_with_ed25519(self): + public_ssh_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPYqhDwOvBaWrGC257fdxfy1iMM6ZY2VwgmP+XlRRMT8 squest@fake" + loaded_module = PluginController.get_api_field_validator_def("is_public_ssh_key") + try: + loaded_module(public_ssh_key) + except ValidationError: + self.fail("UI validator fail") + loaded_module = PluginController.get_api_field_validator_def("is_public_ssh_key") + try: + loaded_module(public_ssh_key) + except serializers.ValidationError: + self.fail("API validator fail") + + diff --git a/tests/test_profiles/test_model/test_scope_get_workflow.py b/tests/test_profiles/test_model/test_scope_get_workflow.py new file mode 100644 index 000000000..e19702db6 --- /dev/null +++ b/tests/test_profiles/test_model/test_scope_get_workflow.py @@ -0,0 +1,49 @@ +from profiles.models import Scope +from service_catalog.models import ApprovalWorkflow, Request +from tests.setup import SetupRequest +from tests.utils import TransactionTestUtils + +from django.db.models.query import QuerySet + + +class TestApprovalWorkflowReset(TransactionTestUtils, SetupRequest): + + def setUp(self): + SetupRequest.setUp(self) + + def _create_approval(self, operation, scopes=None): + if isinstance(scopes, Scope): + scopes = [scopes] + aw = ApprovalWorkflow.objects.create( + name=f"AW - {operation} - {scopes}", + operation=operation, + enabled=True + ) + if scopes: + aw.scopes.set(scopes) + return aw + + def test_get_workflows(self): + # team1org2 -> no workflows + self.assertQuerysetEqualID(self.team1org2.get_workflows(), ApprovalWorkflow.objects.none()) + + # team1org2 -> workflow for all on operation_create1 + aw_for_all = self._create_approval(self.operation_create_1) + self.assertQuerysetEqualID(self.team1org2.get_workflows(), ApprovalWorkflow.objects.filter(id=aw_for_all.id)) + + # team1org2 -> workflow for org2 on operation_create1 ("All" overridden by org2) + aw_for_org2 = self._create_approval(self.operation_create_1, self.org2) + self.assertQuerysetEqualID(self.team1org2.get_workflows(), ApprovalWorkflow.objects.filter(id=aw_for_org2.id)) + + # team1org2 -> workflow for team1org2 on operation_create1 ( org2 overridden by team1org2) + aw_for_org2_team1 = self._create_approval(self.operation_create_1, self.team1org2) + self.assertQuerysetEqualID( + self.team1org2.get_workflows(), + ApprovalWorkflow.objects.filter(id=aw_for_org2_team1.id) + ) + + # team1org2 -> workflow for team1org2 on operation_create1 ( org2 overridden by team1org2) + # team1org2 -> + workflow for all on operation_update1 + aw_for_all_update1 = self._create_approval(self.operation_update_1) + self.assertQuerysetEqualID(self.team1org2.get_workflows(), ApprovalWorkflow.objects.filter( + id__in=[aw_for_org2_team1.id, aw_for_all_update1.id])) diff --git a/tests/test_service_catalog/test_api/test_serializers/test_approval_workflow_serializer.py b/tests/test_service_catalog/test_api/test_serializers/test_approval_workflow_serializer.py index 6a2003498..30530dabd 100644 --- a/tests/test_service_catalog/test_api/test_serializers/test_approval_workflow_serializer.py +++ b/tests/test_service_catalog/test_api/test_serializers/test_approval_workflow_serializer.py @@ -30,6 +30,21 @@ def test_can_add_another_scope_to_scopes(self): serializer = ApprovalWorkflowSerializerEdit(instance=self.test_approval_workflow, data=data) self.assertTrue(serializer.is_valid()) + def test_can_add_another_scope_to_scopes_when_edit(self): + existing_approval = ApprovalWorkflow.objects.create(name="test", + operation=self.create_operation_test, + enabled=True) + + existing_approval.scopes.set([self.test_quota_scope]) + data = { + "name": "test_workflow", + "operation": self.create_operation_test.id, + "scopes": [self.test_quota_scope.id] + } + serializer = ApprovalWorkflowSerializerEdit(instance=self.test_approval_workflow, data=data) + self.assertFalse(serializer.is_valid()) + self.assertIn("has already an approval workflow based on this operation", serializer.errors["scopes"][0]) + def test_cannot_valid_with_empty_scopes_if_already_exists(self): existing_approval = ApprovalWorkflow.objects.create(name="test", operation=self.create_operation_test, diff --git a/tests/test_service_catalog/test_forms/test_approval_workflow_form.py b/tests/test_service_catalog/test_forms/test_approval_workflow_form.py index 375b6c4ea..1f930ea4a 100644 --- a/tests/test_service_catalog/test_forms/test_approval_workflow_form.py +++ b/tests/test_service_catalog/test_forms/test_approval_workflow_form.py @@ -32,6 +32,24 @@ def test_clean(self): self.assertFalse(form.is_valid()) self.assertIn("has already an approval workflow", form["scopes"].errors[0]) + def test_clean_edit(self): + existing_approval = ApprovalWorkflow.objects.create(name="test", + operation=self.create_operation_test, + enabled=True) + existing_approval.scopes.set([self.test_quota_scope]) + + new_approval = ApprovalWorkflow.objects.create(name="test2", + operation=self.create_operation_test, + enabled=True) + data = { + 'name': 'test_approval_workflow', + 'operation': self.create_operation_test, + 'scopes': [self.test_quota_scope] + } + form = ApprovalWorkflowFormEdit(data, instance=new_approval) + self.assertFalse(form.is_valid()) + self.assertIn("has already an approval workflow", form["scopes"].errors[0]) + def test_add_scope_to_scopes(self): existing_approval = ApprovalWorkflow.objects.create(name="test", operation=self.create_operation_test, @@ -46,6 +64,24 @@ def test_add_scope_to_scopes(self): form = ApprovalWorkflowFormEdit(instance=existing_approval, data=data) self.assertTrue(form.is_valid()) + def test_clean_edit_with_empty_scopes(self): + existing_approval = ApprovalWorkflow.objects.create(name="test", + operation=self.create_operation_test, + enabled=True) + + new_approval = ApprovalWorkflow.objects.create(name="test2", + operation=self.create_operation_test, + enabled=True) + new_approval.scopes.set([self.test_quota_scope]) + + data = { + 'name': 'test_approval_workflow', + 'operation': self.create_operation_test + } + form = ApprovalWorkflowFormEdit(data, instance=new_approval) + self.assertFalse(form.is_valid()) + self.assertIn("An approval workflow for all scopes already exists", form["scopes"].errors[0]) + def test_clean_with_empty_scopes(self): existing_approval = ApprovalWorkflow.objects.create(name="test", operation=self.create_operation_test, diff --git a/tests/test_service_catalog/test_urls/test_approvalworkflow.py b/tests/test_service_catalog/test_urls/test_approvalworkflow.py index 306e6c857..54493f532 100644 --- a/tests/test_service_catalog/test_urls/test_approvalworkflow.py +++ b/tests/test_service_catalog/test_urls/test_approvalworkflow.py @@ -41,6 +41,11 @@ def test_approvalworkflow_views(self): perm_str='service_catalog.change_approvalworkflow', url_kwargs={'pk': self.approval_workflow.id} ), + TestingGetContextView( + url='service_catalog:approvalworkflow_preview', + perm_str='service_catalog.view_approvalworkflow', + url_kwargs={'pk': self.approval_workflow.id, 'scope_id': self.test_quota_scope.id} + ), TestingPostContextView( url='service_catalog:approvalworkflow_edit', perm_str='service_catalog.change_approvalworkflow', diff --git a/tests/test_service_catalog/test_views/test_common/test_doc_view.py b/tests/test_service_catalog/test_views/test_common/test_doc_view.py index 23dfa0f9a..08e8d6cab 100644 --- a/tests/test_service_catalog/test_views/test_common/test_doc_view.py +++ b/tests/test_service_catalog/test_views/test_common/test_doc_view.py @@ -11,7 +11,10 @@ def setUp(self): self.client.login(username=self.standard_user, password=self.common_password) self.new_doc = Doc.objects.create(title="test_doc", content="# tittle 1") - self.new_doc.services.add(self.service_test) + self.new_doc_on_service = Doc.objects.create(title="test_doc_service", content="Service") + self.new_doc_on_service.services.add(self.service_test) + self.new_doc_on_operation = Doc.objects.create(title="test_doc_operation", content="Operation") + self.new_doc_on_operation.operations.add(self.create_operation_test_2) def _test_can_list_doc(self): url = reverse('service_catalog:doc_list')