From 562ed39600404367345e2fc8c65dd9d66f563c33 Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Mon, 23 Jan 2023 20:31:20 +1100 Subject: [PATCH] Show restricted benefit criteria in popover --- html/index.html | 27 ++++++++++++-- import_pbs_xml.py | 2 +- render_pbs_criteria.py | 81 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 107 insertions(+), 3 deletions(-) create mode 100644 render_pbs_criteria.py diff --git a/html/index.html b/html/index.html index 1315c24..f6c737d 100644 --- a/html/index.html +++ b/html/index.html @@ -24,6 +24,7 @@ @@ -111,14 +112,28 @@ let ulRestrictions = document.createElement('ul'); for (let restriction of restrictions) { let li = document.createElement('li'); + ulRestrictions.appendChild(li); + li.innerHTML = restriction['indication']; if (item['benefit_type'] === 'streamlined') { li.innerHTML += ' (' + restriction['treatment_of'] + ')'; } if (restriction['num_criteria'] > 0) { - li.innerHTML += ' (' + restriction['num_criteria'] + ' criteria)'; + li.innerHTML += ' '; + + let span = document.createElement('span'); + span.classList.add('text-muted'); + span.innerText = '(' + restriction['num_criteria'] + ' criteria)'; + li.appendChild(span); + + // Create popover for criteria + new bootstrap.Popover(span, { + trigger: 'hover focus', + title: restriction['indication'], + content: restriction['criteria_rendered'], + html: true + }); } - ulRestrictions.appendChild(li); } td.appendChild(ulRestrictions); } @@ -212,6 +227,14 @@ return results; } + function groupBy(array, grouper) { + // Array.prototype.group not yet supported in most (any) browsers + + return array.reduce(function(result, item) { + (result[grouper(item)] = result[grouper(item)] || []).push(item); + return result; + }, {}); + }; main(); diff --git a/import_pbs_xml.py b/import_pbs_xml.py index 12437da..94d75c6 100644 --- a/import_pbs_xml.py +++ b/import_pbs_xml.py @@ -29,7 +29,7 @@ cur.execute('DROP TABLE IF EXISTS pbs_item_restriction') cur.execute('CREATE TABLE pbs_item_restriction (item_code TEXT, restriction_code INTEGER)') cur.execute('DROP TABLE IF EXISTS pbs_restriction') -cur.execute('CREATE TABLE pbs_restriction (code INTEGER PRIMARY KEY, treatment_of INTEGER, indication TEXT, criteria_operator TEXT)') +cur.execute('CREATE TABLE pbs_restriction (code INTEGER PRIMARY KEY, treatment_of INTEGER, indication TEXT, criteria_operator TEXT, criteria_rendered TEXT)') cur.execute('DROP TABLE IF EXISTS pbs_restriction_criteria') cur.execute('CREATE TABLE pbs_restriction_criteria (restriction_code INTEGER, criteria_code INTEGER)') diff --git a/render_pbs_criteria.py b/render_pbs_criteria.py new file mode 100644 index 0000000..901f89a --- /dev/null +++ b/render_pbs_criteria.py @@ -0,0 +1,81 @@ +# Copyright © 2023 Lee Yingtong Li (RunasSudo) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import itertools +import sqlite3 + +# Open database +con = sqlite3.connect('database.db') +con.row_factory = sqlite3.Row +cur = con.cursor() + +# Populate pbs_restriction.criteria_rendered +# sql.js is too slow to do the database JOINs at runtime + +class Operator: + def __init__(self, operator, nodes): + self.operator = operator + self.nodes = nodes + + def flatten(self): + # If contains just 1 element, return it as itself, not as an Operator + if len(self.nodes) == 1: + return self.nodes[0] + + # Otherwise flatten recursively + flattened = [n.flatten() if isinstance(n, Operator) else n for n in self.nodes] + return Operator(self.operator, flattened) + + @staticmethod + def render(op): + if isinstance(op, str): + return '

' + op + '

' + elif isinstance(op, Operator): + if len(op.nodes) == 1: + # Single simple string + return '

' + op.nodes[0] + '

' + if not any(isinstance(n, Operator) for n in op.nodes): + # All simple strings + return '' + else: + # Mix of Operator +/- simple strings + return '

{}

'.format({'all': 'AND', 'any': 'OR', 'one-of': 'OR'}[op.operator]).join(Operator.render(n) for n in op.nodes) + else: + # Must be a list of a single item + return Operator.render(op[0]) + +cur.execute('SELECT * FROM pbs_restriction INNER JOIN pbs_restriction_criteria ON pbs_restriction.code = pbs_restriction_criteria.restriction_code INNER JOIN pbs_criteria ON pbs_restriction_criteria.criteria_code = pbs_criteria.code INNER JOIN pbs_criteria_parameter ON pbs_criteria.code = pbs_criteria_parameter.criteria_code') +flat_parameters = cur.fetchall() + +parameters_by_restriction = itertools.groupby(flat_parameters, lambda p: p['restriction_code']) +for restriction_code, restriction_parameters in parameters_by_restriction: + restriction_parameters = list(restriction_parameters) + + # Build tree of nodes representing parameters + restriction_operator = Operator(restriction_parameters[0]['criteria_operator'], []) + + parameters_by_criteria = itertools.groupby(restriction_parameters, lambda p: p['criteria_code']) + for criteria_code, criteria_parameters in parameters_by_criteria: + criteria_parameters = list(criteria_parameters) + criteria_operator = Operator(criteria_parameters[0]['parameters_operator'], [p['text'] for p in criteria_parameters]) + restriction_operator.nodes.append(criteria_operator) + + # Flatten and render the tree + restriction_operator = restriction_operator.flatten() + rendered = Operator.render(restriction_operator) + + cur.execute('UPDATE pbs_restriction SET criteria_rendered = ? WHERE code = ?', (rendered, restriction_code)) + +con.commit()