From 3cb5657370eedbb1507890ad8ba722f4545fdf39 Mon Sep 17 00:00:00 2001 From: elias-boulharts Date: Mon, 20 Nov 2023 12:08:11 +0100 Subject: [PATCH] [Enhancement] Add service details page --- service_catalog/forms/operation_forms.py | 19 +-- ...ter_operation_name_alter_operation_type.py | 23 ++++ service_catalog/models/operations.py | 5 +- service_catalog/models/services.py | 4 - service_catalog/tables/operation_tables.py | 2 +- service_catalog/tables/service_tables.py | 3 +- service_catalog/urls.py | 13 +- service_catalog/views/operation.py | 113 +++++------------- service_catalog/views/service.py | 53 ++++---- service_catalog/views/tower_survey_field.py | 11 +- templates/generics/breadcrumbs.html | 2 +- .../operation-button-edit-survey.html | 2 +- .../service/operation/operation-delete.html | 2 +- .../operation/operation-edit-survey.html | 2 +- .../buttons/delete_operation_button.html | 2 +- .../buttons/edit_operation_button.html | 2 +- .../buttons/operation_survey_button.html | 2 +- .../custom_columns/operation_actions.html | 10 -- .../custom_columns/service_operations.html | 2 +- templates/service_catalog/service_detail.html | 93 ++++++++++++++ .../test_urls/test_operation.py | 23 ++-- .../test_urls/test_service.py | 5 + .../test_operations/test_create.py | 11 +- .../test_operations/test_delete.py | 2 - .../test_catalog/test_operations/test_edit.py | 19 ++- .../test_catalog/test_operations/test_list.py | 6 +- 26 files changed, 235 insertions(+), 196 deletions(-) create mode 100644 service_catalog/migrations/0032_alter_operation_name_alter_operation_type.py delete mode 100644 templates/service_catalog/custom_columns/operation_actions.html create mode 100644 templates/service_catalog/service_detail.html diff --git a/service_catalog/forms/operation_forms.py b/service_catalog/forms/operation_forms.py index 1c95a718a..bd0f38fbe 100644 --- a/service_catalog/forms/operation_forms.py +++ b/service_catalog/forms/operation_forms.py @@ -2,25 +2,10 @@ from service_catalog.models import Operation -class ServiceOperationForm(SquestModelForm): - def __init__(self, *args, **kwargs): - self.service = kwargs.pop("service") - super(ServiceOperationForm, self).__init__(*args, **kwargs) - choice_type = [('CREATE', 'Create'), ('UPDATE', 'Update'), ('DELETE', 'Delete')] - # Default behavior - self.fields['type'].choices = choice_type - self.fields['type'].initial = self.fields['type'].choices[0] - - def save(self, commit=True): - new_operation = super(ServiceOperationForm, self).save(commit=False) - new_operation.service = self.service - new_operation.save() - return new_operation - - +class OperationForm(SquestModelForm): class Meta: model = Operation - fields = ["name", "description", "job_template", "type", "process_timeout_second", + fields = ["service", "name", "description", "job_template", "type", "process_timeout_second", "auto_accept", "auto_process", "enabled", "is_admin_operation", "extra_vars", "default_inventory_id", "default_limits", "default_tags", "default_skip_tags", "default_credentials_ids", "default_verbosity", "default_diff_mode", "default_job_type"] diff --git a/service_catalog/migrations/0032_alter_operation_name_alter_operation_type.py b/service_catalog/migrations/0032_alter_operation_name_alter_operation_type.py new file mode 100644 index 000000000..07c2a0e4d --- /dev/null +++ b/service_catalog/migrations/0032_alter_operation_name_alter_operation_type.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.6 on 2023-11-20 11:01 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('service_catalog', '0031_service_attribute_definitions'), + ] + + operations = [ + migrations.AlterField( + model_name='operation', + name='name', + field=models.CharField(max_length=100), + ), + migrations.AlterField( + model_name='operation', + name='type', + field=models.CharField(choices=[('CREATE', 'Create'), ('UPDATE', 'Update'), ('DELETE', 'Delete')], default='CREATE', max_length=10), + ), + ] diff --git a/service_catalog/models/operations.py b/service_catalog/models/operations.py index 06495dd71..4d46aa652 100644 --- a/service_catalog/models/operations.py +++ b/service_catalog/models/operations.py @@ -12,13 +12,12 @@ class Operation(SquestModel): - name = CharField(max_length=100, verbose_name="Operation name") + name = CharField(max_length=100) description = CharField(max_length=500, blank=True, null=True) type = CharField( max_length=10, choices=OperationType.choices, default=OperationType.CREATE, - verbose_name="Operation type" ) service = ForeignKey(Service, on_delete=CASCADE, related_name="operations", related_query_name="operation") @@ -55,7 +54,7 @@ def __str__(self): return f"{self.name} ({self.service})" def get_absolute_url(self): - return reverse(f"service_catalog:operation_details", args=[self.service.id, self.pk]) + return reverse(f"service_catalog:operation_details", args=[self.pk]) def clean(self): if self.extra_vars is None or not isinstance(self.extra_vars, dict): diff --git a/service_catalog/models/services.py b/service_catalog/models/services.py index 18897b95b..f7e15503a 100644 --- a/service_catalog/models/services.py +++ b/service_catalog/models/services.py @@ -2,7 +2,6 @@ from django.db.models import CharField, ImageField, BooleanField, ForeignKey, SET_NULL, JSONField, ManyToManyField from django.db.models.signals import pre_save from django.dispatch import receiver -from django.urls import reverse_lazy from django.utils.translation import gettext_lazy as _ from Squest.utils.squest_model import SquestModel @@ -39,9 +38,6 @@ class Meta: help_text="List of attributes linked to the service, they could be used on operation fields.", ) - def get_absolute_url(self): - return reverse_lazy('service_catalog:operation_list', kwargs={"service_id": self.id}) - def can_be_enabled(self): operation_create_list = self.operations.filter(type=OperationType.CREATE, enabled=True) if operation_create_list.exists(): diff --git a/service_catalog/tables/operation_tables.py b/service_catalog/tables/operation_tables.py index 570653fec..9fa31c1ae 100644 --- a/service_catalog/tables/operation_tables.py +++ b/service_catalog/tables/operation_tables.py @@ -17,7 +17,7 @@ class Meta: auto_accept = TemplateColumn(template_name='generics/custom_columns/generic_boolean.html') auto_process = TemplateColumn(template_name='generics/custom_columns/generic_boolean.html') is_admin_operation = TemplateColumn(template_name='generics/custom_columns/generic_boolean.html') - actions = TemplateColumn(template_name='service_catalog/custom_columns/operation_actions.html', orderable=False) + actions = TemplateColumn(template_name='generics/custom_columns/generic_actions.html', orderable=False) class OperationTableFromInstanceDetails(SquestTable): diff --git a/service_catalog/tables/service_tables.py b/service_catalog/tables/service_tables.py index 0f84f7bf6..5cb85d095 100644 --- a/service_catalog/tables/service_tables.py +++ b/service_catalog/tables/service_tables.py @@ -1,4 +1,4 @@ -from django_tables2 import TemplateColumn +from django_tables2 import TemplateColumn, LinkColumn from Squest.utils.squest_table import SquestTable from service_catalog.models import Service @@ -9,6 +9,7 @@ class ServiceTable(SquestTable): enabled = TemplateColumn(template_name='generics/custom_columns/generic_boolean_check.html') operations = TemplateColumn(template_name='service_catalog/custom_columns/service_operations.html', verbose_name="Operations", orderable=False) + name = LinkColumn() class Meta: model = Service diff --git a/service_catalog/urls.py b/service_catalog/urls.py index 7869df3a3..80452b37f 100644 --- a/service_catalog/urls.py +++ b/service_catalog/urls.py @@ -47,13 +47,14 @@ path('service/create/', views.ServiceCreateView.as_view(), name='service_create'), path('service//edit/', views.ServiceEditView.as_view(), name='service_edit'), path('service//delete/', views.ServiceDeleteView.as_view(), name='service_delete'), + path('service//', views.ServiceDetailView.as_view(), name='service_details'), # Operation CRUD - path('service//operation/', views.OperationListView.as_view(), name='operation_list'), - path('service//operation/create/', views.OperationCreateView.as_view(), name='operation_create'), - path('service//operation//delete/', views.OperationDeleteView.as_view(), name='operation_delete'), - path('service//operation//edit/', views.OperationEditView.as_view(), name='operation_edit'), - path('service//operation//', views.OperationDetailView.as_view(), name='operation_details'), + path('operation/', views.OperationListView.as_view(), name='operation_list'), + path('operation/create/', views.OperationCreateView.as_view(), name='operation_create'), + path('operation//delete/', views.OperationDeleteView.as_view(), name='operation_delete'), + path('operation//edit/', views.OperationEditView.as_view(), name='operation_edit'), + path('operation//', views.OperationDetailView.as_view(), name='operation_details'), # Request operation endpoints path('service//operation//request/', @@ -63,7 +64,7 @@ name='create_operation_list'), # Edit operation survey endpoint - path('service//operation//survey/', views.operation_edit_survey, name='operation_edit_survey'), + path('operation//survey/', views.operation_edit_survey, name='operation_edit_survey'), # Instance CRUD path('instance/', views.InstanceListView.as_view(), name='instance_list'), diff --git a/service_catalog/views/operation.py b/service_catalog/views/operation.py index 170b09d9f..b850a9ab1 100644 --- a/service_catalog/views/operation.py +++ b/service_catalog/views/operation.py @@ -1,90 +1,58 @@ -from django.shortcuts import redirect, get_object_or_404 +from django.shortcuts import redirect from Squest.utils.squest_table import SquestRequestConfig from Squest.utils.squest_views import * from service_catalog.filters.operation_filter import OperationFilter, OperationFilterLimited -from service_catalog.forms import ServiceOperationForm +from service_catalog.forms import OperationForm from service_catalog.models import Operation, Service, OperationType, ApprovalWorkflow from service_catalog.tables.approval_workflow_table import ApprovalWorkflowTable from service_catalog.tables.operation_tables import OperationTable, CreateOperationTable +def get_breadcrumbs_for_operation(operation): + breadcrumbs = [ + {'text': 'Service catalog', 'url': reverse('service_catalog:service_catalog_list')}, + {'text': 'Service', 'url': reverse('service_catalog:service_list')}, + {'text': operation.service, 'url': operation.service.get_absolute_url()}, + {'text': 'Operation', 'url': ''}, + ] + if operation is not None: + breadcrumbs.append({'text': operation, 'url': operation.get_absolute_url()}) + return breadcrumbs + + class OperationListView(SquestListView): model = Operation filterset_class = OperationFilter table_class = OperationTable - def get_generic_url_kwargs(self): - return {'service_id': self.kwargs.get('service_id')} - - def get_queryset(self): - return super().get_queryset().filter(service__id=self.kwargs.get('service_id')) - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context['breadcrumbs'] = [ - {'text': 'Service catalog', 'url': reverse('service_catalog:service_catalog_list')}, - {'text': 'Services', 'url': reverse('service_catalog:service_list')}, - {'text': Service.objects.get(id=self.kwargs.get('service_id')), 'url': ""}, - {'text': 'Operations', 'url': ""}, - ] - return context - class OperationCreateView(SquestCreateView): model = Operation - form_class = ServiceOperationForm + form_class = OperationForm - def get_form_kwargs(self): - kwargs = super().get_form_kwargs() - self.service = get_object_or_404(Service, pk=self.kwargs.get('service_id')) - kwargs['service'] = self.service - return kwargs + def get_initial(self): + initial = super().get_initial() + initial["service"] = f"{self.request.GET.get('service')}" + return initial.copy() def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['breadcrumbs'] = [ - {'text': 'Service catalog', 'url': reverse('service_catalog:service_catalog_list')}, - {'text': 'Services', 'url': reverse('service_catalog:service_list')}, - {'text': self.service, 'url': reverse('service_catalog:operation_list', args=[self.service.id])}, - {'text': 'Create a new operation', 'url': ""}, + {'text': 'Operation', 'url': reverse('service_catalog:operation_list')}, + {'text': 'New operation', 'url': ""}, ] return context - def get_success_url(self): - return reverse("service_catalog:operation_edit_survey", kwargs={"service_id": self.service.id, - "pk": self.object.id}) - class OperationDetailView(SquestDetailView): model = Operation - def get_queryset(self): - return super().get_queryset().filter(service__id=self.kwargs.get('service_id')) - def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) config = SquestRequestConfig(self.request) - context['breadcrumbs'] = [ - { - 'text': 'Service catalog', - 'url': reverse('service_catalog:service_catalog_list') - }, - { - 'text': 'Service', - 'url': reverse('service_catalog:service_list') - }, - { - 'text': self.get_object().service, - 'url': reverse('service_catalog:operation_list', - args=[self.get_object().service.id]) - }, - { - 'text': self.get_object(), - 'url': '' - } - ] + context['breadcrumbs'] = get_breadcrumbs_for_operation(self.get_object()) context['extra_html_button_path'] = "service_catalog/buttons/operation_survey_button.html" if self.request.user.has_perm('service_catalog.view_approvalworkflow'): context['workflows_table'] = ApprovalWorkflowTable( @@ -97,49 +65,22 @@ def get_context_data(self, **kwargs): class OperationEditView(SquestUpdateView): model = Operation - form_class = ServiceOperationForm - - def get_queryset(self): - return super().get_queryset().filter(service__id=self.kwargs.get('service_id')) - - def get_success_url(self): - return self.get_object().service.get_absolute_url() - - def get_form_kwargs(self): - kwargs = super().get_form_kwargs() - kwargs['service'] = self.get_object().service - return kwargs + form_class = OperationForm def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['breadcrumbs'] = [ - {'text': 'Service catalog', 'url': reverse('service_catalog:service_catalog_list')}, - {'text': 'Services', 'url': reverse('service_catalog:service_list')}, - {'text': self.get_object().service, - 'url': reverse('service_catalog:operation_list', args=[self.get_object().service.id])}, - {'text': self.get_object(), 'url': ""}, - ] + context['breadcrumbs'] = get_breadcrumbs_for_operation(self.get_object()) + context['breadcrumbs'].append({'text': 'Edit', 'url': ''}) return context class OperationDeleteView(SquestDeleteView): model = Operation - def get_queryset(self): - return super().get_queryset().filter(service__id=self.kwargs.get('service_id')) - - def get_generic_url_kwargs(self): - return {'service_id': self.kwargs.get('service_id')} - def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['breadcrumbs'] = [ - {'text': 'Service catalog', 'url': reverse('service_catalog:service_catalog_list')}, - {'text': 'Services', 'url': reverse('service_catalog:service_list')}, - {'text': self.get_object().service, - 'url': reverse('service_catalog:operation_list', args=[self.get_object().service.id])}, - {'text': self.get_object(), 'url': ""}, - ] + context['breadcrumbs'] = get_breadcrumbs_for_operation(self.get_object()) + context['breadcrumbs'].append({'text': 'Delete', 'url': ''}) return context diff --git a/service_catalog/views/service.py b/service_catalog/views/service.py index a10263b88..bd04d8cea 100644 --- a/service_catalog/views/service.py +++ b/service_catalog/views/service.py @@ -2,10 +2,20 @@ from service_catalog.filters.service_filter import ServiceFilter from service_catalog.forms import ServiceForm -from service_catalog.models import Service +from service_catalog.models import Service, Operation +from service_catalog.tables.operation_tables import OperationTable from service_catalog.tables.service_tables import ServiceTable +def get_breadcrumbs_for_service(service=None): + breadcrumbs = [ + {'text': 'Service catalog', 'url': reverse('service_catalog:service_catalog_list')}, + {'text': 'Service', 'url': reverse('service_catalog:service_list')} + ] + if service is not None: + breadcrumbs.append({'text': service, 'url': service.get_absolute_url()}) + return breadcrumbs + class ServiceListView(SquestListView): table_class = ServiceTable model = Service @@ -13,10 +23,19 @@ class ServiceListView(SquestListView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['breadcrumbs'] = [ - {'text': 'Service catalog', 'url': reverse('service_catalog:service_catalog_list')}, - {'text': 'Services', 'url': ''} - ] + context['breadcrumbs'] = get_breadcrumbs_for_service() + return context + + +class ServiceDetailView(SquestDetailView): + model = Service + filterset_class = ServiceFilter + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['breadcrumbs'] = get_breadcrumbs_for_service(self.get_object()) + if self.request.user.has_perm("service_catalog.list_operation", self.object): + context['operations_table'] = OperationTable(self.object.operations.all(), prefix="operation-") return context @@ -26,16 +45,14 @@ class ServiceCreateView(SquestCreateView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['breadcrumbs'] = [ - {'text': 'Service catalog', 'url': reverse('service_catalog:service_catalog_list')}, - {'text': 'Services', 'url': reverse('service_catalog:service_list')}, - {'text': 'New service', 'url': ""}, - ] + context['breadcrumbs'] = get_breadcrumbs_for_service() + context['breadcrumbs'][1]['url'] = reverse('service_catalog:service_list') + context['breadcrumbs'].append({'text': 'New service', 'url': ""}) context['multipart'] = True return context def get_success_url(self): - return reverse("service_catalog:operation_create", kwargs={"service_id": self.object.id}) + return reverse("service_catalog:operation_create") class ServiceEditView(SquestUpdateView): @@ -44,11 +61,8 @@ class ServiceEditView(SquestUpdateView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['breadcrumbs'] = [ - {'text': 'Service catalog', 'url': reverse('service_catalog:service_catalog_list')}, - {'text': 'Services', 'url': reverse('service_catalog:service_list')}, - {'text': self.get_object(), 'url': ""}, - ] + context['breadcrumbs'] = get_breadcrumbs_for_service(self.get_object()) + context['breadcrumbs'].append({'text': 'Edit', 'url': ''}) context['multipart'] = True return context @@ -58,9 +72,6 @@ class ServiceDeleteView(SquestDeleteView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['breadcrumbs'] = [ - {'text': 'Service catalog', 'url': reverse('service_catalog:service_catalog_list')}, - {'text': 'Services', 'url': reverse('service_catalog:service_list')}, - {'text': self.get_object(), 'url': ""}, - ] + context['breadcrumbs'] = get_breadcrumbs_for_service(self.get_object()) + context['breadcrumbs'].append({'text': 'Delete', 'url': ''}) return context diff --git a/service_catalog/views/tower_survey_field.py b/service_catalog/views/tower_survey_field.py index 6110c2b84..f21462fbb 100644 --- a/service_catalog/views/tower_survey_field.py +++ b/service_catalog/views/tower_survey_field.py @@ -4,11 +4,10 @@ from Squest.utils.squest_views import SquestPermissionDenied from service_catalog.forms.tower_survey_field_form import TowerSurveyFieldForm -from service_catalog.models import Service, Operation, TowerSurveyField +from service_catalog.models import Operation, TowerSurveyField -def operation_edit_survey(request, service_id, pk): - target_service = get_object_or_404(Service, id=service_id) +def operation_edit_survey(request, pk): target_operation = get_object_or_404(Operation, id=pk) if not request.user.has_perm('service_catalog.change_operation', target_operation): raise SquestPermissionDenied('service_catalog.change_operation') @@ -21,18 +20,18 @@ def operation_edit_survey(request, service_id, pk): formset = survey_selector_form_set(request.POST) if formset.is_valid(): formset.save() - return redirect('service_catalog:operation_list', service_id=target_service.id) + return redirect(target_operation.service.get_absolute_url()) breadcrumbs = [ {'text': 'Service catalog', 'url': reverse('service_catalog:service_catalog_list')}, {'text': 'Services', 'url': reverse('service_catalog:service_list')}, - {'text': target_service.name, 'url': reverse('service_catalog:operation_list', args=[service_id])}, + {'text': target_operation.service.name, 'url': target_operation.service.get_absolute_url()}, {'text': "Operation", 'url': ''}, {'text': target_operation.name, 'url': ""}, {'text': "Survey", 'url': ''}, ] context = {'formset': formset, - 'service': target_service, + 'service': target_operation.service, 'operation': target_operation, 'breadcrumbs': breadcrumbs} return render(request, diff --git a/templates/generics/breadcrumbs.html b/templates/generics/breadcrumbs.html index 4791b89e3..158bf598a 100644 --- a/templates/generics/breadcrumbs.html +++ b/templates/generics/breadcrumbs.html @@ -1,7 +1,7 @@ {% if breadcrumbs %}