def generate(name: str, cards: Dict[Tuple[int, int], List[Dict]], semester: Semester, filename: str) -> None: """ 生成 ics 文件并保存到目录 :param name: 姓名 :param cards: 参与的课程 :param semester: 当前导出的学期 :param filename: 输出的文件名称,带后缀 :return: None """ from everyclass.server import statsd with tracer.trace("calendar_init"): semester_string = semester.to_str(simplify=True) semester = semester.to_tuple() # 创建 calender 对象 cal = Calendar() cal.add('prodid', '-//Admirable//EveryClass//EN') cal.add('version', '2.0') cal.add('calscale', 'GREGORIAN') cal.add('method', 'PUBLISH') cal.add('X-WR-CALNAME', name + '的' + semester_string + '课表') cal.add('X-WR-TIMEZONE', 'Asia/Shanghai') # 时区 tzc.add_component(tzs) cal.add_component(tzc) with tracer.trace("add_events"): # 创建 events for time in range(1, 7): for day in range(1, 8): if (day, time) in cards: for card in cards[(day, time)]: for week in card['week']: dtstart = _get_datetime(week, day, get_time(time)[0], semester) dtend = _get_datetime(week, day, get_time(time)[1], semester) if dtstart.year == 1984: continue cal.add_component( _build_event(card_name=card['name'], times=(dtstart, dtend), classroom=card['classroom'], teacher=card['teacher'], week_string=card['week_string'], current_week=week, cid=card['cid'])) with tracer.trace("write_file"): with open(os.path.join(calendar_dir(), filename), 'wb') as f: data = cal.to_ical() statsd.histogram('calendar.ics.generate.size', len(data)) f.write(data)
def legacy_get_ics(student_id, semester_str): """ 早期 iCalendar 订阅端点,出于兼容性考虑保留,仅支持未设定隐私等级的学生,其他情况使用新的日历订阅令牌获得 ics 文件。 """ # fix parameters place = student_id.find('-') semester_str = student_id[place + 1:len(student_id)] + '-' + semester_str student_id = student_id[:place] semester = Semester(semester_str) search_result = Entity.search(student_id) if len(search_result.students) != 1: # bad request return abort(400) if semester.to_str() not in search_result.students[0].semesters: return abort(400) with tracer.trace('get_privacy_settings'): privacy_settings = PrivacySettings.get_level( search_result.students[0].student_id) if privacy_settings != 0: # force user to get a calendar token when the user is privacy-protected but accessed through legacy interface return "Visit {} to get your calendar".format( url_for("main.main", _external=True)), 401 else: token = CalendarToken.get_or_set_calendar_token( resource_type="student", identifier=search_result.students[0].student_id, semester=semester.to_str()) return redirect(url_for('calendar.ics_download', calendar_token=token))
def generate(name: str, cards: Dict[Tuple[int, int], List[Dict]], semester: Semester, ics_token: str): """ 生成 ics 文件并保存到目录 :param name: 姓名 :param cards: 参与的课程 :param semester: 当前导出的学期 :param ics_token: ics 令牌 :return: None """ semester_string = semester.to_str(simplify=True) semester = semester.to_tuple() # 创建 calender 对象 cal = Calendar() cal.add('prodid', '-//Admirable//EveryClass//EN') cal.add('version', '2.0') cal.add('calscale', 'GREGORIAN') cal.add('method', 'PUBLISH') cal.add('X-WR-CALNAME', name + '的' + semester_string + '课表') cal.add('X-WR-TIMEZONE', 'Asia/Shanghai') # 时区 tzc.add_component(tzs) cal.add_component(tzc) # 创建 events for time in range(1, 7): for day in range(1, 8): if (day, time) in cards: for card in cards[(day, time)]: for week in card['week']: dtstart = _get_datetime(week, day, get_time(time)[0], semester) dtend = _get_datetime(week, day, get_time(time)[1], semester) if dtstart.year == 1984: continue cal.add_component( _build_event(card_name=card['name'], times=(dtstart, dtend), classroom=card['classroom'], teacher=card['teacher'], week_string=card['week_string'], current_week=week, cid=card['cid'])) # 写入文件 import os with open( os.path.join(os.path.dirname(__file__), '../../../calendar_files/{}.ics'.format(ics_token)), 'w') as f: f.write(cal.to_ical().decode(encoding='utf-8'))
def ics_download(calendar_token: str): """ iCalendar ics file download 因为课表会更新,所以 ics 文件只能在这里动态生成,不能在日历订阅页面就生成 """ from collections import defaultdict from flask import send_from_directory from everyclass.server.db.dao import CalendarToken from everyclass.server.models import Semester from everyclass.server.calendar import ics_generator from everyclass.server.rpc.api_server import APIServer, teacher_list_to_name_str from everyclass.server.utils import lesson_string_to_tuple result = CalendarToken.find_calendar_token(token=calendar_token) if not result: return 'invalid calendar token', 404 CalendarToken.update_last_used_time(calendar_token) # 获得原始学号或教工号 if result['type'] == 'student': rpc_result = APIServer.get_student_timetable(result['identifier'], result['semester']) else: # teacher rpc_result = APIServer.get_teacher_timetable(result['identifier'], result['semester']) with elasticapm.capture_span('process_rpc_result'): semester = Semester(result['semester']) cards: Dict[Tuple[int, int], List[Dict]] = defaultdict(list) for card in rpc_result.cards: day, time = lesson_string_to_tuple(card.lesson) cards[(day, time)].append( dict(name=card.name, teacher=teacher_list_to_name_str(card.teachers), week=card.weeks, week_string=card.week_string, classroom=card.room, cid=card.card_id_encoded)) ics_generator.generate(name=rpc_result.name, cards=cards, semester=semester, ics_token=calendar_token) return send_from_directory("../../calendar_files", calendar_token + ".ics", as_attachment=True, mimetype='text/calendar')
def ics_download(calendar_token: str): """ iCalendar ics 文件下载 2019-8-25 改为预先缓存文件而非每次动态生成,降低 CPU 压力。如果一小时内两次访问则强刷缓存。 """ import time from everyclass.server import statsd if not is_valid_uuid(calendar_token): return 'invalid calendar token', 404 result = CalendarToken.find_calendar_token(token=calendar_token) if not result: return 'invalid calendar token', 404 CalendarToken.update_last_used_time(calendar_token) cal_dir = calendar_dir() cal_filename = f"{result['type']}_{result['identifier']}_{result['semester']}.ics" cal_full_path = os.path.join(cal_dir, cal_filename) # 有缓存、且缓存时间小于一天,且不用强刷缓存 if os.path.exists(cal_full_path) \ and time.time() - os.path.getmtime(cal_full_path) < SECONDS_IN_ONE_DAY \ and Redis.calendar_token_use_cache(calendar_token): logger.info("ics cache hit") statsd.increment("calendar.ics.cache.hit") return send_from_directory(cal_dir, cal_filename, as_attachment=True, mimetype='text/calendar') statsd.increment("calendar.ics.cache.miss") # 无缓存、或需要强刷缓存 with tracer.trace('rpc'): # 获得原始学号或教工号 if result['type'] == 'student': rpc_result = Entity.get_student_timetable(result['identifier'], result['semester']) else: # teacher rpc_result = Entity.get_teacher_timetable(result['identifier'], result['semester']) semester = Semester(result['semester']) cards: Dict[Tuple[int, int], List[Dict]] = defaultdict(list) for card in rpc_result.cards: cards[lesson_string_to_tuple(card.lesson)].append( dict(name=card.name, teacher=teacher_list_to_name_str(card.teachers), week=card.weeks, week_string=card.week_string, classroom=card.room, cid=card.card_id_encoded)) ics_generator.generate(name=rpc_result.name, cards=cards, semester=semester, filename=cal_filename) return send_from_directory(cal_dir, cal_filename, as_attachment=True, mimetype='text/calendar')
def test_semester_code(self): from everyclass.server.models import Semester self.assertTrue(Semester((2016, 2017, 2)).to_db_code() == "16_17_2")
def test_tuple_semester(self): from everyclass.server.models import Semester self.assertTrue(Semester('2016-2017-2').to_tuple() == (2016, 2017, 2))
def test_string_semester(self): from everyclass.server.models import Semester self.assertTrue(Semester((2016, 2017, 2)).to_str(simplify=False) == '2016-2017-2') self.assertTrue(Semester((2016, 2017, 2)).to_str(simplify=True) == '16-17-2')