Beispiel #1
0
def generate(name: str, courses: Dict[Tuple[int, int], List[Dict]],
             semester: Semester, ics_token: str):
    """
    生成 ics 文件并保存到目录

    :param name: 姓名
    :param courses: 参与的课程
    :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 courses:
                for course in courses[(day, time)]:
                    for week in course['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(course_name=course['name'],
                                         times=(dtstart, dtend),
                                         classroom=course['classroom'],
                                         teacher=course['teacher'],
                                         week_string=course['week_string'],
                                         current_week=week,
                                         cid=course['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'))
Beispiel #2
0
def legacy_get_ics(student_id, semester_str):
    """
    legacy iCalendar endpoint

    query the student first, if the student is not privacy protected, redirect to new ics. else return 401.

    this route is bad. however, many users have already been using it. breaking experience is bad. so we have
    to keep the route here for now. and (maybe) remove it in the future.
    """
    from flask import current_app as app, abort, redirect, url_for

    from everyclass.server.db.dao import PrivacySettingsDAO, CalendarTokenDAO
    from everyclass.server.utils.rpc import HttpRpc
    from everyclass.server.db.model import Semester

    # 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)

    with elasticapm.capture_span('rpc_search'):
        rpc_result = HttpRpc.call_with_error_page('{}/v1/search/{}'.format(
            app.config['API_SERVER_BASE_URL'], student_id),
                                                  retry=True)
        if isinstance(rpc_result, str):
            return rpc_result
        api_response = rpc_result

    if len(api_response['student']) != 1:
        # bad request
        return abort(400)

    if semester.to_str() not in api_response['student'][0]['semester']:
        return abort(400)

    with elasticapm.capture_span('get_privacy_settings'):
        privacy_settings = PrivacySettingsDAO.get_level(
            api_response['student'][0]['sid_orig'])

    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")), 401
    else:
        token = CalendarTokenDAO.get_or_set_calendar_token(
            resource_type="student",
            identifier=api_response['student'][0]['sid_orig'],
            semester=semester.to_str())
        return redirect(url_for('calendar.ics_download', calendar_token=token))
Beispiel #3
0
def get_courses(student_id, semester):
    """
    提供学号与学期,返回学生当前学期的课表。

    :param student_id: 学号
    :param semester: 学期
    """
    import json
    from everyclass.server.db.dao import get_classes_for_student
    from everyclass.server.db.model import Semester
    from everyclass.server.exceptions import IllegalSemesterException, NoStudentException

    try:
        courses = get_classes_for_student(student_id, Semester(semester))
        # TODO: handle if semester does not exist

        elasticapm.set_custom_context(stu_id=student_id, semester=semester)

        courses_to_return = {}
        for k, v in courses.items():
            if str(k[0]) not in courses_to_return:
                courses_to_return[str(k[0])] = {}
            courses_to_return[str(k[0])][str(k[1])] = v
        return json.dumps({'courses': courses_to_return})
    except IllegalSemesterException:
        response = jsonify({'error': 'wrong semester'})
        response.status_code = 400
        return response
    except NoStudentException:
        response = jsonify({'error': 'no such student'})
        response.status_code = 400
        return response
Beispiel #4
0
def cal_page():
    """课表导出页面视图函数"""
    from everyclass.server.calendar import ics_generator
    from everyclass.server.db.dao import get_classes_for_student, get_my_semesters, check_if_stu_exist
    from everyclass.server.db.model import Semester

    # 如果请求中包含 id 就写入 session
    if request.values.get('id'):
        if not check_if_stu_exist(request.values.get('id')):
            flash("你输入的学号不存在")
            return redirect(url_for("main.main"))
        session['stu_id'] = request.values.get('id')

    # 如果 session 中有 stu_id 就生成 ics 并返回页面,没有就跳转回首页
    if session.get('stu_id', None):
        # 获得学生姓名和他的合法学期
        my_available_semesters, student_name = get_my_semesters(
            session['stu_id'])
        semester = Semester.get()
        student_classes = get_classes_for_student(session['stu_id'], semester)
        ics_generator.generate(session['stu_id'], student_name,
                               student_classes, semester.to_str(simplify=True),
                               semester.to_tuple())
        return render_template('ics.html',
                               student_id=session['stu_id'],
                               semester=semester.to_str(simplify=True))
    else:
        return redirect(url_for('main.main'))
Beispiel #5
0
def ics_download(calendar_token):
    """
    iCalendar ics file download

    因为课表会更新,所以 ics 文件只能在这里动态生成,不能在日历订阅页面就生成
    """
    from flask import send_from_directory, current_app
    from everyclass.server.db.dao import CalendarTokenDAO
    from everyclass.server.db.model import Semester
    from everyclass.server.calendar import ics_generator
    from everyclass.server.utils.rpc import HttpRpc
    from everyclass.server.utils import teacher_list_fix
    from everyclass.server.utils import teacher_list_to_str
    from everyclass.server.utils import lesson_string_to_dict

    result = CalendarTokenDAO.find_calendar_token(token=calendar_token)
    if not result:
        return 'invalid calendar token', 404

    with elasticapm.capture_span('rpc_find_people'):
        rpc_result = HttpRpc.call_with_error_page('{}/v1/{}/{}/{}'.format(
            current_app.config['API_SERVER_BASE_URL'], result['type'],
            result['sid'] if result['type'] == 'student' else result['tid'],
            result['semester']),
                                                  params={
                                                      'week_string': 'true'
                                                  },
                                                  retry=True)
        if isinstance(rpc_result, str):
            return rpc_result
        api_response = rpc_result

    with elasticapm.capture_span('process_rpc_result'):
        semester = Semester(result['semester'])

        courses = dict()
        for each_class in api_response['course']:
            day, time = lesson_string_to_dict(each_class['lesson'])
            if (day, time) not in courses:
                courses[(day, time)] = list()
            courses[(day, time)].append(
                dict(name=each_class['name'],
                     teacher=teacher_list_to_str(
                         teacher_list_fix(each_class['teacher'])),
                     week=each_class['week'],
                     week_string=each_class['week_string'],
                     classroom=each_class['room'],
                     classroom_id=each_class['rid'],
                     cid=each_class['cid']))

    ics_generator.generate(name=api_response['name'],
                           courses=courses,
                           semester=semester,
                           ics_token=calendar_token)

    return send_from_directory("../../calendar_files",
                               calendar_token + ".ics",
                               as_attachment=True,
                               mimetype='text/calendar')
Beispiel #6
0
def get_classes_for_student(student_id: str, sem: Semester) -> dict:
    """
    获得一个学生在指定学期的全部课程
    如学生不存在当前学期则引出 NoStudentException

    :param student_id: 学号
    :param sem: 学期,`Semester` 类型对象
    :return: dict,键为 (day, time),值为课程列表,每一节课为一个包含了课程名称、老师、上课时间、地点信息的 dict
    """
    from everyclass.server.exceptions import NoStudentException, IllegalSemesterException

    db = get_connection()
    cursor = db.cursor()

    # 初步合法性检验
    if sem.to_tuple() not in app.config['AVAILABLE_SEMESTERS']:
        raise IllegalSemesterException('No such semester for the student')

    mysql_query = "SELECT classes FROM ec_students_" + sem.to_db_code(
    ) + " WHERE xh=%s"
    cursor.execute(mysql_query, (student_id, ))
    result = cursor.fetchall()
    if not result:
        cursor.close()
        raise NoStudentException(student_id)
    else:
        courses_list = json.loads(result[0][0])
        courses = dict()
        for classes in courses_list:
            mysql_query = "SELECT clsname,day,time,teacher,duration,week,location,id FROM {} WHERE id=%s" \
                .format("ec_classes_" + sem.to_db_code())
            cursor.execute(mysql_query, (classes, ))
            result = cursor.fetchall()
            if (result[0][1], result[0][2]) not in courses:
                courses[(result[0][1], result[0][2])] = list()
            courses[(result[0][1], result[0][2])].append(
                dict(name=result[0][0],
                     teacher=result[0][3],
                     duration=result[0][4],
                     week=result[0][5],
                     location=result[0][6],
                     id=result[0][7]))
        cursor.close()
        return courses
Beispiel #7
0
def get_ics(student_id, semester_str):
    """
    iCalendar service
    """
    from everyclass.server.calendar import ics_generator
    from everyclass.server.db.dao import check_if_stu_exist, get_my_semesters, get_classes_for_student
    from everyclass.server.db.model import Semester
    from everyclass.server.exceptions import IllegalSemesterException
    from everyclass.server import logger

    # TODO: generate ics here and return it to user, instead of generating .ics files in other places.
    # 临时 fix
    place = student_id.find('-')
    semester_str = student_id[place + 1:len(student_id)] + '-' + semester_str
    student_id = student_id[:place]

    # 学号检测
    if not check_if_stu_exist(student_id):
        flash("{} 学号不存在".format(student_id))
        logger.warning("[ics] {} 学号不存在".format(student_id))
        return redirect(url_for("main.main"))

    # 学期检测
    my_available_semesters, student_name = get_my_semesters(student_id)
    try:
        semester = Semester(semester_str)
    except IllegalSemesterException:
        flash("{} 学期格式错误".format(semester_str))
        logger.warning("{} 学期格式错误".format(semester_str))
        return redirect(url_for("main.main"))
    if semester not in my_available_semesters:
        flash("{} 学期不适用于此学生".format(semester_str))
        logger.warning("{} 学期不适用于此学生".format(semester_str))
        return redirect(url_for("main.main"))

    student_classes = get_classes_for_student(student_id, semester)
    ics_generator.generate(student_id, student_name, student_classes,
                           semester.to_str(simplify=True), semester.to_tuple())

    return send_from_directory("../../calendar_files",
                               student_id + "-" + semester_str + ".ics",
                               as_attachment=True,
                               mimetype='text/calendar')
def batch_generate():
    """生成当前学期所有学生的 ics 文件,每次更新当前学期数据后使用"""
    from everyclass.server import create_app
    from everyclass.server.db.dao import get_all_students, get_classes_for_student
    from everyclass.server.db.model import Semester

    config = get_config()
    now_semester = Semester(config.DEFAULT_SEMESTER)
    now_semester_str = str(now_semester)

    with create_app(offline=True).app_context():
        students = get_all_students()
        print("Total {} students".format(len(students)))
        for each in students:
            if now_semester_str in each[2]:
                print("Generate .ics for [{}]{}...".format(each[0], each[1]))
                student_classes = get_classes_for_student(
                    each[0], now_semester)
                generate(student_id=each[0],
                         student_name=each[1],
                         student_classes=student_classes,
                         semester_string=now_semester.to_str(simplify=True),
                         semester=now_semester.to_tuple())
        print("Done.")
Beispiel #9
0
def get_students_in_class(class_id: str):
    """
    获得一门课程的全部学生

    :param class_id: 班级 ID
    :return: 若有学生,返回课程名称、课程时间(day、time)、任课教师、学生列表(包含姓名、学号、学院、专业、班级),
    否则引出 exception
    """
    from everyclass.server.db.model import Semester
    from everyclass.server.exceptions import NoStudentException, NoClassException

    mysql_query = "SELECT students,clsname,day,time,teacher FROM {} WHERE id=%s" \
        .format('ec_classes_' + Semester.get().to_db_code())
    db = get_connection()
    cursor = db.cursor()
    cursor.execute(mysql_query, (class_id, ))
    result = cursor.fetchall()
    if not result:
        cursor.close()
        raise NoClassException(class_id)
    else:
        students = json.loads(result[0][0])
        students_info = list()
        class_name = result[0][1]
        class_day = result[0][2]
        class_time = result[0][3]
        class_teacher = result[0][4]
        if not students:
            cursor.close()
            raise NoStudentException

        students_in_sql = ''
        for each_student in students:
            students_in_sql += "'" + each_student + "',"
        students_in_sql = '(' + students_in_sql[0:len(students_in_sql) -
                                                1] + ')'
        print(students_in_sql)
        mysql_query = "SELECT xh, name, faculty, class_name FROM ec_students WHERE xh IN {}".format(
            students_in_sql)
        cursor.execute(mysql_query)
        result = cursor.fetchall()
        if result:
            # 信息包含姓名、学号、学院、专业、班级
            for each in result:
                students_info.append([each[1], each[0], each[2], each[3]])
        cursor.close()
        return class_name, class_day, class_time, class_teacher, students_info
Beispiel #10
0
def get_students_in_class(class_id):
    """
    获得一门课程的全部学生,若有学生,返回课程名称、课程时间(day、time)、任课教师、学生列表(包含姓名、学号、学院、专业、班级),
    否则引出 exception
    :param class_id:
    :return:
    """
    from everyclass.server.db.model import Semester
    from everyclass.server.exceptions import NoStudentException, NoClassException

    mysql_query = "SELECT students,clsname,day,time,teacher FROM {} WHERE id=%s" \
        .format('ec_classes_' + Semester.get().to_db_code())
    db = get_local_conn()
    cursor = db.cursor()
    cursor.execute(mysql_query, (class_id, ))
    result = cursor.fetchall()
    if not result:
        cursor.close()
        raise NoClassException(class_id)
    else:
        students = json.loads(result[0][0])
        students_info = list()
        class_name = result[0][1]
        class_day = result[0][2]
        class_time = result[0][3]
        class_teacher = result[0][4]
        if not students:
            cursor.close()
            raise NoStudentException
        for each_student in students:
            mysql_query = "SELECT name FROM ec_students WHERE xh=%s"
            cursor.execute(mysql_query, (each_student, ))
            result = cursor.fetchall()
            if result:
                # 信息包含姓名、学号、学院、专业、班级
                students_info.append([
                    result[0][0], each_student,
                    faculty_lookup(each_student),
                    class_lookup(each_student)
                ])
        cursor.close()
        return class_name, class_day, class_time, class_teacher, students_info
Beispiel #11
0
def cal_page():
    """课表导出页面视图函数"""
    from everyclass.server.db.dao import check_if_stu_exist
    from everyclass.server.db.model import Semester

    # 如果请求中包含 id 就写入 session
    if request.values.get('id'):
        if not check_if_stu_exist(request.values.get('id')):
            flash("你输入的学号不存在")
            return redirect(url_for("main.main"))
        session['stu_id'] = request.values.get('id')

    # 如果 session 中有 stu_id 就生成 ics 并返回页面,没有就跳转回首页
    if session.get('stu_id', None):
        # 获得学生姓名和他的合法学期
        semester = Semester.get()
        return render_template('ics.html',
                               student_id=session['stu_id'],
                               semester=semester.to_str(simplify=True))
    else:
        return redirect(url_for('main.main'))
Beispiel #12
0
def get_my_semesters(student_id: str) -> (list, str):
    """
    查询某一学生的可用学期
    ORM中应该做到 Student 类里
    """
    from everyclass.server.db.model import Semester

    mysql_query = "SELECT semesters,name FROM ec_students WHERE xh=%s"
    db = get_local_conn()
    cursor = db.cursor()
    cursor.execute(mysql_query, (student_id, ))
    result = cursor.fetchall()
    if not result:
        logger.error("[db.dao.get_my_semesters] No result from db.",
                     stack=True)
    sems = json.loads(result[0][0])
    student_name = result[0][1]

    semesters = []
    for each_sem in sems:
        semesters.append(Semester(each_sem))

    # print('[db_operations.get_my_semesters] semesters=', semesters)
    return semesters, student_name
Beispiel #13
0
def get_my_semesters(student_id: str) -> (list, str):
    """
    查询某一学生的可用学期

    :param student_id: 学生学号
    :return: 学期列表,学生姓名
    """
    mysql_query = "SELECT semesters,name FROM ec_students WHERE xh=%s"
    db = get_connection()
    cursor = db.cursor()
    cursor.execute(mysql_query, (student_id, ))
    result = cursor.fetchall()
    if not result:
        logger.error("[db.dao.get_my_semesters] No result from db.",
                     stack=True)
    sems = json.loads(result[0][0])
    student_name = result[0][1]

    semesters = []
    for each_sem in sems:
        semesters.append(Semester(each_sem))

    # print('[db_operations.get_my_semesters] semesters=', semesters)
    return semesters, student_name
Beispiel #14
0
def query():
    """
    查询本人课表视图函数
    正常情况应该是 post 方法,但是也兼容 get 防止意外情况,提高用户体验
    """
    from flask import request, render_template, redirect, url_for, session
    from flask import current_app as app
    import elasticapm

    from everyclass.server.tools import is_chinese_char
    from everyclass.server.exceptions import NoStudentException, IllegalSemesterException
    from everyclass.server.db.mysql import get_local_conn
    from everyclass.server.db.dao import faculty_lookup
    from everyclass.server.db.dao import class_lookup
    from everyclass.server.db.dao import get_classes_for_student
    from everyclass.server.db.model import Semester
    from everyclass.server.db.dao import get_privacy_settings
    from everyclass.server.db.dao import get_my_semesters
    from everyclass.server.db.dao import check_if_stu_exist

    # if under maintenance, return to maintenance.html
    if app.config["MAINTENANCE"]:
        return render_template("maintenance.html")

    db = get_local_conn()
    cursor = db.cursor()

    # 如 URL 中有 id 参数,判断是姓名还是学号,然后赋学号给student_id
    if request.values.get('id'):
        id_or_name = request.values.get('id')

        # 首末均为中文,判断为人名
        if is_chinese_char(id_or_name[0:1]) and is_chinese_char(
                id_or_name[-1:]):
            # 使用人名查询打点
            elasticapm.tag(ec_query_method='by_name')

            mysql_query = "SELECT name,xh FROM ec_students WHERE name=%s"
            cursor.execute(mysql_query, (id_or_name, ))
            result = cursor.fetchall()
            if cursor.rowcount > 1:
                # 查询到多个同名,进入选择界面
                students_list = list()
                for each_student in result:
                    students_list.append([
                        each_student[0], each_student[1],
                        faculty_lookup(each_student[1]),
                        class_lookup(each_student[1])
                    ])
                return render_template("query_same_name.html",
                                       count=cursor.rowcount,
                                       student_info=students_list)
            elif cursor.rowcount == 1:
                # 仅能查询到一个人,则赋值学号
                student_id = result[0][1]
            else:
                # 查无此人
                elasticapm.tag(ec_query_not_found=True)
                return _no_student_handle(id_or_name)

        # id 不为中文,则为学号
        else:
            # 学号查询打点
            elasticapm.tag(ec_query_method='by_id')
            student_id = request.values.get('id')

            # 判断学号是否有效
            if not check_if_stu_exist(student_id):
                elasticapm.tag(ec_query_not_found=True)
                return _no_student_handle(student_id)

        # 写入 session 的学号一定有效
        session['stu_id'] = student_id

    # url 中没有 id 参数但 session 中有
    elif session.get('stu_id', None):
        elasticapm.tag(ec_query_method='by_session')
        student_id = session['stu_id']

    # 既没有 id 参数也没有 session,无法知道需要查询谁的课表,返回主页
    else:
        elasticapm.tag(ec_query_method='exception')
        return redirect(url_for('main.main'))

    # 查询学生本人的可用学期
    my_available_semesters, student_name = get_my_semesters(student_id)

    # 如果没有学期,则直接返回
    if not my_available_semesters:
        logger.warning('Not any semester in ec_student', stack=True)
        return _no_student_handle()

    # 如URL参数中包含学期,判断有效性后更新 session
    if request.values.get('semester'):
        try:
            sem = Semester(request.values.get('semester'))
            if sem in my_available_semesters:
                session['semester'] = sem.to_tuple()
                if app.config['DEBUG']:
                    print('[query.query] updated session semester to',
                          Semester(session['semester']).to_str())

        # 用户指定的学期格式不合法
        except IllegalSemesterException:
            if app.config['DEBUG']:
                print('[query.query] IllegalSemesterException handled.' +
                      Semester(session['semester']).to_str())
            session['semester'] = my_available_semesters[-1].to_tuple()

    cursor.close()  # 关闭数据库连接

    # 如果 session 中无学期或学期无效,回落到本人可用最新学期
    # session 中学期使用 tuple 保存,因为 Semester 对象无法被序列化
    semester = session.get('semester', None)
    if not semester or Semester(semester) not in my_available_semesters:
        session['semester'] = my_available_semesters[-1].to_tuple()

    try:
        student_classes = get_classes_for_student(student_id=student_id,
                                                  sem=Semester(
                                                      session['semester']))
    except NoStudentException:
        return _no_student_handle(student_id)
    else:
        # 空闲周末判断,考虑到大多数人周末都是没有课程的
        empty_weekend = True
        for cls_time in range(1, 7):
            for cls_day in range(6, 8):
                if (cls_day, cls_time) in student_classes:
                    empty_weekend = False

        # 空闲课程判断,考虑到大多数人11-12节都是没有课程的
        empty_6 = True
        for cls_day in range(1, 8):
            if (cls_day, 6) in student_classes:
                empty_6 = False
        empty_5 = True
        for cls_day in range(1, 8):
            if (cls_day, 5) in student_classes:
                empty_5 = False

        # available_semesters 为当前学生所能选择的学期,是一个list。
        # 当中每一项又是一个包含两项的list,第一项为学期string,第二项为True/False表示是否为当前学期。
        available_semesters = []

        for each_semester in my_available_semesters:
            if session['semester'] == each_semester:
                available_semesters.append([each_semester, True])
            else:
                available_semesters.append([each_semester, False])

        # Privacy settings
        # Available privacy settings: "show_table_on_page", "import_to_calender", "major"
        privacy_settings = get_privacy_settings(student_id)

        # privacy on
        if "show_table_on_page" in privacy_settings:
            return render_template(
                'blocked.html',
                name=student_name,
                falculty=faculty_lookup(student_id),
                class_name=class_lookup(student_id),
                stu_id=student_id,
                available_semesters=available_semesters,
                no_import_to_calender=True
                if "import_to_calender" in privacy_settings else False)

        # privacy off
        return render_template('query.html',
                               name=student_name,
                               falculty=faculty_lookup(student_id),
                               class_name=class_lookup(student_id),
                               stu_id=student_id,
                               classes=student_classes,
                               empty_wkend=empty_weekend,
                               empty_6=empty_6,
                               empty_5=empty_5,
                               available_semesters=available_semesters)
Beispiel #15
0
 def test_semester_code(self):
     from everyclass.server.db.model import Semester
     self.assertTrue(Semester((2016, 2017, 2)).to_db_code() == "16_17_2")
Beispiel #16
0
 def test_tuple_semester(self):
     from everyclass.server.db.model import Semester
     self.assertTrue(Semester('2016-2017-2').to_tuple() == (2016, 2017, 2))
Beispiel #17
0
 def test_string_semester(self):
     from everyclass.server.db.model 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')