diff --git a/assets/src/bundles/add_forge/request-dashboard.js b/assets/src/bundles/add_forge/request-dashboard.js index 3fed8106..4839c649 100644 --- a/assets/src/bundles/add_forge/request-dashboard.js +++ b/assets/src/bundles/add_forge/request-dashboard.js @@ -1,138 +1,118 @@ /** * 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 {csrfPost, getHumanReadableDate, handleFetchError} from 'utils/functions'; import requestHistoryItem from './add-request-history-item.ejs'; +import emailTempate from './forge-admin-email.ejs'; let forgeRequest; -export function onRequestDashboardLoad(requestId) { +export function onRequestDashboardLoad(requestId, nextStatusesFor) { $(document).ready(() => { - populateRequestDetails(requestId); + populateRequestDetails(requestId, nextStatusesFor); $('#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); + populateRequestDetails(requestId, nextStatusesFor); } catch (response) { $('#userMessage').text('Sorry; Updating the request failed'); $('#userMessage').removeClass('badge-success'); $('#userMessage').addClass('badge-danger'); } }); }); } -async function populateRequestDetails(requestId) { +async function populateRequestDetails(requestId, nextStatusesFor) { 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); + populateDecisionSelectOption(forgeRequest.status, nextStatusesFor); } 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'], - 'UNSUCCESSFUL': [] - }; +export function populateDecisionSelectOption(currentStatus, nextStatusesFor) { // 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( `${label}` ); } // Remove all the options and add new ones $('#decisionOptions').children().remove(); nextStatuses.forEach(addStatusOption); $('#decisionOptions').append( ' -- Add a comment -- ' ); } 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/models.py b/swh/web/add_forge_now/models.py index 14da6c9d..1f8bae2d 100644 --- a/swh/web/add_forge_now/models.py +++ b/swh/web/add_forge_now/models.py @@ -1,142 +1,152 @@ # 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 typing import Dict, 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" 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, + @classmethod + def next_statuses(cls) -> Dict[RequestStatus, List[RequestStatus]]: + return { + cls.PENDING: [cls.WAITING_FOR_FEEDBACK, cls.REJECTED, cls.SUSPENDED], + cls.WAITING_FOR_FEEDBACK: [cls.FEEDBACK_TO_HANDLE], + cls.FEEDBACK_TO_HANDLE: [ + cls.WAITING_FOR_FEEDBACK, + cls.ACCEPTED, + cls.REJECTED, + cls.SUSPENDED, + cls.UNSUCCESSFUL, ], - self.ACCEPTED: [self.SCHEDULED], - self.SCHEDULED: [ - self.FIRST_LISTING_DONE, + cls.ACCEPTED: [cls.SCHEDULED], + cls.SCHEDULED: [ + cls.FIRST_LISTING_DONE, # in case of race condition between lister and loader: - self.FIRST_ORIGIN_LOADED, + cls.FIRST_ORIGIN_LOADED, ], - self.FIRST_LISTING_DONE: [self.FIRST_ORIGIN_LOADED], - self.FIRST_ORIGIN_LOADED: [], - self.REJECTED: [], - self.SUSPENDED: [self.PENDING], - self.UNSUCCESSFUL: [], + cls.FIRST_LISTING_DONE: [cls.FIRST_ORIGIN_LOADED], + cls.FIRST_ORIGIN_LOADED: [], + cls.REJECTED: [], + cls.SUSPENDED: [cls.PENDING], + cls.UNSUCCESSFUL: [], } - return next_statuses[self] # type: ignore + + @classmethod + def next_statuses_str(cls) -> Dict[str, List[str]]: + return { + key.name: [value.name for value in values] + for key, values in cls.next_statuses().items() + } + + def allowed_next_statuses(self) -> List[RequestStatus]: + return self.next_statuses()[self] 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.URLField() 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/admin/add_forge_now.py b/swh/web/admin/add_forge_now.py index a407bd99..64a1e260 100644 --- a/swh/web/admin/add_forge_now.py +++ b/swh/web/admin/add_forge_now.py @@ -1,39 +1,44 @@ # 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 django.conf import settings from django.contrib.auth.decorators import user_passes_test from django.shortcuts import render +from swh.web.add_forge_now.models import RequestStatus from swh.web.admin.adminurls import admin_route from swh.web.auth.utils import is_add_forge_now_moderator @admin_route( r"add-forge/requests/", view_name="add-forge-now-requests-moderation", ) @user_passes_test(is_add_forge_now_moderator, login_url=settings.LOGIN_URL) def add_forge_now_requests_moderation_dashboard(request): """Moderation dashboard to allow listing current requests.""" return render( request, "add_forge_now/requests-moderation.html", {"heading": "Add forge now requests moderation"}, ) @admin_route( r"add-forge/request/(?P(\d)+)/", view_name="add-forge-now-request-dashboard", ) @user_passes_test(is_add_forge_now_moderator, login_url=settings.LOGIN_URL) def add_forge_now_request_dashboard(request, request_id): """Moderation dashboard to allow listing current requests.""" return render( request, "add_forge_now/request-dashboard.html", - {"request_id": request_id, "heading": "Add forge now request dashboard"}, + { + "request_id": request_id, + "heading": "Add forge now request dashboard", + "next_statuses_for": RequestStatus.next_statuses_str(), + }, ) diff --git a/swh/web/templates/add_forge_now/request-dashboard.html b/swh/web/templates/add_forge_now/request-dashboard.html index 0f9e14c9..c263b910 100644 --- a/swh/web/templates/add_forge_now/request-dashboard.html +++ b/swh/web/templates/add_forge_now/request-dashboard.html @@ -1,121 +1,122 @@ {% 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 %} +{% load swh_templatetags %} {% block header %} {% render_bundle 'add_forge' %} {% endblock %} {% block title %}{{heading}} – Software Heritage archive{% endblock %} {% block navbar-content %} Add forge now request dashboard {% endblock %} {% block content %} Error fetching information about the request {% csrf_token %} Choose your decision Comment Enter a comment related to your decision. Submit Request status Forge type Forge URL Contact name Consent to use name Contact email Message Send message to the forge {% endblock %}