Source code for uw.local.teaching.db.cyon

"""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')