2020from django .db import transaction
2121from django .db .models import ObjectDoesNotExist
2222from django .db .models import Q
23+ from django .db .models .signals import post_delete
24+ from django .db .models .signals import post_save
2325from django .utils .translation import gettext_lazy as _
2426
2527from license_expression import Licensing
4042from dje .models import Dataspace
4143from dje .utils import get_help_text
4244from dje .utils import is_uuid4
45+ from policy .signals import evaluate_product_rules_on_productpackage_change
46+ from policy .tasks import evaluate_product_rules_task
4347from product_portfolio .forms import ProductComponentLicenseExpressionFormMixin
4448from product_portfolio .models import CodebaseResource
4549from product_portfolio .models import CodebaseResourceUsage
5054from product_portfolio .models import ProductPackage
5155from product_portfolio .models import ProductRelationStatus
5256from product_portfolio .models import ScanCodeProject
57+ from vulnerabilities .triage .signals import reevaluate_on_product_package_change
58+ from vulnerabilities .triage .tasks import reevaluate_product_triage_rulesets_task
5359
5460logger = logging .getLogger (__name__ )
5561
@@ -62,6 +68,35 @@ def log_elapsed(label):
6268 logger .info (f"{ label } : { time .perf_counter () - start :.1f} s" )
6369
6470
71+ @contextmanager
72+ def paused_product_package_reevaluation ():
73+ """
74+ Pause the policy and triage re-evaluation signals triggered by ProductPackage changes,
75+ for the duration of a bulk import. Call `reevaluate_products()` once the import completes
76+ to evaluate each affected product exactly once, instead of once per imported row.
77+ """
78+ receivers = [
79+ evaluate_product_rules_on_productpackage_change ,
80+ reevaluate_on_product_package_change ,
81+ ]
82+ for receiver in receivers :
83+ post_save .disconnect (receiver , sender = ProductPackage )
84+ post_delete .disconnect (receiver , sender = ProductPackage )
85+ try :
86+ yield
87+ finally :
88+ for receiver in receivers :
89+ post_save .connect (receiver , sender = ProductPackage )
90+ post_delete .connect (receiver , sender = ProductPackage )
91+
92+
93+ def reevaluate_products (products ):
94+ """Queue the policy and triage re-evaluation once for each of the given products."""
95+ for product in products :
96+ evaluate_product_rules_task .delay (product_uuid = product .uuid )
97+ reevaluate_product_triage_rulesets_task .delay (product_uuid = product .uuid )
98+
99+
65100class CleanProductMixin (ComponentRelatedFieldImportMixin ):
66101 def clean_product (self ):
67102 queryset = Product .objects .get_queryset (self .user )
@@ -230,6 +265,14 @@ def clean_package(self):
230265class ProductPackageImporter (BaseImporter ):
231266 model_form = ProductPackageImportForm
232267
268+ def save_all (self ):
269+ with paused_product_package_reevaluation ():
270+ super ().save_all ()
271+
272+ touched_product_packages = self .results ["added" ] + self .results ["modified" ]
273+ products = {product_package .product for product_package in touched_product_packages }
274+ reevaluate_products (products )
275+
233276
234277class CodebaseResourceImportForm (CleanProductMixin , BaseImportModelForm ):
235278 product = forms .CharField (
@@ -434,7 +477,9 @@ def save(self):
434477 self .create_scancode_project ()
435478 self .load_data_from_file ()
436479 self .validate_headers ()
437- self .import_packages ()
480+ with paused_product_package_reevaluation ():
481+ self .import_packages ()
482+ reevaluate_products ([self .product ])
438483 if self .create_codebase_resources :
439484 self .import_codebase_resources ()
440485 self .update_scancode_project ()
@@ -720,7 +765,9 @@ def save(self):
720765 self .dependencies = scancodeio .fetch_project_dependencies (self .project_uuid )
721766
722767 with log_elapsed ("import_packages" ):
723- self .import_packages ()
768+ with paused_product_package_reevaluation ():
769+ self .import_packages ()
770+ reevaluate_products ([self .product ])
724771
725772 if self .create_dependencies :
726773 with log_elapsed ("import_dependencies" ):
0 commit comments