diff --git a/ldvspec/ldvspec/settings/base.py b/ldvspec/ldvspec/settings/base.py index 16345275b175c0ba95ff9658eaf21ab348bac7d6..bb6b9e6e4ff1148637ae752cc29e672da22f0d3a 100644 --- a/ldvspec/ldvspec/settings/base.py +++ b/ldvspec/ldvspec/settings/base.py @@ -130,6 +130,7 @@ REST_FRAMEWORK = { 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', 'PAGE_SIZE': 100 } +LOGIN_URL = '/ldvspec/accounts/login' LOGIN_REDIRECT_URL = '/ldvspec' # Recommended to use an environment variable to set the broker URL. CELERY_BROKER_URL = os.getenv("CELERY_BROKER_URL", "amqp://guest@localhost:5672") diff --git a/ldvspec/lofardata/forms.py b/ldvspec/lofardata/forms.py index faa5152bbb8f74145d54a9e5a9c728ff61583aa7..cb77f160e21be27d1091c60e7d1de0a88f607bc5 100644 --- a/ldvspec/lofardata/forms.py +++ b/ldvspec/lofardata/forms.py @@ -23,7 +23,7 @@ class WorkSpecificationForm(ModelForm): self.cleaned_data = super().clean() self.cleaned_data["filters"] = self._extract_filters() if not 'obs_id' in self.cleaned_data["filters"]: - raise ValidationError(["obs_id: This field is required."]) + raise ValidationError(["SAS ID: This field is required."]) return self.cleaned_data diff --git a/ldvspec/lofardata/mixins.py b/ldvspec/lofardata/mixins.py new file mode 100644 index 0000000000000000000000000000000000000000..0d18ee6797d9ce0deb99361f0fd91476d92e3833 --- /dev/null +++ b/ldvspec/lofardata/mixins.py @@ -0,0 +1,9 @@ +from django.contrib.auth.mixins import UserPassesTestMixin + +class CanAccessWorkSpecificationMixin(UserPassesTestMixin): + """ + A mixin for checking whether a work specification can be accessed by a particular user + Works on all views that implement get_object (DetailView etc) + """ + def test_func(self, **kwargs): + return self.get_object().can_be_accessed_by(self.request.user) \ No newline at end of file diff --git a/ldvspec/lofardata/models.py b/ldvspec/lofardata/models.py index 9b034adb1861da373055c6692e7053b7fc425369..190b45c295a57b688f21aa89d383e96daec18b5f 100644 --- a/ldvspec/lofardata/models.py +++ b/ldvspec/lofardata/models.py @@ -147,6 +147,20 @@ class WorkSpecification(models.Model): return "Unknown" return self.created_by.get_full_name() if self.created_by.get_full_name() else self.created_by + def can_be_accessed_by(self, user): + """ + Indicates whether this specification can be accessed (i.e., perform a CRUD operation) by the specified user + @param user: the user to check access for + @return: whether the specification can be accessed by the specified user, which is true if: + - the user is a staff member, OR + - the specification has not been added to the database yet (in which case, _state == adding) OR + - the specification was created by the user + """ + return user.is_staff or self._state.adding or self.created_by_id == user.pk + + def is_editable(self): + return self.submission_status != SUBMISSION_STATUS.SUBMITTED and self.submission_status != SUBMISSION_STATUS.DEFINING + def __str__(self): return str(self.id) + ' - ' + str(self.filters) + " (" + str(self.created_on) + ")" diff --git a/ldvspec/lofardata/static/lofardata/styling/dias.css b/ldvspec/lofardata/static/lofardata/styling/dias.css index 1eff72969e33b6167a90b13cb68fe8b9cbcfd09d..2d245fae67082007366d918c29037baa82a64ec5 100644 --- a/ldvspec/lofardata/static/lofardata/styling/dias.css +++ b/ldvspec/lofardata/static/lofardata/styling/dias.css @@ -128,10 +128,7 @@ td, th { align-items: center; } -.icon-after:after, .input--date:after, -.icon:before, -.section-toggle-invert + label:before, -.section-toggle + label:before { +.icon-after:after, .input--date:after, .icon:before, .section-toggle-invert + label:before, .section-toggle + label:before { -moz-osx-font-smoothing: grayscale; -webkit-font-smoothing: antialiased; display: inline-block; @@ -168,9 +165,10 @@ td, th { position: absolute; } -.icon.icon--alert:before, .section-toggle-invert + label.icon--alert:before, .section-toggle + label.icon--alert:before { - color: var(--warning); - opacity: 1; +.icon--stacked { + position: absolute; + margin-left: -0.5rem; + transform: scale(0.8); } .icon--square { @@ -185,31 +183,36 @@ td, th { color: inherit; } -.icon--color-bright:before { - color: var(--light-grey); +.icon--success:before { + color: var(--green); + opacity: 1; } -.icon--disabled { - color: var(--faded); +.icon--primary:before { + color: var(--primary); + opacity: 1; } -.icon--disabled:before { - color: var(--faded); +.icon--alert:before { + color: var(--warning); + opacity: 1; } -.icon-after--disabled { - color: var(--faded); +.icon--error:before { + color: var(--red); + opacity: 1; } -.icon-after--disabled:after { - color: var(--faded); +.icon--color-bright:before { + color: var(--light-grey); + opacity: 1; } -.icon-after--disabled { +.icon--disabled, .icon-after--disabled { color: var(--faded); } -.icon-after--disabled:after { +.icon--disabled:before, .icon--disabled:after, .icon-after--disabled:before, .icon-after--disabled:after { color: var(--faded); } @@ -1041,10 +1044,9 @@ html { color: var(--text); } - @media (max-width: 1440px) { html { - font-size: 18px; + font-size: 14px; } } @@ -1068,6 +1070,11 @@ h1 { margin: 0 0 1.5rem; } +.h1--no-margin, +h1--no-margin { + margin: 0; +} + .h2, h2 { font-size: 1.25rem; @@ -2743,6 +2750,10 @@ input.section-toggle-invert:checked ~ .section-wrapper { margin-right: 0; } +.button--icon-button .icon:before.button--inline, .button--icon-button .section-toggle + label:before.button--inline, .button--icon-button .section-toggle-invert + label:before.button--inline { + padding: 0; +} + .button .spinner { line-height: 1; } @@ -2984,6 +2995,14 @@ input.section-toggle-invert:checked ~ .section-wrapper { border: 2px solid var(--grey); } +.button--disabled.button--primary .icon.icon--success:before, .button--disabled.button--primary .section-toggle + label.icon--success:before, .button--disabled.button--primary .section-toggle-invert + label.icon--success:before { + color: var(--green); +} + +.button--disabled.button--primary .icon.icon--primary:before, .button--disabled.button--primary .section-toggle + label.icon--primary:before, .button--disabled.button--primary .section-toggle-invert + label.icon--primary:before { + color: var(--primary); +} + .button--disabled.button--primary .icon:before, .button--disabled.button--primary .section-toggle + label:before, .button--disabled.button--primary .section-toggle-invert + label:before { color: var(--text); } @@ -2994,6 +3013,26 @@ input.section-toggle-invert:checked ~ .section-wrapper { border: 2px solid var(--grey); } +.button--disabled.button--primary:hover .icon.icon--success:before, .button--disabled.button--primary:hover .section-toggle + label.icon--success:before, .button--disabled.button--primary:hover .section-toggle-invert + label.icon--success:before { + color: var(--green); + opacity: 1; +} + +.button--disabled.button--primary:hover .icon.icon--primary:before, .button--disabled.button--primary:hover .section-toggle + label.icon--primary:before, .button--disabled.button--primary:hover .section-toggle-invert + label.icon--primary:before { + color: var(--primary); + opacity: 1; +} + +.button--disabled.button--primary:hover .icon.icon--alert:before, .button--disabled.button--primary:hover .section-toggle + label.icon--alert:before, .button--disabled.button--primary:hover .section-toggle-invert + label.icon--alert:before { + color: var(--warning); + opacity: 1; +} + +.button--disabled.button--primary:hover .icon.icon--error:before, .button--disabled.button--primary:hover .section-toggle + label.icon--error:before, .button--disabled.button--primary:hover .section-toggle-invert + label.icon--error:before { + color: var(--red); + opacity: 1; +} + .button--disabled.button--primary:hover .icon:before, .button--disabled.button--primary:hover .section-toggle + label:before, .button--disabled.button--primary:hover .section-toggle-invert + label:before { color: var(--text); } @@ -3709,3 +3748,23 @@ input.section-toggle-invert:checked ~ .section-wrapper { .custom__input--fixed-width { width: 12rem; } + +.custom--action-button { + padding: 0.025rem 0 0 0; + border: none; + position: absolute; + font-size: initial; + min-width: max-content; +} + +.custom--group { + background: var(--border--light); + border: 2px solid transparent; + padding: 0.75rem; + margin-bottom: 2rem; + border-radius: 0.75rem; +} + +.custom--group-actions { + padding-left: 1rem; +} diff --git a/ldvspec/lofardata/templates/lofardata/index.html b/ldvspec/lofardata/templates/lofardata/index.html index 4e649f613e267babf6cf5458f94d7a70137e5b0a..4b7ed7683d8281d81d70e22cae3d43e59080a804 100644 --- a/ldvspec/lofardata/templates/lofardata/index.html +++ b/ldvspec/lofardata/templates/lofardata/index.html @@ -56,17 +56,24 @@ <div class="table__cell"> {% if specification.get_submission_status_display == "submitted" %} {% define "green" as badgecolor %} + {% define "The work specification has been succcesfully processed in ATDB." as badgeTitle %} {% endif %} {% if specification.get_submission_status_display == "error" %} {% define "red" as badgecolor %} + {% define "Retrieving the files to process resulted in an error. Please reports this to the support helpdesk." as badgeTitle %} {% endif %} {% if specification.get_submission_status_display == "defining" %} {% define "secondary" as badgecolor %} + {% define "The work specification has been send to ATDB but has not yet been processed there." as badgeTitle %} {% endif %} {% if specification.get_submission_status_display == "undefined" or specification.get_submission_status_display == "not submitted" %} {% define "primary" as badgecolor %} + {% define "The work specification has been created but has not yet been send to ATDB for processing." as badgeTitle %} {% endif %} - <div class="badge badge--{{ badgecolor }} margin-top" test-id="submission-status">{{ specification.get_submission_status_display }}</div> + <div class="badge badge--{{ badgecolor }} margin-top" title="{{ badgeTitle }}" + test-id="submission-status"> + {{ specification.get_submission_status_display }} + </div> </div> <div class="table__cell table__cell--truncate"> <a href="{{ specification.processing_site.url }}" @@ -105,46 +112,58 @@ <!-- 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 %}" - title="Edit this work specification"> - <span class="icon icon--pen"></span> - </a> - {% if specification.successor.count %} - <a class="link--disabled button--icon-button margin-right" + <a class="link--disabled button--icon-button margin-left" title="Cannot delete this work specification since it has successors. Delete them first"> <span class="icon icon--color-inherit icon--trash-alt"></span> </a> {% else %} - <a class="link--red button--icon-button margin-right" + <a class="button--icon-button margin-left" href="{% url 'specification-delete' pk=specification.pk %}" title="Delete this work specification"> <span class="icon icon--color-inherit icon--trash-alt"></span> </a> {% endif %} + {% if not specification.is_editable %} + <a class="link--disabled button--icon-button margin-left margin-right" + title="Tasks already present in ATDB"> + <span class="icon icon--color-inherit icon--pen"> + <span class="icon icon--color-inherit icon--stacked icon--success icon--check"></span> + </span> + </a> + {% else %} + <a class="button--icon-button margin-left margin-right" + href="{% url 'specification-update' pk=specification.pk %}" + title="Edit this work specification"> + <span class="icon icon--pen"></span> + </a> + {% endif %} + <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" %} + {% if not specification.is_editable %} <a class="link--disabled button--icon-button" title="Tasks already present in ATDB"> - <span class="icon icon--color-inherit icon--play"></span> + <span class="icon icon--color-inherit icon--play"> + <span class="icon icon--color-inherit icon--stacked icon--success icon--check"></span> + </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> + <span class="icon icon--color-inherit icon--play"> + <span class="icon icon--color-inherit icon--stacked icon--primary icon--file"></span> + </span> </a> {% else %} - <button type="submit" class="button--icon-button" test-id="submit-to-atdb-{{ specification.pk }}" title="Submit to ATDB"> + <button type="submit" class="custom--action-button button button--icon-button" + test-id="submit-to-atdb-{{ specification.pk }}" title="Submit to ATDB"> <span class="icon icon--play"></span> </button> {% endif %} - </form> - </div> </div> </div> diff --git a/ldvspec/lofardata/templates/lofardata/workspecification/create_update.html b/ldvspec/lofardata/templates/lofardata/workspecification/create_update.html index 711892e02ce18d941520e3b21e8d581127187eb0..a0d3be019dfdd37fcacc1408aa6a45f5aa705cb0 100644 --- a/ldvspec/lofardata/templates/lofardata/workspecification/create_update.html +++ b/ldvspec/lofardata/templates/lofardata/workspecification/create_update.html @@ -26,7 +26,7 @@ <h3 class="text text--primary text--title">Selection</h3> <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> + <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> @@ -122,7 +122,7 @@ <!-- Filter fields --> <div class="custom--div-margin"> - <h3 class="text text--primary text--title">Filters*</h3> + <h3 class="text text--primary text--title">Filters</h3> {% for filter in filters %} {% if filter.filter_type == 'Free' %} <div class="flex-wrapper"> diff --git a/ldvspec/lofardata/templates/lofardata/workspecification/dataset_size_info.html b/ldvspec/lofardata/templates/lofardata/workspecification/dataset_size_info.html index 059f3a583a70aa7d7d87a84d470b5347bc8aa54c..af606dbc853db6e72c3d6838355de6056e8ba3ca 100644 --- a/ldvspec/lofardata/templates/lofardata/workspecification/dataset_size_info.html +++ b/ldvspec/lofardata/templates/lofardata/workspecification/dataset_size_info.html @@ -39,7 +39,7 @@ 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[xaxis_key] = xaxis_values[i] === xaxis_values[i + 1] ? xaxis_values[i] : xaxis_values[i] + " - " + xaxis_values[i + 1] entry[yaxis_key] = yaxis_values[i] data[i] = entry } diff --git a/ldvspec/lofardata/templates/lofardata/workspecification/detail.html b/ldvspec/lofardata/templates/lofardata/workspecification/detail.html index 797c0e6472a6dff698d35de98cd701f54151ea1c..95c204c8a48491a5557065bf038aeeb92106d43b 100644 --- a/ldvspec/lofardata/templates/lofardata/workspecification/detail.html +++ b/ldvspec/lofardata/templates/lofardata/workspecification/detail.html @@ -14,10 +14,6 @@ <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 %}" title="Edit this work specification"> - <span class="icon icon--pen"></span> - </a> {% if object.successor.count %} <a class="button button--icon-button button--primary button--disabled margin-right" @@ -25,27 +21,46 @@ <span class="icon icon--trash-alt"></span> </a> {% else %} - <a class="button button--secondary button--red button--icon-button margin-right" + <a class="button button--secondary button--icon-button margin-left" href="{% url 'specification-delete' pk=object.pk %}" title="Delete this work specification"> <span class="icon icon--trash-alt"></span> </a> {% endif %} + {% if not object.is_editable %} + <a class="button button--primary button--disabled button--icon-button margin-left margin-right" + title="Tasks already present in ATDB"> + <span class="icon icon--pen"> + <span class="icon icon--color-inherit icon--stacked icon--success icon--check"></span> + </span> + </a> + {% else %} + <a class="button button--secondary button--icon-button margin-left margin-right" + href="{% url 'specification-update' pk=object.pk %}" + title="Edit this work specification"> + <span class="icon icon--pen"></span> + </a> + {% endif %} + <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" %} + {% if not object.is_editable %} <a class="button button--icon-button button--primary button--disabled" title="Tasks already present in ATDB" href="#"> - <span class="icon icon--play"></span> + <span class="icon icon--color-inherit icon--play"> + <span class="icon icon--color-inherit icon--stacked icon--success icon--check"></span> + </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> + <span class="icon icon--play"> + <span class="icon icon--color-inherit icon--stacked icon--primary icon--file"></span> + </span> </a> {% else %} <button type="submit" @@ -118,17 +133,24 @@ <div class="table__cell"> {% if object.get_submission_status_display == "submitted" %} {% define "green" as badgecolor %} + {% define "The work specification has been succcesfully processed in ATDB." as badgeTitle %} {% endif %} {% if object.get_submission_status_display == "error" %} {% define "red" as badgecolor %} + {% define "Retrieving the files to process resulted in an error. Please reports this to the support helpdesk." as badgeTitle %} {% endif %} {% if object.get_submission_status_display == "defining" %} {% define "secondary" as badgecolor %} + {% define "The work specification has been send to ATDB but has not yet been processed there." as badgeTitle %} {% endif %} {% if object.get_submission_status_display == "undefined" or object.get_submission_status_display == "not submitted" %} {% define "primary" as badgecolor %} + {% define "The work specification has been created but has not yet been send to ATDB for processing." as badgeTitle %} {% endif %} - <div class="badge badge--{{ badgecolor }}" test-id="submission-status">{{ object.get_submission_status_display }}</div> + <div class="badge badge--{{ badgecolor }} margin-top" test-id="submission-status" + title="{{ badgeTitle }}"> + {{ object.get_submission_status_display }} + </div> </div> </div> @@ -149,30 +171,30 @@ </div> - <div class="table__row table__row--dark table__row--padding"> - <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-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">Number of files</div> - {% if number_of_files == 0 %} + {% if number_of_files == 0 %} + <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"> - <a class="tooltip-dias tooltip-dias-right tooltip-dias--without-margin tooltip-dias--red " + <a class="tooltip-dias tooltip-dias-right tooltip-dias--without-margin " data-tooltip="No files found for this SAS ID">m</a> </div> - {% else %} - <div class="table__cell">{{ number_of_files }}</div> - {% endif %} - </div> + </div> + {% else %} + <div class="table__row table__row--dark table__row--padding"> + <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-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">Number of files</div> + <div class="table__cell">{{ number_of_files }}</div> + </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 %} diff --git a/ldvspec/lofardata/templates/lofardata/workspecification/tasks.html b/ldvspec/lofardata/templates/lofardata/workspecification/tasks.html index c679e7c9afd1b49331584d2b3568738982024a67..5333c275e46ca6a0d6a5b53c5c15f9cc24634c38 100644 --- a/ldvspec/lofardata/templates/lofardata/workspecification/tasks.html +++ b/ldvspec/lofardata/templates/lofardata/workspecification/tasks.html @@ -18,7 +18,7 @@ {% for related_task in object.related_tasks %} <dd> <a class="button button--link" - href="{{ object.processing_site.url }}tasks/{{ related_task }}"> + href="{{ object.processing_site.url }}task_details/{{ related_task }}/1"> {{ related_task }} </a> </dd> diff --git a/ldvspec/lofardata/templates/registration/login.html b/ldvspec/lofardata/templates/registration/login.html index 823908ab48cefe13049cd1bd6be3394255b3607a..e18c3ed2fd1826f848da6c772da54ce2ed2b9b25 100644 --- a/ldvspec/lofardata/templates/registration/login.html +++ b/ldvspec/lofardata/templates/registration/login.html @@ -38,6 +38,8 @@ <input id="id_password" class="input maxwidth-input" name="password" type="password" required> </div> + <input type="hidden" name="next" value="{{ request.GET.next }}" /> + <div class="flex-wrapper flex-wrapper--end"> <button type="submit" class="button button--primary margin-right">Sign in</button> </div> diff --git a/ldvspec/lofardata/tests/test_dataproductinfo.py b/ldvspec/lofardata/tests/test_dataproductinfo.py index 0a2460f25e853976ce4431303082c5e87c0e99c1..270acd3cdd4eeee1c27f42cb0cf8e23ee97422a9 100644 --- a/ldvspec/lofardata/tests/test_dataproductinfo.py +++ b/ldvspec/lofardata/tests/test_dataproductinfo.py @@ -124,6 +124,10 @@ class RetrieveDataProductInformation(unittest.TestCase): number_of_dataproducts = DataProduct.objects.count() self.assertEqual(number_of_dataproducts, len(test_objects)) + def test_no_database_entry(self): + actual = retrieve_combined_information('not_existent') + self.assertDictEqual(actual, {}) + def test_single_database_entry(self): actual = retrieve_combined_information('12345') self.assertEqual(actual['dataproduct_source'], ['lofar']) diff --git a/ldvspec/lofardata/tests/test_inputs_processor.py b/ldvspec/lofardata/tests/test_inputs_processor.py index 4101905cf51b048c6c68991dc57ba8bc42891aca..39712417094520b2a6fa52bec57cf497988f4770 100644 --- a/ldvspec/lofardata/tests/test_inputs_processor.py +++ b/ldvspec/lofardata/tests/test_inputs_processor.py @@ -36,6 +36,7 @@ class ComputeInputsHistogram(unittest.TestCase): self.assertListEqual([1, 2, 2, 0, 0, 1], counts) self.assertListEqual(['20.333GB', '25.300GB', '30.267GB', '35.234GB', '40.201GB', '45.168GB', '50.135GB'], bins) + def test_single_range(self): test_data = {"surls": [{"size": 1, "surl": "test"}, {"size": 1, "surl": "test"}, @@ -53,6 +54,7 @@ class ComputeInputsHistogram(unittest.TestCase): self.assertListEqual([6], counts) self.assertListEqual(['0.500B', '1.500B'], bins) + def test_extreme_wide_range(self): test_data = {"surls": [{"size": 1, "surl": "test"}, {"size": 100000, "surl": "test"}, @@ -71,6 +73,7 @@ class ComputeInputsHistogram(unittest.TestCase): self.assertListEqual(['1.000B', '148.030PB', '296.059PB', '444.089PB', '592.119PB', '740.149PB', '888.178PB'], bins) + def test_two_ranges(self): test_data = {"surls": [{"size": 97873920, "surl": "test"}, {"size": 97873920, "surl": "test"}, diff --git a/ldvspec/lofardata/tests/test_workspecification_access.py b/ldvspec/lofardata/tests/test_workspecification_access.py new file mode 100644 index 0000000000000000000000000000000000000000..c4f8c47a23c82a5baa7f29b9c38e8875d23af4a4 --- /dev/null +++ b/ldvspec/lofardata/tests/test_workspecification_access.py @@ -0,0 +1,34 @@ +import unittest + +from django.contrib.auth.models import User +from lofardata.models import WorkSpecification + + +class WorkSpecificationAccess(unittest.TestCase): + def test_access_by_staff(self): + staff_user = User(pk=1, is_staff=True) + owner = User(pk=2, is_staff=False) + workspecification = WorkSpecification(created_by=owner) + workspecification._state.adding = False # existing workspecification, exists in database + self.assertTrue(workspecification.can_be_accessed_by(staff_user)) + + def test_access_by_user_with_permission(self): + owner = User(pk=1, is_staff=False) + workspecification = WorkSpecification(created_by=owner) + workspecification._state.adding = False # existing workspecification, exists in database + self.assertTrue(workspecification.can_be_accessed_by(owner)) + + def test_access_by_user_without_permission(self): + owner = User(pk=1, is_staff=False) + other_user = User(pk=2, is_staff=False) + workspecification = WorkSpecification(created_by=owner) + workspecification._state.adding = False # existing workspecification, exists in database + self.assertFalse(workspecification.can_be_accessed_by(other_user)) + + def test_access_when_new(self): + owner = User(pk=1, is_staff=False) + other_user = User(pk=2, is_staff=False) + workspecification = WorkSpecification(created_by=owner) + workspecification._state.adding = True # newly created, not added to the database yet + self.assertTrue(workspecification.can_be_accessed_by(other_user)) + diff --git a/ldvspec/lofardata/tests/test_workspecification_creation.py b/ldvspec/lofardata/tests/test_workspecification_creation.py new file mode 100644 index 0000000000000000000000000000000000000000..7f40c72452b2862d0b899d7035cca1ef9b08df9b --- /dev/null +++ b/ldvspec/lofardata/tests/test_workspecification_creation.py @@ -0,0 +1,25 @@ +import unittest + +from django.contrib.auth.models import User + +from lofardata.models import WorkSpecification +from lofardata.views import set_post_submit_values + +class WorkSpecificationCreation(unittest.TestCase): + def test_set_created_by_when_already_set(self): + + existing_user = User(pk=1, username='existing') + new_user = User(pk=2, username='new') + + specification = WorkSpecification(created_by=existing_user) + set_post_submit_values(specification, new_user) + + self.assertEqual(specification.created_by, existing_user) + + def test_set_created_by_when_not_set(self): + new_user = User(pk=2, username='new') + + specification = WorkSpecification() + set_post_submit_values(specification, new_user) + + self.assertEqual(specification.created_by, new_user) \ No newline at end of file diff --git a/ldvspec/lofardata/tests/test_workspecification_editable.py b/ldvspec/lofardata/tests/test_workspecification_editable.py new file mode 100644 index 0000000000000000000000000000000000000000..ef359462724d9b8a768f25859261716cb9ffa656 --- /dev/null +++ b/ldvspec/lofardata/tests/test_workspecification_editable.py @@ -0,0 +1,20 @@ +import unittest + +from lofardata.models import WorkSpecification, SUBMISSION_STATUS + +class WorkSpecificationEditable(unittest.TestCase): + def test_not_editable_submitted(self): + specification = WorkSpecification(submission_status=SUBMISSION_STATUS.SUBMITTED) + self.assertFalse(specification.is_editable()) + + def test_not_editable_defining(self): + specification = WorkSpecification(submission_status=SUBMISSION_STATUS.DEFINING) + self.assertFalse(specification.is_editable()) + + def test_editable_not_submitted(self): + specification = WorkSpecification(submission_status=SUBMISSION_STATUS.NOT_SUBMITTED) + self.assertTrue(specification.is_editable()) + + def test_editable_error(self): + specification = WorkSpecification(submission_status=SUBMISSION_STATUS.ERROR) + self.assertTrue(specification.is_editable()) \ No newline at end of file diff --git a/ldvspec/lofardata/view_helpers/dataproductinfo.py b/ldvspec/lofardata/view_helpers/dataproductinfo.py index 5e68f025b1a71fa0183888f01251c3c056038d7b..50f7ab89c7aa9bcea2c56a1cc1885591cbde1f01 100644 --- a/ldvspec/lofardata/view_helpers/dataproductinfo.py +++ b/ldvspec/lofardata/view_helpers/dataproductinfo.py @@ -11,6 +11,10 @@ def retrieve_combined_information(sas_id): 'antenna_set', 'instrument_filter', 'dysco_compression').distinct() + + if len(data_products) == 0: + return {} + combined_data_products_on_key = combine_dataproducts_on_key(data_products) dysco_compressions = combined_data_products_on_key['dysco_compression'] diff --git a/ldvspec/lofardata/views.py b/ldvspec/lofardata/views.py index cdb895968428e5c5c5ddf50ed530ad1a8336da64..ecf0799379fb30b63a3855e9700d2eaa2d5c0939 100644 --- a/ldvspec/lofardata/views.py +++ b/ldvspec/lofardata/views.py @@ -1,5 +1,6 @@ import time +from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.models import User from django.core.exceptions import ObjectDoesNotExist from django.http import HttpResponseRedirect @@ -14,6 +15,7 @@ from rest_framework.reverse import reverse_lazy from rest_framework.schemas.openapi import AutoSchema from .forms import WorkSpecificationForm +from .mixins import CanAccessWorkSpecificationMixin from .models import ( ATDBProcessingSite, DataProduct, @@ -64,6 +66,11 @@ 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 @@ -79,11 +86,18 @@ class Specifications(ListView): return queryset.filter(created_by=current_user.id).order_by("-created_on") -class WorkSpecificationCreateUpdateView(UpdateView): +class WorkSpecificationCreateUpdateView(LoginRequiredMixin, CanAccessWorkSpecificationMixin, UpdateView): template_name = "lofardata/workspecification/create_update.html" model = WorkSpecification form_class = WorkSpecificationForm + def get(self, request, *args, **kwargs): + specification = self.get_object() + if specification.is_editable(): + return super().get(request, *args, **kwargs) + else: + return redirect('specification-detail', self.kwargs["pk"]) + def get_object(self, queryset=None): if self.kwargs.__len__() == 0 or self.kwargs["pk"] is None: specification = WorkSpecification() @@ -113,8 +127,7 @@ class WorkSpecificationCreateUpdateView(UpdateView): action_ = form.data["action"] specification = form.instance if action_ == "Submit": - specification.async_task_result = None - specification.is_ready = False + set_post_submit_values(specification, self.request.user) if action_ == "Send": insert_task_into_atdb.delay(specification.pk) if action_ == "Successor": @@ -134,7 +147,7 @@ class WorkSpecificationCreateUpdateView(UpdateView): return reverse_lazy("specification-update", kwargs={"pk": kwargs["pk"]}) -class WorkSpecificationDetailView(DetailView): +class WorkSpecificationDetailView(LoginRequiredMixin, CanAccessWorkSpecificationMixin, DetailView): template_name = "lofardata/workspecification/detail.html" model = WorkSpecification @@ -155,23 +168,23 @@ class WorkSpecificationDetailView(DetailView): return context -class WorkSpecificationDeleteView(DeleteView): +class WorkSpecificationDeleteView(LoginRequiredMixin, CanAccessWorkSpecificationMixin, DeleteView): template_name = "lofardata/workspecification/delete.html" model = WorkSpecification success_url = reverse_lazy("index") -class WorkSpecificationInputsView(DetailView): +class WorkSpecificationInputsView(LoginRequiredMixin, CanAccessWorkSpecificationMixin, DetailView): template_name = "lofardata/workspecification/inputs.html" model = WorkSpecification -class WorkSpecificationATDBTasksView(DetailView): +class WorkSpecificationATDBTasksView(LoginRequiredMixin, CanAccessWorkSpecificationMixin, DetailView): template_name = "lofardata/workspecification/tasks.html" model = WorkSpecification -class WorkSpecificationDatasetSizeInfoView(DetailView): +class WorkSpecificationDatasetSizeInfoView(LoginRequiredMixin, CanAccessWorkSpecificationMixin, DetailView): template_name = "lofardata/workspecification/dataset_size_info.html" model = WorkSpecification @@ -192,14 +205,14 @@ class WorkSpecificationDatasetSizeInfoView(DetailView): return context -class DataProductViewPerSasID(TemplateView): +class DataProductViewPerSasID(LoginRequiredMixin, CanAccessWorkSpecificationMixin, 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']) + specification = self.get_object() except ObjectDoesNotExist: return context @@ -208,6 +221,8 @@ class DataProductViewPerSasID(TemplateView): context["dataproduct_info"] = dataproductinfo.retrieve_combined_information(sas_id) return context + def get_object(self): + return WorkSpecification.objects.get(pk=self.kwargs['pk']) # ---------- REST API views ---------- class DataProductView(generics.ListCreateAPIView):