diff --git a/.gitignore b/.gitignore index 835d123e55024f7ef741087f7c198d1f936a4dae..6d6cef476da10824272a8c80600560dd9256dd54 100644 --- a/.gitignore +++ b/.gitignore @@ -3,8 +3,8 @@ __pycache__/ # Robot framework reports -*.html -*xml +integration/*.xml +integration/*.html # Integration logs integration/*.log diff --git a/README.md b/README.md index fa311be715d08f420a747c8b1b674290b38c3c18..6fe28edcff6598fb58f66b3500a71c1fdbd5e0ca 100644 --- a/README.md +++ b/README.md @@ -35,39 +35,19 @@ docker exec -ti ldv-specification python manage.py migrate --settings ldvspec.se ## Local Development Environment -### Postgres Database in Docker - -Run `docker-compose -f docker-compose-postgres-dev.yml up -d` with the following compose file to spin up a new Postgres container. -See the `docker-compose-postgres-dev.yml` file in the `docker` directory. -(the shown configuration is based on the `dev.py` file. You can change, but then also change it in `dev.py`) -```yaml - -version: "3.7" -services: - - ldv-spec-db: - image: postgres:14 - container_name: ldv-spec-postgres - ports: - - "5432:5432" - environment: - POSTGRES_PASSWORD: "atdb123" - POSTGRES_USER: "atdb_admin" - POSTGRES_DB: "ldv-spec-db" - volumes: - - ldv-spec-db:/var/lib/postgresql/data - restart: always - - rabbitmq: - container_name: ldv-spec-rabbit - image: rabbitmq:3-management - ports: - - "5672:5672" - -volumes: - ldv-spec-db: - -``` +### Start developing + +- Copy the `ldvspec.example.env`, rename it to `ldvspec.env` and fill in the variables. The variables should match the `local.py` settings which are coherent with the `docker-compose-local.yml` setup. +- Run `docker-compose -f docker-compose-local.yml up -d` with the following compose file to spin up a new Postgres container, celery worker and rabbitMQ. +- Run the following python command to start developing + > python manage.py migrate --settings=ldvspec.settings.local + > + > python manage.py createsuperuser --settings=ldvspec.settings.local + > + > python manage.py runserver --settings=ldvspec.settings.local + > + +-[ ] TODO: load fixture to have sample data ### Django Application * clone the repo diff --git a/ldvspec/Dockerfile b/ldvspec/Dockerfile index 8233ad763713b4ec57644ebb7f441a7861247d9e..c2c972e4f26c4be1dc030333880b9780e4846d9c 100644 --- a/ldvspec/Dockerfile +++ b/ldvspec/Dockerfile @@ -5,9 +5,8 @@ FROM ${BASE_IMAGE} # Make sure we are in the source directory WORKDIR /src COPY requirements /src/requirements -# install dependencies and clean up apk build libs -RUN pip install --no-cache-dir -r requirements/prod.txt && \ - apk --purge del .build-deps +# install dependencies and clean up apt build libs +RUN pip install --no-cache-dir -r requirements/prod.txt # Copy the rest of the files COPY . . diff --git a/ldvspec/Dockerfile.base b/ldvspec/Dockerfile.base index 71aa3d9334c3d7920bf5e80e72a6de6b15b92aac..7c36318086ed9f6a07f2753c3be6f78ac83f8cfa 100644 --- a/ldvspec/Dockerfile.base +++ b/ldvspec/Dockerfile.base @@ -1,9 +1,4 @@ -FROM python:3.10-alpine +FROM python:3.10 ENV PYTHONUNBUFFERED 1 # Main dependencies -RUN apk update && apk add --no-cache \ - bash nano mc postgresql-libs - -# Cache the build dependencies -WORKDIR /src -RUN apk add --no-cache --virtual .build-deps gcc python3-dev musl-dev postgresql-dev +RUN apt-get update && apt-get install -y postgresql-client build-essential \ No newline at end of file diff --git a/ldvspec/docker/docker-compose-local.yml b/ldvspec/docker/docker-compose-local.yml new file mode 100644 index 0000000000000000000000000000000000000000..4eb901f4d35736429178947c82a6c5c8dc2b8006 --- /dev/null +++ b/ldvspec/docker/docker-compose-local.yml @@ -0,0 +1,49 @@ +version: '3.4' +networks: + ldv_network: + traefik_proxy: + external: + name: traefik_proxy + default: + driver: bridge + +services: + ldv-spec-db: + image: postgres:14 + container_name: ldv-spec-postgres + ports: + - "5433:5432" + networks: + - traefik_proxy + - ldv_network + env_file: + - ../ldvspec.env + volumes: + - ldv-spec-db:/var/lib/postgresql/data + restart: always + + rabbitmq: + image: rabbitmq:3-management + ports: + - "5672:5672" + networks: + - ldv_network + container_name: ldv-spec-rabbit + + ldv-specification-background: + container_name: ldv-specification-background + image: git.astron.nl:5000/astron-sdc/ldv-specification:${LDVSPEC_VERSION:-latest} + networks: + - ldv_network + depends_on: + - ldv-spec-db + environment: + CELERY_BROKER_URL: amqp://guest@rabbitmq:5672 + DJANGO_SETTINGS_MODULE: 'ldvspec.settings.local' + env_file: + - ../ldvspec.env + command: celery -A ldvspec worker -l INFO + restart: always + +volumes: + ldv-spec-db: diff --git a/ldvspec/docker/docker-compose-postgres-dev.yml b/ldvspec/docker/docker-compose-postgres-dev.yml deleted file mode 100644 index 2769910429ab49605891edf1002756efcc062bf2..0000000000000000000000000000000000000000 --- a/ldvspec/docker/docker-compose-postgres-dev.yml +++ /dev/null @@ -1,25 +0,0 @@ -version: "3.7" -services: - - ldv-spec-db: - image: postgres:14 - container_name: ldv-spec-postgres - expose: - - "5433" - ports: - - "5433:5432" - environment: - POSTGRES_PASSWORD: "secret" - POSTGRES_USER: "postgres" - POSTGRES_DB: "ldv-spec-db" - volumes: - - ldv-spec-db:/var/lib/postgresql/data - restart: always - - rabbitmq: - image: rabbitmq:3-management - ports: - - "5672:5672" - -volumes: - ldv-spec-db: diff --git a/ldvspec/ldvspec.env b/ldvspec/ldvspec.env new file mode 100755 index 0000000000000000000000000000000000000000..356d0d57e452b1bb69cba5dfb7715242ab5b25dd --- /dev/null +++ b/ldvspec/ldvspec.env @@ -0,0 +1,11 @@ +DEBUG=True +POSTGRES_PASSWORD=secret +POSTGRES_USER=postgres +POSTGRES_DB=ldv-spec-db +DATABASE_HOST=ldv-spec-db +DATABASE_PORT=5433 +DATABASE_NAME=ldv-spec-db +DATABASE_USER=postgres +DATABASE_PASSWORD=secret +ATDB_HOST=https://sdc-dev.astron.nl:5554/atdb/ +CELERY_BROKER_URL=amqp://guest@localhost:5672 diff --git a/ldvspec/ldvspec.example.env b/ldvspec/ldvspec.example.env new file mode 100755 index 0000000000000000000000000000000000000000..ececa1f9d28990cba63495bfae3de0eb2bc85e8b --- /dev/null +++ b/ldvspec/ldvspec.example.env @@ -0,0 +1,11 @@ +DEBUG=True +POSTGRES_PASSWORD= +POSTGRES_USER= +POSTGRES_DB= +DATABASE_HOST= +DATABASE_PORT= +DATABASE_NAME= +DATABASE_USER= +DATABASE_PASSWORD= +ATDB_HOST= +CELERY_BROKER_URL= \ No newline at end of file diff --git a/ldvspec/ldvspec/settings/local.py b/ldvspec/ldvspec/settings/local.py new file mode 100644 index 0000000000000000000000000000000000000000..97827532c7bf836be7e16ddf63bec56fd461c1dd --- /dev/null +++ b/ldvspec/ldvspec/settings/local.py @@ -0,0 +1,18 @@ +from ldvspec.settings.base import * + +DEV = True +DEBUG = True + +ALLOWED_HOSTS = ["*"] +CORS_ORIGIN_ALLOW_ALL = True + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql_psycopg2', + 'USER': 'postgres', + 'PASSWORD': 'secret', + 'NAME': 'ldv-spec-db', + 'HOST': 'localhost', + 'PORT': '5433', + }, +} \ No newline at end of file diff --git a/ldvspec/lofardata/forms.py b/ldvspec/lofardata/forms.py index e7d085e76d1b999fd35ded43257d41182bfc9413..8542d95ab7e91267e7212f160110a686ecda0ba1 100644 --- a/ldvspec/lofardata/forms.py +++ b/ldvspec/lofardata/forms.py @@ -13,7 +13,7 @@ class WorkSpecificationForm(ModelForm): filter_names = [filter_name[0] for filter_name in DataProductFilter.objects.all().values_list('field')] filters = {} for filter_name in filter_names: - if filter_name in self.data: + if filter_name in self.data and self.data[filter_name] != "": filters[filter_name] = self.data[filter_name] return filters @@ -24,10 +24,21 @@ class WorkSpecificationForm(ModelForm): class Meta: model = WorkSpecification - fields = ['filters', 'selected_workflow', 'processing_site', 'predecessor_specification', 'batch_size'] + fields = ['filters', 'selected_workflow', 'selected_workflow_tag', 'processing_site', + 'predecessor_specification', 'batch_size', + 'is_auto_submit'] labels = { 'selected_workflow': 'Selected workflow', + 'selected_workflow_tag': 'Workflow tag', 'processing_site': 'Processing Site (ATDB)', - 'predecessor_specification': 'Predecessor work specification', - 'batch_size': 'Files per task' + 'predecessor_specification': 'Predecessor', + 'batch_size': 'Files per task', + 'is_auto_submit': 'Auto submit' } + 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.", + 'predecessor_specification': "The related predecessor of the current work specification.", + '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." + } diff --git a/ldvspec/lofardata/migrations/0010_dataproductfilter_help_text.py b/ldvspec/lofardata/migrations/0010_dataproductfilter_help_text.py new file mode 100644 index 0000000000000000000000000000000000000000..39e7ea559bdd1c2f109a7b67cecbac297eab2faa --- /dev/null +++ b/ldvspec/lofardata/migrations/0010_dataproductfilter_help_text.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2 on 2022-11-11 15:40 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('lofardata', '0009_auto_20221110_1307'), + ] + + operations = [ + migrations.AddField( + model_name='dataproductfilter', + name='help_text', + field=models.CharField(blank=True, default='', max_length=150), + ), + ] diff --git a/ldvspec/lofardata/migrations/0011_workspecification_selected_workflow_tag.py b/ldvspec/lofardata/migrations/0011_workspecification_selected_workflow_tag.py new file mode 100644 index 0000000000000000000000000000000000000000..2e2737c9f0249dc7b734cccffb0a33e3742f203d --- /dev/null +++ b/ldvspec/lofardata/migrations/0011_workspecification_selected_workflow_tag.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2 on 2022-12-07 11:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('lofardata', '0010_dataproductfilter_help_text'), + ] + + operations = [ + migrations.AddField( + model_name='workspecification', + name='selected_workflow_tag', + field=models.CharField(default='Unknown', max_length=500, null=True), + ), + ] diff --git a/ldvspec/lofardata/migrations/0012_merge_20221213_1102.py b/ldvspec/lofardata/migrations/0012_merge_20221213_1102.py new file mode 100644 index 0000000000000000000000000000000000000000..e6ea54cc063833117a115df74634d6b4bfd82f35 --- /dev/null +++ b/ldvspec/lofardata/migrations/0012_merge_20221213_1102.py @@ -0,0 +1,14 @@ +# Generated by Django 3.2 on 2022-12-13 11:02 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('lofardata', '0010_alter_dataproduct_surl'), + ('lofardata', '0011_workspecification_selected_workflow_tag'), + ] + + operations = [ + ] diff --git a/ldvspec/lofardata/models.py b/ldvspec/lofardata/models.py index b2515eeec301ce26d7048284bf3786b777734f62..7f0ffbe595a8094cc3fa87835df1f4d2c5433294 100644 --- a/ldvspec/lofardata/models.py +++ b/ldvspec/lofardata/models.py @@ -96,6 +96,7 @@ class DataFilterType(models.TextChoices): class DataProductFilter(models.Model): field = models.CharField(max_length=100) name = models.CharField(max_length=20) + help_text = models.CharField(max_length=150, blank=True, default="") lookup_type = models.CharField(max_length=100) filter_type = models.CharField(max_length=20, choices=DataFilterType.choices, default=DataFilterType.FREEFORM) @@ -145,6 +146,7 @@ class WorkSpecification(models.Model): # ATDB Workflow URL selected_workflow = models.CharField(max_length=500, null=True) + selected_workflow_tag = models.CharField(max_length=500, null=True, default="Unknown") # Task ID's that were created in ATDB related_tasks = ArrayField(models.IntegerField(), null=True) @@ -166,6 +168,11 @@ class WorkSpecification(models.Model): max_length=16, choices=PURGE_POLICY.choices, default=PURGE_POLICY.NO ) + def created_by_display_value(self): + if self.created_by is None: + return "Unknown" + return self.created_by.get_full_name() if self.created_by.get_full_name() else self.created_by + def __str__(self): return str(self.id) + ' - ' + str(self.filters) + " (" + str(self.created_on) + ")" diff --git a/ldvspec/lofardata/static/histogram_amcharts.js b/ldvspec/lofardata/static/histogram_amcharts.js new file mode 100644 index 0000000000000000000000000000000000000000..61e0a5aced9e29b51786053d6a293c7b653d21a9 --- /dev/null +++ b/ldvspec/lofardata/static/histogram_amcharts.js @@ -0,0 +1,103 @@ +am5.ready(function () { + +// Create root element +// https://www.amcharts.com/docs/v5/getting-started/#Root_element + var root = am5.Root.new("chartdiv"); + + +// Set themes +// https://www.amcharts.com/docs/v5/concepts/themes/ + root.setThemes([ + am5themes_Animated.new(root) + ]); + + +// Create chart +// https://www.amcharts.com/docs/v5/charts/xy-chart/ + var chart = root.container.children.push(am5xy.XYChart.new(root, { + panX: true, + panY: true, + wheelX: "panX", + wheelY: "zoomX", + pinchZoomX: true + })); + +// Add cursor +// https://www.amcharts.com/docs/v5/charts/xy-chart/cursor/ + var cursor = chart.set("cursor", am5xy.XYCursor.new(root, {})); + cursor.lineY.set("visible", false); + +// Create axes +// https://www.amcharts.com/docs/v5/charts/xy-chart/axes/ + var xRenderer = am5xy.AxisRendererX.new(root, {minGridDistance: 30}); + xRenderer.labels.template.setAll({ + rotation: -90, + centerY: am5.p50, + centerX: am5.p100, + paddingRight: 15 + }); + + var xAxis = chart.xAxes.push(am5xy.CategoryAxis.new(root, { + maxDeviation: 0.3, + categoryField: xaxis_key, //set in accompanying template + renderer: xRenderer, + tooltip: am5.Tooltip.new(root, {}), + })); + + xAxis.children.push( + am5.Label.new(root, { + text: xaxis_header, + x: am5.p50, + }) + ); + + var yAxis = chart.yAxes.push(am5xy.ValueAxis.new(root, { + min: 0, + max: y_max, + strictMinMax: true, + renderer: am5xy.AxisRendererY.new(root, {}), + })); + + yAxis.children.unshift( + am5.Label.new(root, { + rotation: -90, + text: yaxis_header, + y: am5.p50, + centerX: am5.p50, + }) + ); + + +// Create series +// https://www.amcharts.com/docs/v5/charts/xy-chart/series/ + var series = chart.series.push(am5xy.ColumnSeries.new(root, { + name: "Series 1", + xAxis: xAxis, + yAxis: yAxis, + valueYField: yaxis_key, //set in accompanying template + sequencedInterpolation: true, + categoryXField: xaxis_key, //set in accompanying template + tooltip: am5.Tooltip.new(root, { + labelText: "{valueY}" + }) + })); + + series.columns.template.setAll({cornerRadiusTL: 5, cornerRadiusTR: 5}); + series.columns.template.adapters.add("fill", function (fill, target) { + return chart.get("colors").getIndex(series.columns.indexOf(target)); + }); + + series.columns.template.adapters.add("stroke", function (stroke, target) { + return chart.get("colors").getIndex(series.columns.indexOf(target)); + }); + +// Set data; defined in accompanying template + xAxis.data.setAll(data); + series.data.setAll(data); + + +// Make stuff animate on load +// https://www.amcharts.com/docs/v5/concepts/animations/ + series.appear(1000); + chart.appear(1000, 100); +}); \ No newline at end of file diff --git a/ldvspec/lofardata/static/lofardata/styling/dias.css b/ldvspec/lofardata/static/lofardata/styling/dias.css index d1b6b292f7267382f3b93127abebba6d5a84b957..85987a13fd531e78542bffa48561e895fb031043 100644 --- a/ldvspec/lofardata/static/lofardata/styling/dias.css +++ b/ldvspec/lofardata/static/lofardata/styling/dias.css @@ -1035,21 +1035,16 @@ input.section-toggle:checked + label:before { /* Text styles -------------------------------------------------------*/ html { - font-family: Raleway; - font-size: 16px; + font-family: Raleway, sans-serif; + font-size: 20px; line-height: 1.5rem; color: var(--text); } -@supports (font-feature-settings: normal) { - html { - font-feature-settings: "ss02" on; - } -} @media (max-width: 1440px) { html { - font-size: 14px; + font-size: 18px; } } @@ -2671,6 +2666,7 @@ input.section-toggle-invert:checked ~ .section-wrapper { margin: 0.5rem 0 0; font-size: 14px; display: block; + min-width: fit-content; } .input__label--hidden { @@ -3042,7 +3038,7 @@ input.section-toggle-invert:checked ~ .section-wrapper { } .badge { - --badge-height: 16px; + --badge-height: 20px; background-color: var(--text); color: white; border-radius: calc(var(--badge-height) / 2); @@ -3055,7 +3051,7 @@ input.section-toggle-invert:checked ~ .section-wrapper { text-align: center; justify-content: center; align-items: center; - font-size: 12px; + font-size: 16px; font-weight: 500; } @@ -3691,8 +3687,42 @@ input.section-toggle-invert:checked ~ .section-wrapper { .custom-atdb-task-modal { min-height: 15rem; + min-width: 50rem; } .custom-div-margin { margin: 1rem; +} + +.custom-tooltip { + font-size: 0.75rem; + margin-top: calc(var(--spacing) * 0.125); +} + +/*We reset the dias checkbox style because the checkbox label limits inheritance*/ +.custom-checkbox[type="checkbox"]:checked { + width: auto; + position: relative; + display: inline-block; + left: 0; +} + +.custom-checkbox[type="checkbox"]:not(:checked) { + width: auto; + position: relative; + display: inline-block; + left: 0; +} + +.custom-chart-styling { + height: 35rem; + width: 50rem; +} + +.custom-input-selection { + min-width: 9rem; +} + +.custom-input-filters { + min-width: 4rem; } \ No newline at end of file diff --git a/ldvspec/lofardata/static/update_workflow.js b/ldvspec/lofardata/static/update_workflow.js index 06861df55b0bf6b51082b777817683988049fdd7..7237bdbb5160d503a9105dd4ed66bc15335dc3fa 100644 --- a/ldvspec/lofardata/static/update_workflow.js +++ b/ldvspec/lofardata/static/update_workflow.js @@ -1,37 +1,75 @@ -function fetchAvailableWorkflows(ATDBurl) { - fetch(ATDBurl + 'workflows').then((response) => response.json()).then((data) => { - $.each(data.results, function (key, value) { - let workflowDropdownOption = $('<option>', {value: value.workflow_uri}).text(value.workflow_uri); - $('#id_selected_workflow').append(workflowDropdownOption); - }); - }).then(() => { - $('#id_selected_workflow').val($("#id_selected_workflow option:first").val()); - }) -} - - -function updateWorkflows(selectedProcessingSite) { - $('#id_selected_workflow').empty() - if (selectedProcessingSite === '') return - let processingSiteUrl = processingSite.replace('replace', selectedProcessingSite); - fetch(processingSiteUrl).then( - (response) => response.json()).then( - (data) => fetchAvailableWorkflows(data.url) - ) -} - -function selectedProcessingSite(event) { - let site = event.currentTarget.value - updateWorkflows(site); -} - -function onReady() { - let processingSite = $("#id_processing_site"); //#TODO: fix proc site initial on update - let initialProcessingSite = processingSite[0].value; - if (initialProcessingSite) { - updateWorkflows(initialProcessingSite) - } - processingSite.on('change', selectedProcessingSite); -} - -$(document).ready(onReady); \ No newline at end of file + (function() { + document.addEventListener("DOMContentLoaded", function(event) { + ko.applyBindings(new SpecificationViewModel()); + }); + + function SpecificationViewModel() { + var self = this; + + self.existing_workflow = existingWorkflow; + self.existing_workflow_tag = existingWorkflowTag; + + self.processingSites = ko.observableArray( + processingSites + ); + + self.selectedProcessingSite = ko.observable(existingProcessingSite); + + self.selectedProcessingSite.subscribe((context) => { + self.loadWorkflows(); + }); + + self.loadWorkflows = function() { + + const selectedProcessingSite = self.selectedProcessingSite(); + if (selectedProcessingSite) { + const processingSiteUrl = processingSite.replace('replace', selectedProcessingSite); + self.isLoading(true); + + fetch(processingSiteUrl).then((response) => response.json()).then((data) => { + const workflowUrl = data.url + 'workflows'; + + fetch(workflowUrl).then((response) => response.json()).then((data) => { + + const workflows = data.results; + self.workflows(workflows); + self.selectedTag(self.existing_workflow_tag); + self.selectedWorkflow(self.existing_workflow); + + }).finally(() => { + self.isLoading(false); + }); + }).catch(() => { + self.isLoading(false); + }); + } else { + self.workflows([]); + self.selectedWorkflow(null); + self.selectedTag(null); + } + } + + self.isLoading = ko.observable(false); + + self.caption = ko.computed(() => { + return self.isLoading() ? 'Loading...' : '---------' + }); + + self.workflows = ko.observableArray(); + + self.workflowsByTag = ko.computed(() => { + const byTag = self.workflows().filter(workflow => workflow.description === self.selectedTag()); + return byTag; + }); + + self.tags = ko.computed(() => { + const tags = self.workflows().map(workflow => workflow.description); + return [...new Set(tags)]; + }); + + self.selectedTag = ko.observable(); + self.selectedWorkflow = ko.observable(); + + self.loadWorkflows(); + } +})(); \ No newline at end of file diff --git a/ldvspec/lofardata/templates/lofardata/base.html b/ldvspec/lofardata/templates/lofardata/base.html index abf4ce143f5e7a76e909d4a23455c487d0c7c396..1bfaf1a3a42a9aef2e89d76785f943a6e2f043b1 100644 --- a/ldvspec/lofardata/templates/lofardata/base.html +++ b/ldvspec/lofardata/templates/lofardata/base.html @@ -15,7 +15,6 @@ <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.4.2/css/all.css" integrity="sha384-/rXc/GQVaYpyDdyxK+ecHPVYJSN9bmVFBvjA/9eOB+pb3F2w2N6fc5qB9Ew5yIns" crossorigin="anonymous"> - <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script> <link rel="stylesheet" type="text/css" href="{% static 'lofardata/styling/dias.css' %}"> diff --git a/ldvspec/lofardata/templates/lofardata/index.html b/ldvspec/lofardata/templates/lofardata/index.html index 5e0a99b669efc9588417fb6965bc4d661d491377..95d1532297120e94eedb929b0590e9a574b12e0c 100644 --- a/ldvspec/lofardata/templates/lofardata/index.html +++ b/ldvspec/lofardata/templates/lofardata/index.html @@ -28,9 +28,17 @@ <a class="tooltip-dias tooltip-dias-bottom" data-tooltip="The ATDB site where it is processed">m</a> </div> + <div class="table__cell">Created By + <a class="tooltip-dias tooltip-dias-bottom" + data-tooltip="The user who created the specification">m</a> + </div> <div class="table__cell">Workflow <a class="tooltip-dias tooltip-dias-bottom" data-tooltip="The pipeline">m</a> </div> + <div class="table__cell">Tag + <a class="tooltip-dias tooltip-dias-bottom" + data-tooltip="The category to which the workflow belongs ">m</a> + </div> <div class="table__cell table__cell--medium-small-fixed">Successors</div> <div class="table__cell table__cell--medium-small-fixed">Predecessors</div> <div class="table__cell">Actions</div> @@ -58,7 +66,7 @@ {% if specification.get_submission_status_display == "undefined" or specification.get_submission_status_display == "not submitted" %} {% define "primary" as badgecolor %} {% endif %} - <div class="badge badge--{{ badgecolor }}">{{ specification.get_submission_status_display }}</div> + <div class="badge badge--{{ badgecolor }} margin-top">{{ specification.get_submission_status_display }}</div> </div> <div class="table__cell table__cell--truncate"> <a href="{{ specification.processing_site.url }}" @@ -67,8 +75,11 @@ <span class="icon icon--external-link-alt margin-left"></span> </a> </div> + <div class="table__cell table__cell--truncate">{{ specification.created_by_display_value }}</div> <div class="table__cell table__cell--truncate">{{ specification.selected_workflow }}</div> + <div class="table__cell table__cell--truncate">{{ specification.selected_workflow_tag }}</div> + <div class="table__cell table__cell--medium-small-fixed"> {% if specification.successor.count %} @@ -94,9 +105,9 @@ <!-- Actions --> <div class="table__cell"> <div class="flex-wrapper flex-wrapper--row"> - <a class="button--icon-button margin-left margin-right" - href="{% url 'specification-update' pk=specification.pk %}"> + href="{% url 'specification-update' pk=specification.pk %}" + title="Edit this work specification"> <span class="icon icon--pen"></span> </a> @@ -107,15 +118,13 @@ </a> {% else %} <a class="link--red button--icon-button margin-right" - href="{% url 'specification-delete' pk=specification.pk %}"> + href="{% url 'specification-delete' pk=specification.pk %}" + title="Delete this work specification"> <span class="icon icon--color-inherit icon--trash-alt"></span> </a> {% endif %} - <form - method="post" - action="{% url 'workspecification-submit' pk=specification.pk %}" - > + <form method="post" action="{% url 'workspecification-submit' pk=specification.pk %}"> {% csrf_token %} {% if specification.get_submission_status_display == "submitted" or specification.get_submission_status_display == "defining" %} @@ -123,6 +132,11 @@ title="Tasks already present in ATDB"> <span class="icon icon--color-inherit icon--play"></span> </a> + {% elif specification.inputs.surls|length == 0 %} + <a class="link--disabled button--icon-button" + title="Cannot submit this task to ATDB since it does not contain any files"> + <span class="icon icon--color-inherit icon--play"></span> + </a> {% else %} <button type="submit" class="button--icon-button" title="Submit to ATDB"> <span class="icon icon--play"></span> @@ -143,8 +157,4 @@ <h2>Log in to see/add/update work specifications</h2> </div> {% endif %} - <!-- <script> - $(document).ready(() => setInterval(() => window.location.reload(), 50000)) - </script> --> - {% endblock %} diff --git a/ldvspec/lofardata/templates/lofardata/workspecification/create_update.html b/ldvspec/lofardata/templates/lofardata/workspecification/create_update.html index 0796773718cd9188f116a2c68b91e886e2e3fc04..c578f642e856a29a0cfdb8bbf60d77670286290d 100644 --- a/ldvspec/lofardata/templates/lofardata/workspecification/create_update.html +++ b/ldvspec/lofardata/templates/lofardata/workspecification/create_update.html @@ -3,6 +3,7 @@ {% load crispy_forms_tags %} {% load define_action %} {% load widget_tweaks %} +{% load json %} {% block myBlock %} <div class="overlay"> @@ -20,47 +21,108 @@ {% csrf_token %} <div class="flex-wrapper flex-wrapper--row flex-wrapper--start"> + <!-- Standard input fields --> <div class="custom-div-margin"> <h3 class="text text--primary text--title">Selection</h3> <div class="flex-wrapper flex-wrapper--row" id="div_id_processing_site"> - <label class="input__label">{{ form.processing_site.label }}</label> + <div class="flex-wrapper flex-wrapper--row custom-input-selection"> + <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"> - {% render_field form.processing_site class="input maxwidth-input input--select margin-left margin-bottom" %} + <select class="input input--select margin-left margin-bottom" name="processing_site" + data-bind="options:processingSites, + optionsCaption: '---------', + optionsText: function(item) { return item.name + ' - ' + item.url}, + optionsValue: 'name', + value: selectedProcessingSite"></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-selection"> + <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 margin-bottom margin-left" id="id_selected_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"> - <label class="input__label" - for="id_selected_workflow">{{ form.selected_workflow.label }}*</label> - <div class="input-select-wrapper icon--inline icon-after icon-after--angle-down margin-left margin-bottom"> - <select class="input input--select" id="id_selected_workflow" - name="selected_workflow"> - <option disabled value="---" selected>Select a processor first</option> + <div class="flex-wrapper flex-wrapper--row custom-input-selection"> + <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 margin-bottom margin-left" id="id_selected_workflow" name="selected_workflow" style="width: 12rem" + data-bind="options:workflowsByTag, + optionsCaption: caption, + optionsValue: 'workflow_uri', + optionsText: 'workflow_uri', + value: selectedWorkflow, + disable: isLoading"> </select> </div> </div> + <div class="flex-wrapper flex-wrapper--row" id="div_id_predecessor"> - <label class="input__label" - for="id_predecessor">{{ form.predecessor_specification.label }}</label> + <div class="flex-wrapper flex-wrapper--row custom-input-selection"> + <label class="input__label" + for="id_predecessor">{{ form.predecessor_specification.label }}</label> + <a class="tooltip-dias tooltip-dias-right custom-tooltip" + data-tooltip="{{ form.predecessor_specification.help_text }}">m</a> + </div> <div class="input-select-wrapper icon--inline icon-after icon-after--angle-down margin-left margin-bottom"> - {% render_field form.predecessor_specification class="input maxwidth-input input--select" %} + {% render_field form.predecessor_specification class="input input--select" %} </div> </div> <div class="flex-wrapper flex-wrapper--row" id="div_id_batch_size"> - <label class="input__label" - for="id_batch_size">{{ form.batch_size.label_tag }}</label> - {% render_field form.batch_size class="input maxwidth-input input--select margin-left margin-bottom" type="number" %} + <div class="flex-wrapper flex-wrapper--row custom-input-selection"> + <label class="input__label" + for="id_batch_size">{{ form.batch_size.label_tag }}</label> + <a class="tooltip-dias tooltip-dias-right custom-tooltip" + data-tooltip="{{ form.batch_size.help_text }}">m</a> + </div> + {% render_field form.batch_size class="input input--select margin-left margin-bottom" type="number" %} + </div> + + <div class="flex-wrapper flex-wrapper--row" id="div_id_auto_submit"> + {% render_field form.is_auto_submit class="input custom-checkbox margin-top" type="checkbox" %} + <label class="input__label">{{ form.is_auto_submit.label }}</label> + <a class="tooltip-dias tooltip-dias-right custom-tooltip" + data-tooltip="{{ form.is_auto_submit.help_text }}">m</a> </div> </div> + <!-- Filter fields --> <div class="custom-div-margin"> <h3 class="text text--primary text--title">Filters*</h3> {% for filter in filters %} {% if filter.filter_type == 'Free' %} <div class="flex-wrapper"> - <label class="input__label" - for="id_{{ filter.field }}">{{ filter.name }}</label> + <div class="flex-wrapper flex-wrapper--row custom-input-filters"> + <label class="input__label" + for="id_{{ filter.field }}">{{ filter.name }}</label> + {% if filter.help_text %} + <a class="tooltip-dias tooltip-dias-right custom-tooltip" + data-tooltip="{{ filter.help_text }}">m</a> + {% endif %} + </div> <input class="input input--text margin-left margin-bottom" id="id_{{ filter.field }}" name="{{ filter.field }}" placeholder="Enter {{ filter.name }}" type="text" @@ -70,13 +132,21 @@ {% elif filter.filter_type == 'Dropdown' %} <div class="flex-wrapper flex-wrapper--row" id="div_id_filter_{{ filter.name }}"> - <label class="input__label" - for="id_{{ filter.field }}">{{ filter.name }}</label> + <div class="flex-wrapper flex-wrapper--row custom-input-filters"> + <label class="input__label" + for="id_{{ filter.field }}">{{ filter.name }}</label> + {% if filter.help_text %} + <a class="tooltip-dias tooltip-dias-right custom-tooltip" + data-tooltip="{{ filter.help_text }}">m</a> + {% endif %} + </div> <div class="input-select-wrapper icon--inline icon-after icon-after--angle-down margin-left margin-bottom"> <select class="input input--select" id="id_{{ filter.field }}" name="{{ filter.field }}" - data-filter="{{ filter.lookup_type }}"> + data-filter="{{ filter.lookup_type }}" + style="width: 12rem"> + {#for some reason, styling is not applied otherwise#} {% for option in filter.choices %} {% if filter.default == option.0 %} <option selected @@ -100,7 +170,18 @@ type="submit" name="action" value="Submit" - title="Submit the task to inspect the result. Send it later to ATDB.">Submit + 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> <button class="button button--primary margin-right" type="submit" value="Successor" @@ -128,8 +209,13 @@ </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"> - var processingSite = "{% url 'processingsite-detail' 'replace' %}" + 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/workspecification/dataproducts.html b/ldvspec/lofardata/templates/lofardata/workspecification/dataproducts.html new file mode 100644 index 0000000000000000000000000000000000000000..e10def25bc83ef2039b859cb0d1dcac7b42322db --- /dev/null +++ b/ldvspec/lofardata/templates/lofardata/workspecification/dataproducts.html @@ -0,0 +1,50 @@ +{% extends 'lofardata/index.html' %} +{% load static %} +{% load crispy_forms_tags %} +{% load define_action %} +{% load widget_tweaks %} + +{% block myBlock %} + <div class="overlay"> + <div class="modal-dias-wrapper"> + <div class="modal-dias modal-dias--fit-content custom-atdb-task-modal"> + <a class="icon icon--times button--close" href="{% url 'specification-detail' pk %}"></a> + {% if dataproduct_info %} + <header class="flex-wrapper flex-wrapper--centered flex-wrapper--column"> + <h2 class="title">Dataproduct information of SAS ID {{ sas_id }} + <a class="tooltip-dias tooltip-dias-bottom" + data-tooltip="The general data product information for this work specification based on the retrieved information from the SAS ID">m</a> + </h2> + </header> + <div class="table margin-bottom"> + <div class="table__header"> + <div class="table__row table__row--dark table__row--padding"> + <div class="table__cell">Property</div> + <div class="table__cell">Value</div> + </div> + </div> + <div class="table__content"> + {% for key, value in dataproduct_info.items %} + <div class="table__row table__row--dark table__row--padding"> + <div class="table__cell table__cell--title">{{ key }}</div> + <div class="table__cell"> + {% for item in value %} + {{ item }} + {% if not forloop.last %}&{% endif %} + {% endfor %} + </div> + </div> + {% endfor %} + </div> + </div> + {% else %} + <div class="flex-wrapper flex-wrapper--centered flex-wrapper--column"> + <p class="text--red">The information is not present.</p> + <p>This information is directly linked to given SAS ID which is incorrect. </p> + <p class="text note-text">Please notify 'lautenbach@astron.nl' if this occurs.</p> + </div> + {% endif %} + </div> + </div> + </div> +{% endblock %} \ No newline at end of file diff --git a/ldvspec/lofardata/templates/lofardata/workspecification/dataset_size_info.html b/ldvspec/lofardata/templates/lofardata/workspecification/dataset_size_info.html new file mode 100644 index 0000000000000000000000000000000000000000..f8ac2d6777befe21dea2a06bf8dd53dbce9f1788 --- /dev/null +++ b/ldvspec/lofardata/templates/lofardata/workspecification/dataset_size_info.html @@ -0,0 +1,54 @@ +{% extends 'lofardata/index.html' %} +{% load static %} +{% load crispy_forms_tags %} +{% load define_action %} +{% load widget_tweaks %} + +{% block myBlock %} + <div class="overlay"> + <div class="modal-dias-wrapper"> + <div class="modal-dias modal-dias--fit-content custom-atdb-task-modal"> + <a class="icon icon--times button--close" href="{% url 'specification-detail' object.pk %}"></a> + <header class="flex-wrapper flex-wrapper--centered flex-wrapper--column"> + <h2 class="title">Size distributions + <a class="tooltip-dias tooltip-dias-bottom" + data-tooltip="The size distribution of the input files of this work specification">m</a> + </h2> + </header> + <div class="custom-chart-styling" id="chartdiv"></div> + <div class="flex-wrapper flex-wrapper--row flex-wrapper--space-between"> + <p>Minimum: {{ min_size }}</p> + <p>Maximum: {{ max_size }}</p> + </div> + </div> + </div> + </div> + <!-- Amcharts --> + + <!-- Data retriever --> + <script> + const y_max = {{ biggest_bucket }}; + const xaxis_key = "file_size"; + const xaxis_header = "File size"; + const yaxis_key = "total_number_files"; + const yaxis_header = "Number of files"; + const xaxis_values = {{ bins|safe }}; + const yaxis_values = {{ counts|safe }}; + + <!-- Construct the data the way AMCharts needs it --> + let data = []; + for (let i = 0; i < {{ n_bins }}; i++) { + let entry = {} + entry[xaxis_key] = (xaxis_values[i - 1] ? xaxis_values[i - 1] : 0) + " - " + xaxis_values[i] + entry[yaxis_key] = yaxis_values[i] + data[i] = entry + } + </script> + <!-- Resources --> + <script src="https://cdn.amcharts.com/lib/5/index.js"></script> + <script src="https://cdn.amcharts.com/lib/5/xy.js"></script> + <script src="https://cdn.amcharts.com/lib/5/themes/Animated.js"></script> + <!-- Script code --> + <script src="{% static 'histogram_amcharts.js' %}"></script> + +{% endblock %} diff --git a/ldvspec/lofardata/templates/lofardata/workspecification/detail.html b/ldvspec/lofardata/templates/lofardata/workspecification/detail.html index 77630b328e67c3c72c5812b05a0121880dd8b89a..b2d7784dc3b48cddba0d992b310e5296a86a1b0e 100644 --- a/ldvspec/lofardata/templates/lofardata/workspecification/detail.html +++ b/ldvspec/lofardata/templates/lofardata/workspecification/detail.html @@ -15,7 +15,7 @@ <span class="margin-top">Work Specification {{ object.pk }}</span> <a class="button button--secondary button--icon-button margin-left margin-right" - href="{% url 'specification-update' pk=object.pk %}"> + href="{% url 'specification-update' pk=object.pk %}" title="Edit this work specification"> <span class="icon icon--pen"></span> </a> @@ -26,33 +26,33 @@ </a> {% else %} <a class="button button--secondary button--red button--icon-button margin-right" - href="{% url 'specification-delete' pk=object.pk %}"> + href="{% url 'specification-delete' pk=object.pk %}" + title="Delete this work specification"> <span class="icon icon--trash-alt"></span> </a> {% endif %} - <form - method="post" - action="{% url 'workspecification-submit' pk=object.pk %}" - > + <form method="post" action="{% url 'workspecification-submit' pk=object.pk %}"> {% csrf_token %} {% if object.get_submission_status_display == "submitted" or object.get_submission_status_display == "defining" %} - <a - class="button button--icon-button button--primary button--disabled" - title="Tasks already present in ATDB" - href="#" - > - <span class="icon icon--play"></span> + <a class="button button--icon-button button--primary button--disabled" + title="Tasks already present in ATDB" + href="#"> + <span class="icon icon--play"></span> + </a> + {% elif number_of_files == 0 %} + <a class="button button--icon-button button--primary button--disabled" + title="No files to process in ATDB" + href="#"> + <span class="icon icon--play"></span> </a> {% else %} - <button - type="submit" - class="button button--secondary button--icon-button" - title="Submit to ATDB" - > - <span class="icon icon--play"></span> - </button> + <button type="submit" + class="button button--secondary button--icon-button" + title="Submit to ATDB"> + <span class="icon icon--play"></span> + </button> {% endif %} </form> @@ -137,60 +137,96 @@ <div class="table__cell"> {{ object.filters }}</div> </div> - <div class="table__row table__row--dark table__row--padding"> - <div class="table__cell table__cell--title">Files per task</div> - <div class="table__cell">{{ object.batch_size }}</div> - </div> - <div class="table__row table__row--dark table__row--padding"> <div class="table__cell table__cell--title">Purge policy</div> <div class="table__cell">{{ object.purge_policy }}</div> </div> {% if object.is_ready or object.is_defined %} - <div class="table__row table__row--dark table__row--padding"> - <div class="table__cell table__cell--title">Total size</div> - <div class="table__cell">{{ total_input_size }}</div> - </div> - - <div class="table__row table__row--dark table__row--padding"> - <div class="table__cell table__cell--title">Size per task</div> - <div class="table__cell">{{ size_per_task }}</div> - </div> - - <div class="table__row table__row--dark table__row--padding"> - <div class="table__cell table__cell--title">Number of files</div> - <div class="table__cell">{{ number_of_files }}</div> - </div> - <div class="table__row table__row--dark table__row--padding"> <div class="table__cell table__cell--title">Async Task Result</div> <div class="table__cell">{{ object.async_task_result }}</div> </div> + <div class="table__row table__row--dark table__row--padding"> - <div class="table__cell table__cell--title">Inputs</div> + <div class="table__cell table__cell--title">Data Product Information</div> <div class="table__cell"> <a class="button button--icon-button custom-button-no-padding" - href="{% url 'specification-inputs' object.pk %}"> - <span class="icon icon--bars"></span> + href="{% url 'specification-dataproducts' object.pk %}"> + <span class="icon icon--file-alt"></span> </a> </div> </div> <div class="table__row table__row--dark table__row--padding"> - <div class="table__cell table__cell--title">ATDB Tasks</div> - <div class="table__cell"> - {% if object.related_tasks %} - <a class="button button--icon-button custom-button-no-padding" - href="{% url 'specification-tasks' object.pk %}"> - <span class="icon icon--list-ul"></span> - </a> + <div class="table__cell table__cell--title">Number of files</div> + {% if number_of_files == 0 %} + <div class="table__cell"> + <a class="tooltip-dias tooltip-dias-right tooltip-dias--without-margin tooltip-dias--red " + data-tooltip="No files found for this SAS ID">m</a> + </div> + {% else %} + <div class="table__cell">{{ number_of_files }}</div> + {% endif %} + </div> + + + {% if number_of_files > 0 %} + <div class="table__row table__row--dark table__row--padding"> + <div class="table__cell table__cell--title">Files per task</div> + {% if object.batch_size == 0 %} + <div class="table__cell">{{ number_of_files }}</div> {% else %} - <div class="text">-</div> + <div class="table__cell">{{ object.batch_size }}</div> {% endif %} </div> - </div> + + <div class="table__row table__row--dark table__row--padding"> + <div class="table__cell table__cell--title">Total size</div> + <div class="table__cell">{{ total_input_size }}</div> + </div> + + <div class="table__row table__row--dark table__row--padding"> + <div class="table__cell table__cell--title">Size per task</div> + <div class="table__cell">{{ size_per_task }}</div> + </div> + + <div class="table__row table__row--dark table__row--padding"> + <div class="table__cell table__cell--title">Dataset size distribution</div> + <div class="table__cell"> + <a class="button button--icon-button custom-button-no-padding" + href="{% url 'dataset-size-info' object.pk %}"> + <span class="icon icon--chart-bar"></span> + </a> + </div> + </div> + + <div class="table__row table__row--dark table__row--padding"> + <div class="table__cell table__cell--title">Inputs</div> + <div class="table__cell"> + <a class="button button--icon-button custom-button-no-padding" + href="{% url 'specification-inputs' object.pk %}"> + <span class="icon icon--bars"></span> + </a> + </div> + </div> + + <div class="table__row table__row--dark table__row--padding"> + <div class="table__cell table__cell--title">ATDB Tasks</div> + <div class="table__cell"> + {% if object.related_tasks %} + <a class="button button--icon-button custom-button-no-padding" + href="{% url 'specification-tasks' object.pk %}"> + <span class="icon icon--list-ul"></span> + </a> + {% else %} + <div class="text">-</div> + {% endif %} + </div> + </div> + {% endif %} + {% endif %} </div> </div> @@ -198,7 +234,4 @@ </div> </div> </div> - <script> - $(document).ready(() => setTimeout(() => window.location.reload(), 5000)) - </script> {% endblock %} diff --git a/ldvspec/lofardata/templates/lofardata/workspecification/test.json b/ldvspec/lofardata/templates/lofardata/workspecification/test.json new file mode 100644 index 0000000000000000000000000000000000000000..dfd867d5ff69468546838dead974dbd8df8d772f --- /dev/null +++ b/ldvspec/lofardata/templates/lofardata/workspecification/test.json @@ -0,0 +1,108 @@ +{ + "count": 9, + "next": null, + "previous": null, + "results": [ + { + "id": 1, + "description": null, + "workflow_uri": "my_great_workflow_version_1_0", + "repository": "https://git.astron.nl/ldv/compress_pipeline.git", + "commit_id": "master", + "path": "workflow/compress.cwl", + "oi_size_fraction": null, + "meta_scheduling": null, + "default_parameters": null + }, + { + "id": 2, + "description": null, + "workflow_uri": "imaging_compress_v0.1test", + "repository": "https://git.astron.nl/ldv/imaging_compress_pipeline.git", + "commit_id": "v0.1test", + "path": "compress_pipeline.cwl", + "oi_size_fraction": null, + "meta_scheduling": null, + "default_parameters": null + }, + { + "id": 3, + "description": null, + "workflow_uri": "imaging_compress_pipeline_v011", + "repository": "https://git.astron.nl/ldv/imaging_compress_pipeline.git", + "commit_id": "v0.1.1", + "path": "compress_pipeline.cwl", + "oi_size_fraction": 0.25, + "meta_scheduling": null, + "default_parameters": null + }, + { + "id": 4, + "description": null, + "workflow_uri": "ldv_test", + "repository": "https://git.astron.nl/ldv/imaging_compress_pipeline.git", + "commit_id": "v0.1.11", + "path": "download_and_compress_pipeline.cwl", + "oi_size_fraction": 0.4, + "meta_scheduling": null, + "default_parameters": null + }, + { + "id": 6, + "description": null, + "workflow_uri": "prefactor3_calibrator", + "repository": "https://git.astron.nl/eosc/prefactor3-cwl", + "commit_id": "ldv_v01", + "path": "workflows/ldv_prefactor_calibrator.cwl", + "oi_size_fraction": 1.0, + "meta_scheduling": null, + "default_parameters": null + }, + { + "id": 7, + "description": null, + "workflow_uri": "solar_bf_compress_v01", + "repository": "https://git.astron.nl/ssw-ksp/solar-bf-compressing.git", + "commit_id": "v0.3.1", + "path": "pipeline/cwl/workflow/beamformed_compression.cwl", + "oi_size_fraction": 1.0, + "meta_scheduling": null, + "default_parameters": null + }, + { + "id": 8, + "description": null, + "workflow_uri": "prefactor3_target", + "repository": "https://git.astron.nl/eosc/prefactor3-cwl", + "commit_id": "ldv_v03", + "path": "workflows/ldv_prefactor_target.cwl", + "oi_size_fraction": 1.0, + "meta_scheduling": { + "#SBATCH --cpus-per-task": 20 + }, + "default_parameters": null + }, + { + "id": 9, + "description": null, + "workflow_uri": "dummy", + "repository": "dummy", + "commit_id": "dummy", + "path": "dummy", + "oi_size_fraction": null, + "meta_scheduling": null, + "default_parameters": null + }, + { + "id": 10, + "description": null, + "workflow_uri": "bf_remove_double_tgz", + "repository": "https://git.astron.nl/ldv/bf_double_tgz.git", + "commit_id": "v0.3", + "path": "workflow/download_and_run_bf_remove.cwl", + "oi_size_fraction": 1.0, + "meta_scheduling": null, + "default_parameters": null + } + ] +} \ No newline at end of file diff --git a/ldvspec/lofardata/templates/lofardata/workspecification/update.html b/ldvspec/lofardata/templates/lofardata/workspecification/update.html deleted file mode 100644 index 931a139819f9d1f9607fb8a2236d1f88edb71d1b..0000000000000000000000000000000000000000 --- a/ldvspec/lofardata/templates/lofardata/workspecification/update.html +++ /dev/null @@ -1,101 +0,0 @@ -{% extends 'lofardata/index.html' %} -{% load static %} -{% load crispy_forms_tags %} -{% load define_action %} -{% load widget_tweaks %} - -{% 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"> - <h2 class="title text text--primary">Edit Work Specification {{ object.pk }}</h2> - <form method="post">{% csrf_token %} - <div class="flex-wrapper flex-wrapper--row flex-wrapper--start"> - <div class=""> - <h3 class="text text--primary text--title">Selection</h3> - <div class="flex-wrapper flex-wrapper--row" id="div_id_processing_site"> - <label class="input__label">{{ form.processing_site.label_tag }}*</label> - <div class="input-select-wrapper icon--inline icon-after icon-after--angle-down"> - {% render_field form.processing_site class="input maxwidth-input input--select" %} - </div> - </div> - <div class="flex-wrapper flex-wrapper--row" id="div_id_selected_workflow"> - <label class="input__label" - for="id_selected_workflow">{{ form.selected_workflow.label_tag }}*</label> - <div class="input-select-wrapper icon--inline icon-after icon-after--angle-down"> - <select class="input input--select" id="id_selected_workflow" - name="selected_workflow"> - <option value="---">TODO</option> - </select> - </div> - </div> - <div class="flex-wrapper flex-wrapper--row" id="div_id_predecessor"> - <label class="input__label" - for="id_predecessor">{{ form.predecessor_task.label_tag }}</label> - <input class="input input--text" id="field-name2" placeholder="None" - type="number"> - </div> - </div> - - {% if filters is not None %} - <div class=""> - <h3 class="text text--primary text--title">Filters</h3> - {% for filter in filters %} - {% if filter.filter_type == 'Free' %} - <div class="flex-wrapper"> - <label class="input__label" - for="filter_{{ filter.name }}">{{ filter.name }}</label> - <input class="input input--text margin-left" - id="filter_{{ filter.name }}" - placeholder="Enter {{ filter.name }}" type="text" - data-filter="{{ filter.lookup_type }}" - value="{{ filter.default }}"> - </div> - {% elif filter.filter_type == 'Dropdown' %} - <div class="flex-wrapper flex-wrapper--row" - id="div_id_filter_{{ filter.name }}"> - <label class="input__label" - for="filter_{{ filter.name }}">{{ filter.name }}</label> - <div class="input-select-wrapper icon--inline icon-after icon-after--angle-down"> - <select class="input input--select" - id="filter_{{ filter.name }}" - data-filter="{{ filter.lookup_type }}"> - {% for option in filter.choices %} - {% if filter.default == option.0 %} - <option selected - value="{{ option.0 }}">{{ option.0 }}</option> - {% else %} - <option value="{{ option.0 }}">{{ option.0 }}</option> - {% endif %} - {% endfor %} - </select> - </div> - </div> - {% else %} - <div class="text text--red">Not supported field</div> - {% endif %} - {% endfor %} - </div> - {% endif %} - - - </div> - <div class="flex-wrapper flex-wrapper--centered margin-bottom margin-top"> - <button class="button button--primary margin-right" type="submit" value="Submit">Submit - </button> - <a class="button button--primary margin-left" href="{% url 'index' %}">Cancel</a> - </div> - - </form> - </header> - </div> - </div> - </div> - <script type="text/javascript"> - var processingSite = "{% url 'processingsite-detail' 'replace' %}" - </script> - <script src="{% static 'update_workflow.js' %}"></script> - -{% endblock %} \ No newline at end of file diff --git a/ldvspec/lofardata/templatetags/json.py b/ldvspec/lofardata/templatetags/json.py new file mode 100644 index 0000000000000000000000000000000000000000..7c7bed945b2dee3c8c06ab878f656ebee58b601f --- /dev/null +++ b/ldvspec/lofardata/templatetags/json.py @@ -0,0 +1,10 @@ +from django import template +from django.utils.safestring import mark_safe + +import json + +register = template.Library() + +@register.filter(name='json') +def json_dumps(data): + return mark_safe(json.dumps(data)) \ No newline at end of file diff --git a/ldvspec/lofardata/tests/test_util_funcs.py b/ldvspec/lofardata/tests/test_util_funcs.py index ec2b6e2664f9a1eab850e8cebd64c13c5f39fee0..d1e54032a0143f1040f8f6bdcc8e1214c2fa9136 100644 --- a/ldvspec/lofardata/tests/test_util_funcs.py +++ b/ldvspec/lofardata/tests/test_util_funcs.py @@ -1,7 +1,9 @@ import unittest +import random from lofardata.tasks import split_entries_to_batches -from lofardata.views import compute_size_of_inputs +from lofardata.views import compute_size_of_inputs, compute_inputs_histogram, format_size + class SplitEntries(unittest.TestCase): def test_no_splitting(self): @@ -30,14 +32,66 @@ class SplitEntries(unittest.TestCase): class ComputeInputSizes(unittest.TestCase): def test_input_sizes(self): test_data = { - 'only_a_file': {'class': 'File', 'size': 1}, - 'a_list_of_files': [{'class': 'File', 'size': 1}, {'class': 'File', 'size': 1}], - 'nested_files': [{'item1': {'class': 'File', 'size': 1}}, {'class': 'File', 'size': 1}], + 'only_a_file': {'class': 'File', 'size': 2}, + 'a_list_of_files': [{'class': 'File', 'size': 2}, {'class': 'File', 'size': 5}], + 'nested_files': [{'item1': {'class': 'File', 'size': 1}}, {'class': 'File', 'size': 4}], 'not_a_file': 'bla' } - result, number_of_files, average_file_size = compute_size_of_inputs(test_data) + total_size, number_of_files, average_file_size = compute_size_of_inputs(test_data) - self.assertEqual(5, result) + self.assertEqual(14, total_size) self.assertEqual(5, number_of_files) - self.assertEqual(1.0, average_file_size) + self.assertEqual(2.8, average_file_size) + + +class ComputeInputsHistogram(unittest.TestCase): + def test_basic(self): + test_data = {"surls": [{"size": 33832140800, "surl": "test"}, + {"size": 21832140800, "surl": "test"}, + {"size": 33835408000, "surl": "test"}, + {"size": 53832140800, "surl": "test"}, + {"size": 29832140800, "surl": "test"}, + {"size": 30832140801, "surl": "test"}]} + + min_size, max_size, n_bins, counts, biggest_bucket, bins = compute_inputs_histogram(test_data) + + self.assertEqual(6, n_bins) + self.assertEqual(21832140800, min_size) + self.assertEqual(53832140800, max_size) + self.assertEqual(2, biggest_bucket) + self.assertListEqual([1, 2, 2, 0, 0, 1], counts) + self.assertListEqual(['20.3GB', '25.3GB', '30.3GB', '35.2GB', '40.2GB', '45.2GB', '50.1GB'], bins) + + def test_single_range(self): + test_data = {"surls": [{"size": 1, "surl": "test"}, + {"size": 1, "surl": "test"}, + {"size": 1, "surl": "test"}, + {"size": 1, "surl": "test"}, + {"size": 1, "surl": "test"}, + {"size": 1, "surl": "test"}]} + + min_size, max_size, n_bins, counts, biggest_bucket, bins = compute_inputs_histogram(test_data) + + self.assertEqual(1, n_bins) + self.assertEqual(1, min_size) + self.assertEqual(1, max_size) + self.assertEqual(6, biggest_bucket) + self.assertListEqual([6], counts) + self.assertListEqual(['0.5B', '1.5B'], bins) + + def test_extreme_wide_range(self): + test_data = {"surls": [{"size": 1, "surl": "test"}, + {"size": 100000, "surl": "test"}, + {"size": 100000000, "surl": "test"}, + {"size": 100000000000, "surl": "test"}, + {"size": 1000000000000000, "surl": "test"}, + {"size": 1000000000000000000, "surl": "test"}]} + + min_size, max_size, n_bins, counts, biggest_bucket, bins = compute_inputs_histogram(test_data) + self.assertEqual(6, n_bins) + self.assertEqual(1, min_size) + self.assertEqual(1000000000000000000, max_size) + self.assertEqual(5, biggest_bucket) + self.assertListEqual([5, 0, 0, 0, 0, 1], counts) # TODO: if this is the case, adapt it to logarithmic scale + self.assertListEqual(['1.0B', '148.0PB', '296.1PB', '444.1PB', '592.1PB', '740.1PB', '888.2PB'], bins) diff --git a/ldvspec/lofardata/urls.py b/ldvspec/lofardata/urls.py index 9e9a0f9ba460620f4892c09abdf6f5143fe00b00..98ffebcc4267dc90a198fb643804e72bdef68a16 100644 --- a/ldvspec/lofardata/urls.py +++ b/ldvspec/lofardata/urls.py @@ -39,6 +39,8 @@ urlpatterns = [ path('specification/delete/<int:pk>/', views.WorkSpecificationDeleteView.as_view(), name='specification-delete'), path('specification/inputs/<int:pk>/', views.WorkSpecificationInputsView.as_view(), name='specification-inputs'), 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'), # 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 eb7f5af1745721adb92eb12bd450ef9247e50615..ac610adb35481de71ecd0dd98129d7bb4bd89028 100644 --- a/ldvspec/lofardata/views.py +++ b/ldvspec/lofardata/views.py @@ -1,12 +1,14 @@ +import logging import time from typing import Tuple +import numpy from django.contrib.auth.models import User from django.core.exceptions import ObjectDoesNotExist from django.http import HttpResponseRedirect from django.shortcuts import redirect, render from django.urls import reverse -from django.views.generic import CreateView, DeleteView, DetailView, UpdateView +from django.views.generic import CreateView, DeleteView, DetailView, UpdateView, TemplateView from django.views.generic.list import ListView from django_filters import rest_framework as filters from rest_framework import generics, status, viewsets @@ -39,12 +41,12 @@ def compute_size_of_inputs(inputs: dict) -> Tuple[int, int, int]: number_of_files = 0 if isinstance(inputs, dict) and "size" in inputs: - total_size = inputs["size"] number_of_files = 1 + total_size = inputs["size"] elif ( - isinstance(inputs, dict) - or isinstance(inputs, list) - or isinstance(inputs, tuple) + isinstance(inputs, dict) + or isinstance(inputs, list) + or isinstance(inputs, tuple) ): values = inputs if isinstance(inputs, dict): @@ -55,9 +57,31 @@ def compute_size_of_inputs(inputs: dict) -> Tuple[int, int, int]: number_of_files += item_count average_file_size = total_size / number_of_files if number_of_files else 0 + return total_size, number_of_files, average_file_size +def compute_inputs_histogram(inputs): + # create sizes array + if isinstance(inputs, dict): + inputs = inputs.values() + inputs_sizes = [] + for entry in inputs: + for item in entry: + inputs_sizes.append(item['size']) + inputs_sizes = numpy.array(inputs_sizes) + + # define histogram values + min_size = inputs_sizes.min() + max_size = inputs_sizes.max() + + n_bins = 1 if min_size == max_size else (inputs_sizes.__len__() if inputs_sizes.__len__() < 100 else 100) + counts, buckets = numpy.histogram(inputs_sizes, bins=n_bins, range=(min_size, max_size)) + formatted_bins = [format_size(bucket) % bucket for bucket in buckets] + + return min_size, max_size, n_bins, counts.tolist(), counts.max(), formatted_bins + + def format_size(num, suffix="B"): if num == 0: return "-" @@ -107,9 +131,9 @@ def preprocess_filters_specification_view(specification): dataproduct_filters = DataProductFilter.objects.all() for dataproduct_filter in dataproduct_filters: if ( - specification is not None - and specification.filters - and dataproduct_filter.field in specification.filters + specification is not None + and specification.filters + and dataproduct_filter.field in specification.filters ): dataproduct_filter.default = specification.filters[dataproduct_filter.field] else: @@ -122,6 +146,24 @@ def preprocess_filters_specification_view(specification): return dataproduct_filters +def retrieve_general_dataproduct_information(sas_id): + # Per SAS ID, the retrieved data products should have these unique values + data_products = DataProduct.objects.filter(obs_id=sas_id).values("dataproduct_source", + "dataproduct_type", + "project", + "location", + "activity").distinct() + combined_data_products_on_key = {} + for data_product in data_products: + for key, value in data_product.items(): + if combined_data_products_on_key.get(key) and value not in combined_data_products_on_key.get(key): + combined_data_products_on_key[key].append(value) + else: + combined_data_products_on_key[key] = [value] + + return combined_data_products_on_key + + class Specifications(ListView): serializer_class = WorkSpecificationSerializer template_name = "lofardata/index.html" @@ -155,6 +197,8 @@ class WorkSpecificationCreateUpdateView(UpdateView): except ObjectDoesNotExist: specification = None context["filters"] = preprocess_filters_specification_view(specification) + context["processing_sites"] = list(ATDBProcessingSite.objects.values("name", "url")) + return context def create_successor(self, specification): @@ -226,6 +270,43 @@ class WorkSpecificationATDBTasksView(DetailView): model = WorkSpecification +class WorkSpecificationDatasetSizeInfoView(DetailView): + template_name = "lofardata/workspecification/dataset_size_info.html" + model = WorkSpecification + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + specification = WorkSpecification.objects.get(pk=context["object"].pk) + + min_size, max_size, n_bins, counts, biggest_bucket, bins = compute_inputs_histogram(specification.inputs) + + context["min_size"] = format_size(min_size) + context["max_size"] = format_size(max_size) + context["biggest_bucket"] = biggest_bucket + context["n_bins"] = n_bins + context["counts"] = counts + context["bins"] = bins + + return context + + +class DataProductViewPerSasID(TemplateView): + template_name = "lofardata/workspecification/dataproducts.html" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + + try: + specification = WorkSpecification.objects.get(pk=kwargs['pk']) + except ObjectDoesNotExist: + return context + + sas_id = specification.filters['obs_id'] + context["sas_id"] = sas_id + context["dataproduct_info"] = retrieve_general_dataproduct_information(sas_id) + return context + + # ---------- REST API views ---------- class DataProductView(generics.ListCreateAPIView): model = DataProduct @@ -294,6 +375,6 @@ class WorkSpecificationViewset(viewsets.ModelViewSet): # TODO: check that there are some matches in the request? insert_task_into_atdb.delay(pk) - time.sleep(1) # allow for some time to pass + time.sleep(1) # allow for some time to pass return redirect("specification-detail", pk=pk) diff --git a/ldvspec/requirements/base.txt b/ldvspec/requirements/base.txt index 5ce7dcebce6248b62802046b3cff71477627943d..ec4a4f46a3a4c9565e622a48a08e4dee6010ab4e 100644 --- a/ldvspec/requirements/base.txt +++ b/ldvspec/requirements/base.txt @@ -16,4 +16,5 @@ django-uws==0.2.dev355575 django-crispy-forms==1.14.0 crispy-bootstrap5==0.6 humanize==4.4.0 -django-widget-tweaks \ No newline at end of file +django-widget-tweaks +numpy==1.23.0 \ No newline at end of file