From 86d14f748f012eb9874e47917e317366b75136e1 Mon Sep 17 00:00:00 2001
From: Patrick Jentsch <p.jentsch@uni-bielefeld.de>
Date: Wed, 9 Feb 2022 16:02:37 +0100
Subject: [PATCH] More generic implementation of fake enum db types

---
 app/admin/routes.py      |   4 +-
 app/events/sqlalchemy.py |  64 +++++++++------------
 app/models.py            | 121 ++++++++++++++++++---------------------
 app/settings/forms.py    |   4 +-
 app/settings/routes.py   |   4 +-
 5 files changed, 90 insertions(+), 107 deletions(-)

diff --git a/app/admin/routes.py b/app/admin/routes.py
index 79f05e19..f1178398 100644
--- a/app/admin/routes.py
+++ b/app/admin/routes.py
@@ -1,6 +1,6 @@
 from app import db, hashids
 from app.decorators import admin_required
-from app.models import JobStatusMailNotificationLevel, Role, User
+from app.models import Role, User, UserSettingJobStatusMailNotificationLevel
 from app.settings import tasks as settings_tasks
 from app.settings.forms import (
     EditGeneralSettingsForm,
@@ -102,7 +102,7 @@ def edit_user(user_id):
         and edit_notification_settings_form.validate()
     ):
         user.setting_job_status_mail_notification_level = \
-            JobStatusMailNotificationLevel[
+            UserSettingJobStatusMailNotificationLevel[
                 edit_notification_settings_form.job_status_mail_notification_level.data  # noqa
             ]
         db.session.commit()
diff --git a/app/events/sqlalchemy.py b/app/events/sqlalchemy.py
index dd88a0fe..31ffcb20 100644
--- a/app/events/sqlalchemy.py
+++ b/app/events/sqlalchemy.py
@@ -7,9 +7,10 @@ from app.models import (
     JobInput,
     JobResult,
     JobStatus,
-    JobStatusMailNotificationLevel
+    UserSettingJobStatusMailNotificationLevel
 )
 from datetime import datetime
+from enum import Enum
 
 
 ###############################################################################
@@ -33,8 +34,8 @@ def ressource_after_delete(mapper, connection, ressource):
 @db.event.listens_for(JobResult, 'after_insert')
 def ressource_after_insert_handler(mapper, connection, ressource):
     value = ressource.to_dict(backrefs=False, relationships=False)
-    for relationship in mapper.relationships:
-        value[relationship.key] = {}
+    for attr in mapper.relationships:
+        value[attr.key] = {}
     jsonpatch = [
         {'op': 'add', 'path': ressource.jsonpatch_path, 'value': value}
     ]
@@ -50,49 +51,40 @@ def ressource_after_insert_handler(mapper, connection, ressource):
 def ressource_after_update_handler(mapper, connection, ressource):
     jsonpatch = []
     for attr in db.inspect(ressource).attrs:
-        # Don't handle changes in relationship fields
         if attr.key in mapper.relationships:
             continue
-        # Check if their are changes for the current field
-        history = attr.load_history()
-        if not history.has_changes():
+        if not attr.load_history().has_changes():
             continue
-        if isinstance(history.added[0], datetime):
-            # In order to be JSON serializable, DateTime attributes must be
-            # converted to a string
-            attr_name = attr.key
-            value = history.added[0].isoformat() + 'Z'
-        elif attr.key.endswith('_enum_value'):
-            # Handling fake enum attributes
-            attr_name = attr.key[:-11]
-            value = getattr(ressource, attr_name).name
+        if isinstance(attr.value, datetime):
+            value = attr.value.isoformat() + 'Z'
+        elif isinstance(attr.value, Enum):
+            value = attr.value.name
         else:
-            attr_name = attr.key
-            value = history.added[0]
+            value = attr.value
         jsonpatch.append(
             {
                 'op': 'replace',
-                'path': f'{ressource.jsonpatch_path}/{attr_name}',
+                'path': f'{ressource.jsonpatch_path}/{attr.key}',
                 'value': value
             }
         )
-        # Job status update notification if it changed and wanted by the user
-        if isinstance(ressource, Job) and attr_name == 'status':
-            if ressource.user.setting_job_status_mail_notification_level == JobStatusMailNotificationLevel.NONE:  # noqa
-                pass
-            elif (
-                ressource.user.setting_job_status_mail_notification_level == JobStatusMailNotificationLevel.END  # noqa
-                and ressource.status not in [JobStatus.COMPLETED, JobStatus.FAILED]  # noqa
-            ):
-                pass
-            else:
-                msg = create_message(
-                    ressource.user.email,
-                    f'Status update for your Job "{ressource.title}"',
-                    'tasks/email/notification',
-                    job=ressource
-                )
-                mail.send(msg)
+        if isinstance(ressource, Job) and attr.key == 'status':
+            _job_status_email_handler(ressource)
     if jsonpatch:
         room = f'users.{ressource.user_hashid}'
         socketio.emit('users.patch', jsonpatch, room=room)
+
+
+def _job_status_email_handler(job):
+    if job.user.setting_job_status_mail_notification_level == UserSettingJobStatusMailNotificationLevel.NONE:  # noqa
+        return
+    if job.user.setting_job_status_mail_notification_level == UserSettingJobStatusMailNotificationLevel.END:  # noqa
+        if job.status not in [JobStatus.COMPLETED, JobStatus.FAILED]:
+            return
+    msg = create_message(
+        job.user.email,
+        f'Status update for your Job "{job.title}"',
+        'tasks/email/notification',
+        job=job
+    )
+    mail.send(msg)
diff --git a/app/models.py b/app/models.py
index 034edc91..55423d96 100644
--- a/app/models.py
+++ b/app/models.py
@@ -17,33 +17,23 @@ import xml.etree.ElementTree as ET
 import yaml
 
 
-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
-
-
-class JobStatus(IntEnum):
-    INITIALIZING = 1
-    SUBMITTED = 2
-    QUEUED = 3
-    RUNNING = 4
-    CANCELING = 5
-    CANCELED = 6
-    COMPLETED = 7
-    FAILED = 8
-
+class IntEnumProxy(db.TypeDecorator):
+    impl = db.Integer
+
+    def __init__(self, enumtype, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self._enumtype = enumtype
+
+    def process_bind_param(self, value, dialect):
+        if isinstance(value, self._enumtype):
+            return value.value
+        elif isinstance(value, int):
+            return value
+        else:
+            return TypeError()
 
-class JobStatusMailNotificationLevel(IntEnum):
-    NONE = 1
-    END = 2
-    ALL = 3
+    def process_result_value(self, value, dialect):
+        return self._enumtype(value)
 
 
 class Permission(IntEnum):
@@ -143,6 +133,12 @@ class Role(HashidMixin, db.Model):
         db.session.commit()
 
 
+class UserSettingJobStatusMailNotificationLevel(IntEnum):
+    NONE = 1
+    END = 2
+    ALL = 3
+
+
 class User(HashidMixin, UserMixin, db.Model):
     __tablename__ = 'users'
     # Primary key
@@ -159,10 +155,9 @@ class User(HashidMixin, UserMixin, db.Model):
     token_expiration = db.Column(db.DateTime)
     username = db.Column(db.String(64), unique=True, index=True)
     setting_dark_mode = db.Column(db.Boolean, default=False)
-    setting_job_status_mail_notification_level_enum_value = db.Column(
-        'setting_job_status_mail_notification_level',
-        db.Integer,
-        default=2
+    setting_job_status_mail_notification_level = db.Column(
+        IntEnumProxy(UserSettingJobStatusMailNotificationLevel),
+        default=UserSettingJobStatusMailNotificationLevel.END
     )
     # Backrefs: role: Role
     # Relationships
@@ -214,19 +209,6 @@ class User(HashidMixin, UserMixin, db.Model):
         return os.path.join(
             current_app.config.get('NOPAQUE_DATA_DIR'), 'users', str(self.id))
 
-    @property
-    def setting_job_status_mail_notification_level(self):
-        return JobStatusMailNotificationLevel(
-            self.setting_job_status_mail_notification_level_enum_value
-        )
-
-    @setting_job_status_mail_notification_level.setter
-    def setting_job_status_mail_notification_level(self, enum_member):
-        if not isinstance(enum_member, JobStatusMailNotificationLevel):
-            return TypeError()
-        self.setting_job_status_mail_notification_level_enum_value = \
-            enum_member.value
-
     def can(self, permission):
         return self.role.has_permission(permission)
 
@@ -553,6 +535,17 @@ class JobResult(FileMixin, HashidMixin, db.Model):
         return self.job.user_id
 
 
+class JobStatus(IntEnum):
+    INITIALIZING = 1
+    SUBMITTED = 2
+    QUEUED = 3
+    RUNNING = 4
+    CANCELING = 5
+    CANCELED = 6
+    COMPLETED = 7
+    FAILED = 8
+
+
 class Job(HashidMixin, db.Model):
     '''
     Class to define Jobs.
@@ -573,7 +566,10 @@ class Job(HashidMixin, db.Model):
     '''
     service_args = db.Column(db.String(255))
     service_version = db.Column(db.String(16))
-    status_enum_value = db.Column('status', db.Integer, default=1)
+    status = db.Column(
+        IntEnumProxy(JobStatus),
+        default=JobStatus.INITIALIZING
+    )
     title = db.Column(db.String(32))
     # Backrefs: user: User
     # Relationships
@@ -601,16 +597,6 @@ class Job(HashidMixin, db.Model):
     def path(self):
         return os.path.join(self.user.path, 'jobs', str(self.id))
 
-    @property
-    def status(self):
-        return JobStatus(self.status_enum_value)
-
-    @status.setter
-    def status(self, enum_member):
-        if not isinstance(enum_member, JobStatus):
-            return TypeError()
-        self.status_enum_value = enum_member.value
-
     @property
     def url(self):
         return url_for('jobs.job', job_id=self.id)
@@ -780,6 +766,18 @@ class CorpusFile(FileMixin, HashidMixin, db.Model):
         return dict_corpus_file
 
 
+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
+
+
 class Corpus(HashidMixin, db.Model):
     '''
     Class to define a corpus.
@@ -793,7 +791,10 @@ class Corpus(HashidMixin, db.Model):
     creation_date = db.Column(db.DateTime(), default=datetime.utcnow)
     description = db.Column(db.String(255))
     last_edited_date = db.Column(db.DateTime(), default=datetime.utcnow)
-    status_enum_value = db.Column('status', db.Integer, default=1)
+    status = db.Column(
+        IntEnumProxy(CorpusStatus),
+        default=CorpusStatus.UNPREPARED
+    )
     title = db.Column(db.String(32))
     num_analysis_sessions = db.Column(db.Integer, default=0)
     num_tokens = db.Column(db.Integer, default=0)
@@ -824,16 +825,6 @@ class Corpus(HashidMixin, db.Model):
     def path(self):
         return os.path.join(self.user.path, 'corpora', str(self.id))
 
-    @property
-    def status(self):
-        return CorpusStatus(self.status_enum_value)
-
-    @status.setter
-    def status(self, enum_member):
-        if not isinstance(enum_member, CorpusStatus):
-            return TypeError()
-        self.status_enum_value = enum_member.value
-
     @property
     def url(self):
         return url_for('corpora.corpus', corpus_id=self.id)
diff --git a/app/settings/forms.py b/app/settings/forms.py
index 03d7f243..f17c5f89 100644
--- a/app/settings/forms.py
+++ b/app/settings/forms.py
@@ -1,5 +1,5 @@
 from app.auth import USERNAME_REGEX
-from app.models import JobStatusMailNotificationLevel, User
+from app.models import User, UserSettingJobStatusMailNotificationLevel
 from flask_wtf import FlaskForm
 from wtforms import (
     BooleanField,
@@ -96,5 +96,5 @@ class EditNotificationSettingsForm(FlaskForm):
         super().__init__(*args, **kwargs)
         self.job_status_mail_notification_level.choices += [
             (enum_member.name, enum_member.name.capitalize())
-            for enum_member in JobStatusMailNotificationLevel
+            for enum_member in UserSettingJobStatusMailNotificationLevel
         ]
diff --git a/app/settings/routes.py b/app/settings/routes.py
index c59a55f2..8d828e33 100644
--- a/app/settings/routes.py
+++ b/app/settings/routes.py
@@ -8,7 +8,7 @@ from .forms import (
     EditNotificationSettingsForm
 )
 from .. import db
-from ..models import JobStatusMailNotificationLevel
+from ..models import UserSettingJobStatusMailNotificationLevel
 
 
 @bp.route('', methods=['GET', 'POST'])
@@ -57,7 +57,7 @@ def index():
         and edit_notification_settings_form.validate()
     ):
         current_user.setting_job_status_mail_notification_level = \
-            JobStatusMailNotificationLevel[
+            UserSettingJobStatusMailNotificationLevel[
                 edit_notification_settings_form.job_status_mail_notification_level.data  # noqa
             ]
         db.session.commit()
-- 
GitLab