Skip to content

Commit c77f686

Browse files
committed
feat: enhance license detection details view with match text diagnostics
Signed-off-by: berzz26 <aumtamboli2233@gmail.com>
1 parent a76e0cc commit c77f686

6 files changed

Lines changed: 241 additions & 1 deletion

File tree

scancodeio/static/main.css

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,21 @@ pre.wrap {
196196
pre.is-small {
197197
padding: 0.75rem 1rem;
198198
}
199+
.license-match-text {
200+
max-height: 24rem;
201+
overflow: auto;
202+
}
203+
.license-match-comparison pre {
204+
margin-bottom: 0.75rem;
205+
}
206+
.license-match-block {
207+
border-top: 1px solid var(--bulma-border-weak);
208+
padding-top: 0.75rem;
209+
}
210+
.license-match-block:first-child {
211+
border-top: 0;
212+
padding-top: 0;
213+
}
199214
.nexb-orange {
200215
color: rgb(var(--nexb-orange));
201216
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<div class="license-match-comparison">
2+
<div class="columns is-variable is-2 is-multiline mb-0">
3+
<div class="column is-half">
4+
<p class="has-text-weight-semibold mb-1">Matched text</p>
5+
<pre class="wrap is-small license-match-text">{{ match.matched_text|default:"" }}</pre>
6+
</div>
7+
<div class="column is-half">
8+
<p class="has-text-weight-semibold mb-1">Rule text</p>
9+
<pre class="wrap is-small license-match-text">{{ match.rule_text|default:"" }}</pre>
10+
</div>
11+
</div>
12+
{% if match.matched_text_diagnostics %}
13+
<p class="has-text-weight-semibold mb-1">Diagnostic matched text</p>
14+
<pre class="wrap is-small license-match-text">{{ match.matched_text_diagnostics }}</pre>
15+
{% endif %}
16+
</div>

scanpipe/templates/scanpipe/tabset/tab_license_detections.html

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,58 @@
11
<div class="content">
2+
{% if file_match_groups %}
3+
<h2 class="is-size-5 mb-3">Matches by file</h2>
4+
{% for file_group in file_match_groups %}
5+
<article class="box p-3 mb-4">
6+
<div class="is-flex is-justify-content-space-between is-align-items-flex-start is-flex-wrap-wrap mb-3">
7+
<div class="break-all mr-3">
8+
{% if file_group.file_region.path %}
9+
<a href="{% url 'resource_detail' project.slug file_group.file_region.path %}#detection">{{ file_group.file_region.path }}</a>
10+
{% else %}
11+
<span class="has-text-grey">No origin resource path</span>
12+
{% endif %}
13+
</div>
14+
{% if file_group.file_region.start_line or file_group.file_region.end_line %}
15+
<div class="tags">
16+
<span class="tag">Lines {{ file_group.file_region.start_line }}-{{ file_group.file_region.end_line }}</span>
17+
</div>
18+
{% endif %}
19+
</div>
20+
{% if file_group.matches %}
21+
{% for match in file_group.matches %}
22+
<div class="license-match-block{% if not forloop.last %} mb-4{% endif %}">
23+
<div class="tags mb-2">
24+
<span class="tag is-dark">{{ match.license_expression }}</span>
25+
{% if match.matcher %}<span class="tag">Matcher {{ match.matcher }}</span>{% endif %}
26+
{% if match.score %}<span class="tag">Score {{ match.score }}</span>{% endif %}
27+
{% if match.match_coverage %}<span class="tag">Coverage {{ match.match_coverage }}</span>{% endif %}
28+
{% if match.rule_identifier %}
29+
<span class="tag">
30+
{% if match.rule_url %}
31+
<a href="{{ match.rule_url }}">{{ match.rule_identifier }} <i class="fa-solid fa-up-right-from-square is-small"></i></a>
32+
{% else %}
33+
{{ match.rule_identifier }}
34+
{% endif %}
35+
</span>
36+
{% endif %}
37+
</div>
38+
{% include "scanpipe/includes/license_match_comparison.html" with match=match only %}
39+
</div>
40+
{% endfor %}
41+
{% else %}
42+
<p class="has-text-grey">No match text is available for this file region.</p>
43+
{% endif %}
44+
</article>
45+
{% endfor %}
46+
{% endif %}
47+
248
<table class="table is-bordered is-striped is-narrow is-hoverable is-fullwidth is-rounded-table">
349
<thead>
450
<tr>
551
<th>License expression</th>
652
<th>Origin resource path</th>
753
<th>Matched text</th>
54+
<th>Diagnostic matched text</th>
55+
<th>Rule text</th>
856
<th>Rule URL</th>
957
<th>Score</th>
1058
<th>Matcher</th>
@@ -25,6 +73,12 @@
2573
<td class="break-all">
2674
{{ match.matched_text }}
2775
</td>
76+
<td class="break-all">
77+
{{ match.matched_text_diagnostics }}
78+
</td>
79+
<td class="break-all">
80+
{{ match.rule_text }}
81+
</td>
2882
<td class="break-all">
2983
{% if match.rule_url %}
3084
<a href="{{match.rule_url}}">

scanpipe/templates/scanpipe/tabset/tab_resource_detections.html

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<div class="content">
22
{% if tab_data.fields.license_detections.value %}
3-
<table class="table is-bordered is-striped is-narrow is-hoverable is-fullwidth is-rounded-table">
3+
<table class="table is-bordered is-striped is-narrow is-hoverable is-fullwidth is-rounded-table mb-4">
44
<thead>
55
<tr>
66
<th>License detections</th>
@@ -24,6 +24,45 @@
2424
{% endfor %}
2525
</tbody>
2626
</table>
27+
28+
{% for detection in tab_data.fields.license_detections.value %}
29+
<article class="box p-3 mb-4">
30+
<div class="is-flex is-justify-content-space-between is-align-items-flex-start is-flex-wrap-wrap mb-3">
31+
<div class="break-all mr-3">
32+
<a href="{% url 'license_detail' project.slug detection.identifier %}">{{ detection.identifier }}</a>
33+
</div>
34+
<div class="tags">
35+
<span class="tag is-dark">{{ detection.license_expression }}</span>
36+
{% if detection.license_expression_spdx %}
37+
<span class="tag">{{ detection.license_expression_spdx }}</span>
38+
{% endif %}
39+
</div>
40+
</div>
41+
{% for match in detection.matches %}
42+
<div class="license-match-block{% if not forloop.last %} mb-4{% endif %}">
43+
<div class="tags mb-2">
44+
<span class="tag is-dark">{{ match.license_expression }}</span>
45+
{% if match.start_line or match.end_line %}<span class="tag">Lines {{ match.start_line }}-{{ match.end_line }}</span>{% endif %}
46+
{% if match.matcher %}<span class="tag">Matcher {{ match.matcher }}</span>{% endif %}
47+
{% if match.score %}<span class="tag">Score {{ match.score }}</span>{% endif %}
48+
{% if match.match_coverage %}<span class="tag">Coverage {{ match.match_coverage }}</span>{% endif %}
49+
{% if match.rule_identifier %}
50+
<span class="tag">
51+
{% if match.rule_url %}
52+
<a href="{{ match.rule_url }}">{{ match.rule_identifier }} <i class="fa-solid fa-up-right-from-square is-small"></i></a>
53+
{% else %}
54+
{{ match.rule_identifier }}
55+
{% endif %}
56+
</span>
57+
{% endif %}
58+
</div>
59+
{% include "scanpipe/includes/license_match_comparison.html" with match=match only %}
60+
</div>
61+
{% empty %}
62+
<p class="has-text-grey">No match text is available for this detection.</p>
63+
{% endfor %}
64+
</article>
65+
{% endfor %}
2766
{% endif %}
2867
{% if tab_data.fields.license_clues.value %}
2968
<table class="table is-bordered is-striped is-narrow is-hoverable is-fullwidth is-rounded-table">

scanpipe/tests/test_views.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
from scanpipe.models import CodebaseRelation
4343
from scanpipe.models import CodebaseResource
4444
from scanpipe.models import DiscoveredDependency
45+
from scanpipe.models import DiscoveredLicense
4546
from scanpipe.models import DiscoveredPackage
4647
from scanpipe.models import Project
4748
from scanpipe.pipes import make_relation
@@ -1397,6 +1398,84 @@ def test_scanpipe_views_license_details_view(self):
13971398
response = self.client.get(xss_url)
13981399
self.assertEqual(response.status_code, 404)
13991400

1401+
def test_scanpipe_views_license_detection_details_view_match_texts(self):
1402+
matches = [
1403+
{
1404+
"score": 100.0,
1405+
"start_line": 1,
1406+
"end_line": 1,
1407+
"matched_length": 4,
1408+
"match_coverage": 100.0,
1409+
"matcher": "1-hash",
1410+
"license_expression": "mit",
1411+
"rule_identifier": "mit_1.RULE",
1412+
"rule_text": "license: MIT",
1413+
"matched_text": "License: MIT",
1414+
"matched_text_diagnostics": "License MIT",
1415+
"from_file": "LICENSE",
1416+
}
1417+
]
1418+
license_detection = DiscoveredLicense.objects.create(
1419+
project=self.project1,
1420+
license_expression="mit",
1421+
license_expression_spdx="MIT",
1422+
identifier="mit-123",
1423+
matches=matches,
1424+
file_regions=[{"path": "LICENSE", "start_line": 1, "end_line": 1}],
1425+
)
1426+
1427+
url = reverse(
1428+
"license_detail",
1429+
args=[self.project1.slug, license_detection.identifier],
1430+
)
1431+
response = self.client.get(url)
1432+
1433+
self.assertContains(response, "Diagnostic matched text")
1434+
self.assertContains(response, "License MIT")
1435+
self.assertContains(response, "Matched text")
1436+
self.assertContains(response, "License: MIT")
1437+
self.assertContains(response, "Rule text")
1438+
self.assertContains(response, "license: MIT")
1439+
1440+
def test_scanpipe_views_resource_details_view_inlines_detection_match_texts(self):
1441+
license_detections = [
1442+
{
1443+
"identifier": "mit-123",
1444+
"license_expression": "mit",
1445+
"license_expression_spdx": "MIT",
1446+
"matches": [
1447+
{
1448+
"score": 100.0,
1449+
"start_line": 1,
1450+
"end_line": 1,
1451+
"matched_length": 4,
1452+
"match_coverage": 100.0,
1453+
"matcher": "1-hash",
1454+
"license_expression": "mit",
1455+
"rule_identifier": "mit_1.RULE",
1456+
"rule_text": "license: MIT",
1457+
"matched_text": "License: MIT",
1458+
"matched_text_diagnostics": "License MIT",
1459+
}
1460+
],
1461+
}
1462+
]
1463+
resource = make_resource_file(
1464+
self.project1,
1465+
"LICENSE",
1466+
detected_license_expression="mit",
1467+
license_detections=license_detections,
1468+
)
1469+
1470+
response = self.client.get(resource.get_absolute_url())
1471+
1472+
self.assertContains(response, "Diagnostic matched text")
1473+
self.assertContains(response, "License MIT")
1474+
self.assertContains(response, "Matched text")
1475+
self.assertContains(response, "License: MIT")
1476+
self.assertContains(response, "Rule text")
1477+
self.assertContains(response, "license: MIT")
1478+
14001479
@mock.patch("scanpipe.models.DiscoveredPackage.get_absolute_url")
14011480
def test_scanpipe_views_project_dependency_tree(self, mock_get_url):
14021481
mock_get_url.return_value = "mocked-url"

scanpipe/views.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2535,6 +2535,43 @@ class DiscoveredLicenseDetailsView(
25352535
},
25362536
}
25372537

2538+
@staticmethod
2539+
def get_matches_by_file(matches):
2540+
matches_by_file = {}
2541+
for match in matches:
2542+
from_file = match.get("from_file") or ""
2543+
matches_by_file.setdefault(from_file, []).append(match)
2544+
2545+
return matches_by_file
2546+
2547+
def get_file_match_groups(self):
2548+
matches_by_file = self.get_matches_by_file(self.object.matches)
2549+
file_match_groups = []
2550+
2551+
for file_region in self.object.file_regions:
2552+
path = file_region.get("path") or ""
2553+
file_match_groups.append(
2554+
{
2555+
"file_region": file_region,
2556+
"matches": matches_by_file.pop(path, []),
2557+
}
2558+
)
2559+
2560+
for path, matches in matches_by_file.items():
2561+
file_match_groups.append(
2562+
{
2563+
"file_region": {"path": path},
2564+
"matches": matches,
2565+
}
2566+
)
2567+
2568+
return file_match_groups
2569+
2570+
def get_context_data(self, **kwargs):
2571+
context = super().get_context_data(**kwargs)
2572+
context["file_match_groups"] = self.get_file_match_groups()
2573+
return context
2574+
25382575

25392576
@conditional_login_required
25402577
def run_detail_view(request, uuid):

0 commit comments

Comments
 (0)