diff --git a/Squest/settings.py b/Squest/settings.py index 39f5b823e..95b88957e 100644 --- a/Squest/settings.py +++ b/Squest/settings.py @@ -50,7 +50,7 @@ DATE_FORMAT = os.environ.get('DATE_FORMAT', "%d %b, %Y %H:%M") LOGIN_HELPER_TEXT = os.environ.get('LOGIN_HELPER_TEXT', None) IS_DEV_SERVER = str_to_bool(os.environ.get('IS_DEV_SERVER', False)) -SQL_DEBUG = str_to_bool(os.environ.get('SQL_DEBUG', False)) +SQL_DEBUG = str_to_bool(os.environ.get('SQL_DEBUG', True)) # ------------------------------- # SQUEST CONFIG # ------------------------------- diff --git a/Squest/utils/squest_views.py b/Squest/utils/squest_views.py index c59d17fe8..50e1182a0 100644 --- a/Squest/utils/squest_views.py +++ b/Squest/utils/squest_views.py @@ -5,11 +5,14 @@ from django.contrib.contenttypes.models import ContentType from django.core.exceptions import PermissionDenied from django.db.models import ProtectedError +from django.forms import Form +from django.http import HttpResponseRedirect from django.urls import reverse_lazy, NoReverseMatch, reverse from django.utils.safestring import mark_safe from django.views import View from django.views.generic import CreateView, UpdateView, DeleteView, DetailView, FormView -from django.views.generic.detail import SingleObjectMixin +from django.views.generic.detail import SingleObjectMixin, SingleObjectTemplateResponseMixin +from django.views.generic.edit import ModelFormMixin, ProcessFormView from django_filters.views import FilterView from django_tables2 import SingleTableMixin from django_tables2.export import ExportMixin @@ -39,7 +42,7 @@ def get_generic_url(self, action): except AttributeError: try: return reverse(f'{self.django_content_type.app_label}:{self.django_content_type.model}_{action}', - kwargs=self.get_generic_url_kwargs()) + kwargs=self.get_generic_url_kwargs()) except NoReverseMatch: return '#' @@ -140,6 +143,8 @@ def get_context_data(self, **kwargs): return context + + class SquestDeleteView(LoginRequiredMixin, SquestPermissionRequiredMixin, SquestView, DeleteView): template_name = 'generics/confirm-delete-template.html' context_object_name = "object" @@ -215,7 +220,7 @@ def get_context_data(self, **kwargs): class SquestFormView(LoginRequiredMixin, SquestPermissionRequiredMixin, SingleObjectMixin, SquestView, FormView): - template_name = 'generics/generic_form.html' + template_name = 'generics/confirm-delete-template.html' context_object_name = "object" def form_valid(self, form): @@ -244,3 +249,39 @@ def get_context_data(self, **kwargs): ] context['action'] = "edit" return context + + +class SquestConfirmView(LoginRequiredMixin, SquestPermissionRequiredMixin, SingleObjectMixin, SquestView, FormView): + template_name = 'generics/confirm.html' + context_object_name = "object" + + def form_valid(self, form): + return super().form_valid(form) + + def get_success_url(self): + return self.get_object().get_absolute_url() + def get_permission_required(self): + pass + + def get_context_data(self, **kwargs): + self.object = self.get_object() + context = super().get_context_data(**kwargs) + try: + object_url = self.object.get_absolute_url() + except AttributeError: + object_url = "" + context["bootstrap_type"] = "warning" + context['breadcrumbs'] = [ + { + 'text': self.django_content_type.name.capitalize(), + 'url': self.get_generic_url('list') + }, + { + 'text': self.object, + 'url': object_url + }, + + ] + + context['action'] = 'apply' + return context diff --git a/profiles/api/serializers/user_serializers.py b/profiles/api/serializers/user_serializers.py index 944e89f91..db36aad1e 100644 --- a/profiles/api/serializers/user_serializers.py +++ b/profiles/api/serializers/user_serializers.py @@ -15,11 +15,10 @@ class Meta: class UserSerializer(ModelSerializer): class Meta: model = User - fields = ['id', 'username', 'password', 'email', 'profile', 'first_name', 'last_name', 'is_staff', - 'is_superuser', 'is_active', 'groups'] + fields = ['id', 'username', 'password', 'email', 'first_name', 'last_name', 'is_staff', + 'is_superuser', 'is_active'] read_only_fields = ['groups', ] - profile = ProfileSerializer(required=False) password = CharField( write_only=True, required=True, diff --git a/service_catalog/api/views/request_api_views.py b/service_catalog/api/views/request_api_views.py index 9619a9056..708529be3 100644 --- a/service_catalog/api/views/request_api_views.py +++ b/service_catalog/api/views/request_api_views.py @@ -17,7 +17,12 @@ class RequestList(SquestListAPIView): filterset_class = RequestFilter def get_queryset(self): - return Request.get_queryset_for_user(self.request.user, 'service_catalog.view_request') + return Request.get_queryset_for_user(self.request.user, 'service_catalog.view_request').prefetch_related( + "user", "operation", "instance__requester", "instance__quota_scope", "instance__service", + "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" + ) def get_serializer_class(self): if self.request.user.has_perm("service_catalog.view_admin_survey"): diff --git a/service_catalog/migrations/0037_remove_approvalstep_next_approvalworkflow_version_and_more.py b/service_catalog/migrations/0037_remove_approvalstep_next_approvalworkflow_version_and_more.py new file mode 100644 index 000000000..b23f01776 --- /dev/null +++ b/service_catalog/migrations/0037_remove_approvalstep_next_approvalworkflow_version_and_more.py @@ -0,0 +1,27 @@ +# Generated by Django 4.2.6 on 2023-11-28 11:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('service_catalog', '0036_approvalworkflow_enabled'), + ] + + operations = [ + migrations.RemoveField( + model_name='approvalstep', + name='next', + ), + migrations.AddField( + model_name='approvalworkflow', + name='version', + field=models.PositiveIntegerField(default=0, help_text='Current version'), + ), + migrations.AddField( + model_name='approvalworkflowstate', + name='version', + field=models.PositiveIntegerField(default=1, help_text='ApprovalWorkflow version when instantiated'), + ), + ] diff --git a/service_catalog/migrations/0038_alter_request_options.py b/service_catalog/migrations/0038_alter_request_options.py new file mode 100644 index 000000000..0ab8a6ac2 --- /dev/null +++ b/service_catalog/migrations/0038_alter_request_options.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.6 on 2023-12-01 10:27 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('service_catalog', '0037_remove_approvalstep_next_approvalworkflow_version_and_more'), + ] + + operations = [ + migrations.AlterModelOptions( + name='request', + options={'default_permissions': ('add', 'change', 'delete', 'view', 'list'), 'ordering': ['-last_updated'], 'permissions': [('accept_request', 'Can accept request'), ('cancel_request', 'Can cancel request'), ('reject_request', 'Can reject request'), ('reset_request', 'Can reset request'), ('archive_request', 'Can archive request'), ('unarchive_request', 'Can unarchive request'), ('re_submit_request', 'Can re-submit request'), ('process_request', 'Can process request'), ('need_info_request', 'Can ask info request'), ('view_admin_survey', 'Can view admin survey'), ('list_approvers', 'Can view who can accept')]}, + ), + ] diff --git a/service_catalog/models/approval_step.py b/service_catalog/models/approval_step.py index 297f039e9..37481b6fb 100644 --- a/service_catalog/models/approval_step.py +++ b/service_catalog/models/approval_step.py @@ -1,5 +1,6 @@ from django.db.models import ForeignKey, CharField, SET_NULL, CASCADE, IntegerField, ManyToManyField, PROTECT, TextField -from django.db.models.signals import post_save +from django.db.models.signals import post_save, post_delete +from django.dispatch import receiver from django.urls import reverse from Squest.utils.squest_model import SquestModel @@ -22,15 +23,6 @@ class Meta(SquestModel.Meta): name = CharField(max_length=100, blank=False) position = IntegerField(null=True, blank=True, default=0) - next = ForeignKey( - "service_catalog.ApprovalStep", - blank=True, - null=True, - default=None, - on_delete=SET_NULL, - related_name="previous" - ) - permission = ForeignKey( Permission, on_delete=PROTECT, @@ -68,8 +60,7 @@ def on_create_set_position(cls, sender, instance, created, *args, **kwargs): previous_steps = ApprovalStep.objects.filter( approval_workflow=instance.approval_workflow).order_by('position').exclude(id=instance.id) if previous_steps.exists(): - instance.position = previous_steps.last().position + 1 - instance.save() + ApprovalStep.objects.filter(id=instance.id).update(position=previous_steps.last().position + 1) def save(self, *args, **kwargs): # set the default permission @@ -88,3 +79,23 @@ def who_can_approve(self, scope): post_save.connect(ApprovalStep.on_create_set_position, sender=ApprovalStep) + + +@receiver(post_save, sender=ApprovalStep) +def increment_version(sender, instance, created, **kwargs): + from service_catalog.models import ApprovalWorkflow + ApprovalWorkflow.objects.filter( + id=instance.approval_workflow.id + ).update( + version=instance.approval_workflow.version + 1 + ) + + +@receiver(post_delete, sender=ApprovalStep) +def increment_version_post_delete(sender, instance, *args, **kwargs): + from service_catalog.models import ApprovalWorkflow + ApprovalWorkflow.objects.filter( + id=instance.approval_workflow.id + ).update( + version=instance.approval_workflow.version + 1 + ) diff --git a/service_catalog/models/approval_workflow.py b/service_catalog/models/approval_workflow.py index 96060c7f2..85867db6c 100644 --- a/service_catalog/models/approval_workflow.py +++ b/service_catalog/models/approval_workflow.py @@ -1,4 +1,6 @@ -from django.db.models import CharField, ForeignKey, ManyToManyField, CASCADE, BooleanField +from django.db.models import CharField, ForeignKey, ManyToManyField, CASCADE, BooleanField, PositiveIntegerField +from django.db.models.signals import post_save +from django.dispatch import receiver from Squest.utils.squest_model import SquestModel from profiles.models import AbstractScope, GlobalScope, Scope @@ -25,6 +27,8 @@ class ApprovalWorkflow(SquestModel): help_text="This workflow will be triggered for the following scopes. Leave empty to trigger for all scopes" ) + version = PositiveIntegerField(help_text="Current version", default=0) + @property def get_unused_fields(self): all_fields = set(TowerSurveyField.objects.filter(operation=self.operation).values_list('name', flat=True)) @@ -33,6 +37,9 @@ def get_unused_fields(self): flat=True)) return all_fields - used_fields + + + @property def first_step(self): first_step = ApprovalStep.objects.filter(approval_workflow=self, position=0) @@ -46,13 +53,13 @@ def __str__(self): def instantiate(self): from service_catalog.models import ApprovalStepState from service_catalog.models import ApprovalWorkflowState - new_approval_workflow_state = ApprovalWorkflowState.objects.create(approval_workflow=self) + new_approval_workflow_state = ApprovalWorkflowState.objects.create(approval_workflow=self, version=self.version) for approval_step in self.approval_steps.all(): - new_app_workflow_state = ApprovalStepState.objects.create( + current_step_state = ApprovalStepState.objects.create( approval_workflow_state=new_approval_workflow_state, approval_step=approval_step) if approval_step.position == 0: - new_approval_workflow_state.current_step = new_app_workflow_state + new_approval_workflow_state.current_step = current_step_state new_approval_workflow_state.save() return new_approval_workflow_state @@ -63,7 +70,7 @@ def _get_all_requests_that_should_use_workflow(self): ).exclude( approval_workflows__id=self.id ) - expanded_scopes = self.scopes.expand().exclude(id__in=scopes_already_assigned_to_another_workflow.values("id") ) + expanded_scopes = self.scopes.expand().exclude(id__in=scopes_already_assigned_to_another_workflow.values("id")) if self.scopes.exists(): return Request.objects.filter( @@ -82,8 +89,13 @@ def _get_all_requests_that_should_use_workflow(self): def _get_request_using_workflow(self): return Request.objects.filter(approval_workflow_state__approval_workflow=self, state=RequestState.SUBMITTED) + def _get_request_using_workflow_with_wrong_version(self): + return self._get_request_using_workflow().exclude(approval_workflow_state__version=self.version) + def _get_request_using_workflow_with_good_version(self): + return self._get_request_using_workflow().filter(approval_workflow_state__version=self.version) def _get_request_to_reset(self): - return self._get_all_requests_that_should_use_workflow() | self._get_request_using_workflow() + # return self._get_all_requests_that_should_use_workflow() | self._get_request_using_workflow() + return self._get_all_requests_that_should_use_workflow().exclude(id__in=self._get_request_using_workflow_with_good_version().values_list("id",flat=True)) | self._get_request_using_workflow_with_wrong_version() def reset_all_approval_workflow_state(self): for request in self._get_request_to_reset().distinct(): @@ -98,3 +110,8 @@ def get_scopes(self): return scopes else: return GlobalScope.load().get_scopes() + + +@receiver(post_save, sender=ApprovalWorkflow) +def increment_version(sender, instance, created, **kwargs): + ApprovalWorkflow.objects.filter(id=instance.id).update(version=instance.version + 1) diff --git a/service_catalog/models/approval_workflow_state.py b/service_catalog/models/approval_workflow_state.py index 347f7950c..45c08bd91 100644 --- a/service_catalog/models/approval_workflow_state.py +++ b/service_catalog/models/approval_workflow_state.py @@ -1,7 +1,7 @@ from datetime import datetime from django.contrib.auth.models import User -from django.db.models import ForeignKey, CASCADE +from django.db.models import ForeignKey, CASCADE, PositiveIntegerField from Squest.utils.squest_model import SquestModel from service_catalog.models import ApprovalState @@ -26,11 +26,14 @@ class ApprovalWorkflowState(SquestModel): related_query_name='current_approval_workflow_state' ) + version = PositiveIntegerField(help_text="ApprovalWorkflow version when instantiated", default=1) + def get_scopes(self): return self.request.get_scopes() def get_next_step(self): - next_step = self.approval_step_states.filter(approval_step__position=self.current_step.approval_step.position + 1) + next_step = self.approval_step_states.filter( + approval_step__position__gte=self.current_step.approval_step.position + 1) if next_step.exists(): return next_step.first() return None diff --git a/service_catalog/models/request.py b/service_catalog/models/request.py index 4b2ba1aef..3be661ec1 100644 --- a/service_catalog/models/request.py +++ b/service_catalog/models/request.py @@ -33,6 +33,7 @@ class Meta: ("accept_request", "Can accept request"), ("cancel_request", "Can cancel request"), ("reject_request", "Can reject request"), + ("reset_request", "Can reset request"), ("archive_request", "Can archive request"), ("unarchive_request", "Can unarchive request"), ("re_submit_request", "Can re-submit request"), @@ -162,11 +163,16 @@ def tower_job_url(self): def need_info(self): pass - @transition(field=state, source=[RequestState.NEED_INFO, RequestState.REJECTED], target=RequestState.SUBMITTED) - def re_submit(self): - if self.approval_workflow_state is not None: - self.approval_workflow_state.current_step.reset_to_pending() - + @transition(field=state, source=[RequestState.NEED_INFO, RequestState.REJECTED, RequestState.SUBMITTED], + target=RequestState.SUBMITTED) + def re_submit(self, save=True): + if self.instance.state == InstanceState.ABORTED: + self.instance.state = InstanceState.PENDING + self.instance.save() + self.state = RequestState.SUBMITTED + self.setup_approval_workflow(save=True) + if save: + self.save() @transition(field=state, source=[RequestState.SUBMITTED, RequestState.NEED_INFO, RequestState.REJECTED, diff --git a/service_catalog/tables/request_tables.py b/service_catalog/tables/request_tables.py index 8f4353f9b..a0309cdad 100644 --- a/service_catalog/tables/request_tables.py +++ b/service_catalog/tables/request_tables.py @@ -45,6 +45,45 @@ def render_state(self, record, value): f'{value}') +class RequestTablesForApprovalWorkflow(SquestTable): + id = Column(linkify=True, verbose_name="Request") + 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') + instance__quota_scope__name = Column(verbose_name="Quota scope") + approval_workflow_state__version = Column(verbose_name="Approval workflow version") + approval_workflow_state__approval_workflow__name = Column(verbose_name="Approval workflow name") + + class Meta: + model = Request + attrs = {"id": "request_table", "class": "table squest-pagination-tables"} + fields = ( + "id", "approval_workflow_state__version", "approval_workflow_state__approval_workflow__name", + "user__username", + "instance__quota_scope__name", + "instance__service", "operation", "state", "instance", "date_submitted", + "last_updated") + + def render_id(self, value, record): + return format_html(f'{record}') + + def render_operation(self, value, record): + from service_catalog.views import map_operation_type + return format_html( + f'{value.name}') + + def render_state(self, record, value): + from service_catalog.views import map_request_state + if record.approval_workflow_state is not None and record.approval_workflow_state.current_step is not None: + position = record.approval_workflow_state.current_step.approval_step.position + number_of_steps = record.approval_workflow_state.approval_step_states.count() + return format_html( + f'{value} ({position}/{number_of_steps})') + + return format_html( + f'{value}') + + class RequestTableWaitingForActions(SquestTable): id = Column(linkify=True, verbose_name="Request", orderable=False) user__username = Column(orderable=False) @@ -53,6 +92,7 @@ class RequestTableWaitingForActions(SquestTable): review = Column(empty_values=(), orderable=False) operation = Column(orderable=False) state = Column(orderable=False) + class Meta: model = Request attrs = {"id": "request_table", "class": "table squest-pagination-tables"} diff --git a/service_catalog/urls.py b/service_catalog/urls.py index 567c4a06f..0b0bad97e 100644 --- a/service_catalog/urls.py +++ b/service_catalog/urls.py @@ -23,6 +23,7 @@ path('request//archive/', views.request_archive, name='request_archive'), path('request//unarchive/', views.request_unarchive, name='request_unarchive'), path('request//approve/', views.RequestApproveView.as_view(), name='request_approve'), + path('request//reset/', views.RequestResetView.as_view(), name='request_reset'), # Request bulk delete diff --git a/service_catalog/views/approval_workflow_views.py b/service_catalog/views/approval_workflow_views.py index 60192c0ba..e72d8e7fd 100644 --- a/service_catalog/views/approval_workflow_views.py +++ b/service_catalog/views/approval_workflow_views.py @@ -1,12 +1,13 @@ -from django.http import HttpResponseNotAllowed -from django.shortcuts import redirect +from django.forms import Form +from Squest.utils.squest_table import SquestRequestConfig from Squest.utils.squest_views import SquestListView, SquestDetailView, SquestCreateView, SquestUpdateView, \ - SquestDeleteView + SquestDeleteView, SquestConfirmView 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.tables.approval_workflow_table import ApprovalWorkflowTable +from service_catalog.tables.request_tables import RequestTable, RequestTablesForApprovalWorkflow class ApprovalWorkflowListView(SquestListView): @@ -18,6 +19,22 @@ class ApprovalWorkflowListView(SquestListView): class ApprovalWorkflowDetailView(SquestDetailView): model = ApprovalWorkflow + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + config = SquestRequestConfig(self.request) + + context["request_table"] = RequestTablesForApprovalWorkflow( + self.get_object()._get_request_using_workflow().prefetch_related( + "user", "operation", "instance__requester", "instance__quota_scope", "instance__service", + "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" + ), + hide_fields=( + "approval_workflow_state__approval_workflow__name",)) + config.configure(context['request_table']) + return context + class ApprovalWorkflowCreateView(SquestCreateView): model = ApprovalWorkflow @@ -33,14 +50,36 @@ class ApprovalWorkflowDeleteView(SquestDeleteView): model = ApprovalWorkflow -class ApprovalWorkflowResetRequests(SquestDetailView): +class ApprovalWorkflowResetRequests(SquestConfirmView): model = ApprovalWorkflow - permission_required = "service_catalog.change_approvalworkflow" + permission_required = "service_catalog.reset_request" + form_class = Form - def dispatch(self, request, *args, **kwargs): - if request.method != 'GET': - return HttpResponseNotAllowed(['GET']) - super(ApprovalWorkflowResetRequests, self).dispatch(request, *args, **kwargs) + def form_valid(self, form): workflow = self.get_object() workflow.reset_all_approval_workflow_state() - return redirect(workflow.get_absolute_url()) + + return super().form_valid(form) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + config = SquestRequestConfig(self.request) + + context['confirm_text'] = "Confirm reset of the following requests ?" + + context['button_text'] = 'Reset to submitted' + context['breadcrumbs'].append( + { + 'text': 'Reset to submitted', + 'url': '' + } + ) + context['detail_table'] = RequestTablesForApprovalWorkflow( + self.get_object()._get_request_to_reset().prefetch_related( + "user", "operation", "instance__requester", "instance__quota_scope", "instance__service", + "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" + )) + config.configure(context['detail_table']) + return context diff --git a/service_catalog/views/filters.py b/service_catalog/views/filters.py index 5f023a640..70da6124e 100644 --- a/service_catalog/views/filters.py +++ b/service_catalog/views/filters.py @@ -76,7 +76,9 @@ def can_proceed_request_action(args): return can_proceed(target_request.unarchive) return False - +@register.simple_tag() +def can_proceed_request_action_refacto(action, instance): + return can_proceed(instance.__getattribute__(action)) @register.filter(name='can_proceed_instance_action') def can_proceed_instance_action(args): target_action = args.split(',')[0] diff --git a/service_catalog/views/request.py b/service_catalog/views/request.py index ef2776aed..b9ffe71f3 100644 --- a/service_catalog/views/request.py +++ b/service_catalog/views/request.py @@ -32,7 +32,8 @@ def get_queryset(self): self.request.user, 'service_catalog.view_request' ).prefetch_related( "user", "operation", "instance__requester", "instance__quota_scope", "instance__service", - "operation__service", "approval_workflow_state", "approval_workflow_state__current_step", + "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" ) @@ -451,6 +452,34 @@ def request_bulk_delete(request): return redirect("service_catalog:request_list") +class RequestResetView(SquestConfirmView): + model = Request + form_class = Form + + def get_permission_required(self): + return "service_catalog.reset_request" + + def form_valid(self, form): + self.get_object().re_submit() + return super().form_valid(form) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + if self.get_object().approval_workflow_state: + context['confirm_text'] = mark_safe( + f"Confirm reset of {self.object} ? It will reset all steps") + else: + context['confirm_text'] = mark_safe(f"Confirm reset of {self.object} ?") + context['button_text'] = 'Reset to submitted' + context['breadcrumbs'].append( + { + 'text': 'Reset to submitted', + 'url': '' + } + ) + return context + + class RequestApproveView(SquestFormView): template_name = 'generics/generic_form.html' form_class = ApproveWorkflowStepForm diff --git a/templates/generics/confirm.html b/templates/generics/confirm.html new file mode 100644 index 000000000..d131893ef --- /dev/null +++ b/templates/generics/confirm.html @@ -0,0 +1,50 @@ +{% extends 'base.html' %} +{% load render_table from django_tables2 %} + +{% block main %} +
+
+ {% if error_message %} +
+
+

{{ error_message }}

+
+
+ {% else %} +
+
+

{{ confirm_text }}

+
+
+ {% if details.details_list %} +
+
Warning
+

{{ details.warning_sentence }}

+
    +
      {{ details.details_list | unordered_list }}
    +
+
+ {% endif %} + {% if detail_table %} + {% render_table detail_table %} + {% endif %} +
{% csrf_token %} + + +
+
+
+ {% endif %} +
+
+ {% load static %} + + + + +{% endblock %} diff --git a/templates/generics/table/table.html b/templates/generics/table/table.html index e27354d88..192e3d1d6 100644 --- a/templates/generics/table/table.html +++ b/templates/generics/table/table.html @@ -3,6 +3,10 @@ {{ block.super }}
- Showing {{ table.page.start_index }}-{{ table.page.end_index }} of {{ table.page.paginator.count }} + {% if table.page %} + Showing {{ table.page.start_index }}-{{ table.page.end_index }} of {{ table.page.paginator.count }} + {% else %} + Showing 1-{{ table.rows|length }} of {{ table.rows|length }} + {% endif %} {% endblock table %} diff --git a/templates/profiles/role_detail.html b/templates/profiles/role_detail.html index 0abe65692..05f3dfea0 100644 --- a/templates/profiles/role_detail.html +++ b/templates/profiles/role_detail.html @@ -13,6 +13,8 @@ {% has_perm request.user "profiles.view_rbac" as can_view_rbac %}
+ + {# Details left #}
@@ -29,6 +31,8 @@

{{ object.name }}

+ + {# Tabs right side #}
diff --git a/templates/service_catalog/approvalworkflow_detail.html b/templates/service_catalog/approvalworkflow_detail.html index 87d716bc7..c48af849e 100644 --- a/templates/service_catalog/approvalworkflow_detail.html +++ b/templates/service_catalog/approvalworkflow_detail.html @@ -7,8 +7,8 @@ {% block header_button %} Reset related submitted requests + > + Reset related submitted requests {% include 'generics/buttons/edit_button.html' %} @@ -31,6 +31,9 @@

{{ object.name }}

  • Name{{ object.name }}
  • +
  • + Version{{ object.version }} +
  • Operation {{ object.operation.name }} @@ -68,23 +71,33 @@

    {{ object.name }}

  • -
    -

    - Steps -

    - +
    +
    -
    -
    -
    -

    Drag and drop steps to reorganize the order

    +
    +
    +
    + +
    +

    Drag and drop steps to reorganize the order

    +
    + + + Add a step +
    +
    {% for approval_step in object.approval_steps.all|dictsort:"position" %} @@ -129,13 +142,21 @@

    {{ approval_step.name }}

    {% endfor %}
    - +
    +
    +
    + Requests currently using workflow +
    + {% render_table request_table %}
    -
    +
    {% endblock %} +{% block custom_script %} + +{% endblock %} diff --git a/templates/service_catalog/request_detail.html b/templates/service_catalog/request_detail.html index 23448c2ac..7f93932e5 100644 --- a/templates/service_catalog/request_detail.html +++ b/templates/service_catalog/request_detail.html @@ -5,6 +5,15 @@ {% load render_table from django_tables2 %} {% load static %} {% block header_button %} + {% has_perm request.user "service_catalog.reset_request" object as can_reset_request %} + {% can_proceed_request_action_refacto 're_submit' object as can_proceed_re_submit %} + {% if can_reset_request and can_proceed_re_submit %} + Reset to submitted + + + {% endif %} {% include 'service_catalog/buttons/request_state_machine.html' %} {% include 'generics/buttons/edit_button.html' %} {% include 'generics/buttons/delete_button.html' %} diff --git a/templates/service_catalog/request_details/approval.html b/templates/service_catalog/request_details/approval.html index e41c1942b..6bff6ce39 100644 --- a/templates/service_catalog/request_details/approval.html +++ b/templates/service_catalog/request_details/approval.html @@ -104,21 +104,8 @@

    {{ step.approval_step.name }}

    Rejected by {{ step.updated_by }} on {{ step.date_updated | squest_date_format }} -
    - {% with args_filter="re_submit,"|addstr:object.id %} - {% if args_filter|can_proceed_request_action and can_re_submit_request %} - - {% endif %} - {% endwith %} -
    {% else %} - {% if step.is_current_step_in_approval %} + {% if step.is_current_step_in_approval and object.get_state_display == "SUBMITTED" %}
    diff --git a/tests/test_service_catalog/test_models/test_approval_step.py b/tests/test_service_catalog/test_models/test_approval_step.py index 314f6c31f..841650b31 100644 --- a/tests/test_service_catalog/test_models/test_approval_step.py +++ b/tests/test_service_catalog/test_models/test_approval_step.py @@ -20,6 +20,7 @@ def test_set_position(self): test_approval_step_2 = ApprovalStep.objects.create(name="test_approval_step_2", approval_workflow=self.test_approval_workflow) + test_approval_step_2.refresh_from_db() self.assertEqual(test_approval_step_2.position, 1) def test_default_perm_added_on_save(self): diff --git a/tests/test_service_catalog/test_models/test_approval_workflow_version.py b/tests/test_service_catalog/test_models/test_approval_workflow_version.py new file mode 100644 index 000000000..473a3b716 --- /dev/null +++ b/tests/test_service_catalog/test_models/test_approval_workflow_version.py @@ -0,0 +1,87 @@ +from service_catalog.models import ApprovalWorkflow, ApprovalStep, Instance, Request, ApprovalWorkflowState +from tests.setup import SetupOperation, SetupOrg +from tests.utils import TransactionTestUtils + + +class TestApprovalWorkflowVersion(TransactionTestUtils, SetupOperation, SetupOrg): + + def setUp(self): + SetupOperation.setUp(self) + SetupOrg.setUp(self) + + def assertVersionEqual(self, aw, version): + aw.refresh_from_db() + self.assertEqual(aw.version, version) + + def test_version_increment(self): + # Create ApprovalWorkflow + aw = ApprovalWorkflow.objects.create( + name="AW - All scopes", + operation=self.operation_create_1, + enabled=True + ) + + # After creation version is 1 + self.assertVersionEqual(aw, 1) + + # Version increment when creating new step + as1 = ApprovalStep.objects.create(name="step 1", approval_workflow=aw) + self.assertVersionEqual(aw, 2) + + # Version increment when editing a step + as1.name = "Step 1" + self.assertVersionEqual(aw, 2) + as1.save() + self.assertVersionEqual(aw, 3) + + # Version increment when creating a second step + as2 = ApprovalStep.objects.create(name="Step 2", approval_workflow=aw) + self.assertVersionEqual(aw, 4) + + # Version increment when removing a step + as2.delete() + self.assertVersionEqual(aw, 5) + + def test_approval_workflow_state_take_version_number(self): + aw = ApprovalWorkflow.objects.create( + name="AW - All scopes", + operation=self.operation_create_1, + enabled=True + ) + # AW version is 1 + self.assertVersionEqual(aw, 1) + self.instance_1 = Instance.objects.create(name="Instance 1", quota_scope=self.org1, service=self.service_1) + self.request_1 = Request.objects.create(instance=self.instance_1, operation=self.operation_create_1) + # AW version is 1 even after an instantiate + self.assertVersionEqual(aw, 1) + self.assertVersionEqual(self.request_1.approval_workflow_state, 1) + + # Ensure that resetting doesn't increment version + # test que quand aw avance aws avance pas pour etre sur + def test_approval_workflow_state_version_keep_created_version_after_add_step(self): + aw = ApprovalWorkflow.objects.create( + name="AW - All scopes", + operation=self.operation_create_1, + enabled=True + ) + # AW version is 1 + self.assertVersionEqual(aw, 1) + self.instance_1 = Instance.objects.create(name="Instance 1", quota_scope=self.org1, service=self.service_1) + self.request_1 = Request.objects.create(instance=self.instance_1, operation=self.operation_create_1) + # AW version is 1 even after an instantiate + self.assertVersionEqual(aw, 1) + self.assertVersionEqual(self.request_1.approval_workflow_state, 1) + ApprovalStep.objects.create(name="Step 1", approval_workflow=aw) + self.assertVersionEqual(aw, 2) + self.assertVersionEqual(self.request_1.approval_workflow_state, 1) + # TODO: doesn't work value was reset to 1 in tests + self.request_2 = Request.objects.create(instance=self.instance_1, operation=self.operation_create_1) + self.request_2.refresh_from_db() + self.assertVersionEqual(self.request_2.approval_workflow_state, 2) + + + # fonction dans AW qui cherche tous les aws qui ont la bonne version ( get_request_that_doesnt_need_update) + + # fonction get request to reset tu fais (A ou B )- C et C c'est la fonction get_request_that_doesnt_need_update . A et B déjà la + + # view qui affiche le retour de get_request_to reset et fini diff --git a/tests/test_service_catalog/test_views/test_admin/test_approval/test_approval_step_views.py b/tests/test_service_catalog/test_views/test_admin/test_approval/test_approval_step_views.py index d2f4b22b2..ccef1ab0a 100644 --- a/tests/test_service_catalog/test_views/test_admin/test_approval/test_approval_step_views.py +++ b/tests/test_service_catalog/test_views/test_admin/test_approval/test_approval_step_views.py @@ -12,6 +12,8 @@ def setUp(self): super(TestApprovalStepViews, self).setUp() def test_ajax_approval_step_position_update(self): + self.test_approval_step_1.refresh_from_db() + self.test_approval_step_2.refresh_from_db() self.assertEqual(self.test_approval_step_1.position, 0) self.assertEqual(self.test_approval_step_2.position, 1)