<commit_before># -*- coding: utf-8 -*-

"""
    eve.io.mongo.validation
    ~~~~~~~~~~~~~~~~~~~~~~~

    This module implements the mongo Validator class, used to validate that
    objects incoming via POST/PATCH requests conform to the API domain.
    An extension of Cerberus Validator.

    :copyright: (c) 2015 by Nicola Iarocci.
    :license: BSD, see LICENSE for more details.
"""
import copy
from bson import ObjectId
from collections import Mapping
from flask import current_app as app
from werkzeug.datastructures import FileStorage
from cerberus import Validator

from eve.auth import auth_field_and_value
from eve.io.mongo.geo import Point, MultiPoint, LineString, Polygon, \
    MultiLineString, MultiPolygon, GeometryCollection
from eve.utils import config, str_type
from eve.versioning import get_data_version_relation_document

class Validator(Validator):
    """ A cerberus.Validator subclass adding the `unique` contraint to
    Cerberus standard validation.

    :param schema: the validation schema, to be composed according to Cerberus
                   documentation.
    :param resource: the resource name.

    .. versionchanged:: 0.6.2
       __init__ signature update for cerberus v0.8.1 compatibility.
       Remove support for 'transparent_schema_rules' in favor of explicit
       validators for rules unsupported by cerberus.

    .. versionchanged:: 0.5
       Support for _original_document
       Fix crash bug with Cerberus 0.7.1+ and keyschema rule. See Cerberus #48.

    .. versionchanged:: 0.0.6
       Support for 'allow_unknown' which allows to successfully validate
       unknown key/value pairs.

    .. versionchanged:: 0.0.4
       Support for 'transparent_schema_rules' introduced with Cerberus 0.0.3,
       which allows for insertion of 'default' values in POST requests.
    """
    def __init__(self, schema=None, resource=None,
                 transparent_schema_rules=False, allow_unknown=False):
        self.resource = resource
        self._id = None
        self._original_document = None
        schema = self._remove_unique_rules_on_fields_with_unique_index(schema)
        if resource:
            transparent_schema_rules = \
                config.DOMAIN[resource]['transparent_schema_rules']
            allow_unknown = config.DOMAIN[resource]['allow_unknown']
        super(Validator, self).__init__(
            schema,
            transparent_schema_rules=transparent_schema_rules,
            allow_unknown=allow_unknown)

    def _remove_unique_rules_on_fields_with_unique_index(self, schema):
        # TODO: Actually do what the function name suggests. This version just
        # removes the unique constraint on _id. We could use the information
        # available by app.data.driver.db[datasource].index_information() to
        # remove all unnecessary unique constraints.
        result = copy.deepcopy(schema)
        if '_id' in result and 'unique' in result['_id']:
            del(result['_id']['unique'])
        return result

    def validate_update(self, document, _id, original_document=None):
        """ Validate method to be invoked when performing an update, not an
        insert.

        :param document: the document to be validated.
        :param _id: the unique id of the document.
        """
        self._id = _id
        self._original_document = original_document
        return super(Validator, self).validate_update(document)

    def validate_replace(self, document, _id, original_document=None):
        """ Validation method to be invoked when performing a document
        replacement. This differs from :func:`validation_update` since in this
        case we want to perform a full :func:`validate` (the new document is to
        be considered a new insertion and required fields needs validation).
        However, like with validate_update, we also want the current _id
        not to be checked when validationg 'unique' values.

        .. versionadded:: 0.1.0
        """
        self._id = _id
        self._original_document = original_document
        return super(Validator, self).validate(document)

    def _validate_default(self, unique, field, value):
        """ Fake validate function to let cerberus accept "default"
            as keyword in the schema

        .. versionadded:: 0.6.2
        """
        pass

    def _validate_versioned(self, unique, field, value):
        """ Fake validate function to let cerberus accept "versioned"
            as keyword in the schema

        .. versionadded:: 0.6.2
        """
        pass

    def _validate_unique_to_user(self, unique, field, value):
        """ Validates that a value is unique to the active user. Active user is
        the user authenticated for current request. See #646.

        .. versionadded: 0.6
        """

        auth_field, auth_value = auth_field_and_value(self.resource)

        # if an auth value has been set for this request, then make sure it is
        # taken into account when checking for value uniqueness.
        query = {auth_field: auth_value} if auth_field else {}

        self._is_value_unique(unique, field, value, query)

    def _validate_unique(self, unique, field, value):
        """ Enables validation for `unique` schema attribute.

        :param unique: Boolean, wether the field value should be
                       unique or not.
        :param field: field name.
        :param value: field value.

        .. versionchanged:: 0.6
           Validates field value uniqueness against the whole datasource,
           indipendently of the request method. See #646.

        .. versionchanged:: 0.3
           Support for new 'self._error' signature introduced with Cerberus
           v0.5.

        .. versionchanged:: 0.2
           Handle the case in which ID_FIELD is not of ObjectId type.
        """
        self._is_value_unique(unique, field, value, {})

    def _is_value_unique(self, unique, field, value, query):
        """ Validates that a field value is unique.

        .. versionadded:: 0.6
        """
        if unique:
            query[field] = value

            # exclude current document
            if self._id:
                id_field = config.DOMAIN[self.resource]['id_field']
                query[id_field] = {'$ne': self._id}

            # we perform the check on the native mongo driver (and not on
            # app.data.find_one()) because in this case we don't want the usual
            # (for eve) query injection to interfere with this validation. We
            # are still operating within eve's mongo namespace anyway.

            datasource, _, _, _ = app.data.datasource(self.resource)
            if app.data.driver.db[datasource].find_one(query):
                self._error(field, "value '%s' is not unique" % value)

    def _validate_data_relation(self, data_relation, field, value):
        """ Enables validation for `data_relation` field attribute. Makes sure
        'value' of 'field' adheres to the referential integrity rule specified
        by 'data_relation'.

        :param data_relation: a dict following keys:
            'resource': foreign resource name
            'field': foreign field name
            'version': True if this relation points to a specific version
            'type': the type for the reference field if 'version': True
        :param field: field name.
        :param value: field value.

        .. versionchanged:: 0.4
           Support for document versioning.

        .. versionchanged:: 0.3
           Support for new 'self._error' signature introduced with Cerberus
           v0.5.

        .. versionchanged:: 0.1.1
           'collection' key renamed to 'resource' (data_relation)

        .. versionadded: 0.0.5
        """
        if 'version' in data_relation and data_relation['version'] is True:
            value_field = data_relation['field']
            version_field = app.config['VERSION']

            # check value format
            if isinstance(value, dict) and value_field in value \
                    and version_field in value:
                resource_def = config.DOMAIN[data_relation['resource']]
                if resource_def['versioning'] is False:
                    self._error(
                        field, "can't save a version with"
                        " data_relation if '%s' isn't versioned" %
                        data_relation['resource'])
                else:
                    search = get_data_version_relation_document(
                        data_relation, value)

                    if not search:
                        self._error(
                            field, "value '%s' must exist in resource"
                            " '%s', field '%s' at version '%s'." % (
                                value[value_field], data_relation['resource'],
                                data_relation['field'], value[version_field]))
            else:
                self._error(
                    field, "versioned data_relation must be a dict"
                    " with fields '%s' and '%s'" %
                    (value_field, version_field))
        else:
            if not isinstance(value, list):
                value = [value]

            data_resource = data_relation['resource']
            for item in value:
                    query = {data_relation['field']: item}
                    if not app.data.find_one(data_resource, None, **query):
                        self._error(
                            field,
                            "value '%s' must exist in resource"
                            " '%s', field '%s'." %
                            (item, data_resource, data_relation['field']))

    def _validate_type_objectid(self, field, value):
        """ Enables validation for `objectid` data type.

        :param field: field name.
        :param value: field value.

        .. versionchanged:: 0.3
           Support for new 'self._error' signature introduced with Cerberus
           v0.5.

        .. versionchanged:: 0.1.1
           regex check replaced with proper type check.
        """
        if not isinstance(value, ObjectId):
            self._error(field, "value '%s' cannot be converted to a ObjectId"
                        % value)

    def _validate_readonly(self, read_only, field, value):
        """
        .. versionchanged:: 0.5
           Not taking defaul values in consideration anymore since they are now
           being resolved after validation (#353).
           Consider the original value if available (#479).

        .. versionadded:: 0.4
        """
        original_value = self._original_document.get(field) \
            if self._original_document else None
        if value != original_value:
            super(Validator, self)._validate_readonly(read_only, field, value)

    def _validate_dependencies(self, document, dependencies, field,
                               break_on_error=False):
        """ With PATCH method, the validator is only provided with the updated
        fields. If an updated field depends on another field in order to be
        edited and the other field was previously set, the validator doesn't
        see it and rejects the update. In order to avoid that we merge the
        proposed changes with the original document before validating
        dependencies.

        .. versionchanged:: 0.6.1
           Fix: dependencies on sub-document fields are now properly
           processed (#706).

        .. versionchanged:: 0.6
           Fix: Only evaluate dependencies that don't have valid default
           values.

        .. versionchanged:: 0.5.1
           Fix: dependencies with value checking seems broken #547.

        .. versionadded:: 0.5
           If a dependency has a default value, skip it as Cerberus does not
           have the notion of default values and would report a missing
           dependency (#353).
           Fix for #363 (see docstring).
        """
        if dependencies is None:
            return True

        if isinstance(dependencies, str_type):
            dependencies = [dependencies]

        defaults = {}
        for d in dependencies:
            root = d.split('.')[0]
            default = self.schema[root].get('default')
            if default and root not in document:
                defaults[root] = default

        if isinstance(dependencies, Mapping):
            # Only evaluate dependencies that don't have *valid* defaults
            for k, v in defaults.items():
                if v in dependencies[k]:
                    del(dependencies[k])
        else:
            # Only evaluate dependencies that don't have defaults values
            dependencies = [d for d in dependencies if d not in
                            defaults.keys()]

        dcopy = None
        if self._original_document:
            dcopy = copy.copy(document)
            dcopy.update(self._original_document)
        return super(Validator, self)._validate_dependencies(dcopy or document,
                                                             dependencies,
                                                             field,
                                                             break_on_error)

    def _validate_type_media(self, field, value):
        """ Enables validation for `media` data type.

        :param field: field name.
        :param value: field value.

        .. versionadded:: 0.3
        """
        if not isinstance(value, FileStorage):
            self._error(field, "file was expected, got '%s' instead." % value)

    def _validate_type_point(self, field, value):
        """ Enables validation for `point` data type.

        :param field: field name.
        :param value: field value.
        """
        try:
            Point(value)
        except TypeError as e:
            self._error(field, "Point not correct %s: %s" % (value, e))

    def _validate_type_linestring(self, field, value):
        """ Enables validation for `linestring` data type.

        :param field: field name.
        :param value: field value.
        """
        try:
            LineString(value)
        except TypeError:
            self._error(field, "LineString not correct %s " % value)

    def _validate_type_polygon(self, field, value):
        """ Enables validation for `polygon` data type.

        :param field: field name.
        :param value: field value.
        """
        try:
            Polygon(value)
        except TypeError:
            self._error(field, "LineString not correct %s " % value)

    def _validate_type_multipoint(self, field, value):
        """ Enables validation for `multipoint` data type.

        :param field: field name.
        :param value: field value.
        """
        try:
            MultiPoint(value)
        except TypeError:
            self._error(field, "MultiPoint not correct" % value)

    def _validate_type_multilinestring(self, field, value):
        """ Enables validation for `multilinestring`data type.

        :param field: field name.
        :param value: field value.
        """
        try:
            MultiLineString(value)
        except TypeError:
            self._error(field, "MultiLineString not  correct" % value)

    def _validate_type_multipolygon(self, field, value):
        """ Enables validation for `multipolygon` data type.

        :param field: field name.
        :param value: field value.
        """
        try:
            MultiPolygon(value)
        except TypeError:
            self._error(field, "MultiPolygon not  correct" % value)

    def _validate_type_geometrycollection(self, field, value):
        """ Enables validation for `geometrycollection`data type

        :param field: field name.
        :param value: field nvalue
        """
        try:
            GeometryCollection(value)
        except TypeError:
            self._error(field, "GeometryCollection not correct" % value)

    def _error(self, field, _error):
        """ Change the default behaviour so that, if VALIDATION_ERROR_AS_LIST
        is enabled, single validation errors are returned as a list. See #536.

        :param field: field name
        :param _error: field error(s)

        .. versionadded:: 0.6
        """
        super(Validator, self)._error(field, _error)
        if config.VALIDATION_ERROR_AS_LIST:
            err = self._errors[field]
            if not isinstance(err, list):
                self._errors[field] = [err]
<commit_msg>Validation: move transparent_schema_rules argument last<commit_after># -*- coding: utf-8 -*-

"""
    eve.io.mongo.validation
    ~~~~~~~~~~~~~~~~~~~~~~~

    This module implements the mongo Validator class, used to validate that
    objects incoming via POST/PATCH requests conform to the API domain.
    An extension of Cerberus Validator.

    :copyright: (c) 2015 by Nicola Iarocci.
    :license: BSD, see LICENSE for more details.
"""
import copy
from bson import ObjectId
from collections import Mapping
from flask import current_app as app
from werkzeug.datastructures import FileStorage
from cerberus import Validator

from eve.auth import auth_field_and_value
from eve.io.mongo.geo import Point, MultiPoint, LineString, Polygon, \
    MultiLineString, MultiPolygon, GeometryCollection
from eve.utils import config, str_type
from eve.versioning import get_data_version_relation_document

class Validator(Validator):
    """ A cerberus.Validator subclass adding the `unique` contraint to
    Cerberus standard validation.

    :param schema: the validation schema, to be composed according to Cerberus
                   documentation.
    :param resource: the resource name.

    .. versionchanged:: 0.6.2
       __init__ signature update for cerberus v0.8.1 compatibility.
       Remove support for 'transparent_schema_rules' in favor of explicit
       validators for rules unsupported by cerberus.

    .. versionchanged:: 0.5
       Support for _original_document
       Fix crash bug with Cerberus 0.7.1+ and keyschema rule. See Cerberus #48.

    .. versionchanged:: 0.0.6
       Support for 'allow_unknown' which allows to successfully validate
       unknown key/value pairs.

    .. versionchanged:: 0.0.4
       Support for 'transparent_schema_rules' introduced with Cerberus 0.0.3,
       which allows for insertion of 'default' values in POST requests.
    """
    def __init__(self, schema=None, resource=None, allow_unknown=False,
                 transparent_schema_rules=False):
        self.resource = resource
        self._id = None
        self._original_document = None
        schema = self._remove_unique_rules_on_fields_with_unique_index(schema)
        if resource:
            transparent_schema_rules = \
                config.DOMAIN[resource]['transparent_schema_rules']
            allow_unknown = config.DOMAIN[resource]['allow_unknown']
        super(Validator, self).__init__(
            schema,
            transparent_schema_rules=transparent_schema_rules,
            allow_unknown=allow_unknown)

    def _remove_unique_rules_on_fields_with_unique_index(self, schema):
        # TODO: Actually do what the function name suggests. This version just
        # removes the unique constraint on _id. We could use the information
        # available by app.data.driver.db[datasource].index_information() to
        # remove all unnecessary unique constraints.
        result = copy.deepcopy(schema)
        if '_id' in result and 'unique' in result['_id']:
            del(result['_id']['unique'])
        return result

    def validate_update(self, document, _id, original_document=None):
        """ Validate method to be invoked when performing an update, not an
        insert.

        :param document: the document to be validated.
        :param _id: the unique id of the document.
        """
        self._id = _id
        self._original_document = original_document
        return super(Validator, self).validate_update(document)

    def validate_replace(self, document, _id, original_document=None):
        """ Validation method to be invoked when performing a document
        replacement. This differs from :func:`validation_update` since in this
        case we want to perform a full :func:`validate` (the new document is to
        be considered a new insertion and required fields needs validation).
        However, like with validate_update, we also want the current _id
        not to be checked when validationg 'unique' values.

        .. versionadded:: 0.1.0
        """
        self._id = _id
        self._original_document = original_document
        return super(Validator, self).validate(document)

    def _validate_default(self, unique, field, value):
        """ Fake validate function to let cerberus accept "default"
            as keyword in the schema

        .. versionadded:: 0.6.2
        """
        pass

    def _validate_versioned(self, unique, field, value):
        """ Fake validate function to let cerberus accept "versioned"
            as keyword in the schema

        .. versionadded:: 0.6.2
        """
        pass

    def _validate_unique_to_user(self, unique, field, value):
        """ Validates that a value is unique to the active user. Active user is
        the user authenticated for current request. See #646.

        .. versionadded: 0.6
        """

        auth_field, auth_value = auth_field_and_value(self.resource)

        # if an auth value has been set for this request, then make sure it is
        # taken into account when checking for value uniqueness.
        query = {auth_field: auth_value} if auth_field else {}

        self._is_value_unique(unique, field, value, query)

    def _validate_unique(self, unique, field, value):
        """ Enables validation for `unique` schema attribute.

        :param unique: Boolean, wether the field value should be
                       unique or not.
        :param field: field name.
        :param value: field value.

        .. versionchanged:: 0.6
           Validates field value uniqueness against the whole datasource,
           indipendently of the request method. See #646.

        .. versionchanged:: 0.3
           Support for new 'self._error' signature introduced with Cerberus
           v0.5.

        .. versionchanged:: 0.2
           Handle the case in which ID_FIELD is not of ObjectId type.
        """
        self._is_value_unique(unique, field, value, {})

    def _is_value_unique(self, unique, field, value, query):
        """ Validates that a field value is unique.

        .. versionadded:: 0.6
        """
        if unique:
            query[field] = value

            # exclude current document
            if self._id:
                id_field = config.DOMAIN[self.resource]['id_field']
                query[id_field] = {'$ne': self._id}

            # we perform the check on the native mongo driver (and not on
            # app.data.find_one()) because in this case we don't want the usual
            # (for eve) query injection to interfere with this validation. We
            # are still operating within eve's mongo namespace anyway.

            datasource, _, _, _ = app.data.datasource(self.resource)
            if app.data.driver.db[datasource].find_one(query):
                self._error(field, "value '%s' is not unique" % value)

    def _validate_data_relation(self, data_relation, field, value):
        """ Enables validation for `data_relation` field attribute. Makes sure
        'value' of 'field' adheres to the referential integrity rule specified
        by 'data_relation'.

        :param data_relation: a dict following keys:
            'resource': foreign resource name
            'field': foreign field name
            'version': True if this relation points to a specific version
            'type': the type for the reference field if 'version': True
        :param field: field name.
        :param value: field value.

        .. versionchanged:: 0.4
           Support for document versioning.

        .. versionchanged:: 0.3
           Support for new 'self._error' signature introduced with Cerberus
           v0.5.

        .. versionchanged:: 0.1.1
           'collection' key renamed to 'resource' (data_relation)

        .. versionadded: 0.0.5
        """
        if 'version' in data_relation and data_relation['version'] is True:
            value_field = data_relation['field']
            version_field = app.config['VERSION']

            # check value format
            if isinstance(value, dict) and value_field in value \
                    and version_field in value:
                resource_def = config.DOMAIN[data_relation['resource']]
                if resource_def['versioning'] is False:
                    self._error(
                        field, "can't save a version with"
                        " data_relation if '%s' isn't versioned" %
                        data_relation['resource'])
                else:
                    search = get_data_version_relation_document(
                        data_relation, value)

                    if not search:
                        self._error(
                            field, "value '%s' must exist in resource"
                            " '%s', field '%s' at version '%s'." % (
                                value[value_field], data_relation['resource'],
                                data_relation['field'], value[version_field]))
            else:
                self._error(
                    field, "versioned data_relation must be a dict"
                    " with fields '%s' and '%s'" %
                    (value_field, version_field))
        else:
            if not isinstance(value, list):
                value = [value]

            data_resource = data_relation['resource']
            for item in value:
                    query = {data_relation['field']: item}
                    if not app.data.find_one(data_resource, None, **query):
                        self._error(
                            field,
                            "value '%s' must exist in resource"
                            " '%s', field '%s'." %
                            (item, data_resource, data_relation['field']))

    def _validate_type_objectid(self, field, value):
        """ Enables validation for `objectid` data type.

        :param field: field name.
        :param value: field value.

        .. versionchanged:: 0.3
           Support for new 'self._error' signature introduced with Cerberus
           v0.5.

        .. versionchanged:: 0.1.1
           regex check replaced with proper type check.
        """
        if not isinstance(value, ObjectId):
            self._error(field, "value '%s' cannot be converted to a ObjectId"
                        % value)

    def _validate_readonly(self, read_only, field, value):
        """
        .. versionchanged:: 0.5
           Not taking defaul values in consideration anymore since they are now
           being resolved after validation (#353).
           Consider the original value if available (#479).

        .. versionadded:: 0.4
        """
        original_value = self._original_document.get(field) \
            if self._original_document else None
        if value != original_value:
            super(Validator, self)._validate_readonly(read_only, field, value)

    def _validate_dependencies(self, document, dependencies, field,
                               break_on_error=False):
        """ With PATCH method, the validator is only provided with the updated
        fields. If an updated field depends on another field in order to be
        edited and the other field was previously set, the validator doesn't
        see it and rejects the update. In order to avoid that we merge the
        proposed changes with the original document before validating
        dependencies.

        .. versionchanged:: 0.6.1
           Fix: dependencies on sub-document fields are now properly
           processed (#706).

        .. versionchanged:: 0.6
           Fix: Only evaluate dependencies that don't have valid default
           values.

        .. versionchanged:: 0.5.1
           Fix: dependencies with value checking seems broken #547.

        .. versionadded:: 0.5
           If a dependency has a default value, skip it as Cerberus does not
           have the notion of default values and would report a missing
           dependency (#353).
           Fix for #363 (see docstring).
        """
        if dependencies is None:
            return True

        if isinstance(dependencies, str_type):
            dependencies = [dependencies]

        defaults = {}
        for d in dependencies:
            root = d.split('.')[0]
            default = self.schema[root].get('default')
            if default and root not in document:
                defaults[root] = default

        if isinstance(dependencies, Mapping):
            # Only evaluate dependencies that don't have *valid* defaults
            for k, v in defaults.items():
                if v in dependencies[k]:
                    del(dependencies[k])
        else:
            # Only evaluate dependencies that don't have defaults values
            dependencies = [d for d in dependencies if d not in
                            defaults.keys()]

        dcopy = None
        if self._original_document:
            dcopy = copy.copy(document)
            dcopy.update(self._original_document)
        return super(Validator, self)._validate_dependencies(dcopy or document,
                                                             dependencies,
                                                             field,
                                                             break_on_error)

    def _validate_type_media(self, field, value):
        """ Enables validation for `media` data type.

        :param field: field name.
        :param value: field value.

        .. versionadded:: 0.3
        """
        if not isinstance(value, FileStorage):
            self._error(field, "file was expected, got '%s' instead." % value)

    def _validate_type_point(self, field, value):
        """ Enables validation for `point` data type.

        :param field: field name.
        :param value: field value.
        """
        try:
            Point(value)
        except TypeError as e:
            self._error(field, "Point not correct %s: %s" % (value, e))

    def _validate_type_linestring(self, field, value):
        """ Enables validation for `linestring` data type.

        :param field: field name.
        :param value: field value.
        """
        try:
            LineString(value)
        except TypeError:
            self._error(field, "LineString not correct %s " % value)

    def _validate_type_polygon(self, field, value):
        """ Enables validation for `polygon` data type.

        :param field: field name.
        :param value: field value.
        """
        try:
            Polygon(value)
        except TypeError:
            self._error(field, "LineString not correct %s " % value)

    def _validate_type_multipoint(self, field, value):
        """ Enables validation for `multipoint` data type.

        :param field: field name.
        :param value: field value.
        """
        try:
            MultiPoint(value)
        except TypeError:
            self._error(field, "MultiPoint not correct" % value)

    def _validate_type_multilinestring(self, field, value):
        """ Enables validation for `multilinestring`data type.

        :param field: field name.
        :param value: field value.
        """
        try:
            MultiLineString(value)
        except TypeError:
            self._error(field, "MultiLineString not  correct" % value)

    def _validate_type_multipolygon(self, field, value):
        """ Enables validation for `multipolygon` data type.

        :param field: field name.
        :param value: field value.
        """
        try:
            MultiPolygon(value)
        except TypeError:
            self._error(field, "MultiPolygon not  correct" % value)

    def _validate_type_geometrycollection(self, field, value):
        """ Enables validation for `geometrycollection`data type

        :param field: field name.
        :param value: field nvalue
        """
        try:
            GeometryCollection(value)
        except TypeError:
            self._error(field, "GeometryCollection not correct" % value)

    def _error(self, field, _error):
        """ Change the default behaviour so that, if VALIDATION_ERROR_AS_LIST
        is enabled, single validation errors are returned as a list. See #536.

        :param field: field name
        :param _error: field error(s)

        .. versionadded:: 0.6
        """
        super(Validator, self)._error(field, _error)
        if config.VALIDATION_ERROR_AS_LIST:
            err = self._errors[field]
            if not isinstance(err, list):
                self._errors[field] = [err]
<|endoftext|>


<commit_before>""" Handlers for sequencing project information.
"""
from collections import OrderedDict
import json
import string

import tornado.web

from status.util import dthandler, SafeHandler

# Constant dictionary with Displayname:internalname pairs
DEFAULT_COLUMNS = OrderedDict([('Project', 'project'),
                               ('Project Name', 'project_name'),
                               ('Application', 'application'),
                               ('Sequencing progress', 'passed_samples'),
                               ('Number of Samples','no_samples'),
                               ('Type','type')])

EXTRA_COLUMNS = OrderedDict([('Queue Date', 'queued'),
                             ('Ordered million reads per sample', 'ordered_reads'),
                             ('Source','source'),
                             ('Reference Genome', 'reference_genome'),
                             ('Sequencing Setup', 'sequencing_setup'),
                             ('Customer Reference', 'customer_reference'),
                             ('Sequencing Platform', 'sequencing_platform'),
                             ('Uppnex ID', 'uppnex_id'),
                             ('Open Date', 'open_date'),
                             ('Disposal of Samples', 'disposal_of_any_remaining_samples'),
                             ('All Samples Sequenced', 'all_samples_sequenced'),
                             ('Portal ID (somewhere else)', 'Portal_id'),
                             ('Portal ID (Details)', 'portal_id'),
                             ('Lanes', 'sequence_units_ordered_(lanes)'),
                             ('Best Practice Bioinformatics', 'best_practice_bioinformatics'),
                             ('Bioinformatic QC', 'bioinformatic_qc'),
                             ('Comment', 'comment'),
                             ('Aborted', 'aborted'),
                             ('Library Prep Start', 'library_prep_start'),
                             ('QC Library Finished','qc_library_finished'),
                             ('Sequencing Start', 'sequencing_start_date')])

COLUMNS = dict([('DEFAULT_COLUMNS', DEFAULT_COLUMNS),('EXTRA_COLUMNS', EXTRA_COLUMNS)])

class ProjectsBaseDataHandler(SafeHandler):
    def list_projects(self, all_projects=True):
        projects = OrderedDict()

        summary_view = self.application.projects_db.view("project/summary", descending=True)
        if not all_projects:
            summary_view = summary_view[["open",'Z']:["open",'']]

        for row in summary_view:
            # the details key gives values containing multiple udfs on project level
            # and project_summary key gives 'temporary' udfs that will move to 'details'.
            # Include these as normal key:value pairs
            if 'project_summary' in row.value:
                for summary_key, summary_value in row.value['project_summary'].iteritems():
                    row.value[summary_key] = summary_value
                row.value.pop("project_summary", None)

            # If key is in both project_summary and details, details has precedence
            if 'details' in row.value:
                for detail_key, detail_value in row.value['details'].iteritems():
                    row.value[detail_key] = detail_value
                row.value.pop("details", None)
            projects[row.key[1]] = row.value

        # Include dates for each project:
        for row in self.application.projects_db.view("project/summary_dates", descending=True, group_level=1):
            if row.key[0] in projects:
                for date_type, date in row.value.iteritems():
                    projects[row.key[0]][date_type] = date

        return projects

    def list_project_fields(self, undefined=False, project_list=None, all_projects=True):
        # If undefined=True is given, only return fields not in columns defined
        # in constants in this module
        if project_list is None:
            project_list = self.list_projects(all_projects=all_projects)
        field_items = set()
        for project_id, value in project_list.iteritems():
            for key, _ in value.iteritems():
                field_items.add(key)
        if undefined:
            field_items = field_items.difference(set(DEFAULT_COLUMNS.values()))
            field_items = field_items.difference(set(EXTRA_COLUMNS.values()))
        return field_items

class ProjectsDataHandler(ProjectsBaseDataHandler):
    """ Serves brief information for each open project in the database.

    Loaded through /api/v1/projects
    """
    def get(self):
        self.set_header("Content-type", "application/json")
        all_projects = self.get_argument("all_projects", "True")
        all_projects = (str(all_projects).lower() == "true")

        self.write(json.dumps(self.list_projects(all_projects)))

class ProjectsFieldsDataHandler(ProjectsBaseDataHandler):
    """ Serves all fields occuring in the values of the ProjectsDataHandler
    json object.

    Loaded through /api/v1/projects_fields
    """
    def get(self):
        undefined = self.get_argument("undefined", "False")
        undefined = (string.lower(undefined) == "true")
        all_projects = self.get_argument("all_projects", "True")
        all_projects = (str(all_projects).lower() == "true")
        field_items = self.list_project_fields(undefined=undefined, all_projects=all_projects)
        self.write(json.dumps(list(field_items)))

class ProjectDataHandler(SafeHandler):
    """ Serves brief information of a given project.

    Loaded through /api/v1/project_summary/([^/]*)$
    """
    def get(self, project):
        self.set_header("Content-type", "application/json")
        self.write(json.dumps(self.project_info(project)))

    def project_info(self, project):
        result = self.application.projects_db.view("project/summary")[project]
        return result.rows[0].value

class ProjectSamplesDataHandler(SafeHandler):
    """ Serves brief info about all samples in a given project.

    Loaded through /api/v1/projects/([^/]*)$
    """
    def get(self, project):
        self.set_header("Content-type", "application/json")
        self.write(json.dumps(self.sample_list(project), default=dthandler))

    def sample_list(self, project):
        sample_view = self.application.projects_db.view("project/samples")
        result = sample_view[project]

        samples = result.rows[0].value
        samples = OrderedDict(sorted(samples.iteritems(), key=lambda x: x[0]))
        return samples

class ProjectSamplesHandler(SafeHandler):
    """ Serves a page which lists the samples of a given project, with some
    brief information for each sample.
    """
    def get(self, project):
        t = self.application.loader.load("project_samples.html")
        self.write(t.generate(project=project, user=self.get_current_user_name()))

class ProjectsHandler(SafeHandler):
    """ Serves a page with all projects listed, along with some brief info.
    """
    def get(self):
        t = self.application.loader.load("projects.html")
        self.write(t.generate(columns = COLUMNS, all_projects=True, user=self.get_current_user_name()))

class OpenProjectsHandler(SafeHandler):
    """ Serves a page with all OPEN projects listed, along with some brief info.
    """
    def get(self):
        t = self.application.loader.load("projects.html")
        self.write(t.generate(columns = COLUMNS, all_projects=False, user=self.get_current_user_name()))

class UppmaxProjectsDataHandler(SafeHandler):
    """ Serves a list of UPPNEX projects where the storage quota have
    been logged.

    Loaded through /api/v1/uppmax_projects
    """
    def get(self):
        self.set_header("Content-type", "application/json")
        self.write(json.dumps(self.list_projects()))

    def list_projects(self):
        project_list = []
        view = self.application.uppmax_db.view("status/projects", group_level=1)
        for row in view:
            project_list.append(row.key)

        return project_list
<commit_msg>Added days in production field to project list<commit_after>""" Handlers for sequencing project information.
"""
from collections import OrderedDict
import json
import string

import tornado.web
import dateutil.parser
import datetime

from status.util import dthandler, SafeHandler

# Constant dictionary with Displayname:internalname pairs
DEFAULT_COLUMNS = OrderedDict([('Project', 'project'),
                               ('Project Name', 'project_name'),
                               ('Application', 'application'),
                               ('Sequencing progress', 'passed_samples'),
                               ('Number of Samples','no_samples'),
                               ('Type','type')])

EXTRA_COLUMNS = OrderedDict([('Queue Date', 'queued'),
                             ('Days in Production', 'days_in_production'),
                             ('Ordered million reads per sample', 'ordered_reads'),
                             ('Source','source'),
                             ('Reference Genome', 'reference_genome'),
                             ('Sequencing Setup', 'sequencing_setup'),
                             ('Customer Reference', 'customer_reference'),
                             ('Sequencing Platform', 'sequencing_platform'),
                             ('Uppnex ID', 'uppnex_id'),
                             ('Open Date', 'open_date'),
                             ('Disposal of Samples', 'disposal_of_any_remaining_samples'),
                             ('All Samples Sequenced', 'all_samples_sequenced'),
                             ('Portal ID (somewhere else)', 'Portal_id'),
                             ('Portal ID (Details)', 'portal_id'),
                             ('Lanes', 'sequence_units_ordered_(lanes)'),
                             ('Best Practice Bioinformatics', 'best_practice_bioinformatics'),
                             ('Bioinformatic QC', 'bioinformatic_qc'),
                             ('Comment', 'comment'),
                             ('Aborted', 'aborted'),
                             ('Library Prep Start', 'library_prep_start'),
                             ('QC Library Finished','qc_library_finished'),
                             ('Sequencing Start', 'sequencing_start_date')])

COLUMNS = dict([('DEFAULT_COLUMNS', DEFAULT_COLUMNS),('EXTRA_COLUMNS', EXTRA_COLUMNS)])

class ProjectsBaseDataHandler(SafeHandler):
    def list_projects(self, all_projects=True):
        projects = OrderedDict()

        summary_view = self.application.projects_db.view("project/summary", descending=True)
        if not all_projects:
            summary_view = summary_view[["open",'Z']:["open",'']]

        for row in summary_view:
            # the details key gives values containing multiple udfs on project level
            # and project_summary key gives 'temporary' udfs that will move to 'details'.
            # Include these as normal key:value pairs
            if 'project_summary' in row.value:
                for summary_key, summary_value in row.value['project_summary'].iteritems():
                    row.value[summary_key] = summary_value
                row.value.pop("project_summary", None)

            # If key is in both project_summary and details, details has precedence
            if 'details' in row.value:
                for detail_key, detail_value in row.value['details'].iteritems():
                    row.value[detail_key] = detail_value
                row.value.pop("details", None)

            if row.key[0] == 'open' and 'queued' in row.value:
                #Add days in production field
                now = datetime.datetime.now()
                queued = row.value['queued']
                diff = now - dateutil.parser.parse(queued)
                row.value['days_in_production'] = diff.days

            projects[row.key[1]] = row.value

        # Include dates for each project:
        for row in self.application.projects_db.view("project/summary_dates", descending=True, group_level=1):
            if row.key[0] in projects:
                for date_type, date in row.value.iteritems():
                    projects[row.key[0]][date_type] = date

        return projects

    def list_project_fields(self, undefined=False, project_list=None, all_projects=True):
        # If undefined=True is given, only return fields not in columns defined
        # in constants in this module
        if project_list is None:
            project_list = self.list_projects(all_projects=all_projects)
        field_items = set()
        for project_id, value in project_list.iteritems():
            for key, _ in value.iteritems():
                field_items.add(key)
        if undefined:
            field_items = field_items.difference(set(DEFAULT_COLUMNS.values()))
            field_items = field_items.difference(set(EXTRA_COLUMNS.values()))
        return field_items

class ProjectsDataHandler(ProjectsBaseDataHandler):
    """ Serves brief information for each open project in the database.

    Loaded through /api/v1/projects
    """
    def get(self):
        self.set_header("Content-type", "application/json")
        all_projects = self.get_argument("all_projects", "True")
        all_projects = (str(all_projects).lower() == "true")

        self.write(json.dumps(self.list_projects(all_projects)))

class ProjectsFieldsDataHandler(ProjectsBaseDataHandler):
    """ Serves all fields occuring in the values of the ProjectsDataHandler
    json object.

    Loaded through /api/v1/projects_fields
    """
    def get(self):
        undefined = self.get_argument("undefined", "False")
        undefined = (string.lower(undefined) == "true")
        all_projects = self.get_argument("all_projects", "True")
        all_projects = (str(all_projects).lower() == "true")
        field_items = self.list_project_fields(undefined=undefined, all_projects=all_projects)
        self.write(json.dumps(list(field_items)))

class ProjectDataHandler(SafeHandler):
    """ Serves brief information of a given project.

    Loaded through /api/v1/project_summary/([^/]*)$
    """
    def get(self, project):
        self.set_header("Content-type", "application/json")
        self.write(json.dumps(self.project_info(project)))

    def project_info(self, project):
        result = self.application.projects_db.view("project/summary")[project]
        return result.rows[0].value

class ProjectSamplesDataHandler(SafeHandler):
    """ Serves brief info about all samples in a given project.

    Loaded through /api/v1/projects/([^/]*)$
    """
    def get(self, project):
        self.set_header("Content-type", "application/json")
        self.write(json.dumps(self.sample_list(project), default=dthandler))

    def sample_list(self, project):
        sample_view = self.application.projects_db.view("project/samples")
        result = sample_view[project]

        samples = result.rows[0].value
        samples = OrderedDict(sorted(samples.iteritems(), key=lambda x: x[0]))
        return samples

class ProjectSamplesHandler(SafeHandler):
    """ Serves a page which lists the samples of a given project, with some
    brief information for each sample.
    """
    def get(self, project):
        t = self.application.loader.load("project_samples.html")
        self.write(t.generate(project=project, user=self.get_current_user_name()))

class ProjectsHandler(SafeHandler):
    """ Serves a page with all projects listed, along with some brief info.
    """
    def get(self):
        t = self.application.loader.load("projects.html")
        self.write(t.generate(columns = COLUMNS, all_projects=True, user=self.get_current_user_name()))

class OpenProjectsHandler(SafeHandler):
    """ Serves a page with all OPEN projects listed, along with some brief info.
    """
    def get(self):
        t = self.application.loader.load("projects.html")
        self.write(t.generate(columns = COLUMNS, all_projects=False, user=self.get_current_user_name()))

class UppmaxProjectsDataHandler(SafeHandler):
    """ Serves a list of UPPNEX projects where the storage quota have
    been logged.

    Loaded through /api/v1/uppmax_projects
    """
    def get(self):
        self.set_header("Content-type", "application/json")
        self.write(json.dumps(self.list_projects()))

    def list_projects(self):
        project_list = []
        view = self.application.uppmax_db.view("status/projects", group_level=1)
        for row in view:
            project_list.append(row.key)

        return project_list
<|endoftext|>


<commit_before>import struct
import binascii
import logging
import zipfile

try:
    from cStringIO import StringIO
except ImportError:
    from StringIO import StringIO

import gevent
from gevent import event
from gevent import queue
from Crypto.Random import random

from steam.steamid import SteamID
from steam.enums import EResult, EUniverse
from steam.enums.emsg import EMsg
from steam.client import crypto
from steam.client.connection import TCPConnection
from steam.client.msg import is_proto, clear_proto_bit
from steam.client.msg import Msg, MsgProto

server_list = [
    ('162.254.196.41', '27020'), ('162.254.196.40', '27021'),
    ('162.254.196.43', '27019'), ('162.254.196.40', '27018'),
    ('162.254.196.43', '27020'), ('162.254.196.41', '27019'),
    ('162.254.196.41', '27018'), ('162.254.196.42', '27020'),
    ('162.254.196.41', '27017'), ('162.254.196.41', '27021'),
    ('146.66.152.10', '27017'), ('146.66.152.10', '27018'),
    ('146.66.152.11', '27019'), ('146.66.152.11', '27020'),
    ('146.66.152.10', '27019'), ('162.254.197.42', '27018'),
    ('162.254.197.41', '27019'), ('162.254.197.41', '27017'),
    ('208.78.164.14', '27017'), ('208.78.164.14', '27019'),
    ('208.78.164.9', '27019'), ('208.78.164.14', '27018'),
    ('208.78.164.9', '27018'), ('208.78.164.13', '27017'),
]

logger = logging.getLogger("CMClient")

class CMClient:
    TCP = 0
    UDP = 1

    def __init__(self, protocol=0):
        self.verbose_debug = False
        self.reconnect = False

        self._init_attributes()

        self.registered_callbacks = {}

        if protocol == CMClient.TCP:
            self.connection = TCPConnection()
        # elif protocol == CMClient.UDP:
        #     self.connection = UDPConnection()
        else:
            raise ValueError("Only TCP is supported")

        self.event_connected = event.Event()
        self.event_ready = event.Event()
        self.event_disconnected = event.Event()

        self.register_callback(EMsg.ChannelEncryptRequest, self._handle_encrypt_request),
        self.register_callback(EMsg.Multi, self._handle_multi),
        self.register_callback(EMsg.ClientLogOnResponse, self._handle_logon),

    def connect(self, reconnect=None):
        if reconnect is not None:
            self.reconnect = reconnect

        logger.debug("Connect (reconnect=%s)" % self.reconnect)

        while True:
            with gevent.Timeout(15, False):
                server_addr = random.choice(server_list)
                self.connection.connect(server_addr)

            if not self.connection.event_connected.is_set():
                if self.reconnect:
                    logger.debug("Failed to connect. Retrying...")
                    continue

                logger.debug("Failed to connect")
                return False
            break

        logger.debug("Event: Connected")
        self.event_connected.set()
        self._recv_loop = gevent.spawn(self._recv_messages)
        return True

    def disconnect(self):
        self.connection.disconnect()

        self._recv_loop.kill(block=False)

        if self._heartbeat_loop:
            self._heartbeat_loop.kill()

        self._init_attributes()

        self.event_connected.clear()
        self.event_ready.clear()
        self.event_disconnected.set()

        logger.debug("Event: Disconnected")

    def _init_attributes(self):
        self.key = None

        self.steam_id = None
        self.session_id = None

        self._recv_loop = None
        self._heartbeat_loop = None

    def send_message(self, message):
        if not isinstance(message, (Msg, MsgProto)):
            raise ValueError("Expected Msg or MsgProto, got %s" % message)

        if self.steam_id:
            message.steamID = self.steam_id
        if self.session_id:
            message.sessionID = self.session_id

        if self.verbose_debug:
            logger.debug("Outgoing: %s\n%s" % (repr(message), str(message)))
        else:
            logger.debug("Outgoing: %s", repr(message))

        data = message.serialize()

        if self.key:
            data = crypto.encrypt(data, self.key)

        self.connection.put_message(data)

    def _recv_messages(self):
        while True:
            try:
                message = self.connection.get_message(timeout=1)
            except queue.Empty:
                if not self.connection.event_connected.is_set():
                    self.disconnect()
                    if self.reconnect:
                        gevent.spawn(self.connect)
                    return
                continue

            if self.key:
                message = crypto.decrypt(message, self.key)

            self._parse_message(message)

    def _parse_message(self, message):
            emsg_id, = struct.unpack_from("<I", message)
            emsg = EMsg(clear_proto_bit(emsg_id))

            if emsg in (EMsg.ChannelEncryptRequest,
                        EMsg.ChannelEncryptResponse,
                        EMsg.ChannelEncryptResult,
                        ):

                msg = Msg(emsg, message)
            else:
                try:
                    if is_proto(emsg_id):
                        msg = MsgProto(emsg, message)
                    else:
                        msg = Msg(emsg, message, extended=True)
                except:
                    logger.fatal("Failed to deserialize message: %s %s",
                                 str(emsg),
                                 is_proto(emsg_id)
                                 )
                    raise

            gevent.spawn(self.dispatch_message, emsg, msg)

    def dispatch_message(self, emsg, msg):
        if self.verbose_debug:
            logger.debug("Incoming: %s\n%s" % (repr(msg), str(msg)))
        else:
            logger.debug("Incoming: %s", repr(msg))

        if emsg in self.registered_callbacks:
            for callback in list(self.registered_callbacks[emsg]):
                if isinstance(callback, event.AsyncResult):
                    self.unregister_callback(emsg, callback)
                    callback.set((emsg, msg))
                else:
                    callback(emsg, msg)

    def register_callback(self, emsg, callback):
        if emsg not in self.registered_callbacks:
            self.registered_callbacks[emsg] = [callback]
        else:
            allbacks = self.registered_callbacks[emsg]

            if callback not in callbacks:
                callbacks.append(callback)

    def unregister_callback(self, emsg, callback):
        if (emsg not in self.registered_callbacks
           or callback not in self.registered_callbacks[emsg]):
            return ValueError("Callback is not registered")

        callbacks = self.registered_callbacks[emsg]

        if len(callbacks) == 1:
            self.registered_callbacks.pop(emsg)
        else:
            callbacks.pop(callbacks.index(callback))

    def wait_for_message(self, emsg, block=True, timeout=None):
        result = event.AsyncResult()
        self.register_callback(emsg, result)
        return result.get(block, timeout)[1]

    def _handle_encrypt_request(self, emsg, msg):
        logger.debug("Securing channel")

        if msg.body.protocolVersion != 1:
            raise RuntimeError("Unsupported protocol version")
        if msg.body.universe != EUniverse.Public:
            raise RuntimeError("Unsupported universe")

        resp = Msg(EMsg.ChannelEncryptResponse)

        key, resp.body.key = crypto.generate_session_key()
        resp.body.crc = binascii.crc32(resp.body.key) & 0xffffffff

        self.send_message(resp)

        msg = self.wait_for_message(EMsg.ChannelEncryptResult)

        if msg.body.result != EResult.OK:
            logger.debug("Failed to secure channel: %s" % msg.body.result)
            self.disconnect()
            return

        logger.debug("Channel secured")

        self.key = key
        self.event_ready.set()

        logger.debug("Event: Ready")

    def _handle_multi(self, emsg, msg):
        logger.debug("Unpacking CMsgMulti")

        if msg.body.size_unzipped:
            logger.debug("Unzipping body")

            data = zipfile.ZipFile(StringIO(msg.body.message_body)).read('z')

            if len(data) != msg.body.size_unzipped:
                logger.fatal("Unzipped size mismatch")
                self.disconnect()
                return
        else:
            data = msg.body.message_body

        while len(data) > 0:
            size, = struct.unpack_from("<I", data)
            self._parse_message(data[4:4+size])
            data = data[4+size:]

    def _heartbeat(self, interval):
        message = MsgProto(EMsg.ClientHeartBeat)

        while True:
            gevent.sleep(interval)
            self.send_message(message)

    def _handle_logon(self, emsg, msg):
        if msg.body.eresult != EResult.OK:
            self.disconnect()
            return

        logger.debug("Logon completed")

        self.steam_id = SteamID(msg.header.steamid)
        self.session_id = msg.header.client_sessionid

        if self._heartbeat_loop:
            self._heartbeat_loop.kill()

        logger.debug("Heartbeat started.")

        interval = msg.body.out_of_game_heartbeat_seconds
        self._heartbeat_loop = gevent.spawn(self._heartbeat, interval)
<commit_msg>CMClient reconnects always, until disconnect<commit_after>import struct
import binascii
import logging
import zipfile

try:
    from cStringIO import StringIO
except ImportError:
    from StringIO import StringIO

import gevent
from gevent import event
from gevent import queue
from Crypto.Random import random

from steam.steamid import SteamID
from steam.enums import EResult, EUniverse
from steam.enums.emsg import EMsg
from steam.client import crypto
from steam.client.connection import TCPConnection
from steam.client.msg import is_proto, clear_proto_bit
from steam.client.msg import Msg, MsgProto

server_list = [
    ('162.254.196.41', '27020'), ('162.254.196.40', '27021'),
    ('162.254.196.43', '27019'), ('162.254.196.40', '27018'),
    ('162.254.196.43', '27020'), ('162.254.196.41', '27019'),
    ('162.254.196.41', '27018'), ('162.254.196.42', '27020'),
    ('162.254.196.41', '27017'), ('162.254.196.41', '27021'),
    ('146.66.152.10', '27017'), ('146.66.152.10', '27018'),
    ('146.66.152.11', '27019'), ('146.66.152.11', '27020'),
    ('146.66.152.10', '27019'), ('162.254.197.42', '27018'),
    ('162.254.197.41', '27019'), ('162.254.197.41', '27017'),
    ('208.78.164.14', '27017'), ('208.78.164.14', '27019'),
    ('208.78.164.9', '27019'), ('208.78.164.14', '27018'),
    ('208.78.164.9', '27018'), ('208.78.164.13', '27017'),
]

logger = logging.getLogger("CMClient")

class CMClient:
    TCP = 0
    UDP = 1

    def __init__(self, protocol=0):
        self.verbose_debug = False

        self._init_attributes()

        self.registered_callbacks = {}

        if protocol == CMClient.TCP:
            self.connection = TCPConnection()
        # elif protocol == CMClient.UDP:
        #     self.connection = UDPConnection()
        else:
            raise ValueError("Only TCP is supported")

        self.event_connected = event.Event()
        self.event_ready = event.Event()
        self.event_disconnected = event.Event()

        self.register_callback(EMsg.ChannelEncryptRequest, self._handle_encrypt_request),
        self.register_callback(EMsg.Multi, self._handle_multi),
        self.register_callback(EMsg.ClientLogOnResponse, self._handle_logon),

    def connect(self):
        logger.debug("Connect initiated.")

        while True:
            with gevent.Timeout(15, False):
                server_addr = random.choice(server_list)
                self.connection.connect(server_addr)

            if not self.connection.event_connected.is_set():
                logger.debug("Failed to connect. Retrying...")
                continue

            break

        logger.debug("Event: Connected")
        self.event_connected.set()
        self._recv_loop = gevent.spawn(self._recv_messages)

    def disconnect(self):
        self.connection.disconnect()

        self._recv_loop.kill(block=False)

        if self._heartbeat_loop:
            self._heartbeat_loop.kill()

        self._init_attributes()

        self.event_connected.clear()
        self.event_ready.clear()
        self.event_disconnected.set()

        logger.debug("Event: Disconnected")

    def _init_attributes(self):
        self.key = None

        self.steam_id = None
        self.session_id = None

        self._recv_loop = None
        self._heartbeat_loop = None

    def send_message(self, message):
        if not isinstance(message, (Msg, MsgProto)):
            raise ValueError("Expected Msg or MsgProto, got %s" % message)

        if self.steam_id:
            message.steamID = self.steam_id
        if self.session_id:
            message.sessionID = self.session_id

        if self.verbose_debug:
            logger.debug("Outgoing: %s\n%s" % (repr(message), str(message)))
        else:
            logger.debug("Outgoing: %s", repr(message))

        data = message.serialize()

        if self.key:
            data = crypto.encrypt(data, self.key)

        self.connection.put_message(data)

    def _recv_messages(self):
        while True:
            try:
                message = self.connection.get_message(timeout=1)
            except queue.Empty:
                if not self.connection.event_connected.is_set():
                    self.disconnect()
                    gevent.spawn(self.connect)
                    return
                continue

            if self.key:
                message = crypto.decrypt(message, self.key)

            self._parse_message(message)

    def _parse_message(self, message):
            emsg_id, = struct.unpack_from("<I", message)
            emsg = EMsg(clear_proto_bit(emsg_id))

            if emsg in (EMsg.ChannelEncryptRequest,
                        EMsg.ChannelEncryptResponse,
                        EMsg.ChannelEncryptResult,
                        ):

                msg = Msg(emsg, message)
            else:
                try:
                    if is_proto(emsg_id):
                        msg = MsgProto(emsg, message)
                    else:
                        msg = Msg(emsg, message, extended=True)
                except:
                    logger.fatal("Failed to deserialize message: %s %s",
                                 str(emsg),
                                 is_proto(emsg_id)
                                 )
                    raise

            gevent.spawn(self.dispatch_message, emsg, msg)

    def dispatch_message(self, emsg, msg):
        if self.verbose_debug:
            logger.debug("Incoming: %s\n%s" % (repr(msg), str(msg)))
        else:
            logger.debug("Incoming: %s", repr(msg))

        if emsg in self.registered_callbacks:
            for callback in list(self.registered_callbacks[emsg]):
                if isinstance(callback, event.AsyncResult):
                    self.unregister_callback(emsg, callback)
                    callback.set((emsg, msg))
                else:
                    callback(emsg, msg)

    def register_callback(self, emsg, callback):
        if emsg not in self.registered_callbacks:
            self.registered_callbacks[emsg] = [callback]
        else:
            allbacks = self.registered_callbacks[emsg]

            if callback not in callbacks:
                callbacks.append(callback)

    def unregister_callback(self, emsg, callback):
        if (emsg not in self.registered_callbacks
           or callback not in self.registered_callbacks[emsg]):
            return ValueError("Callback is not registered")

        callbacks = self.registered_callbacks[emsg]

        if len(callbacks) == 1:
            self.registered_callbacks.pop(emsg)
        else:
            callbacks.pop(callbacks.index(callback))

    def wait_for_message(self, emsg, block=True, timeout=None):
        result = event.AsyncResult()
        self.register_callback(emsg, result)
        return result.get(block, timeout)[1]

    def _handle_encrypt_request(self, emsg, msg):
        logger.debug("Securing channel")

        if msg.body.protocolVersion != 1:
            raise RuntimeError("Unsupported protocol version")
        if msg.body.universe != EUniverse.Public:
            raise RuntimeError("Unsupported universe")

        resp = Msg(EMsg.ChannelEncryptResponse)

        key, resp.body.key = crypto.generate_session_key()
        resp.body.crc = binascii.crc32(resp.body.key) & 0xffffffff

        self.send_message(resp)

        msg = self.wait_for_message(EMsg.ChannelEncryptResult)

        if msg.body.result != EResult.OK:
            logger.debug("Failed to secure channel: %s" % msg.body.result)
            self.disconnect()
            return

        logger.debug("Channel secured")

        self.key = key
        self.event_ready.set()

        logger.debug("Event: Ready")

    def _handle_multi(self, emsg, msg):
        logger.debug("Unpacking CMsgMulti")

        if msg.body.size_unzipped:
            logger.debug("Unzipping body")

            data = zipfile.ZipFile(StringIO(msg.body.message_body)).read('z')

            if len(data) != msg.body.size_unzipped:
                logger.fatal("Unzipped size mismatch")
                self.disconnect()
                return
        else:
            data = msg.body.message_body

        while len(data) > 0:
            size, = struct.unpack_from("<I", data)
            self._parse_message(data[4:4+size])
            data = data[4+size:]

    def _heartbeat(self, interval):
        message = MsgProto(EMsg.ClientHeartBeat)

        while True:
            gevent.sleep(interval)
            self.send_message(message)

    def _handle_logon(self, emsg, msg):
        result = msg.body.eresult
        if result != EResult.OK:
            self.disconnect()

            if result in (EResult.TryAnotherCM, EResult.ServiceUnavailable):
                gevent.spawn(self.connect)

            return

        logger.debug("Logon completed")

        self.steam_id = SteamID(msg.header.steamid)
        self.session_id = msg.header.client_sessionid

        if self._heartbeat_loop:
            self._heartbeat_loop.kill()

        logger.debug("Heartbeat started.")

        interval = msg.body.out_of_game_heartbeat_seconds
        self._heartbeat_loop = gevent.spawn(self._heartbeat, interval)
<|endoftext|>


<commit_before>"""
查询相关函数
"""
from collections import defaultdict
from typing import Dict, List, Tuple

from ddtrace import tracer
from flask import Blueprint, current_app as app, escape, flash, redirect, render_template, request, session, url_for

from everyclass.common.format import contains_chinese
from everyclass.common.time import get_day_chinese, get_time_chinese, lesson_string_to_tuple
from everyclass.rpc.entity import Entity
from everyclass.server import logger
from everyclass.server.consts import MSG_INVALID_IDENTIFIER, SESSION_CURRENT_USER, SESSION_LAST_VIEWED_STUDENT, URL_EMPTY_SEMESTER
from everyclass.server.user import service as user_service
from everyclass.server.utils import semester_calculate
from everyclass.server.utils.access_control import check_permission
from everyclass.server.utils.decorators import disallow_in_maintenance, url_semester_check
from everyclass.server.utils.encryption import decrypt
from everyclass.server.utils.err_handle import handle_exception_with_error_page
from everyclass.server.utils.session import StudentSession

query_blueprint = Blueprint('query', __name__)

@query_blueprint.route('/query', methods=['GET', 'POST'])
@disallow_in_maintenance
def query():
    """
    All in one 搜索入口，可以查询学生、老师、教室，然后跳转到具体资源页面

    正常情况应该是 post 方法，但是也兼容 get 防止意外情况，提高用户体验

    埋点：
    - `query_resource_type`, 查询的资源类型: classroom, single_student, single_teacher, multiple_people, or not_exist.
    - `query_type`, 查询方式（姓名、学工号）: by_name, by_id, other
    """

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

    keyword = request.values.get('id')

    if not keyword or len(keyword) < 2:
        flash('请输入需要查询的姓名、学号、教工号或教室名称，长度不要小于2个字符')
        return redirect(url_for('main.main'))

    # 调用 api-server 搜索
    with tracer.trace('rpc_search'):
        try:
            rpc_result = Entity.search(keyword)
        except Exception as e:
            return handle_exception_with_error_page(e)

    # 不同类型渲染不同模板
    if len(rpc_result.classrooms) >= 1:  # 优先展示教室
        # 我们在 kibana 中使用服务名过滤 apm 文档，所以 tag 不用增加服务名前缀
        tracer.current_root_span().set_tag("query_resource_type", "classroom")
        tracer.current_root_span().set_tag("query_type", "by_name")

        if len(rpc_result.classrooms) > 1:  # 多个教室选择
            return render_template('query/multipleClassroomChoice.html',
                                   name=keyword,
                                   classrooms=rpc_result.classrooms)
        return redirect('/classroom/{}/{}'.format(rpc_result.classrooms[0].room_id_encoded,
                                                  rpc_result.classrooms[0].semesters[-1]))
    elif len(rpc_result.students) == 1 and len(rpc_result.teachers) == 0:  # 一个学生
        tracer.current_root_span().set_tag("query_resource_type", "single_student")
        if contains_chinese(keyword):
            tracer.current_root_span().set_tag("query_type", "by_name")
        else:
            tracer.current_root_span().set_tag("query_type", "by_id")

        if len(rpc_result.students[0].semesters) < 1:
            flash('没有可用学期')
            return redirect(url_for('main.main'))

        return redirect('/student/{}/{}'.format(rpc_result.students[0].student_id_encoded,
                                                rpc_result.students[0].semesters[-1]))
    elif len(rpc_result.teachers) == 1 and len(rpc_result.students) == 0:  # 一个老师
        tracer.current_root_span().set_tag("query_resource_type", "single_teacher")
        if contains_chinese(keyword):
            tracer.current_root_span().set_tag("query_type", "by_name")
        else:
            tracer.current_root_span().set_tag("query_type", "by_id")

        if len(rpc_result.teachers[0].semesters) < 1:
            flash('没有可用学期')
            return redirect(url_for('main.main'))

        return redirect('/teacher/{}/{}'.format(rpc_result.teachers[0].teacher_id_encoded,
                                                rpc_result.teachers[0].semesters[-1]))
    elif len(rpc_result.teachers) >= 1 or len(rpc_result.students) >= 1:
        # multiple students, multiple teachers, or mix of both
        tracer.current_root_span().set_tag("query_resource_type", "multiple_people")

        if contains_chinese(keyword):
            tracer.current_root_span().set_tag("query_type", "by_name")

        else:
            tracer.current_root_span().set_tag("query_type", "by_id")

        return render_template('query/peopleWithSameName.html',
                               name=keyword,
                               students=rpc_result.students,
                               teachers=rpc_result.teachers)
    else:
        logger.info("No result for user search", extra={"keyword": request.values.get('id')})
        tracer.current_root_span().set_tag("query_resource_type", "not_exist")
        tracer.current_root_span().set_tag("query_type", "other")

        flash('没有找到任何有关 {} 的信息，如果你认为这不应该发生，请联系我们。'.format(escape(request.values.get('id'))))
        return redirect(url_for('main.main'))

@query_blueprint.route('/student/<string:url_sid>/semester/<string:url_semester>')
@url_semester_check
@disallow_in_maintenance
def get_student(url_sid: str, url_semester: str):
    """学生查询"""
    # decrypt identifier in URL
    try:
        _, student_id = decrypt(url_sid, resource_type='student')
    except ValueError:
        return render_template("common/error.html", message=MSG_INVALID_IDENTIFIER)

    if url_semester == URL_EMPTY_SEMESTER:
        try:
            student = Entity.get_student(student_id)
        except Exception as e:
            return handle_exception_with_error_page(e)
    else:
        # RPC 获得学生课表
        try:
            student = Entity.get_student_timetable(student_id, url_semester)
        except Exception as e:
            return handle_exception_with_error_page(e)

    # save sid_orig to session for verifying purpose
    # must be placed before privacy level check. Otherwise a registered user could be redirected to register page.
    session[SESSION_LAST_VIEWED_STUDENT] = StudentSession(sid_orig=student.student_id,
                                                          sid=student.student_id_encoded,
                                                          name=student.name)

    # 权限检查，如果没有权限则返回
    has_permission, return_val = check_permission(student)
    if not has_permission:
        return return_val

    if url_semester != URL_EMPTY_SEMESTER:
        with tracer.trace('process_rpc_result'):
            cards: Dict[Tuple[int, int], List[Dict[str, str]]] = dict()
            for card in student.cards:
                day, time = lesson_string_to_tuple(card.lesson)
                if (day, time) not in cards:
                    cards[(day, time)] = list()
                cards[(day, time)].append(card)
            empty_5, empty_6, empty_sat, empty_sun = _empty_column_check(cards)
            available_semesters = semester_calculate(url_semester, sorted(student.semesters))

        # 增加访客记录
        user_service.add_visitor_count(student.student_id, session.get(SESSION_CURRENT_USER, None))

        return render_template('query/student.html',
                               have_semesters=True,
                               student=student,
                               cards=cards,
                               empty_sat=empty_sat,
                               empty_sun=empty_sun,
                               empty_6=empty_6,
                               empty_5=empty_5,
                               available_semesters=available_semesters,
                               current_semester=url_semester)
    else:
        # 无学期
        return render_template('query/student.html',
                               have_semesters=False,
                               student=student,
                               current_semester=url_semester)

@query_blueprint.route('/teacher/<string:url_tid>/semester/<string:url_semester>')
@disallow_in_maintenance
@url_semester_check
def get_teacher(url_tid, url_semester):
    """老师查询"""

    # decrypt identifier in URL
    try:
        _, teacher_id = decrypt(url_tid, resource_type='teacher')
    except ValueError:
        return render_template("common/error.html", message=MSG_INVALID_IDENTIFIER)

    # RPC to get teacher timetable
    try:
        teacher = Entity.get_teacher_timetable(teacher_id, url_semester)
    except Exception as e:
        return handle_exception_with_error_page(e)

    with tracer.trace('process_rpc_result'):
        cards = defaultdict(list)
        for card in teacher.cards:
            day, time = lesson_string_to_tuple(card.lesson)
            if (day, time) not in cards:
                cards[(day, time)] = list()
            cards[(day, time)].append(card)

    empty_5, empty_6, empty_sat, empty_sun = _empty_column_check(cards)

    available_semesters = semester_calculate(url_semester, teacher.semesters)

    return render_template('query/teacher.html',
                           teacher=teacher,
                           cards=cards,
                           empty_sat=empty_sat,
                           empty_sun=empty_sun,
                           empty_6=empty_6,
                           empty_5=empty_5,
                           available_semesters=available_semesters,
                           current_semester=url_semester)

@query_blueprint.route('/classroom/<string:url_rid>/semester/<string:url_semester>')
@url_semester_check
@disallow_in_maintenance
def get_classroom(url_rid, url_semester):
    """教室查询"""
    # decrypt identifier in URL
    try:
        _, room_id = decrypt(url_rid, resource_type='room')
    except ValueError:
        return render_template("common/error.html", message=MSG_INVALID_IDENTIFIER)

    # RPC to get classroom timetable
    try:
        room = Entity.get_classroom_timetable(url_semester, room_id)
    except Exception as e:
        return handle_exception_with_error_page(e)

    with tracer.trace('process_rpc_result'):
        cards = defaultdict(list)
        for card in room.cards:
            day, time = lesson_string_to_tuple(card.lesson)
            cards[(day, time)].append(card)

    empty_5, empty_6, empty_sat, empty_sun = _empty_column_check(cards)

    available_semesters = semester_calculate(url_semester, room.semesters)

    return render_template('query/room.html',
                           room=room,
                           cards=cards,
                           empty_sat=empty_sat,
                           empty_sun=empty_sun,
                           empty_6=empty_6,
                           empty_5=empty_5,
                           available_semesters=available_semesters,
                           current_semester=url_semester)

@query_blueprint.route('/card/<string:url_cid>/semester/<string:url_semester>')
@url_semester_check
@disallow_in_maintenance
def get_card(url_cid: str, url_semester: str):
    """课程查询"""
    # decrypt identifier in URL
    try:
        _, card_id = decrypt(url_cid, resource_type='klass')
    except ValueError:
        return render_template("common/error.html", message=MSG_INVALID_IDENTIFIER)

    # RPC to get card
    try:
        card = Entity.get_card(url_semester, card_id)
    except Exception as e:
        return handle_exception_with_error_page(e)

    day, time = lesson_string_to_tuple(card.lesson)

    # cotc_id = COTeachingClass.get_id_by_card(card)
    # course_review_doc = CourseReview.get_review(cotc_id)

    return render_template('query/card.html',
                           card=card,
                           card_day=get_day_chinese(day),
                           card_time=get_time_chinese(time),
                           # cotc_id=cotc_id,
                           # cotc_rating=course_review_doc["avg_rate"],
                           cotc_id=0,
                           cotc_rating=0,
                           current_semester=url_semester
                           )

def _empty_column_check(cards: dict) -> Tuple[bool, bool, bool, bool]:
    """检查是否周末和晚上有课，返回三个布尔值"""
    with tracer.trace('_empty_column_check'):
        # 空闲周末判断，考虑到大多数人周末都是没有课程的
        empty_sat = True
        for cls_time in range(1, 7):
            if (6, cls_time) in cards:
                empty_sat = False

        empty_sun = True
        for cls_time in range(1, 7):
            if (7, cls_time) in cards:
                empty_sun = False

        # 空闲课程判断，考虑到大多数人11-12节都是没有课程的
        empty_6 = True
        for cls_day in range(1, 8):
            if (cls_day, 6) in cards:
                empty_6 = False
        empty_5 = True
        for cls_day in range(1, 8):
            if (cls_day, 5) in cards:
                empty_5 = False
    return empty_5, empty_6, empty_sat, empty_sun
<commit_msg>feature(query/card): test if can be compatible with original URL pattern<commit_after>"""
查询相关函数
"""
from collections import defaultdict
from typing import Dict, List, Tuple

from ddtrace import tracer
from flask import Blueprint, current_app as app, escape, flash, redirect, render_template, request, session, url_for

from everyclass.common.format import contains_chinese
from everyclass.common.time import get_day_chinese, get_time_chinese, lesson_string_to_tuple
from everyclass.rpc.entity import Entity
from everyclass.server import logger
from everyclass.server.consts import MSG_INVALID_IDENTIFIER, SESSION_CURRENT_USER, SESSION_LAST_VIEWED_STUDENT, URL_EMPTY_SEMESTER
from everyclass.server.user import service as user_service
from everyclass.server.utils import semester_calculate
from everyclass.server.utils.access_control import check_permission
from everyclass.server.utils.decorators import disallow_in_maintenance, url_semester_check
from everyclass.server.utils.encryption import decrypt
from everyclass.server.utils.err_handle import handle_exception_with_error_page
from everyclass.server.utils.session import StudentSession

query_blueprint = Blueprint('query', __name__)

@query_blueprint.route('/query', methods=['GET', 'POST'])
@disallow_in_maintenance
def query():
    """
    All in one 搜索入口，可以查询学生、老师、教室，然后跳转到具体资源页面

    正常情况应该是 post 方法，但是也兼容 get 防止意外情况，提高用户体验

    埋点：
    - `query_resource_type`, 查询的资源类型: classroom, single_student, single_teacher, multiple_people, or not_exist.
    - `query_type`, 查询方式（姓名、学工号）: by_name, by_id, other
    """

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

    keyword = request.values.get('id')

    if not keyword or len(keyword) < 2:
        flash('请输入需要查询的姓名、学号、教工号或教室名称，长度不要小于2个字符')
        return redirect(url_for('main.main'))

    # 调用 api-server 搜索
    with tracer.trace('rpc_search'):
        try:
            rpc_result = Entity.search(keyword)
        except Exception as e:
            return handle_exception_with_error_page(e)

    # 不同类型渲染不同模板
    if len(rpc_result.classrooms) >= 1:  # 优先展示教室
        # 我们在 kibana 中使用服务名过滤 apm 文档，所以 tag 不用增加服务名前缀
        tracer.current_root_span().set_tag("query_resource_type", "classroom")
        tracer.current_root_span().set_tag("query_type", "by_name")

        if len(rpc_result.classrooms) > 1:  # 多个教室选择
            return render_template('query/multipleClassroomChoice.html',
                                   name=keyword,
                                   classrooms=rpc_result.classrooms)
        return redirect('/classroom/{}/{}'.format(rpc_result.classrooms[0].room_id_encoded,
                                                  rpc_result.classrooms[0].semesters[-1]))
    elif len(rpc_result.students) == 1 and len(rpc_result.teachers) == 0:  # 一个学生
        tracer.current_root_span().set_tag("query_resource_type", "single_student")
        if contains_chinese(keyword):
            tracer.current_root_span().set_tag("query_type", "by_name")
        else:
            tracer.current_root_span().set_tag("query_type", "by_id")

        if len(rpc_result.students[0].semesters) < 1:
            flash('没有可用学期')
            return redirect(url_for('main.main'))

        return redirect('/student/{}/{}'.format(rpc_result.students[0].student_id_encoded,
                                                rpc_result.students[0].semesters[-1]))
    elif len(rpc_result.teachers) == 1 and len(rpc_result.students) == 0:  # 一个老师
        tracer.current_root_span().set_tag("query_resource_type", "single_teacher")
        if contains_chinese(keyword):
            tracer.current_root_span().set_tag("query_type", "by_name")
        else:
            tracer.current_root_span().set_tag("query_type", "by_id")

        if len(rpc_result.teachers[0].semesters) < 1:
            flash('没有可用学期')
            return redirect(url_for('main.main'))

        return redirect('/teacher/{}/{}'.format(rpc_result.teachers[0].teacher_id_encoded,
                                                rpc_result.teachers[0].semesters[-1]))
    elif len(rpc_result.teachers) >= 1 or len(rpc_result.students) >= 1:
        # multiple students, multiple teachers, or mix of both
        tracer.current_root_span().set_tag("query_resource_type", "multiple_people")

        if contains_chinese(keyword):
            tracer.current_root_span().set_tag("query_type", "by_name")

        else:
            tracer.current_root_span().set_tag("query_type", "by_id")

        return render_template('query/peopleWithSameName.html',
                               name=keyword,
                               students=rpc_result.students,
                               teachers=rpc_result.teachers)
    else:
        logger.info("No result for user search", extra={"keyword": request.values.get('id')})
        tracer.current_root_span().set_tag("query_resource_type", "not_exist")
        tracer.current_root_span().set_tag("query_type", "other")

        flash('没有找到任何有关 {} 的信息，如果你认为这不应该发生，请联系我们。'.format(escape(request.values.get('id'))))
        return redirect(url_for('main.main'))

@query_blueprint.route('/student/<string:url_sid>/semester/<string:url_semester>')
@url_semester_check
@disallow_in_maintenance
def get_student(url_sid: str, url_semester: str):
    """学生查询"""
    # decrypt identifier in URL
    try:
        _, student_id = decrypt(url_sid, resource_type='student')
    except ValueError:
        return render_template("common/error.html", message=MSG_INVALID_IDENTIFIER)

    if url_semester == URL_EMPTY_SEMESTER:
        try:
            student = Entity.get_student(student_id)
        except Exception as e:
            return handle_exception_with_error_page(e)
    else:
        # RPC 获得学生课表
        try:
            student = Entity.get_student_timetable(student_id, url_semester)
        except Exception as e:
            return handle_exception_with_error_page(e)

    # save sid_orig to session for verifying purpose
    # must be placed before privacy level check. Otherwise a registered user could be redirected to register page.
    session[SESSION_LAST_VIEWED_STUDENT] = StudentSession(sid_orig=student.student_id,
                                                          sid=student.student_id_encoded,
                                                          name=student.name)

    # 权限检查，如果没有权限则返回
    has_permission, return_val = check_permission(student)
    if not has_permission:
        return return_val

    if url_semester != URL_EMPTY_SEMESTER:
        with tracer.trace('process_rpc_result'):
            cards: Dict[Tuple[int, int], List[Dict[str, str]]] = dict()
            for card in student.cards:
                day, time = lesson_string_to_tuple(card.lesson)
                if (day, time) not in cards:
                    cards[(day, time)] = list()
                cards[(day, time)].append(card)
            empty_5, empty_6, empty_sat, empty_sun = _empty_column_check(cards)
            available_semesters = semester_calculate(url_semester, sorted(student.semesters))

        # 增加访客记录
        user_service.add_visitor_count(student.student_id, session.get(SESSION_CURRENT_USER, None))

        return render_template('query/student.html',
                               have_semesters=True,
                               student=student,
                               cards=cards,
                               empty_sat=empty_sat,
                               empty_sun=empty_sun,
                               empty_6=empty_6,
                               empty_5=empty_5,
                               available_semesters=available_semesters,
                               current_semester=url_semester)
    else:
        # 无学期
        return render_template('query/student.html',
                               have_semesters=False,
                               student=student,
                               current_semester=url_semester)

@query_blueprint.route('/teacher/<string:url_tid>/semester/<string:url_semester>')
@disallow_in_maintenance
@url_semester_check
def get_teacher(url_tid, url_semester):
    """老师查询"""

    # decrypt identifier in URL
    try:
        _, teacher_id = decrypt(url_tid, resource_type='teacher')
    except ValueError:
        return render_template("common/error.html", message=MSG_INVALID_IDENTIFIER)

    # RPC to get teacher timetable
    try:
        teacher = Entity.get_teacher_timetable(teacher_id, url_semester)
    except Exception as e:
        return handle_exception_with_error_page(e)

    with tracer.trace('process_rpc_result'):
        cards = defaultdict(list)
        for card in teacher.cards:
            day, time = lesson_string_to_tuple(card.lesson)
            if (day, time) not in cards:
                cards[(day, time)] = list()
            cards[(day, time)].append(card)

    empty_5, empty_6, empty_sat, empty_sun = _empty_column_check(cards)

    available_semesters = semester_calculate(url_semester, teacher.semesters)

    return render_template('query/teacher.html',
                           teacher=teacher,
                           cards=cards,
                           empty_sat=empty_sat,
                           empty_sun=empty_sun,
                           empty_6=empty_6,
                           empty_5=empty_5,
                           available_semesters=available_semesters,
                           current_semester=url_semester)

@query_blueprint.route('/classroom/<string:url_rid>/semester/<string:url_semester>')
@url_semester_check
@disallow_in_maintenance
def get_classroom(url_rid, url_semester):
    """教室查询"""
    # decrypt identifier in URL
    try:
        _, room_id = decrypt(url_rid, resource_type='room')
    except ValueError:
        return render_template("common/error.html", message=MSG_INVALID_IDENTIFIER)

    # RPC to get classroom timetable
    try:
        room = Entity.get_classroom_timetable(url_semester, room_id)
    except Exception as e:
        return handle_exception_with_error_page(e)

    with tracer.trace('process_rpc_result'):
        cards = defaultdict(list)
        for card in room.cards:
            day, time = lesson_string_to_tuple(card.lesson)
            cards[(day, time)].append(card)

    empty_5, empty_6, empty_sat, empty_sun = _empty_column_check(cards)

    available_semesters = semester_calculate(url_semester, room.semesters)

    return render_template('query/room.html',
                           room=room,
                           cards=cards,
                           empty_sat=empty_sat,
                           empty_sun=empty_sun,
                           empty_6=empty_6,
                           empty_5=empty_5,
                           available_semesters=available_semesters,
                           current_semester=url_semester)

@query_blueprint.route('/card/<string:url_cid>/semester/<string:url_semester>')
@query_blueprint.route('/card/<string:url_cid>/<string:url_semester>')
@url_semester_check
@disallow_in_maintenance
def get_card(url_cid: str, url_semester: str):
    """课程查询"""
    # decrypt identifier in URL
    try:
        _, card_id = decrypt(url_cid, resource_type='klass')
    except ValueError:
        return render_template("common/error.html", message=MSG_INVALID_IDENTIFIER)

    # RPC to get card
    try:
        card = Entity.get_card(url_semester, card_id)
    except Exception as e:
        return handle_exception_with_error_page(e)

    day, time = lesson_string_to_tuple(card.lesson)

    # cotc_id = COTeachingClass.get_id_by_card(card)
    # course_review_doc = CourseReview.get_review(cotc_id)

    return render_template('query/card.html',
                           card=card,
                           card_day=get_day_chinese(day),
                           card_time=get_time_chinese(time),
                           # cotc_id=cotc_id,
                           # cotc_rating=course_review_doc["avg_rate"],
                           cotc_id=0,
                           cotc_rating=0,
                           current_semester=url_semester
                           )

def _empty_column_check(cards: dict) -> Tuple[bool, bool, bool, bool]:
    """检查是否周末和晚上有课，返回三个布尔值"""
    with tracer.trace('_empty_column_check'):
        # 空闲周末判断，考虑到大多数人周末都是没有课程的
        empty_sat = True
        for cls_time in range(1, 7):
            if (6, cls_time) in cards:
                empty_sat = False

        empty_sun = True
        for cls_time in range(1, 7):
            if (7, cls_time) in cards:
                empty_sun = False

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