diff --git a/assets/src/bundles/save/artifact-form-row.ejs b/assets/src/bundles/save/artifact-form-row.ejs new file mode 100644 --- /dev/null +++ b/assets/src/bundles/save/artifact-form-row.ejs @@ -0,0 +1,34 @@ +<%# + Copyright (C) 2021 The Software Heritage developers + See the AUTHORS file at the top-level directory of this distribution + License: GNU Affero General Public License version 3, or any later version + See top-level LICENSE file for more information +%> + +
+
+ + +
The artifact url is mandatory
+
+
+ + +
The artifact version is mandatory
+
+
+ <% if (deletableRow) { %> + + + <% } else { %> + + + <% } %> +
+
\ No newline at end of file diff --git a/assets/src/bundles/save/index.js b/assets/src/bundles/save/index.js --- a/assets/src/bundles/save/index.js +++ b/assets/src/bundles/save/index.js @@ -7,6 +7,7 @@ import {csrfPost, handleFetchError, isGitRepoUrl, htmlAlert, removeUrlFragment} from 'utils/functions'; import {swhSpinnerSrc} from 'utils/constants'; +import artifactFormRowTemplate from './artifact-form-row.ejs'; let saveRequestsTable; @@ -49,19 +50,38 @@ // Read the actual selected value and depending on the origin type, display some extra // inputs or hide them. This makes the extra inputs disabled when not displayed. const originType = $('#swh-input-visit-type').val(); - let cssDisplay; - let disabled; + let display = 'none'; + let disabled = true; if (originType === 'archives') { + display = 'flex'; disabled = false; - cssDisplay = 'flex'; - } else { - cssDisplay = 'none'; - disabled = true; } - $('#optional-origin-forms').css('display', cssDisplay); - $('#swh-input-artifact-url').prop('disabled', disabled); - $('#swh-input-artifact-version').prop('disabled', disabled); + $('.swh-save-origin-archives-form').css('display', display); + if (!disabled) { + // help paragraph must have block display for proper rendering + $('#swh-save-origin-archives-help').css('display', 'block'); + } + $('.swh-save-origin-archives-form .form-control').prop('disabled', disabled); + + if (originType === 'archives' && $('.swh-save-origin-archives-form').length === 1) { + // insert first artifact row when the archives visit type is selected for the first time + $('.swh-save-origin-archives-form').last().after( + artifactFormRowTemplate({deletableRow: false, formId: 0})); + } +} + +export function addArtifactFormRow() { + $('.swh-save-origin-artifact-form').last().after( + artifactFormRowTemplate({ + deletableRow: true, + formId: $('.swh-save-origin-artifact-form').length + }) + ); +} + +export function deleteArtifactFormRow(event) { + $(event.target).closest('.swh-save-origin-artifact-form').remove(); } const userRequestsFilterCheckbox = ` @@ -254,10 +274,16 @@ let originUrl = $('#swh-input-origin-url').val(); // read the extra inputs for the 'archives' type - let extraData = originType !== 'archives' ? {} : { - 'artifact_url': $('#swh-input-artifact-url').val(), - 'artifact_version': $('#swh-input-artifact-version').val() - }; + let extraData = {}; + if (originType === 'archives') { + extraData['archives_data'] = []; + for (let i = 0; i < $('.swh-save-origin-artifact-form').length; ++i) { + extraData['archives_data'].push({ + 'artifact_url': $(`#swh-input-artifact-url-${i}`).val(), + 'artifact_version': $(`#swh-input-artifact-version-${i}`).val() + }); + } + } originSaveRequest(originType, originUrl, extraData, () => $('#swh-origin-save-request-status').html(saveRequestAcceptedAlert), diff --git a/cypress/integration/origin-save.spec.js b/cypress/integration/origin-save.spec.js --- a/cypress/integration/origin-save.spec.js +++ b/cypress/integration/origin-save.spec.js @@ -507,12 +507,12 @@ for (let visitType of anonymousVisitTypes) { cy.get('#swh-input-visit-type').select(visitType); - cy.get('#optional-origin-forms').should('not.be.visible'); + cy.get('.swh-save-origin-archives-form').should('not.be.visible'); } // this should display more inputs with the 'archives' type cy.get('#swh-input-visit-type').select('archives'); - cy.get('#optional-origin-forms').should('be.visible'); + cy.get('.swh-save-origin-archives-form').should('be.visible'); }); @@ -535,9 +535,9 @@ .type(originUrl) .get('#swh-input-visit-type') .select('archives') - .get('#swh-input-artifact-url') + .get('#swh-input-artifact-url-0') .type(artifactUrl) - .get('#swh-input-artifact-version') + .get('#swh-input-artifact-version-0') .type(artifactVersion) .get('#swh-save-origin-form') .submit(); @@ -548,4 +548,81 @@ }); + it('should submit multiple artifacts for the archives visit type', function() { + let originUrl = 'https://ftp.gnu.org/pub/pub/gnu/3dldf'; + let artifactUrl = 'https://ftp.gnu.org/pub/pub/gnu/3dldf/3DLDF-1.1.4.tar.gz'; + let artifactVersion = '1.1.4'; + let artifact2Url = 'https://ftp.gnu.org/pub/pub/gnu/3dldf/3DLDF-1.1.5.tar.gz'; + let artifact2Version = '1.1.5'; + + cy.ambassadorLogin(); + cy.visit(url); + + cy.get('#swh-input-origin-url') + .type(originUrl) + .get('#swh-input-visit-type') + .select('archives'); + + // fill first artifact info + cy.get('#swh-input-artifact-url-0') + .type(artifactUrl) + .get('#swh-input-artifact-version-0') + .type(artifactVersion); + + // add new artifact form row + cy.get('#swh-add-archive-artifact') + .click(); + + // check new row is displayed + cy.get('#swh-input-artifact-url-1') + .should('exist'); + + // request removal of newly added row + cy.get('#swh-remove-archive-artifact-1') + .click(); + + // check row has been removed + cy.get('#swh-input-artifact-url-1') + .should('not.exist'); + + // add new artifact form row + cy.get('#swh-add-archive-artifact') + .click(); + + // fill second artifact info + cy.get('#swh-input-artifact-url-1') + .type(artifact2Url) + .get('#swh-input-artifact-version-1') + .type(artifact2Version); + + // setup request interceptor to check POST data and stub response + cy.intercept('POST', this.Urls.api_1_save_origin('archives', originUrl), (req) => { + expect(req.body).to.deep.equal({ + archives_data: [ + {artifact_url: artifactUrl, artifact_version: artifactVersion}, + {artifact_url: artifact2Url, artifact_version: artifact2Version} + ] + }); + req.reply(genOriginSaveResponse({ + visitType: 'archives', + saveRequestStatus: 'accepted', + originUrl: originUrl, + saveRequestDate: new Date(), + saveTaskStatus: 'not yet scheduled', + visitDate: null, + visitStatus: null + })); + }).as('saveRequest'); + + // submit form + cy.get('#swh-save-origin-form') + .submit(); + + // submission should be successful + cy.wait('@saveRequest').then(() => { + checkAlertVisible('success', saveCodeMsg['success']); + }); + + }); + }); diff --git a/swh/web/common/origin_save.py b/swh/web/common/origin_save.py --- a/swh/web/common/origin_save.py +++ b/swh/web/common/origin_save.py @@ -225,15 +225,11 @@ ) -def _check_origin_exists(origin_url: Optional[str]) -> OriginExistenceCheckInfo: - """Ensure the origin exists, if not raise an explicit message.""" - if not origin_url: - raise BadInputExc("The origin url provided must be set!") - metadata = origin_exists(origin_url) +def _check_origin_exists(url: str) -> OriginExistenceCheckInfo: + """Ensure an URL exists, if not raise an explicit message.""" + metadata = origin_exists(url) if not metadata["exists"]: - raise BadInputExc( - f"The provided origin url ({escape(origin_url)}) does not exist!" - ) + raise BadInputExc(f"The provided url ({escape(url)}) does not exist!") return metadata @@ -429,10 +425,6 @@ _check_visit_type_savable(visit_type, privileged_user) _check_origin_url_valid(origin_url) - artifact_url = kwargs.get("artifact_url") - if visit_type == "archives": - metadata = _check_origin_exists(artifact_url) - # if all checks passed so far, we can try and save the origin save_request_status = can_save_origin(origin_url, privileged_user) task = None @@ -447,18 +439,27 @@ } if visit_type == "archives": # extra arguments for that type are required - assert metadata is not None - task_kwargs = dict( - **task_kwargs, - artifacts=[ + archives_data = kwargs.get("archives_data", []) + if not archives_data: + raise BadInputExc( + "Artifacts data are missing for the archives visit type." + ) + artifacts = [] + for artifact in archives_data: + artifact_url = artifact.get("artifact_url") + artifact_version = artifact.get("artifact_version") + if not artifact_url or not artifact_version: + raise BadInputExc("Missing url or version for an artifact to load.") + metadata = _check_origin_exists(artifact_url) + artifacts.append( { "url": artifact_url, - "version": kwargs["artifact_version"], + "version": artifact_version, "time": metadata["last_modified"], "length": metadata["content_length"], } - ], - ) + ) + task_kwargs = dict(**task_kwargs, artifacts=artifacts, snapshot_append=True) sor = None # get list of previously sumitted save requests current_sors = list( diff --git a/swh/web/templates/misc/origin-save.html b/swh/web/templates/misc/origin-save.html --- a/swh/web/templates/misc/origin-save.html +++ b/swh/web/templates/misc/origin-save.html @@ -50,20 +50,16 @@ - - + +