Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

IVS-205 - Limit number of outcomes per rule #120

Merged
merged 2 commits into from
Nov 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 46 additions & 9 deletions backend/apps/ifc_validation_bff/views_legacy.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import operator
import os, subprocess
import os
import re
import json
from datetime import datetime
import logging
import itertools
import functools
import typing
from collections import defaultdict

from django.db import transaction
from django.http import JsonResponse, HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, HttpResponseRedirect, HttpResponseNotFound
Expand All @@ -22,7 +23,7 @@

from core.settings import MEDIA_ROOT, MAX_FILES_PER_UPLOAD
from core.settings import DEVELOPMENT, LOGIN_URL, USE_WHITELIST
from core.settings import FEATURE_URL
from core.settings import FEATURE_URL, MAX_OUTCOMES_PER_RULE

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -418,14 +419,16 @@ def report(request, id: str):
logger.info('Fetching and mapping syntax done.')

# retrieve and map schema outcome(s) + instances
schema_results_count = defaultdict(int)
schema_results = []
if report_type == 'schema' and request.model:

logger.info('Fetching and mapping schema results...')

task = ValidationTask.objects.filter(request_id=request.id, type=ValidationTask.Type.SCHEMA).last()
if task.outcomes:
for outcome in task.outcomes.iterator():
for outcome in task.outcomes.order_by('-severity').iterator():

mapped = {
"id": outcome.public_id,
"attribute": json.loads(outcome.feature)['attribute'] if outcome.feature else None, # eg. 'IfcSpatialStructureElement.WR41',
Expand All @@ -435,6 +438,15 @@ def report(request, id: str):
"msg": outcome.observed,
"task_id": outcome.validation_task_public_id
}

key = mapped.get('attribute', None) or 'Uncategorized'
_type = mapped.get('constraint_type', None) or 'Uncategorized'
title = _type.replace('_', ' ').capitalize() + ' - ' + key
mapped['title'] = title # eg. 'Schema - SegmentStart'
schema_results_count[title] += 1
if schema_results_count[title] > MAX_OUTCOMES_PER_RULE:
continue

schema_results.append(mapped)

inst = outcome.instance
Expand All @@ -453,14 +465,19 @@ def report(request, id: str):
('prerequisites', (ValidationTask.Type.PREREQUISITES,)),
('industry', (ValidationTask.Type.INDUSTRY_PRACTICES,)))

grouped_gherkin_outcomes_counts = {
'normative': defaultdict(int),
'prerequisites': defaultdict(int),
'industry': defaultdict(int)
}
grouped_gherkin_outcomes = {k: list() for k in map(operator.itemgetter(0), grouping)}

for label, types in grouping:

if not (report_type == label or (label == 'prerequisites' and report_type == 'schema')):
continue

logger.info('Fetching and mapping {label} Gherkin results...')
logger.info(f'Fetching and mapping {label} gherkin results...')

print(*(request.id for t in types))

Expand All @@ -482,6 +499,14 @@ def report(request, id: str):
"task_id": outcome.validation_task_public_id,
"msg": outcome.observed,
}

# TODO: organize this differently?
key = 'Schema - Version' if label == 'prerequisites' else mapped['feature']
mapped['title'] = key
grouped_gherkin_outcomes_counts[label][key] += 1
if grouped_gherkin_outcomes_counts[label][key] > MAX_OUTCOMES_PER_RULE:
continue

grouped_gherkin_outcomes[label].append(mapped)

inst = outcome.instance
Expand All @@ -492,7 +517,7 @@ def report(request, id: str):
}
instances[inst.public_id] = instance

logger.info(f'Mapped {label} Gherkin results.')
logger.info(f'Mapped {label} gherkin results.')

# retrieve and map bsdd results + instances
bsdd_results = []
Expand Down Expand Up @@ -537,11 +562,23 @@ def report(request, id: str):
'model': model,
'results': {
"syntax_results": syntax_results,
"schema_results": schema_results,
"schema": {
"counts": [schema_results_count],
"results": schema_results,
},
"bsdd_results": bsdd_results,
"norm_rules_results": grouped_gherkin_outcomes["normative"],
"ind_rules_results": grouped_gherkin_outcomes["industry"],
"prereq_rules_results": grouped_gherkin_outcomes["prerequisites"],
"norm_rules": {
"counts": grouped_gherkin_outcomes_counts["normative"],
"results": grouped_gherkin_outcomes["normative"],
},
"ind_rules": {
"counts": grouped_gherkin_outcomes_counts["industry"],
"results": grouped_gherkin_outcomes["industry"],
},
"prereq_rules": {
"counts": [grouped_gherkin_outcomes_counts["prerequisites"]],
"results": grouped_gherkin_outcomes["prerequisites"]
}
}
}

Expand Down
3 changes: 3 additions & 0 deletions backend/core/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@
# URL for rule hyperlinks; by default points to bSI Gherkin Rules repo (main)
FEATURE_URL = os.getenv('FEATURE_URL', 'https://github.com/buildingSMART/ifc-gherkin-rules/blob/main/features/')

# Max. number of outcomes shown in UI
MAX_OUTCOMES_PER_RULE = 10

ALLOWED_HOSTS = ["127.0.0.1", "0.0.0.0", "localhost", "backend"]

if os.environ.get("DJANGO_ALLOWED_HOSTS") is not None:
Expand Down
48 changes: 31 additions & 17 deletions frontend/src/GherkinResult.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ function format(obj) {
}
}

export default function GherkinResult({ summary, content, status, instances }) {
export default function GherkinResult({ summary, count, content, status, instances }) {
const [data, setRows] = useState([])
const [grouped, setGrouped] = useState([])
const [page, setPage] = useState(0);
Expand All @@ -126,7 +126,6 @@ export default function GherkinResult({ summary, content, status, instances }) {
});

// only keep visible columns
let columns = ['instance_id', 'severity', 'expected', 'observed', 'msg']
filteredContent = filteredContent.map(function(el) {
const container = {};

Expand All @@ -139,28 +138,29 @@ export default function GherkinResult({ summary, content, status, instances }) {
container.expected = el.expected ? el.expected : '-';
container.severity = el.severity;
container.msg = el.msg;
container.title = el.title;

return container
})

// deduplicate
const uniqueArray = (array, key) => {
// // deduplicate
// const uniqueArray = (array, key) => {

return [
...new Map(
array.map( x => [key(x), x])
).values()
]
}
// return [
// ...new Map(
// array.map( x => [key(x), x])
// ).values()
// ]
// }

filteredContent = uniqueArray(filteredContent, c => c.instance_id + c.feature + c.severity);
// filteredContent = uniqueArray(filteredContent, c => c.instance_id + c.feature + c.severity);

// sort
filteredContent.sort((f1, f2) => f1.feature > f2.feature ? 1 : -1);

for (let c of (filteredContent || [])) {
if (grouped.length === 0 || (c.feature ? c.feature : 'Uncategorized') !== grouped[grouped.length-1][0]) {
grouped.push([c.feature ? c.feature : 'Uncategorized',[]])
if (grouped.length === 0 || (c.title) !== grouped[grouped.length-1][0]) {
grouped.push([c.title,[]])
}
grouped[grouped.length-1][1].push(c);
}
Expand All @@ -177,9 +177,16 @@ export default function GherkinResult({ summary, content, status, instances }) {
setGrouped(grouped)
}, [page, content, checked]);

function partialResultsOnly(rows) {
return count[rows[0].title] > rows.length;
}

function getSuffix(rows, status) {
let times = (rows && rows.length > 1) ? ' times' : ' time';
return (rows && rows.length > 0 && rows[0].severity >= 4) ? '(failed ' + rows.length.toLocaleString() + times + ')' : '';
let occurrences = count[rows[0].title];
let times = (occurrences > 1) ? ' times' : ' time';
//const error_or_warning = status >= 4;
//return (rows && rows.length > 0 && error_or_warning) ? '(occurred ' + rows.length.toLocaleString() + times + ')' : '';
return '(occurred ' + occurrences.toLocaleString() + times + ')';
}

return (
Expand Down Expand Up @@ -242,7 +249,7 @@ export default function GherkinResult({ summary, content, status, instances }) {
>
<TreeItem
nodeId={feature}
label={<div><div class='caption'>{feature} <span class='caption-suffix'>{getSuffix(rows, status)}</span></div></div>}
label={<div><div class='caption'>{feature} <span class='caption-suffix'>{getSuffix(rows, severity)}</span></div></div>}
sx={{ "backgroundColor": severityToColor[severity] }}
>
<div>
Expand All @@ -251,7 +258,14 @@ export default function GherkinResult({ summary, content, status, instances }) {
<br />
<a size='small' target='blank' href={rows[0].feature_url}>{rows[0].feature_url}</a>
<br />
<br />
<br />
{ partialResultsOnly(rows) &&
<div>
ⓘ Note: a high number of occurrences were identified. Only the first {rows.length.toLocaleString()} occurrences are displayed below.
<br />
<br />
</div>
}
</div>
<table width='100%' style={{ 'text-align': 'left'}}>
<thead>
Expand Down
11 changes: 7 additions & 4 deletions frontend/src/Report.js
Original file line number Diff line number Diff line change
Expand Up @@ -157,25 +157,28 @@ function Report({ kind }) {
{(kind === "schema") && <SchemaResult
status={reportData.model.status_schema}
summary={"IFC Schema"}
content={[...reportData.results.schema_results, ...reportData.results.prereq_rules_results]}
count={[...reportData.results.schema.counts, ...reportData.results.prereq_rules.counts]}
content={[...reportData.results.schema.results, ...reportData.results.prereq_rules.results]}
instances={reportData.instances} />}

{(kind === "bsdd") && <BsddTreeView
status={reportData.model.status_bsdd}
summary={"bSDD Compliance"}
content={[...reportData.results.bsdd_results]}
content={reportData.results.bsdd_results}
instances={reportData.instances} />}

{(kind === "normative") && <GherkinResults
status={reportData.model.status_rules}
summary={"Normative IFC Rules"}
content={reportData.results.norm_rules_results}
count={reportData.results.norm_rules.counts}
content={reportData.results.norm_rules.results}
instances={reportData.instances} />}

{(kind === "industry") && <GherkinResults
status={reportData.model.status_ind}
summary={"Industry Practices"}
content={reportData.results.ind_rules_results}
count={reportData.results.ind_rules.counts}
content={reportData.results.ind_rules.results}
instances={reportData.instances} />}
</> }
{!isLoaded && <div>Loading...</div>}
Expand Down
42 changes: 34 additions & 8 deletions frontend/src/SchemaResult.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ function coerceToStr(v) {
return JSON.stringify(v);
}

export default function SchemaResult({ summary, content, status, instances }) {
export default function SchemaResult({ summary, count, content, status, instances }) {
const [data, setRows] = React.useState([])
const [grouped, setGrouped] = useState([])
const [page, setPage] = useState(0);
Expand All @@ -46,19 +46,31 @@ export default function SchemaResult({ summary, content, status, instances }) {
return checked || el.severity > 2; // all or warning/error only?
});

// sort & group
filteredContent.sort((f1, f2) => f1.title > f2.title ? 1 : -1);
for (let c of (filteredContent || [])) {
if (grouped.length === 0 || (c.attribute ? c.attribute : (c.feature ? 'Schema - Version' : 'Uncategorized')) !== grouped[grouped.length-1][0]) {
grouped.push([c.attribute ? c.attribute : (c.feature ? 'Schema - Version' : 'Uncategorized'),[]])
if (grouped.length === 0 || (c.title) !== grouped[grouped.length-1][0]) {
grouped.push([c.title,[]])
}
grouped[grouped.length-1][1].push(c);
}
setRows(grouped.slice(page * 10, page * 10 + 10))
setGrouped(grouped)
}, [page, content, checked]);

function partialResultsOnly(rows) {
let counts = Object.assign({}, count[0], count[1]);
return counts[rows[0].title] > rows.length;
}

function getSuffix(rows, status) {
let times = (rows && rows.length > 1) ? ' times' : ' time';
return (rows && rows.length > 0 && rows[0].severity >= 4) ? '(failed ' + rows.length.toLocaleString() + times + ')' : '';
let counts = Object.assign({}, count[0], count[1]);
// const error_or_warning = status === 'i' || status === 'w';
// //return (rows && rows.length > 0 && error_or_warning) ? '(occurred ' + rows.length.toLocaleString() + times + ')' : '';
//return '- rows: ' + rows.length.toLocaleString() + ' - count: ' + counts[rows[0].title];
let occurrences = counts[rows[0].title];
let times = (occurrences > 1) ? ' times' : ' time';
return '(occurred ' + occurrences.toLocaleString() + times + ')';
}

return (
Expand Down Expand Up @@ -110,17 +122,31 @@ export default function SchemaResult({ summary, content, status, instances }) {
// overflowWrap: 'break-word'
}
}}>
<div >
<div>
{ data.length
? data.map(([hd, rows]) => {
return <TreeView
defaultCollapseIcon={<ExpandMoreIcon />}
defaultExpandIcon={<ChevronRightIcon />}
>
<TreeItem nodeId={hd} label={<div><div class='caption'>{(rows[0].constraint_type || '').replace('_', ' ')}{rows[0].constraint_type && ' - '}{hd} <span class='caption-suffix'>{getSuffix(rows, status)}</span></div><div class='subcaption'>{rows[0].constraint_type !== 'schema' ? (coerceToStr(rows[0].msg)).split('\n')[0] : ''}</div></div>}
<TreeItem
nodeId={hd}
label={
<div>
<div class='caption'>{rows[0].title} <span class='caption-suffix'>{getSuffix(rows, status)}</span>
</div>
<div class='subcaption'>{rows[0].constraint_type !== 'schema' ? (coerceToStr(rows[0].msg)).split('\n')[0] : ''}
</div>
</div>}
sx={{ "backgroundColor": severityToColor[rows[0].severity] }}
>

{ partialResultsOnly(rows) &&
<div>
ⓘ Note: a high number of occurrences were identified. Only the first {rows.length.toLocaleString()} occurrences are displayed below.
<br />
<br />
</div>
}
<table width='100%' style={{ 'text-align': 'left'}}>
<thead>
<tr><th>Id</th><th>Entity</th><th>Severity</th><th>Message</th></tr>
Expand Down