diff --git a/record-files/sample_data/user_assignments.csv b/record-files/sample_data/user_assignments.csv deleted file mode 100644 index 1e922e7..0000000 --- a/record-files/sample_data/user_assignments.csv +++ /dev/null @@ -1 +0,0 @@ -Events,Users Assigned diff --git a/requirements.txt b/requirements.txt index cb70c92..f9eed4c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,187 +1,99 @@ #### Requirements for running the server #### -chardet==3.0.4 \ - --hash=sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691 -Django==2.2.13 \ - --hash=sha256:e8fe3c2b2212dce6126becab7a693157f1a441a07b62ec994c046c76af5bb66d \ - --hash=sha256:84f370f6acedbe1f3c41e1a02de44ac206efda3355e427139ecb785b5f596d80 -django-autocomplete-light==3.3.4 \ - --hash=sha256:cff0b1cad0e233e49c8cce08dff22868951123cbb79a7c1768eda78845044568 -django-background-tasks==1.2.0 \ - --hash=sha256:35a9a54961f3e4486ab2f9482d1e8ac63ab4f47e5e0b7e654a22f7002299ffae -django-ckeditor==5.7.1 \ - --hash=sha256:0147f8905dc64747e45157a185feedee4e39973fa4b571c9c82ad10d9d4b8974 -Pillow==7.0.0 \ - --hash=sha256:c5ed816632204a2fc9486d784d8e0d0ae754347aba99c811458d69fcdfd2a2f9 \ - --hash=sha256:5bfef0b1cdde9f33881c913af14e43db69815c7e8df429ceda4c70a5e529210f \ - --hash=sha256:0a628977ac2e01ca96aaae247ec2bd38e729631ddf2221b4b715446fd45505be \ - --hash=sha256:4d9ed9a64095e031435af120d3c910148067087541131e82b3e8db302f4c8946 -python-decouple==3.1 \ - --hash=sha256:1317df14b43efee4337a4aa02914bf004f010cd56d6c4bd894e6474ec8c4fe2d -pytz==2018.3 \ - --hash=sha256:07edfc3d4d2705a20a6e99d97f0c4b61c800b8232dc1c04d87e8554f130148dd -django-crontab==0.7.1 \ - --hash=sha256:1201810a212460aaaa48eb6a766738740daf42c1a4f6aafecfb1525036929236 \ - --hash=sha256:64e9aa766220173aae5e4f027ed83a834886676004083de10501b4868154c49e +chardet==3.0.4 +Django==2.2.13 +django-autocomplete-light==3.3.4 +django-background-tasks==1.2.0 +django-ckeditor==5.7.1 +Pillow==7.0.0 +python-decouple==3.1 +pytz==2018.3 +django-crontab==0.7.1 #### Requirements for development and testing #### -django-debug-toolbar==1.9.1 \ - --hash=sha256:4af2a4e1e932dadbda197b18585962d4fc20172b4e5a479490bc659fe998864d -requests==2.26.0 \ - --hash=sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24 +django-debug-toolbar==1.9.1 +requests==2.26.0 #### Indirect requirements #### -six==1.12.0 \ - --hash=sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c +six==1.12.0 # required by Django 2.2.3 -sqlparse==0.2.4 \ - --hash=sha256:d9cf190f51cbb26da0412247dfe4fb5f4098edb73db84e02f9fc21fdca31fed4 +sqlparse==0.2.4 # required by django-ckeditor 5.7.1 -django-js-asset==0.1.1 \ - --hash=sha256:0dd2c5f64f2b24eb8a7270a6a59cb914a03f205335bd0eb6207bf61cf7410828 +django-js-asset==0.1.1 # required by django-background-tasks 1.2.0 -django-compat==1.0.15 \ - --hash=sha256:3ac9a3bedc56b9365d9eb241bc5157d0c193769bf995f9a78dc1bc24e7c2331b +django-compat==1.0.15 # required by requests 2.21.0 -certifi==2019.6.16 \ - --hash=sha256:046832c04d4e752f37383b628bc601a7ea7211496b4638f6514d0e5b9acc4939 -idna==2.8 \ - --hash=sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c -urllib3==1.24.3 \ - --hash=sha256:a637e5fae88995b256e3409dc4d52c2e2e0ba32c42a6365fee8bbd2238de3cfb +certifi==2019.6.16 +idna==2.8 +urllib3==1.24.3 # required for wfdb -wfdb==3.1.0 \ - --hash=sha256:0c28d1be15c6202309ac07ceafe83820ec8fe793d91cef978b6388e8b9a85771 -cycler==0.10.0 \ - --hash=sha256:1d8a5ae1ff6c5cf9b93e8811e581232ad8920aeec647c37316ceac982b08cb2d -joblib==0.16.0 \ - --hash=sha256:d348c5d4ae31496b2aa060d6d9b787864dd204f9480baaa52d18850cb43e9f49 -kiwisolver==1.2.0 \ - --hash=sha256:c31bc3c8e903d60a1ea31a754c72559398d91b5929fcb329b1c3a3d3f6e72113 \ - --hash=sha256:603162139684ee56bcd57acc74035fceed7dd8d732f38c0959c8bd157f913fec -matplotlib==3.3.0 \ - --hash=sha256:ebb6168c9330309b1f3360d36c481d8cd621a490cf2a69c9d6625b2a76777c12 \ - --hash=sha256:19cf4db0272da286863a50406f6430101af129f288c421b1a7f33ddfc8d0180f -mne==0.20.7 \ - --hash=sha256:c6aea11d7b3a37f6ad8ca63c177b311a4eb3f057f995fe0417b8535dadfd35a9 -nose==1.3.7 \ - --hash=sha256:9ff7c6cc443f8c51994b34a667bbcf45afd6d945be7477b52e97516fd17c53ac -numpy==1.19.1 \ - --hash=sha256:e45f8e981a0ab47103181773cc0a54e650b2aef8c7b6cd07405d0fa8d869444a \ - --hash=sha256:ef71a1d4fd4858596ae80ad1ec76404ad29701f8ca7cdcebc50300178db14dfc \ - --hash=sha256:b8456987b637232602ceb4d663cb34106f7eb780e247d51a260b84760fd8f491 -pandas==1.1.0 \ - --hash=sha256:0bc440493cf9dc5b36d5d46bbd5508f6547ba68b02a28234cd8e81fdce42744d \ - --hash=sha256:16504f915f1ae424052f1e9b7cd2d01786f098fbb00fa4e0f69d42b22952d798 \ - --hash=sha256:b39508562ad0bb3f384b0db24da7d68a2608b9ddc85b1d931ccaaa92d5e45273 -pyparsing==2.4.7 \ - --hash=sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b -python-dateutil==2.8.1 \ - --hash=sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a -scikit-learn==0.23.1 \ - --hash=sha256:0c3464e46ef8bd4f1bfa5c009648c6449412c8f7e9b3fc0c9e3d800139c48827 \ - --hash=sha256:0e7b55f73b35537ecd0d19df29dd39aa9e076dba78f3507b8136c819d84611fd -scipy==1.5.2 \ - --hash=sha256:fc98f3eac993b9bfdd392e675dfe19850cc8c7246a8fd2b42443e506344be7d9 \ - --hash=sha256:eecf40fa87eeda53e8e11d265ff2254729d04000cd40bae648e76ff268885d66 \ - --hash=sha256:066c513d90eb3fd7567a9e150828d39111ebd88d3e924cdfc9f8ce19ab6f90c9 -sklearn==0.0 \ - --hash=sha256:e23001573aa194b834122d2b9562459bf5ae494a2d59ca6b8aa22c85a44c0e31 -threadpoolctl==2.1.0 \ - --hash=sha256:38b74ca20ff3bb42caca8b00055111d74159ee95c4370882bbff2b93d24da725 +wfdb==3.1.0 +cycler==0.10.0 +joblib==0.16.0 +kiwisolver==1.2.0 +matplotlib==3.3.0 +mne==0.20.7 +nose==1.3.7 +numpy==1.19.1 +pandas==1.1.0 +pyparsing==2.4.7 +python-dateutil==2.8.1 +scikit-learn==0.23.1 +scipy==1.5.2 +sklearn==0.0 +threadpoolctl==2.1.0 # required for data visualization -django_plotly_dash==1.6.6 \ - --hash=sha256:fbb844292237416983f38a01cfb531154abefe8d4bd78994bba8ff03c9b77876 -dpd-components==0.1.0 \ - --hash=sha256:613a6b17d3d7dd449be060e739e4ce36692b46fa012c3a86ee947f6337d09548 -dash-html-components==1.1.3 \ - --hash=sha256:88adb77a674d5d7d0835d71c469f6e7b4aa692f9673808a474d244b71863c58a -dash==1.20.0 \ - --hash=sha256:127c16f71d3c8345dd29ab2aed099330aafd6d558734bec5bbcccadd0a7e6b29 -dash-core-components==1.16.0 \ - --hash=sha256:e8cdfaf3580577670bb2d1c3168efa06f5a7b439fbe5527cfaefa3e32394542f -dash-renderer==1.9.1 \ - --hash=sha256:73a69e3d145880e68e42723ad10182251d92b44f3efe92b8763145cfd2158e7e -dash-table==4.11.3 \ - --hash=sha256:0a4f22a5cf5120882a252a3348fc15ef45a1b75bf900934783e338aceac52f56 -flask==1.1.2 \ - --hash=sha256:8a4fdd8936eba2512e9c85df320a37e694c93945b33ef33c89946a340a238557 -plotly==5.4.0 \ - --hash=sha256:1e5c1a5c87caaf68ce0d9872d4636e3ce1f82c7f6988eb20905ff5b58e57525c +django_plotly_dash==1.6.6 +dpd-components==0.1.0 +dash-html-components==1.1.3 +dash==1.20.0 +dash-core-components==1.16.0 +dash-renderer==1.9.1 +dash-table==4.11.3 +flask==1.1.2 +plotly==5.4.0 # required by dash -flask-compress==1.5.0 \ - --hash=sha256:f367b2b46003dd62be34f7fb1379938032656dca56377a9bc90e7188e4289a7c -future==0.18.2 \ - --hash=sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d -Jinja2==2.11.2 \ - --hash=sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035 -itsdangerous==1.1.0 \ - --hash=sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749 -Werkzeug==1.0.1 \ - --hash=sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43 -click==7.1.2 \ - --hash=sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc -retrying==1.3.3 \ - --hash=sha256:08c039560a6da2fe4f2c426d0766e284d3b736e355f8dd24b37367b0bb41973b +flask-compress==1.5.0 +future==0.18.2 +Jinja2==2.11.2 +itsdangerous==1.1.0 +Werkzeug==1.0.1 +click==7.1.2 +retrying==1.3.3 # required for dash cache -django-redis==5.2.0 \ - --hash=sha256:1d037dc02b11ad7aa11f655d26dac3fb1af32630f61ef4428860a2e29ff92026 -redis==4.1.1 \ - --hash=sha256:bc97d18938ca18d66737d0ef88584a2073069589e4026813cfba9ad6df9a9f40 -Deprecated==1.2.13 \ - --hash=sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d -packaging==21.3 \ - --hash=sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522 -wrapt==1.13.3 \ - --hash=sha256:b73d4b78807bd299b38e4598b8e7bd34ed55d480160d2e7fdaabd9931afa65f9 \ - --hash=sha256:25b1b1d5df495d82be1c9d2fad408f7ce5ca8a38085e2da41bb63c914baadff7 +django-redis==5.2.0 +redis==4.1.1 +Deprecated==1.2.13 +packaging==21.3 +wrapt==1.13.3 # required by flask-compress -brotli==1.0.7 \ - --hash=sha256:71ceee286ea7ec613f1c36f1c6181864a6ca24ebb55e371276f33d6af8742834 \ - --hash=sha256:f192e6d3556714105c10486bbd6d045e38a0c04d9da3cef21e0a8dfd8e162df4 +brotli==1.0.7 # required by Jinja2 -MarkupSafe==1.1.1 \ - --hash=sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15 \ - --hash=sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6 \ - --hash=sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85 +MarkupSafe==1.1.1 # required by GraphQL API -aniso8601==7.0.0 \ - --hash=sha256:d10a4bf949f619f719b227ef5386e31f49a2b6d453004b21f02661ccc8670c7b -graphene==2.1.8 \ - --hash=sha256:09165f03e1591b76bf57b133482db9be6dac72c74b0a628d3c93182af9c5a896 -graphql-core==2.3.2 \ - --hash=sha256:44c9bac4514e5e30c5a595fac8e3c76c1975cae14db215e8174c7fe995825bad -graphql-relay==2.0.1 \ - --hash=sha256:ac514cb86db9a43014d7e73511d521137ac12cf0101b2eaa5f0a3da2e10d913d -promise==2.3 \ - --hash=sha256:dfd18337c523ba4b6a58801c164c1904a9d4d1b1747c7d5dbf45b693a49d93d0 -Rx==1.6.1 \ - --hash=sha256:7357592bc7e881a95e0c2013b73326f704953301ab551fbc8133a6fadab84105 -asgiref==3.2.10 \ - --hash=sha256:9fc6fb5d39b8af147ba40765234fa822b39818b12cc80b35ad9b0cef3a476aed -graphene-django==2.12.1 \ - --hash=sha256:03dfe2081c256e56d94d90b33b0bf6fa46ec274186023ccffb9c3aa46a856587 -singledispatch==3.4.0.3 \ - --hash=sha256:833b46966687b3de7f438c761ac475213e53b306740f1abfaa86e1d1aae56aa8 -Unidecode==1.1.1 \ - --hash=sha256:1d7a042116536098d05d599ef2b8616759f02985c85b4fef50c78a5aaf10822a -django-filter==2.4.0 \ - --hash=sha256:e00d32cebdb3d54273c48f4f878f898dced8d5dfaad009438fe61ebdf535ace1 +aniso8601==7.0.0 +graphene==2.1.8 +graphql-core==2.3.2 +graphql-relay==2.0.1 +promise==2.3 +Rx==1.6.1 +asgiref==3.2.10 +graphene-django==2.12.1 +singledispatch==3.4.0.3 +Unidecode==1.1.1 +django-filter==2.4.0 # Extra -charset-normalizer==2.0.0 \ - --hash=sha256:76fd234253352853909a367630ea0040001df0b4f6e9cb655a7bf861e81a6d32 -tenacity==6.2.0 \ - --hash=sha256:5a5d3dcd46381abe8b4f82b5736b8726fd3160c6c7161f53f8af7f1eb9b82173 -whitenoise==5.3.0 \ - --hash=sha256:d963ef25639d1417e8a247be36e6aedd8c7c6f0a08adcb5a89146980a96b577c \ No newline at end of file +charset-normalizer==2.0.0 +tenacity==6.2.0 +whitenoise==5.3.0 \ No newline at end of file diff --git a/waveform-django/static/caliper.mp4 b/waveform-django/static/caliper.mp4 index 3461bee..48a02fa 100644 Binary files a/waveform-django/static/caliper.mp4 and b/waveform-django/static/caliper.mp4 differ diff --git a/waveform-django/static/interface.mp4 b/waveform-django/static/interface.mp4 index a0ca676..cc7f705 100644 Binary files a/waveform-django/static/interface.mp4 and b/waveform-django/static/interface.mp4 differ diff --git a/waveform-django/static/practice-test.mp4 b/waveform-django/static/practice-test.mp4 index 7c2417b..06a1186 100644 Binary files a/waveform-django/static/practice-test.mp4 and b/waveform-django/static/practice-test.mp4 differ diff --git a/waveform-django/static/self-assignment.mp4 b/waveform-django/static/self-assignment.mp4 index 60d2e77..1f17851 100644 Binary files a/waveform-django/static/self-assignment.mp4 and b/waveform-django/static/self-assignment.mp4 differ diff --git a/waveform-django/templates/base.html b/waveform-django/templates/base.html index bb16e6e..2fd59b1 100755 --- a/waveform-django/templates/base.html +++ b/waveform-django/templates/base.html @@ -61,57 +61,92 @@ Adjudicator Console {% endif %} + {% url 'viewer_overview' as url %} + {% if user.is_annotator is True or user.practice_status != 'CO' %} + + + + {% endif %} + + {% if user.is_annotator %} + {% url 'practice_test' as url %} + + + + + {% else %} + {% url 'assessment' as url %} + + + + + {% endif %} {% url 'leaderboard' as url %} - - {% url 'practice_test' as url %} - - @@ -119,7 +154,7 @@ @@ -127,7 +162,7 @@ diff --git a/waveform-django/waveforms/dash_apps/finished_apps/waveform_vis.py b/waveform-django/waveforms/dash_apps/finished_apps/waveform_vis.py index 02ec44f..6875d0d 100644 --- a/waveform-django/waveforms/dash_apps/finished_apps/waveform_vis.py +++ b/waveform-django/waveforms/dash_apps/finished_apps/waveform_vis.py @@ -1,27 +1,21 @@ -import csv import datetime -import os - import dash import dash_core_components as dcc import dash_html_components as html from django_plotly_dash import DjangoDash -import numpy as np import pytz import wfdb from waveforms.dash_apps.finished_apps.waveform_vis_tools import WaveformVizTools -from waveforms.models import Annotation, User +from waveforms.models import Annotation, User, WaveformEvent, Bookmark from website.middleware import get_current_user from website.settings import base +from pathlib import Path +from itertools import chain + +PROJECT_PATH = Path(base.HEAD_DIR)/'record-files' -# Specify the record file locations -BASE_DIR = base.BASE_DIR -FILE_ROOT = os.path.abspath(os.path.join(BASE_DIR, os.pardir)) -FILE_LOCAL = os.path.join('record-files') -PROJECT_PATH = os.path.join(FILE_ROOT, FILE_LOCAL) -ALL_PROJECTS = base.ALL_PROJECTS # Formatting settings sidebar_width = '100%' event_fontsize = '100%' @@ -31,6 +25,8 @@ button_height = '10%' submit_width = '49%' arrow_width = '23%' +annotations_width = '100%' + # Set the default configuration of the plot top buttons plot_config = { 'responsive': True, @@ -53,6 +49,17 @@ # Specify the app layout app.layout = html.Div([ dcc.Loading(id='loading-1', children=[ + # Previously annotated values + html.Div( + id='annotation_table', + children=[html.Table( + id='annotation_table_contents', + children=[], + style={'width': '100%'} + ), + html.Hr(),], + style={'display': 'block', 'width': annotations_width} + ), # Area to submit annotations html.Div([ # The project display @@ -79,51 +86,55 @@ id='event_text', children=html.Span([''], style={'fontSize': event_fontsize}) ), - # The reviewer decision section - html.Label(['Enter decision here:'], - style={'font-size': label_fontsize}), - dcc.RadioItems( - id='reviewer_decision', - options=[ - {'label': 'True (alarm is correct)', 'value': 'True'}, - {'label': 'False (alarm is incorrect)', 'value': 'False'}, - {'label': 'Uncertain', 'value': 'Uncertain'}, - {'label': 'Reject (alarm is un-readable)', 'value': 'Reject'}, - {'label': 'Save for Later', 'value': 'Save for Later'} - ], - labelStyle={'display': 'block'}, - style={'width': sidebar_width}, - persistence=False - ), - html.Br(), - # The reviewer comment section - html.Label(['Enter comments here:'], - style={'font-size': label_fontsize}), - html.Div( - dcc.Textarea(id='reviewer_comments', - style={ - 'width': comment_box_width, - 'height': comment_box_height, - 'font-size': label_fontsize - }) - ), - # Submit annotation decision and comments - html.Button('Submit', - id='submit_annotation', - style={'height': button_height, - 'width': submit_width, - 'font-size': 'large'}), - # Select previous or next annotation - html.Button('\u2190', - id='previous_annotation', - style={'height': button_height, - 'width': arrow_width, - 'font-size': 'large'}), - html.Button('\u2192', - id='next_annotation', - style={'height': button_height, - 'width': arrow_width, - 'font-size': 'large'}), + html.Div([ + # The reviewer decision section + html.Label(['Enter decision here:'], + style={'font-size': label_fontsize}), + dcc.RadioItems( + id='reviewer_decision', + options=[ + {'label': 'True (alarm is correct)', 'value': 'True'}, + {'label': 'False (alarm is incorrect)', 'value': 'False'}, + {'label': 'Uncertain', 'value': 'Uncertain'}, + {'label': 'Reject (alarm is un-readable)', 'value': 'Reject'}, + {'label': 'Bookmark for Later', 'value': 'Bookmark'} + ], + labelStyle={'display': 'block'}, + style={'width': sidebar_width}, + persistence=False + ), + html.Br(), + # The reviewer comment section + html.Label(['Enter comments here:'], + style={'font-size': label_fontsize}), + html.Div( + dcc.Textarea(id='reviewer_comments', + style={ + 'width': comment_box_width, + 'height': comment_box_height, + 'font-size': label_fontsize + }) + ), + # Submit annotation decision and comments + html.Button('Submit', + id='submit_annotation', + style={'height': button_height, + 'width': submit_width, + 'font-size': 'large'}), + # Select previous or next annotation + html.Button('\u2190', + id='previous_annotation', + style={'height': button_height, + 'width': arrow_width, + 'font-size': 'large'}), + html.Button('\u2192', + id='next_annotation', + style={'height': button_height, + 'width': arrow_width, + 'font-size': 'large'}), + + ], style={'display': 'none'}, id='annotation_buttons'), + ], style={'display': 'inline-block', 'vertical-align': 'top', 'width': '20vw', 'margin-left': '10vw', 'padding-top': '2%'}), @@ -137,227 +148,30 @@ ], style={'display': 'inline-block'}) ], type='default'), # Hidden div inside the app that stores the desired project, record, and event - dcc.Input(id='set_project', type='hidden', persistence=False, value=''), - dcc.Input(id='set_record', type='hidden', persistence=False, value=''), - dcc.Input(id='set_event', type='hidden', persistence=False, value=''), - # Hidden div inside the app that stores the current project, record, and event - dcc.Input(id='temp_project', type='hidden', persistence=False, value=''), - dcc.Input(id='temp_record', type='hidden', persistence=False, value=''), - dcc.Input(id='temp_event', type='hidden', persistence=False, value=''), + dcc.Input(id='set_pageid', type='hidden', persistence=False, value=''), + dcc.Input(id='page_order', type='hidden', persistence=False, value=''), + dcc.Input(id='adjudication_mode', type='hidden', persistence=False, value=''), + dcc.Input(id='admin_mode', type='hidden', persistence=False, value=''), ]) -def get_practice_anns(ann): - """ - Filter Annotation object to only include events in practice set. - - Parameters - ---------- - ann : Annotation object - Object to be filtered. - - Returns - ------- - ann: Annotation object - Filtered object. - - """ - events_per_proj = [list(events.keys()) for events in base.PRACTICE_SET.values()] - events = [] - for i in events_per_proj: - events += i - return ann.filter( - project__in=[key for key in base.PRACTICE_SET.keys()], - event__in=events - ) - - -def get_all_records_events(project_folder): - """ - Get all possible records and events. - - Parameters - ---------- - project_folder : str - The project used to retrieve the records and events. - - Returns - ------- - N/A : list[str] - List of all records. - N/A : list[str] - List of all events. - - """ - # Get records - records_path = os.path.join(PROJECT_PATH, project_folder, - base.RECORDS_FILE) - with open(records_path, 'r') as f: - record_list = f.read().splitlines() - # Get events - event_list = [] - for record in record_list: - event_path = os.path.join(PROJECT_PATH, project_folder, record, - base.RECORDS_FILE) - with open(event_path, 'r') as f: - event_list += f.read().splitlines() - event_list = [e for e in event_list if '_' in e] - return record_list, event_list - - -def get_user_events(user, project_folder): - """ - Get the events assigned to a user in the CSV file. - - Parameters - ---------- - user : User - The User whose events will be retrieved. - project_folder : str - The project used to retrieve the events. - - Returns - ------- - N/A: list[str] - List of events assigned to the user. - - """ - # Find the files - BASE_DIR = base.BASE_DIR - FILE_ROOT = os.path.abspath(os.path.join(BASE_DIR, os.pardir)) - FILE_LOCAL = os.path.join('record-files') - PROJECT_PATH = os.path.join(FILE_ROOT, FILE_LOCAL) - - if user.is_admin and user.practice_status == 'ED': - record_list, event_list = get_all_records_events(project_folder) - elif user.practice_status != 'ED': - events_per_proj = [list(events.keys()) for events in base.PRACTICE_SET.values()] - events = [] - for i in events_per_proj: - events += i - return events - else: - csv_path = os.path.join(PROJECT_PATH, project_folder, - base.ASSIGNMENT_FILE) - event_list = [] - with open(csv_path, 'r') as csv_file: - csvreader = csv.reader(csv_file, delimiter=',') - next(csvreader) - for row in csvreader: - names = [] - for val in row[1:]: - if val: - names.append(val) - if user.username in names: - event_list.append(row[0]) - user_ann = Annotation.objects.filter(user=user, - project=project_folder, - is_adjudication=False) - if user.practice_status != 'ED': - user_ann = get_practice_anns(user_ann) - - event_list += [a.event for a in user_ann if a.event not in event_list] - return event_list - - -def get_user_records(user): - """ - Get the records assigned to a user in the CSV file. - - Parameters - ---------- - user : User - The User whose records will be retrieved. - - Returns - ------- - N/A: dict - The records assigned to the user by project. - - """ - user_records = {} - if user.is_admin and user.practice_status == 'ED': - for project in ALL_PROJECTS: - temp_records, _ = get_all_records_events(project) - user_records[project] = temp_records - return user_records - if user.practice_status == 'ED': - # Get all user annotations - annotations = Annotation.objects.filter( - user=user, is_adjudication=False - ) - if user.practice_status != 'ED': - annotations = get_practice_anns(annotations) - # Get all user events - user_events = {} - for project in ALL_PROJECTS: - user_events[project] = get_user_events(user, project) - # Get all user records - for project in ALL_PROJECTS: - events = user_events[project] - user_records[project] = [] - for evt in events: - rec = evt[:evt.find('_')] - if rec not in user_records[project]: - user_records[project].append(rec) - for ann in annotations: - if ann.record not in user_records[ann.project]: - user_records[ann.project].append(ann.record) - - return user_records - - -def get_header_info(project, file_path): - """ - Return all records/events in header from file path. - - Parameters - ---------- - project : str - Which project the record is in - file_path : str - The directory of the record file to be read. - - Returns - ------- - file_contents : list[str] - The stripped and cleaned lines of the record file. Essentially, all - of the records to be read. - - """ - current_user = User.objects.get(username=get_current_user()) - records_path = os.path.join(PROJECT_PATH, project, file_path, - base.RECORDS_FILE) - with open(records_path, 'r') as f: - file_contents = f.read().splitlines() - all_events = get_user_events(current_user, project) - file_contents = [e for e in file_contents if '_' in e and e in all_events] - return file_contents - - @app.callback( - [dash.dependencies.Output('dropdown_record', 'children'), + [dash.dependencies.Output('dropdown_project', 'children'), + dash.dependencies.Output('dropdown_record', 'children'), dash.dependencies.Output('dropdown_event', 'children'), - dash.dependencies.Output('dropdown_project', 'children'), dash.dependencies.Output('event_text', 'children'), - dash.dependencies.Output('temp_project', 'value'), - dash.dependencies.Output('temp_record', 'value'), - dash.dependencies.Output('temp_event', 'value')], + dash.dependencies.Output('set_pageid', 'value')], [dash.dependencies.Input('submit_annotation', 'n_clicks_timestamp'), dash.dependencies.Input('previous_annotation', 'n_clicks_timestamp'), dash.dependencies.Input('next_annotation', 'n_clicks_timestamp'), - dash.dependencies.Input('set_project', 'value'), - dash.dependencies.Input('set_record', 'value'), - dash.dependencies.Input('set_event', 'value')], - [dash.dependencies.State('temp_project', 'value'), - dash.dependencies.State('temp_record', 'value'), - dash.dependencies.State('temp_event', 'value'), + dash.dependencies.Input('set_pageid', 'value')], + [dash.dependencies.State('page_order', 'value'), dash.dependencies.State('reviewer_decision', 'value'), - dash.dependencies.State('reviewer_comments', 'value')]) -def get_record_event_options(click_submit, click_previous, click_next, - set_project, set_record, set_event, - project_value, record_value, event_value, - decision_value, comments_value): + dash.dependencies.State('reviewer_comments', 'value'), + dash.dependencies.State('adjudication_mode', 'value'), + ]) +def get_record_event_options(click_submit, click_previous, click_next, set_pageid, + page_order, decision_value, comments_value, adjudication_mode): """ Dynamically update the labels and stored variables given the current record and event. @@ -370,18 +184,10 @@ def get_record_event_options(click_submit, click_previous, click_next, The timestamp if the previous button was clicked in ms from epoch. click_next : int The timestamp if the next button was clicked in ms from epoch. - set_project : str - The desired project. - set_record : str - The desired record. - set_event : str - The desired event. - project_value : str - The current project. - record_value : str - The current record. - event_value : str - The current event. + set_pageid : str + The pageid of the current waveform the viewer is displaying. + page_order : str + The order of the pages for the viewer to display. decision_value : str The decision of the user. comments_value : str @@ -389,141 +195,125 @@ def get_record_event_options(click_submit, click_previous, click_next, Returns ------- + return_project : list[html.Span object] + The current project in HTML form so it can be rendered on the page. return_record : list[html.Span object] The current record in HTML form so it can be rendered on the page. return_event : list[html.Span object] The current event in HTML form so it can be rendered on the page. event_text : list[html.Span object] The current event in HTML form so it can be rendered on the page. - dropdown_record : str - The new selected record. - dropdown_event : str - The new selected event. + return_pageid : str + The page the user has navigated to. """ + # Determine what triggered this function ctx = dash.callback_context + # Prepare to return the record and event value for the user current_user = User.objects.get(username=get_current_user()) - # One project at a time - if current_user.practice_status == 'ED': - project = list(set(base.ALL_PROJECTS) - set(base.BLACKLIST))[0] - user_annotations = Annotation.objects.filter( - user=current_user, project=project, is_adjudication=False) - else: - project = [i for i in base.PRACTICE_SET.keys()][0] - user_annotations = Annotation.objects.filter( - user=current_user, project=project, is_adjudication=False) - user_annotations = get_practice_anns(user_annotations) - - # Display "Save for Later" first - user_annotations = sorted( - user_annotations, key=lambda x: 0 if x.decision=='Save for Later' else 1 - ) - all_events = [] - # Handle initial load - if not project_value: - # Completed annotations - completed_events = [] - for a in user_annotations: - completed_events.append([a.project, a.event]) - # Every assigned event / future annotation - proj_events = get_user_events(current_user, project) - for event in proj_events: - all_events.append([project, event]) - if all_events != []: - # Get the earliest annotation - if completed_events: - ann_indices = [all_events.index(a) for a in completed_events] - all_indices = sorted(list(set(np.arange(len(all_events))) - set(ann_indices))) + ann_indices - current_event = all_indices[0] - else: - current_event = 0 - return_project = all_events[current_event][0] - return_record = all_events[current_event][1].split('_')[0] - return_event = all_events[current_event][1] - else: - # Display empty graph since no data - return_project = 'N/A' - return_record = 'N/A' - return_event = 'N/A' - else: - completed_events = [a.event for a in user_annotations if a.project==project_value] - if current_user.is_admin and current_user.practice_status == 'ED': - _, all_events = get_all_records_events(project_value) - else: - all_events = get_user_events(current_user, project_value) - # The indices of completed annotations - ann_indices = [all_events.index(a) for a in completed_events] - # The indices of incomplete annotations - non_ann_indices = sorted(list(set(np.arange(len(all_events))) - set(ann_indices))) - # Eventually `len(non_ann_indices) == 0` so "Save for Later" will - # be first - all_indices = non_ann_indices + ann_indices if ctx.triggered: # Determine what triggered the function click_id = ctx.triggered[0]['prop_id'].split('.')[0] - # We already know the current project - return_project = project_value - # The location of event in the sorted event list - event_idx = all_indices.index(all_events.index(event_value)) - # Going backward in the list - if click_id == 'previous_annotation': - return_record = all_events[all_indices[event_idx-1]].split('_')[0] - return_event = all_events[all_indices[event_idx-1]] - # Going forward in the list - elif (click_id == 'next_annotation') or (click_id == 'submit_annotation'): - try: - return_record = all_events[all_indices[event_idx+1]].split('_')[0] - return_event = all_events[all_indices[event_idx+1]] - except IndexError: - # End of list, go back to the beginning - return_record = all_events[all_indices[0]].split('_')[0] - return_event = all_events[all_indices[0]] # Update the annotations: only save the annotations if a decision is # made and the submit button was pressed if decision_value and (click_id == 'submit_annotation'): - # Convert ms from epoch to datetime object (localize to the time - # zone in the settings) submit_time = datetime.datetime.fromtimestamp(click_submit/1000.0) set_timezone = pytz.timezone(base.TIME_ZONE) submit_time = set_timezone.localize(submit_time) - # Save the annotation to the database only if changes - # were made or a new annotation - try: - res = Annotation.objects.get( - user=current_user, project=project_value, - record=record_value, event=event_value, - is_adjudication=False - ) - current_annotation = [res.decision, res.comments] - proposed_annotation = [decision_value, comments_value] - # Only save annotation if something has changed - if current_annotation != proposed_annotation: - annotation = Annotation( - user=current_user, project=project_value, - record=record_value, event=event_value, - decision=decision_value, comments=comments_value, - decision_date=submit_time, is_adjudication=False + + waveform = WaveformEvent.objects.get(pk=page_order[set_pageid]) + + if decision_value == "Bookmark": + try: + if adjudication_mode: + res = Bookmark.objects.get(waveform=waveform, is_adjudication=True) + else: + res = Bookmark.objects.get(waveform=waveform, user=current_user, is_adjudication=False) + + current_bookmark = [res.comments] + proposed_bookmark = [comments_value] + + if current_bookmark != proposed_bookmark: + bookmark = Bookmark( + user=current_user, waveform=waveform, is_adjudication=adjudication_mode, + comments=comments_value, bookmark_date=submit_time + ) + bookmark.update() + + except Bookmark.DoesNotExist: + bookmark = Bookmark( + user=current_user, waveform=waveform, + comments=comments_value, bookmark_date=submit_time, + is_adjudication=adjudication_mode ) + bookmark.update() + + if adjudication_mode: + try: + Annotation.objects.get(waveform=waveform, is_adjudication=True).delete() + except Annotation.DoesNotExist: + pass + else: + try: + if adjudication_mode: + res = Annotation.objects.get(waveform=waveform, is_adjudication=True) + else: + res = Annotation.objects.get(waveform=waveform, user=current_user, is_adjudication=False) + + current_annotation = [res.decision, res.comments] + proposed_annotation = [decision_value, comments_value] + # Only save annotation if something has changed + if current_annotation != proposed_annotation: + annotation = Annotation( + user=current_user, waveform=waveform, + decision=decision_value, comments=comments_value, + decision_date=submit_time, is_adjudication=adjudication_mode, + ) + annotation.update() + except Annotation.DoesNotExist: + # Create new annotation since none already exist + annotation = Annotation( + user=current_user, waveform=waveform, + decision=decision_value, comments=comments_value, + decision_date=submit_time, is_adjudication=adjudication_mode, + ) annotation.update() - except Annotation.DoesNotExist: - # Create new annotation since none already exist - annotation = Annotation( - user=current_user, project=project_value, - record=record_value, event=event_value, - decision=decision_value, comments=comments_value, - decision_date=submit_time, is_adjudication=False - ) - annotation.update() + + if adjudication_mode: + try: + Bookmark.objects.get(waveform=waveform, is_adjudication=True).delete() + except Bookmark.DoesNotExist: + pass + + # Going backward in the list + if click_id == 'previous_annotation': + if set_pageid == 0: + return_pageid = len(page_order) - 1 + else: + return_pageid = set_pageid - 1 + + # Going forward in the list + elif (click_id == 'next_annotation') or (click_id == 'submit_annotation'): + if set_pageid == len(page_order) - 1: + return_pageid = 0 + else: + return_pageid = set_pageid + 1 + + next_waveform = WaveformEvent.objects.get(pk=page_order[return_pageid]) + return_project = next_waveform.project + return_record = next_waveform.record + return_event = next_waveform.event + else: - # See if record and event was requested (never event without record) - if set_record != '': - return_project = set_project - return_record = set_record - return_event = set_event + return_pageid = set_pageid + next_waveform = WaveformEvent.objects.get(pk=page_order[return_pageid]) + return_project = next_waveform.project + return_record = next_waveform.record + return_event = next_waveform.event # Update the event text alarm_text = html.Span([''], style={'fontSize': event_fontsize}) @@ -535,58 +325,102 @@ def get_record_event_options(click_submit, click_previous, click_next, ] else: # Get the annotation information - ann_path = os.path.join(PROJECT_PATH, return_project, - return_record, return_event) + ann_path = str(PROJECT_PATH / return_project / return_record / return_event) ann = wfdb.rdann(ann_path, 'alm') ann_event = ann.aux_note[0] # Update the annotation event text alarm_text = [ - html.Span(['{}'.format(ann_event), html.Br(), html.Br()], + html.Span([f'{ann_event}', html.Br(), html.Br()], style={'fontSize': event_fontsize}) ] # Update the annotation current project text project_text = [ - html.Span(['{}'.format(return_project)], + html.Span([f'{return_project}'], style={'fontSize': event_fontsize}) ] # Update the annotation current record text record_text = [ - html.Span(['{}'.format(return_record)], + html.Span([f'{return_record}'], style={'fontSize': event_fontsize}) ] # Update the annotation current event text event_text = [ - html.Span(['{}'.format(return_event)], + html.Span([f'{return_event}'], style={'fontSize': event_fontsize}) ] - return (record_text, event_text, project_text, alarm_text, - return_project, return_record, return_event) + return (project_text, record_text, event_text, alarm_text, return_pageid) + + +@app.callback( + [dash.dependencies.Output('annotation_table', 'style'), + dash.dependencies.Output('annotation_table_contents', 'children'), + dash.dependencies.Output('annotation_buttons', 'style')], + [dash.dependencies.Input('set_pageid', 'value')], + [dash.dependencies.State('page_order', 'value'), + dash.dependencies.State('adjudication_mode', 'value'), + dash.dependencies.State('admin_mode', 'value')]) +def mode_displays(set_pageid, page_order, adjudication_mode, admin_mode): + """ + Callback function to set waveform display items for various modes. + """ + annotation_table = {'display': 'none'} + annotation_table_contents = [] + annotation_buttons = {'display': 'block'} + + if adjudication_mode or admin_mode: + waveform = WaveformEvent.objects.get(pk=page_order[set_pageid]) + annotations = Annotation.objects.filter(waveform=waveform) + if annotations.count() > 0: + return_table = [ + # Header + html.Tr([ + html.Th(col) for col in ['User' , 'Decision' , 'Decision Date' , 'Comments'] + ], style={'text-align': 'left'}) + + ] + for annotation in annotations: + if annotation.is_adjudication: + style={'color': 'blue', 'font-weight': 'bold'} + else: + style={} + + return_table.append( + html.Tr([ + html.Td(annotation.user.username), + html.Td(annotation.decision), + html.Td(annotation.decision_date.strftime('%B %d, %Y %I:%M %p')), + html.Td(annotation.comments), + ], style=style) + ) + annotation_table = {'display': 'block'} + annotation_table_contents = return_table + + if admin_mode: + annotation_buttons = {'display': 'none'} + + return [annotation_table, annotation_table_contents, annotation_buttons] @app.callback( [dash.dependencies.Output('the_graph', 'figure'), dash.dependencies.Output('reviewer_decision', 'value'), dash.dependencies.Output('reviewer_comments', 'value')], - [dash.dependencies.Input('dropdown_event', 'children')], - [dash.dependencies.State('dropdown_record', 'children'), - dash.dependencies.State('dropdown_project', 'children')]) -def update_graph(dropdown_event, dropdown_record, dropdown_project): + [dash.dependencies.Input('set_pageid', 'value')], + [dash.dependencies.State('page_order', 'value'), + dash.dependencies.State('adjudication_mode', 'value')]) +def update_graph(set_pageid, page_order, adjudication_mode): """ Run the app and render the waveforms using the chosen initial conditions. Parameters ---------- - dropdown_event : list[dict], dict - Either a list (if multiple input triggers) of dictionaries or a single - dictionary (if single input trigger) of the current record. - dropdown_record : list[dict], dict - Either a list (if multiple input triggers) of dictionaries or a single - dictionary (if single input trigger) of the current event. - dropdown_project : list[dict], dict - Either a list (if multiple input triggers) of dictionaries or a single - dictionary (if single input trigger) of the current project. + set_pageid : int + The current page the viewer is displaying. + + page_order : list + The list of pages to display. Returns ------- @@ -601,10 +435,13 @@ def update_graph(dropdown_event, dropdown_record, dropdown_project): # Import the waveform tools for the current user current_user = get_current_user() wvt = WaveformVizTools(current_user) + display_waveform = WaveformEvent.objects.get(pk=page_order[set_pageid]) + # Create the figure - dropdown_record = wvt.get_dropdown(dropdown_record) - dropdown_event = wvt.get_dropdown(dropdown_event) - dropdown_project = wvt.get_dropdown(dropdown_project) + dropdown_project = display_waveform.project + dropdown_record = display_waveform.record + dropdown_event = display_waveform.event + # Blank figure if empty if ((dropdown_record == 'N/A') or (dropdown_event == 'N/A') or (dropdown_project == 'N/A')): @@ -618,18 +455,38 @@ def update_graph(dropdown_event, dropdown_record, dropdown_project): # Clear the reviewer decision and comments if none has been created or load # them otherwise when loading a new record and event. if (dropdown_event != '') and (dropdown_event is not None) and (current_user != ''): - # Get the decision user = User.objects.get(username=current_user) - try: - res = Annotation.objects.get( - user=user, project=dropdown_project, record=dropdown_record, - event=dropdown_event, is_adjudication=False - ) - return_decision = res.decision - return_comments = res.comments - except Annotation.DoesNotExist: - return_decision = None - return_comments = '' + if adjudication_mode: + try: + res = Annotation.objects.get( + waveform=display_waveform, is_adjudication=True + ) + return_decision = res.decision + return_comments = res.comments + except Annotation.DoesNotExist: + try: + res = Bookmark.objects.get(user=user, waveform=display_waveform, is_adjudication=True) + return_decision = "Bookmark" + return_comments = res.comments + except Bookmark.DoesNotExist: + return_decision = None + return_comments = '' + else: + # Get the decision + try: + res = Annotation.objects.get( + user=user, waveform=display_waveform, is_adjudication=False + ) + return_decision = res.decision + return_comments = res.comments + except Annotation.DoesNotExist: + try: + res = Bookmark.objects.get(user=user, waveform=display_waveform) + return_decision = "Bookmark" + return_comments = res.comments + except Bookmark.DoesNotExist: + return_decision = None + return_comments = '' else: return_decision = None return_comments = '' diff --git a/waveform-django/waveforms/dash_apps/finished_apps/waveform_vis_adjudicate.py b/waveform-django/waveforms/dash_apps/finished_apps/waveform_vis_adjudicate.py deleted file mode 100644 index 1c4965c..0000000 --- a/waveform-django/waveforms/dash_apps/finished_apps/waveform_vis_adjudicate.py +++ /dev/null @@ -1,541 +0,0 @@ -from collections import Counter -import datetime -import os - -import dash -import dash_core_components as dcc -import dash_html_components as html -from django_plotly_dash import DjangoDash -import numpy as np -import pytz -import wfdb - -from waveforms.dash_apps.finished_apps.waveform_vis_tools import WaveformVizTools -from waveforms.models import Annotation, User -from website.middleware import get_current_user -from website.settings import base - - -# Specify the record file locations -BASE_DIR = base.BASE_DIR -FILE_ROOT = os.path.abspath(os.path.join(BASE_DIR, os.pardir)) -FILE_LOCAL = os.path.join('record-files') -PROJECT_PATH = os.path.join(FILE_ROOT, FILE_LOCAL) -ALL_PROJECTS = base.ALL_PROJECTS -# Formatting settings -annotations_width = '100%' -sidebar_width = '14.58%' -event_fontsize = '100%' -comment_box_width = '90%' -comment_box_height = '15vh' -label_fontsize = '100%' -button_height = '10%' -button_width = '50%' -# Set the default configuration of the plot top buttons -plot_config = { - 'displayModeBar': True, - 'modeBarButtonsToAdd': [ - ], - 'modeBarButtonsToRemove': [ - 'hoverClosestCartesian', - 'hoverCompareCartesian', - 'toggleSpikelines', - 'pan2d', - 'zoom2d', - 'resetScale2d' - ] -} - - -# Initialize the Dash App -app = DjangoDash(name='waveform_graph_adjudicate') -# Specify the app layout -app.layout = html.Div([ - dcc.Loading(id='loading-1', children=[ - # Previously annotated values - html.Div( - id='annotation_table', - children=html.Table( - # Header - [ - html.Tr([ - html.Th(col) for col in ['user','decision','comments','decision_date'] - ], style={'text-align': 'left'}) - ] + - # Body - [ - html.Tr([ - html.Td('-') for col in ['user','decision','comments','decision_date'] - ], style={'text-align': 'left'}) for _ in range(2) - ], - style={'width': '100%'} - ), - style={'display': 'block', 'width': annotations_width} - ), - html.Hr(), - # Area to submit annotations - html.Div([ - # The project display - html.Label(['Project:']), - html.Div( - id='dropdown_project', - children=html.Span([''], style={'fontSize': event_fontsize}) - ), - # The record display - html.Label(['Record:']), - html.Div( - id='dropdown_record', - children=html.Span([''], style={'fontSize': event_fontsize}) - ), - # The event display - html.Label(['Event:']), - html.Div( - id='dropdown_event', - children=html.Span([''], style={'fontSize': event_fontsize}) - ), - # The event display - html.Label(['Event Type:']), - html.Div( - id='event_text', - children=html.Span([''], style={'fontSize': event_fontsize}) - ), - # Submit annotation decision and comments - # The reviewer comment section - html.Label(['Enter comments here:'], - style={'font-size': label_fontsize}), - html.Div( - dcc.Textarea(id='reviewer_comments', - style={ - 'width': comment_box_width, - 'height': comment_box_height, - 'font-size': label_fontsize - }) - ), - html.Br(), - # For warning the user of their decision - html.Div(id='output-provider'), - dcc.ConfirmDialogProvider( - children=html.Button( - 'True', - style={'height': button_height, - 'width': button_width, - 'font-size': 'large'}), - id='adjudication_true', - message='You selected True... Are you sure you want to continue?' - ), - html.Br(), - dcc.ConfirmDialogProvider( - children=html.Button( - 'False', - style={'height': button_height, - 'width': button_width, - 'font-size': 'large'}), - id='adjudication_false', - message='You selected False... Are you sure you want to continue?' - ), - html.Br(), - dcc.ConfirmDialogProvider( - children=html.Button( - 'Uncertain', - style={'height': button_height, - 'width': button_width, - 'font-size': 'large'}), - id='adjudication_uncertain', - message='You selected Uncertain... Are you sure you want to continue?' - ), - html.Br(), - dcc.ConfirmDialogProvider( - children=html.Button( - 'Reject', - style={'height': button_height, - 'width': button_width, - 'font-size': 'large'}), - id='adjudication_reject', - message='You selected Reject... Are you sure you want to continue?' - ), - html.Br(), - html.Button('\u2192', - id='next_annotation', - style={'height': button_height, - 'width': button_width, - 'font-size': 'large'}), - ], style={'display': 'inline-block', 'vertical-align': 'top', - 'width': '20hw', 'margin-left': '10vw', - 'padding-top': '3%'}), - # The plot itself - html.Div([ - dcc.Graph( - id='the_graph', - config=plot_config, - style={'height': '70vh', 'width': '60vw'} - ), - ], style={'display': 'inline-block'}) - ], type='default'), - # Hidden div inside the app that stores the desired project, record, and event - dcc.Input(id='set_project', type='hidden', persistence=False, value=''), - dcc.Input(id='set_record', type='hidden', persistence=False, value=''), - dcc.Input(id='set_event', type='hidden', persistence=False, value=''), - # Hidden div inside the app that stores the current project, record, and event - dcc.Input(id='temp_project', type='hidden', persistence=False, value=''), - dcc.Input(id='temp_record', type='hidden', persistence=False, value=''), - dcc.Input(id='temp_event', type='hidden', persistence=False, value=''), -]) - - -def get_current_conflicting_annotation(project='', record='', event=''): - """ - Get the current conflicting annotation which is needed to be adjudicated. - - PARAMETERS - ---------- - project : str, optional - The desired project. - record : str, optional - The desired record. - event : str, optional - The desired event. - - RETURNS - ------- - N/A : tuple - A list of the annotations which are conflicting in the form of: - (project, record, event) - - """ - # Get info of all non-adjudicated annotations assuming non-unique event - # names - non_adjudicated_anns = Annotation.objects.filter(is_adjudication=False) - all_info = [tuple(ann.values()) for ann in non_adjudicated_anns.values('project','record','event')] - unique_anns = Counter(all_info).keys() - ann_counts = Counter(all_info).values() - # Get completed annotations (should be two but I guess could be more if - # glitch or old data) - completed_anns = [c[0] for c in list(zip(unique_anns,ann_counts)) if c[1]>=2] - - # Sort by `decision_date` with older conflicting annotations appearing - # first to predictable traverse the remaining annotations. - sorted_anns = [] - for c in completed_anns: - # Get all the annotations for this event - all_anns = Annotation.objects.filter( - project=c[0], record=c[1], event=c[2] - ) - is_adjudicated = True in [a.is_adjudication for a in all_anns] - if not is_adjudicated: - # Make sure the annotations are complete - current_anns = all_anns.filter(is_adjudication=False).values_list('decision', flat=True) - is_conflicting = len(set(current_anns)) >= 2 - # Make sure there are conflicting decisions and no adjudications - # already - if is_conflicting: - current_ann = non_adjudicated_anns.filter( - project=c[0], record=c[1], event=c[2] - ) - # Get the most recent annotation (i.e. time of completion) - current_ann = sorted(current_ann, key=lambda x: x.decision_date)[-1] - sorted_anns.append(c + (current_ann.decision_date,)) - sorted_anns = sorted(sorted_anns, key=lambda x: x[-1].timestamp()) - - # The oldest conflicting annotation (project, record, event) - if sorted_anns: - if project and record and event: - # Get the index of the current annotation - try: - current_index = [a[:-1] for a in sorted_anns].index((project,record,event)) + 1 - except ValueError: - # Annotation was just adjudicated, return to previous location - current_annotations = Annotation.objects.filter( - project=project, record=record, event=event, - is_adjudication=False - ) - current_timestamp = sorted( - current_annotations, key=lambda x: x.decision_date - )[-1].decision_date - current_index = np.searchsorted( - [a[-1] for a in sorted_anns[::-1]], current_timestamp, - side='left' - ) - 1 - if (current_index < 0) or (current_index >= len(sorted_anns)): - return sorted_anns[0][:-1] - else: - return sorted_anns[current_index][:-1] - # Return the next one unless at the end of the list - if current_index >= len(sorted_anns): - return sorted_anns[0][:-1] - else: - return sorted_anns[current_index][:-1] - else: - return sorted_anns[0][:-1] - else: - return ('N/A', 'N/A', 'N/A') - - -@app.callback( - [dash.dependencies.Output('dropdown_project', 'children'), - dash.dependencies.Output('dropdown_record', 'children'), - dash.dependencies.Output('dropdown_event', 'children'), - dash.dependencies.Output('event_text', 'children'), - dash.dependencies.Output('temp_project', 'value'), - dash.dependencies.Output('temp_record', 'value'), - dash.dependencies.Output('temp_event', 'value')], - [dash.dependencies.Input('adjudication_true', 'submit_n_clicks'), - dash.dependencies.Input('adjudication_false', 'submit_n_clicks'), - dash.dependencies.Input('adjudication_uncertain', 'submit_n_clicks'), - dash.dependencies.Input('adjudication_reject', 'submit_n_clicks'), - dash.dependencies.Input('next_annotation', 'n_clicks_timestamp'), - dash.dependencies.Input('set_project', 'value'), - dash.dependencies.Input('set_record', 'value'), - dash.dependencies.Input('set_event', 'value')], - [dash.dependencies.State('temp_project', 'value'), - dash.dependencies.State('temp_record', 'value'), - dash.dependencies.State('temp_event', 'value'), - dash.dependencies.State('reviewer_comments', 'value')]) -def get_record_event_options(submit_true, submit_false, submit_uncertain, - submit_reject, click_next, set_project, - set_record, set_event, project_value, - record_value, event_value, comments_value): - """ - Dynamically update the record given the current record and event. - - Parameters - ---------- - submit_true : int - The number of times the submit true button was clicked. - submit_false : int - The number of times the submit false button was clicked. - submit_uncertain : int - The number of times the submit uncertain button was clicked. - submit_reject : int - The number of times the submit reject button was clicked. - set_project : str - The desired project. - set_record : str - The desired record. - set_event : str - The desired event. - decision_value : str - The decision of the user. - comment_value : str - The comments of the user. - - Returns - ------- - return_project : list[html.Span object] - The current project in HTML form so it can be rendered on the page. - return_record : list[html.Span object] - The current record in HTML form so it can be rendered on the page. - return_event : list[html.Span object] - The current event in HTML form so it can be rendered on the page. - event_text : list[html.Span object] - The current event in HTML form so it can be rendered on the page. - - """ - # Determine what triggered this function - ctx = dash.callback_context - # Prepare to return the record and event value for the user - current_user = User.objects.get(username=get_current_user()) - - # Handle initial load - if not project_value: - # Display the first conflicting event if none specified - return_project, return_record, return_event = get_current_conflicting_annotation() - - # If something was triggered (submit, request, etc.) - if ctx.triggered: - # Determine what triggered the function - click_id = ctx.triggered[0]['prop_id'].split('.')[0] - # Submit the adjudication and reset - adjudication_ids = ['adjudication_true', 'adjudication_false', - 'adjudication_uncertain', 'adjudication_reject'] - if click_id in adjudication_ids: - # Get the current time and localize to the time zone in the - # settings - submit_time = datetime.datetime.now( - pytz.timezone(base.TIME_ZONE)) - # Convert the decision value to a recognized format - decision_value = click_id.split('_')[1].capitalize() - # Save the annotation to the database only if changes - # were made or a new annotation - try: - res = Annotation.objects.get( - user=current_user, project=project_value, - record=record_value, event=event_value, - is_adjudication=True - ) - current_annotation = [res.decision, res.comments] - proposed_annotation = [decision_value, comments_value] - # Only save annotation if something has changed - if current_annotation != proposed_annotation: - # Delete the old one - res.delete() - # Save the new one - annotation = Annotation( - user=current_user, project=project_value, - record=record_value, event=event_value, - decision=decision_value, comments=comments_value, - decision_date=submit_time, is_adjudication=True - ) - annotation.save() - except Annotation.DoesNotExist: - # Create new annotation since none already exist - annotation = Annotation( - user=current_user, project=project_value, - record=record_value, event=event_value, - decision=decision_value, comments=comments_value, - decision_date=submit_time, is_adjudication=True - ) - annotation.save() - # We already know the current project, record, and event - return_project, return_record, return_event = get_current_conflicting_annotation( - project=project_value, record=record_value, event=event_value - ) - elif click_id == 'next_annotation': - # We already know the current project, record, and event - return_project, return_record, return_event = get_current_conflicting_annotation( - project=project_value, record=record_value, event=event_value - ) - else: - # See if record and event was requested (never event without record) - if set_record != '': - return_project = set_project - return_record = set_record - return_event = set_event - - # Update the annotation current project text - project_text = [ - html.Span(['{}'.format(return_project)], - style={'fontSize': event_fontsize}) - ] - # Update the annotation current record text - record_text = [ - html.Span(['{}'.format(return_record)], - style={'fontSize': event_fontsize}) - ] - # Update the annotation current event text - event_text = [ - html.Span(['{}'.format(return_event)], - style={'fontSize': event_fontsize}) - ] - - # Update the event text - alarm_text = html.Span([''], style={'fontSize': event_fontsize}) - if return_record and return_event and return_project: - if ((return_record == 'N/A') or (return_event == 'N/A') or - (return_project == 'N/A')): - alarm_text = [ - html.Span(['N/A', html.Br(), html.Br()], - style={'fontSize': event_fontsize}) - ] - else: - # Get the annotation information - ann_path = os.path.join(PROJECT_PATH, return_project, - return_record, return_event) - ann = wfdb.rdann(ann_path, 'alm') - ann_event = ann.aux_note[0] - # Update the annotation event text - alarm_text = [ - html.Span(['{}'.format(ann_event), html.Br(), html.Br()], - style={'fontSize': event_fontsize}) - ] - - return (project_text, record_text, event_text, alarm_text, - return_project, return_record, return_event) - - -@app.callback( - [dash.dependencies.Output('the_graph', 'figure'), - dash.dependencies.Output('annotation_table', 'children'), - dash.dependencies.Output('reviewer_comments', 'value')], - [dash.dependencies.Input('dropdown_project', 'children'), - dash.dependencies.Input('dropdown_record', 'children'), - dash.dependencies.Input('dropdown_event', 'children')]) -def update_graph(dropdown_project, dropdown_record, dropdown_event): - """ - Run the app and render the waveforms using the chosen initial conditions. - - Parameters - ---------- - dropdown_event : list[dict], dict - Either a list (if multiple input triggers) of dictionaries or a single - dictionary (if single input trigger) of the current record. - dropdown_record : list[dict], dict - Either a list (if multiple input triggers) of dictionaries or a single - dictionary (if single input trigger) of the current event. - dropdown_project : list[dict], dict - Either a list (if multiple input triggers) of dictionaries or a single - dictionary (if single input trigger) of the current project. - - Returns - ------- - N/A : plotly.subplots - The final figure. - N/A : html.Table - The table of previous annotations for the current adjudication. - - """ - # Import the waveform tools for the current user - current_user = get_current_user() - wvt = WaveformVizTools(current_user) - # Update the adjudication information - if not dropdown_project and not dropdown_record and not dropdown_event: - dropdown_project, dropdown_record, dropdown_event = get_current_conflicting_annotation() - else: - dropdown_project = dropdown_project[0]['props']['children'][0] - dropdown_record = dropdown_record[0]['props']['children'][0] - dropdown_event = dropdown_event[0]['props']['children'][0] - # Annotation table - return_table = [ - html.Table( - # Header - [ - html.Tr([ - html.Th(col) for col in ['user' , 'decision' , 'comments' , 'decision_date'] - ], style={'text-align': 'left'}) - ] + - # Body - [ - html.Tr([ - html.Td('-') for _ in ['user' , 'decision' , 'comments' , 'decision_date'] - ], style={'text-align': 'left'}) for _ in range(2) - ], - style={'width': '100%'} - )] - # Blank figure if empty - if ((dropdown_record == 'N/A') or (dropdown_event == 'N/A') or - (dropdown_project == 'N/A')): - fig = wvt.create_blank_figure() - return (fig), return_table, '' - # Figure - fig = wvt.create_final_figure( - dropdown_project, dropdown_record, dropdown_event - ) - - # Annotation table - conflict_ann_dict = Annotation.objects.filter( - project=dropdown_project, record=dropdown_record, event=dropdown_event - ).values( - *['user__username', 'decision', 'comments', 'decision_date'] - ) - for a in conflict_ann_dict: - a['decision_date'] = a['decision_date'].astimezone( - pytz.timezone(base.TIME_ZONE)).strftime('%B %d, %Y %H:%M:%S') - if not a['comments']: - a['comments'] = '-' - return_table = [ - html.Table( - # Header - [ - html.Tr([ - html.Th(col) for col in ['user__username', 'decision', 'comments', 'decision_date'] - ], style={'text-align': 'left'}) - ] + - # Body - [ - html.Tr([ - html.Td(cad[col]) for col in ['user__username', 'decision', 'comments', 'decision_date'] - ], style={'text-align': 'left'}) for cad in conflict_ann_dict - ], - style={'width': '100%'} - )] - - return (fig), return_table, '' diff --git a/waveform-django/waveforms/dash_apps/finished_apps/waveform_vis_tools.py b/waveform-django/waveforms/dash_apps/finished_apps/waveform_vis_tools.py index ea1ba95..508af7d 100644 --- a/waveform-django/waveforms/dash_apps/finished_apps/waveform_vis_tools.py +++ b/waveform-django/waveforms/dash_apps/finished_apps/waveform_vis_tools.py @@ -9,13 +9,11 @@ from waveforms.models import User, UserSettings from website.settings import base +from pathlib import Path + # Specify the record file locations -BASE_DIR = base.BASE_DIR -FILE_ROOT = os.path.abspath(os.path.join(BASE_DIR, os.pardir)) -FILE_LOCAL = os.path.join('record-files') -PROJECT_PATH = os.path.join(FILE_ROOT, FILE_LOCAL) -ALL_PROJECTS = base.ALL_PROJECTS +PROJECT_PATH = Path(base.HEAD_DIR)/'record-files' # Load in the default variables class WaveformVizTools: @@ -635,15 +633,12 @@ def prepare_graph(self, dropdown_project, dropdown_record, """ # Determine the time of the event (seconds) - ann_path = os.path.join(PROJECT_PATH, dropdown_project, - dropdown_record, dropdown_event) - ann = wfdb.rdann(ann_path, 'alm') + event_path = str(PROJECT_PATH / dropdown_project / dropdown_record / dropdown_event) + ann = wfdb.rdann(event_path, 'alm') event_time = (ann.sample / ann.fs)[0] # Determine the signal information - record_path = os.path.join(PROJECT_PATH, dropdown_project, - dropdown_record, dropdown_event) - record = wfdb.rdsamp(record_path, return_res=16) + record = wfdb.rdsamp(event_path, return_res=16) fs = record[1]['fs'] sig_name = record[1]['sig_name'] units = record[1]['units'] @@ -672,7 +667,7 @@ def prepare_graph(self, dropdown_project, dropdown_record, break else: sig_order, n_ekgs = self.order_sigs( - n_ekgs, sig_name, exclude_sigs=exclude_list + sig_name, exclude_list ) all_y_vals = self.format_y_vals( sig_order, sig_name, n_ekgs, record, index_start, diff --git a/waveform-django/waveforms/migrations/0026_user_is_verified.py b/waveform-django/waveforms/migrations/0026_user_is_verified.py new file mode 100644 index 0000000..7300ce4 --- /dev/null +++ b/waveform-django/waveforms/migrations/0026_user_is_verified.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.13 on 2022-11-29 07:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('waveforms', '0025_annotation_is_adjudication'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='is_verified', + field=models.BooleanField(default=False), + ), + ] diff --git a/waveform-django/waveforms/migrations/0027_auto_20221221_1304.py b/waveform-django/waveforms/migrations/0027_auto_20221221_1304.py new file mode 100644 index 0000000..ccc22ec --- /dev/null +++ b/waveform-django/waveforms/migrations/0027_auto_20221221_1304.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.13 on 2022-12-21 18:04 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('waveforms', '0026_user_is_verified'), + ] + + operations = [ + migrations.RenameField( + model_name='user', + old_name='is_verified', + new_name='is_annotator', + ), + ] diff --git a/waveform-django/waveforms/migrations/0028_auto_20221221_1400.py b/waveform-django/waveforms/migrations/0028_auto_20221221_1400.py new file mode 100644 index 0000000..2d875c9 --- /dev/null +++ b/waveform-django/waveforms/migrations/0028_auto_20221221_1400.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2.13 on 2022-12-21 19:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('waveforms', '0027_auto_20221221_1304'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='entrance_score', + field=models.IntegerField(default=0), + ), + migrations.AlterField( + model_name='user', + name='practice_status', + field=models.CharField(choices=[('BG', 'Began'), ('CO', 'Completed'), ('ED', 'Ended')], default='BG', max_length=2), + ), + ] diff --git a/waveform-django/waveforms/migrations/0029_auto_20230110_0153.py b/waveform-django/waveforms/migrations/0029_auto_20230110_0153.py new file mode 100644 index 0000000..ba5d88c --- /dev/null +++ b/waveform-django/waveforms/migrations/0029_auto_20230110_0153.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.13 on 2023-01-10 06:53 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('waveforms', '0028_auto_20221221_1400'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='entrance_score', + field=models.CharField(default='NA', max_length=8), + ), + ] diff --git a/waveform-django/waveforms/migrations/0030_auto_20230117_1323.py b/waveform-django/waveforms/migrations/0030_auto_20230117_1323.py new file mode 100644 index 0000000..c591467 --- /dev/null +++ b/waveform-django/waveforms/migrations/0030_auto_20230117_1323.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.13 on 2023-01-17 18:23 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('waveforms', '0029_auto_20230110_0153'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='entrance_score', + field=models.CharField(default='N/A', max_length=8), + ), + ] diff --git a/waveform-django/waveforms/migrations/0031_auto_20230522_1643.py b/waveform-django/waveforms/migrations/0031_auto_20230522_1643.py new file mode 100644 index 0000000..88d33af --- /dev/null +++ b/waveform-django/waveforms/migrations/0031_auto_20230522_1643.py @@ -0,0 +1,27 @@ +# Generated by Django 2.2.13 on 2023-05-22 20:43 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('waveforms', '0030_auto_20230117_1323'), + ] + + operations = [ + migrations.CreateModel( + name='WaveformEvent', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('project', models.CharField(max_length=50)), + ('record', models.CharField(max_length=50)), + ('event', models.CharField(max_length=50)), + ], + ), + migrations.AddField( + model_name='user', + name='waveforms', + field=models.ManyToManyField(related_name='annotators', to='waveforms.WaveformEvent'), + ), + ] diff --git a/waveform-django/waveforms/migrations/0032_auto_20230522_1706.py b/waveform-django/waveforms/migrations/0032_auto_20230522_1706.py new file mode 100644 index 0000000..01a7423 --- /dev/null +++ b/waveform-django/waveforms/migrations/0032_auto_20230522_1706.py @@ -0,0 +1,28 @@ +# Generated by Django 2.2.13 on 2023-05-22 21:06 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('waveforms', '0031_auto_20230522_1643'), + ] + + operations = [ + migrations.AlterField( + model_name='waveformevent', + name='event', + field=models.CharField(max_length=50, unique=True), + ), + migrations.AlterField( + model_name='waveformevent', + name='project', + field=models.CharField(max_length=50, unique=True), + ), + migrations.AlterField( + model_name='waveformevent', + name='record', + field=models.CharField(max_length=50, unique=True), + ), + ] diff --git a/waveform-django/waveforms/migrations/0033_auto_20230522_1708.py b/waveform-django/waveforms/migrations/0033_auto_20230522_1708.py new file mode 100644 index 0000000..cfc65c9 --- /dev/null +++ b/waveform-django/waveforms/migrations/0033_auto_20230522_1708.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2.13 on 2023-05-22 21:08 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('waveforms', '0032_auto_20230522_1706'), + ] + + operations = [ + migrations.AlterField( + model_name='waveformevent', + name='project', + field=models.CharField(max_length=50), + ), + migrations.AlterField( + model_name='waveformevent', + name='record', + field=models.CharField(max_length=50), + ), + ] diff --git a/waveform-django/waveforms/migrations/0034_auto_20230523_1528.py b/waveform-django/waveforms/migrations/0034_auto_20230523_1528.py new file mode 100644 index 0000000..9282357 --- /dev/null +++ b/waveform-django/waveforms/migrations/0034_auto_20230523_1528.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2.13 on 2023-05-23 19:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('waveforms', '0033_auto_20230522_1708'), + ] + + operations = [ + migrations.AddField( + model_name='waveformevent', + name='path', + field=models.CharField(default='//', max_length=150, unique=True), + ), + migrations.AlterField( + model_name='waveformevent', + name='event', + field=models.CharField(max_length=50), + ), + ] diff --git a/waveform-django/waveforms/migrations/0035_auto_20230523_1600.py b/waveform-django/waveforms/migrations/0035_auto_20230523_1600.py new file mode 100644 index 0000000..6acf59e --- /dev/null +++ b/waveform-django/waveforms/migrations/0035_auto_20230523_1600.py @@ -0,0 +1,21 @@ +# Generated by Django 2.2.13 on 2023-05-23 20:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('waveforms', '0034_auto_20230523_1528'), + ] + + operations = [ + migrations.RemoveField( + model_name='waveformevent', + name='path', + ), + migrations.AddConstraint( + model_name='waveformevent', + constraint=models.UniqueConstraint(fields=('project', 'record', 'event'), name='unique_fields'), + ), + ] diff --git a/waveform-django/waveforms/migrations/0036_annotation_waveform.py b/waveform-django/waveforms/migrations/0036_annotation_waveform.py new file mode 100644 index 0000000..615bc42 --- /dev/null +++ b/waveform-django/waveforms/migrations/0036_annotation_waveform.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.13 on 2023-05-24 19:01 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('waveforms', '0035_auto_20230523_1600'), + ] + + operations = [ + migrations.AddField( + model_name='annotation', + name='waveform', + field=models.ForeignKey(blank=True, default=None, on_delete=django.db.models.deletion.CASCADE, to='waveforms.WaveformEvent'), + ), + ] diff --git a/waveform-django/waveforms/migrations/0037_auto_20230524_1605.py b/waveform-django/waveforms/migrations/0037_auto_20230524_1605.py new file mode 100644 index 0000000..d7764b5 --- /dev/null +++ b/waveform-django/waveforms/migrations/0037_auto_20230524_1605.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.13 on 2023-05-24 20:05 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('waveforms', '0036_annotation_waveform'), + ] + + operations = [ + migrations.AlterField( + model_name='annotation', + name='waveform', + field=models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, to='waveforms.WaveformEvent'), + ), + ] diff --git a/waveform-django/waveforms/migrations/0038_auto_20230524_1644.py b/waveform-django/waveforms/migrations/0038_auto_20230524_1644.py new file mode 100644 index 0000000..3eb4896 --- /dev/null +++ b/waveform-django/waveforms/migrations/0038_auto_20230524_1644.py @@ -0,0 +1,17 @@ +# Generated by Django 2.2.13 on 2023-05-24 20:44 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('waveforms', '0037_auto_20230524_1605'), + ] + + operations = [ + migrations.AlterModelOptions( + name='waveformevent', + options={'ordering': ['project', 'record', 'event']}, + ), + ] diff --git a/waveform-django/waveforms/migrations/0039_waveformevent_is_practice.py b/waveform-django/waveforms/migrations/0039_waveformevent_is_practice.py new file mode 100644 index 0000000..b45ce73 --- /dev/null +++ b/waveform-django/waveforms/migrations/0039_waveformevent_is_practice.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.13 on 2023-05-26 20:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('waveforms', '0038_auto_20230524_1644'), + ] + + operations = [ + migrations.AddField( + model_name='waveformevent', + name='is_practice', + field=models.BooleanField(default=False), + ), + ] diff --git a/waveform-django/waveforms/migrations/0040_auto_20230606_1622.py b/waveform-django/waveforms/migrations/0040_auto_20230606_1622.py new file mode 100644 index 0000000..051e12c --- /dev/null +++ b/waveform-django/waveforms/migrations/0040_auto_20230606_1622.py @@ -0,0 +1,35 @@ +# Generated by Django 2.2.13 on 2023-06-06 20:22 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('waveforms', '0039_waveformevent_is_practice'), + ] + + operations = [ + migrations.AlterModelOptions( + name='annotation', + options={'ordering': ['waveform__project', 'waveform__record', 'waveform__event']}, + ), + migrations.CreateModel( + name='Bookmark', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('comments', models.TextField(default='')), + ('bookmark_date', models.DateTimeField(null=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='waveforms.User')), + ('waveform', models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, to='waveforms.WaveformEvent')), + ], + options={ + 'ordering': ['waveform__project', 'waveform__record', 'waveform__event'], + }, + ), + migrations.AddConstraint( + model_name='bookmark', + constraint=models.UniqueConstraint(fields=('waveform', 'user'), name='unique_fields'), + ), + ] diff --git a/waveform-django/waveforms/migrations/0041_bookmark_is_adjudication.py b/waveform-django/waveforms/migrations/0041_bookmark_is_adjudication.py new file mode 100644 index 0000000..63a77c0 --- /dev/null +++ b/waveform-django/waveforms/migrations/0041_bookmark_is_adjudication.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.13 on 2023-06-12 23:37 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('waveforms', '0040_auto_20230606_1622'), + ] + + operations = [ + migrations.AddField( + model_name='bookmark', + name='is_adjudication', + field=models.BooleanField(default=False, null=True), + ), + ] diff --git a/waveform-django/waveforms/migrations/0042_waveformevent_decision.py b/waveform-django/waveforms/migrations/0042_waveformevent_decision.py new file mode 100644 index 0000000..d8a7db0 --- /dev/null +++ b/waveform-django/waveforms/migrations/0042_waveformevent_decision.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.13 on 2023-06-18 19:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('waveforms', '0041_bookmark_is_adjudication'), + ] + + operations = [ + migrations.AddField( + model_name='waveformevent', + name='decision', + field=models.CharField(blank=True, max_length=50), + ), + ] diff --git a/waveform-django/waveforms/migrations/0043_auto_20230618_1850.py b/waveform-django/waveforms/migrations/0043_auto_20230618_1850.py new file mode 100644 index 0000000..434cace --- /dev/null +++ b/waveform-django/waveforms/migrations/0043_auto_20230618_1850.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.13 on 2023-06-18 22:50 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('waveforms', '0042_waveformevent_decision'), + ] + + operations = [ + migrations.AlterField( + model_name='waveformevent', + name='decision', + field=models.CharField(default='None', max_length=10), + ), + ] diff --git a/waveform-django/waveforms/migrations/0044_waveformevent_date_added.py b/waveform-django/waveforms/migrations/0044_waveformevent_date_added.py new file mode 100644 index 0000000..bb9cda3 --- /dev/null +++ b/waveform-django/waveforms/migrations/0044_waveformevent_date_added.py @@ -0,0 +1,20 @@ +# Generated by Django 2.2.13 on 2023-08-10 19:12 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('waveforms', '0043_auto_20230618_1850'), + ] + + operations = [ + migrations.AddField( + model_name='waveformevent', + name='date_added', + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), + preserve_default=False, + ), + ] diff --git a/waveform-django/waveforms/models.py b/waveform-django/waveforms/models.py index cdaf5e0..1abca84 100644 --- a/waveform-django/waveforms/models.py +++ b/waveform-django/waveforms/models.py @@ -3,11 +3,68 @@ from django.core.validators import EmailValidator from django.db import models +from django.db.models import UniqueConstraint from django.utils import timezone from website.settings import base +def update_decision(waveform): + """ + Update the decision value of a waveform if it has been annotated max times, or has been adjudicated. + """ + decision_set = set() + annotations = waveform.annotation_set.all() + + for annotation in annotations: + if annotation.is_adjudication: + waveform.decision = annotation.decision + waveform.save() + return + decision_set.add(annotation.decision) + + if len(annotations) < base.NUM_ANNOTATORS: + # Waveform is not done being annotated + waveform.decision = 'None' + + elif len(annotations) == base.NUM_ANNOTATORS: + if len(decision_set) == 1: + # Unanimous decision + waveform.decision = decision_set.pop() + else: + # Conflicting decisions + waveform.decision = 'Conflict' + else: + # More annotations than annotators + waveform.decision = 'Error' + waveform.save() + + +class WaveformEvent(models.Model): + """ + Defines the model for each waveform event. Contains info about each event that has been assigned. + """ + project = models.CharField(max_length=50, unique=False, blank=False) + record = models.CharField(max_length=50, unique=False, blank=False) + event = models.CharField(max_length=50, unique=False, blank=False) + is_practice = models.BooleanField(default=False) + decision = models.CharField(max_length=10, unique=False, default='None') + date_added = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return f'{self.project}/{self.record}/{self.event}' + + @property + def path(self): + return f'{self.project}/{self.record}/{self.event}' + + class Meta: + constraints = [ + UniqueConstraint(fields=['project', 'record', 'event'], name='unique_fields') + ] + ordering = ['project', 'record', 'event'] + + class User(models.Model): """ The model for each user on the platform. @@ -18,6 +75,12 @@ class User(models.Model): join_date = models.DateTimeField(auto_now_add=True) is_adjudicator = models.BooleanField(default=False) is_admin = models.BooleanField(default=False) + is_annotator = models.BooleanField(default=False) + waveforms = models.ManyToManyField(WaveformEvent, related_name='annotators') + entrance_score = models.CharField( + max_length=8, + default='N/A', + ) last_login = models.DateTimeField(default=timezone.now) date_assigned = models.DateTimeField(default=timezone.now) # Completion status of the practice test @@ -32,17 +95,14 @@ class User(models.Model): practice_status = models.CharField( max_length=2, choices=practice_modes, - default=ENDED, + default=BEGAN, ) - def num_annotations(self, project=None): + + def num_annotations(self): """ Determine the number of annotations for the current user. - Parameters - ---------- - project : str, optional - The desired project from which to query for user annotations. Returns ------- @@ -50,12 +110,8 @@ def num_annotations(self, project=None): The number of annotations for the current user. """ - if project: - return len(Annotation.objects.filter(user=self, project=project, - is_adjudication=False)) - else: - return len(Annotation.objects.filter(user=self, - is_adjudication=False)) + return Annotation.objects.filter(user=self, is_adjudication=False, waveform__is_practice=False).count() + def new_settings(self): """ @@ -85,7 +141,8 @@ def new_settings(self): diff_settings[field] = [default, user_set] return diff_settings - def events_remaining(self): + + def num_events_remaining(self): """ Return the total number of event remaining from the user's assignment. @@ -99,27 +156,62 @@ def events_remaining(self): The total number of events remaining from the user's assignment. """ - BASE_DIR = base.BASE_DIR - FILE_ROOT = os.path.abspath(os.path.join(BASE_DIR, os.pardir)) - FILE_LOCAL = os.path.join('record-files') - PROJECT_PATH = os.path.join(FILE_ROOT, FILE_LOCAL) - - count = 0 - for project in base.ALL_PROJECTS: - event_list = [] - csv_path = os.path.join(PROJECT_PATH, project, base.ASSIGNMENT_FILE) - with open(csv_path, 'r') as csv_file: - csvreader = csv.reader(csv_file, delimiter=',') - next(csvreader) - for row in csvreader: - if self.username in row[1:]: - event_list.append(row[0]) - complete_events = Annotation.objects.filter( - user=self, project=project, is_adjudication=False).exclude( - decision='Save for Later').values_list('event', flat=True) - event_list = [e for e in event_list if e not in complete_events] - count += len(event_list) - return count + all_projects = [p for p in base.ALL_PROJECTS if p not in base.BLACKLIST] + all_waveforms = WaveformEvent.objects.filter(annotators=self, project__in=all_projects) + remaining_waveforms = all_waveforms.exclude(annotation__user=self) + return remaining_waveforms.count() + + + def get_waveforms(self, annotation = ''): + """ + Return the waveforms for the user based on the type of waveforms requested. + + Parameters + ---------- + type : str + The type of waveforms to return. Options are: + - EMPTY STRING (default) - all waveforms + - unannotated + - saved + - annotated + - adjudicated + """ + + if self.practice_status == 'ED': + all_projects = [p for p in base.ALL_PROJECTS if p not in base.BLACKLIST] + all_waveforms = WaveformEvent.objects.filter(annotators=self, project__in=all_projects, is_practice=False) + else: + all_waveforms = WaveformEvent.objects.filter(is_practice=True) + + if annotation == 'unannotated': + return all_waveforms.exclude(bookmark__user=self).exclude(annotation__user=self) + elif annotation == 'saved': + return all_waveforms.filter(bookmark__user=self) + elif annotation == 'annotated': + return all_waveforms.filter(annotation__user=self) + else: + return all_waveforms + + + def get_annotations(self, saved=False, is_adjudicated=False): + if self.practice_status == 'ED': + all_projects = [p for p in base.ALL_PROJECTS if p not in base.BLACKLIST] + all_annotations = Annotation.objects.filter(user=self, waveform__project__in=all_projects, is_adjudication=is_adjudicated, waveform__is_practice=False) + else: + all_annotations = Annotation.objects.filter(user=self, is_adjudication=is_adjudicated, waveform__is_practice=True) + + return all_annotations + + + def get_bookmarks(self): + if self.practice_status == 'ED': + all_projects = [p for p in base.ALL_PROJECTS if p not in base.BLACKLIST] + all_bookmarks = Bookmark.objects.filter(user=self, waveform__project__in=all_projects, waveform__is_practice=False) + else: + all_bookmarks = Bookmark.objects.filter(user=self, waveform__is_practice=True) + + return all_bookmarks + class InvitedEmails(models.Model): @@ -139,6 +231,7 @@ class Annotation(models.Model): """ user = models.ForeignKey('User', related_name='annotation', on_delete=models.CASCADE) + waveform = models.ForeignKey('WaveformEvent', on_delete=models.CASCADE, default=None) project = models.CharField(max_length=50, blank=False) record = models.CharField(max_length=50, blank=False) event = models.CharField(max_length=50, blank=False) @@ -146,6 +239,9 @@ class Annotation(models.Model): comments = models.TextField(default='') decision_date = models.DateTimeField(null=True, blank=False) is_adjudication = models.BooleanField(default=False, null=True) + + class Meta: + ordering = ['waveform__project', 'waveform__record', 'waveform__event'] def update(self): """ @@ -160,19 +256,104 @@ def update(self): N/A """ - all_annotations = Annotation.objects.filter( - user=self.user, project=self.project, record=self.record, - event=self.event, is_adjudication=False - ) - if all_annotations: - for a in all_annotations: - a.decision = self.decision - a.comments = self.comments - a.decision_date = self.decision_date - a.save(update_fields=['decision', 'comments', - 'decision_date']) - else: + + try: + if self.is_adjudication: + annotation = Annotation.objects.get(waveform=self.waveform, is_adjudication=self.is_adjudication) + annotation.user = self.user + else: + annotation = Annotation.objects.get(user=self.user, waveform=self.waveform, is_adjudication=self.is_adjudication) + annotation.decision = self.decision + annotation.comments = self.comments + annotation.decision_date = self.decision_date + annotation.save(update_fields=['user', 'decision', 'comments', 'decision_date']) + except Annotation.DoesNotExist: + try: + bookmark = Bookmark.objects.get(user=self.user, waveform=self.waveform, is_adjudication=self.is_adjudication) + bookmark.delete() + except Bookmark.DoesNotExist: + pass + self.save() + + update_decision(self.waveform) + + + def delete(self, *args, **kwargs): + """ + Delete the user's annotation. + + Parameters + ---------- + N/A + + Returns + ------- + N/A + + """ + super(Annotation, self).delete(*args, **kwargs) + update_decision(self.waveform) + + +class Bookmark(models.Model): + """ + Defines the object for a bookmarked waveform. + """ + user = models.ForeignKey('User', on_delete=models.CASCADE) + waveform = models.ForeignKey('WaveformEvent', on_delete=models.CASCADE, default=None) + comments = models.TextField(default='') + bookmark_date = models.DateTimeField(null=True, blank=False) + is_adjudication = models.BooleanField(default=False, null=True) + class Meta: + ordering = ['waveform__project', 'waveform__record', 'waveform__event'] + constraints = [ + UniqueConstraint(fields=['waveform', 'user'], name='unique_fields') + ] + + def update(self): + """ + Update the user's bookmark if it exists, else create a new one. + + Parameters + ---------- + N/A + + Returns + ------- + N/A + + """ + try: + bookmark = Bookmark.objects.get(user=self.user, waveform=self.waveform, is_adjudication=self.is_adjudication) + bookmark.comments = self.comments + bookmark.bookmark_date = self.bookmark_date + bookmark.save(update_fields=['comments', 'bookmark_date']) + except Bookmark.DoesNotExist: + try: + annotation = Annotation.objects.get(user=self.user, waveform=self.waveform, is_adjudication=self.is_adjudication) + annotation.delete() + except Annotation.DoesNotExist: + pass self.save() + + update_decision(self.waveform) + + + def delete(self, *args, **kwargs): + """ + Delete the bookmark and update the decision of the waveform. + + Parameters + ---------- + N/A + + Returns + ------- + N/A + + """ + super(Bookmark, self).delete(*args, **kwargs) + update_decision(self.waveform) class UserSettings(models.Model): diff --git a/waveform-django/waveforms/templates/waveforms/adjudications.html b/waveform-django/waveforms/templates/waveforms/adjudications.html index 90dd6af..bf46f53 100644 --- a/waveform-django/waveforms/templates/waveforms/adjudications.html +++ b/waveform-django/waveforms/templates/waveforms/adjudications.html @@ -7,222 +7,218 @@

All Adjudications

Total Complete - {{ all_anns_frac }} -

+ {{ progress }} +

-
-

Search for record

- {% if messages %} -
    - {% for message in messages %} -

    {{ message }}

    - {% endfor %} -
+ + {% if bookmarked.count > 0 %} +
+

Bookmarked Conflicts

+
+ {% if page_info.0 %} + + {% endif %} +
+ {% for waveform in bookmarked%} + {% ifchanged waveform.project waveform.record %} + {% if not forloop.first %} +
+
+ {% endif %} +
+

{{ waveform.project}}: {{ waveform.record }}



+ {% endifchanged %} + +
{{ waveform.event }} (View + waveform  | Delete Bookmark + )
+ + + + {% for cat in categories %} + + {% endfor %} + + + {% for annotation in waveform.annotation_set.all %} + + + + + + {% endfor %} +
+ {{ cat }} +
+ {{ annotation.user.username }} + + {{ annotation.decision}} + + {{ annotation.decision_date }} + + {{ annotation.comments }} +
+
+ {% endfor %} +
+
+ {% endif %} + + {% if conflicts.count > 0 %} +
+

Conflicting Annotations

+
+ {% if page_info.1 %} + -
-

Complete Adjudications

+ {% endif %} +
+ {% for waveform in conflicts%} + {% ifchanged waveform.project waveform.record %} + {% if not forloop.first %}
- {% if complete_page %} - +
{% endif %} - {% for batch in complete_adjudications %} -
- - - - {% for cat in categories %} - - {% endfor %} - - - - - - {% for val in batch %} - - - {% if forloop.last %} - {% for v in val|slice:"2:" %} - - {% endfor %} - {% else %} - {% for v in val|slice:"2:" %} - - {% endfor %} - {% endif %} - +
+

{{ waveform.project}}: {{ waveform.record }}



+ {% endifchanged %} + +
{{ waveform.event }} (View + waveform )
+ +
- {{ cat }} - - Edit adjudication - - Delete adjudication -
- {{ v }} - - {{ v }} -
+ + {% for cat in categories %} + {% endfor %} -
+ {{ cat }} +
-
-
- {% endfor %} -
-
-

Incomplete Adjudications

+ + + {% for annotation in waveform.annotation_set.all %} + + + {{ annotation.user.username }} + + + {{ annotation.decision}} + + + {{ annotation.decision_date }} + + + {{ annotation.comments }} + + + {% endfor %} + +
+ {% endfor %} +
+
+ {% endif %} + {% if adjudicated.count > 0 %} +
+

Adjudicated Waveforms

+
+ {% if page_info.2 %} + + {% endif %} +
+ {% for waveform in adjudicated%} + {% ifchanged waveform.project waveform.record %} + {% if not forloop.first %}
- {% if incomplete_page %} - +
{% endif %} - {% for batch in incomplete_adjudications %} -
- - - - {% for cat in categories %} - - {% endfor %} - - - - {% for val in batch %} - - {% for v in val|slice:"2:" %} - - {% endfor %} - +
+

{{ waveform.project}}: {{ waveform.record }}



+ {% endifchanged %} + +
{{ waveform.event }} (View + waveform  | Delete Adjudication + )
+ +
- {{ cat }} - - Edit adjudication -
- {{ v }} -
+ + {% for cat in categories %} + {% endfor %} -
+ {{ cat }} +
-
-
- {% endfor %} -
-
+ -{% endblock %} + {% for annotation in waveform.annotation_set.all %} + + + {{ annotation.user.username }} + + + {{ annotation.decision}} + + + {{ annotation.decision_date }} + + + {{ annotation.comments }} + + + {% endfor %} + +
+ {% endfor %} +
+ {% endif %} +
+ +{% endblock %} \ No newline at end of file diff --git a/waveform-django/waveforms/templates/waveforms/adjudicator_console.html b/waveform-django/waveforms/templates/waveforms/adjudicator_console.html deleted file mode 100644 index e9c0e15..0000000 --- a/waveform-django/waveforms/templates/waveforms/adjudicator_console.html +++ /dev/null @@ -1,66 +0,0 @@ -{% extends "base.html" %} -{% load static %} -{% load plotly_dash %} -{% block content %} - - - -
-

Adjudicator Console

- - - - -
- {% plotly_app_bootstrap name='waveform_graph_adjudicate' initial_arguments=dash_context %} -
-
- - -{% endblock %} diff --git a/waveform-django/waveforms/templates/waveforms/admin_console.html b/waveform-django/waveforms/templates/waveforms/admin_console.html index 7b52a09..b253c5e 100644 --- a/waveform-django/waveforms/templates/waveforms/admin_console.html +++ b/waveform-django/waveforms/templates/waveforms/admin_console.html @@ -14,79 +14,79 @@

Admin Console

All Users

{% if messages %} - {% for message in messages %} - {% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %} -
- {% elif message.level == DEFAULT_MESSAGE_LEVELS.INFO %} -
+ {% for message in messages %} + {% if message.level == DEFAULT_MESSAGE_LEVELS.ERROR %} +
+ {% elif message.level == DEFAULT_MESSAGE_LEVELS.INFO %} +
{% else %} -
- {% endif %} +
+ {% endif %} {{ message|safe }}
- {% endfor %} - {% endif %} -

Invite a new user by email address

- + {% endif %} -{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/waveform-django/waveforms/templates/waveforms/annotations.html b/waveform-django/waveforms/templates/waveforms/annotations.html index c4c365c..9a28ac3 100644 --- a/waveform-django/waveforms/templates/waveforms/annotations.html +++ b/waveform-django/waveforms/templates/waveforms/annotations.html @@ -3,335 +3,410 @@ {% block content %}
-

All {%if user.practice_status != "ED"%} Practice {% endif %} Annotations

+ {% if user.practice_status == "ED" %} +

My Annotations

+ {% else %} + {% if user.is_annotator == False%} +

Assessment Samples

+ {% else %} +

Practice Samples

+ {% endif %} + {% endif %} -
- Total Complete - {{ all_anns_frac }} -


- {% if messages %} +
+ Total Complete + {{ progress }} +


+ {% if messages %}
    - {% for message in messages %} -

    {{ message }}

    - {% endfor %} + {% for message in messages %} +

    {{ message }}

    + {% endfor %}
{% endif %} -
- {%if user.practice_status == "ED"%} - {% if finished_assignment%} + +
+ {%if user.practice_status == "ED"%} + {% if user.num_events_remaining == 0 %}
- {% csrf_token %} -

Annotate More Events

- - -
- + {% csrf_token %} +

Annotate More Events

+ + +
+
- {% else %} + {% else %} {% if save_warning %} - + {% endif %} {% endif %} - {% endif %} + {% endif %} +
+

+ {%if user.practice_status == "ED"%} + Current Assignment + {% elif user.is_annotator == False %} + Assessment + {% else %} + Practice Mode + {%endif%} +

+ + {% if user.is_annotator == False and user.practice_status == "CO"%} +

Assessment has been submitted and is under review

+
+ {% else%} + {%if user.practice_status != "ED"%} + {% if user.is_annotator == True %} + Go to the "Practice Info" page to read the instructions for the practice test, and + when + you are ready to submit your responses + {% else %} + Go to the Assessment Info page to read the + instructions for this assessment. When you are ready to submit your responses, press "Submit Assessment". +
+
+
+ +
{% endif %}
-

{%if user.practice_status == "ED"%}Current Assignment{% else %} Practice Mode {%endif%}

- {%if user.practice_status != "ED"%} - Go to the "Submit Practice" page when you are ready to submit your responses and view the answers -
-
+
{% endif %} Remaining Events: - {{ remaining }} + {{ user.num_events_remaining }}

-
-
+
+

Search for record

{% if messages %} -
    +
      {% for message in messages %} -

      {{ message }}

      +

      {{ message }}

      {% endfor %} -
    +
{% endif %} -
- -

+ + +

{% if search %} - {% for rec,info in search.items %} - -
- - + {% for rec,info in search.items %} + +
+
- {% for cat in categories %} + {% for cat in categories %} - {% endfor %} + {% endfor %} {% for val in info|slice:"2:" %} - + {% for v in val %} - + {% endfor %} {% if val.3 != "-" %} - + {% endif %} - + {% endfor %} -
- {{ cat }} + {{ cat }}
+ {{ v }} - - Edit annotation + Edit annotation + Delete annotation -
-
-
- {% endfor %} - {% endif %} -
-
-

Save for Later Annotations

-
- {% if saved_page %} - - {% endif %} - {% for rec,info in saved_anns.items %} - -
- - +
+ {% endfor %} + {% endif %} + + +{% if saved_waveforms.count > 0 %} +
+

Bookmarked Waveforms

+
+{% if page_info.0 %} + +{% endif %} +{% for bookmark in saved_waveforms %} +{% ifchanged bookmark.waveform.project bookmark.waveform.record%} +{% if not forloop.first %} +
+
+{% endif %} + +
+ - {% for cat in categories %} + {% for cat in categories %} - {% endfor %} - - - {% for val in info|slice:"2:" %} - - {% for v in val %} - {% endfor %} + + {% endifchanged %} + - - {% if val.3 != "-" %} - - {% endif %} - + + + + + + {% endfor %} -
- {{ cat }} + {{ cat }}
- {{ v }} -
- Edit annotation + {{ bookmark.waveform.event }} - Delete annotation -
+ Bookmarked + + {{bookmark.comments}} + + {{bookmark.decision_date}} + + Return + to Event + + Delete + Bookmark +
-
- {% endfor %} -
-
-

Complete Annotations

-
- {% if complete_page %} - - {% endif %} - {% for rec,info in completed_anns.items %} - -
- - +
+
+
+{% endif %} + + +{% if annotated_waveforms.count > 0 %} +
+

Annotated Waveforms

+
+{% if page_info.1 %} + +{% endif %} +{% for annotation in annotated_waveforms %} +{% ifchanged annotation.waveform.project annotation.waveform.record%} +{% if not forloop.first %} + +
+{% endif %} + +
+ - {% for cat in categories %} + {% for cat in categories %} - {% endfor %} - - - {% for val in info|slice:"2:" %} - - {% for v in val %} - {% endfor %} + + {% endifchanged %} + - - {% if val.3 != "-" %} - - {% endif %} - + + + + + + {% endfor %} -
- {{ cat }} + {{ cat }}
- {{ v }} -
- Edit annotation + {{ annotation.waveform.event }} - Delete annotation -
+ {{ annotation.decision}} + + {{ annotation.comments}} + + {{ annotation.decision_date}} + + Edit + annotation + + Delete + annotation +
-
- {% endfor %} -
-
-

Incomplete Annotations

-
- {% if incomplete_page %} - - {% endif %} - {% for rec,info in incompleted_anns.items %} - -
- - +
+
+
+{% endif %} + +{% if unannotated_waveforms.count > 0 %} +
+

Unannotated Waveforms

+
+{% if page_info.2 %} + +{% endif %} +{% for waveform in unannotated_waveforms %} +{% ifchanged waveform.project waveform.record%} +{% if not forloop.first %} + +
+{% endif %} + +
+ - {% for cat in categories %} + {% for cat in categories %} - {% endfor %} + {% endfor %} - - {% for val in info|slice:"2:" %} - - {% for v in val %} - + {% endifchanged %} + + + {% for _ in "123" %} + {% endfor %} - - {% if val.3 != "-" %} - - {% endif %} - + {% endfor %} -
- {{ cat }} + {{ cat }}
- {{ v }} -
+ {{ waveform.event }} + + None + - Edit annotation + Edit + annotation - Delete annotation -
-
- {% endfor %} + +
+

+{% endif %} +{% endif %} + -{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/waveform-django/waveforms/templates/waveforms/assessment_info.html b/waveform-django/waveforms/templates/waveforms/assessment_info.html new file mode 100644 index 0000000..62e0734 --- /dev/null +++ b/waveform-django/waveforms/templates/waveforms/assessment_info.html @@ -0,0 +1,66 @@ +{% extends "base.html" %} +{% load static %} + +{% block content %} +
+ +

Entry Assessment

+ +
+

Instructions

+ +
    +
  • You have been assigned a set of {{ total }} waveform events to annotate, which can be accessed on the View All Assessment Samples page.
  • +
  • This assessment will not be timed, but the staff will see the results of this assessment.
  • +
  • You may return to this page at any time.
  • +
  • When you are ready to submit your exam, simply press the "Submit Assessment" button below. Your responses will be sent to the administrators.
  • +
  • You cannot change your answers once you submit.
  • +
+
+ +
+ {% if user.practice_status == "CO" %} + {% if user.is_annotator == True %} +

Results

+ {% else %} +

Responses

+ {% endif %} + {% for project,events in results.items %} + {% for event,responses in events.items %} +

+ {{ project }} {{ event }}  Your answer: {{ responses.1 }} + {% if user.is_annotator %} +  Correct answer: {{ responses.0 }}  + {% if responses.0 == responses.1 %} ✔️ {% else %} ❌ {% endif %} + {% endif %} +

+ {% endfor %} + {% endfor %} + {% if user.is_annotator %} +

Score: {{ correct }}/{{ total }}

+ {% else %} + Score is hidden until the staff review your responses + {% endif %} + {% endif %} +
+
+
+ {% csrf_token %} + {% if user.practice_status == "BG" %} + {% if user.is_annotator == True %} + + {% else %} + + {% endif %} + {% endif %} + {% if user.practice_status == "ED" %} + + {% else %} + {% if user.is_annotator == True%} + + {% endif %} + {% endif %} +
+
+
+{% endblock %} diff --git a/waveform-django/waveforms/templates/waveforms/assessment_result.html b/waveform-django/waveforms/templates/waveforms/assessment_result.html new file mode 100755 index 0000000..b9234a5 --- /dev/null +++ b/waveform-django/waveforms/templates/waveforms/assessment_result.html @@ -0,0 +1,49 @@ +{% extends "base.html" %} +{% load static %} +{% block content %} +
+ +

Assessment Results

+
+

Username: {{annotator.username}}

+
+
Responses:
+ + + + + + + + {% for project,events in results.items %} + {% for event,responses in events.items %} + + + + + + + {% endfor %} + {% endfor %} +
ProjectEventCorrect LabelResponse
{{ project }}{{ event }}{{ responses.1 }}{{ responses.2 }} {% if responses.1 == responses.2 %} ✔️ {% else %} ❌ {% endif %}
+
+
Total Score: {{ correct }} / {{ total }}
+
+ {% if annotator.is_annotator %} +
+ +
+ {% else %} +
+ +
+ {% endif %} +
+ +{% endblock %} diff --git a/waveform-django/waveforms/templates/waveforms/overview.html b/waveform-django/waveforms/templates/waveforms/overview.html new file mode 100755 index 0000000..16d3c96 --- /dev/null +++ b/waveform-django/waveforms/templates/waveforms/overview.html @@ -0,0 +1,79 @@ +{% extends "base.html" %} +{% load static %} + +{% block content %} +
+ +

Project Overview

+
+
Thank you for offering to volunteer your time and expertise to help us build our database!
+
+

Objective

+
+

+ Our goal is to build a large dataset of patient-monitoring data, where ECG samples that show signs of Ventricular Tachycardia (VT) have been classified by experts. +

    +
  • Several commercial heart-monitor companies have provided us with data captured from patient monitors
  • +
  • This data represents instances where the vendor's VT detection algorithm was triggered
  • +
  • Commercial VT detection algorithms are often unreliable, as they are easily triggered without the occurance of a VT event
  • +
  • Hospital staff often have to deal with false-positive alarms, which can desensitize them to the alarms
  • +
      +
    • Desensitization may lead to delayed responses when a patient is suffering from a true VT event, which can significantly impact their chances of receiving appropriate care in a timely fashion
    • +
    • The overabundance of alarms has also shown to increase the stress experienced by patients and staff in the ICU
    • +
    +
  • There is a great need to improve the VT detection abilities of bedside monitors
  • +
  • We aim to employ a Machine-Learning approach to developing a VT detection algorithm
  • +
      +
    • To begin this process, we first need a database to train and test our ML model on
    • +
    • Each data sample must be labeled based on the presence of a VT event by experts
    • +
    • This platform allows experts to view and label data samples in an effort to build our database
    • +
    +
+

+
+
+

Getting Started

+
+

Initial Assessment

+

+ Before you can begin annotating events, we require all participants to take an evaluation on their ability to accurately identify VT. +

    +
  • We have created an assessment that consists of a set of events that our experts have already labeled
  • +
  • New members are asked to view these events and label them to the best of their abilities
  • +
  • These labels will be compared to the correct labels, and the assessment will be scored for accuracy
  • +
  • After submitting the assessment, your results will be sent to our team
  • +
      +
    • If your score is approved by our team, you will be given access to our unlabeled data, and you may proceed to assign yourself events to annotate
    • + {% if user.is_annotator %} +
    • Our records indicate that you have completed this assessment, and have been approved to annotate new events
    • + {% else %} + {% if user.practice_status == "CO" %} +
    • Our records indicate that you have completed this assessment and are awaiting evaluation by our team. Please check back at a later time!
    • + {% else %} +
    • Our records indicate that you have not completed this assessment. Please go to the Assessment Info page to get started.
    • + {% endif %} + {% endif %} +
    +
+

+
+
+

Annotating Events

+

+

    +
  • To begin annotating events, you must first assign yourself a set of events you plan to annotate
  • + +
  • You may begin annotating events by going to the Create Annotation page
  • +
  • The Current Assignment page will show you all of your assigned events
  • +
      +
    • This includes annotated events, unannotated events, and events that you have saved for later
    • +
    +
+

+
+ +
+ +{% endblock %} diff --git a/waveform-django/waveforms/templates/waveforms/practice.html b/waveform-django/waveforms/templates/waveforms/practice.html index 67228b3..c5eb45a 100755 --- a/waveform-django/waveforms/templates/waveforms/practice.html +++ b/waveform-django/waveforms/templates/waveforms/practice.html @@ -3,41 +3,77 @@ {% block content %}
-

Answer practice questions

+ + {% if user.is_annotator == False%} +

Entry Assessment

+ {% else %} +

Answer practice questions

+ {% endif %}

Instructions

-
-
    -
  • You will be assigned a set of {{ total }} waveform events to complete.
  • -
  • You can come return to this page whenever you are ready to view the answers.
  • -
  • This assessment will not be timed or graded, and may be exited at any time by pressing the "End Practice" button below.
  • -
  • When you wish to continue working on the main dataset, simply press the "End Practice" button below. You will resume working on your active assignment.
  • -
  • You may take the practice test as many times as you wish, but your score will be lost after you end the practice session.
  • + + {% if user.is_annotator == False%} +
      +
    • You have been assigned a set of {{ total }} waveform events to annotate, which can be accessed on the "View All Exam Samples" Page.
    • +
    • This assessment will not be timed, but the staff will see the results of this assessment.
    • +
    • You may return to this page at any time.
    • +
    • When you are ready to submit your exam, simply press the "Submit Assessment" button below. Your responses will be sent to the administrators.
    • +
    • You cannot change your answers once you submit.
    • +
    +
    + {% else %} +
      +
    • You will be assigned a set of {{ total }} waveform events to complete.
    • +
    • This assessment will not be timed or graded, and may be exited at any time by pressing the "End Practice" button below.
    • +
    • You may return to this page whenever you are ready to view the answers.
    • +
    • When you wish to continue working on the main dataset, simply press the "End Practice" button below. You will resume working on your active assignment.
    • +
    • You may take the practice test as many times as you wish, but your score will be lost after you end the practice session.
    + {% endif %} + +
    + + + {% if user.practice_status == "CO" %} -

    Results

    - {% for project,events in results.items %} - {% for event,responses in events.items %} -

    - {{ project }} {{ event }}  Correct answer: {{ responses.0 }}  Your answer: {{ responses.1 }} - {% if responses.0 == responses.1 %} ✔️ {% else %} ❌ {% endif %} -

    - {% endfor %} + {% if user.is_annotator == True %} +

    Results

    + {% else %} +

    Responses

    + {% endif %} + {% for response in results %} +

    + {{ response.0 }} | {{ response.1 }} | {{ response.2 }}  Your answer: {{ response.4 }} + {% if user.is_annotator %} +  Correct answer: {{ response.3 }}  + {% if response.3 == response.4 %} ✔️ {% else %} ❌ {% endif %} + {% endif %} +

    {% endfor %} + {% if user.is_annotator %}

    Score: {{ correct }}/{{ total }}

    + {% else %} + Score is hidden until the staff review your responses + {% endif %} {% endif %}
    {% csrf_token %} {% if user.practice_status == "BG" %} - + {% if user.is_annotator == True %} + + {% else %} + + {% endif %} {% endif %} {% if user.practice_status == "ED" %} {% else %} - + {% if user.is_annotator == True%} + + {% endif %} {% endif %}
    diff --git a/waveform-django/waveforms/templates/waveforms/tutorial.html b/waveform-django/waveforms/templates/waveforms/tutorial.html index 15f505a..55d85ad 100644 --- a/waveform-django/waveforms/templates/waveforms/tutorial.html +++ b/waveform-django/waveforms/templates/waveforms/tutorial.html @@ -7,28 +7,28 @@

    Basic Usage Instructions

    • After logging in, you should see your username with the option to logout in the top left corner.
    • -
    • Below that, you should see links to view your current annotations, view a brief tutorial explaining the layout of the annotator (this page), and a way to change the settings / preferences of your annotator (including grid color, time before and after an event, the level of downsampling, etc.).
    • +
    • Below that, you should see links to view your current annotations, view a brief tutorial explaining the layout of the annotator (this page), and a way to change the settings / preferences of your annotator (including grid color, time before and after an event, the level of downsampling, etc.).
    • To begin annotating, go to the annotator home and scroll down to view the entire annotator.
    • On the top left side, you should see the name of the current annotation record, event, and label (It has been filtered so only VTach and VFib/VFlutter events are displayed. Please let us know if you see otherwise!).
    • Directly below this is where you can enter your decision for the label (True, False, Uncertain, Reject, or Save for Later) as well as your additional comments explaining your decision (this field is not necessary but may prove useful later on for either self-reference or research purposes).
    • You can submit the annotation with the "Submit" button, or go to the next or previous annotation without saving using the arrows. Submitting the annotation will automatically send you to the next event.
    • If you hover over the signals, you will be able to see its value and how it aligns with the other signals.
    • Also, you can click and drag the signal in any direction to view more of it.
    • -
    • At any point, you can view all of your previous annotations with the "View current annotations" link and either edit or delete them if needed. You will also be able to see your progress on the annotations using the fractions on the far right side. Also note that if you reload the annotator home page at any point, you will be forwarded to your first non-annotated event.
    • +
    • At any point, you can view all of your previous annotations with the "View current annotations" link and either edit or delete them if needed. You will also be able to see your progress on the annotations using the fractions on the far right side. Also note that if you reload the annotator home page at any point, you will be forwarded to your first non-annotated event.
    • You can also adjust the settings for the annotator using the "Change annotator settings" link; for example, you can reduce the downsampling to view more detail on the EKG signals if needed.

    Getting Events To Annotate

    -

    Taking The Practice Test

    -