Skip to content

Commit e1ab5a3

Browse files
committed
Merge remote-tracking branch 'upstream/develop' into admin_ui
2 parents e32a904 + b9c3a6d commit e1ab5a3

File tree

5 files changed

+250
-68
lines changed

5 files changed

+250
-68
lines changed

packet/routes/api.py

Lines changed: 3 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from packet.models import Packet, MiscSignature, NotificationSubscription, Freshman, FreshSignature, UpperSignature
1919
from packet.notifications import packet_signed_notification, packet_100_percent_notification, \
2020
packet_starting_notification, packets_starting_notification
21+
import packet.stats as stats
2122

2223

2324
class POSTFreshman:
@@ -231,79 +232,13 @@ def report(info):
231232
@app.route('/api/v1/stats/packet/<packet_id>')
232233
@packet_auth
233234
def packet_stats(packet_id):
234-
packet = Packet.by_id(packet_id)
235-
236-
dates = [packet.start.date() + timedelta(days=x) for x in range(0, (packet.end - packet.start).days + 1)]
237-
238-
print(dates)
239-
240-
upper_stats = {date: list() for date in dates}
241-
for uid, date in map(lambda sig: (sig.member, sig.updated),
242-
filter(lambda sig: sig.signed, packet.upper_signatures)):
243-
upper_stats[date.date()].append(uid)
244-
245-
fresh_stats = {date: list() for date in dates}
246-
for username, date in map(lambda sig: (sig.freshman_username, sig.updated),
247-
filter(lambda sig: sig.signed, packet.fresh_signatures)):
248-
fresh_stats[date.date()].append(username)
249-
250-
misc_stats = {date: list() for date in dates}
251-
for uid, date in map(lambda sig: (sig.member, sig.updated), packet.misc_signatures):
252-
misc_stats[date.date()].append(uid)
253-
254-
total_stats = dict()
255-
for date in dates:
256-
total_stats[date.isoformat()] = {
257-
'upper': upper_stats[date],
258-
'fresh': fresh_stats[date],
259-
'misc': misc_stats[date],
260-
}
261-
262-
return {
263-
'packet_id': packet_id,
264-
'dates': total_stats,
265-
}
266-
267-
268-
def sig2dict(sig):
269-
"""
270-
A utility function for upperclassman stats.
271-
Converts an UpperSignature to a dictionary with the date and the packet.
272-
"""
273-
packet = Packet.by_id(sig.packet_id)
274-
return {
275-
'date': sig.updated.date(),
276-
'packet': {
277-
'id': packet.id,
278-
'freshman_username': packet.freshman_username,
279-
},
280-
}
235+
return stats.packet_stats(packet_id)
281236

282237

283238
@app.route('/api/v1/stats/upperclassman/<uid>')
284239
@packet_auth
285240
def upperclassman_stats(uid):
286-
sigs = UpperSignature.query.filter(
287-
UpperSignature.signed,
288-
UpperSignature.member == uid
289-
).all() + MiscSignature.query.filter(MiscSignature.member == uid).all()
290-
291-
sig_dicts = list(map(sig2dict, sigs))
292-
293-
dates = set(map(lambda sd: sd['date'], sig_dicts))
294-
295-
return {
296-
'member': uid,
297-
'signatures': {
298-
date.isoformat(): list(
299-
map(lambda sd: sd['packet'],
300-
filter(lambda sig, d=date: sig['date'] == d,
301-
sig_dicts
302-
)
303-
)
304-
) for date in dates
305-
}
306-
}
241+
return stats.upperclassman_stats(uid)
307242

308243

309244
def commit_sig(packet, was_100, 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)