diff --git a/assets/src/bundles/add_forge/create-request.js b/assets/src/bundles/add_forge/create-request.js index 07c3d2f9..3d23afb0 100644 --- a/assets/src/bundles/add_forge/create-request.js +++ b/assets/src/bundles/add_forge/create-request.js @@ -1,128 +1,155 @@ /** * Copyright (C) 2022 The Software Heritage developers * See the AUTHORS file at the top-level directory of this distribution * License: GNU Affero General Public License version 3, or any later version * See top-level LICENSE file for more information */ import {handleFetchError, removeUrlFragment, csrfPost, getHumanReadableDate} from 'utils/functions'; +import userRequestsFilterCheckboxFn from 'utils/requests-filter-checkbox.ejs'; +import {swhSpinnerSrc} from 'utils/constants'; let requestBrowseTable; +const addForgeCheckboxId = 'swh-add-forge-user-filter'; +const userRequestsFilterCheckbox = userRequestsFilterCheckboxFn({ + 'inputId': addForgeCheckboxId, + 'checked': true // by default, display only user requests +}); + export function onCreateRequestPageLoad() { $(document).ready(() => { $('#requestCreateForm').submit(async function(event) { event.preventDefault(); try { const response = await csrfPost($(this).attr('action'), {'Content-Type': 'application/x-www-form-urlencoded'}, $(this).serialize()); handleFetchError(response); $('#userMessageDetail').empty(); $('#userMessage').text('Your request has been submitted'); $('#userMessage').removeClass('badge-danger'); $('#userMessage').addClass('badge-success'); requestBrowseTable.draw(); // redraw the table to update the list } catch (errorResponse) { $('#userMessageDetail').empty(); let errorMessage; let errorMessageDetail = ''; const errorData = await errorResponse.json(); // if (errorResponse.content_type === 'text/plain') { // does not work? if (errorResponse.status === 409) { errorMessage = errorData; } else { // assuming json response // const exception = errorData['exception']; errorMessage = 'An unknown error occurred during the request creation'; try { const reason = JSON.parse(errorData['reason']); Object.entries(reason).forEach((keys, _) => { const key = keys[0]; const message = keys[1][0]; // take only the first issue errorMessageDetail += `\n${key}: ${message}`; }); } catch (_) { errorMessageDetail = errorData['reason']; // can't parse it, leave it raw } } $('#userMessage').text( errorMessageDetail ? `Error: ${errorMessageDetail}` : errorMessage ); $('#userMessage').removeClass('badge-success'); $('#userMessage').addClass('badge-danger'); } }); $('#swh-add-forge-requests-list-tab').on('shown.bs.tab', () => { + requestBrowseTable.draw(); window.location.hash = '#browse-requests'; }); $('#swh-add-forge-requests-help-tab').on('shown.bs.tab', () => { window.location.hash = '#help'; }); $('#swh-add-forge-tab').on('shown.bs.tab', () => { removeUrlFragment(); }); $(window).on('hashchange', () => { onPageHashChage(); }); onPageHashChage(); // Explicit call to handle a hash during the page load populateRequestBrowseList(); // Load existing requests }); } function onPageHashChage() { if (window.location.hash === '#browse-requests') { $('.nav-tabs a[href="#swh-add-forge-requests-list"]').tab('show'); } else if (window.location.hash === '#help') { $('.nav-tabs a[href="#swh-add-forge-requests-help"]').tab('show'); } else { $('.nav-tabs a[href="#swh-add-forge-submit-request"]').tab('show'); } } export function populateRequestBrowseList() { requestBrowseTable = $('#add-forge-request-browse') .on('error.dt', (e, settings, techNote, message) => { $('#add-forge-browse-request-error').text(message); }) .DataTable({ serverSide: true, processing: true, + language: { + processing: `` + }, retrieve: true, searching: true, info: false, - dom: '<<"d-flex justify-content-between align-items-center"f' + - '<"#list-exclude">l>rt<"bottom"ip>>', + // Layout configuration, see [1] for more details + // [1] https://datatables.net/reference/option/dom + dom: '<"row"<"col-sm-3"l><"col-sm-6 text-left user-requests-filter"><"col-sm-3"f>>' + + '<"row"<"col-sm-12"tr>>' + + '<"row"<"col-sm-5"i><"col-sm-7"p>>', ajax: { - 'url': Urls.add_forge_request_list_datatables() + 'url': Urls.add_forge_request_list_datatables(), + data: (d) => { + if (swh.webapp.isUserLoggedIn() && $(`#${addForgeCheckboxId}`).prop('checked')) { + d.user_requests_only = '1'; + } + } + }, + fnInitComplete: function() { + if (swh.webapp.isUserLoggedIn()) { + $('div.user-requests-filter').html(userRequestsFilterCheckbox); + $(`#${addForgeCheckboxId}`).on('change', () => { + requestBrowseTable.draw(); + }); + } }, columns: [ { data: 'submission_date', name: 'submission_date', render: getHumanReadableDate }, { data: 'forge_type', name: 'forge_type' }, { data: 'forge_url', name: 'forge_url' }, { data: 'status', name: 'status', render: function(data, type, row, meta) { return swh.add_forge.formatRequestStatusName(data); } } ] }); - requestBrowseTable.draw(); } diff --git a/cypress/integration/add-forge-now-request-create.spec.js b/cypress/integration/add-forge-now-request-create.spec.js index 4b533060..d9612f4e 100644 --- a/cypress/integration/add-forge-now-request-create.spec.js +++ b/cypress/integration/add-forge-now-request-create.spec.js @@ -1,183 +1,255 @@ /** * Copyright (C) 2022 The Software Heritage developers * See the AUTHORS file at the top-level directory of this distribution * License: GNU Affero General Public License version 3, or any later version * See top-level LICENSE file for more information */ function populateForm(type, url, contact, email, consent, comment) { cy.get('#swh-input-forge-type').select(type); - cy.get('#swh-input-forge-url').type(url); - cy.get('#swh-input-forge-contact-name').type(contact); - cy.get('#swh-input-forge-contact-email').type(email); - cy.get('#swh-input-forge-comment').type(comment); + cy.get('#swh-input-forge-url').clear().type(url, {delay: 0, force: true}); + cy.get('#swh-input-forge-contact-name').clear().type(contact, {delay: 0, force: true}); + cy.get('#swh-input-forge-contact-email').clear().type(email, {delay: 0, force: true}); + if (comment) { + cy.get('#swh-input-forge-comment').clear().type(comment, {delay: 0, force: true}); + } cy.get('#swh-input-consent-check').click({force: consent === 'on'}); } +describe('Browse requests list tests', function() { + beforeEach(function() { + this.addForgeNowUrl = this.Urls.forge_add(); + this.listAddForgeRequestsUrl = this.Urls.add_forge_request_list_datatables(); + }); + + it('should not show user requests filter checkbox for anonymous users', function() { + cy.visit(this.addForgeNowUrl); + cy.get('#swh-add-forge-requests-list-tab').click(); + cy.get('#swh-add-forge-user-filter').should('not.exist'); + }); + + it('should show user requests filter checkbox for authenticated users', function() { + cy.userLogin(); + cy.visit(this.addForgeNowUrl); + cy.get('#swh-add-forge-requests-list-tab').click(); + cy.get('#swh-add-forge-user-filter').should('exist').should('be.checked'); + }); + + it('should only display user requests when filter is activated', function() { + // Clean up previous state + cy.task('db:add_forge_now:delete'); + // 'user2' logs in and create requests + cy.user2Login(); + cy.visit(this.addForgeNowUrl); + + // create requests for the user 'user' + populateForm('gitlab', 'gitlab.org', 'admin', 'admin@example.org', 'on', ''); + cy.get('#requestCreateForm').submit(); + + // user requests filter checkbox should be in the DOM + cy.get('#swh-add-forge-requests-list-tab').click(); + cy.get('#swh-add-forge-user-filter').should('exist').should('be.checked'); + + // check unfiltered user requests + cy.get('tbody tr').then(rows => { + expect(rows.length).to.eq(1); + }); + + // user1 logout + cy.contains('a', 'logout').click(); + + // user logs in + cy.userLogin(); + cy.visit(this.addForgeNowUrl); + + populateForm('gitea', 'gitea.org', 'admin', 'admin@example.org', 'on', ''); + cy.get('#requestCreateForm').submit(); + populateForm('cgit', 'cgit.org', 'admin', 'admin@example.org', 'on', ''); + cy.get('#requestCreateForm').submit(); + + // user requests filter checkbox should be in the DOM + cy.get('#swh-add-forge-requests-list-tab').click(); + cy.get('#swh-add-forge-user-filter').should('exist').should('be.checked'); + + // check unfiltered user requests + cy.get('tbody tr').then(rows => { + expect(rows.length).to.eq(2); + }); + + cy.get('#swh-add-forge-user-filter') + .uncheck({force: true}); + + // Users now sees everything + cy.get('tbody tr').then(rows => { + expect(rows.length).to.eq(2 + 1); + }); + }); +}); + describe('Test add-forge-request creation', function() { beforeEach(function() { this.addForgeNowUrl = this.Urls.forge_add(); }); it('should show all the tabs for every user', function() { cy.visit(this.addForgeNowUrl); cy.get('#swh-add-forge-tab') .should('have.class', 'nav-link'); cy.get('#swh-add-forge-requests-list-tab') .should('have.class', 'nav-link'); cy.get('#swh-add-forge-requests-help-tab') .should('have.class', 'nav-link'); }); it('should show create forge tab by default', function() { cy.visit(this.addForgeNowUrl); cy.get('#swh-add-forge-tab') .should('have.class', 'active'); cy.get('#swh-add-forge-requests-list-tab') .should('not.have.class', 'active'); }); it('should show login link for anonymous user', function() { cy.visit(this.addForgeNowUrl); cy.get('#loginLink') .should('be.visible') .should('contain', 'log in'); }); it('should bring back after login', function() { cy.visit(this.addForgeNowUrl); cy.get('#loginLink') .should('have.attr', 'href') .and('include', `${this.Urls.login()}?next=${this.Urls.forge_add()}`); }); it('should change tabs on click', function() { cy.visit(this.addForgeNowUrl); cy.get('#swh-add-forge-requests-list-tab').click(); cy.get('#swh-add-forge-tab') .should('not.have.class', 'active'); cy.get('#swh-add-forge-requests-list-tab') .should('have.class', 'active'); cy.get('#swh-add-forge-requests-help-tab') .should('not.have.class', 'active'); cy.hash().should('eq', '#browse-requests'); cy.get('#swh-add-forge-requests-help-tab').click(); cy.get('#swh-add-forge-tab') .should('not.have.class', 'active'); cy.get('#swh-add-forge-requests-list-tab') .should('not.have.class', 'active'); cy.get('#swh-add-forge-requests-help-tab') .should('have.class', 'active'); cy.hash().should('eq', '#help'); cy.get('#swh-add-forge-tab').click(); cy.get('#swh-add-forge-tab') .should('have.class', 'active'); cy.get('#swh-add-forge-requests-list-tab') .should('not.have.class', 'active'); cy.get('#swh-add-forge-requests-help-tab') .should('not.have.class', 'active'); cy.hash().should('eq', ''); }); it('should show create form elements to authenticated user', function() { cy.userLogin(); cy.visit(this.addForgeNowUrl); cy.get('#swh-input-forge-type') .should('be.visible'); cy.get('#swh-input-forge-url') .should('be.visible'); cy.get('#swh-input-forge-contact-name') .should('be.visible'); cy.get('#swh-input-consent-check') .should('be.visible'); cy.get('#swh-input-forge-comment') .should('be.visible'); cy.get('#swh-input-form-submit') .should('be.visible'); }); it('should show browse requests table for every user', function() { // testing only for anonymous cy.visit(this.addForgeNowUrl); cy.get('#swh-add-forge-requests-list-tab').click(); cy.get('#add-forge-request-browse') .should('be.visible'); cy.get('#loginLink') .should('not.be.visible'); }); it('should update browse list on successful submission', function() { cy.userLogin(); cy.visit(this.addForgeNowUrl); populateForm('bitbucket', 'gitlab.com', 'test', 'test@example.com', 'on', 'test comment'); cy.get('#requestCreateForm').submit(); cy.visit(this.addForgeNowUrl); cy.get('#swh-add-forge-requests-list-tab').click(); cy.get('#add-forge-request-browse') .should('be.visible') .should('contain', 'gitlab.com'); cy.get('#add-forge-request-browse') .should('be.visible') .should('contain', 'Pending'); }); it('should show error message on conflict', function() { cy.userLogin(); cy.visit(this.addForgeNowUrl); populateForm('bitbucket', 'gitlab.com', 'test', 'test@example.com', 'on', 'test comment'); cy.get('#requestCreateForm').submit(); cy.get('#requestCreateForm').submit(); // Submitting the same data again cy.get('#userMessage') .should('have.class', 'badge-danger') .should('contain', 'already exists'); }); it('should show error message', function() { cy.userLogin(); cy.intercept('POST', `${this.Urls.api_1_add_forge_request_create()}**`, { body: { 'exception': 'BadInputExc', 'reason': '{"add-forge-comment": ["This field is required"]}' }, statusCode: 400 }).as('errorRequest'); cy.visit(this.addForgeNowUrl); populateForm( 'bitbucket', 'gitlab.com', 'test', 'test@example.com', 'off', 'comment' ); cy.get('#requestCreateForm').submit(); cy.wait('@errorRequest').then((xhr) => { cy.get('#userMessage') .should('have.class', 'badge-danger') .should('contain', 'field is required'); }); }); }); diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js index 01245fcc..eeb7373b 100644 --- a/cypress/plugins/index.js +++ b/cypress/plugins/index.js @@ -1,152 +1,160 @@ /** * Copyright (C) 2019-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 axios = require('axios'); const fs = require('fs'); const sqlite3 = require('sqlite3').verbose(); async function httpGet(url) { const response = await axios.get(url); return response.data; } async function getMetadataForOrigin(originUrl, baseUrl) { const originVisitsApiUrl = `${baseUrl}/api/1/origin/${originUrl}/visits`; const originVisits = await httpGet(originVisitsApiUrl); const lastVisit = originVisits[0]; const snapshotApiUrl = `${baseUrl}/api/1/snapshot/${lastVisit.snapshot}`; const lastOriginSnapshot = await httpGet(snapshotApiUrl); let revision = lastOriginSnapshot.branches.HEAD.target; if (lastOriginSnapshot.branches.HEAD.target_type === 'alias') { revision = lastOriginSnapshot.branches[revision].target; } const revisionApiUrl = `${baseUrl}/api/1/revision/${revision}`; const lastOriginHeadRevision = await httpGet(revisionApiUrl); return { 'directory': lastOriginHeadRevision.directory, 'revision': lastOriginHeadRevision.id, 'snapshot': lastOriginSnapshot.id }; }; 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 on('before:browser:launch', function(browser, launchOptions) { return new Promise((resolve) => { const p1 = axios.get(`${config.baseUrl}/tests/data/content/code/extensions/`); const p2 = axios.get(`${config.baseUrl}/tests/data/content/code/filenames/`); Promise.all([p1, p2]) .then(function(responses) { fs.writeFileSync('cypress/fixtures/source-file-extensions.json', JSON.stringify(responses[0].data)); fs.writeFileSync('cypress/fixtures/source-file-names.json', JSON.stringify(responses[1].data)); resolve(); }); }); }); on('task', { getSwhTestsData: async() => { if (!global.swhTestsData) { const swhTestsData = {}; swhTestsData.unarchivedRepo = { url: 'https://github.com/SoftwareHeritage/swh-web', type: 'git', revision: '7bf1b2f489f16253527807baead7957ca9e8adde', snapshot: 'd9829223095de4bb529790de8ba4e4813e38672d', rootDirectory: '7d887d96c0047a77e2e8c4ee9bb1528463677663', content: [{ sha1git: 'b203ec39300e5b7e97b6e20986183cbd0b797859' }] }; swhTestsData.origin = [{ url: 'https://github.com/memononen/libtess2', type: 'git', content: [{ path: 'Source/tess.h' }, { path: 'premake4.lua' }], directory: [{ path: 'Source', id: 'cd19126d815470b28919d64b2a8e6a3e37f900dd' }], revisions: [], invalidSubDir: 'Source1' }, { url: 'https://github.com/wcoder/highlightjs-line-numbers.js', type: 'git', content: [{ path: 'src/highlightjs-line-numbers.js' }], directory: [], revisions: ['1c480a4573d2a003fc2630c21c2b25829de49972'], release: { name: 'v2.6.0', id: '6877028d6e5412780517d0bfa81f07f6c51abb41', directory: '5b61d50ef35ca9a4618a3572bde947b8cccf71ad' } }]; for (const origin of swhTestsData.origin) { const metadata = await getMetadataForOrigin(origin.url, config.baseUrl); const directoryApiUrl = `${config.baseUrl}/api/1/directory/${metadata.directory}`; origin.dirContent = await httpGet(directoryApiUrl); origin.rootDirectory = metadata.directory; origin.revisions.push(metadata.revision); origin.snapshot = metadata.snapshot; for (const content of origin.content) { const contentPathApiUrl = `${config.baseUrl}/api/1/directory/${origin.rootDirectory}/${content.path}`; const contentMetaData = await httpGet(contentPathApiUrl); content.name = contentMetaData.name.split('/').slice(-1)[0]; content.sha1git = contentMetaData.target; content.directory = contentMetaData.dir_id; const rawFileUrl = `${config.baseUrl}/browse/content/sha1_git:${content.sha1git}/raw/?filename=${content.name}`; const fileText = await httpGet(rawFileUrl); const fileLines = fileText.split('\n'); content.numberLines = fileLines.length; if (!fileLines[content.numberLines - 1]) { // If last line is empty its not shown content.numberLines -= 1; } } } 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; + }, + 'db:add_forge_now:delete': () => { + const db = getDatabase(); + db.serialize(function() { + db.run('DELETE FROM add_forge_now_request'); + }); + db.close(); + return true; } }); return config; }; diff --git a/cypress/support/index.js b/cypress/support/index.js index 1c09614d..e703ec17 100644 --- a/cypress/support/index.js +++ b/cypress/support/index.js @@ -1,106 +1,110 @@ /** * Copyright (C) 2019-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 'cypress-hmr-restarter'; import '@cypress/code-coverage/support'; Cypress.Screenshot.defaults({ screenshotOnRunFailure: false }); Cypress.Commands.add('xhrShouldBeCalled', (alias, timesCalled) => { const testRoutes = cy.state('routes'); const aliasRoute = Cypress._.find(testRoutes, {alias}); expect(Object.keys(aliasRoute.requests || {})).to.have.length(timesCalled); }); function loginUser(username, password) { const url = '/admin/login/'; return cy.request({ url: url, method: 'GET' }).then(() => { cy.getCookie('sessionid').should('not.exist'); cy.getCookie('csrftoken').its('value').then((token) => { cy.request({ url: url, method: 'POST', form: true, followRedirect: false, body: { username: username, password: password, csrfmiddlewaretoken: token } }).then(() => { cy.getCookie('sessionid').should('exist'); return cy.getCookie('csrftoken').its('value'); }); }); }); } Cypress.Commands.add('adminLogin', () => { return loginUser('admin', 'admin'); }); Cypress.Commands.add('userLogin', () => { return loginUser('user', 'user'); }); +Cypress.Commands.add('user2Login', () => { + return loginUser('user2', 'user2'); +}); + Cypress.Commands.add('ambassadorLogin', () => { return loginUser('ambassador', 'ambassador'); }); Cypress.Commands.add('depositLogin', () => { return loginUser('deposit', 'deposit'); }); Cypress.Commands.add('addForgeModeratorLogin', () => { return loginUser('add-forge-moderator', 'add-forge-moderator'); }); function mockCostlyRequests() { cy.intercept('https://status.softwareheritage.org/**', { body: { 'result': { 'status': [ { 'id': '5f7c4c567f50b304c1e7bd5f', 'name': 'Save Code Now', 'updated': '2020-11-30T13:51:21.151Z', 'status': 'Operational', 'status_code': 100 } ] } }}).as('swhPlatformStatus'); cy.intercept('/coverage', { body: '' }).as('swhCoverageWidget'); } Cypress.Commands.add('mailmapAdminLogin', () => { return loginUser('mailmap-admin', 'mailmap-admin'); }); before(function() { mockCostlyRequests(); cy.task('getSwhTestsData').then(testsData => { Object.assign(this, testsData); }); cy.visit('/').window().then(async win => { this.Urls = win.Urls; }); }); beforeEach(function() { mockCostlyRequests(); }); diff --git a/swh/web/tests/create_test_users.py b/swh/web/tests/create_test_users.py index 968de489..2ffc8902 100644 --- a/swh/web/tests/create_test_users.py +++ b/swh/web/tests/create_test_users.py @@ -1,50 +1,51 @@ # Copyright (C) 2021-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 typing import Dict, List, Tuple from django.contrib.auth import get_user_model from swh.web.auth.utils import ( ADD_FORGE_MODERATOR_PERMISSION, ADMIN_LIST_DEPOSIT_PERMISSION, - SWH_AMBASSADOR_PERMISSION, MAILMAP_ADMIN_PERMISSION, + SWH_AMBASSADOR_PERMISSION, ) from swh.web.tests.utils import create_django_permission User = get_user_model() users: Dict[str, Tuple[str, str, List[str]]] = { "user": ("user", "user@example.org", []), + "user2": ("user2", "user2@example.org", []), "ambassador": ( "ambassador", "ambassador@example.org", [SWH_AMBASSADOR_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], ), } for username, (password, email, permissions) in users.items(): if not User.objects.filter(username=username).exists(): user = User.objects.create_user(username, email, password) if permissions: for perm_name in permissions: permission = create_django_permission(perm_name) user.user_permissions.add(permission) user.save()