Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Attendance-over-time chart #3189

Merged
merged 3 commits into from
Dec 31, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 13 additions & 11 deletions esp/esp/program/modules/handlers/bigboardmodule.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ def bigboard(self, request, tl, one, two, module, extra, prog):
numbers = [(desc, num) for desc, num in numbers if num]

timess = [
("completed the medical form", [(1, time) for time in self.times_medical(prog)]),
("signed up for classes", [(1, time) for time in self.times_classes(prog)]),
("completed the medical form", [(1, time) for time in self.times_medical(prog)], True),
("signed up for classes", [(1, time) for time in self.times_classes(prog)], True),
]

timess_data, start = self.make_graph_data(timess, 4, 0, 5)
Expand Down Expand Up @@ -240,12 +240,12 @@ def times_classes(self, prog):
return sorted(ssi_times_dict.itervalues())

@staticmethod
def chunk_times(times, start, end, delta=datetime.timedelta(0, 3600)):
def chunk_times(times, start, end, delta=datetime.timedelta(0, 3600), cumulative = True):
"""Given a list of times, return hourly summaries.

`times` should be a list of tuples, sorted by time, containing some metric (duration, capacity, etc.) and datetime.datetime objects
`start` and `end` should be datetimes; the chunks will be for hours
between them, inclusive.
`start` and `end` should be datetimes; the chunks will be for hours between them, inclusive.
`cumulative` should be a boolean determining whether counts should be summed cumulatively for conseculative hours or if they should only be summed within individual hours
Returns a list of integers, each of which is the number of times that
precede the given hour.
"""
Expand All @@ -266,36 +266,38 @@ def chunk_times(times, start, end, delta=datetime.timedelta(0, 3600)):
# times preceding it for the previous hour.
chunks.append(float(count))
start += delta
if not cumulative:
count = 0
return chunks

@staticmethod
def make_graph_data(timess, drop_beg = 0, drop_end = 0, cutoff = 1):
"""Given a list of time series, return graph data series.

`timess` should be a list of pairs (description, sorted tuples of metrics and datetime.datetime objects).
`timess` should be a list of tuples (description, sorted tuples of metrics and datetime.datetime objects, whether counts should be cumulative).
`drop_beg` should be a number of items to drop from the beginning of each list
`drop_end` should be a number of items to drop from the end of each list
`cutoff` should be the minimum number of items that must exist in a time series

Returns a dict of cleaned time series and the start time for graphing
"""
#Remove any time series without at least 'cutoff' times
timess = [(desc, times) for desc, times in timess if len(times) >= cutoff]
timess = [(desc, times, cumulative) for desc, times, cumulative in timess if len(times) >= cutoff]
# Drop the first and last times if specified
# Then round start down and end up to the nearest day.
if not timess:
graph_data = []
start = None
else:
start = min([times[drop_beg:(len(times)-drop_end)][0][1] for desc, times in timess])
start = min([times[drop_beg:(len(times)-drop_end)][0][1] for desc, times, cumulative in timess])
start = start.replace(hour=0, minute=0, second=0, microsecond=0)
end = max([times[drop_beg:(len(times)-drop_end)][-1][1] for desc, times in timess])
end = max([times[drop_beg:(len(times)-drop_end)][-1][1] for desc, times, cumulative in timess])
end = end.replace(hour=0, minute=0, second=0, microsecond=0)
end += datetime.timedelta(1)
end = min(end, datetime.datetime.now())
graph_data = [{"description": desc,
"data": BigBoardModule.chunk_times(times, start, end)}
for desc, times in timess]
"data": BigBoardModule.chunk_times(times, start, end, cumulative = cumulative)}
for desc, times, cumulative in timess]
return graph_data, start

# runs in 9ms, so don't bother caching
Expand Down
53 changes: 52 additions & 1 deletion esp/esp/program/modules/handlers/onsiteattendance.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,18 @@
Email: web-team@learningu.org
"""

from django.db.models.aggregates import Min
from django.db.models.query import Q

from argcache import cache_function_for

from esp.program.modules.base import ProgramModuleObj, needs_onsite, main_call, aux_call
from esp.program.models import StudentRegistration, ClassSection
from esp.utils.web import render_to_response
from esp.users.models import ESPUser
from esp.users.models import ESPUser, Record
from esp.cal.models import Event
from esp.utils.query_utils import nest_Q
from esp.program.modules.handlers.bigboardmodule import BigBoardModule
from esp.program.modules.handlers.teacherclassregmodule import TeacherClassRegModule

import datetime
Expand Down Expand Up @@ -126,9 +130,56 @@ def attendance(self, request, tl, one, two, module, extra, prog):
'not_attending': not_attending,
'no_attendance': no_attendance
})
else:
att_dict = self.times_attending_class(prog)
att_keys = sorted(att_dict.keys())
timess = [
("checked in to the program", [(1, time) for time in self.times_checked_in(prog)], True), # cumulative
("attended a class", [(len(att_dict[time]), time) for time in att_keys], False), # not cumulative
]
timess_data, start = BigBoardModule.make_graph_data(timess)
context["left_axis_data"] = [{"axis_name": "# students", "series_data": timess_data}]
context["first_hour"] = start

return render_to_response(self.baseDir()+'attendance.html', request, context)

@cache_function_for(105)
def times_checked_in(self, prog):
return list(
Record.objects
.filter(program=prog, event='attended')
.values('user').annotate(Min('time'))
.order_by('time__min').values_list('time__min', flat=True))

@cache_function_for(105)
def times_attending_class(self, prog):
srs = StudentRegistration.objects.filter(section__parent_class__parent_program=prog,
relationship__name="Attended", section__meeting_times__isnull=False
).order_by('start_date')
att_dict = {}
for sr in srs:
# For classes that are multiple hours, we want to count a student for
# each hour starting from when they are marked and ending at the end of the class
# Also, for multi-week programs (e.g. Sprout), we want to adjust the start and end times based on the attendance sr
start_time = sr.start_date.replace(minute = 0, second = 0, microsecond = 0)
end_time = sr.section.end_time().end.replace(
year = sr.start_date.year, month = sr.start_date.month, day = sr.start_date.day,
minute = 0, second = 0, microsecond = 0)
user = sr.user
time = start_time
# loop through hours until we get to the end time of the section
while(True):
if time in att_dict:
# Only count each student a maximum of one time per hour
if user not in att_dict[time]:
att_dict[time].append(user)
else:
att_dict[time] = [user]
time = time + datetime.timedelta(hours = 1)
if time > end_time:
break
return att_dict

@aux_call
@needs_onsite
def section_attendance(self, request, tl, one, two, module, extra, prog):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ def phasezero(self, request, tl, one, two, module, extra, prog):
context['grade_caps'] = sorted(prog.grade_caps().iteritems())

recs = PhaseZeroRecord.objects.filter(program=prog).order_by('time')
timess = [("number of lottery students", [(rec.user.count(), rec.time) for rec in recs])]
timess = [("number of lottery students", [(rec.user.count(), rec.time) for rec in recs], True)]
timess_data, start = BigBoardModule.make_graph_data(timess)
context["left_axis_data"] = [{"axis_name": "#", "series_data": timess_data}]
context["first_hour"] = start
Expand Down
16 changes: 8 additions & 8 deletions esp/esp/program/modules/handlers/teacherbigboardmodule.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,10 @@ def teacherbigboard(self, request, tl, one, two, module, extra, prog):
start = self.mindate
else:
timess = [
("number of registered classes", [(1, time) for time in self.reg_classes(prog)]),
("number of approved classes", [(1, time) for time in self.reg_classes(prog, True)]),
("number of teachers registered", [(1, time) for time in self.teach_times(prog)]),
("number of teachers approved", [(1, time) for time in self.teach_times(prog, True)]),
("number of registered classes", [(1, time) for time in self.reg_classes(prog)], True),
("number of approved classes", [(1, time) for time in self.reg_classes(prog, True)], True),
("number of teachers registered", [(1, time) for time in self.teach_times(prog)], True),
("number of teachers approved", [(1, time) for time in self.teach_times(prog, True)], True),
]

timess_data, start = BigBoardModule.make_graph_data(timess)
Expand All @@ -88,14 +88,14 @@ def teacherbigboard(self, request, tl, one, two, module, extra, prog):
class_hours_approved, student_hours_approved = self.get_hours(prog, approved = True)

class_hourss = [
("number of registered class-hours", class_hours),
("number of approved class-hours", class_hours_approved),
("number of registered class-hours", class_hours, True),
("number of approved class-hours", class_hours_approved, True),
]
class_hourss_data, _ = BigBoardModule.make_graph_data(class_hourss)

student_hourss = [
("number of registered class-student-hours", student_hours),
("number of approved class-student-hours", student_hours_approved),
("number of registered class-student-hours", student_hours, True),
("number of approved class-student-hours", student_hours_approved, True),
]
student_hourss_data, _ = BigBoardModule.make_graph_data(student_hourss)

Expand Down
11 changes: 11 additions & 0 deletions esp/templates/program/modules/onsiteattendance/attendance.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,21 @@
table-layout: fixed;
overflow-wrap: break-word;
}
#highcharts-placeholder {
width: 100%;
height: 250px;
}
</style>
{% endblock %}

{% block xtrajs %}
{{ block.super }}
<script src="/media/scripts/sorttable.js"></script>
{% if not timeslot %}
<script type="text/javascript" src="//code.highcharts.com/highcharts.js"></script>
<script type="text/javascript" src="//code.highcharts.com/highcharts-more.js"></script>
<script type="text/javascript" src="//code.highcharts.com/modules/no-data-to-display.js"></script>
{% endif %}
{% endblock %}

{% block content %}
Expand Down Expand Up @@ -68,6 +77,8 @@ <h1>Student Attendance for {{ program.niceName }}</h1>
</form>
<br />
<a class="btn" href="/onsite/{{ one }}/{{ two }}/section_attendance/{{ timeslot.id }}">See or record section attendance{% if timeslot %} for this timeslot{% endif %}</a>
{% else %}
{% include "program/modules/bigboardmodule/bigboard_graph.html" %}
{% endif %}
<br /><br />
</center>
Expand Down