2020from django .contrib .auth import get_user_model
2121from django .contrib .auth .models import UserManager
2222from django .core import exceptions
23+ from django .core .cache import cache
2324from django .core .exceptions import ValidationError
2425from django .core .paginator import Paginator
2526from django .core .validators import MaxValueValidator
@@ -471,18 +472,19 @@ def get_fixed_by_package_versions(self, purl: PackageURL, fix=True):
471472 Return a queryset of all the package versions of this `package` that fix any vulnerability.
472473 If `fix` is False, return all package versions whether or not they fix a vulnerability.
473474 """
474- filter_dict = {
475- "name" : purl .name ,
476- "namespace" : purl .namespace ,
475+ # TODO: Move this to Package object method
476+ filters = {
477477 "type" : purl .type ,
478+ "namespace" : purl .namespace ,
479+ "name" : purl .name ,
478480 "qualifiers" : purl .qualifiers ,
479481 "subpath" : purl .subpath ,
480482 }
481483
482484 if fix :
483- filter_dict ["fixing_vulnerabilities__isnull" ] = False
485+ filters ["fixing_vulnerabilities__isnull" ] = False
484486
485- return Package .objects .filter (** filter_dict ).distinct ()
487+ return Package .objects .filter (** filters ).distinct ()
486488
487489 def get_or_create_from_purl (self , purl : Union [PackageURL , str ]):
488490 """
@@ -648,7 +650,8 @@ class Package(PackageURLMixin):
648650 fixing_vulnerabilities = models .ManyToManyField (
649651 to = "Vulnerability" ,
650652 through = "FixingPackageRelatedVulnerability" ,
651- related_name = "fixed_by_packages" , # Unique related_name
653+ # Unique related_name
654+ related_name = "fixed_by_packages" ,
652655 )
653656
654657 package_url = models .CharField (
@@ -779,6 +782,10 @@ def version_class(self):
779782 def current_version (self ):
780783 return self .version_class (self .version )
781784
785+ @property
786+ def vulnerabilities (self ):
787+ return self .affected_by_vulnerabilities .all () | self .fixing_vulnerabilities .all ()
788+
782789 @property
783790 def next_non_vulnerable_version (self ):
784791 """
@@ -787,10 +794,6 @@ def next_non_vulnerable_version(self):
787794 next_non_vulnerable , _ = self .get_non_vulnerable_versions ()
788795 return next_non_vulnerable .version if next_non_vulnerable else None
789796
790- @property
791- def vulnerabilities (self ):
792- return self .affected_by_vulnerabilities .all () | self .fixing_vulnerabilities .all ()
793-
794797 @property
795798 def latest_non_vulnerable_version (self ):
796799 """
@@ -823,6 +826,67 @@ def get_non_vulnerable_versions(self):
823826
824827 return None , None
825828
829+ @property
830+ def non_vulnerable_versions (self ):
831+ """
832+ Cache the result of get_non_vulnerable_versions_v2 to avoid redundant computations.
833+ """
834+ if not hasattr (self , "_non_vulnerable_versions_cache" ):
835+ self ._non_vulnerable_versions_cache = self .get_non_vulnerable_versions_v2 ()
836+ return self ._non_vulnerable_versions_cache
837+
838+ @property
839+ def next_non_vulnerable_package (self ):
840+ """
841+ Return the purl of the next non-vulnerable package version.
842+ """
843+ next_non_vulnerable , _ = self .get_non_vulnerable_versions_v2 ()
844+ return next_non_vulnerable .purl if next_non_vulnerable else None
845+
846+ @property
847+ def latest_non_vulnerable_package (self ):
848+ """
849+ Return the purl of the latest non-vulnerable package version.
850+ """
851+ _ , latest_non_vulnerable = self .get_non_vulnerable_versions_v2 ()
852+ return latest_non_vulnerable .purl if latest_non_vulnerable else None
853+
854+ def get_non_vulnerable_versions_v2 (self ):
855+ """
856+ Return a tuple of three Package instance:
857+ - first fixing version
858+ - next non-vulnerable version
859+ - latest non-vulnerable version
860+ Return a tuple of (None, None) if there is no non-vulnerable version.
861+ """
862+ cache_key = f"non_vulnerable_versions_{ self .id } "
863+ result = cache .get (cache_key )
864+ if result is not None :
865+ return result
866+
867+ non_vulnerable_versions = Package .objects .get_fixed_by_package_versions (
868+ self , fix = False
869+ ).only_non_vulnerable ()
870+ sorted_versions = self .sort_by_version (non_vulnerable_versions )
871+
872+ later_non_vulnerable_versions = [
873+ non_vuln_ver
874+ for non_vuln_ver in sorted_versions
875+ if self .version_class (non_vuln_ver .version ) > self .current_version
876+ ]
877+
878+ if later_non_vulnerable_versions :
879+ sorted_versions = self .sort_by_version (later_non_vulnerable_versions )
880+ next_non_vulnerable = sorted_versions [0 ]
881+ latest_non_vulnerable = sorted_versions [- 1 ]
882+ cache .set (
883+ cache_key , (next_non_vulnerable , latest_non_vulnerable ), timeout = 3600
884+ )
885+ return next_non_vulnerable , latest_non_vulnerable
886+
887+ cache .set (cache_key , (None , None ), timeout = 3600 )
888+ return None , None
889+
826890 @property
827891 def fixed_package_details (self ):
828892 """
@@ -928,15 +992,14 @@ class PackageRelatedVulnerabilityBase(models.Model):
928992 package = models .ForeignKey (
929993 Package ,
930994 on_delete = models .CASCADE ,
931- # related_name="%(class)s_set", # Unique related_name per subclass
932995 )
933996
934997 vulnerability = models .ForeignKey (
935998 Vulnerability ,
936999 on_delete = models .CASCADE ,
937- # related_name="%(class)s_set", # Unique related_name per subclass
9381000 )
9391001
1002+ # TODO: Fix the help text
9401003 created_by = models .CharField (
9411004 max_length = 100 ,
9421005 blank = True ,
0 commit comments