diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a38f8fc4e..b34ebbb90 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -18,6 +18,9 @@ v34.7.1 (unreleased) The `package_uid` is now included in each BOM Component as a property. https://github.com/nexB/scancode.io/issues/1316 +- Add ``results_url`` and ``summary_url`` on the API ProjectSerializer. + https://github.com/nexB/scancode.io/issues/1325 + v34.7.0 (2024-07-02) -------------------- diff --git a/scanpipe/api/serializers.py b/scanpipe/api/serializers.py index 2f579a57e..fc8e75c1f 100644 --- a/scanpipe/api/serializers.py +++ b/scanpipe/api/serializers.py @@ -23,6 +23,7 @@ from django.apps import apps from rest_framework import serializers +from rest_framework.reverse import reverse from taggit.serializers import TaggitSerializer from taggit.serializers import TagListSerializerField @@ -144,6 +145,7 @@ class Meta: class ProjectSerializer( ExcludeFromListViewMixin, + SerializerExcludeFieldsMixin, PipelineChoicesMixin, TaggitSerializer, serializers.ModelSerializer, @@ -177,6 +179,8 @@ class ProjectSerializer( discovered_dependencies_summary = serializers.SerializerMethodField() codebase_relations_summary = serializers.SerializerMethodField() labels = TagListSerializerField(required=False) + results_url = serializers.SerializerMethodField() + summary_url = serializers.SerializerMethodField() class Meta: model = Project @@ -210,8 +214,9 @@ class Meta: "discovered_packages_summary", "discovered_dependencies_summary", "codebase_relations_summary", + "results_url", + "summary_url", ) - exclude_from_list_view = [ "settings", "input_root", @@ -258,6 +263,16 @@ def validate_input_urls(self, value): """Add support for providing multiple URLs in a single string.""" return [url for entry in value for url in entry.split()] + def get_action_url(self, obj, action_name): + request = self.context.get("request") + return reverse(f"project-{action_name}", kwargs={"pk": obj.pk}, request=request) + + def get_results_url(self, obj): + return self.get_action_url(obj, "results") + + def get_summary_url(self, obj): + return self.get_action_url(obj, "summary") + def create(self, validated_data): """ Create a new `project` with `upload_file` and `pipeline` as optional. diff --git a/scanpipe/models.py b/scanpipe/models.py index 940d168e8..dc6465500 100644 --- a/scanpipe/models.py +++ b/scanpipe/models.py @@ -3807,20 +3807,25 @@ def __str__(self): return str(self.uuid) def get_payload(self, pipeline_run): - return { - "project": { - "uuid": self.project.uuid, - "name": self.project.name, - "input_sources": self.project.get_inputs_with_source(), - }, - "run": { - "uuid": pipeline_run.uuid, - "pipeline_name": pipeline_run.pipeline_name, - "status": pipeline_run.status, - "scancodeio_version": pipeline_run.scancodeio_version, - }, + """Return the Webhook payload generated from project and pipeline_run data.""" + from scanpipe.api.serializers import ProjectSerializer + from scanpipe.api.serializers import RunSerializer + + project_serializer = ProjectSerializer( + instance=self.project, + exclude_fields=("url", "runs"), + ) + run_serializer = RunSerializer( + instance=pipeline_run, + exclude_fields=("url", "project"), + ) + payload = { + "project": project_serializer.data, + "run": run_serializer.data, } + return payload + def deliver(self, pipeline_run): """Deliver this Webhook by sending a POST request to the `target_url`.""" payload = self.get_payload(pipeline_run) diff --git a/scanpipe/tests/pipes/test_scancode.py b/scanpipe/tests/pipes/test_scancode.py index 6f1318f82..775e80bac 100644 --- a/scanpipe/tests/pipes/test_scancode.py +++ b/scanpipe/tests/pipes/test_scancode.py @@ -143,17 +143,8 @@ def test_scanpipe_pipes_scancode_extract_archive_vmimage_qcow2(self): self.assertEqual(sorted(expected), sorted(results)) else: - expected = { - str(input_location): [ - "Unable to read kernel at: /boot/vmlinuz-6.5.0-1022-azure.\n" - "libguestfs requires the kernel executable to be readable.\n" - "This is the case by default on most Linux distributions except on " - "Ubuntu.\nPlease follow the ExtractCode installation instructions " - "in the README.rst at:\n" - "https://github.com/nexB/extractcode/blob/main/README.rst '\n" - ] - } - self.assertEqual(expected, errors) + expected = "libguestfs requires the kernel executable to be readable" + self.assertIn(expected, errors[str(input_location)][0]) def test_scanpipe_pipes_scancode_get_resource_info(self): input_location = str(self.data / "aboutcode" / "notice.NOTICE") diff --git a/scanpipe/tests/test_api.py b/scanpipe/tests/test_api.py index 3181189ea..94d5da67e 100644 --- a/scanpipe/tests/test_api.py +++ b/scanpipe/tests/test_api.py @@ -186,6 +186,14 @@ def test_scanpipe_api_project_detail(self): self.assertEqual(1, response.data["package_count"]) self.assertEqual(1, response.data["dependency_count"]) self.assertEqual(1, response.data["relation_count"]) + self.assertEqual( + f"http://testserver/api/projects/{self.project1.uuid}/results/", + response.data["results_url"], + ) + self.assertEqual( + f"http://testserver/api/projects/{self.project1.uuid}/summary/", + response.data["summary_url"], + ) expected = {"": 1} self.assertEqual(expected, response.data["codebase_resources_summary"]) diff --git a/scanpipe/tests/test_models.py b/scanpipe/tests/test_models.py index 101e10db7..3ea2d9033 100644 --- a/scanpipe/tests/test_models.py +++ b/scanpipe/tests/test_models.py @@ -1895,7 +1895,7 @@ def test_scanpipe_codebase_resource_model_walk_method_problematic_filenames(self self.assertEqual(expected_paths, result) @mock.patch("requests.post") - def test_scanpipe_webhook_subscription_deliver_method(self, mock_post): + def test_scanpipe_webhook_subscription_model_deliver_method(self, mock_post): webhook = self.project1.add_webhook_subscription("https://localhost") self.assertFalse(webhook.delivered) run1 = self.create_run() @@ -1922,6 +1922,67 @@ def test_scanpipe_webhook_subscription_deliver_method(self, mock_post): self.assertTrue(webhook.success) self.assertEqual("text", webhook.response_text) + def test_scanpipe_webhook_subscription_model_get_payload(self): + webhook = self.project1.add_webhook_subscription("https://localhost") + run1 = self.create_run() + payload = webhook.get_payload(run1) + + expected = { + "project": { + "name": "Analysis", + "uuid": str(self.project1.uuid), + "is_archived": False, + "notes": "", + "labels": [], + "settings": {}, + "input_sources": [], + "input_root": [], + "output_root": [], + "next_run": "pipeline", + "extra_data": {}, + "message_count": 0, + "resource_count": 0, + "package_count": 0, + "dependency_count": 0, + "relation_count": 0, + "codebase_resources_summary": {}, + "discovered_packages_summary": { + "total": 0, + "with_missing_resources": 0, + "with_modified_resources": 0, + }, + "discovered_dependencies_summary": { + "total": 0, + "is_runtime": 0, + "is_optional": 0, + "is_resolved": 0, + }, + "codebase_relations_summary": {}, + "results_url": f"/api/projects/{self.project1.uuid}/results/", + "summary_url": f"/api/projects/{self.project1.uuid}/summary/", + }, + "run": { + "pipeline_name": "pipeline", + "status": run1.status, + "description": "", + "selected_groups": None, + "selected_steps": None, + "uuid": str(run1.uuid), + "scancodeio_version": "", + "task_id": None, + "task_start_date": None, + "task_end_date": None, + "task_exitcode": None, + "task_output": "", + "log": "", + "execution_time": None, + }, + } + + del payload["project"]["created_date"] + del payload["run"]["created_date"] + self.assertDictEqual(expected, payload) + def test_scanpipe_discovered_package_model_extract_purl_data(self): package_data = {} expected = {