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
@@ -1647,6 +1651,86 @@ def __str__(self):
16471651 return self .label
16481652
16491653
1654+ class PackageContentFieldMixin (models .Model ):
1655+ # Keep in sync with purldb/packagedb.models.PackageContentType
1656+ class PackageTypes (models .IntegerChoices ):
1657+ CURATION = 1 , "curation"
1658+ PATCH = 2 , "patch"
1659+ SOURCE_REPO = 3 , "source_repo"
1660+ SOURCE_ARCHIVE = 4 , "source_archive"
1661+ BINARY = 5 , "binary"
1662+ TEST = 6 , "test"
1663+ DOC = 7 , "doc"
1664+
1665+ package_content = models .IntegerField (
1666+ null = True ,
1667+ choices = PackageTypes .choices ,
1668+ help_text = _ ("Content of this package as one of: {}" .format (", " .join (PackageTypes .labels ))),
1669+ )
1670+
1671+ class Meta :
1672+ abstract = True
1673+
1674+
1675+ def get_plain_package_url_expression ():
1676+ """
1677+ Return a Django expression to compute the "PLAIN" Package URL (purl).
1678+ Return an empty string if the required `type` or `name` values are missing.
1679+ """
1680+ plain_package_url = Concat (
1681+ Value ("pkg:" ),
1682+ F ("type" ),
1683+ Case (
1684+ When (namespace = "" , then = Value ("" )),
1685+ default = Concat (Value ("/" ), F ("namespace" )),
1686+ output_field = CharField (),
1687+ ),
1688+ Value ("/" ),
1689+ F ("name" ),
1690+ Case (
1691+ When (version = "" , then = Value ("" )),
1692+ default = Concat (Value ("@" ), F ("version" )),
1693+ output_field = CharField (),
1694+ ),
1695+ output_field = CharField (),
1696+ )
1697+
1698+ return Case (
1699+ When (type = "" , then = Value ("" )),
1700+ When (name = "" , then = Value ("" )),
1701+ default = plain_package_url ,
1702+ output_field = CharField (),
1703+ )
1704+
1705+
1706+ def get_package_url_expression ():
1707+ """
1708+ Return a Django expression to compute the "FULL" Package URL (purl).
1709+ Return an empty string if the required `type` or `name` values are missing.
1710+ """
1711+ package_url = Concat (
1712+ get_plain_package_url_expression (),
1713+ Case (
1714+ When (qualifiers = "" , then = Value ("" )),
1715+ default = Concat (Value ("?" ), F ("qualifiers" )),
1716+ output_field = CharField (),
1717+ ),
1718+ Case (
1719+ When (subpath = "" , then = Value ("" )),
1720+ default = Concat (Value ("#" ), F ("subpath" )),
1721+ output_field = CharField (),
1722+ ),
1723+ output_field = CharField (),
1724+ )
1725+
1726+ return Case (
1727+ When (type = "" , then = Value ("" )),
1728+ When (name = "" , then = Value ("" )),
1729+ default = package_url ,
1730+ output_field = CharField (),
1731+ )
1732+
1733+
16501734PACKAGE_URL_FIELDS = ["type" , "namespace" , "name" , "version" , "qualifiers" , "subpath" ]
16511735
16521736
@@ -1665,6 +1749,13 @@ def annotate_sortable_identifier(self):
16651749 sortable_identifier = Concat (* PACKAGE_URL_FIELDS , "filename" , output_field = CharField ())
16661750 )
16671751
1752+ def annotate_plain_purl (self ):
1753+ """
1754+ Annotate the QuerySet with a database computed "PLAIN" PURL value that combines
1755+ the base Package URL fields: `type, `namespace`, `name`, `version`.
1756+ """
1757+ return self .annotate (plain_purl = get_plain_package_url_expression ())
1758+
16681759 def only_rendering_fields (self ):
16691760 """Minimum requirements to render a Package element in the UI."""
16701761 return self .only (
@@ -1707,6 +1798,7 @@ class Package(
17071798 URLFieldsMixin ,
17081799 HashFieldsMixin ,
17091800 PackageURLMixin ,
1801+ PackageContentFieldMixin ,
17101802 DataspacedModel ,
17111803):
17121804 filename = models .CharField (
@@ -1850,6 +1942,11 @@ class Package(
18501942 related_name = "affected_%(class)ss" ,
18511943 help_text = _ ("Vulnerabilities affecting this object." ),
18521944 )
1945+ # related_packages = models.ManyToManyField(
1946+ # to="component_catalog.PackageSet",
1947+ # # related_name="packages",
1948+ # help_text=_("A set representing the Package sets this Package is a member of."),
1949+ # )
18531950
18541951 objects = DataspacedManager .from_queryset (PackageQuerySet )()
18551952
@@ -2538,6 +2635,39 @@ class Meta:
25382635 unique_together = (("package" , "vulnerability" ), ("dataspace" , "uuid" ))
25392636
25402637
2638+ # class PackageSet(DataspacedModel):
2639+ # """A group of related Packages by their plain PURL."""
2640+ #
2641+ # type = models.CharField(
2642+ # max_length=16,
2643+ # blank=True,
2644+ # )
2645+ # namespace = models.CharField(
2646+ # max_length=255,
2647+ # blank=True,
2648+ # )
2649+ # name = models.CharField(
2650+ # max_length=100,
2651+ # blank=True,
2652+ # )
2653+ # version = models.CharField(
2654+ # max_length=100,
2655+ # blank=True,
2656+ # )
2657+ #
2658+ # def add_to_package_set(self, package):
2659+ # self.packages.add(package)
2660+ #
2661+ # def get_package_set_members(self):
2662+ # """Return related Packages"""
2663+ # return self.packages.order_by(
2664+ # "package_content",
2665+ # )
2666+ #
2667+ # class Meta:
2668+ # unique_together = [("dataspace", "uuid")]
2669+
2670+
25412671class ComponentAffectedByVulnerability (AffectedByVulnerabilityRelationship ):
25422672 component = models .ForeignKey (
25432673 to = "component_catalog.Component" ,
0 commit comments