From 52c25fd563f2f84b033b97a8694aa2f8a353f4fa Mon Sep 17 00:00:00 2001
From: Patrick Jentsch <p.jentsch@uni-bielefeld.de>
Date: Wed, 15 Sep 2021 12:31:53 +0200
Subject: [PATCH] Simplify Config setup and move some functions to dedicated
 files

---
 .env.tpl                 | 22 +++-----------------
 .flaskenv                |  1 +
 app/__init__.py          | 18 +++++++++-------
 app/cli.py               | 25 ++++++++++++++++++++++
 app/models.py            | 25 +++-------------------
 config.py                | 44 +++++++--------------------------------
 nopaque.py               | 45 +++++-----------------------------------
 tests/__init__.py        |  6 ++++++
 tests/test_basics.py     |  7 ++++---
 tests/test_client.py     | 17 +++++++++------
 tests/test_user_model.py |  7 ++++---
 11 files changed, 81 insertions(+), 136 deletions(-)
 create mode 100644 .flaskenv
 create mode 100644 app/cli.py

diff --git a/.env.tpl b/.env.tpl
index ad27fb60..d05e16ae 100644
--- a/.env.tpl
+++ b/.env.tpl
@@ -91,9 +91,8 @@ MAIL_USERNAME=
 # Flask-SQLAlchemy                                                             #
 # https://flask-sqlalchemy.palletsprojects.com/en/2.x/config/                  #
 ################################################################################
-# DEFAULT with development config: postgresql://nopaque:nopaque@db/nopaque_dev
-# DEFAULT with production config: postgresql://nopaque:nopaque@db/nopaque
-# DEFAULT with testing config: postgresql://nopaque:nopaque@db/nopaque_test
+# DEFAULT: 'sqlite:///<nopaque-root-dir>/app.db'
+# NOTE: Use `.` as <nopaque-root-dir>
 # SQLALCHEMY_DATABASE_URI=
 
 
@@ -105,10 +104,6 @@ MAIL_USERNAME=
 # EXAMPLE: admin.nopaque@example.com
 NOPAQUE_ADMIN=
 
-# DEFAULT: development
-# CHOOSE ONE: development, production, testing
-# NOPAQUE_CONFIG=
-
 # This email adress is used for the contact button in the nopaque footer. If
 # not set, no contact button is displayed.
 # DEFAULT: None
@@ -124,24 +119,13 @@ NOPAQUE_ADMIN=
 # DEFAULT: True
 # NOPAQUE_DAEMON_ENABLED=
 
-# The hostname or IP address for the server to listen on.
-# DEFAULT: 0.0.0.0
-# NOTES: To use a domain locally, add any names that should route to the app to your hosts file.
-#        If nopaque is running in a Docker container, you propably want to use the default value.
-# NOPAQUE_HOST=
-
-# The port number for the server to listen on.
-# DEFAULT: 5000
-# NOTE: If nopaque is running in a Docker container, you propably want to use the default value.
-# NOPAQUE_PORT=
-
 # transport://[userid:password]@hostname[:port]/[virtual_host]
 NOPAQUE_SOCKETIO_MESSAGE_QUEUE_URI=
 
 # DEFAULT: %Y-%m-%d %H:%M:%S
 # NOPAQUE_LOG_DATE_FORMAT=
 
-# DEFAULT: <nopaque-root-dir>/nopaque.log ~ /home/nopaque/nopaque.log
+# DEFAULT: <nopaque-root-dir>/nopaque.log
 # NOTE: Use `.` as <nopaque-root-dir>
 # NOPAQUE_LOG_FILE=
 
diff --git a/.flaskenv b/.flaskenv
new file mode 100644
index 00000000..1fd672d3
--- /dev/null
+++ b/.flaskenv
@@ -0,0 +1 @@
+FLASK_APP=nopaque.py
diff --git a/app/__init__.py b/app/__init__.py
index 4649a090..c03a25ea 100644
--- a/app/__init__.py
+++ b/app/__init__.py
@@ -1,7 +1,8 @@
-from config import config
+from config import Config
 from flask import Flask
 from flask_login import LoginManager
 from flask_mail import Mail
+from flask_migrate import Migrate
 from flask_paranoid import Paranoid
 from flask_socketio import SocketIO
 from flask_sqlalchemy import SQLAlchemy
@@ -10,23 +11,26 @@ import flask_assets
 
 assets = flask_assets.Environment()
 db = SQLAlchemy()
-login_manager = LoginManager()
-login_manager.login_view = 'auth.login'
+login = LoginManager()
+login.login_view = 'auth.login'
+login.login_message = 'Please log in to access this page.'
 mail = Mail()
+migrate = Migrate()
 paranoid = Paranoid()
 paranoid.redirect_view = '/'
 socketio = SocketIO()
 
 
-def create_app(config_name):
+def create_app(config_class=Config):
     app = Flask(__name__)
-    app.config.from_object(config[config_name])
+    app.config.from_object(config_class)
 
     assets.init_app(app)
-    config[config_name].init_app(app)
+    config_class.init_app(app)
     db.init_app(app)
-    login_manager.init_app(app)
+    login.init_app(app)
     mail.init_app(app)
+    migrate.init_app(app, db)
     paranoid.init_app(app)
     socketio.init_app(
         app, message_queue=app.config['NOPAQUE_SOCKETIO_MESSAGE_QUEUE_URI'])
diff --git a/app/cli.py b/app/cli.py
new file mode 100644
index 00000000..7c0782b2
--- /dev/null
+++ b/app/cli.py
@@ -0,0 +1,25 @@
+from .models import Role
+from flask_migrate import upgrade
+
+
+def register(app):
+    @app.cli.command()
+    def deploy():
+        """Run deployment tasks."""
+        # migrate database to latest revision
+        upgrade()
+        # create or update user roles
+        Role.insert_roles()
+
+    @app.cli.command()
+    def tasks():
+        from app.tasks import TaskRunner
+        task_runner = TaskRunner()
+        task_runner.run()
+
+    @app.cli.command()
+    def test():
+        """Run the unit tests."""
+        import unittest
+        tests = unittest.TestLoader().discover('tests')
+        unittest.TextTestRunner(verbosity=2).run(tests)
diff --git a/app/models.py b/app/models.py
index abc6b9f2..5f79cb59 100644
--- a/app/models.py
+++ b/app/models.py
@@ -1,11 +1,11 @@
 from datetime import datetime, timedelta
 from flask import current_app, url_for
-from flask_login import UserMixin, AnonymousUserMixin
+from flask_login import UserMixin
 from itsdangerous import BadSignature, TimedJSONWebSignatureSerializer
 from time import sleep
 from werkzeug.security import generate_password_hash, check_password_hash
 import xml.etree.ElementTree as ET
-from . import db, login_manager
+from . import db, login
 import base64
 import logging
 import os
@@ -285,18 +285,6 @@ class User(UserMixin, db.Model):
         return user
 
 
-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.
@@ -722,13 +710,6 @@ class QueryResult(db.Model):
         return '<QueryResult {}>'.format(self.title)
 
 
-'''
-' 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
+@login.user_loader
 def load_user(user_id):
     return User.query.get(int(user_id))
diff --git a/config.py b/config.py
index 913229cb..2c99b412 100644
--- a/config.py
+++ b/config.py
@@ -1,9 +1,11 @@
+from dotenv import load_dotenv
 from werkzeug.middleware.proxy_fix import ProxyFix
 import logging
 import os
 
 
-ROOT_DIR = os.path.abspath(os.path.dirname(__file__))
+basedir = os.path.abspath(os.path.dirname(__file__))
+load_dotenv(os.path.join(basedir, '.env'))
 
 
 class Config:
@@ -33,6 +35,10 @@ class Config:
     MAIL_USE_TLS = os.environ.get('MAIL_USE_TLS', 'false').lower() == 'true'
 
     ''' # Flask-SQLAlchemy # '''
+    SQLALCHEMY_DATABASE_URI = os.environ.get(
+        'SQLALCHEMY_DATABASE_URI',
+        'sqlite:///' + os.path.join(basedir, 'app.db')
+    )
     SQLALCHEMY_RECORD_QUERIES = True
     SQLALCHEMY_TRACK_MODIFICATIONS = False
 
@@ -52,7 +58,7 @@ class Config:
             'datefmt': os.environ.get('NOPAQUE_LOG_DATE_FORMAT',
                                       '%Y-%m-%d %H:%M:%S'),
             'filename': os.environ.get('NOPAQUE_LOG_FILE',
-                                       os.path.join(ROOT_DIR, 'nopaque.log')),
+                                       os.path.join(basedir, 'nopaque.log')),
             'format': os.environ.get(
                 'NOPAQUE_LOG_FORMAT',
                 '[%(asctime)s] %(levelname)s in '
@@ -72,37 +78,3 @@ class Config:
             'x_proto': int(os.environ.get('NOPAQUE_PROXY_FIX_X_PROTO', '0'))
         }
         app.wsgi_app = ProxyFix(app.wsgi_app, **proxy_fix_kwargs)
-
-
-class DevelopmentConfig(Config):
-    ''' # Flask # '''
-    DEBUG = True
-
-    ''' # Flask-SQLAlchemy # '''
-    SQLALCHEMY_DATABASE_URI = os.environ.get(
-        'SQLALCHEMY_DATABASE_URI',
-        'postgresql://nopaque:nopaque@db/nopaque_dev'
-    )
-
-
-class ProductionConfig(Config):
-    ''' # Flask-SQLAlchemy # '''
-    SQLALCHEMY_DATABASE_URI = os.environ.get(
-        'SQLALCHEMY_DATABASE_URI', 'postgresql://nopaque:nopaque@db/nopaque')
-
-
-class TestingConfig(Config):
-    ''' # Flask # '''
-    TESTING = True
-    WTF_CSRF_ENABLED = False
-
-    ''' # Flask-SQLAlchemy # '''
-    SQLALCHEMY_DATABASE_URI = os.environ.get(
-        'SQLALCHEMY_DATABASE_URI',
-        'postgresql://nopaque:nopaque@db/nopaque_test'
-    )
-
-
-config = {'development': DevelopmentConfig,
-          'production': ProductionConfig,
-          'testing': TestingConfig}
diff --git a/nopaque.py b/nopaque.py
index 53c75e29..1e32e381 100644
--- a/nopaque.py
+++ b/nopaque.py
@@ -6,23 +6,13 @@ import eventlet
 eventlet.monkey_patch()
 
 
-from dotenv import load_dotenv  # noqa
-import os  # noqa
-
-# Load environment variables
-DOTENV_FILE = os.path.join(os.path.dirname(__file__), '.env')
-if os.path.exists(DOTENV_FILE):
-    load_dotenv(DOTENV_FILE)
-
-
-from app import create_app, db, socketio  # noqa
+from app import db, cli, create_app, socketio  # noqa
 from app.models import (Corpus, CorpusFile, Job, JobInput, JobResult,
                         QueryResult, Role, User)  # noqa
-from flask_migrate import Migrate, upgrade  # noqa
 
 
-app = create_app(os.environ.get('NOPAQUE_CONFIG', 'development'))
-migrate = Migrate(app, db, compare_type=True)
+app = create_app()
+cli.register(app)
 
 
 @app.shell_context_processor
@@ -38,32 +28,7 @@ def make_shell_context():
             'User': User}
 
 
-@app.cli.command()
-def deploy():
-    """Run deployment tasks."""
-    # migrate database to latest revision
-    upgrade()
-
-    # create or update user roles
-    Role.insert_roles()
-
-
-@app.cli.command()
-def tasks():
-    from app.tasks import TaskRunner
-    task_runner = TaskRunner()
-    task_runner.run()
-
-
-@app.cli.command()
-def test():
-    """Run the unit tests."""
-    import unittest
-    tests = unittest.TestLoader().discover('tests')
-    unittest.TextTestRunner(verbosity=2).run(tests)
-
-
 if __name__ == '__main__':
-    host = os.environ.get('NOPAQUE_HOST', '0.0.0.0')
-    port = int(os.environ.get('NOPAQUE_PORT', '5000'))
+    host = '0.0.0.0'
+    port = 5000
     socketio.run(app, host=host, port=port)
diff --git a/tests/__init__.py b/tests/__init__.py
index e69de29b..b36bebf9 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -0,0 +1,6 @@
+from config import Config
+
+
+class TestConfig(Config):
+    TESTING = True
+    SQLALCHEMY_DATABASE_URI = 'sqlite://'
diff --git a/tests/test_basics.py b/tests/test_basics.py
index e52943de..df9d0ff5 100644
--- a/tests/test_basics.py
+++ b/tests/test_basics.py
@@ -1,11 +1,12 @@
-import unittest
-from flask import current_app
 from app import create_app, db
+from flask import current_app
+from . import TestConfig
+import unittest
 
 
 class BasicsTestCase(unittest.TestCase):
     def setUp(self):
-        self.app = create_app('testing')
+        self.app = create_app(TestConfig)
         self.app_context = self.app.app_context()
         self.app_context.push()
         db.create_all()
diff --git a/tests/test_client.py b/tests/test_client.py
index e4c0738c..89aaa74a 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -1,10 +1,11 @@
-import unittest
 from app import create_app, db
+from . import TestConfig
+import unittest
 
 
 class FlaskClientTestCase(unittest.TestCase):
     def setUp(self):
-        self.app = create_app('testing')
+        self.app = create_app(TestConfig)
         self.app_context = self.app.app_context()
         self.app_context.push()
         db.create_all()
@@ -48,7 +49,8 @@ class FlaskClientTestCase(unittest.TestCase):
             'password2': 'cat'
         })
         self.assertEqual(response.status_code, 200)
-        self.assertTrue('Usernames must have only letters, numbers, dots or underscores' in response.get_data(as_text=True))
+        self.assertTrue(
+            'Usernames must have only letters, numbers, dots or underscores' in response.get_data(as_text=True))
 
     def test_register_false_email(self):
         # register a new account with wrong username
@@ -59,7 +61,8 @@ class FlaskClientTestCase(unittest.TestCase):
             'password2': 'cat'
         })
         self.assertEqual(response.status_code, 200)
-        self.assertTrue('Invalid email address.' in response.get_data(as_text=True))
+        self.assertTrue(
+            'Invalid email address.' in response.get_data(as_text=True))
 
     def test_duplicates(self):
         # tries to register an account that has already been registered
@@ -78,7 +81,8 @@ class FlaskClientTestCase(unittest.TestCase):
             'password2': 'cat'
         })
         self.assertEqual(response.status_code, 200)
-        self.assertTrue('Username already in use.' in response.get_data(as_text=True))
+        self.assertTrue(
+            'Username already in use.' in response.get_data(as_text=True))
         response = self.client.post('/auth/register', data={
             'email': 'john@example.com',
             'username': 'johnsmith',
@@ -86,7 +90,8 @@ class FlaskClientTestCase(unittest.TestCase):
             'password2': 'cat'
         })
         self.assertEqual(response.status_code, 200)
-        self.assertTrue('Email already registered.' in response.get_data(as_text=True))
+        self.assertTrue(
+            'Email already registered.' in response.get_data(as_text=True))
 
         def test_admin_forbidden(self):
             response = self.client.post('/auth/login', data={
diff --git a/tests/test_user_model.py b/tests/test_user_model.py
index adc650bd..30066087 100644
--- a/tests/test_user_model.py
+++ b/tests/test_user_model.py
@@ -1,12 +1,13 @@
-import unittest
-import time
 from app import create_app, db
 from app.models import User, AnonymousUser, Role, Permission
+from . import TestConfig
+import time
+import unittest
 
 
 class UserModelTestCase(unittest.TestCase):
     def setUp(self):
-        self.app = create_app('testing')
+        self.app = create_app(TestConfig)
         self.app_context = self.app.app_context()
         self.app_context.push()
         db.create_all()
-- 
GitLab