Skip to content

Commit

Permalink
Merge pull request #120 from buildingSMART/IVS_205_Limit_Number_of_Ou…
Browse files Browse the repository at this point in the history
…tcomes

IVS-205 - Limit number of outcomes per rule
  • Loading branch information
civilx64 authored Nov 12, 2024
2 parents 348192c + 9dcb5f5 commit 215beb8
Show file tree
Hide file tree
Showing 5 changed files with 121 additions and 38 deletions.
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

0 comments on commit 215beb8

Please sign in to comment.