├── README.md ├── data ├── README.md └── outlier_count_data.csv ├── notebooks ├── README.md ├── clustering_tutorial_clean.ipynb ├── getting_provider_counts.ipynb └── hyperparameter_testing.ipynb └── src ├── README.md ├── anomaly_tools.py ├── config.yaml ├── database_tools.py ├── fh_config.py ├── flask_app_java.py ├── plotting_tools.py ├── static ├── css │ ├── bootstrap-grid.css │ ├── bootstrap-grid.css.map │ ├── bootstrap-grid.min.css │ ├── bootstrap-grid.min.css.map │ ├── bootstrap-reboot.css │ ├── bootstrap-reboot.css.map │ ├── bootstrap-reboot.min.css │ ├── bootstrap-reboot.min.css.map │ ├── bootstrap.css │ ├── bootstrap.css.map │ ├── bootstrap.min.css │ ├── bootstrap.min.css.map │ ├── sb-admin.css │ └── sb-admin.min.css ├── js │ ├── bootstrap.js │ ├── bootstrap.min.js │ ├── sb-admin-charts.js │ ├── sb-admin-charts.js.old │ ├── sb-admin-charts.min.js │ ├── sb-admin-datatables.js │ ├── sb-admin-datatables.min.js │ ├── sb-admin.js │ └── sb-admin.min.js └── vendor │ ├── bootstrap │ ├── css │ │ ├── bootstrap-grid.css │ │ ├── bootstrap-grid.min.css │ │ ├── bootstrap-reboot.css │ │ ├── bootstrap-reboot.min.css │ │ ├── bootstrap.css │ │ └── bootstrap.min.css │ └── js │ │ ├── bootstrap.js │ │ └── bootstrap.min.js │ ├── chart.js │ ├── Chart.bundle.js │ ├── Chart.bundle.min.js │ ├── Chart.js │ └── Chart.min.js │ ├── datatables │ ├── dataTables.bootstrap4.css │ ├── dataTables.bootstrap4.js │ └── jquery.dataTables.js │ ├── font-awesome │ ├── css │ │ ├── font-awesome.css │ │ └── font-awesome.min.css │ ├── fonts │ │ ├── FontAwesome.otf │ │ ├── fontawesome-webfont.eot │ │ ├── fontawesome-webfont.svg │ │ ├── fontawesome-webfont.ttf │ │ ├── fontawesome-webfont.woff │ │ └── fontawesome-webfont.woff2 │ ├── less │ │ ├── animated.less │ │ ├── bordered-pulled.less │ │ ├── core.less │ │ ├── fixed-width.less │ │ ├── font-awesome.less │ │ ├── icons.less │ │ ├── larger.less │ │ ├── list.less │ │ ├── mixins.less │ │ ├── path.less │ │ ├── rotated-flipped.less │ │ ├── screen-reader.less │ │ ├── stacked.less │ │ └── variables.less │ └── scss │ │ ├── _animated.scss │ │ ├── _bordered-pulled.scss │ │ ├── _core.scss │ │ ├── _fixed-width.scss │ │ ├── _icons.scss │ │ ├── _larger.scss │ │ ├── _list.scss │ │ ├── _mixins.scss │ │ ├── _path.scss │ │ ├── _rotated-flipped.scss │ │ ├── _screen-reader.scss │ │ ├── _stacked.scss │ │ ├── _variables.scss │ │ └── font-awesome.scss │ ├── jquery-easing │ ├── jquery.easing.compatibility.js │ ├── jquery.easing.js │ └── jquery.easing.min.js │ ├── jquery │ ├── Readme.markdown │ ├── color.jquery.js │ ├── jquery.js │ ├── jquery.min.js │ ├── jquery.usmap.js │ ├── lib │ │ └── raphael.js │ └── svg │ │ ├── Blank_USA,_w_territories.svg │ │ └── Blank_US_Map.svg │ └── popper │ ├── popper.js │ └── popper.min.js └── templates ├── charts_internal.html ├── css ├── sb-admin.css └── sb-admin.min.css ├── index.html ├── index_bootstrap_orig.html ├── index_old.html ├── index_v2.html ├── js ├── sb-admin-charts.js ├── sb-admin-charts.js.old ├── sb-admin-charts.min.js ├── sb-admin-datatables.js ├── sb-admin-datatables.min.js ├── sb-admin.js └── sb-admin.min.js ├── layout.html ├── navbar.html ├── output.html ├── pug ├── blank.pug ├── cards.pug ├── charts.pug ├── forgot-password.pug ├── includes │ ├── css │ │ ├── core.pug │ │ └── custom.pug │ ├── footer.pug │ ├── js │ │ ├── core.pug │ │ └── custom.pug │ ├── modals │ │ └── logout.pug │ ├── navbar.pug │ └── scroll-to-top.pug ├── index.pug ├── login.pug ├── navbar.pug ├── register.pug └── tables.pug ├── scss ├── _cards.scss ├── _footer.scss ├── _global.scss ├── _login.scss ├── _mixins.scss ├── _utilities.scss ├── _variables.scss ├── navbar │ ├── _navbar_colors.scss │ ├── _navbar_fixed.scss │ ├── _navbar_global.scss │ ├── _navbar_static.scss │ └── _navbar_toggle.scss └── sb-admin.scss ├── slides.html ├── starter-template.css ├── tables.html ├── tmp_what_was_rendered.html └── vendor ├── bootstrap ├── css │ ├── bootstrap-grid.css │ ├── bootstrap-grid.min.css │ ├── bootstrap-reboot.css │ ├── bootstrap-reboot.min.css │ ├── bootstrap.css │ └── bootstrap.min.css └── js │ ├── bootstrap.js │ └── bootstrap.min.js ├── chart.js ├── Chart.bundle.js ├── Chart.bundle.min.js ├── Chart.js └── Chart.min.js ├── datatables ├── dataTables.bootstrap4.css ├── dataTables.bootstrap4.js └── jquery.dataTables.js ├── font-awesome ├── css │ ├── font-awesome.css │ └── font-awesome.min.css ├── fonts │ ├── FontAwesome.otf │ ├── fontawesome-webfont.eot │ ├── fontawesome-webfont.svg │ ├── fontawesome-webfont.ttf │ ├── fontawesome-webfont.woff │ └── fontawesome-webfont.woff2 ├── less │ ├── animated.less │ ├── bordered-pulled.less │ ├── core.less │ ├── fixed-width.less │ ├── font-awesome.less │ ├── icons.less │ ├── larger.less │ ├── list.less │ ├── mixins.less │ ├── path.less │ ├── rotated-flipped.less │ ├── screen-reader.less │ ├── stacked.less │ └── variables.less └── scss │ ├── _animated.scss │ ├── _bordered-pulled.scss │ ├── _core.scss │ ├── _fixed-width.scss │ ├── _icons.scss │ ├── _larger.scss │ ├── _list.scss │ ├── _mixins.scss │ ├── _path.scss │ ├── _rotated-flipped.scss │ ├── _screen-reader.scss │ ├── _stacked.scss │ ├── _variables.scss │ └── font-awesome.scss ├── jquery-easing ├── jquery.easing.compatibility.js ├── jquery.easing.js └── jquery.easing.min.js ├── jquery ├── jquery.js └── jquery.min.js └── popper ├── popper.js └── popper.min.js /README.md: -------------------------------------------------------------------------------- 1 | # FraudHacker 2 | FraudHacker is an anomaly detection system for Medicare insurance claims data. I built FraudHacker using Python3 along with various scientific computing and machine learning packages (numpy, scikit-learn, and many others). For more background about _why_ I built FraudHacker, please see [my blog post on the subject](http://www.fraudhacker.site/about). I will focus on the technical details here. 3 | 4 | ## Structure 5 | * `data/`: Contains a CSV file displaying the outlier count data generated by the anomaly labeling engine. 6 | * `notebooks/`: Jupyter notebooks demonstrating various aspects of FraudHacker's workflow, including the outlier detection, physician ranking, and hyperparameter sweeping. 7 | * `src/`: The actual source code for FraudHacker and the Flask app that displays its results to users. 8 | 9 | Each directory has its own README file with more information. 10 | 11 | ## Overview 12 | FraudHacker ultimately utilizes clustering to perform outlier detection on Medicare claims data from [the Center for Medicare and Medicaid Services](https://www.cms.gov/) (CMS). Each record contains aggregated information about one type of procedure (for example, a blood draw) performed by one physician. This data was downloaded in CSV format and loaded directly into a PostgreSQL database, which is the starting point of FraudHacker's interaction with the data. FraudHacker extracts numerical values from this database and uses these to perform clustering on the data for all of the physicians of a particular specialty (e.g. Neurology) in a particular state. The number of fraudulent procedures associated with each physician is tallied and output into a second database. The tallying could, in principle, be done on fly by operating directly on the PostgreSQL database containing the CMS data, but is much faster to pre-run the model and access the results. The outlier counts for each physician are then displayed to the user using the FraudHacker dashboard, which runs as a Javascript-driven Flask app. I currently have a copy of FraudHacker running on an AWS EC2 instance. It can be found at [http://www.fraudhacker.site](http://www.fraudhacker.site). 13 | 14 | ## Workflow 15 | A reader class, PandasDBReader (implemented in [database_tools.py](https://github.com/dchannah/fraudhacker/blob/master/src/database_tools.py)), reads the data from the PostgreSQL database (whose info is specified in an [external YAML file](https://github.com/dchannah/fraudhacker/blob/master/src/config.yaml)) and loads it into a Pandas DataFrame. Then, this dataframe is ingested by an AnomalyDetector sub-class (depending on the desired algorithm; these are implemented in [anomaly_tools.py](https://github.com/dchannah/fraudhacker/blob/master/src/anomaly_tools.py)). The AnomalyDetector performs the actual clustering and outlier labeling, produces an outlier score for each record. A threshold on the outlier scores is used to formally label certain records as outliers. The AnomalyDetector class also adds up the outlier counts for each physician. 16 | 17 | The next step is currently done semi-manually (this could be improved in the future). I export the outlier counts for each physician to a CSV file (an example of what this data looks like can be found in the data folder). The outlier count data is in turn imported to another PostgreSQL database, which is ultimately directly read by the Flask app. This accomplished via another class, the OutlierCountDBReader (also implemented into [database_tools.py](https://github.com/dchannah/fraudhacker/blob/master/src/database_tools.py)). The OutlierCountDBReader produces the values that are ultimately displayed in the Flask app. 18 | -------------------------------------------------------------------------------- /data/README.md: -------------------------------------------------------------------------------- 1 | # Sample data for FraudHacker 2 | * outlier_count_data.csv: A sample output of outlier counts and Medicare cost calculation for each physician across a variety of states and specialties. This is the data that is produced by the outlier labeling and ranking engine implemented in FraudHacker. This data is ultimately loaded into a PostgreSQL database on an AWS EC2 instance and displayed to the user via a Flask app. 3 | -------------------------------------------------------------------------------- /notebooks/README.md: -------------------------------------------------------------------------------- 1 | # Jupyter notebooks for FraudHacker 2 | * `clustering_tutorial_clean.ipynb`: A notebook that walks through the process of accessing the PostgreSQL database containing the raw CMS.gov data and the labeling of outliers in this data set using the HDBSCAN clustering algorithm. 3 | * `getting_provider_counts.ipynb`: A notebook that steps through the process of converting labeled outlier to per-physician outlier counts. This notebook ultimately dumps to a CSV file - a sample output of this CSV dumping procedure can be found in the [data folder](https://github.com/dchannah/fraudhacker/tree/master/data). 4 | * `hyperparameter_testing.ipynb`: Here I examine the stability of physician rankings with respect to the two hyperparameters in the model - the minimum cluster size and the threshold for outlier detection. 5 | -------------------------------------------------------------------------------- /notebooks/getting_provider_counts.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Getting outlier counts for all providers in the database. #\n", 8 | "This notebook will iterate through all of the providers in the database and append their outlier counts to a CSV file, which can then be copied into the PostgreSQL cms_complete database. We could do that directly through pandas, but empirically it seems to be a lot slower (for whatever reason), so I'm just writing the CSV and doing the copying from inside PSQL later. Here are some imports and utility functions:" 9 | ] 10 | }, 11 | { 12 | "cell_type": "code", 13 | "execution_count": 3, 14 | "metadata": {}, 15 | "outputs": [], 16 | "source": [ 17 | "# Imports\n", 18 | "import pandas as pd\n", 19 | "\n", 20 | "# Need the source files for FraudHacker.\n", 21 | "import sys\n", 22 | "sys.path.append('/home/dan/PycharmProjects/fraudhacker/src')\n", 23 | "\n", 24 | "from anomaly_tools import HDBAnomalyDetector\n", 25 | "from database_tools import PandasDBReader\n", 26 | "from fh_config import regression_vars, response_var" 27 | ] 28 | }, 29 | { 30 | "cell_type": "code", 31 | "execution_count": 3, 32 | "metadata": { 33 | "collapsed": true 34 | }, 35 | "outputs": [], 36 | "source": [ 37 | "# Some routines to make it a bit simpler.\n", 38 | "def get_df_with_counts(states, specialty):\n", 39 | " pdb_reader = PandasDBReader(\"./config.yaml\", states, specialty)\n", 40 | " hdb = HDBAnomalyDetector(regression_vars, response_var, pdb_reader.d_f, use_response_var=True)\n", 41 | " hdb.get_outlier_scores(min_size=15)\n", 42 | " return hdb.get_most_frequent()\n", 43 | "\n", 44 | "def build_new_table_data(states, specialty):\n", 45 | " counted_df = get_df_with_counts(states, specialty)\n", 46 | " new_table_data = {\n", 47 | " 'state': [ginfo['state'] for ginfo in counted_df['address'].values],\n", 48 | " 'lastname': counted_df['last_name'],\n", 49 | " 'provider_type': [specialty[0] for i in range(len(list(counted_df.index)))],\n", 50 | " 'outlier_count': counted_df['outlier_count'],\n", 51 | " 'outlier_rate': counted_df['outlier_count_rate'],\n", 52 | " 'cost': counted_df['cost_to_medicare'],\n", 53 | " }\n", 54 | " return pd.DataFrame(data=new_table_data, index=list(counted_df.index))" 55 | ] 56 | }, 57 | { 58 | "cell_type": "code", 59 | "execution_count": 3, 60 | "metadata": { 61 | "collapsed": true 62 | }, 63 | "outputs": [], 64 | "source": [ 65 | "# Temporarily testing some of the new functionality.\n", 66 | "test_state = \"TX\"\n", 67 | "test_spec = \"Internal Medicine\"\n", 68 | "table_data = build_new_table_data([test_state], [test_spec])\n" 69 | ] 70 | }, 71 | { 72 | "cell_type": "code", 73 | "execution_count": 5, 74 | "metadata": {}, 75 | "outputs": [ 76 | { 77 | "name": "stdout", 78 | "output_type": "stream", 79 | "text": [ 80 | " cost counts lastname outlier_count outlier_rate \\\n", 81 | "1124227533 996.5 658 LA HOZ 1 0.00152 \n", 82 | "\n", 83 | " provider_type state \n", 84 | "1124227533 Internal Medicine TX \n", 85 | " cost counts lastname outlier_count outlier_rate \\\n", 86 | "1225062383 1013.1 11 MASTERSON 1 0.090909 \n", 87 | "\n", 88 | " provider_type state \n", 89 | "1225062383 Internal Medicine TX \n" 90 | ] 91 | } 92 | ], 93 | "source": [ 94 | "print(table_data.loc[table_data[\"lastname\"] == \"LA HOZ\"])\n", 95 | "print(table_data.loc[table_data[\"lastname\"] == \"MASTERSON\"])" 96 | ] 97 | }, 98 | { 99 | "cell_type": "markdown", 100 | "metadata": {}, 101 | "source": [ 102 | "Now we need to iterate through a list of specialties and states and append the results for the counting to a running csv master file. We'll start with Cardiology." 103 | ] 104 | }, 105 | { 106 | "cell_type": "code", 107 | "execution_count": null, 108 | "metadata": {}, 109 | "outputs": [], 110 | "source": [ 111 | "csv_output = \"/home/dan/insight/cms_claims/master_outlier_counts_more_data_fixed.csv\"\n", 112 | "states = [\"AL\", \"AK\", \"AZ\", \"AR\", \"CA\", \"CO\", \"CT\", \"DC\", \"DE\", \"FL\", \"GA\", \n", 113 | " \"HI\", \"ID\", \"IL\", \"IN\", \"IA\", \"KS\", \"KY\", \"LA\", \"ME\", \"MD\", \n", 114 | " \"MA\", \"MI\", \"MN\", \"MS\", \"MO\", \"MT\", \"NE\", \"NV\", \"NH\", \"NJ\", \n", 115 | " \"NM\", \"NY\", \"NC\", \"ND\", \"OH\", \"OK\", \"OR\", \"PA\", \"RI\", \"SC\", \n", 116 | " \"SD\", \"TN\", \"TX\", \"UT\", \"VT\", \"VA\", \"WA\", \"WV\", \"WI\", \"WY\"]\n", 117 | "specialties = ['Internal Medicine', 'Family Practice', 'Psychiatry', \n", 118 | " 'Neurology', 'Endocrinology', 'Physical Medicine and Rehabilitation']\n", 119 | "for state in states:\n", 120 | " for specialty in specialties:\n", 121 | " table_data = build_new_table_data([state], [specialty])\n", 122 | " with open(csv_output, 'a') as f:\n", 123 | " table_data.to_csv(f)\n", 124 | " print(\"Finished \" + specialty + \" in \" + state + \".\")" 125 | ] 126 | }, 127 | { 128 | "cell_type": "code", 129 | "execution_count": null, 130 | "metadata": { 131 | "collapsed": true 132 | }, 133 | "outputs": [], 134 | "source": [] 135 | } 136 | ], 137 | "metadata": { 138 | "kernelspec": { 139 | "display_name": "Python 3", 140 | "language": "python", 141 | "name": "python3" 142 | }, 143 | "language_info": { 144 | "codemirror_mode": { 145 | "name": "ipython", 146 | "version": 3 147 | }, 148 | "file_extension": ".py", 149 | "mimetype": "text/x-python", 150 | "name": "python", 151 | "nbconvert_exporter": "python", 152 | "pygments_lexer": "ipython3", 153 | "version": "3.5.2" 154 | } 155 | }, 156 | "nbformat": 4, 157 | "nbformat_minor": 2 158 | } 159 | -------------------------------------------------------------------------------- /src/README.md: -------------------------------------------------------------------------------- 1 | # Source code for FraudHacker 2 | This directory contains the source code for FraudHacker. An overall explanation of the workflow can be found in the [parent directory for this repository](https://github.com/dchannah/fraudhacker); here I focus on the content of each file. 3 | 4 | * `anomaly_tools.py`: Implementation of tools to label outliers in the CMS.gov dataset. The classes herein operate on a Pandas DataFrame and designed for modularity - all anomaly detectors inherit certain useful functions from a parent super class, and the idea of an "outlier metric" is deliberately intended to be flexible (for example, for K-means clustering, the outlier metric is distance to the cluster centroid, while it is a GLOSH score for HDBSCAN). 5 | 6 | * `config.yaml`: Configuration for the PostgreSQL database reader; includes database name, user name (removed), and password (removed). A list of features to extract (to keep the DataFrame size manageable) is also included this file. This lets the user change which features are selected without needing to go into the Python source code. 7 | 8 | * `database_tools.py`: Various classes for interacting with PostgreSQL databases. A parent super class is again used, but different subclasses are created for interacting with the raw data PostgreSQL database and the outlier counts PostgreSQL database. 9 | 10 | * `fh_config.py`: A collection of lengthy but necessary variables for the Flask app. These variables are stored in their own file to cut down on messiness in the Flask app. 11 | 12 | * `flask_app_java.py`: The Flask app which actually collects calculated and ranked outlier count data for each physician and renders it to a webpage for user viewing. It also ties together the rest of the pages on [www.fraudhacker.site](http://www.fraudhacker.site). 13 | 14 | * `plotting_tools.py`: A collection of plotting tools to render plots on the webpage. Most of these plotting tools are now deprecated since I switched from Bokeh to ChartJS for my plot rendering, but as at least one of these routines is still used in the Flask app, this file remains (and I left the Bokeh functions in just in case I ever want to quickly switch back to Bokeh for rendering figures). 15 | 16 | The `static` and `templates` folders contain the web files for the Flask app. 17 | -------------------------------------------------------------------------------- /src/config.yaml: -------------------------------------------------------------------------------- 1 | database_name: cms_complete 2 | user_name: # YOUR POSTGRES USERNAME HERE 3 | password: # YOUR PASSWORD HERE 4 | features: 5 | - 'npi' 6 | - 'nppes_provider_city' 7 | - 'nppes_provider_last_org_name' 8 | - 'nppes_provider_street1' 9 | - 'nppes_provider_street2' 10 | - 'nppes_provider_zip' 11 | - 'nppes_provider_state' 12 | - 'line_srvc_cnt' 13 | - 'bene_unique_cnt' 14 | - 'bene_day_srvc_cnt' 15 | - 'average_medicare_allowed_amt' 16 | - 'average_submitted_chrg_amt' 17 | - 'average_medicare_payment_amt' 18 | -------------------------------------------------------------------------------- /src/database_tools.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import yaml 4 | import psycopg2 5 | import pandas as pd 6 | 7 | __author__ = "Daniel Hannah" 8 | __email__ = "dan@danhannah.site" 9 | 10 | """Tools for interacting with a PostgreSQL database that stores CMS claim data. 11 | 12 | The PandasDBReader class builds a dataframe from the database with a requested 13 | set of features as the columns. Eventually I may include a PandasCSVReader which 14 | populates the same fields so that the Flask app can take CSV input. 15 | 16 | """ 17 | 18 | 19 | class CMSDBReader: 20 | """General superclass for database readers. 21 | 22 | Attributes: 23 | configuration (JSON): A YAML-read configuration set. 24 | connection (psycopg2): An SQL database connection. 25 | 26 | """ 27 | 28 | def __init__(self, config_yaml): 29 | """Initialization for all of the DB Readers. 30 | 31 | Args: 32 | config_yaml (str): Path to a configuration yaml. 33 | 34 | """ 35 | # Get the DB reader config from a YAML file. 36 | with open(config_yaml, 'r') as f: 37 | self.configuration = yaml.load(f) 38 | 39 | # Set up an SQL connection. 40 | self.connection = psycopg2.connect( 41 | database=self.configuration['database_name'], 42 | user=self.configuration['user_name'], 43 | password=self.configuration['password'] 44 | ) 45 | 46 | @staticmethod 47 | def build_query(need_cols, q_dict, table='cms'): 48 | """Queries the SQL database for a subset of the data. 49 | 50 | This routine is specifically tailored to query specific data from 51 | specific regions. Custom query functionality is pending. 52 | 53 | Args: 54 | need_cols (list): A list of properties we want to query. 55 | q_dict (dict): A dictionary mapping columns to allowed values. 56 | table (str): The table to query from in the database. 57 | 58 | Returns: 59 | A properly-formatted SQL query. 60 | 61 | """ 62 | query_str = "SELECT " 63 | for idx, col in enumerate(need_cols): 64 | if idx == len(need_cols) - 1: 65 | query_str += col 66 | else: 67 | query_str += col + ", " 68 | query_str += " from " + table + " WHERE " 69 | for cname in q_dict: 70 | opt_list = str(q_dict[cname]).replace('[', '(').replace(']', ')') 71 | query_str += cname + " IN " + opt_list + " AND " 72 | return query_str[:-4] # Need to remove final AND 73 | 74 | 75 | class PandasDBReader(CMSDBReader): 76 | """Class for interacting with the PostgreSQL database containing CMS data. 77 | 78 | Right now this class bundles the "go-between" for the Flask app and the 79 | database. 80 | 81 | Attributes: 82 | connection (psycopg2): A SQL database connection. 83 | d_f (DataFrame): A Pandas data frame. 84 | 85 | """ 86 | 87 | def __init__(self, config_yaml, region_list, specialty_list): 88 | """Initialization for the PandasDBReader. 89 | 90 | Args: 91 | config_yaml (YAML): A YAML file containing configuration info. 92 | region_list (list): A list of US states to get info from. 93 | specialty_list (list): A list of specialties to get info on. 94 | 95 | """ 96 | super().__init__(config_yaml) 97 | 98 | # Build a query from the provided region/specialty lists. 99 | query_dict = {"provider_type": specialty_list, 100 | "nppes_provider_state": region_list} 101 | query = self.build_query(self.configuration['features'], query_dict) 102 | 103 | # Use the query to create a dataframe from the database. 104 | self.d_f = pd.read_sql_query(query, self.connection) 105 | 106 | 107 | class OutlierCountDBReader(CMSDBReader): 108 | """A database reader class for the outlier counts table (for speed!) 109 | 110 | Attributes: 111 | connection (psycopg2): A SQL database connection. 112 | d_f (DataFrame): A Pandas data frame. 113 | 114 | """ 115 | 116 | def __init__(self, config_yaml, region_list, specialty_list, metric='hdb_total'): 117 | """Initialization for the PandasDBReader. 118 | 119 | Args: 120 | config_yaml (YAML): A YAML file containing configuration info. 121 | region_list (list): A list of US states to get info from. 122 | specialty_list (list): A list of specialties to get info on. 123 | metric (str): Which outlier metric should be pulled? 124 | 125 | """ 126 | super().__init__(config_yaml) 127 | 128 | outlier_cols = ['npi', 'state', 'lastname', 'provider_type', 129 | 'outlier_count', 'cost', 'outlier_rate'] 130 | table_name = "provider_anomaly_counts_" + metric 131 | print(table_name) 132 | 133 | # Build a query from the provided region/specialty lists. 134 | query_dict = {"provider_type": specialty_list, 135 | "state": region_list} 136 | query = self.build_query(outlier_cols, query_dict, table=table_name) 137 | print("RUNNING QUERY " + query) 138 | 139 | # Use the query to create a dataframe from the database. 140 | self.d_f = pd.read_sql_query(query, self.connection) 141 | -------------------------------------------------------------------------------- /src/fh_config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | __author__ = "Daniel Hannah" 4 | __email__ = "dan@danhannah.site" 5 | 6 | """A configuration file for the Flask app. 7 | 8 | This is not the most elegant way to do this but because there are various types 9 | of objects I want to import from an external file, a direct python file is 10 | easiest for now. 11 | 12 | """ 13 | 14 | regional_options = [ 15 | {'state': 'AL'}, 16 | {'state': 'AK'}, 17 | {'state': 'AZ'}, 18 | {'state': 'AR'}, 19 | {'state': 'CA'}, 20 | {'state': 'CO'}, 21 | {'state': 'CT'}, 22 | {'state': 'DC'}, 23 | {'state': 'DE'}, 24 | {'state': 'FL'}, 25 | {'state': 'GA'}, 26 | {'state': 'HI'}, 27 | {'state': 'ID'}, 28 | {'state': 'IL'}, 29 | {'state': 'IN'}, 30 | {'state': 'IA'}, 31 | {'state': 'KS'}, 32 | {'state': 'KY'}, 33 | {'state': 'LA'}, 34 | {'state': 'ME'}, 35 | {'state': 'MD'}, 36 | {'state': 'MA'}, 37 | {'state': 'MI'}, 38 | {'state': 'MN'}, 39 | {'state': 'MS'}, 40 | {'state': 'MO'}, 41 | {'state': 'MT'}, 42 | {'state': 'NE'}, 43 | {'state': 'NV'}, 44 | {'state': 'NH'}, 45 | {'state': 'NJ'}, 46 | {'state': 'NM'}, 47 | {'state': 'NY'}, 48 | {'state': 'NC'}, 49 | {'state': 'ND'}, 50 | {'state': 'OH'}, 51 | {'state': 'OK'}, 52 | {'state': 'OR'}, 53 | {'state': 'PA'}, 54 | {'state': 'RI'}, 55 | {'state': 'SC'}, 56 | {'state': 'SD'}, 57 | {'state': 'TN'}, 58 | {'state': 'TX'}, 59 | {'state': 'UT'}, 60 | {'state': 'VT'}, 61 | {'state': 'VA'}, 62 | {'state': 'WA'}, 63 | {'state': 'WV'}, 64 | {'state': 'WI'}, 65 | {'state': 'WY'}, 66 | ] 67 | 68 | specialty_options = [ 69 | {'type': 'Cardiology'}, 70 | {'type': 'Endocrinology'}, 71 | {'type': 'Family Practice'}, 72 | {'type': 'Internal Medicine'}, 73 | {'type': 'Ophthalmology'}, 74 | {'type': 'Neurology'}, 75 | {'type': 'Psychiatry'}, 76 | {'type': 'Physical Medicine and Rehabilitation'} 77 | ] 78 | 79 | regression_vars = [ 80 | "line_srvc_cnt", 81 | "bene_unique_cnt", 82 | "bene_day_srvc_cnt", 83 | "average_medicare_allowed_amt", 84 | "average_submitted_chrg_amt" 85 | ] 86 | 87 | response_var = "average_medicare_payment_amt" 88 | 89 | fraudulent_npis = [1245298371, 1922021195, 1225082886, 1023119898, 1881660959, 90 | 1942373923, 1013038629, 1356311591, 1013998640, 1881746501, 91 | 1881622090, 1295836245, 1275534935, 1801909338, 1528086618, 92 | 1235182189, 1730383993, 1376697995, 1457696908, 1033145487, 93 | 1619930260, 1356341911, 1427060375, 1013059740, 1235135138, 94 | 1225022627, 1326044835, 1720255144, 1073589420, 1396906160, 95 | 1659355840, 1841493707, 1356354252, 1477606614, 1659399897, 96 | 1609071836, 1154498277, 1316151525, 1912049388, 1316935406, 97 | 1841218799, 1780708594, 1093809907, 1285889782, 1942206198, 98 | 1245377787, 1477559037, 1952455941, 1336220425, 1861493009, 99 | 1619162898, 1184786196, 1053499673, 1750320412, 1720003882, 100 | 1164414769, 1245212471, 1588675896, 1841268026, 1013093178, 101 | 1801846597, 1427361609, 1770555724, 1457303380, 1841230166, 102 | 1164459350, 1124055900, 1740396381, 1316947807, 1194745695, 103 | 1275695975, 1902815087, 1174545271, 1578510723, 1275588485, 104 | 1225129349, 1841246105, 1750451613, 1225188766, 1053492132, 105 | 1174607782, 1275559338, 1649317298, 1639169972, 1801850292, 106 | 1952477622, 1043219405, 1831225812, 1376629717, 1609861590, 107 | 1437276128, 1124058086, 1427029479, 1184601577, 1003813866, 108 | 1518028281, 1942320635, 1427034271, 1760477269, 1407877046, 109 | 1053330225, 1538359104, 1679643191] 110 | 111 | fraudulent_npis = [str(npi) for npi in fraudulent_npis] 112 | -------------------------------------------------------------------------------- /src/flask_app_java.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | __author__ = "Daniel Hannah" 4 | __email__ = "dan@danhannah.site" 5 | 6 | from flask import Flask, render_template, request 7 | from database_tools import OutlierCountDBReader 8 | from plotting_tools import get_bar_colors 9 | from fh_config import regional_options, specialty_options, fraudulent_npis 10 | 11 | # Global variables 12 | YAML_CONFIG = "./config.yaml" 13 | 14 | app = Flask(__name__) 15 | 16 | 17 | @app.route('/', methods=['GET', 'POST']) 18 | def fraudhacker_input(): 19 | return render_template("index.html", geo_data=regional_options, 20 | provider_data=specialty_options) 21 | 22 | 23 | @app.route('/charts_internal', methods=['POST', 'GET']) 24 | def fraudhacker_output(): 25 | # Get provider and specialty choice from user selections on input. 26 | state = request.form.get('geo_select') 27 | specialty = request.form.get('provider_select') 28 | 29 | odb = OutlierCountDBReader(YAML_CONFIG, [state], [specialty]) 30 | 31 | # First we get the top 20 ranked by outlier count. 32 | w_n_df = odb.d_f.head(20) 33 | npis = list(w_n_df['npi'].values) 34 | last_names = w_n_df['lastname'].values 35 | o_cts = list(w_n_df['outlier_count'].values) 36 | costs = list(w_n_df['cost'].values) 37 | labels = [(l_n + " (" + npi + ")") for l_n, npi in zip(last_names, npis)] 38 | colorlist = get_bar_colors(npis) 39 | 40 | # Now we get the top 20 ranked by outlier rate. 41 | odb_count_sorted = odb.d_f.sort_values(by="outlier_rate", ascending=False) 42 | w_n_rate_df = odb_count_sorted.head(20) 43 | rate_npis = list(w_n_rate_df['npi'].values) 44 | rate_last_names = w_n_rate_df['lastname'].values 45 | rate_cts = list(w_n_rate_df['outlier_rate'].values) 46 | rate_costs = list(w_n_rate_df['cost'].values) 47 | rate_labels = [(l_n + " (" + npi + ")") for l_n, npi in 48 | zip(rate_last_names, rate_npis)] 49 | rate_colors = get_bar_colors(rate_npis) 50 | 51 | return render_template("charts_internal.html", labels=labels, cts=o_cts, 52 | state=state, specialty=specialty, 53 | colorlist=colorlist, rate_cts=rate_cts, 54 | rate_labels=rate_labels, costs=costs, 55 | rate_costs=rate_costs, rate_colors=rate_colors) 56 | 57 | 58 | @app.route('/slides') 59 | def show_slides(): 60 | return render_template("slides.html") 61 | 62 | @app.route('/about') 63 | def show_about(): 64 | return render_template("about.html") 65 | 66 | def main(): 67 | app.run(debug=True) 68 | 69 | 70 | if __name__ == "__main__": 71 | main() 72 | -------------------------------------------------------------------------------- /src/plotting_tools.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from bokeh.models import ColumnDataSource, DataRange1d, SingleIntervalTicker,\ 4 | LinearAxis, LabelSet 5 | from bokeh.plotting import figure 6 | from bokeh.models.glyphs import HBar 7 | from bokeh.embed import components 8 | from fh_config import fraudulent_npis 9 | 10 | __author__ = "Daniel Hannah" 11 | __email__ = "dan@danhannah.site" 12 | 13 | """Tools for plotting bar charts with Bokeh. 14 | 15 | This set of tools has been deprecated and is no longer in use on FraudHacker (I 16 | switched to chartJS for a variety of reasons), but is here just in case it 17 | becomes useful in the future. 18 | 19 | """ 20 | 21 | 22 | def render_bar_plot(source, plt_title): 23 | """Renders a bokeh bar plot from a data source. 24 | 25 | Args: 26 | source (dataframe): A dict-style Pandas dataframe. 27 | plt_title (str): Title for the plot. 28 | 29 | Returns: 30 | Components of a bar plot figure. 31 | """ 32 | bar_height = 0.5 33 | top_stop = len(source.data["tick_labels"]) - 1 + bar_height 34 | bottom_start = -1 * bar_height 35 | 36 | xdr = DataRange1d() 37 | ydr = DataRange1d(start=bottom_start, end=top_stop) 38 | plot = figure(title=plt_title, x_range=xdr, y_range=ydr, plot_width=1200, 39 | plot_height=1200, h_symmetry=False, v_symmetry=False, 40 | toolbar_location=None, y_axis_type=None, x_axis_type=None) 41 | 42 | # Add vertical bar glyphs to the plot. 43 | glyph = HBar(y="provider_idx", right="outlier_count", left=0, 44 | height=bar_height, fill_color="#DB4437") 45 | plot.add_glyph(source, glyph) 46 | 47 | # Need to convert provider names to tick labels. 48 | yaxis = LinearAxis(ticker=SingleIntervalTicker(interval=1), 49 | axis_line_color='white', major_tick_line_color='white', 50 | minor_tick_line_color='white') 51 | plot.add_layout(yaxis, 'left') 52 | plot.yaxis.major_label_overrides = { 53 | idx: label for idx, label in enumerate(source.data["tick_labels"]) 54 | } 55 | 56 | # Add some labels to the bar plots 57 | labels = LabelSet(x='outlier_count', y='provider_idx', x_offset=5, 58 | y_offset=-8, text='outlier_count', source=source, 59 | text_font_size='18pt') 60 | plot.add_layout(labels) 61 | 62 | # Making the plot pretty 63 | plot.title.text_font_size = '40pt' 64 | plot.yaxis.axis_label_text_font_size = '30pt' 65 | plot.yaxis.major_label_text_font_size = '18pt' 66 | plot.xaxis.axis_label = "Anomaly Count" 67 | plot.outline_line_color = None 68 | 69 | # Rotating the plot labels so that the text fit 70 | # plot.xaxis.major_label_orientation = pi/3 71 | 72 | return components(plot) 73 | 74 | 75 | def create_source(w_n_df, bar_value): 76 | """Creates a column data source from the output of the anomaly detector. 77 | 78 | Args: 79 | w_n_df (DataFrame): A Pandas DataFrame containing an outlier count. 80 | bar_value (str): Column name for the bar chart values. 81 | 82 | Returns: 83 | A ColumnDataSource object. 84 | 85 | """ 86 | # Get the info we need from the DataFrame. 87 | indices = [i for i in range(w_n_df.shape[0])] 88 | npis = w_n_df['npi'].values 89 | last_names = w_n_df['lastname'].values 90 | o_cts = w_n_df[bar_value].values 91 | 92 | labels = [(l_n + " (" + npi + ")") for l_n, npi in zip(last_names, npis)] 93 | 94 | source = ColumnDataSource(dict(provider_idx=indices, 95 | outlier_count=o_cts, 96 | tick_labels=labels)) 97 | 98 | return source 99 | 100 | 101 | def generate_bar_plot(w_n_df, bar_value='outlier_count', plt_title=None): 102 | """Method to generate a Bokeh bar plot. 103 | 104 | Args: 105 | w_n_df (DataFrame): A Pandas data frame containing our bar chart data. 106 | bar_value (str): Column name that we want to make a bar plot for. 107 | plt_title (str): A title for the plot. 108 | 109 | Returns: 110 | Components of a bar plot figure. 111 | 112 | """ 113 | data_source = create_source(w_n_df.iloc[::-1], bar_value) 114 | return render_bar_plot(data_source, plt_title=plt_title) 115 | 116 | 117 | def get_bar_colors(target_npis): 118 | """Generates a list of bar colors (to pass on to JavaScript) for NPIs. 119 | 120 | Args: 121 | target_npis (list): A list of the NPIs we seek to plot. 122 | 123 | Returns: 124 | A list of appropriate RGBA values in the same order as the list. 125 | 126 | """ 127 | colors = [] 128 | for npi in target_npis: 129 | if npi in fraudulent_npis: 130 | colors.append("rgba(255, 0, 0, 1)") 131 | else: 132 | colors.append("rgba(2, 117, 216, 1)") 133 | return colors 134 | -------------------------------------------------------------------------------- /src/static/css/bootstrap-reboot.css: -------------------------------------------------------------------------------- 1 | html { 2 | box-sizing: border-box; 3 | font-family: sans-serif; 4 | line-height: 1.15; 5 | -webkit-text-size-adjust: 100%; 6 | -ms-text-size-adjust: 100%; 7 | -ms-overflow-style: scrollbar; 8 | -webkit-tap-highlight-color: transparent; 9 | } 10 | 11 | *, 12 | *::before, 13 | *::after { 14 | box-sizing: inherit; 15 | } 16 | 17 | @-ms-viewport { 18 | width: device-width; 19 | } 20 | 21 | article, aside, dialog, figcaption, figure, footer, header, hgroup, main, nav, section { 22 | display: block; 23 | } 24 | 25 | body { 26 | margin: 0; 27 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; 28 | font-size: 1rem; 29 | font-weight: normal; 30 | line-height: 1.5; 31 | color: #212529; 32 | background-color: #fff; 33 | } 34 | 35 | [tabindex="-1"]:focus { 36 | outline: none !important; 37 | } 38 | 39 | hr { 40 | box-sizing: content-box; 41 | height: 0; 42 | overflow: visible; 43 | } 44 | 45 | h1, h2, h3, h4, h5, h6 { 46 | margin-top: 0; 47 | margin-bottom: .5rem; 48 | } 49 | 50 | p { 51 | margin-top: 0; 52 | margin-bottom: 1rem; 53 | } 54 | 55 | abbr[title], 56 | abbr[data-original-title] { 57 | text-decoration: underline; 58 | -webkit-text-decoration: underline dotted; 59 | text-decoration: underline dotted; 60 | cursor: help; 61 | border-bottom: 0; 62 | } 63 | 64 | address { 65 | margin-bottom: 1rem; 66 | font-style: normal; 67 | line-height: inherit; 68 | } 69 | 70 | ol, 71 | ul, 72 | dl { 73 | margin-top: 0; 74 | margin-bottom: 1rem; 75 | } 76 | 77 | ol ol, 78 | ul ul, 79 | ol ul, 80 | ul ol { 81 | margin-bottom: 0; 82 | } 83 | 84 | dt { 85 | font-weight: bold; 86 | } 87 | 88 | dd { 89 | margin-bottom: .5rem; 90 | margin-left: 0; 91 | } 92 | 93 | blockquote { 94 | margin: 0 0 1rem; 95 | } 96 | 97 | dfn { 98 | font-style: italic; 99 | } 100 | 101 | b, 102 | strong { 103 | font-weight: bolder; 104 | } 105 | 106 | small { 107 | font-size: 80%; 108 | } 109 | 110 | sub, 111 | sup { 112 | position: relative; 113 | font-size: 75%; 114 | line-height: 0; 115 | vertical-align: baseline; 116 | } 117 | 118 | sub { 119 | bottom: -.25em; 120 | } 121 | 122 | sup { 123 | top: -.5em; 124 | } 125 | 126 | a { 127 | color: #007bff; 128 | text-decoration: none; 129 | background-color: transparent; 130 | -webkit-text-decoration-skip: objects; 131 | } 132 | 133 | a:hover { 134 | color: #0056b3; 135 | text-decoration: underline; 136 | } 137 | 138 | a:not([href]):not([tabindex]) { 139 | color: inherit; 140 | text-decoration: none; 141 | } 142 | 143 | a:not([href]):not([tabindex]):focus, a:not([href]):not([tabindex]):hover { 144 | color: inherit; 145 | text-decoration: none; 146 | } 147 | 148 | a:not([href]):not([tabindex]):focus { 149 | outline: 0; 150 | } 151 | 152 | pre, 153 | code, 154 | kbd, 155 | samp { 156 | font-family: monospace, monospace; 157 | font-size: 1em; 158 | } 159 | 160 | pre { 161 | margin-top: 0; 162 | margin-bottom: 1rem; 163 | overflow: auto; 164 | } 165 | 166 | figure { 167 | margin: 0 0 1rem; 168 | } 169 | 170 | img { 171 | vertical-align: middle; 172 | border-style: none; 173 | } 174 | 175 | svg:not(:root) { 176 | overflow: hidden; 177 | } 178 | 179 | a, 180 | area, 181 | button, 182 | [role="button"], 183 | input, 184 | label, 185 | select, 186 | summary, 187 | textarea { 188 | -ms-touch-action: manipulation; 189 | touch-action: manipulation; 190 | } 191 | 192 | table { 193 | border-collapse: collapse; 194 | } 195 | 196 | caption { 197 | padding-top: 0.75rem; 198 | padding-bottom: 0.75rem; 199 | color: #868e96; 200 | text-align: left; 201 | caption-side: bottom; 202 | } 203 | 204 | th { 205 | text-align: left; 206 | } 207 | 208 | label { 209 | display: inline-block; 210 | margin-bottom: .5rem; 211 | } 212 | 213 | button:focus { 214 | outline: 1px dotted; 215 | outline: 5px auto -webkit-focus-ring-color; 216 | } 217 | 218 | input, 219 | button, 220 | select, 221 | optgroup, 222 | textarea { 223 | margin: 0; 224 | font-family: inherit; 225 | font-size: inherit; 226 | line-height: inherit; 227 | } 228 | 229 | button, 230 | input { 231 | overflow: visible; 232 | } 233 | 234 | button, 235 | select { 236 | text-transform: none; 237 | } 238 | 239 | button, 240 | html [type="button"], 241 | [type="reset"], 242 | [type="submit"] { 243 | -webkit-appearance: button; 244 | } 245 | 246 | button::-moz-focus-inner, 247 | [type="button"]::-moz-focus-inner, 248 | [type="reset"]::-moz-focus-inner, 249 | [type="submit"]::-moz-focus-inner { 250 | padding: 0; 251 | border-style: none; 252 | } 253 | 254 | input[type="radio"], 255 | input[type="checkbox"] { 256 | box-sizing: border-box; 257 | padding: 0; 258 | } 259 | 260 | input[type="date"], 261 | input[type="time"], 262 | input[type="datetime-local"], 263 | input[type="month"] { 264 | -webkit-appearance: listbox; 265 | } 266 | 267 | textarea { 268 | overflow: auto; 269 | resize: vertical; 270 | } 271 | 272 | fieldset { 273 | min-width: 0; 274 | padding: 0; 275 | margin: 0; 276 | border: 0; 277 | } 278 | 279 | legend { 280 | display: block; 281 | width: 100%; 282 | max-width: 100%; 283 | padding: 0; 284 | margin-bottom: .5rem; 285 | font-size: 1.5rem; 286 | line-height: inherit; 287 | color: inherit; 288 | white-space: normal; 289 | } 290 | 291 | progress { 292 | vertical-align: baseline; 293 | } 294 | 295 | [type="number"]::-webkit-inner-spin-button, 296 | [type="number"]::-webkit-outer-spin-button { 297 | height: auto; 298 | } 299 | 300 | [type="search"] { 301 | outline-offset: -2px; 302 | -webkit-appearance: none; 303 | } 304 | 305 | [type="search"]::-webkit-search-cancel-button, 306 | [type="search"]::-webkit-search-decoration { 307 | -webkit-appearance: none; 308 | } 309 | 310 | ::-webkit-file-upload-button { 311 | font: inherit; 312 | -webkit-appearance: button; 313 | } 314 | 315 | output { 316 | display: inline-block; 317 | } 318 | 319 | summary { 320 | display: list-item; 321 | } 322 | 323 | template { 324 | display: none; 325 | } 326 | 327 | [hidden] { 328 | display: none !important; 329 | } 330 | /*# sourceMappingURL=bootstrap-reboot.css.map */ -------------------------------------------------------------------------------- /src/static/css/bootstrap-reboot.min.css: -------------------------------------------------------------------------------- 1 | html{box-sizing:border-box;font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-ms-overflow-style:scrollbar;-webkit-tap-highlight-color:transparent}*,::after,::before{box-sizing:inherit}@-ms-viewport{width:device-width}article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff}[tabindex="-1"]:focus{outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}dfn{font-style:italic}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent;-webkit-text-decoration-skip:objects}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([tabindex]){color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus,a:not([href]):not([tabindex]):hover{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg:not(:root){overflow:hidden}[role=button],a,area,button,input,label,select,summary,textarea{-ms-touch-action:manipulation;touch-action:manipulation}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#868e96;text-align:left;caption-side:bottom}th{text-align:left}label{display:inline-block;margin-bottom:.5rem}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item}template{display:none}[hidden]{display:none!important} 2 | /*# sourceMappingURL=bootstrap-reboot.min.css.map */ -------------------------------------------------------------------------------- /src/static/js/sb-admin-charts.js: -------------------------------------------------------------------------------- 1 | // Chart.js scripts 2 | // -- Set new default font family and font color to mimic Bootstrap's default styling 3 | Chart.defaults.global.defaultFontFamily = '-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif'; 4 | Chart.defaults.global.defaultFontColor = '#292b2c'; 5 | // -- Bar Chart Example 6 | var ctx = document.getElementById("myBarChart"); 7 | var myLineChart = new Chart(ctx, { 8 | // type: 'bar', 9 | type: 'horizontalBar', 10 | data: { 11 | labels: ["January", "February", "March", "April", "May", "June"], 12 | datasets: [{ 13 | label: "Revenue", 14 | backgroundColor: "rgba(2,117,216,1)", 15 | borderColor: "rgba(2,117,216,1)", 16 | data: [4215, 5312, 6251, 7841, 9821, 14984], 17 | }], 18 | }, 19 | options: { 20 | scales: { 21 | xAxes: [{ 22 | time: { 23 | unit: 'month' 24 | }, 25 | gridLines: { 26 | display: false 27 | }, 28 | ticks: { 29 | maxTicksLimit: 6 30 | } 31 | }], 32 | yAxes: [{ 33 | ticks: { 34 | min: 0, 35 | max: 15000, 36 | maxTicksLimit: 5 37 | }, 38 | gridLines: { 39 | display: true 40 | } 41 | }], 42 | }, 43 | legend: { 44 | display: false 45 | } 46 | } 47 | }); 48 | -------------------------------------------------------------------------------- /src/static/js/sb-admin-charts.js.old: -------------------------------------------------------------------------------- 1 | // Chart.js scripts 2 | // -- Set new default font family and font color to mimic Bootstrap's default styling 3 | Chart.defaults.global.defaultFontFamily = '-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif'; 4 | Chart.defaults.global.defaultFontColor = '#292b2c'; 5 | // -- Area Chart Example 6 | var ctx = document.getElementById("myAreaChart"); 7 | var myLineChart = new Chart(ctx, { 8 | type: 'line', 9 | data: { 10 | labels: ["Mar 1", "Mar 2", "Mar 3", "Mar 4", "Mar 5", "Mar 6", "Mar 7", "Mar 8", "Mar 9", "Mar 10", "Mar 11", "Mar 12", "Mar 13"], 11 | datasets: [{ 12 | label: "Sessions", 13 | lineTension: 0.3, 14 | backgroundColor: "rgba(2,117,216,0.2)", 15 | borderColor: "rgba(2,117,216,1)", 16 | pointRadius: 5, 17 | pointBackgroundColor: "rgba(2,117,216,1)", 18 | pointBorderColor: "rgba(255,255,255,0.8)", 19 | pointHoverRadius: 5, 20 | pointHoverBackgroundColor: "rgba(2,117,216,1)", 21 | pointHitRadius: 20, 22 | pointBorderWidth: 2, 23 | data: [10000, 30162, 26263, 18394, 18287, 28682, 31274, 33259, 25849, 24159, 32651, 31984, 38451], 24 | }], 25 | }, 26 | options: { 27 | scales: { 28 | xAxes: [{ 29 | time: { 30 | unit: 'date' 31 | }, 32 | gridLines: { 33 | display: false 34 | }, 35 | ticks: { 36 | maxTicksLimit: 7 37 | } 38 | }], 39 | yAxes: [{ 40 | ticks: { 41 | min: 0, 42 | max: 40000, 43 | maxTicksLimit: 5 44 | }, 45 | gridLines: { 46 | color: "rgba(0, 0, 0, .125)", 47 | } 48 | }], 49 | }, 50 | legend: { 51 | display: false 52 | } 53 | } 54 | }); 55 | // -- Bar Chart Example 56 | var ctx = document.getElementById("myBarChart"); 57 | var myLineChart = new Chart(ctx, { 58 | // type: 'bar', 59 | type: 'horizontalBar', 60 | data: { 61 | labels: ["January", "February", "March", "April", "May", "June"], 62 | datasets: [{ 63 | label: "Revenue", 64 | backgroundColor: "rgba(2,117,216,1)", 65 | borderColor: "rgba(2,117,216,1)", 66 | data: [4215, 5312, 6251, 7841, 9821, 14984], 67 | }], 68 | }, 69 | options: { 70 | scales: { 71 | xAxes: [{ 72 | time: { 73 | unit: 'month' 74 | }, 75 | gridLines: { 76 | display: false 77 | }, 78 | ticks: { 79 | maxTicksLimit: 6 80 | } 81 | }], 82 | yAxes: [{ 83 | ticks: { 84 | min: 0, 85 | max: 15000, 86 | maxTicksLimit: 5 87 | }, 88 | gridLines: { 89 | display: true 90 | } 91 | }], 92 | }, 93 | legend: { 94 | display: false 95 | } 96 | } 97 | }); 98 | // -- Pie Chart Example 99 | var ctx = document.getElementById("myPieChart"); 100 | var myPieChart = new Chart(ctx, { 101 | type: 'pie', 102 | data: { 103 | labels: ["Blue", "Red", "Yellow", "Green"], 104 | datasets: [{ 105 | data: [12.21, 15.58, 11.25, 8.32], 106 | backgroundColor: ['#007bff', '#dc3545', '#ffc107', '#28a745'], 107 | }], 108 | }, 109 | }); 110 | -------------------------------------------------------------------------------- /src/static/js/sb-admin-charts.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Start Bootstrap - SB Admin v4.0.0-beta (https://startbootstrap.com/template-overviews/sb-admin) 3 | * Copyright 2013-2017 Start Bootstrap 4 | * Licensed under MIT (https://github.com/BlackrockDigital/startbootstrap-sb-admin/blob/master/LICENSE) 5 | */ 6 | Chart.defaults.global.defaultFontFamily='-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif',Chart.defaults.global.defaultFontColor="#292b2c";var ctx=document.getElementById("myAreaChart"),myLineChart=new Chart(ctx,{type:"line",data:{labels:["Mar 1","Mar 2","Mar 3","Mar 4","Mar 5","Mar 6","Mar 7","Mar 8","Mar 9","Mar 10","Mar 11","Mar 12","Mar 13"],datasets:[{label:"Sessions",lineTension:.3,backgroundColor:"rgba(2,117,216,0.2)",borderColor:"rgba(2,117,216,1)",pointRadius:5,pointBackgroundColor:"rgba(2,117,216,1)",pointBorderColor:"rgba(255,255,255,0.8)",pointHoverRadius:5,pointHoverBackgroundColor:"rgba(2,117,216,1)",pointHitRadius:20,pointBorderWidth:2,data:[1e4,30162,26263,18394,18287,28682,31274,33259,25849,24159,32651,31984,38451]}]},options:{scales:{xAxes:[{time:{unit:"date"},gridLines:{display:!1},ticks:{maxTicksLimit:7}}],yAxes:[{ticks:{min:0,max:4e4,maxTicksLimit:5},gridLines:{color:"rgba(0, 0, 0, .125)"}}]},legend:{display:!1}}}),ctx=document.getElementById("myBarChart"),myLineChart=new Chart(ctx,{type:"horizontalBar",data:{labels:["January","February","March","April","May","June"],datasets:[{label:"Revenue",backgroundColor:"rgba(2,117,216,1)",borderColor:"rgba(2,117,216,1)",data:[4215,5312,6251,7841,9821,14984]}]},options:{scales:{xAxes:[{time:{unit:"month"},gridLines:{display:!1},ticks:{maxTicksLimit:6}}],yAxes:[{ticks:{min:0,max:15e3,maxTicksLimit:5},gridLines:{display:!0}}]},legend:{display:!1}}}),ctx=document.getElementById("myPieChart"),myPieChart=new Chart(ctx,{type:"pie",data:{labels:["Blue","Red","Yellow","Green"],datasets:[{data:[12.21,15.58,11.25,8.32],backgroundColor:["#007bff","#dc3545","#ffc107","#28a745"]}]}}); 7 | -------------------------------------------------------------------------------- /src/static/js/sb-admin-datatables.js: -------------------------------------------------------------------------------- 1 | // Call the dataTables jQuery plugin 2 | $(document).ready(function() { 3 | $('#dataTable').DataTable(); 4 | }); 5 | -------------------------------------------------------------------------------- /src/static/js/sb-admin-datatables.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Start Bootstrap - SB Admin v4.0.0-beta (https://startbootstrap.com/template-overviews/sb-admin) 3 | * Copyright 2013-2017 Start Bootstrap 4 | * Licensed under MIT (https://github.com/BlackrockDigital/startbootstrap-sb-admin/blob/master/LICENSE) 5 | */ 6 | $(document).ready(function(){$("#dataTable").DataTable()}); -------------------------------------------------------------------------------- /src/static/js/sb-admin.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | "use strict"; // Start of use strict 3 | // Configure tooltips for collapsed side navigation 4 | $('.navbar-sidenav [data-toggle="tooltip"]').tooltip({ 5 | template: '' 6 | }) 7 | // Toggle the side navigation 8 | $("#sidenavToggler").click(function(e) { 9 | e.preventDefault(); 10 | $("body").toggleClass("sidenav-toggled"); 11 | $(".navbar-sidenav .nav-link-collapse").addClass("collapsed"); 12 | $(".navbar-sidenav .sidenav-second-level, .navbar-sidenav .sidenav-third-level").removeClass("show"); 13 | }); 14 | // Force the toggled class to be removed when a collapsible nav link is clicked 15 | $(".navbar-sidenav .nav-link-collapse").click(function(e) { 16 | e.preventDefault(); 17 | $("body").removeClass("sidenav-toggled"); 18 | }); 19 | // Prevent the content wrapper from scrolling when the fixed side navigation hovered over 20 | $('body.fixed-nav .navbar-sidenav, body.fixed-nav .sidenav-toggler, body.fixed-nav .navbar-collapse').on('mousewheel DOMMouseScroll', function(e) { 21 | var e0 = e.originalEvent, 22 | delta = e0.wheelDelta || -e0.detail; 23 | this.scrollTop += (delta < 0 ? 1 : -1) * 30; 24 | e.preventDefault(); 25 | }); 26 | // Scroll to top button appear 27 | $(document).scroll(function() { 28 | var scrollDistance = $(this).scrollTop(); 29 | if (scrollDistance > 100) { 30 | $('.scroll-to-top').fadeIn(); 31 | } else { 32 | $('.scroll-to-top').fadeOut(); 33 | } 34 | }); 35 | // Configure tooltips globally 36 | $('[data-toggle="tooltip"]').tooltip() 37 | // Smooth scrolling using jQuery easing 38 | $(document).on('click', 'a.scroll-to-top', function(event) { 39 | var $anchor = $(this); 40 | $('html, body').stop().animate({ 41 | scrollTop: ($($anchor.attr('href')).offset().top) 42 | }, 1000, 'easeInOutExpo'); 43 | event.preventDefault(); 44 | }); 45 | })(jQuery); // End of use strict 46 | -------------------------------------------------------------------------------- /src/static/js/sb-admin.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Start Bootstrap - SB Admin v4.0.0-beta (https://startbootstrap.com/template-overviews/sb-admin) 3 | * Copyright 2013-2017 Start Bootstrap 4 | * Licensed under MIT (https://github.com/BlackrockDigital/startbootstrap-sb-admin/blob/master/LICENSE) 5 | */ 6 | !function(e){"use strict";e('.navbar-sidenav [data-toggle="tooltip"]').tooltip({template:''}),e("#sidenavToggler").click(function(o){o.preventDefault(),e("body").toggleClass("sidenav-toggled"),e(".navbar-sidenav .nav-link-collapse").addClass("collapsed"),e(".navbar-sidenav .sidenav-second-level, .navbar-sidenav .sidenav-third-level").removeClass("show")}),e(".navbar-sidenav .nav-link-collapse").click(function(o){o.preventDefault(),e("body").removeClass("sidenav-toggled")}),e("body.fixed-nav .navbar-sidenav, body.fixed-nav .sidenav-toggler, body.fixed-nav .navbar-collapse").on("mousewheel DOMMouseScroll",function(e){var o=e.originalEvent,a=o.wheelDelta||-o.detail;this.scrollTop+=30*(a<0?1:-1),e.preventDefault()}),e(document).scroll(function(){e(this).scrollTop()>100?e(".scroll-to-top").fadeIn():e(".scroll-to-top").fadeOut()}),e('[data-toggle="tooltip"]').tooltip(),e(document).on("click","a.scroll-to-top",function(o){var a=e(this);e("html, body").stop().animate({scrollTop:e(a.attr("href")).offset().top},1e3,"easeInOutExpo"),o.preventDefault()})}(jQuery); -------------------------------------------------------------------------------- /src/static/vendor/bootstrap/css/bootstrap-reboot.css: -------------------------------------------------------------------------------- 1 | html { 2 | box-sizing: border-box; 3 | font-family: sans-serif; 4 | line-height: 1.15; 5 | -webkit-text-size-adjust: 100%; 6 | -ms-text-size-adjust: 100%; 7 | -ms-overflow-style: scrollbar; 8 | -webkit-tap-highlight-color: transparent; 9 | } 10 | 11 | *, 12 | *::before, 13 | *::after { 14 | box-sizing: inherit; 15 | } 16 | 17 | @-ms-viewport { 18 | width: device-width; 19 | } 20 | 21 | article, aside, dialog, figcaption, figure, footer, header, hgroup, main, nav, section { 22 | display: block; 23 | } 24 | 25 | body { 26 | margin: 0; 27 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; 28 | font-size: 1rem; 29 | font-weight: normal; 30 | line-height: 1.5; 31 | color: #212529; 32 | background-color: #fff; 33 | } 34 | 35 | [tabindex="-1"]:focus { 36 | outline: none !important; 37 | } 38 | 39 | hr { 40 | box-sizing: content-box; 41 | height: 0; 42 | overflow: visible; 43 | } 44 | 45 | h1, h2, h3, h4, h5, h6 { 46 | margin-top: 0; 47 | margin-bottom: .5rem; 48 | } 49 | 50 | p { 51 | margin-top: 0; 52 | margin-bottom: 1rem; 53 | } 54 | 55 | abbr[title], 56 | abbr[data-original-title] { 57 | text-decoration: underline; 58 | -webkit-text-decoration: underline dotted; 59 | text-decoration: underline dotted; 60 | cursor: help; 61 | border-bottom: 0; 62 | } 63 | 64 | address { 65 | margin-bottom: 1rem; 66 | font-style: normal; 67 | line-height: inherit; 68 | } 69 | 70 | ol, 71 | ul, 72 | dl { 73 | margin-top: 0; 74 | margin-bottom: 1rem; 75 | } 76 | 77 | ol ol, 78 | ul ul, 79 | ol ul, 80 | ul ol { 81 | margin-bottom: 0; 82 | } 83 | 84 | dt { 85 | font-weight: bold; 86 | } 87 | 88 | dd { 89 | margin-bottom: .5rem; 90 | margin-left: 0; 91 | } 92 | 93 | blockquote { 94 | margin: 0 0 1rem; 95 | } 96 | 97 | dfn { 98 | font-style: italic; 99 | } 100 | 101 | b, 102 | strong { 103 | font-weight: bolder; 104 | } 105 | 106 | small { 107 | font-size: 80%; 108 | } 109 | 110 | sub, 111 | sup { 112 | position: relative; 113 | font-size: 75%; 114 | line-height: 0; 115 | vertical-align: baseline; 116 | } 117 | 118 | sub { 119 | bottom: -.25em; 120 | } 121 | 122 | sup { 123 | top: -.5em; 124 | } 125 | 126 | a { 127 | color: #007bff; 128 | text-decoration: none; 129 | background-color: transparent; 130 | -webkit-text-decoration-skip: objects; 131 | } 132 | 133 | a:hover { 134 | color: #0056b3; 135 | text-decoration: underline; 136 | } 137 | 138 | a:not([href]):not([tabindex]) { 139 | color: inherit; 140 | text-decoration: none; 141 | } 142 | 143 | a:not([href]):not([tabindex]):focus, a:not([href]):not([tabindex]):hover { 144 | color: inherit; 145 | text-decoration: none; 146 | } 147 | 148 | a:not([href]):not([tabindex]):focus { 149 | outline: 0; 150 | } 151 | 152 | pre, 153 | code, 154 | kbd, 155 | samp { 156 | font-family: monospace, monospace; 157 | font-size: 1em; 158 | } 159 | 160 | pre { 161 | margin-top: 0; 162 | margin-bottom: 1rem; 163 | overflow: auto; 164 | } 165 | 166 | figure { 167 | margin: 0 0 1rem; 168 | } 169 | 170 | img { 171 | vertical-align: middle; 172 | border-style: none; 173 | } 174 | 175 | svg:not(:root) { 176 | overflow: hidden; 177 | } 178 | 179 | a, 180 | area, 181 | button, 182 | [role="button"], 183 | input, 184 | label, 185 | select, 186 | summary, 187 | textarea { 188 | -ms-touch-action: manipulation; 189 | touch-action: manipulation; 190 | } 191 | 192 | table { 193 | border-collapse: collapse; 194 | } 195 | 196 | caption { 197 | padding-top: 0.75rem; 198 | padding-bottom: 0.75rem; 199 | color: #868e96; 200 | text-align: left; 201 | caption-side: bottom; 202 | } 203 | 204 | th { 205 | text-align: left; 206 | } 207 | 208 | label { 209 | display: inline-block; 210 | margin-bottom: .5rem; 211 | } 212 | 213 | button:focus { 214 | outline: 1px dotted; 215 | outline: 5px auto -webkit-focus-ring-color; 216 | } 217 | 218 | input, 219 | button, 220 | select, 221 | optgroup, 222 | textarea { 223 | margin: 0; 224 | font-family: inherit; 225 | font-size: inherit; 226 | line-height: inherit; 227 | } 228 | 229 | button, 230 | input { 231 | overflow: visible; 232 | } 233 | 234 | button, 235 | select { 236 | text-transform: none; 237 | } 238 | 239 | button, 240 | html [type="button"], 241 | [type="reset"], 242 | [type="submit"] { 243 | -webkit-appearance: button; 244 | } 245 | 246 | button::-moz-focus-inner, 247 | [type="button"]::-moz-focus-inner, 248 | [type="reset"]::-moz-focus-inner, 249 | [type="submit"]::-moz-focus-inner { 250 | padding: 0; 251 | border-style: none; 252 | } 253 | 254 | input[type="radio"], 255 | input[type="checkbox"] { 256 | box-sizing: border-box; 257 | padding: 0; 258 | } 259 | 260 | input[type="date"], 261 | input[type="time"], 262 | input[type="datetime-local"], 263 | input[type="month"] { 264 | -webkit-appearance: listbox; 265 | } 266 | 267 | textarea { 268 | overflow: auto; 269 | resize: vertical; 270 | } 271 | 272 | fieldset { 273 | min-width: 0; 274 | padding: 0; 275 | margin: 0; 276 | border: 0; 277 | } 278 | 279 | legend { 280 | display: block; 281 | width: 100%; 282 | max-width: 100%; 283 | padding: 0; 284 | margin-bottom: .5rem; 285 | font-size: 1.5rem; 286 | line-height: inherit; 287 | color: inherit; 288 | white-space: normal; 289 | } 290 | 291 | progress { 292 | vertical-align: baseline; 293 | } 294 | 295 | [type="number"]::-webkit-inner-spin-button, 296 | [type="number"]::-webkit-outer-spin-button { 297 | height: auto; 298 | } 299 | 300 | [type="search"] { 301 | outline-offset: -2px; 302 | -webkit-appearance: none; 303 | } 304 | 305 | [type="search"]::-webkit-search-cancel-button, 306 | [type="search"]::-webkit-search-decoration { 307 | -webkit-appearance: none; 308 | } 309 | 310 | ::-webkit-file-upload-button { 311 | font: inherit; 312 | -webkit-appearance: button; 313 | } 314 | 315 | output { 316 | display: inline-block; 317 | } 318 | 319 | summary { 320 | display: list-item; 321 | } 322 | 323 | template { 324 | display: none; 325 | } 326 | 327 | [hidden] { 328 | display: none !important; 329 | } 330 | /*# sourceMappingURL=bootstrap-reboot.css.map */ -------------------------------------------------------------------------------- /src/static/vendor/bootstrap/css/bootstrap-reboot.min.css: -------------------------------------------------------------------------------- 1 | html{box-sizing:border-box;font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-ms-overflow-style:scrollbar;-webkit-tap-highlight-color:transparent}*,::after,::before{box-sizing:inherit}@-ms-viewport{width:device-width}article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff}[tabindex="-1"]:focus{outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}dfn{font-style:italic}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent;-webkit-text-decoration-skip:objects}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([tabindex]){color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus,a:not([href]):not([tabindex]):hover{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg:not(:root){overflow:hidden}[role=button],a,area,button,input,label,select,summary,textarea{-ms-touch-action:manipulation;touch-action:manipulation}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#868e96;text-align:left;caption-side:bottom}th{text-align:left}label{display:inline-block;margin-bottom:.5rem}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item}template{display:none}[hidden]{display:none!important} 2 | /*# sourceMappingURL=bootstrap-reboot.min.css.map */ -------------------------------------------------------------------------------- /src/static/vendor/datatables/dataTables.bootstrap4.css: -------------------------------------------------------------------------------- 1 | table.dataTable { 2 | clear: both; 3 | margin-top: 6px !important; 4 | margin-bottom: 6px !important; 5 | max-width: none !important; 6 | border-collapse: separate !important; 7 | } 8 | table.dataTable td, 9 | table.dataTable th { 10 | -webkit-box-sizing: content-box; 11 | box-sizing: content-box; 12 | } 13 | table.dataTable td.dataTables_empty, 14 | table.dataTable th.dataTables_empty { 15 | text-align: center; 16 | } 17 | table.dataTable.nowrap th, 18 | table.dataTable.nowrap td { 19 | white-space: nowrap; 20 | } 21 | 22 | div.dataTables_wrapper div.dataTables_length label { 23 | font-weight: normal; 24 | text-align: left; 25 | white-space: nowrap; 26 | } 27 | div.dataTables_wrapper div.dataTables_length select { 28 | width: 75px; 29 | display: inline-block; 30 | } 31 | div.dataTables_wrapper div.dataTables_filter { 32 | text-align: right; 33 | } 34 | div.dataTables_wrapper div.dataTables_filter label { 35 | font-weight: normal; 36 | white-space: nowrap; 37 | text-align: left; 38 | } 39 | div.dataTables_wrapper div.dataTables_filter input { 40 | margin-left: 0.5em; 41 | display: inline-block; 42 | width: auto; 43 | } 44 | div.dataTables_wrapper div.dataTables_info { 45 | padding-top: 0.85em; 46 | white-space: nowrap; 47 | } 48 | div.dataTables_wrapper div.dataTables_paginate { 49 | margin: 0; 50 | white-space: nowrap; 51 | text-align: right; 52 | } 53 | div.dataTables_wrapper div.dataTables_paginate ul.pagination { 54 | margin: 2px 0; 55 | white-space: nowrap; 56 | justify-content: flex-end; 57 | } 58 | div.dataTables_wrapper div.dataTables_processing { 59 | position: absolute; 60 | top: 50%; 61 | left: 50%; 62 | width: 200px; 63 | margin-left: -100px; 64 | margin-top: -26px; 65 | text-align: center; 66 | padding: 1em 0; 67 | } 68 | 69 | table.dataTable thead > tr > th.sorting_asc, table.dataTable thead > tr > th.sorting_desc, table.dataTable thead > tr > th.sorting, 70 | table.dataTable thead > tr > td.sorting_asc, 71 | table.dataTable thead > tr > td.sorting_desc, 72 | table.dataTable thead > tr > td.sorting { 73 | padding-right: 30px; 74 | } 75 | table.dataTable thead > tr > th:active, 76 | table.dataTable thead > tr > td:active { 77 | outline: none; 78 | } 79 | table.dataTable thead .sorting, 80 | table.dataTable thead .sorting_asc, 81 | table.dataTable thead .sorting_desc, 82 | table.dataTable thead .sorting_asc_disabled, 83 | table.dataTable thead .sorting_desc_disabled { 84 | cursor: pointer; 85 | position: relative; 86 | } 87 | table.dataTable thead .sorting:before, table.dataTable thead .sorting:after, 88 | table.dataTable thead .sorting_asc:before, 89 | table.dataTable thead .sorting_asc:after, 90 | table.dataTable thead .sorting_desc:before, 91 | table.dataTable thead .sorting_desc:after, 92 | table.dataTable thead .sorting_asc_disabled:before, 93 | table.dataTable thead .sorting_asc_disabled:after, 94 | table.dataTable thead .sorting_desc_disabled:before, 95 | table.dataTable thead .sorting_desc_disabled:after { 96 | position: absolute; 97 | bottom: 0.9em; 98 | display: block; 99 | opacity: 0.3; 100 | } 101 | table.dataTable thead .sorting:before, 102 | table.dataTable thead .sorting_asc:before, 103 | table.dataTable thead .sorting_desc:before, 104 | table.dataTable thead .sorting_asc_disabled:before, 105 | table.dataTable thead .sorting_desc_disabled:before { 106 | right: 1em; 107 | content: "\2191"; 108 | } 109 | table.dataTable thead .sorting:after, 110 | table.dataTable thead .sorting_asc:after, 111 | table.dataTable thead .sorting_desc:after, 112 | table.dataTable thead .sorting_asc_disabled:after, 113 | table.dataTable thead .sorting_desc_disabled:after { 114 | right: 0.5em; 115 | content: "\2193"; 116 | } 117 | table.dataTable thead .sorting_asc:before, 118 | table.dataTable thead .sorting_desc:after { 119 | opacity: 1; 120 | } 121 | table.dataTable thead .sorting_asc_disabled:before, 122 | table.dataTable thead .sorting_desc_disabled:after { 123 | opacity: 0; 124 | } 125 | 126 | div.dataTables_scrollHead table.dataTable { 127 | margin-bottom: 0 !important; 128 | } 129 | 130 | div.dataTables_scrollBody table { 131 | border-top: none; 132 | margin-top: 0 !important; 133 | margin-bottom: 0 !important; 134 | } 135 | div.dataTables_scrollBody table thead .sorting:after, 136 | div.dataTables_scrollBody table thead .sorting_asc:after, 137 | div.dataTables_scrollBody table thead .sorting_desc:after { 138 | display: none; 139 | } 140 | div.dataTables_scrollBody table tbody tr:first-child th, 141 | div.dataTables_scrollBody table tbody tr:first-child td { 142 | border-top: none; 143 | } 144 | 145 | div.dataTables_scrollFoot table { 146 | margin-top: 0 !important; 147 | border-top: none; 148 | } 149 | 150 | @media screen and (max-width: 767px) { 151 | div.dataTables_wrapper div.dataTables_length, 152 | div.dataTables_wrapper div.dataTables_filter, 153 | div.dataTables_wrapper div.dataTables_info, 154 | div.dataTables_wrapper div.dataTables_paginate { 155 | text-align: center; 156 | } 157 | } 158 | table.dataTable.table-condensed > thead > tr > th { 159 | padding-right: 20px; 160 | } 161 | table.dataTable.table-condensed .sorting:after, 162 | table.dataTable.table-condensed .sorting_asc:after, 163 | table.dataTable.table-condensed .sorting_desc:after { 164 | top: 6px; 165 | right: 6px; 166 | } 167 | 168 | table.table-bordered.dataTable th, 169 | table.table-bordered.dataTable td { 170 | border-left-width: 0; 171 | } 172 | table.table-bordered.dataTable th:last-child, table.table-bordered.dataTable th:last-child, 173 | table.table-bordered.dataTable td:last-child, 174 | table.table-bordered.dataTable td:last-child { 175 | border-right-width: 0; 176 | } 177 | table.table-bordered.dataTable tbody th, 178 | table.table-bordered.dataTable tbody td { 179 | border-bottom-width: 0; 180 | } 181 | 182 | div.dataTables_scrollHead table.table-bordered { 183 | border-bottom-width: 0; 184 | } 185 | 186 | div.table-responsive > div.dataTables_wrapper > div.row { 187 | margin: 0; 188 | } 189 | div.table-responsive > div.dataTables_wrapper > div.row > div[class^="col-"]:first-child { 190 | padding-left: 0; 191 | } 192 | div.table-responsive > div.dataTables_wrapper > div.row > div[class^="col-"]:last-child { 193 | padding-right: 0; 194 | } 195 | -------------------------------------------------------------------------------- /src/static/vendor/datatables/dataTables.bootstrap4.js: -------------------------------------------------------------------------------- 1 | /*! DataTables Bootstrap 3 integration 2 | * ©2011-2015 SpryMedia Ltd - datatables.net/license 3 | */ 4 | 5 | /** 6 | * DataTables integration for Bootstrap 3. This requires Bootstrap 3 and 7 | * DataTables 1.10 or newer. 8 | * 9 | * This file sets the defaults and adds options to DataTables to style its 10 | * controls using Bootstrap. See http://datatables.net/manual/styling/bootstrap 11 | * for further information. 12 | */ 13 | (function( factory ){ 14 | if ( typeof define === 'function' && define.amd ) { 15 | // AMD 16 | define( ['jquery', 'datatables.net'], function ( $ ) { 17 | return factory( $, window, document ); 18 | } ); 19 | } 20 | else if ( typeof exports === 'object' ) { 21 | // CommonJS 22 | module.exports = function (root, $) { 23 | if ( ! root ) { 24 | root = window; 25 | } 26 | 27 | if ( ! $ || ! $.fn.dataTable ) { 28 | // Require DataTables, which attaches to jQuery, including 29 | // jQuery if needed and have a $ property so we can access the 30 | // jQuery object that is used 31 | $ = require('datatables.net')(root, $).$; 32 | } 33 | 34 | return factory( $, root, root.document ); 35 | }; 36 | } 37 | else { 38 | // Browser 39 | factory( jQuery, window, document ); 40 | } 41 | }(function( $, window, document, undefined ) { 42 | 'use strict'; 43 | var DataTable = $.fn.dataTable; 44 | 45 | 46 | /* Set the defaults for DataTables initialisation */ 47 | $.extend( true, DataTable.defaults, { 48 | dom: 49 | "<'row'<'col-sm-12 col-md-6'l><'col-sm-12 col-md-6'f>>" + 50 | "<'row'<'col-sm-12'tr>>" + 51 | "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>", 52 | renderer: 'bootstrap' 53 | } ); 54 | 55 | 56 | /* Default class modification */ 57 | $.extend( DataTable.ext.classes, { 58 | sWrapper: "dataTables_wrapper container-fluid dt-bootstrap4", 59 | sFilterInput: "form-control form-control-sm", 60 | sLengthSelect: "form-control form-control-sm", 61 | sProcessing: "dataTables_processing card", 62 | sPageButton: "paginate_button page-item" 63 | } ); 64 | 65 | 66 | /* Bootstrap paging button renderer */ 67 | DataTable.ext.renderer.pageButton.bootstrap = function ( settings, host, idx, buttons, page, pages ) { 68 | var api = new DataTable.Api( settings ); 69 | var classes = settings.oClasses; 70 | var lang = settings.oLanguage.oPaginate; 71 | var aria = settings.oLanguage.oAria.paginate || {}; 72 | var btnDisplay, btnClass, counter=0; 73 | 74 | var attach = function( container, buttons ) { 75 | var i, ien, node, button; 76 | var clickHandler = function ( e ) { 77 | e.preventDefault(); 78 | if ( !$(e.currentTarget).hasClass('disabled') && api.page() != e.data.action ) { 79 | api.page( e.data.action ).draw( 'page' ); 80 | } 81 | }; 82 | 83 | for ( i=0, ien=buttons.length ; i 0 ? 102 | '' : ' disabled'); 103 | break; 104 | 105 | case 'previous': 106 | btnDisplay = lang.sPrevious; 107 | btnClass = button + (page > 0 ? 108 | '' : ' disabled'); 109 | break; 110 | 111 | case 'next': 112 | btnDisplay = lang.sNext; 113 | btnClass = button + (page < pages-1 ? 114 | '' : ' disabled'); 115 | break; 116 | 117 | case 'last': 118 | btnDisplay = lang.sLast; 119 | btnClass = button + (page < pages-1 ? 120 | '' : ' disabled'); 121 | break; 122 | 123 | default: 124 | btnDisplay = button + 1; 125 | btnClass = page === button ? 126 | 'active' : ''; 127 | break; 128 | } 129 | 130 | if ( btnDisplay ) { 131 | node = $('
  • ', { 132 | 'class': classes.sPageButton+' '+btnClass, 133 | 'id': idx === 0 && typeof button === 'string' ? 134 | settings.sTableId +'_'+ button : 135 | null 136 | } ) 137 | .append( $('', { 138 | 'href': '#', 139 | 'aria-controls': settings.sTableId, 140 | 'aria-label': aria[ button ], 141 | 'data-dt-idx': counter, 142 | 'tabindex': settings.iTabIndex, 143 | 'class': 'page-link' 144 | } ) 145 | .html( btnDisplay ) 146 | ) 147 | .appendTo( container ); 148 | 149 | settings.oApi._fnBindAction( 150 | node, {action: button}, clickHandler 151 | ); 152 | 153 | counter++; 154 | } 155 | } 156 | } 157 | }; 158 | 159 | // IE9 throws an 'unknown error' if document.activeElement is used 160 | // inside an iframe or frame. 161 | var activeEl; 162 | 163 | try { 164 | // Because this approach is destroying and recreating the paging 165 | // elements, focus is lost on the select button which is bad for 166 | // accessibility. So we want to restore focus once the draw has 167 | // completed 168 | activeEl = $(host).find(document.activeElement).data('dt-idx'); 169 | } 170 | catch (e) {} 171 | 172 | attach( 173 | $(host).empty().html('