Skip to content
Snippets Groups Projects
models.py 15.8 KiB
Newer Older
  • Learn to ignore specific revisions
  • from datetime import datetime
    
    from flask import current_app
    
    from flask_login import UserMixin, AnonymousUserMixin
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
    from itsdangerous import BadSignature, TimedJSONWebSignatureSerializer
    
    from werkzeug.security import generate_password_hash, check_password_hash
    from . import db
    from . import login_manager
    
    import os
    import shutil
    
    import xml.etree.ElementTree as ET
    
        """
        Defines User permissions as integers by the power of 2. User permission
        can be evaluated using the bitwise operator &. 3 equals to CREATE_JOB and
        DELETE_JOB and so on.
        """
    
        CREATE_JOB = 1
        DELETE_JOB = 2
        # WRITE = 4
        # MODERATE = 8
        ADMIN = 16
    
    
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
        Model for the different roles Users can have. Is a one-to-many
        relationship. A Role can be associated with many User rows.
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
        # Primary key
    
        id = db.Column(db.Integer, primary_key=True)
    
        default = db.Column(db.Boolean, default=False, index=True)
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
        name = db.Column(db.String(64), unique=True)
    
        permissions = db.Column(db.Integer)
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
        # Relationships
    
        users = db.relationship('User', backref='role', lazy='dynamic')
    
        def __init__(self, **kwargs):
            super(Role, self).__init__(**kwargs)
            if self.permissions is None:
                self.permissions = 0
    
            """
            String representation of the Role. For human readability.
            """
    
            """
            Add new permission to Role. Input is a Permission.
            """
    
            if not self.has_permission(perm):
                self.permissions += perm
    
        def remove_permission(self, perm):
    
            """
            Removes permission from a Role. Input a Permission.
            """
    
            if self.has_permission(perm):
                self.permissions -= perm
    
        def reset_permissions(self):
    
            """
            Resets permissions to zero. Zero equals no permissions at all.
            """
    
            self.permissions = 0
    
        def has_permission(self, perm):
    
            Checks if a Role has a specific Permission. Does this with the bitwise
    
            operator.
            """
    
            return self.permissions & perm == perm
    
        @staticmethod
        def insert_roles():
    
            """
            Inserts roles into the databes. This has to be executed befor Users are
            added to the database. Otherwiese Users will not have a Role assigned
            to them. Order of the roles dictionary determines the ID of each role.
            User hast the ID 1 and Administrator has the ID 2.
            """
    
            roles = {
                        'User': [Permission.CREATE_JOB],
                        'Administrator': [Permission.ADMIN,
                                          Permission.CREATE_JOB,
                                          Permission.DELETE_JOB]
            }
            default_role = 'User'
            for r in roles:
                role = Role.query.filter_by(name=r).first()
                if role is None:
                    role = Role(name=r)
                role.reset_permissions()
                for perm in roles[r]:
                    role.add_permission(perm)
                role.default = (role.name == default_role)
                db.session.add(role)
            db.session.commit()
    
    
        """
        Model for Users that are registered to Opaque.
        """
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
        # Primary key
    
        id = db.Column(db.Integer, primary_key=True)
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
        confirmed = db.Column(db.Boolean, default=False)
    
        email = db.Column(db.String(254), unique=True, index=True)
    
        password_hash = db.Column(db.String(128))
    
        registration_date = db.Column(db.DateTime(), default=datetime.utcnow)
    
        role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
        username = db.Column(db.String(64), unique=True, index=True)
        # Relationships
    
        corpora = db.relationship('Corpus', backref='creator', lazy='dynamic',
                                  cascade='save-update, merge, delete')
        jobs = db.relationship('Job', backref='creator', lazy='dynamic',
                               cascade='save-update, merge, delete')
    
    Stephan Porada's avatar
    Stephan Porada committed
        is_dark = db.Column(db.Boolean, default=False)
    
            """
            String representation of the User. For human readability.
            """
    
        def __init__(self, **kwargs):
            super(User, self).__init__(**kwargs)
            if self.role is None:
                if self.email == current_app.config['OPAQUE_ADMIN']:
                    self.role = Role.query.filter_by(name='Administrator').first()
                if self.role is None:
                    self.role = Role.query.filter_by(default=True).first()
    
    
    Stephan Porada's avatar
    Stephan Porada committed
        def generate_confirmation_token(self, expiration=3600):
    
            """
            Generates a confirmation token for user confirmation via email.
            """
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
            s = TimedJSONWebSignatureSerializer(current_app.config['SECRET_KEY'],
                                                expiration)
    
    Stephan Porada's avatar
    Stephan Porada committed
            return s.dumps({'confirm': self.id}).decode('utf-8')
    
        def generate_reset_token(self, expiration=3600):
    
            """
            Generates a reset token for password reset via email.
            """
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
            s = TimedJSONWebSignatureSerializer(current_app.config['SECRET_KEY'],
                                                expiration)
    
            return s.dumps({'reset': self.id}).decode('utf-8')
    
    
    Stephan Porada's avatar
    Stephan Porada committed
        def confirm(self, token):
    
            """
            Confirms User if the given token is valid and not expired.
            """
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
            s = TimedJSONWebSignatureSerializer(current_app.config['SECRET_KEY'])
    
    Stephan Porada's avatar
    Stephan Porada committed
            try:
                data = s.loads(token.encode('utf-8'))
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
            except BadSignature:
    
    Stephan Porada's avatar
    Stephan Porada committed
                return False
            if data.get('confirm') != self.id:
                return False
            self.confirmed = True
            db.session.add(self)
            return True
    
    
        @staticmethod
        def reset_password(token, new_password):
    
            """
            Resets password for User if the given token is valid and not expired.
            """
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
            s = TimedJSONWebSignatureSerializer(current_app.config['SECRET_KEY'])
    
            try:
                data = s.loads(token.encode('utf-8'))
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
            except BadSignature:
    
                return False
            user = User.query.get(data.get('reset'))
            if user is None:
                return False
            user.password = new_password
            db.session.add(user)
            return True
    
    
        @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)
    
        def verify_password(self, password):
            return check_password_hash(self.password_hash, password)
    
    
            """
            Checks if a User with its current role can doe something. Checks if the
            associated role actually has the needed Permission.
            """
    
            return self.role is not None and self.role.has_permission(perm)
    
        def is_administrator(self):
    
            """
            Checks if User has Admin permissions.
            """
    
        def delete_user(self):
    
            """
            Delete user from database. Also delete all associated jobs and corpora
            files.
            """
    
            delete_path = os.path.join('/mnt/opaque/', str(self.id))
            while os.path.exists(delete_path):
                try:
                    shutil.rmtree(delete_path, ignore_errors=True)
                except OSError:
                    pass
            db.session.delete(self)
    
    class AnonymousUser(AnonymousUserMixin):
    
        """
        Model replaces the default AnonymousUser.
        """
    
        def can(self, permissions):
            return False
    
        def is_administrator(self):
            return False
    
    
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
    class JobInput(db.Model):
        """
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
        Class to define JobInputs.
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
        """
        __tablename__ = 'job_inputs'
        # Primary key
        id = db.Column(db.Integer, primary_key=True)
        filename = db.Column(db.String(255))
    
        dir = db.Column(db.String(255))
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
        job_id = db.Column(db.Integer, db.ForeignKey('jobs.id'))
    
    Stephan Porada's avatar
    Stephan Porada committed
        # Relationships
        results = db.relationship('JobResult',
                                  backref='job_input',
                                  lazy='dynamic',
                                  cascade='save-update, merge, delete')
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
        def __repr__(self):
            """
            String representation of the JobInput. For human readability.
            """
            return '<JobInput %r>' % self.filename
    
        def to_dict(self):
            return {'id': self.id,
    
                    'dir': self.dir,
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
                    'filename': self.filename,
    
                    'job_id': self.job_id,
                    'results': [result.to_dict() for result in self.results]}
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
    
    class JobResult(db.Model):
        """
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
        Class to define JobResults.
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
        """
        __tablename__ = 'job_results'
        # Primary key
        id = db.Column(db.Integer, primary_key=True)
        filename = db.Column(db.String(255))
    
        dir = db.Column(db.String(255))
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
        job_id = db.Column(db.Integer, db.ForeignKey('jobs.id'))
    
    Stephan Porada's avatar
    Stephan Porada committed
        job_input_id = db.Column(db.Integer, db.ForeignKey('job_inputs.id'))
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
        def __repr__(self):
            """
            String representation of the JobResult. For human readability.
            """
            return '<JobResult %r>' % self.filename
    
        def to_dict(self):
            return {'id': self.id,
    
                    'dir': self.dir,
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
                    'filename': self.filename,
    
                    'job_id': self.job_id,
                    'job_input_id': self.job_input_id}
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
    class Job(db.Model):
    
    Stephan Porada's avatar
    Stephan Porada committed
        """
        Class to define Jobs.
        """
        __tablename__ = 'jobs'
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
        # Primary key
    
    Stephan Porada's avatar
    Stephan Porada committed
        id = db.Column(db.Integer, primary_key=True)
    
        creation_date = db.Column(db.DateTime(), default=datetime.utcnow)
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
        description = db.Column(db.String(255))
    
        end_date = db.Column(db.DateTime())
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
        mem_mb = db.Column(db.Integer)
        n_cores = db.Column(db.Integer)
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
        service = db.Column(db.String(64))
        '''
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
        ' Service specific arguments as string list.
        ' Example: ["-l eng", "--keep-intermediates", "--skip-binarization"]
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
        '''
        service_args = db.Column(db.String(255))
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
        service_version = db.Column(db.String(16))
        status = db.Column(db.String(16))
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
        title = db.Column(db.String(32))
        user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
        # Relationships
        inputs = db.relationship('JobInput',
                                 backref='job',
                                 lazy='dynamic',
                                 cascade='save-update, merge, delete')
        results = db.relationship('JobResult',
                                  backref='job',
                                  lazy='dynamic',
                                  cascade='save-update, merge, delete')
    
    Stephan Porada's avatar
    Stephan Porada committed
    
        def __init__(self, **kwargs):
            super(Job, self).__init__(**kwargs)
    
        def __repr__(self):
            """
            String representation of the Job. For human readability.
            """
            return '<Job %r>' % self.title
    
    
        def to_dict(self):
    
            return {'id': self.id,
                    'creation_date': self.creation_date.timestamp(),
                    'description': self.description,
                    'end_date': (self.end_date.timestamp() if self.end_date else
                                 None),
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
                    'inputs': [input.to_dict() for input in self.inputs],
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
                    'results': [result.to_dict() for result in self.results],
    
                    'service': self.service,
                    'service_args': self.service_args,
                    'service_version': self.service_version,
                    'status': self.status,
                    'title': self.title,
                    'user_id': self.user_id}
    
    
        def flag_for_stop(self):
            """
            Flag running or failed job (anything that is not completed) with
            stopping. Opaque daemon will end services flaged with 'stopping'.
            """
            self.status = 'stopping'
            db.session.commit()
    
    
        def delete_job(self):
    
            """
            Delete job with given job id from database. Also delete associated job
    
            files. Contianers are still running for a few seconds after
            the associated service has been removed. This is the reason for the
            while loop. The loop checks if the file path to all the job files still
            exists and removes it again and again till the container did shutdown
            for good.
    
            See: https://docs.docker.com/engine/swarm/swarm-tutorial/delete-service/
    
            delete_path = os.path.join('/mnt/opaque/', str(self.user_id), 'jobs',
                                       str(self.id))
    
            while os.path.exists(delete_path):
                try:
                    shutil.rmtree(delete_path, ignore_errors=True)
                except OSError:
                    pass
    
            db.session.delete(self)
            db.session.commit()
    
    Stephan Porada's avatar
    Stephan Porada committed
    
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
    class CorpusFile(db.Model):
        """
        Class to define Files.
        """
        __tablename__ = 'corpus_files'
        # Primary key
        id = db.Column(db.Integer, primary_key=True)
    
        author = db.Column(db.String(64))
    
        dir = db.Column(db.String(255))
    
        filename = db.Column(db.String(255))
        publishing_year = db.Column(db.Integer)
        title = db.Column(db.String(64))
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
        corpus_id = db.Column(db.Integer, db.ForeignKey('corpora.id'))
    
    
        def delete(self):
            path = os.path.join(current_app.config['OPAQUE_STORAGE_DIRECTORY'],
                                self.dir,
                                self.filename)
            try:
                os.remove(path)
            except:
                return
    
            self.corpus.status = 'unprepared'
    
            db.session.delete(self)
            db.session.commit()
    
    
        def insert_metadata(self):
            file = os.path.join(current_app.config['OPAQUE_STORAGE_DIRECTORY'],
                                self.dir,
                                self.filename)
            element_tree = ET.parse(file)
            text_node = element_tree.find('text')
            text_node.set('author', self.author)
            text_node.set('publishing_year', str(self.publishing_year))
            text_node.set('title', self.title)
            element_tree.write(file)
    
            self.corpus.status = 'unprepared'
            db.session.commit()
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
    class Corpus(db.Model):
        """
        Class to define a corpus.
        """
        __tablename__ = 'corpora'
        # Primary key
        id = db.Column(db.Integer, primary_key=True)
    
        creation_date = db.Column(db.DateTime(), default=datetime.utcnow)
    
        description = db.Column(db.String(255))
    
        status = db.Column(db.String(16))
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
        title = db.Column(db.String(32))
        user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
    
        analysis_container_ip = db.Column(db.String(16))
        analysis_container_name = db.Column(db.String(32))
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
        # Relationships
        files = db.relationship('CorpusFile',
    
    Stephan Porada's avatar
    Stephan Porada committed
                                backref='corpus',
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
                                lazy='dynamic',
                                cascade='save-update, merge, delete')
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
    
        def __repr__(self):
            """
            String representation of the corpus. For human readability.
            """
            return '<Corpus %r>' % self.title
    
    
        def to_dict(self):
    
                    'creation_date': self.creation_date.timestamp(),
    
                    'status': self.status,
    
        def delete(self):
            for corpus_file in self.files:
                corpus_file.delete()
            path = os.path.join(current_app.config['OPAQUE_STORAGE_DIRECTORY'],
    
                                str(self.user_id),
    
                                'corpora',
    
                                str(self.id))
    
            try:
                shutil.rmtree(path)
            except:
                return
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
        def prepare(self):
            pass
    
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
    
    
    Patrick Jentsch's avatar
    Patrick Jentsch committed
    '''
    ' Flask-Login is told to use the application’s custom anonymous user by setting
    ' its class in the login_manager.anonymous_user attribute.
    '''
    login_manager.anonymous_user = AnonymousUser
    
    
    @login_manager.user_loader
    def load_user(user_id):
        return User.query.get(int(user_id))