Page Menu
Home
Software Heritage
Search
Configure Global Search
Log In
Files
F7343031
D7396.id.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
80 KB
Subscribers
None
D7396.id.diff
View Options
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 – 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
Details
Attached
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
Attached To
D7396: admin: Add mailmaps administration Web UI
Event Timeline
Log In to Comment