Page MenuHomeSoftware Heritage

D7396.id.diff
No OneTemporary

D7396.id.diff

diff --git a/assets/src/bundles/admin/index.js b/assets/src/bundles/admin/index.js
--- a/assets/src/bundles/admin/index.js
+++ b/assets/src/bundles/admin/index.js
@@ -1,9 +1,10 @@
/**
- * Copyright (C) 2018 The Software Heritage developers
+ * Copyright (C) 2018-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
*/
export * from './deposit';
+export * from './mailmap';
export * from './origin-save';
diff --git a/assets/src/bundles/admin/mailmap-form.ejs b/assets/src/bundles/admin/mailmap-form.ejs
new file mode 100644
--- /dev/null
+++ b/assets/src/bundles/admin/mailmap-form.ejs
@@ -0,0 +1,29 @@
+<%#
+ 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
+%>
+
+<form id="swh-mailmap-form" class="text-left">
+ <div class="form-group">
+ <label for="swh-mailmap-from-email">Email address</label>
+ <input type="email" class="form-control" id="swh-mailmap-from-email" value="<%= email %>"
+ <% if (updateForm) { %> readonly <% } %> required>
+ </div>
+ <div class="form-group">
+ <label for="swh-mailmap-display-name">Display name</label>
+ <input class="form-control" id="swh-mailmap-display-name" value="<%= displayName %>"
+ placeholder="John Doe <jdoe@example.org>" required>
+ </div>
+ <div class="custom-control custom-checkbox">
+ <input class="custom-control-input" type="checkbox" value=""
+ id="swh-mailmap-display-name-activated"
+ <% if (displayNameActivated) { %> checked <% } %>>
+ <label class="custom-control-label pt-0"
+ for="swh-mailmap-display-name-activated">Activate display name</label>
+ </div>
+ <div class="d-flex justify-content-center">
+ <input id="swh-mailmap-form-submit" type="submit" value="<%= buttonText %>">
+ </div>
+</form>
\ No newline at end of file
diff --git a/assets/src/bundles/admin/mailmap.js b/assets/src/bundles/admin/mailmap.js
new file mode 100644
--- /dev/null
+++ b/assets/src/bundles/admin/mailmap.js
@@ -0,0 +1,159 @@
+/**
+ * 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 {csrfPost, handleFetchError} from 'utils/functions';
+
+import mailmapFormTemplate from './mailmap-form.ejs';
+
+let mailmapsTable;
+
+export function mailmapForm(buttonText, email = '', displayName = '',
+ displayNameActivated = false, update = false) {
+ return mailmapFormTemplate({
+ buttonText: buttonText,
+ email: email,
+ displayName: displayName,
+ displayNameActivated: displayNameActivated,
+ updateForm: update
+ });
+}
+
+function getMailmapDataFromForm() {
+ return {
+ 'from_email': $('#swh-mailmap-from-email').val(),
+ 'display_name': $('#swh-mailmap-display-name').val(),
+ 'display_name_activated': $('#swh-mailmap-display-name-activated').prop('checked')
+ };
+}
+
+function processMailmapForm(formTitle, formHtml, formApiUrl) {
+ swh.webapp.showModalHtml(formTitle, formHtml);
+ $(`#swh-mailmap-form`).on('submit', async event => {
+ event.preventDefault();
+ event.stopPropagation();
+ const postData = getMailmapDataFromForm();
+ try {
+ const response = await csrfPost(
+ formApiUrl, {'Content-Type': 'application/json'}, JSON.stringify(postData)
+ );
+ $('#swh-web-modal-html').modal('hide');
+ handleFetchError(response);
+ mailmapsTable.draw();
+ } catch (response) {
+ const error = await response.text();
+ swh.webapp.showModalMessage('Error', error);
+ }
+ });
+}
+
+export function addNewMailmap() {
+ const mailmapFormHtml = mailmapForm('Add mailmap');
+ processMailmapForm('Add new mailmap', mailmapFormHtml, Urls.profile_mailmap_add());
+}
+
+export function updateMailmap(mailmapId) {
+ let mailmapData;
+ const rows = mailmapsTable.rows().data();
+ for (let i = 0; i < rows.length; ++i) {
+ const row = rows[i];
+ if (row.id === mailmapId) {
+ mailmapData = row;
+ break;
+ }
+ }
+ const mailmapFormHtml = mailmapForm('Update mailmap', mailmapData.from_email,
+ mailmapData.display_name,
+ mailmapData.display_name_activated, true);
+ processMailmapForm('Update existing mailmap', mailmapFormHtml, Urls.profile_mailmap_update());
+}
+
+const mdiCheckBold = '<i class="mdi mdi-check-bold" aria-hidden="true"></i>';
+const mdiCloseThick = '<i class="mdi mdi-close-thick" aria-hidden="true"></i>';
+
+export function initMailmapUI() {
+ $(document).ready(() => {
+ mailmapsTable = $('#swh-mailmaps-table')
+ .on('error.dt', (e, settings, techNote, message) => {
+ $('#swh-mailmaps-list-error').text(
+ 'An error occurred while retrieving the mailmaps list');
+ console.log(message);
+ })
+ .DataTable({
+ serverSide: true,
+ ajax: Urls.profile_mailmap_list_datatables(),
+ columns: [
+ {
+ data: 'from_email',
+ name: 'from_email',
+ render: $.fn.dataTable.render.text()
+ },
+ {
+ data: 'from_email_verified',
+ name: 'from_email_verified',
+ render: (data, type, row) => {
+ return data ? mdiCheckBold : mdiCloseThick;
+ },
+ className: 'dt-center'
+ },
+ {
+ data: 'display_name',
+ name: 'display_name',
+ render: $.fn.dataTable.render.text()
+ },
+ {
+ data: 'display_name_activated',
+ name: 'display_name_activated',
+ render: (data, type, row) => {
+ return data ? mdiCheckBold : mdiCloseThick;
+ },
+ className: 'dt-center'
+ },
+ {
+ data: 'last_update_date',
+ name: 'last_update_date',
+ render: (data, type, row) => {
+ if (type === 'display') {
+ const date = new Date(data);
+ return date.toLocaleString();
+ }
+ return data;
+ }
+ },
+ {
+ render: (data, type, row) => {
+ const lastUpdateDate = new Date(row.last_update_date);
+ const lastProcessingDate = new Date(row.mailmap_last_processing_date);
+ if (!lastProcessingDate || lastProcessingDate < lastUpdateDate) {
+ return mdiCloseThick;
+ } else {
+ return mdiCheckBold;
+ }
+ },
+ className: 'dt-center',
+ orderable: false
+ },
+ {
+ render: (data, type, row) => {
+ const html =
+ `<button class="btn btn-default"
+ onclick="swh.admin.updateMailmap(${row.id})">
+ Edit
+ </button>`;
+ return html;
+ },
+ orderable: false
+ }
+
+ ],
+ ordering: true,
+ searching: true,
+ searchDelay: 1000,
+ scrollY: '50vh',
+ scrollCollapse: true
+ });
+ });
+}
diff --git a/cypress/integration/mailmap.spec.js b/cypress/integration/mailmap.spec.js
new file mode 100644
--- /dev/null
+++ b/cypress/integration/mailmap.spec.js
@@ -0,0 +1,226 @@
+/**
+ * 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
+ */
+
+const $ = Cypress.$;
+
+function fillFormAndSubmitMailmap(fromEmail, displayName, activated) {
+ if (fromEmail) {
+ cy.get('#swh-mailmap-from-email')
+ .clear()
+ .type(fromEmail, {delay: 0, force: true});
+ }
+
+ if (displayName) {
+ cy.get('#swh-mailmap-display-name')
+ .clear()
+ .type(displayName, {delay: 0, force: true});
+ }
+
+ if (activated) {
+ cy.get('#swh-mailmap-display-name-activated')
+ .check({force: true});
+ } else {
+ cy.get('#swh-mailmap-display-name-activated')
+ .uncheck({force: true});
+ }
+
+ cy.get('#swh-mailmap-form-submit')
+ .click();
+}
+
+function addNewMailmap(fromEmail, displayName, activated) {
+ cy.get('#swh-add-new-mailmap')
+ .click();
+
+ fillFormAndSubmitMailmap(fromEmail, displayName, activated);
+}
+
+function updateMailmap(fromEmail, displayName, activated) {
+ cy.contains('Edit')
+ .click();
+
+ fillFormAndSubmitMailmap(fromEmail, displayName, activated);
+}
+
+function checkMailmapRow(fromEmail, displayName, activated,
+ processed = false, row = 1, nbRows = 1) {
+ cy.get('tbody tr').then(rows => {
+ assert.equal(rows.length, 1);
+ const cells = rows[0].cells;
+ assert.equal($(cells[0]).text(), fromEmail);
+ assert.include($(cells[1]).html(), 'mdi-check-bold');
+ assert.equal($(cells[2]).text(), displayName);
+ assert.include($(cells[3]).html(), activated ? 'mdi-check-bold' : 'mdi-close-thick');
+ assert.notEqual($(cells[4]).text(), '');
+ assert.include($(cells[5]).html(), processed ? 'mdi-check-bold' : 'mdi-close-thick');
+ });
+}
+
+describe('Test mailmap administration', function() {
+
+ before(function() {
+ this.url = this.Urls.admin_mailmap();
+ });
+
+ beforeEach(function() {
+ cy.task('db:user_mailmap:delete');
+ cy.intercept('POST', this.Urls.profile_mailmap_add())
+ .as('mailmapAdd');
+ cy.intercept('POST', this.Urls.profile_mailmap_update())
+ .as('mailmapUpdate');
+ cy.intercept(`${this.Urls.profile_mailmap_list_datatables()}**`)
+ .as('mailmapList');
+ });
+
+ it('should not display mailmap admin link in sidebar when anonymous', function() {
+ cy.visit(this.url);
+ cy.get('.swh-mailmap-admin-item')
+ .should('not.exist');
+ });
+
+ it('should not display mailmap admin link when connected as unprivileged user', function() {
+ cy.userLogin();
+ cy.visit(this.url);
+
+ cy.get('.swh-mailmap-admin-item')
+ .should('not.exist');
+
+ });
+
+ it('should display mailmap admin link in sidebar when connected as privileged user', function() {
+ cy.mailmapAdminLogin();
+ cy.visit(this.url);
+
+ cy.get('.swh-mailmap-admin-item')
+ .should('exist');
+ });
+
+ it('should not create a new mailmap when input data are empty', function() {
+ cy.mailmapAdminLogin();
+ cy.visit(this.url);
+
+ addNewMailmap('', '', true);
+
+ cy.get('#swh-mailmap-form :invalid').should('exist');
+
+ cy.get('#swh-mailmap-form')
+ .should('be.visible');
+
+ });
+
+ it('should not create a new mailmap when from email is invalid', function() {
+ cy.mailmapAdminLogin();
+ cy.visit(this.url);
+
+ addNewMailmap('invalid_email', 'display name', true);
+
+ cy.get('#swh-mailmap-form :invalid').should('exist');
+
+ cy.get('#swh-mailmap-form')
+ .should('be.visible');
+ });
+
+ it('should create a new mailmap when input data are valid', function() {
+ cy.mailmapAdminLogin();
+ cy.visit(this.url);
+
+ const fromEmail = 'user@example.org';
+ const displayName = 'New user display name';
+ addNewMailmap(fromEmail, displayName, true);
+ cy.wait('@mailmapAdd');
+
+ cy.get('#swh-mailmap-form :invalid').should('not.exist');
+
+ // ensure table redraw before next check
+ cy.contains(fromEmail);
+
+ cy.get('#swh-mailmap-form')
+ .should('not.be.visible');
+
+ checkMailmapRow(fromEmail, displayName, true);
+
+ });
+
+ it('should not create a new mailmap for an email already mapped', function() {
+ cy.mailmapAdminLogin();
+ cy.visit(this.url);
+
+ const fromEmail = 'user@example.org';
+ const displayName = 'New user display name';
+ addNewMailmap(fromEmail, displayName, true);
+ cy.wait('@mailmapAdd');
+
+ addNewMailmap(fromEmail, displayName, true);
+ cy.wait('@mailmapAdd');
+
+ cy.get('#swh-mailmap-form')
+ .should('not.be.visible');
+
+ cy.contains('Error')
+ .should('be.visible');
+
+ checkMailmapRow(fromEmail, displayName, true);
+
+ });
+
+ it('should update a mailmap', function() {
+ cy.mailmapAdminLogin();
+ cy.visit(this.url);
+
+ const fromEmail = 'user@example.org';
+ const displayName = 'New display name';
+ addNewMailmap(fromEmail, displayName, false);
+ cy.wait('@mailmapAdd');
+
+ cy.get('#swh-mailmap-form :invalid').should('not.exist');
+
+ // ensure table redraw before next check
+ cy.contains(fromEmail);
+
+ cy.get('#swh-mailmap-form')
+ .should('not.be.visible');
+
+ checkMailmapRow(fromEmail, displayName, false);
+
+ const newDisplayName = 'Updated display name';
+ updateMailmap('', newDisplayName, true);
+ cy.wait('@mailmapUpdate');
+
+ cy.get('#swh-mailmap-form :invalid').should('not.exist');
+
+ // ensure table redraw before next check
+ cy.contains(fromEmail);
+
+ cy.get('#swh-mailmap-form')
+ .should('not.be.visible');
+
+ checkMailmapRow(fromEmail, newDisplayName, true);
+
+ });
+
+ it('should indicate when a mailmap has been processed', function() {
+ cy.mailmapAdminLogin();
+ cy.visit(this.url);
+
+ const fromEmail = 'user@example.org';
+ const displayName = 'New user display name';
+ addNewMailmap(fromEmail, displayName, true);
+ cy.wait('@mailmapAdd');
+
+ // ensure table redraw before next check
+ cy.contains(fromEmail);
+
+ checkMailmapRow(fromEmail, displayName, true, false);
+
+ cy.task('db:user_mailmap:mark_processed');
+
+ cy.visit(this.url);
+ checkMailmapRow(fromEmail, displayName, true, true);
+
+ });
+
+});
diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js
--- a/cypress/plugins/index.js
+++ b/cypress/plugins/index.js
@@ -7,6 +7,7 @@
const axios = require('axios');
const fs = require('fs');
+const sqlite3 = require('sqlite3').verbose();
async function httpGet(url) {
const response = await axios.get(url);
@@ -32,6 +33,10 @@
};
};
+function getDatabase() {
+ return new sqlite3.Database('./swh-web-test.sqlite3');
+}
+
module.exports = (on, config) => {
require('@cypress/code-coverage/task')(on, config);
// produce JSON files prior launching browser in order to dynamically generate tests
@@ -124,6 +129,23 @@
global.swhTestsData = swhTestsData;
}
return global.swhTestsData;
+ },
+ 'db:user_mailmap:delete': () => {
+ const db = getDatabase();
+ db.serialize(function() {
+ db.run('DELETE FROM user_mailmap');
+ db.run('DELETE FROM user_mailmap_event');
+ });
+ db.close();
+ return true;
+ },
+ 'db:user_mailmap:mark_processed': () => {
+ const db = getDatabase();
+ db.serialize(function() {
+ db.run('UPDATE user_mailmap SET mailmap_last_processing_date=datetime("now", "+1 hour")');
+ });
+ db.close();
+ return true;
}
});
return config;
diff --git a/cypress/support/index.js b/cypress/support/index.js
--- a/cypress/support/index.js
+++ b/cypress/support/index.js
@@ -84,6 +84,9 @@
body: ''
}).as('swhCoverageWidget');
}
+Cypress.Commands.add('mailmapAdminLogin', () => {
+ return loginUser('mailmap-admin', 'mailmap-admin');
+});
before(function() {
diff --git a/package.json b/package.json
--- a/package.json
+++ b/package.json
@@ -145,6 +145,7 @@
"schema-utils": "^4.0.0",
"script-loader": "^0.7.2",
"spdx-expression-parse": "^3.0.1",
+ "sqlite3": "^5.0.2",
"style-loader": "^3.3.1",
"stylelint": "^14.5.3",
"stylelint-config-standard": "^25.0.0",
diff --git a/swh/web/admin/mailmap.py b/swh/web/admin/mailmap.py
new file mode 100644
--- /dev/null
+++ b/swh/web/admin/mailmap.py
@@ -0,0 +1,16 @@
+# 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.contrib.auth.decorators import permission_required
+from django.shortcuts import render
+
+from swh.web.admin.adminurls import admin_route
+from swh.web.auth.utils import MAILMAP_ADMIN_PERMISSION
+
+
+@admin_route(r"mailmap/", view_name="admin-mailmap")
+@permission_required(MAILMAP_ADMIN_PERMISSION)
+def _admin_mailmap(request):
+ return render(request, "admin/mailmap.html")
diff --git a/swh/web/admin/urls.py b/swh/web/admin/urls.py
--- a/swh/web/admin/urls.py
+++ b/swh/web/admin/urls.py
@@ -9,6 +9,7 @@
from swh.web.admin.adminurls import AdminUrls
import swh.web.admin.deposit # noqa
+import swh.web.admin.mailmap # noqa
import swh.web.admin.origin_save # noqa
from swh.web.config import is_feature_enabled
diff --git a/swh/web/auth/mailmap.py b/swh/web/auth/mailmap.py
--- a/swh/web/auth/mailmap.py
+++ b/swh/web/auth/mailmap.py
@@ -4,15 +4,18 @@
# See top-level LICENSE file for more information
import json
+from typing import Any, Dict
from django.conf.urls import url
+from django.core.paginator import Paginator
from django.db import IntegrityError
from django.db.models import Q
+from django.http.request import HttpRequest
from django.http.response import (
HttpResponse,
HttpResponseBadRequest,
- HttpResponseForbidden,
HttpResponseNotFound,
+ JsonResponse,
)
from rest_framework import serializers
from rest_framework.decorators import api_view
@@ -20,7 +23,11 @@
from rest_framework.response import Response
from swh.web.auth.models import UserMailmap, UserMailmapEvent
-from swh.web.auth.utils import MAILMAP_PERMISSION
+from swh.web.auth.utils import (
+ MAILMAP_ADMIN_PERMISSION,
+ MAILMAP_PERMISSION,
+ any_permission_required,
+)
class UserMailmapSerializer(serializers.ModelSerializer):
@@ -30,18 +37,20 @@
@api_view(["GET"])
+@any_permission_required(MAILMAP_PERMISSION, MAILMAP_ADMIN_PERMISSION)
def profile_list_mailmap(request: Request) -> HttpResponse:
- if not request.user.has_perm(MAILMAP_PERMISSION):
- return HttpResponseForbidden()
+ mailmap_admin = request.user.has_perm(MAILMAP_ADMIN_PERMISSION)
- mms = UserMailmap.objects.filter(user_id=str(request.user.id),).all()
+ mms = UserMailmap.objects.filter(
+ user_id=None if mailmap_admin else str(request.user.id)
+ ).all()
return Response(UserMailmapSerializer(mms, many=True).data)
@api_view(["POST"])
+@any_permission_required(MAILMAP_PERMISSION, MAILMAP_ADMIN_PERMISSION)
def profile_add_mailmap(request: Request) -> HttpResponse:
- if not request.user.has_perm(MAILMAP_PERMISSION):
- return HttpResponseForbidden()
+ mailmap_admin = request.user.has_perm(MAILMAP_ADMIN_PERMISSION)
event = UserMailmapEvent.objects.create(
user_id=str(request.user.id),
@@ -51,29 +60,46 @@
from_email = request.data.pop("from_email", None)
if not from_email:
- return HttpResponseBadRequest("'from_email' must be provided and non-empty.")
+ return HttpResponseBadRequest(
+ "'from_email' must be provided and non-empty.", content_type="text/plain"
+ )
+
+ user_id = None if mailmap_admin else str(request.user.id)
+
+ from_email_verified = request.data.pop("from_email_verified", False)
+ if mailmap_admin:
+ # consider email verified when mailmap is added by admin
+ from_email_verified = True
try:
UserMailmap.objects.create(
- user_id=str(request.user.id), from_email=from_email, **request.data
+ user_id=user_id,
+ from_email=from_email,
+ from_email_verified=from_email_verified,
+ **request.data,
)
except IntegrityError as e:
- if "user_mailmap_from_email_key" in e.args[0]:
- return HttpResponseBadRequest("This 'from_email' already exists.")
+ if (
+ "user_mailmap_from_email_key" in e.args[0]
+ or "user_mailmap.from_email" in e.args[0]
+ ):
+ return HttpResponseBadRequest(
+ "This 'from_email' already exists.", content_type="text/plain"
+ )
else:
raise
event.successful = True
event.save()
- mm = UserMailmap.objects.get(user_id=str(request.user.id), from_email=from_email)
+ mm = UserMailmap.objects.get(user_id=user_id, from_email=from_email)
return Response(UserMailmapSerializer(mm).data)
@api_view(["POST"])
+@any_permission_required(MAILMAP_PERMISSION, MAILMAP_ADMIN_PERMISSION)
def profile_update_mailmap(request: Request) -> HttpResponse:
- if not request.user.has_perm(MAILMAP_PERMISSION):
- return HttpResponseForbidden()
+ mailmap_admin = request.user.has_perm(MAILMAP_ADMIN_PERMISSION)
event = UserMailmapEvent.objects.create(
user_id=str(request.user.id),
@@ -83,18 +109,20 @@
from_email = request.data.pop("from_email", None)
if not from_email:
- return HttpResponseBadRequest("'from_email' must be provided and non-empty.")
+ return HttpResponseBadRequest(
+ "'from_email' must be provided and non-empty.", content_type="text/plain"
+ )
- user_id = str(request.user.id)
+ user_id = None if mailmap_admin else str(request.user.id)
try:
to_update = (
- UserMailmap.objects.filter(Q(user_id__isnull=True) | Q(user_id=user_id))
+ UserMailmap.objects.filter(user_id=user_id)
.filter(from_email=from_email)
.get()
)
except UserMailmap.DoesNotExist:
- return HttpResponseNotFound()
+ return HttpResponseNotFound("'from_email' cannot be found in mailmaps.")
for attr, value in request.data.items():
setattr(to_update, attr, value)
@@ -108,6 +136,50 @@
return Response(UserMailmapSerializer(mm).data)
+@any_permission_required(MAILMAP_PERMISSION, MAILMAP_ADMIN_PERMISSION)
+def profile_list_mailmap_datatables(request: HttpRequest) -> HttpResponse:
+ mailmap_admin = request.user.has_perm(MAILMAP_ADMIN_PERMISSION)
+
+ mailmaps = UserMailmap.objects.filter(
+ user_id=None if mailmap_admin else str(request.user.id)
+ )
+
+ search_value = request.GET.get("search[value]", "")
+
+ column_order = request.GET.get("order[0][column]")
+ field_order = request.GET.get(f"columns[{column_order}][name]", "from_email")
+ order_dir = request.GET.get("order[0][dir]", "asc")
+ if order_dir == "desc":
+ field_order = "-" + field_order
+
+ mailmaps = mailmaps.order_by(field_order)
+
+ table_data: Dict[str, Any] = {}
+ table_data["draw"] = int(request.GET.get("draw", 1))
+ table_data["recordsTotal"] = mailmaps.count()
+
+ length = int(request.GET.get("length", 10))
+ page = int(request.GET.get("start", 0)) / length + 1
+
+ if search_value:
+ mailmaps = mailmaps.filter(
+ Q(from_email__icontains=search_value)
+ | Q(display_name__icontains=search_value)
+ )
+
+ table_data["recordsFiltered"] = mailmaps.count()
+
+ paginator = Paginator(mailmaps, length)
+
+ mailmaps_data = [
+ UserMailmapSerializer(mm).data for mm in paginator.page(int(page)).object_list
+ ]
+
+ table_data["data"] = mailmaps_data
+
+ return JsonResponse(table_data)
+
+
urlpatterns = [
url(r"^profile/mailmap/list/$", profile_list_mailmap, name="profile-mailmap-list",),
url(r"^profile/mailmap/add/$", profile_add_mailmap, name="profile-mailmap-add",),
@@ -116,4 +188,9 @@
profile_update_mailmap,
name="profile-mailmap-update",
),
+ url(
+ r"^profile/mailmap/list/datatables/$",
+ profile_list_mailmap_datatables,
+ name="profile-mailmap-list-datatables",
+ ),
]
diff --git a/swh/web/auth/migrations/0006_fix_mailmap_admin_user_id.py b/swh/web/auth/migrations/0006_fix_mailmap_admin_user_id.py
new file mode 100644
--- /dev/null
+++ b/swh/web/auth/migrations/0006_fix_mailmap_admin_user_id.py
@@ -0,0 +1,41 @@
+# 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 datetime
+
+from django.db import migrations
+
+
+def _set_first_mailmaps_as_edited_by_admin(apps, schema_editor):
+ """First mailmaps in production database have been created by a user
+ with "swh.web.mailmap" permission because no "swh.web.admin.mailmap"
+ permission existed at the time.
+
+ So change user_id to None to indicate these mailmaps have been created
+ by a mailmap administrator.
+ """
+ UserMailmap = apps.get_model("swh_web_auth", "UserMailmap")
+
+ for mailmap in UserMailmap.objects.filter(
+ last_update_date__lte=datetime.datetime(
+ 2022, 2, 12
+ ) # first mailmaps added on 2022/2/11 in production
+ ):
+ if mailmap.user_id is not None:
+ mailmap.user_id = None
+ mailmap.save()
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("swh_web_auth", "0005_usermailmapevent"),
+ ]
+
+ operations = [
+ migrations.RunPython(
+ _set_first_mailmaps_as_edited_by_admin, migrations.RunPython.noop
+ ),
+ ]
diff --git a/swh/web/auth/utils.py b/swh/web/auth/utils.py
--- a/swh/web/auth/utils.py
+++ b/swh/web/auth/utils.py
@@ -11,8 +11,11 @@
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
+from django.contrib.auth.decorators import user_passes_test
from django.http.request import HttpRequest
+from swh.web.common.exc import ForbiddenExc
+
OIDC_SWH_WEB_CLIENT_ID = "swh-web"
SWH_AMBASSADOR_PERMISSION = "swh.ambassador"
@@ -20,6 +23,7 @@
ADMIN_LIST_DEPOSIT_PERMISSION = "swh.web.admin.list_deposits"
MAILMAP_PERMISSION = "swh.web.mailmap"
ADD_FORGE_MODERATOR_PERMISSION = "swh.web.add_forge_now.moderator"
+MAILMAP_ADMIN_PERMISSION = "swh.web.admin.mailmap"
def _get_fernet(password: bytes, salt: bytes) -> Fernet:
@@ -96,3 +100,16 @@
return user.is_authenticated and (
user.is_staff or any([user.has_perm(perm) for perm in permissions])
)
+
+
+def any_permission_required(*perms):
+ """View decorator granting access to it if user has at least one
+ permission among those passed as parameters.
+ """
+
+ def check_perms(user):
+ if any(user.has_perm(perm) for perm in perms):
+ return True
+ raise ForbiddenExc
+
+ return user_passes_test(check_perms)
diff --git a/swh/web/common/utils.py b/swh/web/common/utils.py
--- a/swh/web/common/utils.py
+++ b/swh/web/common/utils.py
@@ -30,6 +30,7 @@
from swh.web.auth.utils import (
ADD_FORGE_MODERATOR_PERMISSION,
ADMIN_LIST_DEPOSIT_PERMISSION,
+ MAILMAP_ADMIN_PERMISSION,
)
from swh.web.common.exc import BadInputExc
from swh.web.common.typing import QueryParameters
@@ -316,6 +317,7 @@
"ADMIN_LIST_DEPOSIT_PERMISSION": ADMIN_LIST_DEPOSIT_PERMISSION,
"ADD_FORGE_MODERATOR_PERMISSION": ADD_FORGE_MODERATOR_PERMISSION,
"FEATURES": get_config()["features"],
+ "MAILMAP_ADMIN_PERMISSION": MAILMAP_ADMIN_PERMISSION,
}
diff --git a/swh/web/templates/admin/mailmap.html b/swh/web/templates/admin/mailmap.html
new file mode 100644
--- /dev/null
+++ b/swh/web/templates/admin/mailmap.html
@@ -0,0 +1,53 @@
+{% 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 swh_templatetags %}
+{% load render_bundle from webpack_loader %}
+
+{% block header %}
+{{ block.super }}
+{% render_bundle 'admin' %}
+{% endblock %}
+
+{% block title %} Mailmap administration &ndash; Software Heritage archive {% endblock %}
+
+{% block navbar-content %}
+<h4>Mailmap administration</h4>
+{% endblock %}
+
+{% block content %}
+ <p class="mt-3">
+ This interface enables to manage author display names in the archive based
+ on their emails.
+ </p>
+ <div class="float-right">
+ <button class="btn btn-default" id="swh-add-new-mailmap" onclick="swh.admin.addNewMailmap()">
+ Add new mailmap
+ </button>
+ </div>
+ <div style="padding-top: 3rem;">
+ <table id="swh-mailmaps-table" class="table swh-table swh-table-striped" width="100%">
+ <thead>
+ <tr>
+ <th title="Email identifying author in the archive">Email</th>
+ <th title="Is email verified ?">Verified</th>
+ <th title="New display name for the author in the archive">Display name</th>
+ <th title="Should display name be used in the archive ?">Activated</th>
+ <th title="Date when that mailmap was last updated">Last update</th>
+ <th title="Is last mailmap update processed and effective in the archive ?">Effective</th>
+ <th></th>
+ </tr>
+ </thead>
+ </table>
+ <p id="swh-mailmaps-list-error"></p>
+ </div>
+ <script>
+ swh.admin.initMailmapUI();
+ </script>
+{% endblock content %}
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
@@ -244,12 +244,20 @@
{% endif %}
{% endif %}
{% if user.is_staff or ADMIN_LIST_DEPOSIT_PERMISSION in user.get_all_permissions %}
- <li class="nav-item swh-deposit-admin-item" title="Deposit administration">
- <a href="{% url 'admin-deposit' %}" class="nav-link swh-deposit-admin-link">
- <i style="color: #fecd1b;" class="nav-icon mdi mdi-24px mdi-folder-upload"></i>
- <p>Deposit</p>
- </a>
- </li>
+ <li class="nav-item swh-deposit-admin-item" title="Deposit administration">
+ <a href="{% url 'admin-deposit' %}" class="nav-link swh-deposit-admin-link">
+ <i style="color: #fecd1b;" class="nav-icon mdi mdi-24px mdi-folder-upload"></i>
+ <p>Deposit</p>
+ </a>
+ </li>
+ {% endif %}
+ {% if MAILMAP_ADMIN_PERMISSION in user.get_all_permissions %}
+ <li class="nav-item swh-mailmap-admin-item" title="Mailmap administration">
+ <a href="{% url 'admin-mailmap' %}" class="nav-link swh-mailmap-admin-link">
+ <i style="color: #fecd1b;" class="nav-icon mdi mdi-24px mdi-email"></i>
+ <p>Mailmap</p>
+ </a>
+ </li>
{% endif %}
{% endif %}
</ul>
diff --git a/swh/web/tests/auth/test_mailmap.py b/swh/web/tests/auth/test_mailmap.py
--- a/swh/web/tests/auth/test_mailmap.py
+++ b/swh/web/tests/auth/test_mailmap.py
@@ -16,21 +16,14 @@
from swh.model.model import Person
from swh.web.auth.models import UserMailmap, UserMailmapEvent
-from swh.web.auth.utils import MAILMAP_PERMISSION
from swh.web.common.utils import reverse
from swh.web.tests.utils import (
check_api_post_response,
check_http_get_response,
- create_django_permission,
+ check_http_post_response,
)
-@pytest.fixture
-def mailmap_user(regular_user):
- regular_user.user_permissions.add(create_django_permission(MAILMAP_PERMISSION))
- return regular_user
-
-
@pytest.mark.django_db(transaction=True)
@pytest.mark.parametrize("view_name", ["profile-mailmap-add", "profile-mailmap-update"])
def test_mailmap_endpoints_anonymous_user(api_client, view_name):
@@ -39,172 +32,332 @@
@pytest.mark.django_db(transaction=True)
-def test_mailmap_endpoints_user_with_permission(api_client, mailmap_user):
- api_client.force_login(mailmap_user)
+def test_mailmap_endpoints_user_with_permission(
+ api_client, mailmap_user, mailmap_admin
+):
- request_data = {"from_email": "bar@example.org", "display_name": "bar"}
+ for user, name in ((mailmap_user, "bar"), (mailmap_admin, "baz")):
- for view_name in ("profile-mailmap-add", "profile-mailmap-update"):
- url = reverse(view_name)
- check_api_post_response(
- api_client, url, data=request_data, status_code=200,
- )
+ UserMailmapEvent.objects.all().delete()
- # FIXME: use check_api_get_responses; currently this crashes without
- # content_type="application/json"
- resp = check_http_get_response(
- api_client,
- reverse("profile-mailmap-list"),
- status_code=200,
- content_type="application/json",
- ).data
- assert len(resp) == 1
- assert resp[0]["from_email"] == "bar@example.org"
- assert resp[0]["display_name"] == "bar"
-
- events = UserMailmapEvent.objects.order_by("timestamp").all()
- assert len(events) == 2
- assert events[0].request_type == "add"
- assert json.loads(events[0].request) == request_data
- assert events[1].request_type == "update"
- assert json.loads(events[1].request) == request_data
+ api_client.force_login(user)
+
+ request_data = {"from_email": f"{name}@example.org", "display_name": name}
+
+ for view_name in ("profile-mailmap-add", "profile-mailmap-update"):
+ url = reverse(view_name)
+ check_api_post_response(
+ api_client, url, data=request_data, status_code=200,
+ )
+
+ # FIXME: use check_api_get_responses; currently this crashes without
+ # content_type="application/json"
+ resp = check_http_get_response(
+ api_client,
+ reverse("profile-mailmap-list"),
+ status_code=200,
+ content_type="application/json",
+ ).data
+ assert len(resp) == 1
+ assert resp[0]["from_email"] == f"{name}@example.org"
+ assert resp[0]["display_name"] == name
+
+ events = UserMailmapEvent.objects.order_by("timestamp").all()
+ assert len(events) == 2
+ assert events[0].request_type == "add"
+ assert json.loads(events[0].request) == request_data
+ assert events[1].request_type == "update"
+ assert json.loads(events[1].request) == request_data
@pytest.mark.django_db(transaction=True)
-def test_mailmap_add_duplicate(api_client, mailmap_user):
- api_client.force_login(mailmap_user)
+def test_mailmap_add_duplicate(api_client, mailmap_user, mailmap_admin):
- check_api_post_response(
- api_client,
- reverse("profile-mailmap-add"),
- data={"from_email": "foo@example.org", "display_name": "bar"},
- status_code=200,
- )
- check_api_post_response(
- api_client,
- reverse("profile-mailmap-add"),
- data={"from_email": "foo@example.org", "display_name": "baz"},
- status_code=400,
- )
+ for user, name in ((mailmap_user, "foo"), (mailmap_admin, "bar")):
+
+ api_client.force_login(user)
+
+ check_api_post_response(
+ api_client,
+ reverse("profile-mailmap-add"),
+ data={"from_email": f"{name}@example.org", "display_name": name},
+ status_code=200,
+ )
+ check_api_post_response(
+ api_client,
+ reverse("profile-mailmap-add"),
+ data={"from_email": f"{name}@example.org", "display_name": name},
+ status_code=400,
+ )
@pytest.mark.django_db(transaction=True)
-def test_mailmap_add_full(api_client, mailmap_user):
- api_client.force_login(mailmap_user)
-
- request_data = {
- "from_email": "foo@example.org",
- "from_email_verified": True,
- "from_email_verification_request_date": "2021-02-07T14:04:15Z",
- "display_name": "bar",
- "display_name_activated": True,
- "to_email": "bar@example.org",
- "to_email_verified": True,
- "to_email_verification_request_date": "2021-02-07T15:54:59Z",
- }
+def test_mailmap_add_full(api_client, mailmap_user, mailmap_admin):
- check_api_post_response(
- api_client, reverse("profile-mailmap-add"), data=request_data, status_code=200,
- )
+ for user, name in ((mailmap_user, "foo"), (mailmap_admin, "bar")):
- resp = check_http_get_response(
- api_client,
- reverse("profile-mailmap-list"),
- status_code=200,
- content_type="application/json",
- ).data
- assert len(resp) == 1
- assert resp[0].items() >= request_data.items()
+ api_client.force_login(user)
+
+ UserMailmapEvent.objects.all().delete()
+
+ request_data = {
+ "from_email": f"{name}@example.org",
+ "from_email_verified": True,
+ "from_email_verification_request_date": "2021-02-07T14:04:15Z",
+ "display_name": name,
+ "display_name_activated": True,
+ "to_email": "baz@example.org",
+ "to_email_verified": True,
+ "to_email_verification_request_date": "2021-02-07T15:54:59Z",
+ }
+
+ check_api_post_response(
+ api_client,
+ reverse("profile-mailmap-add"),
+ data=request_data,
+ status_code=200,
+ )
+
+ resp = check_http_get_response(
+ api_client,
+ reverse("profile-mailmap-list"),
+ status_code=200,
+ content_type="application/json",
+ ).data
+ assert len(resp) == 1
+ assert resp[0].items() >= request_data.items()
- events = UserMailmapEvent.objects.all()
- assert len(events) == 1
- assert events[0].request_type == "add"
- assert json.loads(events[0].request) == request_data
- assert events[0].successful
+ events = UserMailmapEvent.objects.all()
+ assert len(events) == 1
+ assert events[0].request_type == "add"
+ assert json.loads(events[0].request) == request_data
+ assert events[0].successful
@pytest.mark.django_db(transaction=True)
-def test_mailmap_endpoints_error_response(api_client, mailmap_user):
- api_client.force_login(mailmap_user)
+def test_mailmap_endpoints_error_response(api_client, mailmap_user, mailmap_admin):
+
+ for user in (mailmap_user, mailmap_admin):
+
+ api_client.force_login(user)
- url = reverse("profile-mailmap-add")
- resp = check_api_post_response(api_client, url, status_code=400)
- assert b"from_email" in resp.content
+ UserMailmapEvent.objects.all().delete()
- url = reverse("profile-mailmap-update")
- resp = check_api_post_response(api_client, url, status_code=400)
- assert b"from_email" in resp.content
+ url = reverse("profile-mailmap-add")
+ resp = check_api_post_response(api_client, url, status_code=400)
+ assert b"from_email" in resp.content
- events = UserMailmapEvent.objects.order_by("timestamp").all()
- assert len(events) == 2
+ url = reverse("profile-mailmap-update")
+ resp = check_api_post_response(api_client, url, status_code=400)
+ assert b"from_email" in resp.content
- assert events[0].request_type == "add"
- assert json.loads(events[0].request) == {}
- assert not events[0].successful
+ events = UserMailmapEvent.objects.order_by("timestamp").all()
+ assert len(events) == 2
- assert events[1].request_type == "update"
- assert json.loads(events[1].request) == {}
- assert not events[1].successful
+ assert events[0].request_type == "add"
+ assert json.loads(events[0].request) == {}
+ assert not events[0].successful
+
+ assert events[1].request_type == "update"
+ assert json.loads(events[1].request) == {}
+ assert not events[1].successful
@pytest.mark.django_db(transaction=True)
-def test_mailmap_update(api_client, mailmap_user):
- api_client.force_login(mailmap_user)
+def test_mailmap_update(api_client, mailmap_user, mailmap_admin):
- before_add = datetime.datetime.now(tz=datetime.timezone.utc)
+ for user, name in ((mailmap_user, "foo"), (mailmap_admin, "bar")):
- check_api_post_response(
- api_client,
- reverse("profile-mailmap-add"),
- data={"from_email": "orig1@example.org", "display_name": "Display Name 1"},
- status_code=200,
- )
- check_api_post_response(
- api_client,
- reverse("profile-mailmap-add"),
- data={"from_email": "orig2@example.org", "display_name": "Display Name 2"},
- status_code=200,
- )
- after_add = datetime.datetime.now(tz=datetime.timezone.utc)
+ api_client.force_login(user)
+
+ UserMailmapEvent.objects.all().delete()
+
+ before_add = datetime.datetime.now(tz=datetime.timezone.utc)
+
+ check_api_post_response(
+ api_client,
+ reverse("profile-mailmap-add"),
+ data={
+ "from_email": f"{name}1@example.org",
+ "display_name": "Display Name 1",
+ },
+ status_code=200,
+ )
+ check_api_post_response(
+ api_client,
+ reverse("profile-mailmap-add"),
+ data={
+ "from_email": f"{name}2@example.org",
+ "display_name": "Display Name 2",
+ },
+ status_code=200,
+ )
+ after_add = datetime.datetime.now(tz=datetime.timezone.utc)
+
+ user_id = None if user == mailmap_admin else str(user.id)
+
+ mailmaps = list(
+ UserMailmap.objects.filter(user_id=user_id).order_by("from_email").all()
+ )
+ assert len(mailmaps) == 2, mailmaps
+
+ assert mailmaps[0].from_email == f"{name}1@example.org", mailmaps
+ assert mailmaps[0].display_name == "Display Name 1", mailmaps
+ assert before_add <= mailmaps[0].last_update_date <= after_add
+
+ assert mailmaps[1].from_email == f"{name}2@example.org", mailmaps
+ assert mailmaps[1].display_name == "Display Name 2", mailmaps
+ assert before_add <= mailmaps[0].last_update_date <= after_add
+
+ before_update = datetime.datetime.now(tz=datetime.timezone.utc)
+
+ check_api_post_response(
+ api_client,
+ reverse("profile-mailmap-update"),
+ data={
+ "from_email": f"{name}1@example.org",
+ "display_name": "Display Name 1b",
+ },
+ status_code=200,
+ )
+
+ after_update = datetime.datetime.now(tz=datetime.timezone.utc)
- mailmaps = list(UserMailmap.objects.order_by("from_email").all())
- assert len(mailmaps) == 2, mailmaps
+ mailmaps = list(
+ UserMailmap.objects.filter(user_id=user_id).order_by("from_email").all()
+ )
+ assert len(mailmaps) == 2, mailmaps
- assert mailmaps[0].from_email == "orig1@example.org", mailmaps
- assert mailmaps[0].display_name == "Display Name 1", mailmaps
- assert before_add <= mailmaps[0].last_update_date <= after_add
+ assert mailmaps[0].from_email == f"{name}1@example.org", mailmaps
+ assert mailmaps[0].display_name == "Display Name 1b", mailmaps
+ assert before_update <= mailmaps[0].last_update_date <= after_update
- assert mailmaps[1].from_email == "orig2@example.org", mailmaps
- assert mailmaps[1].display_name == "Display Name 2", mailmaps
- assert before_add <= mailmaps[0].last_update_date <= after_add
+ assert mailmaps[1].from_email == f"{name}2@example.org", mailmaps
+ assert mailmaps[1].display_name == "Display Name 2", mailmaps
+ assert before_add <= mailmaps[1].last_update_date <= after_add
- before_update = datetime.datetime.now(tz=datetime.timezone.utc)
+ events = UserMailmapEvent.objects.order_by("timestamp").all()
+ assert len(events) == 3
+ assert events[0].request_type == "add"
+ assert events[1].request_type == "add"
+ assert events[2].request_type == "update"
+
+@pytest.mark.django_db(transaction=True)
+def test_mailmap_update_from_email_not_found(api_client, mailmap_admin):
+ api_client.force_login(mailmap_admin)
check_api_post_response(
api_client,
reverse("profile-mailmap-update"),
- data={"from_email": "orig1@example.org", "display_name": "Display Name 1b"},
- status_code=200,
+ data={"from_email": "invalid@example.org", "display_name": "Display Name",},
+ status_code=404,
)
- after_update = datetime.datetime.now(tz=datetime.timezone.utc)
- mailmaps = list(UserMailmap.objects.order_by("from_email").all())
- assert len(mailmaps) == 2, mailmaps
+NB_MAILMAPS = 20
+MM_PER_PAGE = 10
+
+
+def _create_mailmaps(client):
+ mailmaps = []
+ for i in range(NB_MAILMAPS):
+ resp = check_http_post_response(
+ client,
+ reverse("profile-mailmap-add"),
+ data={
+ "from_email": f"user{i:02d}@example.org",
+ "display_name": f"User {i:02d}",
+ },
+ status_code=200,
+ )
+ mailmaps.append(json.loads(resp.content))
+ return mailmaps
+
+
+@pytest.mark.django_db(transaction=True, reset_sequences=True)
+def test_mailmap_list_datatables_no_parameters(client, mailmap_admin):
+ client.force_login(mailmap_admin)
+ mailmaps = _create_mailmaps(client)
+
+ url = reverse("profile-mailmap-list-datatables")
+
+ resp = check_http_get_response(client, url, status_code=200)
+ mailmap_data = json.loads(resp.content)
+
+ assert mailmap_data["recordsTotal"] == NB_MAILMAPS
+ assert mailmap_data["recordsFiltered"] == NB_MAILMAPS
+
+ # mailmaps sorted by ascending from_email by default
+ for i in range(10):
+ assert mailmap_data["data"][i]["from_email"] == mailmaps[i]["from_email"]
+
+
+@pytest.mark.django_db(transaction=True, reset_sequences=True)
+@pytest.mark.parametrize("sort_direction", ["asc", "desc"])
+def test_mailmap_list_datatables_ordering(client, mailmap_admin, sort_direction):
+ client.force_login(mailmap_admin)
+ mailmaps = _create_mailmaps(client)
+ mailmaps_sorted = list(sorted(mailmaps, key=lambda d: d["display_name"]))
+ all_display_names = [mm["display_name"] for mm in mailmaps_sorted]
+ if sort_direction == "desc":
+ all_display_names = list(reversed(all_display_names))
+
+ for i in range(NB_MAILMAPS // MM_PER_PAGE):
+ url = reverse(
+ "profile-mailmap-list-datatables",
+ query_params={
+ "draw": i,
+ "length": MM_PER_PAGE,
+ "start": i * MM_PER_PAGE,
+ "order[0][column]": 2,
+ "order[0][dir]": sort_direction,
+ "columns[2][name]": "display_name",
+ },
+ )
+
+ resp = check_http_get_response(client, url, status_code=200)
+ data = json.loads(resp.content)
+
+ assert data["draw"] == i
+ assert data["recordsFiltered"] == NB_MAILMAPS
+ assert data["recordsTotal"] == NB_MAILMAPS
+ assert len(data["data"]) == MM_PER_PAGE
+
+ display_names = [mm["display_name"] for mm in data["data"]]
+
+ expected_display_names = all_display_names[
+ i * MM_PER_PAGE : (i + 1) * MM_PER_PAGE
+ ]
+ assert display_names == expected_display_names
+
+
+@pytest.mark.django_db(transaction=True, reset_sequences=True)
+def test_mailmap_list_datatables_search(client, mailmap_admin):
+ client.force_login(mailmap_admin)
+ _create_mailmaps(client)
+
+ search_value = "user1"
+
+ url = reverse(
+ "profile-mailmap-list-datatables",
+ query_params={
+ "draw": 1,
+ "length": MM_PER_PAGE,
+ "start": 0,
+ "search[value]": search_value,
+ },
+ )
- assert mailmaps[0].from_email == "orig1@example.org", mailmaps
- assert mailmaps[0].display_name == "Display Name 1b", mailmaps
- assert before_update <= mailmaps[0].last_update_date <= after_update
+ resp = check_http_get_response(client, url, status_code=200)
+ data = json.loads(resp.content)
- assert mailmaps[1].from_email == "orig2@example.org", mailmaps
- assert mailmaps[1].display_name == "Display Name 2", mailmaps
- assert before_add <= mailmaps[1].last_update_date <= after_add
+ assert data["draw"] == 1
+ assert data["recordsFiltered"] == MM_PER_PAGE
+ assert data["recordsTotal"] == NB_MAILMAPS
+ assert len(data["data"]) == MM_PER_PAGE
- events = UserMailmapEvent.objects.order_by("timestamp").all()
- assert len(events) == 3
- assert events[0].request_type == "add"
- assert events[1].request_type == "add"
- assert events[2].request_type == "update"
+ for mailmap in data["data"]:
+ assert search_value in mailmap["from_email"]
def populate_mailmap():
diff --git a/swh/web/tests/auth/test_migrations.py b/swh/web/tests/auth/test_migrations.py
new file mode 100644
--- /dev/null
+++ b/swh/web/tests/auth/test_migrations.py
@@ -0,0 +1,38 @@
+# Copyright (C) 2022 The Software Heritage developers
+# See the AUTHORS file at the top-level directory of this distribution
+# License: GNU General Public License version 3, or any later version
+# See top-level LICENSE file for more information
+
+from datetime import datetime
+
+APP_NAME = "swh_web_auth"
+
+MIGRATION_0005 = "0005_usermailmapevent"
+MIGRATION_0006 = "0006_fix_mailmap_admin_user_id"
+
+
+def test_fix_mailmap_admin_user_id(migrator):
+ state = migrator.apply_tested_migration((APP_NAME, MIGRATION_0005))
+ UserMailmap = state.apps.get_model(APP_NAME, "UserMailmap")
+
+ user_id = "45"
+
+ UserMailmap.objects.create(
+ user_id=user_id,
+ from_email="user@example.org",
+ from_email_verified=True,
+ display_name="New display name",
+ )
+
+ UserMailmap.objects.filter(user_id=user_id).update(
+ last_update_date=datetime(2022, 2, 11, 14, 16, 13, 614000)
+ )
+
+ assert UserMailmap.objects.filter(user_id=user_id).count() == 1
+ assert UserMailmap.objects.filter(user_id=None).count() == 0
+
+ state = migrator.apply_tested_migration((APP_NAME, MIGRATION_0006))
+ UserMailmap = state.apps.get_model(APP_NAME, "UserMailmap")
+
+ assert UserMailmap.objects.filter(user_id=user_id).count() == 0
+ assert UserMailmap.objects.filter(user_id=None).count() == 1
diff --git a/swh/web/tests/conftest.py b/swh/web/tests/conftest.py
--- a/swh/web/tests/conftest.py
+++ b/swh/web/tests/conftest.py
@@ -36,7 +36,12 @@
from swh.storage.algos.origin import origin_get_latest_visit_status
from swh.storage.algos.revisions_walker import get_revisions_walker
from swh.storage.algos.snapshot import snapshot_get_all_branches, snapshot_get_latest
-from swh.web.auth.utils import ADD_FORGE_MODERATOR_PERMISSION, OIDC_SWH_WEB_CLIENT_ID
+from swh.web.auth.utils import (
+ ADD_FORGE_MODERATOR_PERMISSION,
+ MAILMAP_ADMIN_PERMISSION,
+ MAILMAP_PERMISSION,
+ OIDC_SWH_WEB_CLIENT_ID,
+)
from swh.web.common import converters
from swh.web.common.origin_save import get_scheduler_load_task_types
from swh.web.common.typing import OriginVisitInfo
@@ -1224,3 +1229,19 @@
create_django_permission(ADD_FORGE_MODERATOR_PERMISSION)
)
return moderator
+
+
+@pytest.fixture
+def mailmap_admin():
+ mailmap_admin = User.objects.create_user(username="mailmap-admin", password="")
+ mailmap_admin.user_permissions.add(
+ create_django_permission(MAILMAP_ADMIN_PERMISSION)
+ )
+ return mailmap_admin
+
+
+@pytest.fixture
+def mailmap_user():
+ mailmap_user = User.objects.create_user(username="mailmap-user", password="")
+ mailmap_user.user_permissions.add(create_django_permission(MAILMAP_PERMISSION))
+ return mailmap_user
diff --git a/swh/web/tests/create_test_users.py b/swh/web/tests/create_test_users.py
--- a/swh/web/tests/create_test_users.py
+++ b/swh/web/tests/create_test_users.py
@@ -11,6 +11,7 @@
ADD_FORGE_MODERATOR_PERMISSION,
ADMIN_LIST_DEPOSIT_PERMISSION,
SWH_AMBASSADOR_PERMISSION,
+ MAILMAP_ADMIN_PERMISSION,
)
from swh.web.tests.utils import create_django_permission
@@ -24,12 +25,17 @@
"ambassador@example.org",
[SWH_AMBASSADOR_PERMISSION],
),
- "deposit": ("deposit", "deposit@example.org", [ADMIN_LIST_DEPOSIT_PERMISSION],),
+ "deposit": ("deposit", "deposit@example.org", [ADMIN_LIST_DEPOSIT_PERMISSION]),
"add-forge-moderator": (
"add-forge-moderator",
"moderator@example.org",
[ADD_FORGE_MODERATOR_PERMISSION],
),
+ "mailmap-admin": (
+ "mailmap-admin",
+ "mailmap-admin@example.org",
+ [MAILMAP_ADMIN_PERMISSION],
+ ),
}
diff --git a/swh/web/tests/utils.py b/swh/web/tests/utils.py
--- a/swh/web/tests/utils.py
+++ b/swh/web/tests/utils.py
@@ -228,7 +228,9 @@
app_label = ".".join(perm_splitted[:-1])
perm_name = perm_splitted[-1]
content_type = ContentType.objects.create(
- id=1000 + ContentType.objects.count(), app_label=app_label, model="dummy"
+ id=1000 + ContentType.objects.count(),
+ app_label=app_label,
+ model=perm_splitted[-1],
)
return Permission.objects.create(
diff --git a/yarn.lock b/yarn.lock
--- a/yarn.lock
+++ b/yarn.lock
@@ -1885,6 +1885,11 @@
resolved "https://registry.yarnpkg.com/ansi-html-community/-/ansi-html-community-0.0.8.tgz#69fbc4d6ccbe383f9736934ae34c3f8290f1bf41"
integrity sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==
+ansi-regex@^2.0.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
+ integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8=
+
ansi-regex@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998"
@@ -1940,6 +1945,11 @@
dependencies:
default-require-extensions "^3.0.0"
+aproba@^1.0.3:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a"
+ integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==
+
"aproba@^1.0.3 || ^2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc"
@@ -1963,6 +1973,14 @@
delegates "^1.0.0"
readable-stream "^3.6.0"
+are-we-there-yet@~1.1.2:
+ version "1.1.7"
+ resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz#b15474a932adab4ff8a50d9adfa7e4e926f21146"
+ integrity sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==
+ dependencies:
+ delegates "^1.0.0"
+ readable-stream "^2.0.6"
+
argparse@^1.0.7:
version "1.0.10"
resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
@@ -2261,6 +2279,13 @@
resolved "https://registry.yarnpkg.com/blob-util/-/blob-util-2.0.2.tgz#3b4e3c281111bb7f11128518006cdc60b403a1eb"
integrity sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==
+block-stream@*:
+ version "0.0.9"
+ resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a"
+ integrity sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=
+ dependencies:
+ inherits "~2.0.0"
+
bluebird@^3.7.2:
version "3.7.2"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
@@ -2889,6 +2914,11 @@
resolved "https://registry.yarnpkg.com/chosen-js/-/chosen-js-1.8.7.tgz#9bfa5597f5081d602ff4ae904af9aef33265bb1d"
integrity sha512-eVdrZJ2U5ISdObkgsi0od5vIJdLwq1P1Xa/Vj/mgxkMZf14DlgobfB6nrlFi3kW4kkvKLsKk4NDqZj1MU1DCpw==
+chownr@^1.1.4:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b"
+ integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==
+
chownr@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece"
@@ -3022,6 +3052,11 @@
resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e"
integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4=
+code-point-at@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
+ integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=
+
codemirror@^5.65.1:
version "5.65.1"
resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.65.1.tgz#5988a812c974c467f964bcc1a00c944e373de502"
@@ -3209,7 +3244,7 @@
resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336"
integrity sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==
-console-control-strings@^1.0.0, console-control-strings@^1.1.0:
+console-control-strings@^1.0.0, console-control-strings@^1.1.0, console-control-strings@~1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=
@@ -4122,7 +4157,7 @@
dependencies:
ms "2.1.2"
-debug@^3.1.0, debug@^3.1.1, debug@^3.2.7:
+debug@^3.1.0, debug@^3.1.1, debug@^3.2.6, debug@^3.2.7:
version "3.2.7"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a"
integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==
@@ -4294,6 +4329,11 @@
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=
+detect-libc@^1.0.2:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
+ integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=
+
detect-node@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.4.tgz#014ee8f8f669c5c58023da64b8179c083a28c46c"
@@ -5437,6 +5477,13 @@
jsonfile "^6.0.1"
universalify "^2.0.0"
+fs-minipass@^1.2.7:
+ version "1.2.7"
+ resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.7.tgz#ccff8570841e7fe4265693da88936c55aed7f7c7"
+ integrity sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==
+ dependencies:
+ minipass "^2.6.0"
+
fs-minipass@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb"
@@ -5459,6 +5506,16 @@
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
+fstream@^1.0.0, fstream@^1.0.12:
+ version "1.0.12"
+ resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.12.tgz#4e8ba8ee2d48be4f7d0de505455548eae5932045"
+ integrity sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==
+ dependencies:
+ graceful-fs "^4.1.2"
+ inherits "~2.0.0"
+ mkdirp ">=0.5 0"
+ rimraf "2"
+
fsu@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/fsu/-/fsu-1.1.1.tgz#bd36d3579907c59d85b257a75b836aa9e0c31834"
@@ -5509,6 +5566,20 @@
strip-ansi "^6.0.1"
wide-align "^1.1.2"
+gauge@~2.7.3:
+ version "2.7.4"
+ resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
+ integrity sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=
+ dependencies:
+ aproba "^1.0.3"
+ console-control-strings "^1.0.0"
+ has-unicode "^2.0.0"
+ object-assign "^4.1.0"
+ signal-exit "^3.0.0"
+ string-width "^1.0.1"
+ strip-ansi "^3.0.1"
+ wide-align "^1.1.0"
+
gaze@^1.0.0:
version "1.1.3"
resolved "https://registry.yarnpkg.com/gaze/-/gaze-1.1.3.tgz#c441733e13b927ac8c0ff0b4c3b033f28812924a"
@@ -5831,7 +5902,7 @@
dependencies:
has-symbols "^1.0.2"
-has-unicode@^2.0.1:
+has-unicode@^2.0.0, has-unicode@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9"
integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=
@@ -6302,7 +6373,7 @@
resolved "https://registry.yarnpkg.com/icheck-bootstrap/-/icheck-bootstrap-3.0.1.tgz#60c9c9a71524e1d9dd5bd05167a62fef05cc3a1b"
integrity sha512-Rj3SybdcMcayhsP4IJ+hmCNgCKclaFcs/5zwCuLXH1WMo468NegjhZVxbSNKhEjJjnwc4gKETogUmPYSQ9lEZQ==
-iconv-lite@0.4.24:
+iconv-lite@0.4.24, iconv-lite@^0.4.4:
version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
@@ -6336,6 +6407,13 @@
resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09"
integrity sha1-SMptcvbGo68Aqa1K5odr44ieKwk=
+ignore-walk@^3.0.1:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.4.tgz#c9a09f69b7c7b479a5d74ac1a3c0d4236d2a6335"
+ integrity sha512-PY6Ii8o1jMRA1z4F2hRkH/xN59ox43DavKvD3oDpfurRlOJyAHpifIwpbdv1n4jt4ov0jSpw3kQ4GhJnpBL6WQ==
+ dependencies:
+ minimatch "^3.0.4"
+
ignore@^5.1.1, ignore@^5.1.4, ignore@^5.1.8, ignore@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a"
@@ -6415,7 +6493,7 @@
once "^1.3.0"
wrappy "1"
-inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3, inherits@~2.0.4:
+inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3, inherits@~2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
@@ -6614,6 +6692,13 @@
resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=
+is-fullwidth-code-point@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb"
+ integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs=
+ dependencies:
+ number-is-nan "^1.0.0"
+
is-fullwidth-code-point@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
@@ -7786,6 +7871,14 @@
dependencies:
minipass "^3.0.0"
+minipass@^2.6.0, minipass@^2.9.0:
+ version "2.9.0"
+ resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.9.0.tgz#e713762e7d3e32fed803115cf93e04bca9fcc9a6"
+ integrity sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==
+ dependencies:
+ safe-buffer "^5.1.2"
+ yallist "^3.0.0"
+
minipass@^3.0.0, minipass@^3.1.0, minipass@^3.1.1, minipass@^3.1.3:
version "3.1.6"
resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.6.tgz#3b8150aa688a711a1521af5e8779c1d3bb4f45ee"
@@ -7793,6 +7886,13 @@
dependencies:
yallist "^4.0.0"
+minizlib@^1.3.3:
+ version "1.3.3"
+ resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.3.3.tgz#2290de96818a34c29551c8a8d301216bd65a861d"
+ integrity sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==
+ dependencies:
+ minipass "^2.9.0"
+
minizlib@^2.0.0, minizlib@^2.1.1:
version "2.1.2"
resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931"
@@ -7806,7 +7906,7 @@
resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113"
integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==
-mkdirp@^0.5.5, mkdirp@~0.5.1:
+"mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.5, mkdirp@~0.5.1:
version "0.5.5"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def"
integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==
@@ -7973,7 +8073,7 @@
resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.1.tgz#d7be34dfa3105b91494c3147089315eff8874b01"
integrity sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==
-nanoid@3.3.1, nanoid@^3.2.0, nanoid@^3.3.1:
+nanoid@3.3.1, nanoid@^3.3.1:
version "3.3.1"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.1.tgz#6347a18cac88af88f58af0b3594b723d5e99bb35"
integrity sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==
@@ -7988,6 +8088,15 @@
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
+needle@^2.2.1:
+ version "2.9.1"
+ resolved "https://registry.yarnpkg.com/needle/-/needle-2.9.1.tgz#22d1dffbe3490c2b83e301f7709b6736cd8f2684"
+ integrity sha512-6R9fqJ5Zcmf+uYaFgdIHmLwNldn5HbK8L5ybn7Uz+ylX/rnOsSp1AHcvQSrCaFN+qNM1wpymHqD7mVasEOlHGQ==
+ dependencies:
+ debug "^3.2.6"
+ iconv-lite "^0.4.4"
+ sax "^1.2.4"
+
negotiator@0.6.2, negotiator@^0.6.2:
version "0.6.2"
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
@@ -8010,11 +8119,34 @@
dependencies:
lower-case "^1.1.1"
+node-addon-api@^3.0.0:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161"
+ integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==
+
node-forge@^1.2.0:
version "1.2.1"
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.2.1.tgz#82794919071ef2eb5c509293325cec8afd0fd53c"
integrity sha512-Fcvtbb+zBcZXbTTVwqGA5W+MKBj56UjVRevvchv5XrcyXbmNdesfZL37nlcWOfpgHhgmxApw3tQbTr4CqNmX4w==
+node-gyp@3.x:
+ version "3.8.0"
+ resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.8.0.tgz#540304261c330e80d0d5edce253a68cb3964218c"
+ integrity sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA==
+ dependencies:
+ fstream "^1.0.0"
+ glob "^7.0.3"
+ graceful-fs "^4.1.2"
+ mkdirp "^0.5.0"
+ nopt "2 || 3"
+ npmlog "0 || 1 || 2 || 3 || 4"
+ osenv "0"
+ request "^2.87.0"
+ rimraf "2"
+ semver "~5.3.0"
+ tar "^2.0.0"
+ which "1"
+
node-gyp@^8.4.1:
version "8.4.1"
resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-8.4.1.tgz#3d49308fc31f768180957d6b5746845fbd429937"
@@ -8031,6 +8163,22 @@
tar "^6.1.2"
which "^2.0.2"
+node-pre-gyp@^0.11.0:
+ version "0.11.0"
+ resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz#db1f33215272f692cd38f03238e3e9b47c5dd054"
+ integrity sha512-TwWAOZb0j7e9eGaf9esRx3ZcLaE5tQ2lvYy1pb5IAaG1a2e2Kv5Lms1Y4hpj+ciXJRofIxxlt5haeQ/2ANeE0Q==
+ dependencies:
+ detect-libc "^1.0.2"
+ mkdirp "^0.5.1"
+ needle "^2.2.1"
+ nopt "^4.0.1"
+ npm-packlist "^1.1.6"
+ npmlog "^4.0.2"
+ rc "^1.2.7"
+ rimraf "^2.6.1"
+ semver "^5.3.0"
+ tar "^4"
+
node-preload@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/node-preload/-/node-preload-0.2.1.tgz#c03043bb327f417a18fee7ab7ee57b408a144301"
@@ -8080,6 +8228,21 @@
undefsafe "^2.0.5"
update-notifier "^5.1.0"
+"nopt@2 || 3":
+ version "3.0.6"
+ resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9"
+ integrity sha1-xkZdvwirzU2zWTF/eaxopkayj/k=
+ dependencies:
+ abbrev "1"
+
+nopt@^4.0.1:
+ version "4.0.3"
+ resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.3.tgz#a375cad9d02fd921278d954c2254d5aa57e15e48"
+ integrity sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==
+ dependencies:
+ abbrev "1"
+ osenv "^0.1.4"
+
nopt@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88"
@@ -8144,6 +8307,27 @@
jsdom "^16.6.0"
marked "^4.0.10"
+npm-bundled@^1.0.1:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.1.2.tgz#944c78789bd739035b70baa2ca5cc32b8d860bc1"
+ integrity sha512-x5DHup0SuyQcmL3s7Rx/YQ8sbw/Hzg0rj48eN0dV7hf5cmQq5PXIeioroH3raV1QC1yh3uTYuMThvEQF3iKgGQ==
+ dependencies:
+ npm-normalize-package-bin "^1.0.1"
+
+npm-normalize-package-bin@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz#6e79a41f23fd235c0623218228da7d9c23b8f6e2"
+ integrity sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==
+
+npm-packlist@^1.1.6:
+ version "1.4.8"
+ resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.4.8.tgz#56ee6cc135b9f98ad3d51c1c95da22bbb9b2ef3e"
+ integrity sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==
+ dependencies:
+ ignore-walk "^3.0.1"
+ npm-bundled "^1.0.1"
+ npm-normalize-package-bin "^1.0.1"
+
npm-run-path@^4.0.0, npm-run-path@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea"
@@ -8151,6 +8335,16 @@
dependencies:
path-key "^3.0.0"
+"npmlog@0 || 1 || 2 || 3 || 4", npmlog@^4.0.2:
+ version "4.1.2"
+ resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b"
+ integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==
+ dependencies:
+ are-we-there-yet "~1.1.2"
+ console-control-strings "~1.1.0"
+ gauge "~2.7.3"
+ set-blocking "~2.0.0"
+
npmlog@^5.0.0:
version "5.0.1"
resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-5.0.1.tgz#f06678e80e29419ad67ab964e0fa69959c1eb8b0"
@@ -8178,6 +8372,11 @@
dependencies:
boolbase "^1.0.0"
+number-is-nan@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
+ integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=
+
nwsapi@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7"
@@ -8221,7 +8420,7 @@
resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==
-object-assign@^4.0.1, object-assign@^4.1.1:
+object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
@@ -8356,6 +8555,24 @@
resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27"
integrity sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=
+os-homedir@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
+ integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M=
+
+os-tmpdir@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
+ integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=
+
+osenv@0, osenv@^0.1.4:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410"
+ integrity sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==
+ dependencies:
+ os-homedir "^1.0.0"
+ os-tmpdir "^1.0.0"
+
ospath@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/ospath/-/ospath-1.2.2.tgz#1276639774a3f8ef2572f7fe4280e0ea4550c07b"
@@ -9215,7 +9432,7 @@
resolved "https://registry.yarnpkg.com/raw-loader/-/raw-loader-0.5.1.tgz#0c3d0beaed8a01c966d9787bf778281252a979aa"
integrity sha1-DD0L6u2KAclm2Xh793goElKpeao=
-rc@^1.2.8:
+rc@^1.2.7, rc@^1.2.8:
version "1.2.8"
resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==
@@ -9265,7 +9482,7 @@
string_decoder "^1.1.1"
util-deprecate "^1.0.1"
-readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.2.2, readable-stream@~2.3.3, readable-stream@~2.3.6:
+readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.2.2, readable-stream@~2.3.3, readable-stream@~2.3.6:
version "2.3.7"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
@@ -9404,7 +9621,7 @@
dependencies:
throttleit "^1.0.0"
-request@^2.88.0:
+request@^2.87.0, request@^2.88.0:
version "2.88.2"
resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3"
integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==
@@ -9538,7 +9755,7 @@
resolved "https://registry.yarnpkg.com/rgba-regex/-/rgba-regex-1.0.0.tgz#43374e2e2ca0968b0ef1523460b7d730ff22eeb3"
integrity sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=
-rimraf@^2.6.3:
+rimraf@2, rimraf@^2.6.1, rimraf@^2.6.3:
version "2.7.1"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==
@@ -9595,7 +9812,7 @@
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
-safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0:
+safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@^5.2.1, safe-buffer@~5.2.0:
version "5.2.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
@@ -9628,7 +9845,7 @@
klona "^2.0.4"
neo-async "^2.6.2"
-sax@^1.2.1:
+sax@^1.2.1, sax@^1.2.4:
version "1.2.4"
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
@@ -9725,7 +9942,7 @@
dependencies:
semver "^6.3.0"
-"semver@2 || 3 || 4 || 5", semver@^5.7.1:
+"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.7.1:
version "5.7.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
@@ -9752,6 +9969,11 @@
dependencies:
lru-cache "^6.0.0"
+semver@~5.3.0:
+ version "5.3.0"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f"
+ integrity sha1-myzl094C0XxgEq0yaqa00M9U+U8=
+
send@0.17.1:
version "0.17.1"
resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8"
@@ -9801,7 +10023,7 @@
parseurl "~1.3.3"
send "0.17.1"
-set-blocking@^2.0.0:
+set-blocking@^2.0.0, set-blocking@~2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc=
@@ -10091,6 +10313,16 @@
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=
+sqlite3@^5.0.2:
+ version "5.0.2"
+ resolved "https://registry.yarnpkg.com/sqlite3/-/sqlite3-5.0.2.tgz#00924adcc001c17686e0a6643b6cbbc2d3965083"
+ integrity sha512-1SdTNo+BVU211Xj1csWa8lV6KM0CtucDwRyA0VHl91wEH1Mgh7RxUpI4rVvG7OhHrzCSGaVyW5g8vKvlrk9DJA==
+ dependencies:
+ node-addon-api "^3.0.0"
+ node-pre-gyp "^0.11.0"
+ optionalDependencies:
+ node-gyp "3.x"
+
sshpk@^1.14.1, sshpk@^1.7.0:
version "1.16.1"
resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877"
@@ -10199,6 +10431,15 @@
inherits "^2.0.1"
readable-stream "^2.0.2"
+string-width@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3"
+ integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=
+ dependencies:
+ code-point-at "^1.0.0"
+ is-fullwidth-code-point "^1.0.0"
+ strip-ansi "^3.0.0"
+
"string-width@^1.0.1 || ^2.0.0":
version "2.1.1"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
@@ -10255,6 +10496,13 @@
dependencies:
safe-buffer "~5.1.0"
+strip-ansi@^3.0.0, strip-ansi@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
+ integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=
+ dependencies:
+ ansi-regex "^2.0.0"
+
"strip-ansi@^3.0.1 || ^4.0.0", strip-ansi@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f"
@@ -10489,6 +10737,28 @@
resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.0.tgz#5c373d281d9c672848213d0e037d1c4165ab426b"
integrity sha512-FBk4IesMV1rBxX2tfiK8RAmogtWn53puLOQlvO8XuwlgxcYbP4mVPS9Ph4aeamSyyVjOl24aYWAuc8U5kCVwMw==
+tar@^2.0.0:
+ version "2.2.2"
+ resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.2.tgz#0ca8848562c7299b8b446ff6a4d60cdbb23edc40"
+ integrity sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA==
+ dependencies:
+ block-stream "*"
+ fstream "^1.0.12"
+ inherits "2"
+
+tar@^4:
+ version "4.4.19"
+ resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.19.tgz#2e4d7263df26f2b914dee10c825ab132123742f3"
+ integrity sha512-a20gEsvHnWe0ygBY8JbxoM4w3SJdhc7ZAuxkLqh+nvNQN2IOt0B5lLgM490X5Hl8FF0dl0tOf2ewFYAlIFgzVA==
+ dependencies:
+ chownr "^1.1.4"
+ fs-minipass "^1.2.7"
+ minipass "^2.9.0"
+ minizlib "^1.3.3"
+ mkdirp "^0.5.5"
+ safe-buffer "^5.2.1"
+ yallist "^3.1.1"
+
tar@^6.0.2, tar@^6.1.2:
version "6.1.11"
resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.11.tgz#6760a38f003afa1b2ffd0ffe9e9abbd0eab3d621"
@@ -11365,6 +11635,13 @@
has-tostringtag "^1.0.0"
is-typed-array "^1.1.7"
+which@1, which@^1.3.1:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
+ integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
+ dependencies:
+ isexe "^2.0.0"
+
which@2.0.2, which@^2.0.1, which@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
@@ -11372,14 +11649,7 @@
dependencies:
isexe "^2.0.0"
-which@^1.3.1:
- version "1.3.1"
- resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
- integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
- dependencies:
- isexe "^2.0.0"
-
-wide-align@^1.1.2:
+wide-align@^1.1.0, wide-align@^1.1.2:
version "1.1.5"
resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.5.tgz#df1d4c206854369ecf3c9a4898f1b23fbd9d15d3"
integrity sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==
@@ -11501,6 +11771,11 @@
resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.5.tgz#8769ec08d03b1ea2df2500acef561743bbb9ab18"
integrity sha512-hsRUr4FFrvhhRH12wOdfs38Gy7k2FFzB9qgN9v3aLykRq0dRcdcpz5C9FxdS2NuhOrI/628b/KSTJ3rwHysYSg==
+yallist@^3.0.0, yallist@^3.1.1:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"
+ integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==
+
yallist@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"

File Metadata

Mime Type
text/plain
Expires
Mar 17 2025, 6:51 PM (7 w, 3 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3221120

Event Timeline