nopaque/app/models.py
2020-04-17 11:04:09 +02:00

506 lines
17 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from datetime import datetime
from flask import current_app
from flask_login import UserMixin, AnonymousUserMixin
from itsdangerous import BadSignature, TimedJSONWebSignatureSerializer
from time import sleep
from werkzeug.security import generate_password_hash, check_password_hash
from werkzeug.utils import secure_filename
from . import db, logger, login_manager
import os
import shutil
import xml.etree.ElementTree as ET
class Permission:
"""
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
class Role(db.Model):
"""
Model for the different roles Users can have. Is a one-to-many
relationship. A Role can be associated with many User rows.
"""
__tablename__ = 'roles'
# Primary key
id = db.Column(db.Integer, primary_key=True)
default = db.Column(db.Boolean, default=False, index=True)
name = db.Column(db.String(64), unique=True)
permissions = db.Column(db.Integer)
# 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
def __repr__(self):
"""
String representation of the Role. For human readability.
"""
return '<Role %r>' % self.name
def add_permission(self, perm):
"""
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()
class User(UserMixin, db.Model):
"""
Model for Users that are registered to Opaque.
"""
__tablename__ = 'users'
# Primary key
id = db.Column(db.Integer, primary_key=True)
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'))
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')
is_dark = db.Column(db.Boolean, default=False)
def __repr__(self):
"""
String representation of the User. For human readability.
"""
return '<User %r>' % self.username
def __init__(self, **kwargs):
super(User, self).__init__(**kwargs)
if self.role is None:
if self.email == current_app.config['NOPAQUE_ADMIN']:
self.role = Role.query.filter_by(name='Administrator').first()
if self.role is None:
self.role = Role.query.filter_by(default=True).first()
def generate_confirmation_token(self, expiration=3600):
"""
Generates a confirmation token for user confirmation via email.
"""
s = TimedJSONWebSignatureSerializer(current_app.config['SECRET_KEY'],
expiration)
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.
"""
s = TimedJSONWebSignatureSerializer(current_app.config['SECRET_KEY'],
expiration)
return s.dumps({'reset': self.id}).decode('utf-8')
def confirm(self, token):
"""
Confirms User if the given token is valid and not expired.
"""
s = TimedJSONWebSignatureSerializer(current_app.config['SECRET_KEY'])
try:
data = s.loads(token.encode('utf-8'))
except BadSignature:
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.
"""
s = TimedJSONWebSignatureSerializer(current_app.config['SECRET_KEY'])
try:
data = s.loads(token.encode('utf-8'))
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)
def can(self, perm):
"""
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.
"""
return self.can(Permission.ADMIN)
def delete(self):
"""
Delete the user and its corpora and jobs from database and filesystem.
"""
for job in self.jobs:
job.delete()
for corpus in self.corpora:
corpus.delete()
path = os.path.join(current_app.config['NOPAQUE_STORAGE'],
str(self.id))
try:
shutil.rmtree(path)
except Exception as e:
logger.warning(e)
pass
db.session.delete(self)
db.session.commit()
class AnonymousUser(AnonymousUserMixin):
"""
Model replaces the default AnonymousUser.
"""
def can(self, permissions):
return False
def is_administrator(self):
return False
class JobInput(db.Model):
"""
Class to define JobInputs.
"""
__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))
job_id = db.Column(db.Integer, db.ForeignKey('jobs.id'))
def __repr__(self):
"""
String representation of the JobInput. For human readability.
"""
return '<JobInput %r>' % self.filename
def to_dict(self):
return {'id': self.id,
'filename': self.filename,
'job_id': self.job_id}
class JobResult(db.Model):
"""
Class to define JobResults.
"""
__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))
job_id = db.Column(db.Integer, db.ForeignKey('jobs.id'))
def __repr__(self):
"""
String representation of the JobResult. For human readability.
"""
return '<JobResult %r>' % self.filename
def to_dict(self):
return {'id': self.id,
'filename': self.filename,
'job_id': self.job_id}
class Job(db.Model):
"""
Class to define Jobs.
"""
__tablename__ = 'jobs'
# 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))
end_date = db.Column(db.DateTime())
mem_mb = db.Column(db.Integer)
n_cores = db.Column(db.Integer)
secure_filename = db.Column(db.String(32))
service = db.Column(db.String(64))
'''
' Service specific arguments as string list.
' Example: ["-l eng", "--keep-intermediates", "--skip-binarization"]
'''
service_args = db.Column(db.String(255))
service_version = db.Column(db.String(16))
status = db.Column(db.String(16))
title = db.Column(db.String(32))
user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
# 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')
def __repr__(self):
"""
String representation of the Job. For human readability.
"""
return '<Job %r>' % self.title
def create_secure_filename(self):
"""
Takes the job.title string nad cratesa a secure filename from this.
"""
self.secure_filename = secure_filename(self.title)
def delete(self):
"""
Delete the job and its inputs and outputs from database and filesystem.
"""
if self.status != 'complete' and self.status != 'failed':
self.status = 'canceling'
db.session.commit()
while self.status != 'canceled':
# In case the daemon handled a job in any way
if self.status != 'canceling':
self.status = 'canceling'
db.session.commit()
sleep(1)
db.session.refresh(self)
path = os.path.join(current_app.config['NOPAQUE_STORAGE'],
str(self.user_id), 'jobs', str(self.id))
try:
shutil.rmtree(path)
except Exception as e:
''' TODO: Proper exception handling '''
logger.warning(e)
pass
db.session.delete(self)
db.session.commit()
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),
'inputs': [input.to_dict() for input in self.inputs],
'mem_mb': self.mem_mb,
'n_cores': self.n_cores,
'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}
class CorpusFile(db.Model):
"""
Class to define Files.
"""
__tablename__ = 'corpus_files'
# Primary key
id = db.Column(db.Integer, primary_key=True)
address = db.Column(db.String(255))
author = db.Column(db.String(255))
booktitle = db.Column(db.String(255))
chapter = db.Column(db.String(255))
dir = db.Column(db.String(255))
editor = db.Column(db.String(255))
filename = db.Column(db.String(255))
institution = db.Column(db.String(255))
journal = db.Column(db.String(255))
pages = db.Column(db.String(255))
publisher = db.Column(db.String(255))
publishing_year = db.Column(db.Integer)
school = db.Column(db.String(255))
title = db.Column(db.String(255))
corpus_id = db.Column(db.Integer, db.ForeignKey('corpora.id'))
def delete(self):
path = os.path.join(current_app.config['NOPAQUE_STORAGE'],
self.dir, self.filename)
try:
os.remove(path)
except Exception as e:
''' TODO: Proper exception handling '''
logger.warning(e)
pass
self.corpus.status = 'unprepared'
db.session.delete(self)
db.session.commit()
def insert_metadata(self):
file = os.path.join(current_app.config['NOPAQUE_STORAGE'],
self.dir, self.filename)
element_tree = ET.parse(file)
text_node = element_tree.find('text')
text_node.set('address', self.address if self.address else "NULL")
text_node.set('author', self.author)
text_node.set('booktitle', self.booktitle if self.booktitle else "NULL")
text_node.set('chapter', self.chapter if self.chapter else "NULL")
text_node.set('editor', self.editor if self.editor else "NULL")
text_node.set('institution', self.institution if self.institution else "NULL")
text_node.set('journal', self.journal if self.journal else "NULL")
text_node.set('pages', self.pages if self.pages else "NULL")
text_node.set('publisher', self.publisher if self.publisher else "NULL")
text_node.set('publishing_year', str(self.publishing_year))
text_node.set('school', self.school if self.school else "NULL")
text_node.set('title', self.title)
element_tree.write(file)
self.corpus.status = 'unprepared'
db.session.commit()
def to_dict(self):
return {'id': self.id,
'address': self.address,
'author': self.author,
'booktitle': self.booktitle,
'chapter': self.chapter,
'editor': self.editor,
'filename': self.filename,
'institution': self.institution,
'journal': self.journal,
'pages': self.pages,
'publisher': self.publisher,
'publishing_year': self.publishing_year,
'school': self.school,
'title': self.title,
'corpus_id': self.corpus_id}
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))
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))
# Relationships
files = db.relationship('CorpusFile', backref='corpus', lazy='dynamic',
cascade='save-update, merge, delete')
def __repr__(self):
"""
String representation of the corpus. For human readability.
"""
return '<Corpus %r>' % self.title
def to_dict(self):
return {'id': self.id,
'creation_date': self.creation_date.timestamp(),
'description': self.description,
'files': [file.to_dict() for file in self.files],
'status': self.status,
'title': self.title,
'user_id': self.user_id}
def delete(self):
for corpus_file in self.files:
corpus_file.delete()
path = os.path.join(current_app.config['NOPAQUE_STORAGE'],
str(self.user_id), 'corpora', str(self.id))
try:
shutil.rmtree(path)
except Exception as e:
''' TODO: Proper exception handling '''
logger.warning(e)
pass
db.session.delete(self)
db.session.commit()
def prepare(self):
pass
'''
' Flask-Login is told to use the applications 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))