def get_level(student_id: str) -> int:
    with pg_conn_context() as conn, conn.cursor() as cursor:
        select_query = "SELECT level FROM privacy_settings WHERE student_id=%s"
        cursor.execute(select_query, (student_id, ))
        result = cursor.fetchone()
    return result[0] if result is not None else get_config(
    ).DEFAULT_PRIVACY_LEVEL
Beispiel #2
0
def _get_datetime(week: int, day: int, time: Tuple[int, int], semester: Tuple[int, int, int]) -> datetime:
    """
    根据学期、周次、时间,生成 `datetime` 类型的时间

    :param week: 周次
    :param day: 星期
    :param time: 时间tuple(时,分)
    :param semester: 学期
    :return: datetime 类型的时间
    """
    config = get_config()
    tz = pytz.timezone("Asia/Shanghai")
    dt = datetime(*(config.AVAILABLE_SEMESTERS[semester]['start'] + time), tzinfo=tz)  # noqa: T484
    dt += timedelta(days=(week - 1) * 7 + day)  # 调整到当前周

    if 'adjustments' in config.AVAILABLE_SEMESTERS[semester]:
        ymd = (dt.year, dt.month, dt.day)
        adjustments = config.AVAILABLE_SEMESTERS[semester]['adjustments']
        if ymd in adjustments:
            if adjustments[ymd]['to']:
                # 调课
                dt = dt.replace(year=adjustments[ymd]['to'][0],
                                month=adjustments[ymd]['to'][1],
                                day=adjustments[ymd]['to'][2])
            else:
                # 冲掉的课年份设置为1984,返回之后被抹去
                dt = dt.replace(year=1984)

    return dt
def decode_jwt_payload(token: str) -> Dict:
    """验证JWT Token并解出payload

    如果payload被修改,抛出jwt.exceptions.InvalidSignatureError。如果签名被修改,抛出jwt.exceptions.DecodeError
    """
    from everyclass.server.utils.config import get_config
    config = get_config()

    return jwt.decode(token, config.JWT_PUBLIC_KEY, algorithms=['RS256'])
def issue_token(user_identifier: str) -> str:
    """签发指定用户名的JWT token"""
    from everyclass.server.utils.config import get_config
    config = get_config()

    payload = {"username": user_identifier}
    token = jwt.encode(payload, config.JWT_PRIVATE_KEY, algorithm='RS256')

    return token.decode('utf8')
def plugin_available(plugin_name: str) -> bool:
    """
    check if a plugin (Sentry, apm, logstash) is available in the current environment.
    :return True if available else False
    """
    from everyclass.server.utils.config import get_config
    config = get_config()
    mode = os.environ.get("MODE", None)
    if mode:
        return mode.lower() in getattr(
            config, "{}_AVAILABLE_IN".format(plugin_name).upper())
    else:
        raise EnvironmentError("MODE not in environment variables")
Beispiel #6
0
def enter_maintenance():
    config = get_config()
    auth = request.authorization
    if auth \
            and auth.username in config.MAINTENANCE_CREDENTIALS \
            and config.MAINTENANCE_CREDENTIALS[auth.username] == auth.password:
        open(config.MAINTENANCE_FILE, "w+").close()  # maintenance file
        open(os.path.join(os.getcwd(), 'reload'), "w+").close()  # uwsgi reload
        return 'success'
    else:
        return Response(
            'Could not verify your access level for that URL.\n'
            'You have to login with proper credentials', 401,
            {'WWW-Authenticate': 'Basic realm="Login Required"'})
def encrypt(resource_type: str, data: str, encryption_key: str = None) -> Text:
    """
    加密资源标识符

    :param resource_type: student、teacher、klass、room
    :param data: 资源标识符
    :param encryption_key: 加密使用的 key
    :return: 加密后的资源标识符
    """
    if resource_type not in (RTYPE_STUDENT, RTYPE_TEACHER, RTYPE_CLASS,
                             RTYPE_ROOM, RTYPE_PEOPLE):
        raise ValueError("resource_type not valid")
    if not encryption_key:
        encryption_key = get_config().RESOURCE_IDENTIFIER_ENCRYPTION_KEY

    return _aes_encrypt(encryption_key, "%s;%s" % (resource_type, data))
Beispiel #8
0
def get_semester_date(date: datetime.date) -> Tuple[str, int, int]:
    """获取日期对应的学期、所属周次及星期(0表示周日,1表示周一...)

    >>> get_semester_date(datetime.date(2020, 2, 22))
    ('2019-2020-1', 26, 6)

    >>> get_semester_date(datetime.date(2020, 2, 23))
    ('2019-2020-2', 1, 0)
    """
    config = get_config()

    semesters = list(config.AVAILABLE_SEMESTERS.items())
    semesters.sort(key=lambda x: x[0], reverse=True)

    for sem in semesters:
        sem_start_date = datetime.date(*sem[1]["start"])
        if date >= sem_start_date:
            days_delta = (date - sem_start_date).days
            return "-".join([str(x) for x in sem[0]
                             ]), days_delta // 7 + 1, days_delta % 7
    raise ValueError("no applicable semester")
def decrypt(data: str, encryption_key: str = None, resource_type: str = None):
    """
    解密资源标识符

    :param data: 加密后的字符串
    :param encryption_key: 可选的 key
    :param resource_type: 验证资源类型(student、teacher、klass、room)
    :return: 资源类型和资源ID
    """
    if not encryption_key:
        encryption_key = get_config().RESOURCE_IDENTIFIER_ENCRYPTION_KEY

    data = _aes_decrypt(encryption_key, data)

    group = re.match(r'^(student|teacher|klass|room);([\s\S]+)$',
                     data)  # 通过正则校验确定数据的正确性
    if group is None:
        raise ValueError('Decrypted data is invalid: %s' % data)
    else:
        if resource_type and group.group(1) != resource_type:
            raise ValueError('Resource type not correspond')
        return group.group(1), group.group(2)
from everyclass.rpc import init as _init_rpc
from everyclass.rpc.entity import Entity as _entity
from everyclass.server.utils.config import get_config
from everyclass.server.utils.encryption import encrypt

_init_rpc(resource_id_encrypt_function=encrypt)  # 为 everyclass.rpc 模块注入 encrypt 函数

_entity.set_base_url(get_config().ENTITY_BASE_URL)
_entity.set_request_token(get_config().ENTITY_TOKEN)
Beispiel #11
0
 def wrapped(*args, **kwargs):
     config = get_config()
     if config.MAINTENANCE:
         return render_template('maintenance.html')
     return func(*args, **kwargs)
Beispiel #12
0
import redis

from everyclass.server.utils.config import get_config

config = get_config()
redis = redis.Redis(**config.REDIS)

redis_prefix = "ec_sv"
Beispiel #13
0
import os
import sys
from logging.config import fileConfig

from alembic import context
from sqlalchemy import engine_from_config
from sqlalchemy import pool

sys.path.append(
    os.path.abspath(os.path.join(os.path.dirname(__file__), '../../../../')))

from everyclass.server.utils.db.postgres import Base, register_model_to_base
from everyclass.server.utils.config import get_config

_app_config = get_config()

# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config

# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)

# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
register_model_to_base()
target_metadata = Base.metadata
Beispiel #14
0
def get_connection() -> database.Database:
    """在连接池中获得连接"""
    if not has_app_context():
        config = get_config()
        return MongoClient(**config.MONGODB).get_database(config.MONGODB_DB)
    return current_app.mongo.get_database(current_app.config['MONGODB_DB'])
Beispiel #15
0
def create_app() -> Flask:
    """创建 flask app"""
    from everyclass.server.utils.web_consts import MSG_INTERNAL_ERROR
    from everyclass.server import plugin_available
    from everyclass.server.utils import generate_error_response, api_helpers, base_exceptions
    from everyclass.common.env import is_production
    from everyclass.server.utils.web_consts import MSG_404

    app = Flask(__name__,
                static_folder='../../frontend/dist',
                static_url_path='',
                template_folder="../../frontend/templates")

    # load app config
    from everyclass.server.utils.config import get_config
    _config = get_config()
    app.config.from_object(_config)  # noqa: T484
    """
    每课统一日志机制


    规则如下:
    - DEBUG 模式下会输出 DEBUG 等级的日志,否则输出 INFO 及以上等级的日志
    - 日志为 JSON 格式,会被节点上的 agent 采集并发送到 datadog,方便结合 metrics 和 APM 数据分析
    - WARNING 以上级别的输出到 Sentry 做错误聚合


    日志等级:
    critical – for errors that lead to termination
    error – for errors that occur, but are handled
    warning – for exceptional circumstances that might not be errors
    notice – for non-error messages you usually want to see
    info – for messages you usually don’t want to see
    debug – for debug messages


    Sentry:
    https://docs.sentry.io/clients/python/api/#raven.Client.captureMessage
    - stack 默认是 False
    """
    if app.config['DEBUG']:
        logging.getLogger().setLevel(logging.DEBUG)
    else:
        logging.getLogger().setLevel(logging.INFO)

    # CDN
    CDN(app)

    # moment
    Moment(app)

    # encrypted session
    app.session_interface = EncryptedSessionInterface()

    # 导入并注册 blueprints
    from everyclass.server.calendar.views import calendar_bp
    from everyclass.server.calendar.views_api import calendar_api_bp
    from everyclass.server.entity.views import entity_bp
    from everyclass.server.entity.views_api import entity_api_bp
    from everyclass.server.user.views import user_bp
    from everyclass.server.user.views_api import user_api_bp
    from everyclass.server.course.views import course_bp
    from everyclass.server.course.views_api import course_api_bp
    from everyclass.server.views_main import main_blueprint
    app.register_blueprint(calendar_bp)
    app.register_blueprint(entity_bp)
    app.register_blueprint(user_bp, url_prefix='/user')
    app.register_blueprint(entity_api_bp, url_prefix='/mobile/entity')
    app.register_blueprint(user_api_bp, url_prefix='/mobile/user')
    app.register_blueprint(calendar_api_bp, url_prefix='/mobile/calendar')
    if 'course' in _config.FEATURE_GATING and _config.FEATURE_GATING['course']:
        app.register_blueprint(course_api_bp, url_prefix='/mobile/course')
        app.register_blueprint(course_bp, url_prefix='/course')
    app.register_blueprint(main_blueprint)

    # 初始化 RPC 模块
    from everyclass.rpc.auth import Auth
    if 'AUTH_BASE_URL' in app.config:
        Auth.set_base_url(app.config['AUTH_BASE_URL'])

    @app.before_request
    def set_user_id():
        """在请求之前设置 session uid,方便 APM 标识用户"""
        from everyclass.server.utils.web_consts import SESSION_CURRENT_USER, SESSION_USER_SEQ
        from everyclass.server.user import service as user_service

        if not session.get('user_id', None) and request.endpoint not in (
                "main.health_check", "static"):
            logger.info(
                f"Give a new user ID for new user. endpoint: {request.endpoint}"
            )
            session['user_id'] = user_service.get_user_id()
        if session.get('user_id', None):
            tracer.current_root_span().set_tag(
                "user_id", session[SESSION_USER_SEQ])  # 唯一用户 ID
        if session.get(SESSION_CURRENT_USER, None):
            tracer.current_root_span().set_tag(
                "username", session[SESSION_CURRENT_USER].identifier)  # 学号或教工号

    @app.before_request
    def log_request():
        """日志中记录请求"""
        logger.info(f'Request received: {request.method} {request.path}')

    @app.after_request
    def response_minify(response):
        """用 htmlmin 压缩 HTML,减轻带宽压力"""
        if app.config[
                'HTML_MINIFY'] and response.content_type == u'text/html; charset=utf-8':
            response.set_data(minify(response.get_data(as_text=True)))
        return response

    from everyclass.server.utils.db.postgres import db_session

    @app.teardown_appcontext
    def shutdown_db_session(exception=None):
        db_session.remove()

    @app.template_filter('versioned')
    def version_filter(filename):
        """
        模板过滤器。如果 STATIC_VERSIONED,返回类似 'style-v1-c012dr.css' 的文件,而不是 'style-v1.css'

        :param filename: 文件名
        :return: 新的文件名
        """
        if app.config['STATIC_VERSIONED']:
            if filename[:4] == 'css/':
                new_filename = app.config['STATIC_MANIFEST'][filename[4:]]
                return 'css/' + new_filename
            elif filename[:3] == 'js/':
                new_filename = app.config['STATIC_MANIFEST'][filename[3:]]
                return new_filename
            else:
                return app.config['STATIC_MANIFEST'][filename]
        return filename

    @app.context_processor
    def inject_consts():
        """允许在模板中使用常量模块,以便使用session key等常量而不用在模板中硬编码"""
        return dict(consts=web_consts,
                    api_base_url=app.config['MOBILE_API_BASE_URL'])

    @app.errorhandler(404)
    def page_not_found(error):
        if request.path.startswith('/mobile'):
            return generate_error_response(
                None, api_helpers.STATUS_CODE_INVALID_REQUEST, "no such API")
        return render_template('common/error.html', message=MSG_404)

    @app.errorhandler(base_exceptions.BizException)
    def handle_biz_exception(error: base_exceptions.BizException):
        if request.path.startswith("/mobile"):
            if isinstance(error, base_exceptions.InternalError):
                logger.error(repr(error))

            # 业务错误的status_message可以对外展示
            actual_error = {'status_message_overwrite': error.status_message}
            return generate_error_response(None, error.status_code,
                                           **actual_error)

    @app.errorhandler(500)
    def internal_server_error(error):
        if request.path.startswith("/mobile"):
            # 对于非业务错误,生产环境中不进行返回,其他环境中可返回
            actual_error = {
                'status_message_overwrite':
                f"server internal error: {repr(error.original_exception)}"
            } if not is_production() else {}
            return generate_error_response(
                None, api_helpers.STATUS_CODE_INTERNAL_ERROR, **actual_error)
        if plugin_available("sentry"):
            return render_template(
                'common/error.html',
                message=MSG_INTERNAL_ERROR,
                event_id=g.sentry_event_id,
                public_dsn=sentry.client.get_public_dsn('https'))
        return f"<h4>500 Error: {repr(error.original_exception)}</h4><br>You are seeing this page because Sentry is not available."

    global __app
    __app = app

    return app
 def test_import_config(self):
     from everyclass.server.utils.config import get_config
     config = get_config()
     self.assertTrue(config)