From 93724bcda5c45940790c848f19ded3beb9a64656 Mon Sep 17 00:00:00 2001 From: tdruez Date: Fri, 2 Aug 2024 12:17:09 +0400 Subject: [PATCH] Add declared_dependency field on ProductDependency model #138 Signed-off-by: tdruez --- component_catalog/models.py | 4 +-- product_portfolio/admin.py | 1 + product_portfolio/api.py | 2 ++ product_portfolio/importers.py | 3 +++ ...7_productdependency_declared_dependency.py | 18 +++++++++++++ product_portfolio/models.py | 13 ++++++---- .../tabs/tab_dependencies.html | 6 ++--- product_portfolio/views.py | 26 +++++-------------- ...er_columntemplate_content_type_and_more.py | 25 ++++++++++++++++++ reporting/tests/test_introspection.py | 1 + 10 files changed, 68 insertions(+), 31 deletions(-) create mode 100644 product_portfolio/migrations/0007_productdependency_declared_dependency.py create mode 100644 reporting/migrations/0002_alter_columntemplate_content_type_and_more.py diff --git a/component_catalog/models.py b/component_catalog/models.py index 9950ed16..6b3a8bbb 100644 --- a/component_catalog/models.py +++ b/component_catalog/models.py @@ -1634,9 +1634,7 @@ def annotate_sortable_identifier(self): ) def only_rendering_fields(self): - """ - Minimum requirements to render a Package element in the UI. - """ + """Minimum requirements to render a Package element in the UI.""" return self.only( "uuid", *PACKAGE_URL_FIELDS, diff --git a/product_portfolio/admin.py b/product_portfolio/admin.py index 9b161362..cc98d8f6 100644 --- a/product_portfolio/admin.py +++ b/product_portfolio/admin.py @@ -801,6 +801,7 @@ class ProductDependencyAdmin(ProductRelatedAdminMixin): AsLink("product"), AsLink("for_package"), AsLink("resolved_to_package"), + "declared_dependency", "extracted_requirement", "scope", "datasource_id", diff --git a/product_portfolio/api.py b/product_portfolio/api.py index 9b6a9ffa..ddee9a06 100644 --- a/product_portfolio/api.py +++ b/product_portfolio/api.py @@ -796,6 +796,7 @@ class Meta: "uuid", "product", "dependency_uid", + "declared_dependency", "scope", "datasource_id", "is_runtime", @@ -834,6 +835,7 @@ class Meta: "dependency_uid", "for_package", "resolved_to_package", + "declared_dependency", "extracted_requirement", "scope", "datasource_id", diff --git a/product_portfolio/importers.py b/product_portfolio/importers.py index 98e08550..df92bada 100644 --- a/product_portfolio/importers.py +++ b/product_portfolio/importers.py @@ -748,6 +748,9 @@ def import_dependency(self, dependency_data): resolved_to_package_uid ) + if purl := dependency_data.get("purl"): + dependency_data["declared_dependency"] = purl + try: dependency = ProductDependency.create_from_data( user=self.user, diff --git a/product_portfolio/migrations/0007_productdependency_declared_dependency.py b/product_portfolio/migrations/0007_productdependency_declared_dependency.py new file mode 100644 index 00000000..6a438d6e --- /dev/null +++ b/product_portfolio/migrations/0007_productdependency_declared_dependency.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.6 on 2024-08-02 08:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('product_portfolio', '0006_productdependency'), + ] + + operations = [ + migrations.AddField( + model_name='productdependency', + name='declared_dependency', + field=models.CharField(blank=True, help_text='A dependency as stated in a project manifest or lockfile, which may be required or optional, and may be associated with specific version ranges.', max_length=1024), + ), + ] diff --git a/product_portfolio/models.py b/product_portfolio/models.py index 1a71ab9a..50f89b24 100644 --- a/product_portfolio/models.py +++ b/product_portfolio/models.py @@ -1362,6 +1362,14 @@ class ProductDependency(HistoryFieldsMixin, DataspacedModel): blank=True, null=True, ) + declared_dependency = models.CharField( + max_length=1024, + blank=True, + help_text=_( + "A dependency as stated in a project manifest or lockfile, which may be " + "required or optional, and may be associated with specific version ranges." + ), + ) extracted_requirement = models.CharField( max_length=256, blank=True, @@ -1414,8 +1422,3 @@ class Meta: def __str__(self): return self.dependency_uid - - @property - def package_url(self): - if self.dependency_uid.startswith("pkg:"): - return self.dependency_uid.split("?")[0] diff --git a/product_portfolio/templates/product_portfolio/tabs/tab_dependencies.html b/product_portfolio/templates/product_portfolio/tabs/tab_dependencies.html index 5886586b..fad81795 100644 --- a/product_portfolio/templates/product_portfolio/tabs/tab_dependencies.html +++ b/product_portfolio/templates/product_portfolio/tabs/tab_dependencies.html @@ -128,9 +128,9 @@ {% endif %} - {% if dependency.package_url %} - {{ dependency.package_url }} - + {% if dependency.declared_dependency %} + {{ dependency.declared_dependency }} + {% endif %} diff --git a/product_portfolio/views.py b/product_portfolio/views.py index 825bc6fd..7e2bee78 100644 --- a/product_portfolio/views.py +++ b/product_portfolio/views.py @@ -412,8 +412,7 @@ def tab_hierarchy(self): ) declared_dependencies_prefetch = models.Prefetch( - "package__declared_dependencies", - ProductDependency.objects.product(product) + "package__declared_dependencies", ProductDependency.objects.product(product) ) productpackage_qs = ( @@ -651,8 +650,7 @@ def get_context_data(self, **kwargs): "licenses", License.objects.select_related("usage_policy") ) declared_dependencies_prefetch = models.Prefetch( - "package__declared_dependencies", - ProductDependency.objects.product(self.object) + "package__declared_dependencies", ProductDependency.objects.product(self.object) ) productpackage_qs = ( @@ -968,16 +966,9 @@ class ProductTabDependenciesView( def get_context_data(self, **kwargs): context_data = super().get_context_data(**kwargs) - dependency_qs = ( - self.object.dependencies - .prefetch_related( - models.Prefetch( - "for_package", Package.objects.only_rendering_fields() - ), - models.Prefetch( - "resolved_to_package", Package.objects.only_rendering_fields() - ) - ) + dependency_qs = self.object.dependencies.prefetch_related( + models.Prefetch("for_package", Package.objects.only_rendering_fields()), + models.Prefetch("resolved_to_package", Package.objects.only_rendering_fields()), ) filter_dependency = DependencyFilterSet( @@ -991,8 +982,7 @@ def get_context_data(self, **kwargs): filtered_and_annotated_qs = ( filter_dependency.qs # TODO: This is not scoped by product - .with_resolved_to_dependencies_count() - .order_by( + .with_resolved_to_dependencies_count().order_by( "for_package__type", "for_package__namespace", "for_package__name", @@ -1009,10 +999,6 @@ def get_context_data(self, **kwargs): for field in ProductDependency._meta.get_fields() if hasattr(field, "help_text") } - help_texts["declared_dependency"] = ( - "A dependency as stated in a project manifest or lockfile, which may be " - "required or optional, and may be associated with specific version ranges." - ) context_data.update( { diff --git a/reporting/migrations/0002_alter_columntemplate_content_type_and_more.py b/reporting/migrations/0002_alter_columntemplate_content_type_and_more.py new file mode 100644 index 00000000..b5c3b2cd --- /dev/null +++ b/reporting/migrations/0002_alter_columntemplate_content_type_and_more.py @@ -0,0 +1,25 @@ +# Generated by Django 5.0.6 on 2024-08-02 08:11 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ('reporting', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='columntemplate', + name='content_type', + field=models.ForeignKey(help_text='Choose the primary data source for your column template: licenses, components, or owners.', limit_choices_to=models.Q(models.Q(('app_label', 'license_library'), ('model', 'license')), models.Q(('app_label', 'component_catalog'), ('model', 'component')), models.Q(('app_label', 'component_catalog'), ('model', 'subcomponent')), models.Q(('app_label', 'component_catalog'), ('model', 'package')), models.Q(('app_label', 'organization'), ('model', 'owner')), models.Q(('app_label', 'workflow'), ('model', 'request')), models.Q(('app_label', 'license_library'), ('model', 'licensetag')), models.Q(('app_label', 'license_library'), ('model', 'licenseprofile')), models.Q(('app_label', 'license_library'), ('model', 'licensechoice')), models.Q(('app_label', 'product_portfolio'), ('model', 'product')), models.Q(('app_label', 'product_portfolio'), ('model', 'productcomponent')), models.Q(('app_label', 'product_portfolio'), ('model', 'productpackage')), models.Q(('app_label', 'product_portfolio'), ('model', 'productinventoryitem')), models.Q(('app_label', 'product_portfolio'), ('model', 'productdependency')), models.Q(('app_label', 'product_portfolio'), ('model', 'codebaseresource')), _connector='OR'), on_delete=django.db.models.deletion.PROTECT, to='contenttypes.contenttype', verbose_name='object type'), + ), + migrations.AlterField( + model_name='query', + name='content_type', + field=models.ForeignKey(help_text='Choose the primary data source for your query: licenses, components, or owners.', limit_choices_to=models.Q(models.Q(('app_label', 'license_library'), ('model', 'license')), models.Q(('app_label', 'component_catalog'), ('model', 'component')), models.Q(('app_label', 'component_catalog'), ('model', 'subcomponent')), models.Q(('app_label', 'component_catalog'), ('model', 'package')), models.Q(('app_label', 'organization'), ('model', 'owner')), models.Q(('app_label', 'workflow'), ('model', 'request')), models.Q(('app_label', 'license_library'), ('model', 'licensetag')), models.Q(('app_label', 'license_library'), ('model', 'licenseprofile')), models.Q(('app_label', 'license_library'), ('model', 'licensechoice')), models.Q(('app_label', 'product_portfolio'), ('model', 'product')), models.Q(('app_label', 'product_portfolio'), ('model', 'productcomponent')), models.Q(('app_label', 'product_portfolio'), ('model', 'productpackage')), models.Q(('app_label', 'product_portfolio'), ('model', 'productinventoryitem')), models.Q(('app_label', 'product_portfolio'), ('model', 'productdependency')), models.Q(('app_label', 'product_portfolio'), ('model', 'codebaseresource')), _connector='OR'), on_delete=django.db.models.deletion.PROTECT, to='contenttypes.contenttype', verbose_name='object type'), + ), + ] diff --git a/reporting/tests/test_introspection.py b/reporting/tests/test_introspection.py index c3274f1a..bac7c22a 100644 --- a/reporting/tests/test_introspection.py +++ b/reporting/tests/test_introspection.py @@ -60,6 +60,7 @@ def test_reporting_get_reportable_models(self): ", " ", " ", " + ", " "" "]" )