1818from django .core .exceptions import ValidationError
1919from django .core .validators import EMPTY_VALUES
2020from django .db import models
21+ from django .db .models import Case
2122from django .db .models import CharField
2223from django .db .models import Count
2324from django .db .models import Exists
25+ from django .db .models import F
2426from django .db .models import OuterRef
27+ from django .db .models import Value
28+ from django .db .models import When
2529from django .db .models .functions import Concat
2630from django .dispatch import receiver
2731from django .template .defaultfilters import filesizeformat
@@ -1651,6 +1655,65 @@ def __str__(self):
16511655PACKAGE_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+
16541717class 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 (
0 commit comments