diff --git a/app/models.py b/app/models.py index 246bb441..ba6424f7 100644 --- a/app/models.py +++ b/app/models.py @@ -8,11 +8,11 @@ from . import db, login_manager 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. - """ + ''' MANAGE_CORPORA = 1 MANAGE_JOBS = 2 # PERMISSION_NAME = 4 @@ -21,10 +21,10 @@ class Permission: 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) @@ -47,46 +47,46 @@ class Role(db.Model): self.permissions = 0 def __repr__(self): - """ + ''' String representation of the Role. For human readability. - """ + ''' return '' % 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 database. 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. Users have the ID 1 and Administrators have the ID 2. - """ + ''' roles = {'User': [Permission.MANAGE_CORPORA, Permission.MANAGE_JOBS], 'Administrator': [Permission.MANAGE_CORPORA, Permission.MANAGE_JOBS, Permission.ADMIN]} @@ -104,9 +104,9 @@ class Role(db.Model): 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) @@ -148,9 +148,9 @@ class User(UserMixin, db.Model): 'jobs': {job.id: job.to_dict() for job in self.jobs}} def __repr__(self): - """ + ''' String representation of the User. For human readability. - """ + ''' return '' % self.username def __init__(self, **kwargs): @@ -162,25 +162,25 @@ class User(UserMixin, db.Model): 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')) @@ -194,9 +194,9 @@ class User(UserMixin, db.Model): @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')) @@ -221,16 +221,16 @@ class User(UserMixin, db.Model): 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 ping(self): @@ -238,9 +238,9 @@ class User(UserMixin, db.Model): db.session.add(self) 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: @@ -250,9 +250,9 @@ class User(UserMixin, db.Model): class AnonymousUser(AnonymousUserMixin): - """ + ''' Model replaces the default AnonymousUser. - """ + ''' def can(self, permissions): return False @@ -262,9 +262,9 @@ class AnonymousUser(AnonymousUserMixin): class JobInput(db.Model): - """ + ''' Class to define JobInputs. - """ + ''' __tablename__ = 'job_inputs' # Primary key id = db.Column(db.Integer, primary_key=True) @@ -275,9 +275,9 @@ class JobInput(db.Model): filename = db.Column(db.String(255)) def __repr__(self): - """ + ''' String representation of the JobInput. For human readability. - """ + ''' return '' % self.filename def to_dict(self): @@ -287,9 +287,9 @@ class JobInput(db.Model): class JobResult(db.Model): - """ + ''' Class to define JobResults. - """ + ''' __tablename__ = 'job_results' # Primary key id = db.Column(db.Integer, primary_key=True) @@ -300,9 +300,9 @@ class JobResult(db.Model): filename = db.Column(db.String(255)) def __repr__(self): - """ + ''' String representation of the JobResult. For human readability. - """ + ''' return '' % self.filename def to_dict(self): @@ -312,9 +312,9 @@ class JobResult(db.Model): class Job(db.Model): - """ + ''' Class to define Jobs. - """ + ''' __tablename__ = 'jobs' # Primary key id = db.Column(db.Integer, primary_key=True) @@ -341,31 +341,34 @@ class Job(db.Model): cascade='save-update, merge, delete') results = db.relationship('JobResult', backref='job', lazy='dynamic', cascade='save-update, merge, delete') - notifications_data = db.relationship('NotificationData', - backref='job', - lazy='dynamic', - cascade='save-update, merge, delete') + notification_data = db.relationship('NotificationData', + cascade='save-update, merge, delete', + uselist=False, + back_populates='job') # One-to-One relationship + notification_email_data = db.relationship('NotificationEmailData', + cascade='save-update, merge, delete', + back_populates='job') def __repr__(self): - """ + ''' String representation of the Job. For human readability. - """ + ''' return '' % 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 results from the database. - """ + ''' for input in self.inputs: db.session.delete(input) for result in self.results: - db.session.delete(result) + db.session.delete(result) # TODO: shouldn't this happen through the cascade option? db.session.delete(self) db.session.commit() @@ -389,27 +392,53 @@ class Job(db.Model): class NotificationData(db.Model): - """ + ''' Class to define notification data used for sending a notification mail with nopaque_notify. - """ - __tablename__ = 'notifications_data' + ''' + __tablename__ = 'notification_data' # Primary key id = db.Column(db.Integer, primary_key=True) # Foreign Key job_id = db.Column(db.Integer, db.ForeignKey('jobs.id')) + # relationships + job = db.relationship('Job', back_populates='notification_data') # Fields - notified_on_submitted = db.Column(db.Boolean, default=False) - notified_on_queued = db.Column(db.Boolean, default=False) - notified_on_running = db.Column(db.Boolean, default=False) - notified_on_complete = db.Column(db.Boolean, default=False) - notified_on_canceling = db.Column(db.Boolean, default=False) + notified_on = db.Column(db.String(16), default=None) + + def __repr__(self): + ''' + String representation of the NotificationData. For human readability. + ''' + return '' % self.id # TODO: Why not .format()? + + def to_dict(self): + return {'id': self.id, + 'job_id': self.job_id, + 'job': self.job, + 'notified': self.notified} + + +class NotificationEmailData(db.Model): + ''' + Class to define data that will be used to send a corresponding Notification + via email. + ''' + __tablename__ = 'notification_email_data' + # Primary Key + id = db.Column(db.Integer, primary_key=True) + # Foreign Key + job_id = db.Column(db.Integer, db.ForeignKey('jobs.id')) + # relationships + job = db.relationship('Job', back_populates='notification_email_data') + notify_status = db.Column(db.String(16), default=None) + creation_date = db.Column(db.DateTime(), default=datetime.utcnow) class CorpusFile(db.Model): - """ + ''' Class to define Files. - """ + ''' __tablename__ = 'corpus_files' # Primary key id = db.Column(db.Integer, primary_key=True) @@ -455,9 +484,9 @@ class CorpusFile(db.Model): class Corpus(db.Model): - """ + ''' Class to define a corpus. - """ + ''' __tablename__ = 'corpora' # Primary key id = db.Column(db.Integer, primary_key=True) @@ -492,9 +521,9 @@ class Corpus(db.Model): db.session.commit() def __repr__(self): - """ + ''' String representation of the corpus. For human readability. - """ + ''' return '' % self.title diff --git a/migrations/versions/10a92d8f4616_.py b/migrations/versions/10a92d8f4616_.py new file mode 100644 index 00000000..c750487f --- /dev/null +++ b/migrations/versions/10a92d8f4616_.py @@ -0,0 +1,30 @@ +"""empty message + +Revision ID: 10a92d8f4616 +Revises: 4638e6509e13 +Create Date: 2020-05-12 11:24:46.022674 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '10a92d8f4616' +down_revision = '4638e6509e13' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('notifications_data', sa.Column('notified_on', sa.String(length=16), nullable=True)) + op.drop_column('notifications_data', 'notified') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('notifications_data', sa.Column('notified', sa.BOOLEAN(), autoincrement=False, nullable=True)) + op.drop_column('notifications_data', 'notified_on') + # ### end Alembic commands ### diff --git a/migrations/versions/3d9a20b8b26c_.py b/migrations/versions/3d9a20b8b26c_.py new file mode 100644 index 00000000..a384f219 --- /dev/null +++ b/migrations/versions/3d9a20b8b26c_.py @@ -0,0 +1,28 @@ +"""empty message + +Revision ID: 3d9a20b8b26c +Revises: 421ba4373e50 +Create Date: 2020-05-14 09:30:56.266381 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '3d9a20b8b26c' +down_revision = '421ba4373e50' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('notification_email_data', sa.Column('notify_status', sa.String(length=16), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('notification_email_data', 'notify_status') + # ### end Alembic commands ### diff --git a/migrations/versions/421ba4373e50_.py b/migrations/versions/421ba4373e50_.py new file mode 100644 index 00000000..af0c8f10 --- /dev/null +++ b/migrations/versions/421ba4373e50_.py @@ -0,0 +1,33 @@ +"""empty message + +Revision ID: 421ba4373e50 +Revises: 5ba6786a847e +Create Date: 2020-05-14 08:35:47.367125 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '421ba4373e50' +down_revision = '5ba6786a847e' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('notification_email_data', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('job_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['job_id'], ['jobs.id'], ), + sa.PrimaryKeyConstraint('id') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('notification_email_data') + # ### end Alembic commands ### diff --git a/migrations/versions/4638e6509e13_.py b/migrations/versions/4638e6509e13_.py new file mode 100644 index 00000000..66a965ca --- /dev/null +++ b/migrations/versions/4638e6509e13_.py @@ -0,0 +1,38 @@ +"""empty message + +Revision ID: 4638e6509e13 +Revises: 471aa04c1a92 +Create Date: 2020-05-12 06:42:04.475585 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '4638e6509e13' +down_revision = '471aa04c1a92' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('notifications_data', sa.Column('notified', sa.Boolean(), nullable=True)) + op.drop_column('notifications_data', 'notified_on_queued') + op.drop_column('notifications_data', 'notified_on_running') + op.drop_column('notifications_data', 'notified_on_submitted') + op.drop_column('notifications_data', 'notified_on_complete') + op.drop_column('notifications_data', 'notified_on_canceling') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('notifications_data', sa.Column('notified_on_canceling', sa.BOOLEAN(), autoincrement=False, nullable=True)) + op.add_column('notifications_data', sa.Column('notified_on_complete', sa.BOOLEAN(), autoincrement=False, nullable=True)) + op.add_column('notifications_data', sa.Column('notified_on_submitted', sa.BOOLEAN(), autoincrement=False, nullable=True)) + op.add_column('notifications_data', sa.Column('notified_on_running', sa.BOOLEAN(), autoincrement=False, nullable=True)) + op.add_column('notifications_data', sa.Column('notified_on_queued', sa.BOOLEAN(), autoincrement=False, nullable=True)) + op.drop_column('notifications_data', 'notified') + # ### end Alembic commands ### diff --git a/migrations/versions/4886241e0f5d_.py b/migrations/versions/4886241e0f5d_.py new file mode 100644 index 00000000..3db8b0ac --- /dev/null +++ b/migrations/versions/4886241e0f5d_.py @@ -0,0 +1,28 @@ +"""empty message + +Revision ID: 4886241e0f5d +Revises: 3d9a20b8b26c +Create Date: 2020-05-14 11:58:08.498454 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '4886241e0f5d' +down_revision = '3d9a20b8b26c' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('notification_email_data', sa.Column('creation_date', sa.DateTime(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('notification_email_data', 'creation_date') + # ### end Alembic commands ### diff --git a/migrations/versions/5ba6786a847e_.py b/migrations/versions/5ba6786a847e_.py new file mode 100644 index 00000000..62080605 --- /dev/null +++ b/migrations/versions/5ba6786a847e_.py @@ -0,0 +1,42 @@ +"""empty message + +Revision ID: 5ba6786a847e +Revises: 10a92d8f4616 +Create Date: 2020-05-12 13:15:56.265610 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '5ba6786a847e' +down_revision = '10a92d8f4616' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('notification_data', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('job_id', sa.Integer(), nullable=True), + sa.Column('notified_on', sa.String(length=16), nullable=True), + sa.ForeignKeyConstraint(['job_id'], ['jobs.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.drop_table('notifications_data') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('notifications_data', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('job_id', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('notified_on', sa.VARCHAR(length=16), autoincrement=False, nullable=True), + sa.ForeignKeyConstraint(['job_id'], ['jobs.id'], name='notifications_data_job_id_fkey'), + sa.PrimaryKeyConstraint('id', name='notifications_data_pkey') + ) + op.drop_table('notification_data') + # ### end Alembic commands ### diff --git a/nopaque.py b/nopaque.py index b1e5a2ec..9ae49f8c 100644 --- a/nopaque.py +++ b/nopaque.py @@ -1,7 +1,7 @@ import eventlet eventlet.monkey_patch() # noqa from app import create_app, db, socketio -from app.models import Corpus, Job, Role, User +from app.models import Corpus, Job, Role, User, NotificationData from flask_migrate import Migrate, upgrade import os @@ -15,7 +15,8 @@ def make_shell_context(): return {'db': db, 'Corpus': Corpus, 'Job': Job, - 'User': User} + 'User': User, + 'NotificationData': NotificationData} @app.cli.command()