5
5
import re
6
6
import time
7
7
from datetime import datetime
8
- from functools import reduce
8
+ from functools import partial , reduce
9
9
from pathlib import Path
10
10
from tempfile import NamedTemporaryFile
11
11
from time import strftime
16
16
from django .contrib .auth .models import User
17
17
from django .core .exceptions import PermissionDenied , ValidationError
18
18
from django .db import DEFAULT_DB_ALIAS
19
- from django .db .models import Count , Q
19
+ from django .db .models import Count , OuterRef , Q , Value
20
+ from django .db .models .functions import Coalesce
20
21
from django .db .models .query import Prefetch , QuerySet
21
22
from django .http import HttpRequest , HttpResponse , HttpResponseRedirect , QueryDict , StreamingHttpResponse
22
23
from django .shortcuts import get_object_or_404 , render
104
105
add_error_message_to_response ,
105
106
add_success_message_to_response ,
106
107
async_delete ,
108
+ build_count_subquery ,
107
109
calculate_grade ,
108
110
generate_file_response_from_file_path ,
109
111
get_cal_event ,
@@ -151,7 +153,6 @@ def engagement_calendar(request):
151
153
152
154
153
155
def get_filtered_engagements (request , view ):
154
-
155
156
if view not in {"all" , "active" }:
156
157
msg = f"View { view } is not allowed"
157
158
raise ValidationError (msg )
@@ -161,37 +162,29 @@ def get_filtered_engagements(request, view):
161
162
if view == "active" :
162
163
engagements = engagements .filter (active = True )
163
164
164
- engagements = engagements .select_related ("product" , "product__prod_type" ) \
165
+ engagements = (
166
+ engagements
167
+ .select_related ("product" , "product__prod_type" )
165
168
.prefetch_related ("lead" , "tags" , "product__tags" )
169
+ )
166
170
167
171
if System_Settings .objects .get ().enable_jira :
168
172
engagements = engagements .prefetch_related (
169
173
"jira_project__jira_instance" ,
170
174
"product__jira_project_set__jira_instance" ,
171
175
)
172
176
177
+ test_count_subquery = build_count_subquery (
178
+ Test .objects .filter (engagement = OuterRef ("pk" )), group_field = "engagement_id" ,
179
+ )
180
+ engagements = engagements .annotate (test_count = Coalesce (test_count_subquery , Value (0 )))
181
+
173
182
filter_string_matching = get_system_setting ("filter_string_matching" , False )
174
183
filter_class = EngagementDirectFilterWithoutObjectLookups if filter_string_matching else EngagementDirectFilter
175
184
return filter_class (request .GET , queryset = engagements )
176
185
177
186
178
- def get_test_counts (engagements ):
179
- # Get the test counts per engagement. As a separate query, this is much
180
- # faster than annotating the above `engagements` query.
181
- return {
182
- test ["engagement" ]: test ["test_count" ]
183
- for test in Test .objects .filter (
184
- engagement__in = engagements ,
185
- ).values (
186
- "engagement" ,
187
- ).annotate (
188
- test_count = Count ("engagement" ),
189
- )
190
- }
191
-
192
-
193
187
def engagements (request , view ):
194
-
195
188
if not view :
196
189
view = "active"
197
190
@@ -209,7 +202,6 @@ def engagements(request, view):
209
202
return render (
210
203
request , "dojo/engagement.html" , {
211
204
"engagements" : engs ,
212
- "engagement_test_counts" : get_test_counts (filtered_engagements .qs ),
213
205
"filter_form" : filtered_engagements .form ,
214
206
"product_name_words" : product_name_words ,
215
207
"engagement_name_words" : engagement_name_words ,
@@ -592,23 +584,27 @@ def post(self, request, eid, *args, **kwargs):
592
584
593
585
594
586
def prefetch_for_view_tests (tests ):
595
- prefetched = tests
596
- if isinstance (tests ,
597
- QuerySet ): # old code can arrive here with prods being a list because the query was already executed
598
-
599
- prefetched = prefetched .select_related ("lead" )
600
- prefetched = prefetched .prefetch_related ("tags" , "test_type" , "notes" )
601
- prefetched = prefetched .annotate (count_findings_test_all = Count ("finding__id" , distinct = True ))
602
- prefetched = prefetched .annotate (count_findings_test_active = Count ("finding__id" , filter = Q (finding__active = True ), distinct = True ))
603
- prefetched = prefetched .annotate (count_findings_test_active_verified = Count ("finding__id" , filter = Q (finding__active = True ) & Q (finding__verified = True ), distinct = True ))
604
- prefetched = prefetched .annotate (count_findings_test_mitigated = Count ("finding__id" , filter = Q (finding__is_mitigated = True ), distinct = True ))
605
- prefetched = prefetched .annotate (count_findings_test_dups = Count ("finding__id" , filter = Q (finding__duplicate = True ), distinct = True ))
606
- prefetched = prefetched .annotate (total_reimport_count = Count ("test_import__id" , filter = Q (test_import__type = Test_Import .REIMPORT_TYPE ), distinct = True ))
607
-
608
- else :
587
+ # old code can arrive here with prods being a list because the query was already executed
588
+ if not isinstance (tests , QuerySet ):
609
589
logger .warning ("unable to prefetch because query was already executed" )
610
-
611
- return prefetched
590
+ return tests
591
+
592
+ prefetched = tests .select_related ("lead" , "test_type" ).prefetch_related ("tags" , "notes" )
593
+ base_findings = Finding .objects .filter (test_id = OuterRef ("pk" ))
594
+ count_subquery = partial (build_count_subquery , group_field = "test_id" )
595
+ return prefetched .annotate (
596
+ count_findings_test_all = Coalesce (count_subquery (base_findings ), Value (0 )),
597
+ count_findings_test_active = Coalesce (count_subquery (base_findings .filter (active = True )), Value (0 )),
598
+ count_findings_test_active_verified = Coalesce (
599
+ count_subquery (base_findings .filter (active = True , verified = True )), Value (0 ),
600
+ ),
601
+ count_findings_test_mitigated = Coalesce (count_subquery (base_findings .filter (is_mitigated = True )), Value (0 )),
602
+ count_findings_test_dups = Coalesce (count_subquery (base_findings .filter (duplicate = True )), Value (0 )),
603
+ total_reimport_count = Coalesce (
604
+ count_subquery (Test_Import .objects .filter (test_id = OuterRef ("pk" ), type = Test_Import .REIMPORT_TYPE )),
605
+ Value (0 ),
606
+ ),
607
+ )
612
608
613
609
614
610
@user_is_authorized (Engagement , Permissions .Test_Add , "eid" )
@@ -1583,14 +1579,18 @@ def get_engagements(request):
1583
1579
query = get_list_index (path_items , 1 )
1584
1580
1585
1581
request .GET = QueryDict (query )
1586
- engagements = get_filtered_engagements (request , view ).qs
1587
- test_counts = get_test_counts (engagements )
1588
-
1589
- return engagements , test_counts
1582
+ return get_filtered_engagements (request , view ).qs
1590
1583
1591
1584
1592
1585
def get_excludes ():
1593
- return ["is_ci_cd" , "jira_issue" , "jira_project" , "objects" , "unaccepted_open_findings" ]
1586
+ return [
1587
+ "is_ci_cd" ,
1588
+ "jira_issue" ,
1589
+ "jira_project" ,
1590
+ "objects" ,
1591
+ "unaccepted_open_findings" ,
1592
+ "test_count" , # already exported separately as “tests”
1593
+ ]
1594
1594
1595
1595
1596
1596
def get_foreign_keys ():
@@ -1600,7 +1600,7 @@ def get_foreign_keys():
1600
1600
1601
1601
def csv_export (request ):
1602
1602
logger .debug ("starting csv export" )
1603
- engagements , test_counts = get_engagements (request )
1603
+ engagements = get_engagements (request )
1604
1604
1605
1605
response = HttpResponse (content_type = "text/csv" )
1606
1606
response ["Content-Disposition" ] = "attachment; filename=engagements.csv"
@@ -1627,7 +1627,7 @@ def csv_export(request):
1627
1627
if value and isinstance (value , str ):
1628
1628
value = value .replace ("\n " , " NEWLINE " ).replace ("\r " , "" )
1629
1629
fields .append (value )
1630
- fields .append (test_counts . get (engagement . id , 0 ))
1630
+ fields .append (getattr (engagement , "test_count" , 0 ))
1631
1631
1632
1632
writer .writerow (fields )
1633
1633
logger .debug ("done with csv export" )
@@ -1636,7 +1636,7 @@ def csv_export(request):
1636
1636
1637
1637
def excel_export (request ):
1638
1638
logger .debug ("starting excel export" )
1639
- engagements , test_counts = get_engagements (request )
1639
+ engagements = get_engagements (request )
1640
1640
1641
1641
workbook = Workbook ()
1642
1642
workbook .iso_dates = True
@@ -1668,7 +1668,7 @@ def excel_export(request):
1668
1668
value = value .replace (tzinfo = None )
1669
1669
worksheet .cell (row = row_num , column = col_num , value = value )
1670
1670
col_num += 1
1671
- worksheet .cell (row = row_num , column = col_num , value = test_counts . get (engagement . id , 0 ))
1671
+ worksheet .cell (row = row_num , column = col_num , value = getattr (engagement , "test_count" , 0 ))
1672
1672
row_num += 1
1673
1673
1674
1674
with NamedTemporaryFile () as tmp :
0 commit comments