18
18
from django .core .exceptions import ValidationError
19
19
from django .core .validators import EMPTY_VALUES
20
20
from django .db import models
21
+ from django .db .models import Case
21
22
from django .db .models import CharField
22
23
from django .db .models import Count
23
24
from django .db .models import Exists
25
+ from django .db .models import F
24
26
from django .db .models import OuterRef
27
+ from django .db .models import Value
28
+ from django .db .models import When
25
29
from django .db .models .functions import Concat
26
30
from django .dispatch import receiver
27
31
from django .template .defaultfilters import filesizeformat
@@ -1651,6 +1655,65 @@ def __str__(self):
1651
1655
PACKAGE_URL_FIELDS = ["type" , "namespace" , "name" , "version" , "qualifiers" , "subpath" ]
1652
1656
1653
1657
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
+
1654
1717
class PackageQuerySet (PackageURLQuerySetMixin , VulnerabilityQuerySetMixin , DataspacedQuerySet ):
1655
1718
def has_package_url (self ):
1656
1719
"""Return objects with Package URL defined."""
@@ -1666,6 +1729,26 @@ def annotate_sortable_identifier(self):
1666
1729
sortable_identifier = Concat (* PACKAGE_URL_FIELDS , "filename" , output_field = CharField ())
1667
1730
)
1668
1731
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
+
1669
1752
def only_rendering_fields (self ):
1670
1753
"""Minimum requirements to render a Package element in the UI."""
1671
1754
return self .only (
@@ -2533,17 +2616,12 @@ def update_from_purldb(self, user):
2533
2616
for field_name in [* hash_field_names , * identifier_fields ]:
2534
2617
package_data .pop (field_name , None )
2535
2618
2536
- # try:
2537
2619
updated_fields = self .update_from_data (
2538
2620
user ,
2539
2621
package_data ,
2540
2622
override = False ,
2541
2623
override_unknown = True ,
2542
2624
)
2543
- # except IntegrityError as e:
2544
- # logger.error(f"[update_from_purldb] Skipping {self} due to IntegrityError: {e}")
2545
- # return []
2546
-
2547
2625
return updated_fields
2548
2626
2549
2627
def update_from_scan (self , user ):
@@ -2560,6 +2638,32 @@ def update_from_scan(self, user):
2560
2638
updated_fields = scancodeio .update_from_scan (package = self , user = user )
2561
2639
return updated_fields
2562
2640
2641
+ def get_related_packages_qs (self ):
2642
+ """
2643
+ Return a QuerySet of packages that are considered part of the same
2644
+ "Package Set".
2645
+
2646
+ A "Package Set" consists of all packages that share the same "plain"
2647
+ Package URL (PURL), meaning they have identical values for the following PURL
2648
+ components:
2649
+ `type`, `namespace`, `name`, and `version`.
2650
+ The `qualifiers` and `subpath` components are ignored for this comparison.
2651
+ """
2652
+ plain_package_url = self .plain_package_url
2653
+ if not plain_package_url :
2654
+ return None
2655
+
2656
+ return (
2657
+ self .__class__ .objects .scope (self .dataspace )
2658
+ .for_package_url (plain_package_url , exact_match = True )
2659
+ .order_by (
2660
+ * PACKAGE_URL_FIELDS ,
2661
+ "filename" ,
2662
+ "download_url" ,
2663
+ )
2664
+ .distinct ()
2665
+ )
2666
+
2563
2667
2564
2668
class PackageAssignedLicense (DataspacedModel ):
2565
2669
package = models .ForeignKey (
0 commit comments