aboutsummaryrefslogtreecommitdiff
path: root/packet
diff options
context:
space:
mode:
authorDevin Matte <devinmatte@gmail.com>2020-08-19 10:29:51 -0400
committerDevin Matte <devinmatte@gmail.com>2020-08-19 10:29:51 -0400
commite1ab5a398f3b3f89ed0a8fd4e89fd929f111186c (patch)
treee17c93d54a3b45fccae6647c1aa109c21cf4469d /packet
parente32a90448b322020a3fa0be5423797e668e8116f (diff)
parentb9c3a6dec3f392b81f0156117b5674cb1f4a2bf5 (diff)
Merge remote-tracking branch 'upstream/develop' into admin_ui
Diffstat (limited to 'packet')
-rw-r--r--packet/routes/api.py71
-rw-r--r--packet/routes/upperclassmen.py43
-rw-r--r--packet/stats.py113
-rw-r--r--packet/templates/packet.html7
-rw-r--r--packet/templates/packet_stats.html84
5 files changed, 250 insertions, 68 deletions
diff --git a/packet/routes/api.py b/packet/routes/api.py
index a3eaa86..6a80478 100644
--- a/packet/routes/api.py
+++ b/packet/routes/api.py
@@ -18,6 +18,7 @@ from packet.utils import before_request, packet_auth, notify_slack, sync_freshma
from packet.models import Packet, MiscSignature, NotificationSubscription, Freshman, FreshSignature, UpperSignature
from packet.notifications import packet_signed_notification, packet_100_percent_notification, \
packet_starting_notification, packets_starting_notification
+import packet.stats as stats
class POSTFreshman:
@@ -231,79 +232,13 @@ def report(info):
@app.route('/api/v1/stats/packet/<packet_id>')
@packet_auth
def packet_stats(packet_id):
- packet = Packet.by_id(packet_id)
-
- dates = [packet.start.date() + timedelta(days=x) for x in range(0, (packet.end - packet.start).days + 1)]
-
- print(dates)
-
- upper_stats = {date: list() for date in dates}
- for uid, date in map(lambda sig: (sig.member, sig.updated),
- filter(lambda sig: sig.signed, packet.upper_signatures)):
- upper_stats[date.date()].append(uid)
-
- fresh_stats = {date: list() for date in dates}
- for username, date in map(lambda sig: (sig.freshman_username, sig.updated),
- filter(lambda sig: sig.signed, packet.fresh_signatures)):
- fresh_stats[date.date()].append(username)
-
- misc_stats = {date: list() for date in dates}
- for uid, date in map(lambda sig: (sig.member, sig.updated), packet.misc_signatures):
- misc_stats[date.date()].append(uid)
-
- total_stats = dict()
- for date in dates:
- total_stats[date.isoformat()] = {
- 'upper': upper_stats[date],
- 'fresh': fresh_stats[date],
- 'misc': misc_stats[date],
- }
-
- return {
- 'packet_id': packet_id,
- 'dates': total_stats,
- }
-
-
-def sig2dict(sig):
- """
- A utility function for upperclassman stats.
- Converts an UpperSignature to a dictionary with the date and the packet.
- """
- packet = Packet.by_id(sig.packet_id)
- return {
- 'date': sig.updated.date(),
- 'packet': {
- 'id': packet.id,
- 'freshman_username': packet.freshman_username,
- },
- }
+ return stats.packet_stats(packet_id)
@app.route('/api/v1/stats/upperclassman/<uid>')
@packet_auth
def upperclassman_stats(uid):
- sigs = UpperSignature.query.filter(
- UpperSignature.signed,
- UpperSignature.member == uid
- ).all() + MiscSignature.query.filter(MiscSignature.member == uid).all()
-
- sig_dicts = list(map(sig2dict, sigs))
-
- dates = set(map(lambda sd: sd['date'], sig_dicts))
-
- return {
- 'member': uid,
- 'signatures': {
- date.isoformat(): list(
- map(lambda sd: sd['packet'],
- filter(lambda sig, d=date: sig['date'] == d,
- sig_dicts
- )
- )
- ) for date in dates
- }
- }
+ return stats.upperclassman_stats(uid)
def commit_sig(packet, was_100, uid):
diff --git a/packet/routes/upperclassmen.py b/packet/routes/upperclassmen.py
index c8cdd7c..11bc2b1 100644
--- a/packet/routes/upperclassmen.py
+++ b/packet/routes/upperclassmen.py
@@ -1,6 +1,7 @@
"""
Routes available to CSH users only
"""
+import json
from itertools import chain
from operator import itemgetter
@@ -10,6 +11,7 @@ from packet import app
from packet.models import Packet, MiscSignature
from packet.utils import before_request, packet_auth
from packet.log_utils import log_cache, log_time
+from packet.stats import packet_stats
@app.route('/')
@@ -61,3 +63,44 @@ def upperclassmen_total(info=None):
return render_template('upperclassmen_totals.html', info=info, num_open_packets=len(open_packets),
upperclassmen=sorted(upperclassmen.items(), key=itemgetter(1), reverse=True))
+
+
+@app.route('/stats/packet/<packet_id>')
+@packet_auth
+@before_request
+def packet_graphs(packet_id, info=None):
+ stats = packet_stats(packet_id)
+ fresh = []
+ misc = []
+ upper = []
+
+
+ # Make a rolling sum of signatures over time
+ agg = lambda l, attr, date: l.append((l[-1] if l else 0) + len(stats['dates'][date][attr]))
+ dates = list(stats['dates'].keys())
+ for date in dates:
+ agg(fresh, 'fresh', date)
+ agg(misc, 'misc', date)
+ agg(upper, 'upper', date)
+
+ # Stack misc and upper on top of fresh for a nice stacked line graph
+ for i in range(len(dates)):
+ misc[i] = misc[i] + fresh[i]
+ upper[i] = upper[i] + misc[i]
+
+ return render_template('packet_stats.html',
+ info=info,
+ data=json.dumps({
+ 'dates':dates,
+ 'accum': {
+ 'fresh':fresh,
+ 'misc':misc,
+ 'upper':upper,
+ },
+ 'daily': {
+
+ }
+ }),
+ fresh=stats['freshman'],
+ packet=Packet.by_id(packet_id),
+ )
diff --git a/packet/stats.py b/packet/stats.py
new file mode 100644
index 0000000..2ac5fb2
--- /dev/null
+++ b/packet/stats.py
@@ -0,0 +1,113 @@
+from datetime import timedelta
+
+from packet.models import Packet, MiscSignature, UpperSignature
+
+
+def packet_stats(packet_id):
+ """
+ Gather statistics for a packet in the form of number of signatures per day
+
+ Return format: {
+ packet_id,
+ freshman: {
+ name,
+ rit_username,
+ },
+ dates: {
+ <date>: {
+ upper: [ uid ],
+ misc: [ uid ],
+ fresh: [ freshman_username ],
+ },
+ },
+ }
+ """
+ packet = Packet.by_id(packet_id)
+
+ dates = [packet.start.date() + timedelta(days=x) for x in range(0, (packet.end-packet.start).days + 1)]
+
+ print(dates)
+
+ upper_stats = {date: list() for date in dates}
+ for uid, date in map(lambda sig: (sig.member, sig.updated),
+ filter(lambda sig: sig.signed, packet.upper_signatures)):
+ upper_stats[date.date()].append(uid)
+
+ fresh_stats = {date: list() for date in dates}
+ for username, date in map(lambda sig: (sig.freshman_username, sig.updated),
+ filter(lambda sig: sig.signed, packet.fresh_signatures)):
+ fresh_stats[date.date()].append(username)
+
+ misc_stats = {date: list() for date in dates}
+ for uid, date in map(lambda sig: (sig.member, sig.updated), packet.misc_signatures):
+ misc_stats[date.date()].append(uid)
+
+ total_stats = dict()
+ for date in dates:
+ total_stats[date.isoformat()] = {
+ 'upper': upper_stats[date],
+ 'fresh': fresh_stats[date],
+ 'misc': misc_stats[date],
+ }
+
+ return {
+ 'packet_id': packet_id,
+ 'freshman': {
+ 'name': packet.freshman.name,
+ 'rit_username': packet.freshman.rit_username,
+ },
+ 'dates': total_stats,
+ }
+
+
+def sig2dict(sig):
+ """
+ A utility function for upperclassman stats.
+ Converts an UpperSignature to a dictionary with the date and the packet.
+ """
+ packet = Packet.by_id(sig.packet_id)
+ return {
+ 'date': sig.updated.date(),
+ 'packet': {
+ 'id': packet.id,
+ 'freshman_username': packet.freshman_username,
+ },
+ }
+
+
+def upperclassman_stats(uid):
+ """
+ Gather statistics for an upperclassman's signature habits
+
+ Return format: {
+ member: <uid>,
+ signautes: {
+ <date>: [{
+ id: <packet_id>,
+ freshman_username,
+ }],
+ },
+ }
+ """
+
+ sigs = UpperSignature.query.filter(
+ UpperSignature.signed,
+ UpperSignature.member == uid
+ ).all() + MiscSignature.query.filter(MiscSignature.member == uid).all()
+
+ sig_dicts = list(map(sig2dict, sigs))
+
+ dates = set(map(lambda sd: sd['date'], sig_dicts))
+
+ return {
+ 'member': uid,
+ 'signatures': {
+ date.isoformat() : list(
+ map(lambda sd: sd['packet'],
+ filter(lambda sig, d=date: sig['date'] == d,
+ sig_dicts
+ )
+ )
+ ) for date in dates
+ }
+ }
diff --git a/packet/templates/packet.html b/packet/templates/packet.html
index 62708a7..d15f3ad 100644
--- a/packet/templates/packet.html
+++ b/packet/templates/packet.html
@@ -22,6 +22,13 @@
{% endif %}
</div>
</div>
+ <div class="row w-100 mb-1">
+ {% if info.realm == "csh" %}
+ <div class="col">
+ <a class="btn btn-primary" style="float: right" href="{{ url_for('packet_graphs', packet_id=packet.id) }}">Graphs</a>
+ </div>
+ {% endif %}
+ </div>
<div class="row">
<div class="col ml-1 mb-1">
<h6>Signatures: <span class="badge badge-secondary">{{ received.total }}/{{ required.total }}</span></h6>
diff --git a/packet/templates/packet_stats.html b/packet/templates/packet_stats.html
new file mode 100644
index 0000000..7742397
--- /dev/null
+++ b/packet/templates/packet_stats.html
@@ -0,0 +1,84 @@
+{% extends "extend/base.html" %}
+
+{% block head %}
+{{ super() }}
+<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.bundle.min.js"></script>
+{% endblock %}
+
+{% block body %}
+<div class="container main">
+ <div class="card">
+ <h5 class="card-header bg-primary text-white">Cumulative Signatures Over Time for
+ <a class="text-white" href="{{ url_for('freshman_packet', packet_id=packet.id) }}">
+ <img class="eval-user-img"
+ alt="{{ get_rit_name(packet.freshman_username) }}"
+ src="{{ get_rit_image(packet.freshman_username) }}"
+ width="25"
+ height="25"/> {{ get_rit_name(packet.freshman_username) }}
+ </a>
+</h5>
+ <tr>
+ <td data-priority="1">
+ </td>
+ </tr>
+ <div class="card-body">
+ <canvas id="myChart" width="400" height="400"></canvas>
+ <script>
+var data = {{ data|safe }};
+// Stack the lines
+var ctx = document.getElementById('myChart');
+var myChart = new Chart(ctx, {
+ type: 'line',
+ data: {
+ labels: data.dates,
+ datasets: [
+ {
+ fill: 'origin',
+ label: 'Fresh Sigs',
+ data: data.accum.fresh,
+ backgroundColor: '#b0197e80',
+ borderColor: '#b0197e',
+ borderWidth: 1,
+ lineTension: 0
+ },
+ {
+ fill: '-1',
+ label: 'Misc Sigs',
+ data: data.accum.misc,
+ backgroundColor: '#0000ff80',
+ borderColor: 'blue',
+ borderWidth: 1,
+ lineTension: 0
+ },
+ {
+ fill: '-1',
+ label: 'Upper Sigs',
+ data: data.accum.upper,
+ backgroundColor: '#00ff0080',
+ borderColor: 'green',
+ borderWidth: 1,
+ lineTension: 0
+ }
+ ]
+ },
+ options: {
+ scales: {
+ xAxes: [{
+ type: 'time',
+ time: {
+ unit: 'day',
+ },
+ }],
+ yAxes: [{
+ ticks: {
+ beginAtZero: true
+ }
+ }]
+ }
+ }
+});
+ </script>
+ </div>
+ </div>
+</div>
+{% endblock %}