diff --git a/ldvspec/lofardata/forms.py b/ldvspec/lofardata/forms.py index cb77f160e21be27d1091c60e7d1de0a88f607bc5..1d2cf1d1ddc0d989290af65ce3ee5d17a901f143 100644 --- a/ldvspec/lofardata/forms.py +++ b/ldvspec/lofardata/forms.py @@ -1,8 +1,8 @@ from django.core.exceptions import ValidationError -from .models import WorkSpecification, DataProductFilter +from .models import WorkSpecification, DataProductFilter, Group -from django.forms import ModelForm +from django.forms import ModelForm, CharField class WorkSpecificationForm(ModelForm): @@ -47,3 +47,24 @@ class WorkSpecificationForm(ModelForm): 'batch_size': "The number of files every task generated by this work specification should have. Example: 10 files in total can be split into 5 tasks of 2 files.", 'is_auto_submit': "By checking this box, the work specification will be directly sent to the processing site and thus does not need inspection." } + + +class GroupForm(ModelForm): + obs_ids = CharField(label='SAS IDs', + help_text="A list of SAS IDs seprated with a comma+space. Example: 123, 456, 789", + required=True) + + class Meta: + model = Group + fields = ['name', 'selected_workflow', 'selected_workflow_tag', 'processing_site', 'obs_ids'] + labels = { + 'selected_workflow': 'Selected workflow', + 'selected_workflow_tag': 'Workflow tag', + 'processing_site': 'Processing Site (ATDB)', + 'name': 'Name', + } + help_texts = {'selected_workflow': "The pipeline to run on the processing site.", + 'selected_workflow_tag': "The pipeline's tag as specified in ATDB.", + 'processing_site': "The ATDB processing site to run a specific workflow in.", + 'name': "A unique and custom name for this group" + } diff --git a/ldvspec/lofardata/templates/lofardata/group/create_update.html b/ldvspec/lofardata/templates/lofardata/group/create_update.html new file mode 100644 index 0000000000000000000000000000000000000000..6609f01f6605dcbbecd0a35b74897a771ca32933 --- /dev/null +++ b/ldvspec/lofardata/templates/lofardata/group/create_update.html @@ -0,0 +1,172 @@ +{% extends 'lofardata/index.html' %} +{% load static %} +{% load crispy_forms_tags %} +{% load define_action %} +{% load widget_tweaks %} +{% load json %} + +{% block myBlock %} + <div class="overlay"> + <div class="modal-dias-wrapper"> + <div class="modal-dias modal-dias--fit-content"> + <a class="icon icon--times button--close" href="{% url 'index' %}"></a> + <header class="flex-wrapper flex-wrapper--centered flex-wrapper--column"> + {% if object.pk %} + <h2 class="title text text--primary">Edit Group {{ object.pk }}</h2> + <form method="post" action="{% url 'group-update' object.pk %}"> + {% else %} + <h2 class="title text text--primary">New Group </h2> + <form method="post" action="{% url 'group-create' %}"> + {% endif %} + + {% csrf_token %} + <div class="flex-wrapper flex-wrapper--row flex-wrapper--start"> + <!-- Standard input fields --> + <div class="custom--div-margin"> + <div class="flex-wrapper"> + <div class="flex-wrapper flex-wrapper--row custom__input--fixed-min-width"> + <label class="input__label">{{ form.name.label }}*</label> + <a class="tooltip-dias tooltip-dias-right custom--tooltip" + data-tooltip="{{ form.name.help_text }}">m</a> + </div> + <input class="input input--text margin-left margin-bottom" + id="id_name" name="name" + test-id="name" + placeholder="Enter a group name" type="text"> + </div> + + + <div class="flex-wrapper flex-wrapper--row" id="div_id_processing_site"> + <div class="flex-wrapper flex-wrapper--row custom__input--fixed-min-width"> + <label class="input__label">{{ form.processing_site.label }}*</label> + <a class="tooltip-dias tooltip-dias-right custom--tooltip" + data-tooltip="{{ form.processing_site.help_text }}">m</a> + </div> + <div class="input-select-wrapper icon--inline icon-after icon-after--angle-down"> + <select class="input input--select custom__input--fixed-width margin-left margin-bottom" + name="processing_site" + data-bind="options:processingSites, + optionsCaption: '---------', + optionsText: function(item) { return item.name + ' - ' + item.url}, + optionsValue: 'name', + value: selectedProcessingSite" + test-id="processing_site" + ></select> + </div> + </div> + + + <div class="flex-wrapper flex-wrapper--row" id="div_id_selected_workflow_tag"> + <div class="flex-wrapper flex-wrapper--row custom__input--fixed-min-width"> + <label class="input__label" + for="id_selected_workflow_tag">{{ form.selected_workflow_tag.label_tag }}*</label> + <a class="tooltip-dias tooltip-dias-right custom--tooltip" + data-tooltip="{{ form.selected_workflow_tag.help_text }}">m</a> + </div> + <div class="input-select-wrapper icon--inline icon-after icon-after--angle-down"> + <select class="input input--select custom__input--fixed-width margin-bottom margin-left" + id="id_selected_workflow_tag" + test-id="workflow_tag" + name="selected_workflow_tag" + style="width: 12rem" + data-bind="options:tags, + optionsCaption: caption, + value: selectedTag, + disable: isLoading"> + </select> + </div> + </div> + + + <div class="flex-wrapper flex-wrapper--row" id="div_id_selected_workflow"> + <div class="flex-wrapper flex-wrapper--row custom__input--fixed-min-width"> + <label class="input__label" + for="id_selected_workflow">{{ form.selected_workflow.label }}*</label> + <a class="tooltip-dias tooltip-dias-right custom--tooltip" + data-tooltip="{{ form.selected_workflow.help_text }}">m</a> + </div> + <div class="input-select-wrapper icon--inline icon-after icon-after--angle-down"> + <select class="input input--select custom__input--fixed-width margin-bottom margin-left" + id="id_selected_workflow" + name="selected_workflow" + style="width: 12rem" + test-id="workflow" + data-bind="options:workflowsByTag, + optionsCaption: caption, + optionsValue: 'workflow_uri', + optionsText: 'workflow_uri', + value: selectedWorkflow, + disable: isLoading"> + </select> + </div> + </div> + + <div class="flex-wrapper"> + <div class="flex-wrapper flex-wrapper--row custom__input--fixed-min-width"> + <label class="input__label">{{ form.obs_ids.label }}*</label> + <a class="tooltip-dias tooltip-dias-right custom--tooltip" + data-tooltip="{{ form.obs_ids.help_text }}">m</a> + </div> + <input class="input input--text margin-left margin-bottom" + id="id_obs_ids" name="obs_ids" + test-id="obs_ids" + placeholder="Enter a a list of SAS IDs" type="text"> + </div> + + </div> + </div> + + <div class="flex-wrapper flex-wrapper--centered margin-bottom margin-top"> + <button class="button button--primary margin-right" + type="submit" + name="action" + value="Submit" + test-id="create-update" + title=" + {% if object.pk %} + Update the task + {% else %} + Create the task to inspect the result. Send it later to ATDB. + {% endif %}"> + + {% if object.pk %} + Update + {% else %} + Create + {% endif %} + </button> + <a class="button button--red button--primary margin-left" href="{% url 'index' %}">Cancel</a> + </div> + + </form> + </header> + {% if form.errors %} + <div class="popup-bar popup-bar--red"> + <span class="icon icon--left icon--color-inherit icon--times"></span> + <div class="flex-wrapper flex-wrapper--column"> + The input is invalid: + {% for field, errors in form.errors.items %} + {% for error in errors %} + {% if field == '__all__' %} + <li class="text text--red text--faded">{{ error }}</li> + {% else %} + <li class="text text--red text--faded">{{ field }}: {{ error }}</li> + {% endif %} + {% endfor %} + {% endfor %} + </div> + </div> + {% endif %} + </div> + </div> + </div> + <script type='text/javascript' src='https://cdnjs.cloudflare.com/ajax/libs/knockout/3.5.0/knockout-min.js'></script> + <script type="text/javascript"> + const processingSite = "{% url 'processingsite-detail' 'replace' %}"; + const existingWorkflow = '{{ object.selected_workflow|default_if_none:"null" }}'; + const existingWorkflowTag = '{{ object.selected_workflow_tag|default_if_none:"null" }}'; + const processingSites = {{ processing_sites | json }}; + const existingProcessingSite = '{{ object.processing_site.name }}'; + </script> + <script src="{% static 'update_workflow.js' %}"></script> +{% endblock %} diff --git a/ldvspec/lofardata/templates/lofardata/index.html b/ldvspec/lofardata/templates/lofardata/index.html index 4b7ed7683d8281d81d70e22cae3d43e59080a804..1e9895a21068cd7651afe5b57d94530f52e2a240 100644 --- a/ldvspec/lofardata/templates/lofardata/index.html +++ b/ldvspec/lofardata/templates/lofardata/index.html @@ -13,6 +13,10 @@ title="Create a new work specification" href="{% url 'specification-create' %}"> <span class="icon icon--plus"></span></a> + <a class="button button--secondary button--icon-button margin-left" + title="Create a new group" + href="{% url 'group-create' %}"> + <span class="icon icon--layer-plus"></span></a> </div> <div class="table"> diff --git a/ldvspec/lofardata/tests/test_group_creation.py b/ldvspec/lofardata/tests/test_group_creation.py new file mode 100644 index 0000000000000000000000000000000000000000..037db535934d080df9eb727943f651b99dd234b4 --- /dev/null +++ b/ldvspec/lofardata/tests/test_group_creation.py @@ -0,0 +1,10 @@ +import unittest + +from lofardata.models import WorkSpecification, Group + +class WorkSpecificationCreation(unittest.TestCase): + def test_create_group_with_one_obs_id(self): + pass + + def test_create_group_with_multiple_obs_ids(self): + pass diff --git a/ldvspec/lofardata/urls.py b/ldvspec/lofardata/urls.py index 2b1ff6b53c68d279a6165ed269e3f13b6079edcd..afcc17c81be747766bac4d816ff449359cc3aa6a 100644 --- a/ldvspec/lofardata/urls.py +++ b/ldvspec/lofardata/urls.py @@ -32,6 +32,8 @@ urlpatterns = [ # GUI path('', views.Specifications.as_view(), name='index'), path('api/', views.api, name='api'), + + path('specification/<int:pk>/', views.WorkSpecificationDetailView.as_view(), name='specification-detail'), path('specification/add/', views.WorkSpecificationCreateUpdateView.as_view(), name='specification-create'), path('specification/update/<int:pk>/', views.WorkSpecificationCreateUpdateView.as_view(), name='specification-update'), @@ -40,6 +42,8 @@ urlpatterns = [ path('specification/tasks/<int:pk>/', views.WorkSpecificationATDBTasksView.as_view(), name='specification-tasks'), path('specification/dataset-size-info/<int:pk>/', views.WorkSpecificationDatasetSizeInfoView.as_view(), name='dataset-size-info'), path('specification/dataproducts/<int:pk>', views.DataProductViewPerSasID.as_view(), name='specification-dataproducts'), + + path('group/add/', views.GroupCreateUpdateView.as_view(), name='group-create') # Workaround for injecting the urls from the ModelViewSet, which requires a "Router" diff --git a/ldvspec/lofardata/views.py b/ldvspec/lofardata/views.py index ecf0799379fb30b63a3855e9700d2eaa2d5c0939..c4a28b5778f89b5e39e919ea40f138da0ee09e16 100644 --- a/ldvspec/lofardata/views.py +++ b/ldvspec/lofardata/views.py @@ -14,13 +14,14 @@ from rest_framework.response import Response from rest_framework.reverse import reverse_lazy from rest_framework.schemas.openapi import AutoSchema -from .forms import WorkSpecificationForm +from .forms import WorkSpecificationForm, GroupForm from .mixins import CanAccessWorkSpecificationMixin from .models import ( ATDBProcessingSite, DataProduct, DataProductFilter, WorkSpecification, + Group, ) from .serializers import ( ATDBProcessingSiteSerializer, @@ -66,12 +67,14 @@ def api(request): atdb_hosts = ATDBProcessingSite.objects.values("name", "url") return render(request, "lofardata/api.html", {"atdb_hosts": atdb_hosts}) + def set_post_submit_values(specification, user): specification.async_task_result = None specification.is_ready = False if specification.created_by is None: specification.created_by = user + class Specifications(ListView): serializer_class = WorkSpecificationSerializer template_name = "lofardata/index.html" @@ -224,6 +227,7 @@ class DataProductViewPerSasID(LoginRequiredMixin, CanAccessWorkSpecificationMixi def get_object(self): return WorkSpecification.objects.get(pk=self.kwargs['pk']) + # ---------- REST API views ---------- class DataProductView(generics.ListCreateAPIView): model = DataProduct @@ -289,3 +293,21 @@ class WorkSpecificationViewset(viewsets.ModelViewSet): time.sleep(1) # allow for some time to pass return redirect("index") + + +class GroupCreateUpdateView(LoginRequiredMixin, UpdateView): + template_name = "lofardata/group/create_update.html" + model = Group + form_class = GroupForm + + def get_object(self, queryset=None): + if self.kwargs.__len__() == 0 or self.kwargs["pk"] is None: + group = Group() + else: + group = Group.objects.get(pk=self.kwargs["pk"]) + return group + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["processing_sites"] = list(ATDBProcessingSite.objects.values("name", "url")) + return context