Skip to content
Snippets Groups Projects
models.py 61.3 KiB
Newer Older
  • Learn to ignore specific revisions
  • from datetime import datetime, timedelta
    
    from enum import Enum, IntEnum
    
    from flask import abort, current_app, url_for
    
    from flask_hashids import HashidMixin
    
    from flask_login import UserMixin
    
    from sqlalchemy.ext.associationproxy import association_proxy
    
    from typing import Union
    
    from werkzeug.security import generate_password_hash, check_password_hash
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
    from werkzeug.utils import secure_filename
    
    import jwt
    
    import re
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
    import secrets
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
    from app import db, hashids, login, mail, socketio
    from app.converters.vrt import normalize_vrt_file
    from app.email import create_message
    
    ##############################################################################
    # enums                                                                      #
    ##############################################################################
    # region enums
    class CorpusStatus(IntEnum):
        UNPREPARED = 1
        SUBMITTED = 2
        QUEUED = 3
        BUILDING = 4
        BUILT = 5
        FAILED = 6
        STARTING_ANALYSIS_SESSION = 7
        RUNNING_ANALYSIS_SESSION = 8
        CANCELING_ANALYSIS_SESSION = 9
    
    
        @staticmethod
        def get(corpus_status: Union['CorpusStatus', int, str]) -> 'CorpusStatus':
            if isinstance(corpus_status, CorpusStatus):
                return corpus_status
            if isinstance(corpus_status, int):
                return CorpusStatus(corpus_status)
            if isinstance(corpus_status, str):
                return CorpusStatus[corpus_status]
            raise TypeError('corpus_status must be CorpusStatus, int, or str')
    
    
    
    class JobStatus(IntEnum):
        INITIALIZING = 1
        SUBMITTED = 2
        QUEUED = 3
        RUNNING = 4
        CANCELING = 5
        CANCELED = 6
        COMPLETED = 7
        FAILED = 8
    
    
        @staticmethod
        def get(job_status: Union['JobStatus', int, str]) -> 'JobStatus':
            if isinstance(job_status, JobStatus):
                return job_status
            if isinstance(job_status, int):
                return JobStatus(job_status)
            if isinstance(job_status, str):
                return JobStatus[job_status]
            raise TypeError('job_status must be JobStatus, int, or str')
    
    
    
    class Permission(IntEnum):
        '''
        Defines User permissions as integers by the power of 2. User permission
        can be evaluated using the bitwise operator &.
        '''
        ADMINISTRATE = 1
        CONTRIBUTE = 2
        USE_API = 4
    
    
        @staticmethod
        def get(permission: Union['Permission', int, str]) -> 'Permission':
            if isinstance(permission, Permission):
                return permission
            if isinstance(permission, int):
                return Permission(permission)
            if isinstance(permission, str):
                return Permission[permission]
            raise TypeError('permission must be Permission, int, or str')
    
    class UserSettingJobStatusMailNotificationLevel(IntEnum):
        NONE = 1
        END = 2
        ALL = 3
    
    
    
    class ProfilePrivacySettings(IntEnum):
        SHOW_EMAIL = 1
        SHOW_LAST_SEEN = 2
        SHOW_MEMBER_SINCE = 4
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
        @staticmethod
        def get(profile_privacy_setting: Union['ProfilePrivacySettings', int, str]) -> 'ProfilePrivacySettings':
            if isinstance(profile_privacy_setting, ProfilePrivacySettings):
                return profile_privacy_setting
            if isinstance(profile_privacy_setting, int):
                return ProfilePrivacySettings(profile_privacy_setting)
            if isinstance(profile_privacy_setting, str):
                return ProfilePrivacySettings[profile_privacy_setting]
            raise TypeError('profile_privacy_setting must be ProfilePrivacySettings, int, or str')
    
    
    class CorpusFollowerPermission(IntEnum):
    
    Inga Kirschnick's avatar
    Inga Kirschnick committed
        MANAGE_FILES = 2
        MANAGE_FOLLOWERS = 4
        MANAGE_CORPUS = 8
    
    
        @staticmethod
    
        def get(corpus_follower_permission: Union['CorpusFollowerPermission', int, str]) -> 'CorpusFollowerPermission':
            if isinstance(corpus_follower_permission, CorpusFollowerPermission):
                return corpus_follower_permission
            if isinstance(corpus_follower_permission, int):
                return CorpusFollowerPermission(corpus_follower_permission)
            if isinstance(corpus_follower_permission, str):
                return CorpusFollowerPermission[corpus_follower_permission]
            raise TypeError('corpus_follower_permission must be CorpusFollowerPermission, int, or str')
    
    # endregion enums
    
    
    ##############################################################################
    # mixins                                                                     #
    ##############################################################################
    # region mixins
    
    class FileMixin:
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
        '''
        Mixin for db.Model classes. All file related models should use this.
        '''
    
        creation_date = db.Column(db.DateTime, default=datetime.utcnow)
    
        mimetype = db.Column(db.String(255))
    
    
        def file_mixin_to_json_serializeable(self, backrefs=False, relationships=False):
    
            return {
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
                'creation_date': f'{self.creation_date.isoformat()}Z',
    
                'filename': self.filename,
                'mimetype': self.mimetype
            }
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
        
        @classmethod
        def create(cls, file_storage, **kwargs):
            filename = kwargs.pop('filename', file_storage.filename)
            mimetype = kwargs.pop('mimetype', file_storage.mimetype)
            obj = cls(
                filename=secure_filename(filename),
                mimetype=mimetype,
                **kwargs
            )
            db.session.add(obj)
            db.session.flush(objects=[obj])
            db.session.refresh(obj)
            try:
                file_storage.save(obj.path)
            except (AttributeError, OSError) as e:
                current_app.logger.error(e)
                db.session.rollback()
                raise e
            return obj
    
    ##############################################################################
    # type_decorators                                                            #
    ##############################################################################
    # region type_decorators
    class IntEnumColumn(db.TypeDecorator):
        impl = db.Integer
    
        def __init__(self, enum_type, *args, **kwargs):
            super().__init__(*args, **kwargs)
            self.enum_type = enum_type
    
        def process_bind_param(self, value, dialect):
            if isinstance(value, self.enum_type) and isinstance(value.value, int):
                return value.value
            elif isinstance(value, int):
                return self.enum_type(value).value
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
            elif isinstance(value, str):
                return self.enum_type[value].value
    
            else:
                return TypeError()
    
        def process_result_value(self, value, dialect):
            return self.enum_type(value)
    
    
    class ContainerColumn(db.TypeDecorator):
        impl = db.String
    
        def __init__(self, container_type, *args, **kwargs):
            super().__init__(*args, **kwargs)
            self.container_type = container_type
    
        def process_bind_param(self, value, dialect):
            if isinstance(value, self.container_type):
                return json.dumps(value)
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
            elif isinstance(value, str) and isinstance(json.loads(value), self.container_type):
    
                return value
            else:
                return TypeError()
    
        def process_result_value(self, value, dialect):
            return json.loads(value)
    # endregion type_decorators
    
    
    ##############################################################################
    # Models                                                                     #
    ##############################################################################
    # region models
    
    class Role(HashidMixin, db.Model):
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
        # Primary key
    
        id = db.Column(db.Integer, primary_key=True)
    
    Stephan Porada's avatar
    Stephan Porada committed
        # Fields
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
        name = db.Column(db.String(64), unique=True)
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
        default = db.Column(db.Boolean, default=False, index=True)
        permissions = db.Column(db.Integer, default=0)
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
        # Relationships
    
        users = db.relationship('User', back_populates='role', lazy='dynamic')
    
            return f'<Role {self.name}>'
    
        def has_permission(self, permission: Union[Permission, int, str]):
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
            p = Permission.get(permission)
            return self.permissions & p.value == p.value
    
        
        def add_permission(self, permission: Union[Permission, int, str]):
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
            p = Permission.get(permission)
            if not self.has_permission(p):
                self.permissions += p.value
    
        
        def remove_permission(self, permission: Union[Permission, int, str]):
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
            p = Permission.get(permission)
            if self.has_permission(p):
                self.permissions -= p.value
    
    
        def reset_permissions(self):
            self.permissions = 0
    
    
        def to_json_serializeable(self, backrefs=False, relationships=False):
            json_serializeable = {
    
                'id': self.hashid,
                'default': self.default,
                'name': self.name,
    
                'permissions': [
                    x.name for x in Permission
                    if self.has_permission(x.value)
                ]
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
            if backrefs:
                pass
    
            if relationships:
    
                json_serializeable['users'] = {
                    x.hashid: x.to_json_serializeable(relationships=True)
    
                    for x in self.users
                }
    
            return json_serializeable
    
            roles = {
                'User': [],
    
                'API user': [Permission.USE_API],
                'Contributor': [Permission.CONTRIBUTE],
                'Administrator': [
                    Permission.ADMINISTRATE,
                    Permission.CONTRIBUTE,
                    Permission.USE_API
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
                ],
                'System user': []
    
            }
            default_role_name = 'User'
            for role_name, permissions in roles.items():
                role = Role.query.filter_by(name=role_name).first()
    
                    role = Role(name=role_name)
    
                for permission in permissions:
                    role.add_permission(permission)
                role.default = role.name == default_role_name
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
    
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
    class Token(db.Model):
        __tablename__ = 'tokens'
        # Primary key
        id = db.Column(db.Integer, primary_key=True)
        # Foreign keys
        user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
        # Fields
        access_token = db.Column(db.String(64), index=True)
        access_expiration = db.Column(db.DateTime)
        refresh_token = db.Column(db.String(64), index=True)
        refresh_expiration = db.Column(db.DateTime)
    
        # Relationships
        user = db.relationship('User', back_populates='tokens')
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
    
        def expire(self):
            self.access_expiration = datetime.utcnow()
            self.refresh_expiration = datetime.utcnow()
    
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
        def to_json_serializeable(self, backrefs=False, relationships=False):
            json_serializeable = {
                'id': self.hashid,
                'access_token': self.access_token,
                'access_expiration': (
                    None if self.access_expiration is None
                    else f'{self.access_expiration.isoformat()}Z'
                ),
                'refresh_token': self.refresh_token,
                'refresh_expiration': (
                    None if self.refresh_expiration is None
                    else f'{self.refresh_expiration.isoformat()}Z'
                )
            }
            if backrefs:
                json_serializeable['user'] = \
                    self.user.to_json_serializeable(backrefs=True)
            if relationships:
                pass
            return json_serializeable
    
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
        @staticmethod
        def clean():
            """Remove any tokens that have been expired for more than a day."""
            yesterday = datetime.utcnow() - timedelta(days=1)
            Token.query.filter(Token.refresh_expiration < yesterday).delete()
    
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
    
    
    class Avatar(HashidMixin, FileMixin, db.Model):
        __tablename__ = 'avatars'
        # Primary key
        id = db.Column(db.Integer, primary_key=True)
        # Foreign keys
    
        user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
    
        # Relationships
        user = db.relationship('User', back_populates='avatar')
    
    
        @property
        def path(self):
            return os.path.join(self.user.path, 'avatar')
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
    
    
        def delete(self):
            try:
                os.remove(self.path)
            except OSError as e:
                current_app.logger.error(e)
            db.session.delete(self)
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
    
    
    Inga Kirschnick's avatar
    Inga Kirschnick committed
        def to_json_serializeable(self, backrefs=False, relationships=False):
            json_serializeable = {
    
                'id': self.hashid,
    
                **self.file_mixin_to_json_serializeable()
    
    Inga Kirschnick's avatar
    Inga Kirschnick committed
            }
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
            if backrefs:
                json_serializeable['user'] = \
                    self.user.to_json_serializeable(backrefs=True)
            if relationships:
                pass
    
    Inga Kirschnick's avatar
    Inga Kirschnick committed
            return json_serializeable
    
    class CorpusFollowerRole(HashidMixin, db.Model):
        __tablename__ = 'corpus_follower_roles'
    
        # Primary key
        id = db.Column(db.Integer, primary_key=True)
        # Fields
    
        name = db.Column(db.String(64), unique=True)
        default = db.Column(db.Boolean, default=False, index=True)
        permissions = db.Column(db.Integer, default=0)
    
        # Relationships
    
        corpus_follower_associations = db.relationship(
            'CorpusFollowerAssociation',
    
            back_populates='role'
    
    
        def __repr__(self):
    
            return f'<CorpusFollowerRole {self.name}>'
    
        def has_permission(self, permission: Union[CorpusFollowerPermission, int, str]):
            perm = CorpusFollowerPermission.get(permission)
            return self.permissions & perm.value == perm.value
    
        def add_permission(self, permission: Union[CorpusFollowerPermission, int, str]):
            perm = CorpusFollowerPermission.get(permission)
            if not self.has_permission(perm):
                self.permissions += perm.value
    
        def remove_permission(self, permission: Union[CorpusFollowerPermission, int, str]):
            perm = CorpusFollowerPermission.get(permission)
            if self.has_permission(perm):
                self.permissions -= perm.value
    
        def reset_permissions(self):
            self.permissions = 0
    
        def to_json_serializeable(self, backrefs=False, relationships=False):
            json_serializeable = {
                'id': self.hashid,
    
                'default': self.default,
                'name': self.name,
    
                'permissions': [
    
                    x.name
                    for x in CorpusFollowerPermission
    
                    if self.has_permission(x)
    
                ]
            }
            if backrefs:
                pass
            if relationships:
                json_serializeable['corpus_follower_association'] = {
                    x.hashid: x.to_json_serializeable(relationships=True)
                    for x in self.corpus_follower_association
                }
            return json_serializeable
    
        @staticmethod
        def insert_defaults():
            roles = {
    
    Inga Kirschnick's avatar
    Inga Kirschnick committed
                'Anonymous': [],
    
                'Viewer': [
                    CorpusFollowerPermission.VIEW
                ],
                'Contributor': [
                    CorpusFollowerPermission.VIEW,
    
    Inga Kirschnick's avatar
    Inga Kirschnick committed
                    CorpusFollowerPermission.MANAGE_FILES
    
                'Administrator': [
                    CorpusFollowerPermission.VIEW,
    
    Inga Kirschnick's avatar
    Inga Kirschnick committed
                    CorpusFollowerPermission.MANAGE_FILES,
                    CorpusFollowerPermission.MANAGE_FOLLOWERS,
                    CorpusFollowerPermission.MANAGE_CORPUS
    
    
                ]
            }
            default_role_name = 'Viewer'
            for role_name, permissions in roles.items():
                role = CorpusFollowerRole.query.filter_by(name=role_name).first()
                if role is None:
                    role = CorpusFollowerRole(name=role_name)
                role.reset_permissions()
                for permission in permissions:
                    role.add_permission(permission)
                role.default = role.name == default_role_name
                db.session.add(role)
            db.session.commit()
    
    
    class CorpusFollowerAssociation(HashidMixin, db.Model):
        __tablename__ = 'corpus_follower_associations'
        # Primary key
        id = db.Column(db.Integer, primary_key=True)
        # Foreign keys
        corpus_id = db.Column(db.Integer, db.ForeignKey('corpora.id'))
        follower_id = db.Column(db.Integer, db.ForeignKey('users.id'))
        role_id = db.Column(db.Integer, db.ForeignKey('corpus_follower_roles.id'))
        # Relationships
        corpus = db.relationship(
            'Corpus',
            back_populates='corpus_follower_associations'
        )
        follower = db.relationship(
            'User',
            back_populates='corpus_follower_associations'
        )
        role = db.relationship(
            'CorpusFollowerRole',
            back_populates='corpus_follower_associations'
        )
    
        def __init__(self, **kwargs):
    
            if 'role' not in kwargs:
                kwargs['role'] = CorpusFollowerRole.query.filter_by(default=True).first()
    
            super().__init__(**kwargs)
    
        def __repr__(self):
    
            return f'<CorpusFollowerAssociation {self.follower.__repr__()} ~ {self.role.__repr__()} ~ {self.corpus.__repr__()}>'
    
    
        def to_json_serializeable(self, backrefs=False, relationships=False):
            json_serializeable = {
                'id': self.hashid,
    
                'corpus': self.corpus.to_json_serializeable(),
    
                'follower': self.follower.to_json_serializeable(),
                'role': self.role.to_json_serializeable()
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
            if backrefs:
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
            if relationships:
                pass
    
            return json_serializeable
    
    
    
    class User(HashidMixin, UserMixin, db.Model):
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
        # Primary key
    
        id = db.Column(db.Integer, primary_key=True)
    
        # Foreign keys
        role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))
    
    Stephan Porada's avatar
    Stephan Porada committed
        # Fields
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
        email = db.Column(db.String(254), index=True, unique=True)
        username = db.Column(db.String(64), index=True, unique=True)
    
        username_pattern = re.compile(r'^[A-Za-zÄÖÜäöüß0-9_.]*$')
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
        password_hash = db.Column(db.String(128))
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
        confirmed = db.Column(db.Boolean, default=False)
    
        terms_of_use_accepted = db.Column(db.Boolean, default=False)
    
        member_since = db.Column(db.DateTime(), default=datetime.utcnow)
    
        setting_job_status_mail_notification_level = db.Column(
    
            IntEnumColumn(UserSettingJobStatusMailNotificationLevel),
    
            default=UserSettingJobStatusMailNotificationLevel.END
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
        last_seen = db.Column(db.DateTime())
    
    Inga Kirschnick's avatar
    Inga Kirschnick committed
        full_name = db.Column(db.String(64))
        about_me = db.Column(db.String(256))
        location = db.Column(db.String(64))
        website = db.Column(db.String(128))
        organization = db.Column(db.String(128))
    
        is_public = db.Column(db.Boolean, default=False)
        profile_privacy_settings = db.Column(db.Integer(), default=0)
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
        # Relationships
    
        avatar = db.relationship(
            'Avatar',
    
            back_populates='user',
    
            cascade='all, delete-orphan',
            uselist=False
        )
    
        corpora = db.relationship(
            'Corpus',
            back_populates='user',
    
        corpus_follower_associations = db.relationship(
    
            'CorpusFollowerAssociation',
    
            back_populates='follower',
    
            cascade='all, delete-orphan'
    
        followed_corpora = association_proxy(
    
            'corpus_follower_associations',
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
            'corpus',
            creator=lambda c: CorpusFollowerAssociation(corpus=c)
    
        )
        jobs = db.relationship(
            'Job',
            back_populates='user',
    
            cascade='all, delete-orphan',
            lazy='dynamic'
        )
    
        role = db.relationship(
            'Role',
            back_populates='users'
    
        spacy_nlp_pipeline_models = db.relationship(
            'SpaCyNLPPipelineModel',
            back_populates='user',
            cascade='all, delete-orphan',
    
            lazy='dynamic'
        )
    
        tesseract_ocr_pipeline_models = db.relationship(
            'TesseractOCRPipelineModel',
            back_populates='user',
    
            cascade='all, delete-orphan',
            lazy='dynamic'
        )
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
        tokens = db.relationship(
            'Token',
    
            back_populates='user',
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
            cascade='all, delete-orphan',
            lazy='dynamic'
        )
    
        def __init__(self, **kwargs):
    
            if 'role' not in kwargs:
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
                kwargs['role'] = (
                    Role.query.filter_by(name='Administrator').first()
                    if kwargs['email'] == current_app.config['NOPAQUE_ADMIN']
                    else Role.query.filter_by(default=True).first()
                )
    
            super().__init__(**kwargs)
    
        def __repr__(self):
            return f'<User {self.username}>'
    
        def jsonpatch_path(self):
            return f'/users/{self.hashid}'
    
    
        @property
        def password(self):
            raise AttributeError('password is not a readable attribute')
    
        @password.setter
        def password(self, password):
            self.password_hash = generate_password_hash(password)
    
    
        @property
        def path(self):
            return os.path.join(
    
                current_app.config.get('NOPAQUE_DATA_DIR'), 'users', str(self.id))
    
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
        @staticmethod
        def create(**kwargs):
            user = User(**kwargs)
            db.session.add(user)
            db.session.flush(objects=[user])
            db.session.refresh(user)
            try:
                os.mkdir(user.path)
    
                os.mkdir(os.path.join(user.path, 'spacy_nlp_pipeline_models'))
    
                os.mkdir(os.path.join(user.path, 'tesseract_ocr_pipeline_models'))
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
                os.mkdir(os.path.join(user.path, 'corpora'))
                os.mkdir(os.path.join(user.path, 'jobs'))
            except OSError as e:
                current_app.logger.error(e)
                db.session.rollback()
                raise e
            return user
    
        @staticmethod
        def insert_defaults():
            nopaque_user = User.query.filter_by(username='nopaque').first()
            system_user_role = Role.query.filter_by(name='System user').first()
            if nopaque_user is None:
                nopaque_user = User.create(
                    username='nopaque',
                    role=system_user_role
                )
                db.session.add(nopaque_user)
            elif nopaque_user.role != system_user_role:
                nopaque_user.role = system_user_role
            db.session.commit()
    
        @staticmethod
        def reset_password(token, new_password):
            try:
                payload = jwt.decode(
                    token,
                    current_app.config['SECRET_KEY'],
                    algorithms=['HS256'],
                    issuer=current_app.config['SERVER_NAME'],
                    options={'require': ['exp', 'iat', 'iss', 'purpose', 'sub']}
                )
            except jwt.PyJWTError:
                return False
            if payload.get('purpose') != 'User.reset_password':
                return False
            user_hashid = payload.get('sub')
            user_id = hashids.decode(user_hashid)
            user = User.query.get(user_id)
            if user is None:
                return False
            user.password = new_password
            db.session.add(user)
            return True
    
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
        @staticmethod
        def verify_access_token(access_token, refresh_token=None):
            token = Token.query.filter(Token.access_token == access_token).first()
            if token is not None:
                if token.access_expiration > datetime.utcnow():
                    token.user.ping()
                    db.session.commit()
                    if token.user.role.name != 'System user':
                        return token.user
    
        @staticmethod
        def verify_refresh_token(refresh_token, access_token):
            token = Token.query.filter((Token.refresh_token == refresh_token) & (Token.access_token == access_token)).first()
            if token is not None:
                if token.refresh_expiration > datetime.utcnow():
                    return token
                # someone tried to refresh with an expired token
                # revoke all tokens from this user as a precaution
                token.user.revoke_auth_tokens()
                db.session.commit()
    
    
        def can(self, permission):
    
            return self.role is not None and self.role.has_permission(permission)
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
        def confirm(self, confirmation_token):
    
    Stephan Porada's avatar
    Stephan Porada committed
            try:
    
                payload = jwt.decode(
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
                    confirmation_token,
    
                    current_app.config['SECRET_KEY'],
                    algorithms=['HS256'],
                    issuer=current_app.config['SERVER_NAME'],
                    options={'require': ['exp', 'iat', 'iss', 'purpose', 'sub']}
                )
            except jwt.PyJWTError:
    
    Stephan Porada's avatar
    Stephan Porada committed
                return False
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
            if payload.get('purpose') != 'user.confirm':
    
                return False
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
            if payload.get('sub') != self.hashid:
    
    Stephan Porada's avatar
    Stephan Porada committed
                return False
            self.confirmed = True
            db.session.add(self)
            return True
    
    
        def delete(self):
    
            shutil.rmtree(self.path, ignore_errors=True)
    
            db.session.delete(self)
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
        def generate_auth_token(self):
            return Token(
                access_token=secrets.token_urlsafe(),
                access_expiration=datetime.utcnow() + timedelta(minutes=15),
                refresh_token=secrets.token_urlsafe(),
                refresh_expiration=datetime.utcnow() + timedelta(days=7),
                user=self
            )
    
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
        def generate_confirm_token(self, expiration=3600):
            now = datetime.utcnow()
    
            payload = {
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
                'exp': now + timedelta(seconds=expiration),
                'iat': now,
    
                'iss': current_app.config['SERVER_NAME'],
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
                'purpose': 'user.confirm',
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
                'sub': self.hashid
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
            return jwt.encode(
                payload,
                current_app.config['SECRET_KEY'],
                algorithm='HS256'
            )
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
        def generate_reset_password_token(self, expiration=3600):
            now = datetime.utcnow()
    
            payload = {
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
                'exp': now + timedelta(seconds=expiration),
                'iat': now,
    
                'iss': current_app.config['SERVER_NAME'],
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
                'purpose': 'User.reset_password',
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
                'sub': self.hashid
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
            return jwt.encode(
                payload,
                current_app.config['SECRET_KEY'],
                algorithm='HS256'
            )
    
        def is_administrator(self):
            return self.can(Permission.ADMINISTRATE)
    
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
        def ping(self):
            self.last_seen = datetime.utcnow()
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
        def revoke_auth_tokens(self):
            for token in self.tokens:
                db.session.delete(token)
    
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
        def verify_password(self, password):
            if self.role.name == 'System user':
                return False
            return check_password_hash(self.password_hash, password)
    
        #region Profile Privacy settings
        def has_profile_privacy_setting(self, setting):
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
            s = ProfilePrivacySettings.get(setting)
            return self.profile_privacy_settings & s.value == s.value
    
        
        def add_profile_privacy_setting(self, setting):
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
            s = ProfilePrivacySettings.get(setting)
            if not self.has_profile_privacy_setting(s):
                self.profile_privacy_settings += s.value
    
    
        def remove_profile_privacy_setting(self, setting):
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
            s = ProfilePrivacySettings.get(setting)
            if self.has_profile_privacy_setting(s):
                self.profile_privacy_settings -= s.value
    
    
        def reset_profile_privacy_settings(self):
            self.profile_privacy_settings = 0
        #endregion Profile Privacy settings
    
    
        def follow_corpus(self, corpus, role=None):
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
            if self.is_following_corpus(corpus):
                return
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
            r = CorpusFollowerRole.query.filter_by(default=True).first() if role is None else role
    
            cfa = CorpusFollowerAssociation(corpus=corpus, role=r, follower=self)
            db.session.add(cfa)
    
    
        def unfollow_corpus(self, corpus):
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
            if not self.is_following_corpus(corpus):
                return
            self.followed_corpora.remove(corpus)
    
    
        def is_following_corpus(self, corpus):
            return corpus in self.followed_corpora
    
        def generate_follow_corpus_token(self, corpus_hashid, role_name, expiration=7):
    
            now = datetime.utcnow()
            payload = {
                'exp': expiration,
                'iat': now,
                'iss': current_app.config['SERVER_NAME'],
    
                'purpose': 'User.follow_corpus',
                'role_name': role_name,
                'sub': corpus_hashid
    
            }
            return jwt.encode(
                payload,
                current_app.config['SECRET_KEY'],
                algorithm='HS256'
            )
        
    
        def follow_corpus_by_token(self, token):
    
            try:
                payload = jwt.decode(
                    token,
                    current_app.config['SECRET_KEY'],
                    algorithms=['HS256'],
                    issuer=current_app.config['SERVER_NAME'],
    
                    options={'require': ['exp', 'iat', 'iss', 'purpose', 'role_name', 'sub']}
    
                )
            except jwt.PyJWTError:
                return False
    
            if payload.get('purpose') != 'User.follow_corpus':
                return False
            corpus_hashid = payload.get('sub')
            corpus_id = hashids.decode(corpus_hashid)
    
            corpus = Corpus.query.get_or_404(corpus_id)
            if corpus is None:
                return False
    
            role_name = payload.get('role_name')
            role = CorpusFollowerRole.query.filter_by(name=role_name).first()
            if role is None:
                return False
            self.follow_corpus(corpus, role)
            db.session.add(self)
            return True
    
        def to_json_serializeable(self, backrefs=False, relationships=False, filter_by_privacy_settings=False):
    
            json_serializeable = {
    
                'id': self.hashid,
                'confirmed': self.confirmed,
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
                'avatar': url_for('users.user_avatar', user_id=self.id),
    
                'email': self.email,
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
                'last_seen': (
                    None if self.last_seen is None
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
                    else f'{self.last_seen.isoformat()}Z'
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
                ),
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
                'member_since': f'{self.member_since.isoformat()}Z',
    
                'username': self.username,
    
                'full_name': self.full_name,
                'about_me': self.about_me,
                'website': self.website,
                'location': self.location,
                'organization': self.organization,
    
                'job_status_mail_notification_level': \
    
                        self.setting_job_status_mail_notification_level.name,
    
                'profile_privacy_settings': {
                    'is_public': self.is_public,
                    'show_email': self.has_profile_privacy_setting(ProfilePrivacySettings.SHOW_EMAIL),
                    'show_last_seen': self.has_profile_privacy_setting(ProfilePrivacySettings.SHOW_LAST_SEEN),
                    'show_member_since': self.has_profile_privacy_setting(ProfilePrivacySettings.SHOW_MEMBER_SINCE)
                }
    
            }
            if backrefs:
    
                json_serializeable['role'] = \
                    self.role.to_json_serializeable(backrefs=True)
    
            if relationships:
    
                json_serializeable['corpus_follower_associations'] = {
    
                    x.hashid: x.to_json_serializeable()
    
                    for x in self.corpus_follower_associations
    
                json_serializeable['corpora'] = {
                    x.hashid: x.to_json_serializeable(relationships=True)
    
                    for x in self.corpora
                }
    
                json_serializeable['jobs'] = {
                    x.hashid: x.to_json_serializeable(relationships=True)
    
                    for x in self.jobs
                }
    
                json_serializeable['tesseract_ocr_pipeline_models'] = {
                    x.hashid: x.to_json_serializeable(relationships=True)
    
                    for x in self.tesseract_ocr_pipeline_models
    
                json_serializeable['spacy_nlp_pipeline_models'] = {
                    x.hashid: x.to_json_serializeable(relationships=True)
    
                    for x in self.spacy_nlp_pipeline_models
                }
    
    Inga Kirschnick's avatar
    Inga Kirschnick committed
    
            if filter_by_privacy_settings:
                if not self.has_profile_privacy_setting(ProfilePrivacySettings.SHOW_EMAIL):
                    json_serializeable.pop('email')
                if not self.has_profile_privacy_setting(ProfilePrivacySettings.SHOW_LAST_SEEN):
                    json_serializeable.pop('last_seen')
                if not self.has_profile_privacy_setting(ProfilePrivacySettings.SHOW_MEMBER_SINCE):
                    json_serializeable.pop('member_since')
    
            return json_serializeable
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
    
    
    class TesseractOCRPipelineModel(FileMixin, HashidMixin, db.Model):
        __tablename__ = 'tesseract_ocr_pipeline_models'
    
        # Primary key
        id = db.Column(db.Integer, primary_key=True)
        # Foreign keys
        user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
        # Fields
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
        title = db.Column(db.String(64))
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
        version = db.Column(db.String(16))
        compatible_service_versions = db.Column(ContainerColumn(list, 255))
    
        publisher_url = db.Column(db.String(512))
        publishing_url = db.Column(db.String(512))
    
        is_public = db.Column(db.Boolean, default=False)
    
        # Relationships
        user = db.relationship('User', back_populates='tesseract_ocr_pipeline_models')
    
                'tesseract_ocr_pipeline_models',
    
        @property
        def jsonpatch_path(self):
            return f'{self.user.jsonpatch_path}/tesseract_ocr_pipeline_models/{self.hashid}'
    
    
        @property
        def url(self):
            return url_for(
                'contributions.tesseract_ocr_pipeline_model',
                tesseract_ocr_pipeline_model_id=self.id
            )
    
    
        @property
        def user_hashid(self):
            return self.user.hashid
    
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
            nopaque_user = User.query.filter_by(username='nopaque').first()
    
            defaults_file = os.path.join(
                os.path.dirname(os.path.abspath(__file__)),
    
                'TesseractOCRPipelineModel.defaults.yml'
    
            )
            with open(defaults_file, 'r') as f:
                defaults = yaml.safe_load(f)
            for m in defaults:
    
                model = TesseractOCRPipelineModel.query.filter_by(title=m['title'], version=m['version']).first()  # noqa
    
                if model is not None:
                    model.compatible_service_versions = m['compatible_service_versions']
                    model.description = m['description']
                    model.publisher = m['publisher']
    
                    model.publisher_url = m['publisher_url']
                    model.publishing_url = m['publishing_url']
    
                    model.publishing_year = m['publishing_year']
    
                    model.title = m['title']
                    model.version = m['version']
    
                model = TesseractOCRPipelineModel(
    
                    compatible_service_versions=m['compatible_service_versions'],
    
                    description=m['description'],
                    publisher=m['publisher'],
    
                    publisher_url=m['publisher_url'],
                    publishing_url=m['publishing_url'],
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
                    user=nopaque_user,
    
                db.session.add(model)
                db.session.flush(objects=[model])
                db.session.refresh(model)
                model.filename = f'{model.id}.traineddata'
    
                    desc=f'{model.title} ({model.filename})',
    
                    unit="B",
                    unit_scale=True,
                    unit_divisor=1024,
                    total=int(r.headers['Content-Length'])
                )
                pbar.clear()
    
                with open(model.path, 'wb') as f:
                    for chunk in r.iter_content(chunk_size=1024):
                        if chunk:  # filter out keep-alive new chunks
                            pbar.update(len(chunk))
                            f.write(chunk)