diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js index eeb7373b..70c084e7 100644 --- a/cypress/plugins/index.js +++ b/cypress/plugins/index.js @@ -1,160 +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.run('DELETE FROM add_forge_request'); }); db.close(); return true; } }); return config; }; diff --git a/swh/web/add_forge_now/__init__.py b/swh/web/add_forge_now/__init__.py index e69de29b..f27e23ec 100644 --- a/swh/web/add_forge_now/__init__.py +++ b/swh/web/add_forge_now/__init__.py @@ -0,0 +1,6 @@ +# 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 + +default_app_config = "swh.web.add_forge_now.apps.AddForgeNowConfig" diff --git a/swh/web/add_forge_now/apps.py b/swh/web/add_forge_now/apps.py index 465cc78f..7a27e3d4 100644 --- a/swh/web/add_forge_now/apps.py +++ b/swh/web/add_forge_now/apps.py @@ -1,11 +1,13 @@ # 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.apps import AppConfig +APP_LABEL = "swh_web_add_forge_now" + class AddForgeNowConfig(AppConfig): name = "swh.web.add_forge_now" - label = "swh_web_add_forge_now" + label = APP_LABEL diff --git a/swh/web/add_forge_now/migrations/0001_initial.py b/swh/web/add_forge_now/migrations/0001_initial.py index c049a13b..dfed76b2 100644 --- a/swh/web/add_forge_now/migrations/0001_initial.py +++ b/swh/web/add_forge_now/migrations/0001_initial.py @@ -1,109 +1,109 @@ # Generated by Django 2.2.24 on 2022-03-08 10:25 from django.db import migrations, models import django.db.models.deletion class Migration(migrations.Migration): initial = True dependencies = [] # type: ignore operations = [ migrations.CreateModel( name="Request", fields=[ ( "id", models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), ( "status", models.TextField( choices=[ ("PENDING", "Pending"), ("WAITING_FOR_FEEDBACK", "Waiting for feedback"), ("FEEDBACK_TO_HANDLE", "Feedback to handle"), ("ACCEPTED", "Accepted"), ("SCHEDULED", "Scheduled"), ("FIRST_LISTING_DONE", "First listing done"), ("FIRST_ORIGIN_LOADED", "First origin loaded"), ("REJECTED", "Rejected"), ("SUSPENDED", "Suspended"), ("DENIED", "Denied"), ], default="PENDING", ), ), ("submission_date", models.DateTimeField(auto_now_add=True)), ("submitter_name", models.TextField()), ("submitter_email", models.TextField()), ("forge_type", models.TextField()), ("forge_url", models.TextField()), ("forge_contact_email", models.EmailField(max_length=254)), ("forge_contact_name", models.TextField()), ( "forge_contact_comment", models.TextField( help_text=( "Where did you find this contact information (url, ...)" ) ), ), ], ), migrations.CreateModel( name="RequestHistory", fields=[ ( "id", models.AutoField( auto_created=True, primary_key=True, serialize=False, verbose_name="ID", ), ), ("text", models.TextField()), ("actor", models.TextField()), ( "actor_role", models.TextField( choices=[("MODERATOR", "moderator"), ("SUBMITTER", "submitter")] ), ), ("date", models.DateTimeField(auto_now_add=True)), ( "new_status", models.TextField( choices=[ ("PENDING", "Pending"), ("WAITING_FOR_FEEDBACK", "Waiting for feedback"), ("FEEDBACK_TO_HANDLE", "Feedback to handle"), ("ACCEPTED", "Accepted"), ("SCHEDULED", "Scheduled"), ("FIRST_LISTING_DONE", "First listing done"), ("FIRST_ORIGIN_LOADED", "First origin loaded"), ("REJECTED", "Rejected"), ("SUSPENDED", "Suspended"), ("DENIED", "Denied"), ], null=True, ), ), ( "request", models.ForeignKey( on_delete=django.db.models.deletion.DO_NOTHING, - to="add_forge_now.Request", + to="swh_web_add_forge_now.Request", ), ), ], ), ] diff --git a/swh/web/add_forge_now/migrations/0002_authorized_null_comment.py b/swh/web/add_forge_now/migrations/0002_authorized_null_comment.py index 0c02e2ff..713c993f 100644 --- a/swh/web/add_forge_now/migrations/0002_authorized_null_comment.py +++ b/swh/web/add_forge_now/migrations/0002_authorized_null_comment.py @@ -1,32 +1,32 @@ # Generated by Django 2.2.24 on 2022-03-21 15:42 from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ("add_forge_now", "0001_initial"), + ("swh_web_add_forge_now", "0001_initial"), ] operations = [ migrations.AlterField( model_name="request", name="forge_contact_comment", field=models.TextField( help_text="Where did you find this contact information (url, ...)", null=True, ), ), migrations.AlterField( model_name="requesthistory", name="actor_role", field=models.TextField( choices=[ ("MODERATOR", "moderator"), ("SUBMITTER", "submitter"), ("FORGE_ADMIN", "forge admin"), ] ), ), ] diff --git a/swh/web/add_forge_now/migrations/0003_request_submitter_forward_username.py b/swh/web/add_forge_now/migrations/0003_request_submitter_forward_username.py index d6b0a841..4e639841 100644 --- a/swh/web/add_forge_now/migrations/0003_request_submitter_forward_username.py +++ b/swh/web/add_forge_now/migrations/0003_request_submitter_forward_username.py @@ -1,18 +1,18 @@ # Generated by Django 2.2.24 on 2022-03-21 16:37 from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ("add_forge_now", "0002_authorized_null_comment"), + ("swh_web_add_forge_now", "0002_authorized_null_comment"), ] operations = [ migrations.AddField( model_name="request", name="submitter_forward_username", field=models.BooleanField(default=False), ), ] diff --git a/swh/web/add_forge_now/migrations/0004_rename_tables.py b/swh/web/add_forge_now/migrations/0004_rename_tables.py new file mode 100644 index 00000000..0c02304f --- /dev/null +++ b/swh/web/add_forge_now/migrations/0004_rename_tables.py @@ -0,0 +1,17 @@ +# Generated by Django 2.2.27 on 2022-03-29 11:42 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("swh_web_add_forge_now", "0003_request_submitter_forward_username"), + ] + + operations = [ + migrations.AlterModelTable(name="request", table="add_forge_request",), + migrations.AlterModelTable( + name="requesthistory", table="add_forge_request_history", + ), + ] diff --git a/swh/web/add_forge_now/models.py b/swh/web/add_forge_now/models.py index 5f9f0603..62c5baf3 100644 --- a/swh/web/add_forge_now/models.py +++ b/swh/web/add_forge_now/models.py @@ -1,100 +1,110 @@ # Copyright (C) 2022 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU Affero General Public License version 3, or any later version # See top-level LICENSE file for more information from __future__ import annotations import enum from typing import List from django.db import models +from .apps import APP_LABEL + class RequestStatus(enum.Enum): """Request statuses. Values are used in the ui. """ PENDING = "Pending" WAITING_FOR_FEEDBACK = "Waiting for feedback" FEEDBACK_TO_HANDLE = "Feedback to handle" ACCEPTED = "Accepted" SCHEDULED = "Scheduled" FIRST_LISTING_DONE = "First listing done" FIRST_ORIGIN_LOADED = "First origin loaded" REJECTED = "Rejected" SUSPENDED = "Suspended" DENIED = "Denied" @classmethod def choices(cls): return tuple((variant.name, variant.value) for variant in cls) def allowed_next_statuses(self) -> List[RequestStatus]: next_statuses = { self.PENDING: [self.WAITING_FOR_FEEDBACK, self.REJECTED, self.SUSPENDED], self.WAITING_FOR_FEEDBACK: [self.FEEDBACK_TO_HANDLE], self.FEEDBACK_TO_HANDLE: [ self.WAITING_FOR_FEEDBACK, self.ACCEPTED, self.REJECTED, self.SUSPENDED, ], self.ACCEPTED: [self.SCHEDULED], self.SCHEDULED: [ self.FIRST_LISTING_DONE, # in case of race condition between lister and loader: self.FIRST_ORIGIN_LOADED, ], self.FIRST_LISTING_DONE: [self.FIRST_ORIGIN_LOADED], self.FIRST_ORIGIN_LOADED: [], self.REJECTED: [], self.SUSPENDED: [self.PENDING], self.DENIED: [], } return next_statuses[self] # type: ignore class RequestActorRole(enum.Enum): MODERATOR = "moderator" SUBMITTER = "submitter" FORGE_ADMIN = "forge admin" @classmethod def choices(cls): return tuple((variant.name, variant.value) for variant in cls) class RequestHistory(models.Model): """Comment or status change. This is commented or changed by either submitter or moderator. """ request = models.ForeignKey("Request", models.DO_NOTHING) text = models.TextField() actor = models.TextField() actor_role = models.TextField(choices=RequestActorRole.choices()) date = models.DateTimeField(auto_now_add=True) new_status = models.TextField(choices=RequestStatus.choices(), null=True) + class Meta: + app_label = APP_LABEL + db_table = "add_forge_request_history" + class Request(models.Model): status = models.TextField( choices=RequestStatus.choices(), default=RequestStatus.PENDING.name, ) submission_date = models.DateTimeField(auto_now_add=True) submitter_name = models.TextField() submitter_email = models.TextField() submitter_forward_username = models.BooleanField(default=False) # FIXME: shall we do create a user model inside the webapp instead? forge_type = models.TextField() forge_url = models.TextField() forge_contact_email = models.EmailField() forge_contact_name = models.TextField() forge_contact_comment = models.TextField( null=True, help_text="Where did you find this contact information (url, ...)", ) + + class Meta: + app_label = APP_LABEL + db_table = "add_forge_request" diff --git a/swh/web/add_forge_now/tests/test_migration.py b/swh/web/add_forge_now/tests/test_migration.py index fa245366..6eaf9dd6 100644 --- a/swh/web/add_forge_now/tests/test_migration.py +++ b/swh/web/add_forge_now/tests/test_migration.py @@ -1,111 +1,111 @@ # 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, timezone import pytest -APP_NAME = "add_forge_now" +from swh.web.add_forge_now.apps import APP_LABEL MIGRATION_0001 = "0001_initial" MIGRATION_0002 = "0002_authorized_null_comment" MIGRATION_0003 = "0003_request_submitter_forward_username" def now() -> datetime: return datetime.now(tz=timezone.utc) def test_add_forge_now_initial_migration(migrator): """Basic migration test to check the model is fine""" - state = migrator.apply_tested_migration((APP_NAME, MIGRATION_0001)) - request = state.apps.get_model(APP_NAME, "Request") - request_history = state.apps.get_model(APP_NAME, "RequestHistory") + state = migrator.apply_tested_migration((APP_LABEL, MIGRATION_0001)) + request = state.apps.get_model(APP_LABEL, "Request") + request_history = state.apps.get_model(APP_LABEL, "RequestHistory") from swh.web.add_forge_now.models import RequestActorRole, RequestStatus date_now = now() req = request( status=RequestStatus.PENDING, submitter_name="dudess", submitter_email="dudess@orga.org", forge_type="cgit", forge_url="https://example.org/forge", forge_contact_email="forge@//example.org", forge_contact_name="forge", forge_contact_comment=( "Discovered on the main forge homepag, following contact link." ), ) req.save() assert req.submission_date > date_now req_history = request_history( request=req, text="some comment from the moderator", actor="moderator", actor_role=RequestActorRole.MODERATOR, new_status=None, ) req_history.save() assert req_history.date > req.submission_date req_history2 = request_history( request=req, text="some answer from the user", actor="user", actor_role=RequestActorRole.SUBMITTER, new_status=None, ) req_history2.save() assert req_history2.date > req_history.date def test_add_forge_now_allow_no_comment(migrator): """Basic migration test to check new model authorized empty comment""" from django.db.utils import IntegrityError - state = migrator.apply_tested_migration((APP_NAME, MIGRATION_0001)) + state = migrator.apply_tested_migration((APP_LABEL, MIGRATION_0001)) def make_request_with_empty_comment(requestModel): return requestModel( status="PENDING", submitter_name="dudess", submitter_email="dudess@orga.org", forge_type="cgit", forge_url="https://example.org/forge", forge_contact_email="forge@//example.org", forge_contact_name="forge", forge_contact_comment=None, ) - requestModel = state.apps.get_model(APP_NAME, "Request") + requestModel = state.apps.get_model(APP_LABEL, "Request") req = make_request_with_empty_comment(requestModel) with pytest.raises(IntegrityError, match="violates not-null constraint"): req.save() - state = migrator.apply_tested_migration((APP_NAME, MIGRATION_0002)) - requestModel2 = state.apps.get_model(APP_NAME, "Request") + state = migrator.apply_tested_migration((APP_LABEL, MIGRATION_0002)) + requestModel2 = state.apps.get_model(APP_LABEL, "Request") req2 = make_request_with_empty_comment(requestModel2) req2.save() def test_add_forge_now_store_submitter_forward_username(migrator): """Basic migration test to check new model authorized empty comment""" - state = migrator.apply_tested_migration((APP_NAME, MIGRATION_0002)) - requestModel = state.apps.get_model(APP_NAME, "Request") + state = migrator.apply_tested_migration((APP_LABEL, MIGRATION_0002)) + requestModel = state.apps.get_model(APP_LABEL, "Request") assert not hasattr(requestModel, "submitter_forward_username") - state = migrator.apply_tested_migration((APP_NAME, MIGRATION_0003)) - requestModel2 = state.apps.get_model(APP_NAME, "Request") + state = migrator.apply_tested_migration((APP_LABEL, MIGRATION_0003)) + requestModel2 = state.apps.get_model(APP_LABEL, "Request") assert hasattr(requestModel2, "submitter_forward_username")