diff --git a/assets/src/bundles/add_forge/create-request.js b/assets/src/bundles/add_forge/create-request.js new file mode 100644 --- /dev/null +++ b/assets/src/bundles/add_forge/create-request.js @@ -0,0 +1,92 @@ +/** + * Copyright (C) 2022 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 + */ + +import {handleFetchError, removeUrlFragment, csrfPost} from 'utils/functions'; + +let requestBrowseTable; + +export function onCreateRequestPageLoad() { + $(document).ready(() => { + $('#requestCreateForm').submit(async function(event) { + event.preventDefault(); + try { + const response = await csrfPost($(this).attr('action'), + {'Content-Type': 'application/x-www-form-urlencoded'}, + $(this).serialize()); + handleFetchError(response); + $('#userMessageDetail').empty(); + $('#userMessage').text('Your request has been submitted'); + $('#userMessage').removeClass('badge-danger'); + $('#userMessage').addClass('badge-success'); + requestBrowseTable.draw(); // redraw the table to update the list + } catch (response) { + const responseText = await response.json(); + $('#userMessageDetail').empty(); + $('#userMessage').text('Sorry; an error occurred'); + $('#userMessageDetail').text(responseText.substring(0, 500)); + $('#userMessage').removeClass('badge-success'); + $('#userMessage').addClass('badge-danger'); + } + }); + + $(window).on('hashchange', () => { + if (window.location.hash === '#browse-requests') { + $('.nav-tabs a[href="#swh-add-forge-requests-list"]').tab('show'); + } else { + $('.nav-tabs a[href="#swh-add-forge-submit-request"]').tab('show'); + } + }); + + $('#swh-add-forge-requests-list-tab').on('shown.bs.tab', () => { + window.location.hash = '#browse-requests'; + }); + + $('#swh-add-forge-tab').on('shown.bs.tab', () => { + removeUrlFragment(); + }); + + populateRequestBrowseList(); // Load existing requests + }); +} + +export function populateRequestBrowseList() { + requestBrowseTable = $('#add-forge-request-browse') + .on('error.dt', (e, settings, techNote, message) => { + $('#add-forge-browse-request-error').text(message); + }) + .DataTable({ + serverSide: true, + processing: true, + retrieve: true, + searching: true, + info: false, + dom: '<<"d-flex justify-content-between align-items-center"f' + + '<"#list-exclude">l>rt<"bottom"ip>>', + ajax: { + 'url': Urls.add_forge_request_list_datatables() + }, + columns: [ + { + data: 'submission_date', + name: 'submission_date' + }, + { + data: 'forge_type', + name: 'forge_type' + }, + { + data: 'forge_url', + name: 'forge_url' + }, + { + data: 'status', + name: 'status' + } + ] + }); + requestBrowseTable.draw(); +} diff --git a/assets/src/bundles/add_forge/index.js b/assets/src/bundles/add_forge/index.js new file mode 100644 --- /dev/null +++ b/assets/src/bundles/add_forge/index.js @@ -0,0 +1,10 @@ +/** + * Copyright (C) 2022 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 + */ + +// bundle for add forge views + +export * from './create-request'; diff --git a/cypress/integration/add-forge-now-request-create.spec.js b/cypress/integration/add-forge-now-request-create.spec.js new file mode 100644 --- /dev/null +++ b/cypress/integration/add-forge-now-request-create.spec.js @@ -0,0 +1,139 @@ +/** + * Copyright (C) 2022 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 + */ + +function populateForm(type, url, contact, email, consent, comment) { + cy.get('#swh-input-forge-type').select(type); + cy.get('#swh-input-forge-url').type(url); + cy.get('#swh-input-forge-contact-name').type(contact); + cy.get('#swh-input-forge-contact-email').type(email); + cy.get('#swh-input-forge-comment').type(comment); +} + +describe('Test add-forge-request creation', function() { + beforeEach(function() { + this.addForgeNowUrl = this.Urls.forge_add(); + }); + + it('should show both tabs for every user', function() { + cy.visit(this.addForgeNowUrl); + + cy.get('#swh-add-forge-tab') + .should('have.class', 'nav-link'); + + cy.get('#swh-add-forge-requests-list-tab') + .should('have.class', 'nav-link'); + }); + + it('should show create forge tab by default', function() { + cy.visit(this.addForgeNowUrl); + + cy.get('#swh-add-forge-tab') + .should('have.class', 'active'); + + cy.get('#swh-add-forge-requests-list-tab') + .should('not.have.class', 'active'); + }); + + it('should show login link for anonymous user', function() { + cy.visit(this.addForgeNowUrl); + + cy.get('#loginLink') + .should('be.visible') + .should('contain', 'log in'); + }); + + it('should bring back after login', function() { + cy.visit(this.addForgeNowUrl); + + cy.get('#loginLink') + .should('have.attr', 'href') + .and('include', `${this.Urls.login()}?next=${this.Urls.forge_add()}`); + }); + + it('should change tabs on click', function() { + cy.visit(this.addForgeNowUrl); + + cy.get('#swh-add-forge-requests-list-tab').click(); + cy.get('#swh-add-forge-tab') + .should('not.have.class', 'active'); + + cy.get('#swh-add-forge-requests-list-tab') + .should('have.class', 'active'); + + cy.get('#swh-add-forge-tab').click(); + cy.get('#swh-add-forge-tab') + .should('have.class', 'active'); + + cy.get('#swh-add-forge-requests-list-tab') + .should('not.have.class', 'active'); + }); + + it('should show create form elements to authenticated user', function() { + cy.userLogin(); + cy.visit(this.addForgeNowUrl); + + cy.get('#swh-input-forge-type') + .should('be.visible'); + + cy.get('#swh-input-forge-url') + .should('be.visible'); + + cy.get('#swh-input-forge-contact-name') + .should('be.visible'); + + cy.get('#swh-input-consent-check') + .should('be.visible'); + + cy.get('#swh-input-forge-comment') + .should('be.visible'); + + cy.get('#swh-input-form-submit') + .should('be.visible'); + }); + + it('should show browse requests table for every user', function() { + // testing only for anonymous + cy.visit(this.addForgeNowUrl); + + cy.get('#swh-add-forge-requests-list-tab').click(); + cy.get('#add-forge-request-browse') + .should('be.visible'); + + cy.get('#loginLink') + .should('not.be.visible'); + }); + + it('should update browse list on successful submission', function() { + cy.userLogin(); + cy.visit(this.addForgeNowUrl); + populateForm('bitbucket', 'gitlab.com', 'test', 'test@example.com', 'on', 'test comment'); + cy.get('#requestCreateForm').submit(); + + cy.visit(this.addForgeNowUrl); + cy.get('#swh-add-forge-requests-list-tab').click(); + cy.get('#add-forge-request-browse') + .should('be.visible') + .should('contain', 'gitlab.com'); + + cy.get('#add-forge-request-browse') + .should('be.visible') + .should('contain', 'PENDING'); + }); + + it('should show error message on conflict', function() { + cy.userLogin(); + cy.visit(this.addForgeNowUrl); + populateForm('bitbucket', 'gitlab.com', 'test', 'test@example.com', 'on', 'test comment'); + cy.get('#requestCreateForm').submit(); + + cy.get('#requestCreateForm').submit(); // Submitting the same data again + + cy.get('#userMessage') + .should('have.class', 'badge-danger') + .should('contain', 'Sorry; an error occurred'); + }); +}); diff --git a/swh/web/add_forge_now/views.py b/swh/web/add_forge_now/views.py --- a/swh/web/add_forge_now/views.py +++ b/swh/web/add_forge_now/views.py @@ -3,22 +3,22 @@ # License: GNU Affero General Public License version 3, or any later version # See top-level LICENSE file for more information -from typing import Any, Dict +from typing import Any, Dict, List from django.conf.urls import url from django.core.paginator import Paginator from django.db.models import Q from django.http.request import HttpRequest from django.http.response import HttpResponse, JsonResponse +from django.shortcuts import render +from swh.web.add_forge_now.models import Request as AddForgeRequest from swh.web.api.views.add_forge_now import ( AddForgeNowRequestPublicSerializer, AddForgeNowRequestSerializer, ) from swh.web.auth.utils import ADD_FORGE_MODERATOR_PERMISSION -from .models import Request as AddForgeRequest - def add_forge_request_list_datatables(request: HttpRequest) -> HttpResponse: """Dedicated endpoint used by datatables to display the add-forge @@ -78,10 +78,30 @@ return JsonResponse(table_data) +FORGE_TYPES: List[str] = [ + "bitbucket", + "cgit", + "gitlab", + "gitea", + "heptapod", +] + + +def create_request(request): + """View to create a new 'add_forge_now' request. + + """ + + return render( + request, "add_forge_now/create-request.html", {"forge_types": FORGE_TYPES}, + ) + + urlpatterns = [ url( r"^add-forge/request/list/datatables$", add_forge_request_list_datatables, name="add-forge-request-list-datatables", ), + url(r"^add-forge/request/create$", create_request, name="forge-add"), ] diff --git a/swh/web/templates/add_forge_now/create-request.html b/swh/web/templates/add_forge_now/create-request.html new file mode 100644 --- /dev/null +++ b/swh/web/templates/add_forge_now/create-request.html @@ -0,0 +1,234 @@ +{% extends "../layout.html" %} + +{% comment %} +Copyright (C) 2022 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 +{% endcomment %} + +{% load render_bundle from webpack_loader %} +{% load static %} + +{% block header %} +{% render_bundle 'add_forge' %} +{% endblock %} + +{% block title %} +Add forge now – Software Heritage archive +{% endblock %} + +{% block navbar-content %} +

Request the addition of a forge into the archive

+{% endblock %} + +{% block content %} +
+
+
+
+

+ “Add forge now” provides a service for Software Heritage users to save a + complete forge in the Software Heritage archive by requesting the addition + of the forge URL into the list of regularly visited forges. + {% if not user.is_authenticated %} +

+ You can submit an “Add forge now” request only when you are authenticated, + please login to submit the request. +

+ {% endif %} +

+
+ + +
+ + +
+
+ {% if not user.is_authenticated %} +

+

+ You must be logged in to submit an add forge request. Please + log in +

+

+ {% else %} + +
+ {% csrf_token %} +
+
+ + + + Supported forge types in software archive. + +
+ +
+ + + + Remote URL of the forge to list. + +
+
+ +
+
+ + + + Name of the Forge administrator. + +
+ +
+ + + + Email of the forge administrator. The given email address will not be used for any purpose outside the “add forge now” process. + +
+
+ +
+
+ + +
+
+ +
+
+ + + + If you wish to leave a comment regarding your request. + +
+
+ +
+
+ +
+
+ +
+
+

+ +

+

+ +

+
+
+
+
+ +
+

+ Once submitted, your "add forge" request can either be: +

+
    +
  • + Pending: + the request was submitted and is waiting for a moderator + to check its validity. +
  • + +
  • + Waiting for feedback: + the request was processed by a moderator + and the forge was contacted, the request is waiting for feedback from the + forge. +
  • + +
  • + Feedback to handle: + the forge has responded to the request and + there is feedback to handle for the request.
  • + +
  • + Accepted: + the request has been accepted and waiting to be + scheduled. +
  • + +
  • + Scheduled: + the request has been scheduled is considered + done. +
  • + +
  • + First listing done: + The first listing of the forge is + completed. +
  • + +
  • + First origin loaded: + The first origin or repository processed by + loader and archived (using a search query). +
  • + +
  • Rejected: the request is not a valid request and is rejected by a + Software Heritage moderator.
  • + +
  • Denied: the forge has requested not to archive the forge.
  • + +
  • Suspended: the request is for a forge with a non supported + VCS.
  • +
+

+ Once a add request has been submitted, you can follow its current status in + the + submitted requests list + . This process is depending on human interactions and might take a few days to be + handled (it primarily depends on the response time of the forge). +

+
+
+ {% endif %} +
+
+ + + + + + + + + +
Submission dateForge typeForge URLStatus
+
+
+
+
+
+ + + +{% endblock %} diff --git a/swh/web/templates/layout.html b/swh/web/templates/layout.html --- a/swh/web/templates/layout.html +++ b/swh/web/templates/layout.html @@ -209,6 +209,12 @@

Save code now

+