Skip to content
Open
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
10 changes: 10 additions & 0 deletions .babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"presets": [
"@babel/preset-env",
"@babel/preset-react"
],
"plugins": [
["@babel/plugin-proposal-class-properties", {}, "Jest breaks without this, but parcel breaks wihtout a unique name"],
"@babel/plugin-transform-runtime"
]
}
55 changes: 55 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{
"ignorePatterns": ["client/posts/javascript/scroll-header/scroll.js"],
"parser": "babel-eslint",
"extends": [
"prettier",
"eslint:recommended",
"plugin:react/recommended"
],

"settings": {
"import/resolver": {
"node": {
"extensions": [
".js",
]
}
},
"react": { "version": "detect" },
},

"plugins": [
"prettier",
"promise",
"react"
],

"rules": {
"prettier/prettier": ["error", {
"singleQuote": true,
"trailingComma": "all",
"semi": false,
}],
"no-var": 2,
"no-console": [2, { "allow": ["warn", "error"] }],
"eol-last": [2, "always"],
"no-multiple-empty-lines": [2, {"max": 1, "maxEOF": 0}],
"eqeqeq": 2,
"prefer-const": ["error", { "destructuring": "all" }],
"no-unused-vars": ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
"react/prop-types": 0,
"react/no-unescaped-entities": 0,
},

"env": {
"browser": true,
"jest": true,
"es6": true,
},

"globals": {
"document": false,
"window": false,
"process": true,
}
}
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
yarn.lock binary
docs/* binary
13 changes: 13 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
# untracked files
GoogleDrive/*
notebooks/keys.py

# python/django specific
server/local_settings.py
__pycache__
.venv
db.sqlite3
server/settings/.secret_key
server/settings/local.py

# node specific
node_modules
dist
.cache
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,24 @@ For each of the forms bounding boxes need to be created around text fields and m

The client (Prevention Point) has flagged "Name" and "UniqueID" as the fields that need the most accurate information. The Microsoft Azure Read API returns a certainty number. If the certainty number is below a certain threshold. The software should display the form and a field that allows for manual entry and correction.

# Installation

Before installing you must install python3, python-virtualenv, node, and yarn. If you have trouble installing these, feel free to reach out to @chriscauley on the slack.

```
# install packages, this will need to be re-run if requirements.txt or package.json is changed
./bin/install

# create a superuser for viewing data in admin (follow prompts)
python manage.py createsuperuser
```

# Usage and Development

Start the server

```
./bin/develop
```

This will start the django server and the node compiler in the same terminal. Navigate to http://localhost:8000 and select form type. Upload an image. Currently the image processing isn't hooked up, so step 3 (processing) is skipped and a blank form is shown.
22 changes: 22 additions & 0 deletions bin/develop
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#! /usr/bin/env bash

. .venv/bin/activate
function _django {
source .venv/bin/activate
python manage.py runserver
}

function _yarn {
yarn develop
}

if [[ $1 == "tmux" ]];then
# open dev servers in tmux session
tmux new-session -s prevpoint-forms _django
tmux new-window -t prevpoint-forms:1 _yarn
else
# open both in the same terminal
trap "exit" INT TERM ERR
trap "kill 0" EXIT
_django & _yarn
fi
20 changes: 20 additions & 0 deletions bin/install
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#! /usr/bin/env bash
set -e

# install node packages
yarn install

# create virtual environment
if [[ ! -d .venv ]];
then
python3 -m venv .venv
fi

# activate virtualenv
source .venv/bin/activate

# install packages
pip install -r requirements.txt

# create database and tables
python manage.py migrate
223 changes: 223 additions & 0 deletions client/FormMapper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
import React from 'react'

// TODO this is hard coded now, but the page count and the pdf names could be pulled from the server
const forms = [
'Narcan Participant Refill Form -2018.pdf',
'Narcan Participant Training Form - 2018.pdf',
'Provider Training Form - 201.pdf',
]

const getPages = (s) => [1, 2].map((n) => s + `-${n}.png`)

export const FormMapperPicker = () => {
return (
<ul className="list-inside">
{forms.map((form_name) => (
<li key={form_name} className="mb-2">
{form_name}
{getPages(form_name).map((img_name, i) => (
<div className="" key={img_name}>
<a href={img_name}>Page #{i + 1}</a>
</div>
))}
</li>
))}
</ul>
)
}

const EntryBox = (props) => {
const [name, ...xywh] = props.entry
return (
<div className="mb-2">
<input
className="px-2 py-1 border"
value={name}
onChange={props.onChange}
placeholder={props.placeholder}
/>
<span className="mx-4">{xywh.join(', ')}</span>
<i
className="text-red-500 fa fa-trash cursor-pointer"
onClick={props.onDelete}
/>
</div>
)
}

export default class FormMapper extends React.Component {
state = {
entries: [],
}
constructor(props) {
super(props)
this.storage_key = 'mapper__' + this.props.img_name
const _entries = window.localStorage.getItem(this.storage_key)
if (_entries) {
this.state.entries = JSON.parse(_entries)
}

this.canvasRef = React.createRef()
this.imgRef = React.createRef()
this.scrollRef = React.createRef()
}
imgLoad = () => {
const { width, height } = this.imgRef.current
this.setState({ width, height })
setTimeout(this.draw, 50) // not sure why it isn't drawing on the previous line
}
draw = () => {
const canvas = this.canvasRef.current
if (!canvas || !this.state.width) {
return
}
const { width, height } = this.state
canvas.width = width
canvas.height = height
const ctx = canvas.getContext('2d')
ctx.clearRect(0, 0, width, height)
this.state.entries.forEach((entry, i) => {
const [name, x, y, w, h] = entry
ctx.fillStyle = 'rgba(128,255,128,0.5)'
ctx.fillRect(x, y, w, h)
ctx.fillStyle = 'black'
ctx.font = '10px serif'
ctx.fillText(name || `Item #${i + 1}`, x, y + 10)
})

const { click, hover } = this.state
if (click && hover) {
ctx.fillStyle = undefined
ctx.lineWidth = 3
ctx.strokeStyle = 'black'
ctx.strokeRect(
click[0],
click[1],
hover[0] - click[0],
hover[1] - click[1],
)
}
window.localStorage.setItem(
this.storage_key,
JSON.stringify(this.state.entries),
)
}

getXY = (e) => {
const { left, top } = this.canvasRef.current.getBoundingClientRect()
return [e.clientX - left, e.clientY - top]
}
onMouseDown = (e) => this.setState({ error: null, click: this.getXY(e) })
onMouseMove = (e) =>
this.state.click && this.setState({ hover: this.getXY(e) })
onMouseUp = (e) => {
const { click, entries } = this.state
let [x0, y0] = click
let [x1, y1] = this.getXY(e)
if (x0 > x1) {
const temp = x0
x0 = x1
x1 = temp
}
if (y0 > y1) {
const temp = y0
y0 = y1
y1 = temp
}
const w = x1 - x0
const h = y1 - y0
if (w < 10 || h < 10) {
this.setState({
error: 'Cannot make boxes smaller than 10px',
hover: null,
click: null,
})
setTimeout(() => this.setState({ error: null }), 5000)
return
}
entries.push(['', x0, y0, w, h])
if (this.scrollRef.current) {
this.scrollRef.current.scrollTop = this.scrollRef.current.scrollHeight
}
this.setState({ hover: null, click: null, entries })
}
changeEntry = (i) => (e) => {
const { entries } = this.state
entries[i][0] = e.target.value
this.setState({ entries })
}
deleteEntry = (i) => () => {
const { entries } = this.state
entries.splice(i, 1)
this.setState({ entries })
}
copy = () => {
const lines = []
this.state.entries.forEach(([name, x, y, width, height]) => {
lines.push(`"${name}": ${JSON.stringify({ x, y, width, height })},\n`)
})
navigator.clipboard.writeText(`{\n${lines.join('')}}`)
this.setState({ copied: true })
setTimeout(() => this.setState({ copied: false }), 5000)
}
render() {
const { img_name } = this.props.match.params
const { width, height, entries, copied, error } = this.state
this.draw()
return (
<div className="fixed left-0 bottom-0 right-0" style={{ top: 77 }}>
<div className="relative max-w-full max-h-full overflow-scroll pb-32">
<img
src={`/blank_form/${img_name}`}
className="max-w-none"
onLoad={this.imgLoad}
ref={this.imgRef}
/>
<canvas
className="absolute top-0 left-0 bottom-0 right-0"
onMouseDown={this.onMouseDown}
onMouseMove={this.onMouseMove}
onMouseUp={this.onMouseUp}
ref={this.canvasRef}
height={height}
width={width}
/>
<div className="fixed bottom-0 right-0 bg-white p-4 border">
{entries.length > 0 ? (
<div>
<div
style={{ maxHeight: 200, overflowY: 'auto' }}
ref={this.scrollRef}
>
{entries.map((entry, i) => (
<EntryBox
key={i}
entry={entry}
onChange={this.changeEntry(i)}
onDelete={this.deleteEntry(i)}
placeholder={`Item #${i + 1}`}
/>
))}
</div>
<div
onClick={this.copy}
className="cursor-pointer text-underline"
>
{copied ? 'Copied!' : 'Copy To Clipboard'}
</div>
</div>
) : (
<div>Click and drag anywhere to make a box.</div>
)}
{error && (
<div className="p-2 bg-red-200 text-red-600 mt-2">
<i className="fa fa-exclamation-circle mr-2" />
{error}
</div>
)}
</div>
</div>
</div>
)
}
}
Loading