Skip to content

Commit cdc94bd

Browse files
committed
Add charts for Overview and Data Quality Panel
Signed-off-by: Sampurna Pyne <sampurnapyne1710@gmail.com>
1 parent 451e985 commit cdc94bd

3 files changed

Lines changed: 429 additions & 0 deletions

File tree

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
#
2+
# Copyright (c) nexB Inc. and others. All rights reserved.
3+
# VulnerableCode is a trademark of nexB Inc.
4+
# SPDX-License-Identifier: Apache-2.0
5+
# See http://www.apache.org/licenses/LICENSE-2.0 for the license text.
6+
# See https://github.com/aboutcode-org/vulnerablecode for support or download.
7+
# See https://aboutcode.org for more information about nexB OSS projects.
8+
#
9+
10+
from collections import defaultdict
11+
from datetime import timedelta
12+
from itertools import accumulate
13+
from typing import Any
14+
from typing import Dict
15+
16+
from django.db.models import Count
17+
from django.db.models.functions import TruncMonth
18+
from django.utils import timezone
19+
20+
from insights.models import DataQualityIssueByDatasourceInsight
21+
from insights.models import DataQualityToDosResolutionInsight
22+
from insights.utils import format_issue_type_label
23+
from vulnerabilities.models import AdvisoryToDoV2
24+
25+
# Ignore Phantom Importers that don't collect Affected Packages
26+
IGNORED_IMPORTERS = {
27+
"epss_importer_v2",
28+
"epss",
29+
"vulnrichment_importer_v2",
30+
"vulnrichment",
31+
"suse_importer_v2",
32+
"suse_score",
33+
}
34+
35+
36+
# Open Issues by Type and Datasource Contribution per Open Issue
37+
def open_issues_to_datasource_queryset():
38+
"""Return a query set of open issue counts by type and datasource."""
39+
return (
40+
AdvisoryToDoV2.objects.filter(is_resolved=False, advisories__datasource_id__isnull=False)
41+
.exclude(advisories__datasource_id__in=IGNORED_IMPORTERS)
42+
.values("issue_type", "advisories__datasource_id")
43+
.annotate(count=Count("todo_id", distinct=True))
44+
.iterator()
45+
)
46+
47+
48+
def iter_open_issues_to_datasource_insights():
49+
"""Yield DataQualityIssueByDatasourceInsight objects."""
50+
for row in open_issues_to_datasource_queryset():
51+
yield DataQualityIssueByDatasourceInsight(
52+
issue_type=row["issue_type"],
53+
datasource_id=row["advisories__datasource_id"],
54+
count=row["count"],
55+
)
56+
57+
58+
def collect_open_issues_to_datasource(pipeline: Any) -> None:
59+
"""Collect open issue counts by type and datasource."""
60+
pipeline.data_quality_issue_types = list(iter_open_issues_to_datasource_insights())
61+
62+
63+
def build_issue_type_columns(issue_counts: dict) -> Dict[str, Any]:
64+
"""Helper to build column data for the issue type bar chart as expected by Billboard"""
65+
issue_types = list(issue_counts.keys())
66+
open_issue_counts = list(issue_counts.values())
67+
68+
return {
69+
"columns": [
70+
["x"] + issue_types,
71+
["To-Dos"] + open_issue_counts,
72+
],
73+
"x_label": "Type of Issue",
74+
"y_label": "To-Dos Open Issue Count",
75+
"color": "var(--bulma-danger)",
76+
}
77+
78+
79+
def format_issue_type_bar(snapshot: Any) -> Dict[str, Any]:
80+
"""Format open issues by type per datasource for the colored bar chart."""
81+
open_issue_counts_by_datasource = defaultdict(dict)
82+
total_issues_by_type = defaultdict(int)
83+
84+
for insight in snapshot.data_quality_issue_types.all():
85+
label = format_issue_type_label(insight.issue_type)
86+
open_issue_counts_by_datasource[insight.datasource_id][label] = insight.count
87+
total_issues_by_type[label] += insight.count
88+
89+
data = {}
90+
for datasource_id, counts in open_issue_counts_by_datasource.items():
91+
data[datasource_id] = build_issue_type_columns(counts)
92+
93+
if total_issues_by_type:
94+
data["global"] = build_issue_type_columns(total_issues_by_type)
95+
96+
return data
97+
98+
99+
def format_issue_contribution_donut(snapshot: Any) -> Dict[str, Any]:
100+
"""Format datasource contribution per open issue for the donut chart as expected by Billboard"""
101+
datasource_counts_by_issue = defaultdict(dict)
102+
total_issues_by_datasource = defaultdict(int)
103+
104+
for insight in snapshot.data_quality_issue_types.all():
105+
label = format_issue_type_label(insight.issue_type)
106+
datasource_counts_by_issue[label][insight.datasource_id] = insight.count
107+
total_issues_by_datasource[insight.datasource_id] += insight.count
108+
109+
data = {}
110+
for issue_type, counts in datasource_counts_by_issue.items():
111+
columns = [[datasource_id, count] for datasource_id, count in counts.items()]
112+
data[issue_type] = {"columns": columns}
113+
114+
if total_issues_by_datasource:
115+
global_columns = [
116+
[datasource_id, count] for datasource_id, count in total_issues_by_datasource.items()
117+
]
118+
data["global"] = {"columns": global_columns}
119+
120+
return data
121+
122+
123+
# To-Dos Issue Resolution Timeline
124+
def data_quality_todos_resolutions_queryset():
125+
"""Return resolution rates by month for open and resolved to-dos."""
126+
start_date = timezone.now() - timedelta(days=365) # Collect last 12 months only
127+
128+
open_todos = (
129+
AdvisoryToDoV2.objects.filter(
130+
created_at__isnull=False,
131+
created_at__gte=start_date,
132+
advisories__datasource_id__isnull=False,
133+
)
134+
.exclude(advisories__datasource_id__in=IGNORED_IMPORTERS)
135+
.annotate(month=TruncMonth("created_at"))
136+
.values("month", "advisories__datasource_id")
137+
.annotate(count=Count("todo_id", distinct=True))
138+
.iterator()
139+
)
140+
141+
resolved_todos = (
142+
AdvisoryToDoV2.objects.filter(
143+
is_resolved=True,
144+
resolved_at__isnull=False,
145+
resolved_at__gte=start_date,
146+
advisories__datasource_id__isnull=False,
147+
)
148+
.exclude(advisories__datasource_id__in=IGNORED_IMPORTERS)
149+
.annotate(month=TruncMonth("resolved_at"))
150+
.values("month", "advisories__datasource_id")
151+
.annotate(count=Count("todo_id", distinct=True))
152+
.iterator()
153+
)
154+
return open_todos, resolved_todos
155+
156+
157+
def iter_data_quality_todos_resolutions_insights():
158+
"""Yield DataQualityToDosResolutionInsight objects."""
159+
open_todos, resolved_todos = data_quality_todos_resolutions_queryset()
160+
161+
open_counts = defaultdict(int)
162+
for open_record in open_todos:
163+
datasource = open_record["advisories__datasource_id"]
164+
month = open_record["month"].date()
165+
open_counts[datasource, month] += open_record["count"]
166+
167+
resolved_counts = defaultdict(int)
168+
for resolved_record in resolved_todos:
169+
datasource = resolved_record["advisories__datasource_id"]
170+
month = resolved_record["month"].date()
171+
resolved_counts[datasource, month] += resolved_record["count"]
172+
173+
all_keys = set(open_counts.keys()) | set(
174+
resolved_counts.keys()
175+
) # All unique (datasource, month) pairs across opened and resolved to-dos
176+
177+
for datasource_id, month_date in sorted(all_keys):
178+
yield DataQualityToDosResolutionInsight(
179+
datasource_id=datasource_id,
180+
month=month_date,
181+
open_count=open_counts[(datasource_id, month_date)],
182+
resolved_count=resolved_counts[(datasource_id, month_date)],
183+
)
184+
185+
186+
def collect_data_quality_todos_resolutions(pipeline: Any) -> None:
187+
"""Collect historical resolution rates by month."""
188+
pipeline.data_quality_todos_resolutions = list(iter_data_quality_todos_resolutions_insights())
189+
190+
191+
def build_todos_resolution_columns(month_counts: Dict[str, Dict[str, int]]) -> Dict[str, Any]:
192+
"""Helper to build issue resolution line chart as expected by Billboard"""
193+
sorted_months = sorted(month_counts.keys())
194+
new_open_counts = [month_counts[month]["open"] for month in sorted_months]
195+
new_resolved_counts = [month_counts[month]["resolved"] for month in sorted_months]
196+
197+
return {
198+
"columns": [
199+
["x"] + sorted_months,
200+
["Open"] + list(accumulate(new_open_counts)),
201+
["Resolved"] + list(accumulate(new_resolved_counts)),
202+
],
203+
"new_open_counts": new_open_counts,
204+
"new_resolved_counts": new_resolved_counts,
205+
"y_label": "Cumulative Count",
206+
}
207+
208+
209+
def format_todos_resolution_timeline(snapshot: Any) -> Dict[str, Any]:
210+
"""Format cumulative to-dos resolution line chart"""
211+
resolution_insights = snapshot.data_quality_todos_resolutions.all().order_by("month")
212+
213+
datasource_month_counts = defaultdict(dict)
214+
global_month_counts = {}
215+
216+
for insight in resolution_insights:
217+
formatted_month = insight.month.strftime("%Y-%m-%d")
218+
datasource_id = insight.datasource_id
219+
220+
# Initialize global month count if not exists
221+
if formatted_month not in global_month_counts:
222+
global_month_counts[formatted_month] = {"open": 0, "resolved": 0}
223+
224+
global_month_counts[formatted_month]["open"] += insight.open_count
225+
global_month_counts[formatted_month]["resolved"] += insight.resolved_count
226+
227+
datasource_month_counts[datasource_id][formatted_month] = {
228+
"open": insight.open_count,
229+
"resolved": insight.resolved_count,
230+
}
231+
232+
data = {}
233+
if global_month_counts:
234+
data["global"] = build_todos_resolution_columns(global_month_counts)
235+
for datasource_id, month_counts in datasource_month_counts.items():
236+
data[datasource_id] = build_todos_resolution_columns(month_counts)
237+
238+
return data

0 commit comments

Comments
 (0)