diff --git a/assets/src/bundles/add_forge/index.js b/assets/src/bundles/add_forge/index.js index 2e92c2df..92696858 100644 --- a/assets/src/bundles/add_forge/index.js +++ b/assets/src/bundles/add_forge/index.js @@ -1,30 +1,30 @@ /** * 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 './add-forge.css'; export * from './create-request'; export * from './moderation-dashboard'; export * from './request-dashboard'; export function formatRequestStatusName(status) { // Mapping to format the request status to a human readable text const statusLabel = { 'PENDING': 'Pending', 'WAITING_FOR_FEEDBACK': 'Waiting for feedback', 'FEEDBACK_TO_HANDLE': 'Feedback to handle', 'ACCEPTED': 'Accepted', 'SCHEDULED': 'Scheduled', 'FIRST_LISTING_DONE': 'First listing done', 'FIRST_ORIGIN_LOADED': 'First origin loaded', 'REJECTED': 'Rejected', 'SUSPENDED': 'Suspended', - 'DENIED': 'Denied' + 'UNSUCCESSFUL': 'Unsuccessful' }; return status in statusLabel ? statusLabel[status] : status; } diff --git a/assets/src/bundles/add_forge/request-dashboard.js b/assets/src/bundles/add_forge/request-dashboard.js index fcb6219e..3fed8106 100644 --- a/assets/src/bundles/add_forge/request-dashboard.js +++ b/assets/src/bundles/add_forge/request-dashboard.js @@ -1,138 +1,138 @@ /** * 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, csrfPost, getHumanReadableDate} from 'utils/functions'; import emailTempate from './forge-admin-email.ejs'; import requestHistoryItem from './add-request-history-item.ejs'; let forgeRequest; export function onRequestDashboardLoad(requestId) { $(document).ready(() => { populateRequestDetails(requestId); $('#contactForgeAdmin').click((event) => { contactForgeAdmin(event); }); $('#updateRequestForm').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); $('#userMessage').text('The request status has been updated '); $('#userMessage').removeClass('badge-danger'); $('#userMessage').addClass('badge-success'); populateRequestDetails(requestId); } catch (response) { $('#userMessage').text('Sorry; Updating the request failed'); $('#userMessage').removeClass('badge-success'); $('#userMessage').addClass('badge-danger'); } }); }); } async function populateRequestDetails(requestId) { try { const response = await fetch(Urls.api_1_add_forge_request_get(requestId)); handleFetchError(response); const data = await response.json(); forgeRequest = data.request; $('#requestStatus').text(swh.add_forge.formatRequestStatusName(forgeRequest.status)); $('#requestType').text(forgeRequest.forge_type); $('#requestURL').text(forgeRequest.forge_url); $('#requestContactName').text(forgeRequest.forge_contact_name); $('#requestContactConsent').text(forgeRequest.submitter_forward_username); $('#requestContactEmail').text(forgeRequest.forge_contact_email); $('#submitterMessage').text(forgeRequest.forge_contact_comment); $('#updateComment').val(''); // Setting data for the email, now adding static data $('#contactForgeAdmin').attr('emailTo', forgeRequest.forge_contact_email); $('#contactForgeAdmin').attr('emailCc', forgeRequest.inbound_email_address); $('#contactForgeAdmin').attr('emailSubject', `Software Heritage archival notification for ${forgeRequest.forge_domain}`); populateRequestHistory(data.history); populateDecisionSelectOption(forgeRequest.status); } catch (e) { if (e instanceof Response) { // The fetch request failed (in handleFetchError), show the error message $('#fetchError').removeClass('d-none'); $('#requestDetails').addClass('d-none'); } else { // Unknown exception, pass it through throw e; } } } function populateRequestHistory(history) { $('#requestHistory').children().remove(); history.forEach((event, index) => { const historyEvent = requestHistoryItem({ 'event': event, 'index': index, 'getHumanReadableDate': getHumanReadableDate }); $('#requestHistory').append(historyEvent); }); } export function populateDecisionSelectOption(currentStatus) { const nextStatusesFor = { 'PENDING': ['WAITING_FOR_FEEDBACK', 'REJECTED', 'SUSPENDED'], 'WAITING_FOR_FEEDBACK': ['FEEDBACK_TO_HANDLE'], 'FEEDBACK_TO_HANDLE': [ 'WAITING_FOR_FEEDBACK', 'ACCEPTED', 'REJECTED', 'SUSPENDED' ], 'ACCEPTED': ['SCHEDULED'], 'SCHEDULED': [ 'FIRST_LISTING_DONE', 'FIRST_ORIGIN_LOADED' ], 'FIRST_LISTING_DONE': ['FIRST_ORIGIN_LOADED'], 'FIRST_ORIGIN_LOADED': [], 'REJECTED': [], 'SUSPENDED': ['PENDING'], - 'DENIED': [] + 'UNSUCCESSFUL': [] }; // Determine the possible next status out of the current one const nextStatuses = nextStatusesFor[currentStatus]; function addStatusOption(status, index) { // Push the next possible status options const label = swh.add_forge.formatRequestStatusName(status); $('#decisionOptions').append( `` ); } // Remove all the options and add new ones $('#decisionOptions').children().remove(); nextStatuses.forEach(addStatusOption); $('#decisionOptions').append( '' ); } function contactForgeAdmin(event) { // Open the mailclient with pre-filled text const mailTo = encodeURIComponent($('#contactForgeAdmin').attr('emailTo')); const mailCc = encodeURIComponent($('#contactForgeAdmin').attr('emailCc')); const subject = encodeURIComponent($('#contactForgeAdmin').attr('emailSubject')); const emailText = encodeURIComponent(emailTempate({'forgeUrl': forgeRequest.forge_url}).trim().replace(/\n/g, '\r\n')); const w = window.open('', '_blank', '', true); w.location.href = `mailto:${mailTo}?Cc=${mailCc}&Reply-To=${mailCc}&Subject=${subject}&body=${emailText}`; w.focus(); } diff --git a/swh/web/add_forge_now/migrations/0007_rename_denied_request_status.py b/swh/web/add_forge_now/migrations/0007_rename_denied_request_status.py new file mode 100644 index 00000000..fae6d12c --- /dev/null +++ b/swh/web/add_forge_now/migrations/0007_rename_denied_request_status.py @@ -0,0 +1,51 @@ +# Generated by Django 2.2.28 on 2022-08-16 14:13 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("swh_web_add_forge_now", "0006_request_add_new_fields"), + ] + + operations = [ + migrations.AlterField( + model_name="request", + name="status", + field=models.TextField( + choices=[ + ("PENDING", "Pending"), + ("WAITING_FOR_FEEDBACK", "Waiting for feedback"), + ("FEEDBACK_TO_HANDLE", "Feedback to handle"), + ("ACCEPTED", "Accepted"), + ("SCHEDULED", "Scheduled"), + ("FIRST_LISTING_DONE", "First listing done"), + ("FIRST_ORIGIN_LOADED", "First origin loaded"), + ("REJECTED", "Rejected"), + ("SUSPENDED", "Suspended"), + ("UNSUCCESSFUL", "Unsuccessful"), + ], + default="PENDING", + ), + ), + migrations.AlterField( + model_name="requesthistory", + name="new_status", + field=models.TextField( + choices=[ + ("PENDING", "Pending"), + ("WAITING_FOR_FEEDBACK", "Waiting for feedback"), + ("FEEDBACK_TO_HANDLE", "Feedback to handle"), + ("ACCEPTED", "Accepted"), + ("SCHEDULED", "Scheduled"), + ("FIRST_LISTING_DONE", "First listing done"), + ("FIRST_ORIGIN_LOADED", "First origin loaded"), + ("REJECTED", "Rejected"), + ("SUSPENDED", "Suspended"), + ("UNSUCCESSFUL", "Unsuccessful"), + ], + null=True, + ), + ), + ] diff --git a/swh/web/add_forge_now/models.py b/swh/web/add_forge_now/models.py index 86abbe1c..6d6f85b7 100644 --- a/swh/web/add_forge_now/models.py +++ b/swh/web/add_forge_now/models.py @@ -1,141 +1,142 @@ # 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 from __future__ import annotations import enum from typing import List from urllib.parse import urlparse from django.db import models from ..config import get_config from ..inbound_email.utils import get_address_for_pk from .apps import APP_LABEL class RequestStatus(enum.Enum): """Request statuses. Values are used in the ui. """ PENDING = "Pending" WAITING_FOR_FEEDBACK = "Waiting for feedback" FEEDBACK_TO_HANDLE = "Feedback to handle" ACCEPTED = "Accepted" SCHEDULED = "Scheduled" FIRST_LISTING_DONE = "First listing done" FIRST_ORIGIN_LOADED = "First origin loaded" REJECTED = "Rejected" SUSPENDED = "Suspended" - DENIED = "Denied" + UNSUCCESSFUL = "Unsuccessful" @classmethod def choices(cls): return tuple((variant.name, variant.value) for variant in cls) def allowed_next_statuses(self) -> List[RequestStatus]: next_statuses = { self.PENDING: [self.WAITING_FOR_FEEDBACK, self.REJECTED, self.SUSPENDED], self.WAITING_FOR_FEEDBACK: [self.FEEDBACK_TO_HANDLE], self.FEEDBACK_TO_HANDLE: [ self.WAITING_FOR_FEEDBACK, self.ACCEPTED, self.REJECTED, self.SUSPENDED, + self.UNSUCCESSFUL, ], self.ACCEPTED: [self.SCHEDULED], self.SCHEDULED: [ self.FIRST_LISTING_DONE, # in case of race condition between lister and loader: self.FIRST_ORIGIN_LOADED, ], self.FIRST_LISTING_DONE: [self.FIRST_ORIGIN_LOADED], self.FIRST_ORIGIN_LOADED: [], self.REJECTED: [], self.SUSPENDED: [self.PENDING], - self.DENIED: [], + self.UNSUCCESSFUL: [], } return next_statuses[self] # type: ignore class RequestActorRole(enum.Enum): MODERATOR = "moderator" SUBMITTER = "submitter" FORGE_ADMIN = "forge admin" EMAIL = "email" @classmethod def choices(cls): return tuple((variant.name, variant.value) for variant in cls) class RequestHistory(models.Model): """Comment or status change. This is commented or changed by either submitter or moderator. """ request = models.ForeignKey("Request", models.DO_NOTHING) text = models.TextField() actor = models.TextField() actor_role = models.TextField(choices=RequestActorRole.choices()) date = models.DateTimeField(auto_now_add=True) new_status = models.TextField(choices=RequestStatus.choices(), null=True) message_source = models.BinaryField(null=True) class Meta: app_label = APP_LABEL db_table = "add_forge_request_history" class Request(models.Model): status = models.TextField( choices=RequestStatus.choices(), default=RequestStatus.PENDING.name, ) submission_date = models.DateTimeField(auto_now_add=True) submitter_name = models.TextField() submitter_email = models.TextField() submitter_forward_username = models.BooleanField(default=False) # FIXME: shall we do create a user model inside the webapp instead? forge_type = models.TextField() forge_url = models.TextField() forge_contact_email = models.EmailField() forge_contact_name = models.TextField() forge_contact_comment = models.TextField( null=True, help_text="Where did you find this contact information (url, ...)", ) last_moderator = models.TextField(default="None") last_modified_date = models.DateTimeField(null=True) class Meta: app_label = APP_LABEL db_table = "add_forge_request" @property def inbound_email_address(self) -> str: """Generate an email address for correspondence related to this request.""" base_address = get_config()["add_forge_now"]["email_address"] return get_address_for_pk(salt=APP_LABEL, base_address=base_address, pk=self.pk) @property def forge_domain(self) -> str: """Get the domain/netloc out of the forge_url. Fallback to using the first part of the url path, if the netloc can't be found (for instance, if the url scheme hasn't been set). """ parsed_url = urlparse(self.forge_url) domain = parsed_url.netloc if not domain: domain = parsed_url.path.split("/", 1)[0] return domain diff --git a/swh/web/templates/add_forge_now/help.html b/swh/web/templates/add_forge_now/help.html index 650368f2..9b4d7092 100644 --- a/swh/web/templates/add_forge_now/help.html +++ b/swh/web/templates/add_forge_now/help.html @@ -1,89 +1,89 @@ {% extends "./common.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 %} {% block tab_content %}
For submitting an "Add forge now" request", you have to provide the following details:
Once submitted, your "add forge" request can be in one of the following states