From b6c4f36fb381459e07ff569f94b851b22ff11ab8 Mon Sep 17 00:00:00 2001 From: Lucas McCullum Date: Wed, 3 Feb 2021 15:38:41 -0500 Subject: [PATCH 1/2] Adds credential application tracker to admin console --- .../console/static/console/css/console.css | 22 +++ .../console/static/console/js/console.js | 144 ++++++++++++++++++ .../templates/console/console_navbar.html | 3 + .../templates/console/graph_stats.html | 36 +++++ physionet-django/console/urls.py | 1 + physionet-django/console/views.py | 35 +++++ 6 files changed, 241 insertions(+) create mode 100644 physionet-django/console/templates/console/graph_stats.html diff --git a/physionet-django/console/static/console/css/console.css b/physionet-django/console/static/console/css/console.css index 960a209996..a6e02a42da 100644 --- a/physionet-django/console/static/console/css/console.css +++ b/physionet-django/console/static/console/css/console.css @@ -542,4 +542,26 @@ footer.sticky-footer { } .table-process { width: 11%; + +.axis path, +.axis line { + fill: none; + stroke: #000; + shape-rendering: crispEdges; +} + +.grid path, +.grid line { + fill: none; + stroke: rgba(0, 0, 0, 0.25); + shape-rendering: crispEdges; +} + +.x.axis path { + display: none; +} + +.line { + fill: none; + stroke-width: 2.5px; } diff --git a/physionet-django/console/static/console/js/console.js b/physionet-django/console/static/console/js/console.js index d2ea82f675..b892cdc1cc 100644 --- a/physionet-django/console/static/console/js/console.js +++ b/physionet-django/console/static/console/js/console.js @@ -36,3 +36,147 @@ event.preventDefault(); }); })(jQuery); // End of use strict + +// Define the chart (try to make dynamic) +var cred_chart = d3_cred_chart() + .width(0.845 * window.innerWidth) + .height(0.7 * window.innerHeight) + .x_label("Date") + .y_label("Pending Applications"); +// Define the canvas +var svg = d3.select("#cred_tracker").append("svg") + .datum(data) + .call(cred_chart); +// Create the figure +function d3_cred_chart() { + // Actually edit the chart + function chart(selection){ + selection.each(function(input_data) { + // Set the margins + var margin = {top: 20, right: 80, bottom: 80, left: 100}, + inner_width = width - margin.left - margin.right, + inner_height = height - margin.top - margin.bottom; + // Parse the input dates + var parse_time = d3.time.format("%d-%b-%y").parse; + input_data[0].x.forEach(function(d,i){ + input_data[0].x[i] = parse_time(d); + }); + // Set the x-axis scale + var x_scale = d3.time.scale() + .range([0, inner_width]) + .domain([d3.min(input_data, function(d) { return d3.min(d.x); }), + d3.max(input_data, function(d) { return d3.max(d.x); })]); + // Set the y-axis scale + var y_scale = d3.scale.linear() + .range([inner_height, 0]) + .domain([0, + d3.max(input_data, function(d) { return 1.1*d3.max(d.y); })]); + // Define the x-axis + var x_axis = d3.svg.axis() + .scale(x_scale) + .orient("bottom") + .tickFormat(d3.time.format("%d-%b-%y")); + // Define the y-axis + var y_axis = d3.svg.axis() + .scale(y_scale) + .orient("left"); + // Set the x-axis grid + var x_grid = d3.svg.axis() + .scale(x_scale) + .orient("bottom") + .tickSize(-inner_height) + .tickFormat(""); + // Set the y-axis grid + var y_grid = d3.svg.axis() + .scale(y_scale) + .orient("left") + .tickSize(-inner_width) + .tickFormat(""); + // Draw the actual line + var draw_line = d3.svg.line() + .interpolate("basis") + .x(function(d) { return x_scale(d[0]); }) + .y(function(d) { return y_scale(d[1]); }); + // Create the SVG object + var svg = d3.select(this) + .attr("width", width) + .attr("height", height) + .append("g") + .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); + // Set the inner padding for the x-axis + svg.append("g") + .attr("class", "x grid") + .attr("transform", "translate(0," + inner_height + ")") + .call(x_grid); + // Set the inner padding for the y-axis + svg.append("g") + .attr("class", "y grid") + .call(y_grid); + // Set the x-label + svg.append("g") + .attr("class", "x axis") + .attr("transform", "translate(0," + inner_height + ")") + .call(x_axis) + .selectAll("text") + .style("text-anchor", "middle") + .attr("dx", "-2.5em") + .attr("dy", "0.7em") + .attr("transform", function(d) { + return "rotate(-45)" + }) + // Set the y-label + svg.append("g") + .attr("class", "y axis") + .call(y_axis) + .append("text") + .attr("transform", "translate(0," + inner_height/2 + ")rotate(-90)") + .attr("dy", "-4em") + .attr('text-anchor', 'middle') + .text(y_label); + // Get the line data + var data_lines = svg.selectAll(".d3_cred_chart_line") + .data(input_data.map(function(d) { return d3.zip(d.x, d.y); })) + .enter().append("g") + .attr("class", "d3_cred_chart_line"); + // Draw the line and set the color + data_lines.append("path") + .attr("class", "line") + .attr("d", function(d){ return draw_line(d); }) + .attr("stroke", "red"); + }); + }; + // Set the canvas width + chart.width = function(value) { + if (!arguments.length) { + return width; + }; + width = value; + return chart; + }; + // Set the canvas height + chart.height = function(value) { + if (!arguments.length) { + return height; + }; + height = value; + return chart; + }; + // Set the x-label + chart.x_label = function(value) { + if (!arguments.length) { + return x_label; + }; + x_label = value; + return chart; + }; + // Set the y-label + chart.y_label = function(value) { + if (!arguments.length) { + return y_label; + }; + y_label = value; + return chart; + }; + // Render the figure + return chart; +}; diff --git a/physionet-django/console/templates/console/console_navbar.html b/physionet-django/console/templates/console/console_navbar.html index cceeafc99a..fd169c39ca 100644 --- a/physionet-django/console/templates/console/console_navbar.html +++ b/physionet-django/console/templates/console/console_navbar.html @@ -188,6 +188,9 @@ + diff --git a/physionet-django/console/templates/console/graph_stats.html b/physionet-django/console/templates/console/graph_stats.html new file mode 100644 index 0000000000..72823c7b17 --- /dev/null +++ b/physionet-django/console/templates/console/graph_stats.html @@ -0,0 +1,36 @@ +{% extends "console/base_console.html" %} + +{% load static %} + +{% block title %}Graphs{% endblock %} + +{% block local_js_top %} + + + + +{% endblock %} + +{% block content %} + +
+
+ Graph metrics +
+
+
+

Previous Credentialing Applications

+
+

This graph displays the number of pending credentialing applications over time. Pending applications are determined by taking the number of submitted applications and subtracting the number of fully processed applications (accept, reject, etc.).

+
+
+
+
+ +{% endblock %} diff --git a/physionet-django/console/urls.py b/physionet-django/console/urls.py index 8e4f228ba8..3896913e47 100644 --- a/physionet-django/console/urls.py +++ b/physionet-django/console/urls.py @@ -95,4 +95,5 @@ path('usage/editorial/stats/', views.editorial_stats, name='editorial_stats'), path('usage/credentialing/stats/', views.credentialing_stats, name='credentialing_stats'), + path('usage/graph/stats/', views.graph_stats, name='graph_stats'), ] diff --git a/physionet-django/console/views.py b/physionet-django/console/views.py index 6b99c10cd8..b0931afc29 100644 --- a/physionet-django/console/views.py +++ b/physionet-django/console/views.py @@ -1845,6 +1845,41 @@ def credentialing_stats(request): 'stats': stats}) +@login_required +@user_passes_test(is_admin, redirect_field_name='project_home') +def graph_stats(request): + """ + Graphs. + """ + apps = CredentialApplication.objects.all() + + now = datetime.now() + # Get the times of each application submission + app_times = [a.application_datetime for a in apps] + app_times.sort() + # Get the times of each application decision + des_times = [a.decision_datetime for a in apps if a.decision_datetime] + des_times.sort() + # Keep track of running total of applications in queue + all_times = app_times + des_times + all_times.sort() + net_apps = [0] * (len(all_times)) + for i,t in enumerate(all_times): + if i > 0: + net_apps[i] = net_apps[i-1] + if t in app_times: + net_apps[i] += 1 + if t in des_times: + net_apps[i] -= 1 + # Convert datetime objects to readable form + timeline = [t.strftime("%d-%b-%y") for t in all_times] + timeline = ','.join(timeline) + + return render(request, 'console/graph_stats.html', + {'stats_nav': True, 'submenu': 'graph', + 'timeline': timeline, 'net_apps': net_apps}) + + @login_required @user_passes_test(is_admin, redirect_field_name='project_home') def download_credentialed_users(request): From 9a090e3ce97c2f372e11d5288ac48ce848e5b9df Mon Sep 17 00:00:00 2001 From: Lucas McCullum Date: Wed, 3 Feb 2021 15:29:44 -0500 Subject: [PATCH 2/2] Fixes invalid demo credential application information --- physionet-django/console/static/console/css/console.css | 5 +++++ physionet-django/user/fixtures/demo-user.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/physionet-django/console/static/console/css/console.css b/physionet-django/console/static/console/css/console.css index a6e02a42da..088c229fff 100644 --- a/physionet-django/console/static/console/css/console.css +++ b/physionet-django/console/static/console/css/console.css @@ -531,17 +531,22 @@ footer.sticky-footer { table-layout: fixed; width: 100%; } + .table-index { width: 5%; } + .table-user { width: 10%; } + .table-elapsed { width: 9%; } + .table-process { width: 11%; +} .axis path, .axis line { diff --git a/physionet-django/user/fixtures/demo-user.json b/physionet-django/user/fixtures/demo-user.json index 049d083444..a1299b55f9 100644 --- a/physionet-django/user/fixtures/demo-user.json +++ b/physionet-django/user/fixtures/demo-user.json @@ -9779,7 +9779,7 @@ "pk": 1, "fields": { "slug": "YgqTQmcerj0gLx4MAaZV", - "application_datetime": "2020-03-31T18:20:33.850Z", + "application_datetime": "2018-10-14T18:20:33.850Z", "user": [ "rgmark" ],