"""Cyon interface implementation.
"""
import datetime
from uw.scientia.uef import *
[docs]def time_add (time, offset):
return (datetime.datetime.combine (datetime.date.today (), time) + offset).time ()
[docs]class UWExamsUEFFile (UEFFile):
def __init__ (self, cursor, term, timestamp=None):
termdata = cursor.execute_required_tuple ("with t as (select lower (term_exam_dates) as exams_startdate, upper (term_exam_dates) as exams_enddate, coalesce (%(timestamp)s, current_timestamp) as timestamp from uw_term where term_id = %(term_id)s) select *, date_trunc ('week', exams_startdate)::date as institution_startdate, date_trunc ('week', exams_enddate + '6 days'::interval)::date as institution_enddate from t", timestamp=timestamp, term_id=term.code ())
super ().__init__ (1, ORIGINATOR_ES, termdata.timestamp, 'UW RO Finals %s' % term.code (), termdata.institution_startdate, termdata.institution_enddate, '')
self.__cursor = cursor
self.__term = term
self.__exams_startdate = termdata.exams_startdate
self.__exams_enddate = termdata.exams_enddate
self.__all_weeks = frozenset (range (52))
self.__timeslot_duration = datetime.timedelta (hours=2, minutes=30)
@property
def cursor (self):
return self.__cursor
@property
def term (self):
return self.__term
@property
def exams_startdate (self):
return self.__exams_startdate
@property
def exams_enddate (self):
return self.__exams_enddate
@property
def all_weeks (self):
return self.__all_weeks
@property
def timeslot_duration (self):
return self.__timeslot_duration
@property
def term_condition (self):
"""Condition for finding everything relevant to this scheduling session.
"""
return "term_id = %(term_id)s"
@property
def exam_condition (self):
"""Condition for finding all examinations to be scheduled.
Unused at present because we are including all examinations and even
courses with no examination.
"""
return self.term_condition + " and schedule_admin_id is not null"
@property
def setup (self):
yield UEFComment ('---- Setup')
timeslot_gap = datetime.timedelta (hours=1)
day_start = datetime.time (9)
day_end = datetime.time (22)
key = 'UWATR'
yield UEFNameRecord (BASE_Institution, key, 'University of Waterloo')
# Time slot setup
for weekday in range (7):
for pref, duration in [
(False, self.timeslot_duration),
(True, datetime.timedelta (minutes=30)),
]:
yield UEFPreferenceTimeRecord (BASE_Institution, key, weekday, day_start, day_end, -10, pref)
for start in [time_add (datetime.time (9), (self.timeslot_duration + timeslot_gap) * i) for i in range (4)]:
yield UEFPreferenceTimeRecord (BASE_Institution, key, weekday,
start, time_add (start, duration), 10, pref)
# Examination period
yield UEFStartAndEndDaysRecord (BASE_Institution, key, self.convert_date (self.exams_startdate), self.convert_date (self.exams_enddate))
@property
def map_admins (self):
return self.__map_admins
@property
def administrators (self):
# Staff and Suitabilities
yield UEFComment ('---- Administrators')
self.__map_admins = {}
for administer_admin_id, key, name in [
(None, 'NOEX', 'No Exam'),
(None, 'SELF', 'Self Scheduled'),
(None, 'NOND', 'Scheduled Only - Department Space'),
(None, 'NONR', 'Scheduled Only - Registrar Space'),
(20050, 'ROO', 'RO Administered - Optometry'),
(20050, 'RO', 'RO Administered'),
(20080, 'WLU', 'WLU Administered'),
(20070, 'CEL', 'CEL Administered'),
]:
self.__map_admins[administer_admin_id] = key
yield UEFNameRecord (BASE_Suitability, key, name)
yield UEFNameRecord (BASE_Staff, key, name)
yield UEFSuitabilityRecord (BASE_Staff, key, '+', key, False)
yield UEFStartAndEndDaysRecord (BASE_Staff, key, self.convert_date (self.exams_startdate), self.convert_date (self.exams_enddate))
yield UEFWeekPatternRecord (BASE_Staff, key, self.all_weeks)
@property
def locations (self):
# Old file has 5 real locations: PAC, RCH, STC, DC/M3, MC
# "WLU" stays for WLU exams
# "Scheduled Only (Dept Space)" becomes "DEPT"
# "Scheduled Only" becomes is absorbed into scheduled & administered
# "CEL" isn't really a location and we use real locations
# "Non Scheduled Exams" disappears along with the exams
# Some oddities; we expect numbers lower than actual to try to avoid
# problems with leaving space in rooms so exams don't have to share.
# Still:
# STC - 400, actually has 768
# DC/M3 - 300, actually has 446 + 267 = 713
# MC - 200, actually has 1163
# RCH - 400, actually has 846
# These "actually" numbers are a bit high because they include non-RO
# space, but still should check with Annette.
base = BASE_Location
yield UEFComment ('---- Locations')
for key, name, capacity, suitabilities in [
# Fake room for no-exam courses
('NOEX', 'No Exam', 0, ('NOEX',)),
# Real locations
('PAC', 'PAC', 900, ('RO', 'CEL',)),
('STC', 'STC', 400, ('RO', 'NONR',)),
('STP', 'STP 105', 200, ('ROO',)),
('DC', 'DC 1350/1351', 250, ('RO', 'NONR', 'CEL',)),
('M3', 'M3 1006', 175, ('RO', 'NONR',)),
('MC', 'MC', 400, ('RO', 'NONR', 'CEL',)),
('RCH', 'RCH', 400, ('NONR',)),
# Default to this for scheduled-only:
('RO', 'Other Registrar Space', 500, ('NONR',)),
# Annette will change some exams to this:
('DEPT', 'Department Space', 500, ('NOND', 'SELF',)),
# Extra space to account for CEL remote centres etc.
('CEL', 'CEL Overflow', 0, ('CEL',)),
# Fake room for WLU exams
('WLU', 'WLU', 10000, ('WLU',)),
]:
yield UEFNameRecord (base, key, name)
yield UEFSizeCapacityRecord (base, key, capacity)
for s in suitabilities:
yield UEFSuitabilityRecord (base, key, '+', s, False)
yield UEFStartAndEndDaysRecord (base, key, self.convert_date (self.exams_startdate), self.convert_date (self.exams_enddate))
yield UEFWeekPatternRecord (base, key, self.all_weeks)
@property
def exams (self):
# Examination groups
schedulers = self.cursor.execute_tuples ("with t as (select distinct schedule_admin_id as admin_id from exam_exam where (%s)) select admin_id, 'SCHED' || admin_id as group_key, admin_description || ' Scheduled' as group_description from t natural join teaching_admin" % self.term_condition, term_id=self.term.code ())
exam_groups = { None: None }
for s in schedulers:
exam_groups[s.admin_id] = s.group_key
yield UEFNameGroupRecord (BASE_Examination, s.group_key, s.group_description)
yield UEFMemberToGroupRecord (BASE_Examination, s.group_key, '-all', None)
exams = self.cursor.execute_tuples ("select coalesce (exam_id, -admin_id) as exam_id, administer_admin_id, schedule_admin_id, coalesce (exam_exam_full_title (exam_id), admin_description || ' No Exam') as exam_exam_full_title, exam_duration, teaching_admin_write_all_sections (term_id, admin_id) as sections, schedule_special_requests from teaching_admin_term natural join teaching_admin left join (select * from exam_exam where (series_code, series_sequence) = ('F', 1)) as ee using (term_id, admin_id) natural left join teaching_admin_term_exam_schedule where (%s) order by exam_id" % self.term_condition, term_id=self.term.code ())
base = BASE_Examination
yield UEFComment ('---- Examinations')
for exam in exams:
key = str (exam.exam_id)
yield UEFNameRecord (base, key, exam.exam_exam_full_title)
yield UEFDescriptionRecord (base, key, exam.sections)
yield UEFStudentRecord (base, key, '-all', None)
if exam.exam_duration is not None:
yield UEFDurationRecord (base, key, exam.exam_duration)
# Scheduling notes
yield UEFNotesRecord (base, key, '-all', None)
if exam.schedule_special_requests is not None:
for line in exam.schedule_special_requests.split ('\n'):
yield UEFNotesRecord (base, key, '+', line)
# Administration information
if exam.exam_id < 0:
admin = 'NOEX'
elif exam.schedule_admin_id is None:
admin = 'SELF'
else:
admin = self.map_admins[exam.administer_admin_id]
yield UEFRequiredSuitabilityForStaffMemberRecord (base, key, '-all', None)
yield UEFRequiredSuitabilityForStaffMemberRecord (base, key, '+', admin)
yield UEFRequiredSuitabilityForLocationRecord (base, key, '-all', None)
yield UEFRequiredSuitabilityForLocationRecord (base, key, '+', admin)
# Group by scheduler
group_key = exam_groups[exam.schedule_admin_id]
if group_key is not None:
yield UEFMemberToGroupRecord (base, group_key, '+', key)
# CEL time slots
if admin == 'CEL':
for weekday in range (7):
yield UEFUnavailablesAcrossWeekPatternRecord (base, key, '+', self.all_weeks, weekday, datetime.time.min, datetime.time.max)
for sitting in self.cursor.execute_tuples ("select * from exam_sitting where (sitting_term_id, sitting_admin_id) = (%(term_id)s, 23767)", term_id=self.term.code ()):
day = self.convert_date (sitting.start_time.date ())
yield UEFUnavailablesAcrossWeekPatternRecord (base, key, '-', {day // 7}, day % 7, sitting.start_time.time (), time_add (sitting.start_time.time (), self.timeslot_duration))
@property
def candidates (self):
# If a student drops a course they need to be dropped from their exams.
# This is handled by doing a -all on each exam rather than here because
# we would have to find all students who had previously been registered
# for each exam.
students = self.cursor.execute_tuples ("with s as (select person_id, coalesce (exam_id, -admin_id) as exam_id from (select term_id, admin_id, person_id from teaching_admin_registration where dropped is null union select term_id, admin_id, person_id from exam_exam_student_sitting natural join exam_exam where (series_code, series_sequence) = ('F', 1)) as rcc left join (select * from exam_exam where (series_code, series_sequence) = ('F', 1)) as ee using (term_id, admin_id) where (%s)), t as (select uw_id::quest_uw_id, array_agg (exam_id order by exam_id) as exams from s natural join person_identity_complete group by uw_id) select to_char (uw_id) as uw_id, exams, email, last_name, first_name from t natural join std_student natural join std_name_preferred order by uw_id" % self.term_condition, term_id=self.term.code ())
base = BASE_Student
yield UEFComment ('---- Students')
for student in students:
key = student.uw_id
yield UEFNameRecord (base, key, '%s, %s' % (student.last_name, student.first_name))
yield UEFDescriptionRecord (base, key, student.email)
for exam in student.exams:
yield UEFExaminationsRecord (base, key, '+', str (exam), False)
@property
def data (self):
yield from self.setup
yield from self.administrators
yield from self.locations
yield from self.exams
yield from self.candidates
yield UEFComment ('---- End of file')