Skip to content

Commit b9c3a6d

Browse files
authored
Merge pull request #205 from mxmeinhold/stats
2 parents 6e1265e + 7db9966 commit b9c3a6d

File tree

5 files changed

+250
-69
lines changed

5 files changed

+250
-69
lines changed

packet/routes/api.py

Lines changed: 3 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from packet.models import Packet, MiscSignature, NotificationSubscription, Freshman, FreshSignature, UpperSignature
1818
from packet.notifications import packet_signed_notification, packet_100_percent_notification, \
1919
packet_starting_notification, packets_starting_notification
20+
import packet.stats as stats
2021

2122

2223
@app.route('/api/v1/freshmen', methods=['POST'])
@@ -255,80 +256,13 @@ def report(info):
255256
@app.route('/api/v1/stats/packet/<packet_id>')
256257
@packet_auth
257258
def packet_stats(packet_id):
258-
packet = Packet.by_id(packet_id)
259-
260-
dates = [packet.start.date() + timedelta(days=x) for x in range(0, (packet.end-packet.start).days + 1)]
261-
262-
print(dates)
263-
264-
upper_stats = {date: list() for date in dates}
265-
for uid, date in map(lambda sig: (sig.member, sig.updated),
266-
filter(lambda sig: sig.signed, packet.upper_signatures)):
267-
upper_stats[date.date()].append(uid)
268-
269-
fresh_stats = {date: list() for date in dates}
270-
for username, date in map(lambda sig: (sig.freshman_username, sig.updated),
271-
filter(lambda sig: sig.signed, packet.fresh_signatures)):
272-
fresh_stats[date.date()].append(username)
273-
274-
misc_stats = {date: list() for date in dates}
275-
for uid, date in map(lambda sig: (sig.member, sig.updated), packet.misc_signatures):
276-
misc_stats[date.date()].append(uid)
277-
278-
total_stats = dict()
279-
for date in dates:
280-
total_stats[date.isoformat()] = {
281-
'upper': upper_stats[date],
282-
'fresh': fresh_stats[date],
283-
'misc': misc_stats[date],
284-
}
285-
286-
return {
287-
'packet_id': packet_id,
288-
'dates': total_stats,
289-
}
290-
291-
292-
def sig2dict(sig):
293-
"""
294-
A utility function for upperclassman stats.
295-
Converts an UpperSignature to a dictionary with the date and the packet.
296-
"""
297-
packet = Packet.by_id(sig.packet_id)
298-
return {
299-
'date': sig.updated.date(),
300-
'packet': {
301-
'id': packet.id,
302-
'freshman_username': packet.freshman_username,
303-
},
304-
}
259+
return stats.packet_stats(packet_id)
305260

306261

307262
@app.route('/api/v1/stats/upperclassman/<uid>')
308263
@packet_auth
309264
def upperclassman_stats(uid):
310-
311-
sigs = UpperSignature.query.filter(
312-
UpperSignature.signed,
313-
UpperSignature.member == uid
314-
).all() + MiscSignature.query.filter(MiscSignature.member == uid).all()
315-
316-
sig_dicts = list(map(sig2dict, sigs))
317-
318-
dates = set(map(lambda sd: sd['date'], sig_dicts))
319-
320-
return {
321-
'member': uid,
322-
'signatures': {
323-
date.isoformat() : list(
324-
map(lambda sd: sd['packet'],
325-
filter(lambda sig, d=date: sig['date'] == d,
326-
sig_dicts
327-
)
328-
)
329-
) for date in dates
330-
}
331-
}
265+
return stats.upperclassman_stats(uid)
332266

333267
def commit_sig(packet, was_100, uid):
334268
packet_signed_notification(packet, uid)

packet/routes/upperclassmen.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""
22
Routes available to CSH users only
33
"""
4+
import json
45

56
from itertools import chain
67
from operator import itemgetter
@@ -10,6 +11,7 @@
1011
from packet.models import Packet, MiscSignature
1112
from packet.utils import before_request, packet_auth
1213
from packet.log_utils import log_cache, log_time
14+
from packet.stats import packet_stats
1315

1416

1517
@app.route('/')
@@ -61,3 +63,44 @@ def upperclassmen_total(info=None):
6163

6264
return render_template('upperclassmen_totals.html', info=info, num_open_packets=len(open_packets),
6365
upperclassmen=sorted(upperclassmen.items(), key=itemgetter(1), reverse=True))
66+
67+
68+
@app.route('/stats/packet/<packet_id>')
69+
@packet_auth
70+
@before_request
71+
def packet_graphs(packet_id, info=None):
72+
stats = packet_stats(packet_id)
73+
fresh = []
74+
misc = []
75+
upper = []
76+
77+
78+
# Make a rolling sum of signatures over time
79+
agg = lambda l, attr, date: l.append((l[-1] if l else 0) + len(stats['dates'][date][attr]))
80+
dates = list(stats['dates'].keys())
81+
for date in dates:
82+
agg(fresh, 'fresh', date)
83+
agg(misc, 'misc', date)
84+
agg(upper, 'upper', date)
85+
86+
# Stack misc and upper on top of fresh for a nice stacked line graph
87+
for i in range(len(dates)):
88+
misc[i] = misc[i] + fresh[i]
89+
upper[i] = upper[i] + misc[i]
90+
91+
return render_template('packet_stats.html',
92+
info=info,
93+
data=json.dumps({
94+
'dates':dates,
95+
'accum': {
96+
'fresh':fresh,
97+
'misc':misc,
98+
'upper':upper,
99+
},
100+
'daily': {
101+
102+
}
103+
}),
104+
fresh=stats['freshman'],
105+
packet=Packet.by_id(packet_id),
106+
)

packet/stats.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
from datetime import timedelta
2+
3+
from packet.models import Packet, MiscSignature, UpperSignature
4+
5+
6+
def packet_stats(packet_id):
7+
"""
8+
Gather statistics for a packet in the form of number of signatures per day
9+
10+
Return format: {
11+
packet_id,
12+
freshman: {
13+
name,
14+
rit_username,
15+
},
16+
dates: {
17+
<date>: {
18+
upper: [ uid ],
19+
misc: [ uid ],
20+
fresh: [ freshman_username ],
21+
},
22+
},
23+
}
24+
"""
25+
packet = Packet.by_id(packet_id)
26+
27+
dates = [packet.start.date() + timedelta(days=x) for x in range(0, (packet.end-packet.start).days + 1)]
28+
29+
print(dates)
30+
31+
upper_stats = {date: list() for date in dates}
32+
for uid, date in map(lambda sig: (sig.member, sig.updated),
33+
filter(lambda sig: sig.signed, packet.upper_signatures)):
34+
upper_stats[date.date()].append(uid)
35+
36+
fresh_stats = {date: list() for date in dates}
37+
for username, date in map(lambda sig: (sig.freshman_username, sig.updated),
38+
filter(lambda sig: sig.signed, packet.fresh_signatures)):
39+
fresh_stats[date.date()].append(username)
40+
41+
misc_stats = {date: list() for date in dates}
42+
for uid, date in map(lambda sig: (sig.member, sig.updated), packet.misc_signatures):
43+
misc_stats[date.date()].append(uid)
44+
45+
total_stats = dict()
46+
for date in dates:
47+
total_stats[date.isoformat()] = {
48+
'upper': upper_stats[date],
49+
'fresh': fresh_stats[date],
50+
'misc': misc_stats[date],
51+
}
52+
53+
return {
54+
'packet_id': packet_id,
55+
'freshman': {
56+
'name': packet.freshman.name,
57+
'rit_username': packet.freshman.rit_username,
58+
},
59+
'dates': total_stats,
60+
}
61+
62+
63+
def sig2dict(sig):
64+
"""
65+
A utility function for upperclassman stats.
66+
Converts an UpperSignature to a dictionary with the date and the packet.
67+
"""
68+
packet = Packet.by_id(sig.packet_id)
69+
return {
70+
'date': sig.updated.date(),
71+
'packet': {
72+
'id': packet.id,
73+
'freshman_username': packet.freshman_username,
74+
},
75+
}
76+
77+
78+
def upperclassman_stats(uid):
79+
"""
80+
Gather statistics for an upperclassman's signature habits
81+
82+
Return format: {
83+
member: <uid>,
84+
signautes: {
85+
<date>: [{
86+
id: <packet_id>,
87+
freshman_username,
88+
}],
89+
},
90+
}
91+
"""
92+
93+
sigs = UpperSignature.query.filter(
94+
UpperSignature.signed,
95+
UpperSignature.member == uid
96+
).all() + MiscSignature.query.filter(MiscSignature.member == uid).all()
97+
98+
sig_dicts = list(map(sig2dict, sigs))
99+
100+
dates = set(map(lambda sd: sd['date'], sig_dicts))
101+
102+
return {
103+
'member': uid,
104+
'signatures': {
105+
date.isoformat() : list(
106+
map(lambda sd: sd['packet'],
107+
filter(lambda sig, d=date: sig['date'] == d,
108+
sig_dicts
109+
)
110+
)
111+
) for date in dates
112+
}
113+
}

packet/templates/packet.html

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,13 @@ <h3>{{ get_rit_name(packet.freshman_username) }}</h3>
2222
{% endif %}
2323
</div>
2424
</div>
25+
<div class="row w-100 mb-1">
26+
{% if info.realm == "csh" %}
27+
<div class="col">
28+
<a class="btn btn-primary" style="float: right" href="{{ url_for('packet_graphs', packet_id=packet.id) }}">Graphs</a>
29+
</div>
30+
{% endif %}
31+
</div>
2532
<div class="row">
2633
<div class="col ml-1 mb-1">
2734
<h6>Signatures: <span class="badge badge-secondary">{{ received.total }}/{{ required.total }}</span></h6>

packet/templates/packet_stats.html

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
{% extends "extend/base.html" %}
2+
3+
{% block head %}
4+
{{ super() }}
5+
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.bundle.min.js"></script>
6+
{% endblock %}
7+
8+
{% block body %}
9+
<div class="container main">
10+
<div class="card">
11+
<h5 class="card-header bg-primary text-white">Cumulative Signatures Over Time for
12+
<a class="text-white" href="{{ url_for('freshman_packet', packet_id=packet.id) }}">
13+
<img class="eval-user-img"
14+
alt="{{ get_rit_name(packet.freshman_username) }}"
15+
src="{{ get_rit_image(packet.freshman_username) }}"
16+
width="25"
17+
height="25"/> {{ get_rit_name(packet.freshman_username) }}
18+
</a>
19+
</h5>
20+
<tr>
21+
<td data-priority="1">
22+
</td>
23+
</tr>
24+
<div class="card-body">
25+
<canvas id="myChart" width="400" height="400"></canvas>
26+
<script>
27+
var data = {{ data|safe }};
28+
// Stack the lines
29+
var ctx = document.getElementById('myChart');
30+
var myChart = new Chart(ctx, {
31+
type: 'line',
32+
data: {
33+
labels: data.dates,
34+
datasets: [
35+
{
36+
fill: 'origin',
37+
label: 'Fresh Sigs',
38+
data: data.accum.fresh,
39+
backgroundColor: '#b0197e80',
40+
borderColor: '#b0197e',
41+
borderWidth: 1,
42+
lineTension: 0
43+
},
44+
{
45+
fill: '-1',
46+
label: 'Misc Sigs',
47+
data: data.accum.misc,
48+
backgroundColor: '#0000ff80',
49+
borderColor: 'blue',
50+
borderWidth: 1,
51+
lineTension: 0
52+
},
53+
{
54+
fill: '-1',
55+
label: 'Upper Sigs',
56+
data: data.accum.upper,
57+
backgroundColor: '#00ff0080',
58+
borderColor: 'green',
59+
borderWidth: 1,
60+
lineTension: 0
61+
}
62+
]
63+
},
64+
options: {
65+
scales: {
66+
xAxes: [{
67+
type: 'time',
68+
time: {
69+
unit: 'day',
70+
},
71+
}],
72+
yAxes: [{
73+
ticks: {
74+
beginAtZero: true
75+
}
76+
}]
77+
}
78+
}
79+
});
80+
</script>
81+
</div>
82+
</div>
83+
</div>
84+
{% endblock %}

0 commit comments

Comments
 (0)