Skip to content

Commit 4c5576b

Browse files
committed
Add purl annotation methods on the PackageQuerySet #276
Signed-off-by: tdruez <[email protected]>
1 parent e06fb5c commit 4c5576b

File tree

2 files changed

+112
-0
lines changed

2 files changed

+112
-0
lines changed

component_catalog/models.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,14 @@
1818
from django.core.exceptions import ValidationError
1919
from django.core.validators import EMPTY_VALUES
2020
from django.db import models
21+
from django.db.models import Case
2122
from django.db.models import CharField
2223
from django.db.models import Count
2324
from django.db.models import Exists
25+
from django.db.models import F
2426
from django.db.models import OuterRef
27+
from django.db.models import Value
28+
from django.db.models import When
2529
from django.db.models.functions import Concat
2630
from django.dispatch import receiver
2731
from django.template.defaultfilters import filesizeformat
@@ -1651,6 +1655,65 @@ def __str__(self):
16511655
PACKAGE_URL_FIELDS = ["type", "namespace", "name", "version", "qualifiers", "subpath"]
16521656

16531657

1658+
def get_plain_package_url_expression():
1659+
"""
1660+
Return a Django expression to compute the "PLAIN" Package URL (PURL).
1661+
Return an empty string if the required `type` or `name` values are missing.
1662+
"""
1663+
plain_package_url = Concat(
1664+
Value("pkg:"),
1665+
F("type"),
1666+
Case(
1667+
When(namespace="", then=Value("")),
1668+
default=Concat(Value("/"), F("namespace")),
1669+
output_field=CharField(),
1670+
),
1671+
Value("/"),
1672+
F("name"),
1673+
Case(
1674+
When(version="", then=Value("")),
1675+
default=Concat(Value("@"), F("version")),
1676+
output_field=CharField(),
1677+
),
1678+
output_field=CharField(),
1679+
)
1680+
1681+
return Case(
1682+
When(type="", then=Value("")),
1683+
When(name="", then=Value("")),
1684+
default=plain_package_url,
1685+
output_field=CharField(),
1686+
)
1687+
1688+
1689+
def get_package_url_expression():
1690+
"""
1691+
Return a Django expression to compute the "FULL" Package URL (PURL).
1692+
Return an empty string if the required `type` or `name` values are missing.
1693+
"""
1694+
package_url = Concat(
1695+
get_plain_package_url_expression(),
1696+
Case(
1697+
When(qualifiers="", then=Value("")),
1698+
default=Concat(Value("?"), F("qualifiers")),
1699+
output_field=CharField(),
1700+
),
1701+
Case(
1702+
When(subpath="", then=Value("")),
1703+
default=Concat(Value("#"), F("subpath")),
1704+
output_field=CharField(),
1705+
),
1706+
output_field=CharField(),
1707+
)
1708+
1709+
return Case(
1710+
When(type="", then=Value("")),
1711+
When(name="", then=Value("")),
1712+
default=package_url,
1713+
output_field=CharField(),
1714+
)
1715+
1716+
16541717
class PackageQuerySet(PackageURLQuerySetMixin, VulnerabilityQuerySetMixin, DataspacedQuerySet):
16551718
def has_package_url(self):
16561719
"""Return objects with Package URL defined."""
@@ -1666,6 +1729,26 @@ def annotate_sortable_identifier(self):
16661729
sortable_identifier=Concat(*PACKAGE_URL_FIELDS, "filename", output_field=CharField())
16671730
)
16681731

1732+
def annotate_plain_package_url(self):
1733+
"""
1734+
Annotate the QuerySet with a computed 'plain' Package URL (PURL).
1735+
1736+
This plain PURL is a simplified version that includes only the core fields:
1737+
`type`, `namespace`, `name`, and `version`. It omits any qualifiers or
1738+
subpath components, providing a normalized and minimal representation
1739+
of the Package URL.
1740+
"""
1741+
return self.annotate(plain_purl=get_plain_package_url_expression())
1742+
1743+
def annotate_package_url(self):
1744+
"""
1745+
Annotate the QuerySet with a fully-computed Package URL (PURL).
1746+
1747+
This includes the core PURL fields (`type`, `namespace`, `name`, `version`)
1748+
as well as any qualifiers and subpath components.
1749+
"""
1750+
return self.annotate(purl=get_package_url_expression())
1751+
16691752
def only_rendering_fields(self):
16701753
"""Minimum requirements to render a Package element in the UI."""
16711754
return self.only(

component_catalog/tests/test_models.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2709,3 +2709,32 @@ def test_vulnerability_mixin_is_vulnerable_property(self):
27092709
package2 = make_package(self.dataspace)
27102710
self.assertTrue(package1.is_vulnerable)
27112711
self.assertFalse(package2.is_vulnerable)
2712+
2713+
def test_package_queryset_has_package_url(self):
2714+
package1 = make_package(self.dataspace, package_url="pkg:pypi/[email protected]")
2715+
make_package(self.dataspace)
2716+
qs = Package.objects.has_package_url()
2717+
self.assertQuerySetEqual(qs, [package1])
2718+
2719+
def test_package_queryset_annotate_sortable_identifier(self):
2720+
package1 = make_package(self.dataspace, package_url="pkg:pypi/[email protected]")
2721+
package2 = make_package(self.dataspace)
2722+
qs = Package.objects.annotate_sortable_identifier()
2723+
self.assertEqual("pypidjango5.0", qs.get(pk=package1.pk).sortable_identifier)
2724+
self.assertEqual(package2.filename, qs.get(pk=package2.pk).sortable_identifier)
2725+
2726+
def test_package_queryset_annotate_package_url(self):
2727+
package_url = "pkg:pypi/[email protected]?qualifier=true#path"
2728+
package1 = make_package(self.dataspace, package_url=package_url)
2729+
package2 = make_package(self.dataspace)
2730+
qs = Package.objects.annotate_package_url()
2731+
self.assertEqual(package_url, qs.get(pk=package1.pk).purl)
2732+
self.assertEqual("", qs.get(pk=package2.pk).purl)
2733+
2734+
def test_package_queryset_annotate_plain_package_url(self):
2735+
package_url = "pkg:pypi/[email protected]?qualifier=true#path"
2736+
package1 = make_package(self.dataspace, package_url=package_url)
2737+
package2 = make_package(self.dataspace)
2738+
qs = Package.objects.annotate_plain_package_url()
2739+
self.assertEqual("pkg:pypi/[email protected]", qs.get(pk=package1.pk).plain_purl)
2740+
self.assertEqual("", qs.get(pk=package2.pk).plain_purl)

0 commit comments

Comments
 (0)