Skip to content

Commit

Permalink
[wip] Added multi-line input validation
Browse files Browse the repository at this point in the history
  • Loading branch information
veronikaslc committed Oct 13, 2020
1 parent c0ec78f commit 8462766
Showing 1 changed file with 209 additions and 119 deletions.
328 changes: 209 additions & 119 deletions guids-generator.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,174 +9,264 @@
content="GUID generator from patient identifiable information"
/>
<title>GUID generator</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" />
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500">
<script src="https://unpkg.com/react@16/umd/react.production.min.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js" crossorigin></script>
<script src="https://unpkg.com/@material-ui/core/umd/material-ui.production.min.js" crossorigin="anonymous"></script>
<script src="https://unpkg.com/react-copy-to-clipboard/build/react-copy-to-clipboard.js"></script>
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" />
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500">

<script type="text/javascript" src="https://unpkg.com/bcryptjs@2.4.3/dist/bcrypt.js"></script>

<!-- Don't use this in production: -->
<script src="https://unpkg.com/@material-ui/core/umd/material-ui.production.min.js" crossorigin="anonymous"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script src="http://requirejs.org/docs/release/2.1.5/comments/require.js"></script>
<script src="http://requirejs.org/docs/release/2.1.5/comments/require.js"></script>

This comment has been minimized.

Copy link
@marta-

marta- Oct 14, 2020

Contributor

All scripts should be requested over https, so that we can serve the guid generator web page over https in turn.

This comment has been minimized.

Copy link
@marta-

marta- Oct 14, 2020

Contributor

Fixed in aa00629

</head>
<body>
<div id="root"></div>
<script type="text/babel">

require.config({
paths: {
"bcrypt": "https://unpkg.com/bcryptjs@2.4.3/dist/bcrypt"
}
paths: {
"bcrypt": "https://unpkg.com/bcryptjs@2.4.3/dist/bcrypt"
}
});

require(["bcrypt"], function(bcrypt) {

const { useState } = window['React'];
const { TextField, InputAdornment, Icon, Button, IconButton, Typography, makeStyles } = window['MaterialUI'];
const { CopyToClipboard } = window['CopyToClipboard'];

const useStyles = makeStyles((theme) => ({
root: {
'& .MuiTextField-root': {
margin: theme.spacing(2),
width: '25ch',
},
},
binput: {
width: theme.spacing(62),
height: theme.spacing(10),
paddingTop: theme.spacing(4),
marginTop: theme.spacing(2),
marginBottom: theme.spacing(2),
},
bbutton: {
margin: theme.spacing(2),
},
labelRoot: {
width: theme.spacing(69),
"&$labelFocused": {
color: "purple"
}
},
labelFocused: {},
main: {margin : theme.spacing(6),},
const { TextField, InputAdornment, Icon, Button, IconButton, Typography, makeStyles, Grid } = window['MaterialUI'];

const useStyles = makeStyles((theme) => ({
btextinput: {
width: theme.spacing(50),
paddingTop: theme.spacing(2),
marginTop: theme.spacing(3),
marginBottom: theme.spacing(2),
},
projectidinput: {
width: theme.spacing(50),
},
boutput: {
width: theme.spacing(70),
marginBottom: theme.spacing(2),
},
bbutton: {
margin: theme.spacing(2),
},
labelRoot: {
width: theme.spacing(70),
"&$labelFocused": {
color: "purple"
}
},
labelFocused: {},
main: {
margin : theme.spacing(6),
},
helperText: {
lineHeight: "normal",
},
}));

const provinces = ["AB", "BC", "MB", "NB", "NL", "NS", "NT", "NU", "ON", "PE", "QC", "SK", "YT"];

function GUIDGeneratorComponent() {
const classes = useStyles();
const [ binput, setBinput ] = useState();
const [ projectId, setProjectId ] = useState(new URLSearchParams(window.location.search).get('project_id'));
const [ bhash, setBhash ] = useState();
const [ projectId, setProjectId ] = useState(new URLSearchParams(window.location.search).get('project_id') || '');
const [ bhashArray, setBhashArray ] = useState([]);
const [ isValid, setIsValid ] = useState(false);
const [ errorText, setErrorText ] = useState();
const [ copied, setCopied ] = useState(false);
const [ postProcessedInfo, setPostProcessedInfo ] = useState([]);

// MOD 10 Check Digit algorithm
let isValidHealthCard = (num) => {

let calc, i, check, checksum = 0, r = [2,1];

// iterate on all the numbers in 'num'
for ( let i=num.length-1; i--; ){
calc = num.charAt(i) * r[i % r.length];
// handle cases where it's a 2 digits number
calc = ((calc/10)|0) + (calc % 10);
checksum += calc;
}
check = (10-(checksum % 10)) % 10; // make sure to get '0' if checksum is '10'
let checkDigit = num % 10;

let onSubmit = () => {
return check == checkDigit;
}

// Input validation and sanitising
let handleChange = (value) => {
setIsValid(false);
setErrorText(null);

bcrypt.hash(binput, 10, (err, hash) => {
if (err) {
console.error(err);
setErrorText(err);
return;
setBhashArray([]);
setBinput(value);

if (!value) return;

let postProcessed = [];
let err = '';

let lines = value.split('\n');
for (var line of lines) {
// skip & ignore empty lines
if (!line) continue;

// There should be 3 pieces per each line: Health card #, Province code, Date of birth, all separated by ','
let info = line.trim()
.split(',')
.map( (item) => (item.trim()) )
.filter( (item) => (item) );

if (info.length < 3) {

This comment has been minimized.

Copy link
@marta-

marta- Oct 14, 2020

Contributor

We should warn even if length >3. Anything != 3 is potentially invalid input.

This comment has been minimized.

Copy link
@veronikaslc

veronikaslc Oct 14, 2020

Contributor

fixed

err = "Line '" + line + "' has " + info.length

This comment has been minimized.

Copy link
@marta-

marta- Oct 14, 2020

Contributor

The specs state the GUIDs should be generated for all valid lines, and error messages should be displayed for invalid lines, keeping the order from the input.

If we're going to change the specs according to the current implementation and block the generation of all GUIDs if any line is invalid, it is important to communicate clearly to the user which like is wrong. Right now just referencing the content of the line is insufficient, the number of the line should also be outputed. E.g. Line 5, "line content", has N ...

This comment has been minimized.

Copy link
@veronikaslc

veronikaslc Oct 14, 2020

Contributor

Sorry, I understood the following specs as related to the and did the current implementation according to the input DOM element validation because I notices tat for lines specs use "line" word. May be I missed the specs place where it was stated that GUIDs generated for all valid lines and display error messages for all invalid lines in order, could not find it.

If the input is found valid, concatenate the post-processed health card number and date of birth and generate the GUID. If the input is found invalid at any of the verification steps, do not generate a GUID and display the corresponding error message in red instead of a GUID.

I will change my implementation to generate GUIDs for valid lines.

This comment has been minimized.

Copy link
@marta-

marta- Oct 14, 2020

Contributor

I should have finished the above paragraph with "in the list of GUIDs generated as specified by #1", to clarify. I will update issue #3 with this clarification and an input/output example.

This comment has been minimized.

Copy link
@veronikaslc

veronikaslc Oct 15, 2020

Contributor

Fixed in b87c1f2

+ " component(s) instead of 3. Each line should contain the patient's Health Card number, the province code, and the patient's date of birth.";
break;
}
console.log(hash)
setBhash(hash);
})

// Validate DOB

This comment has been minimized.

Copy link
@marta-

marta- Oct 14, 2020

Contributor

there should be separate functions for validating each component to improve readability.

This comment has been minimized.

Copy link
@marta-

marta- Oct 14, 2020

Contributor

To be addressed as part of issue #7

This comment has been minimized.

Copy link
@veronikaslc

veronikaslc Oct 15, 2020

Contributor

Addressed in b87c1f2

let dateReg = /^\d{4}[\-\/\s]?((((0[13578])|(1[02]))[\-\/\s]?(([0-2][0-9])|(3[01])))|(((0[469])|(11))[\-\/\s]?(([0-2][0-9])|(30)))|(02[\-\/\s]?[0-2][0-9]))$/;
let matched = info[2].match(dateReg);

if (!matched || matched[0] != info[2]) {
err = "The date of birth " + info[2] + " appears to be invalid. Please enter a correct date of birth in the format YYYY-MM-DD.";
break;
}

// Replace \ and space with - before encryption
info[2] = info[2].replace(/\\/g, '-').replace(/\s+/g, '-');

// Validate Province code
// Should be one of: AB, BC, MB, NB, NL, NS, NT, NU, ON, PE, QC, SK, YT. Lower case version is accepted.
if (!provinces.includes(info[1].toUpperCase())) {
err = "Province code " + info[1] + " is invalid. It should be one of : AB, BC, MB, NB, NL, NS, NT, NU, ON, PE, QC, SK, YT.";

This comment has been minimized.

Copy link
@marta-

marta- Oct 14, 2020

Contributor

"Province code " + info[1].toUpperCase() + " is invalid. It should be one of :" + provinces.join(", ") +".";

This comment has been minimized.

Copy link
@veronikaslc

veronikaslc Oct 14, 2020

Contributor

changed in next commit

break;
}

// validate Health card number
// For ON:
if (info[1].toUpperCase() == "ON") {

This comment has been minimized.

Copy link
@marta-

marta- Oct 14, 2020

Contributor

This could be made more elegant as a map from province code to validation function plus a default validation function.

This comment has been minimized.

Copy link
@marta-

marta- Oct 14, 2020

Contributor

To be addressed as part of issue #7

This comment has been minimized.

Copy link
@veronikaslc

veronikaslc Oct 15, 2020

Contributor

Addressed in b87c1f2

// Remove everyhing except digits
info[0] = info[0].replace(/\D+/g, '');

if (info[0].length != 10) {
err = "Health card number " + info[0] + " is invalid for the province of ON. A 10 digit number is expected.";
break;
}

// validate Ontario Health card with the MOD 10 Check Digit algorithm
if (!isValidHealthCard(info[0])) {
err = "Health card number " + info[0] + " is invalid for the province of ON. Please check the number and try again.";
break;
}
} else {
// Remove spaces, dashes, trailing letters and assume the result is valid (for now).
info[0] = info[0].replace(/\D+/g, '');

This comment has been minimized.

Copy link
@marta-

marta- Oct 14, 2020

Contributor

Per specs, only remove spaces and dashes, not letters. Not all provinces have health card numbers that are actual ... numbers. E.g. QC has letters in the health card identifier.

This comment has been minimized.

Copy link
@veronikaslc

veronikaslc Oct 14, 2020

Contributor

fixed

}

// If the input is found valid, concatenate the post-processed health card number and date of birth and generate the GUID
postProcessed.push(info[0] + info[2]);
}

setPostProcessedInfo(postProcessed);
setErrorText(err);
!err && postProcessed.length > 0 && setIsValid(true);
}
let handleChange = (value) => {

let onSubmit = () => {
setIsValid(false);
setErrorText(null);
setBhash(null);
setCopied(false)
setBinput(value);

// sanitise and validate
setIsValid(true);

let hashes = [];

postProcessedInfo.forEach( (input, index) => {

bcrypt.hash(input + projectId, 10, (err, hash) => {
if (err) {
console.error(err);
setErrorText(err);
return;
}
// console.log(hash);
hashes.push(hash);
(index == postProcessedInfo.length - 1) && setBhashArray(hashes);
})
})
}



return (
<div className={classes.main}>
<Typography variant="h3">GUID generator</Typography>
<TextField
multiline
id="binput"
value={binput}
className={classes.binput}
fullWidth
onChange={(event) => {handleChange(event.target.value)}}
helperText={!errorText ? "Example: '2345678904,ON,2020-02-02'." : errorText}
error={errorText}
label="Please enter the health card number, province code, and date of birth as YYYY-MM-DD, separated by ',' for one or more patients, one patient per line."
placeholder="2345678904,ON,2002-01-23"
InputProps={{
startAdornment: (
<InputAdornment position="start">
<Icon>folder_shared</Icon>
</InputAdornment>
),
}}
InputLabelProps={{
classes: {
root: classes.labelRoot,
focused: classes.labelFocused
}
}}
/>
<br/>
<TextField
id="projectid"
label="Project Id"
value={projectId}
onChange={(event) => {setProjectId(event.target.value)}}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<Icon>label</Icon>
</InputAdornment>
),
}}
/>
<br/>
<Typography variant="h3">GUID generator</Typography>
<Grid container spacing={1}>

This comment has been minimized.

Copy link
@marta-

marta- Oct 14, 2020

Contributor

direction="column" wrap="nowrap" spacing={4}

This comment has been minimized.

Copy link
@veronikaslc

veronikaslc Oct 14, 2020

Contributor

added

<Grid item>
<TextField
multiline
id="binput"
value={binput}
className={classes.btextinput}
onChange={(event) => {handleChange(event.target.value)}}
helperText={!errorText ? "Example: '2345678904,ON,2020-02-02'." : errorText}
error={errorText}
label="Please enter the health card number, province code, and date of birth as YYYY-MM-DD, separated by ',' for one or more patients, one patient per line."
placeholder="2345678904,ON,2002-01-23"
InputProps={{
startAdornment: (
<InputAdornment position="start">
<Icon color="primary">folder_shared</Icon>
</InputAdornment>
),
}}
InputLabelProps={{
classes: {
root: classes.labelRoot,
focused: classes.labelFocused
}
}}
FormHelperTextProps={{
className: classes.helperText
}}
/>
</Grid>
<Grid item>
<TextField
id="projectid"
label="Project Id (* optional)"
value={projectId}
className={classes.projectidinput}
onChange={(event) => {setProjectId(event.target.value)}}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<Icon color="primary">label</Icon>
</InputAdornment>
),
}}
/>
</Grid>
</Grid>
<Button onClick={() => onSubmit()} variant="contained" color="primary" disabled={!isValid} className={classes.bbutton}>Generate GUIDs</Button>

This comment has been minimized.

Copy link
@marta-

marta- Oct 14, 2020

Contributor

The button should be in the grid for consistent spacing

This comment has been minimized.

Copy link
@marta-

marta- Oct 14, 2020

Contributor

The button should be re-enabled when the input changes. If The user adds a valid line to an already valid input, they should be able to re-generate the GUIDs.

This comment has been minimized.

Copy link
@veronikaslc

veronikaslc Oct 14, 2020

Contributor

That is exactly how it works now afaik.

This comment has been minimized.

Copy link
@marta-

marta- Oct 14, 2020

Contributor

My mistake, you are right. It only stays disabled when project id changes. It the disabled status should be re-evaluated at any form change.

This comment has been minimized.

Copy link
@veronikaslc

veronikaslc Oct 14, 2020

Contributor

fixed

<br/>
{bhash && <div>
{ bhashArray.length > 0 && bhashArray.map( bhash =>

This comment has been minimized.

Copy link
@marta-

marta- Oct 14, 2020

Contributor

These appear as one labeled input per guid and are really hard to follow and copy all at once

This comment has been minimized.

Copy link
@veronikaslc

veronikaslc Oct 14, 2020

Contributor

What would be the best UI solution for multiple GUIDs display?

This comment has been minimized.

Copy link
@marta-

marta- Oct 14, 2020

Contributor

The way we display the output will likely evolve, but the ultimate goal is easy manual transfer to another application.

I would start by putting them in a simple list that you can select and copy as one chunk.

This comment has been minimized.

Copy link
@veronikaslc

veronikaslc Oct 14, 2020

Contributor

done

<div>
<TextField
id="bhash-input"
multiline
label="Generated bcrypted GUID"
value={bhash}
InputProps={{
readOnly: true,
}}
variant="outlined"
className={classes.boutput}
/>
<CopyToClipboard text={bhash}
onCopy={() => setCopied(true)}>
<IconButton title="Copy GGUIDs to clipboard" >
<Icon>file_copy</Icon>
</IconButton>
</CopyToClipboard>

{copied ? <span style={{color: 'red'}}>Copied.</span> : null}
</div>
)
}

</div>
);
}
ReactDOM.render(

ReactDOM.render(
<GUIDGeneratorComponent/>,
document.querySelector('#root')
);

);

});

Expand Down

0 comments on commit 8462766

Please sign in to comment.