# -*- coding: utf-8 -*-
"""Open ResearchNotes is an electronic lab book (ELN)."""
__version__ = "1.4.2"
import os
from typing import Optional
import logging
from logging.handlers import RotatingFileHandler, SysLogHandler
# from sqlalchemy import MetaData
from markupsafe import escape
from flask import Flask
# from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
# from flask_flatpages import FlatPages
from .rn_class import RNFlask
from .database import db
# Import and initialize database migration using Flask_Migrate
migrate = Migrate()
# Rendering FlatPages using Markdown (Mainly help, changelog and setup)
# pages = FlatPages()
[docs]def create_app(
config_file: Optional[str] = None,
testing: bool = False,
testing_search: bool = False,
debug: bool = False,
) -> Flask:
"""
Create the flask web application.
This is the only function to be called directly. It is the Flask application
factory function to create a Flask Class object to be run.
Parameters
----------
config_file : str|None, optional
Provide a configuration file during app creation. That might be practical,
if your config file is somewhere outside the standard path, and you cannot
set an environment variable.
The default is None.
testing : bool
Switch to test mode. Default is False.
testing_search :bool
Test the fulltext search. Default is False.
debug : bool
Switch on debug mode
Returns
-------
app : flask.Flask
Returns the flask webapp that then can be served by any uwsgi server
like Gunicorn, uWSGI or waitress. These should in praxis sit behind a real webserver.
Example
-------
app = create_app()
app = create_app("path_to_your_file/ResearchNotes_conf.py")
"""
# create and configure the app
app = RNFlask(__name__, instance_relative_config=True)
# Do some config to jinja2 to strip whitespace
app.jinja_env.trim_blocks = True
app.jinja_env.lstrip_blocks = True
# Read in the config. We will first import our configuration from
# the app_configuration module. Depending on the mode (production/development),
# we load the default configuration. Then we get the version string from the version.txt
# Finally, we check for environment variable and a config file.
#
# pylint: disable=import-outside-toplevel
from . import app_configuration
if debug:
app.config.from_object(app_configuration.DevConfig)
app.logger.debug("In development mode")
elif testing:
if testing_search:
app.config.from_object(app_configuration.TestSearchConfig)
else:
app.config.from_object(app_configuration.TestConfig)
app.logger.debug("In test mode")
else:
app.config.from_object(app_configuration.ProConfig)
app.logger.debug("In production mode")
if not testing:
if "RESEARCHNOTES_CONF" in os.environ:
app.logger.debug("Found environment variable for config file")
app.config.from_envvar("RESEARCHNOTES_CONF")
elif config_file is not None:
app.logger.debug(f"Using config file {config_file}")
app.config.from_pyfile(config_file, silent=False)
else:
app.logger.debug(f"Looking for config: {app.config['CONFIG_FILE']}")
app.config.from_pyfile(app.config["CONFIG_FILE"], silent=True)
# Define version string
app.config["VERSION_STRING"] = escape(__version__)
# app.config["SESSION_COOKIE_SAMESITE"] = "None"
# app.config["SESSION_COOKIE_SECURE"] = True
# Start logging
if not app.debug and not app.testing:
if app.config["LOG_FILE"]:
if not os.path.exists(app.config["LOG_PATH"]):
os.mkdir(app.config["LOG_PATH"])
#
log_file = os.path.join(app.config["LOG_PATH"], app.config["LOG_FILENAME"])
app.logger.info(f"Logging to: {log_file}")
file_handler = RotatingFileHandler(log_file, maxBytes=10240, backupCount=10)
file_handler.setFormatter(
logging.Formatter("%(asctime)s : %(levelname)s : %(funcName)s %(message)s")
)
file_handler.setLevel(logging.INFO)
app.logger.addHandler(file_handler)
if app.config["LOG_SYSLOG"]:
app.logger.info("Logging to syslog")
sys_handler = SysLogHandler(
address=app.config["LOG_SYSLOG_ADDR"],
facility=app.config["LOG_SYSLOG_FAC"],
)
sys_handler.setFormatter(
logging.Formatter(
"%(asctime)s : ResearchNotes[%(process)d] %(levelname)s : %(funcName)s %(message)s"
)
)
sys_handler.setLevel(logging.INFO)
app.logger.addHandler(sys_handler)
app.logger.setLevel(logging.INFO)
app.logger.info("ResearchNotes startup")
app.logger.debug(f" : Upload path: {app.config['UPLOAD_PATH']}")
app.logger.debug(f" : Database uri: {app.config['SQLALCHEMY_DATABASE_URI']}")
# Setting up our elastic search
if app.config["ELASTICSEARCH_URL"]:
from elasticsearch import Elasticsearch
app.elasticsearch = Elasticsearch(app.config["ELASTICSEARCH_URL"])
else:
app.elasticsearch = None
if app.config["MEILISEARCH_URL"]:
import meilisearch
app.meilisearch = meilisearch.Client(
app.config["MEILISEARCH_URL"], app.config["MEILISEARCH_MKEY"]
)
else:
app.meilisearch = None
app.logger.debug(f" : Elasticsearch URL: {app.config['ELASTICSEARCH_URL']}")
app.logger.debug(f" : Meilisearch URL: {app.config['MEILISEARCH_URL']}")
# Register the util submodule. Also, define a set of converters for later use
from . import util
app.register_blueprint(util.bp)
# define our filter to render Markdown pages
# app.config["FLATPAGES_HTML_RENDERER"] = util.markdown_render
app.url_map.converters["list"] = util.ListConverter
# ensure the instance folder exists
try:
os.makedirs(app.instance_path)
except OSError:
pass
# ensure that the upload folder exists
if not os.path.exists(app.config["UPLOAD_PATH"]) and not testing:
os.makedirs(app.config["UPLOAD_PATH"])
# Register our database engine
db.init_app(app)
with app.app_context():
if db.engine.url.drivername == "sqlite":
migrate.init_app(app, db, render_as_batch=True)
else:
migrate.init_app(app, db)
# To be removed soon (ToDo)
# Init FlatPages
# pages.init_app(app)
# Register the blueprint for authorization of users
from . import auth
app.register_blueprint(auth.bp)
# Register the sample submodule - deals all function for samples
from . import samples
app.register_blueprint(samples.bp)
# Register measurements submodule. Deal with measurements for samples
from . import measurement
app.register_blueprint(measurement.bp)
# Register the report submodule. Provides all function for reports related to measurements
from . import report
app.register_blueprint(report.bp)
# Register the instrument submodule. Provides the lab instrumentation functionality.
from . import instruments
app.register_blueprint(instruments.bp)
# Register the inventory submodule. Provides item tracking.
from . import inventory
app.register_blueprint(inventory.bp)
# Register the entry submodule. Provides the instrumentation journal functionality.
from . import entry
app.register_blueprint(entry.bp)
# Register the entry type submodule. Provides the instrumentation journal entry type functionality.
from . import type_management
app.register_blueprint(type_management.bp)
# Register files submodule. Deals with writing and getting files in the UPLOAD directory
from . import files
app.register_blueprint(files.bp)
# Register template submodule. Provides all function to have templates for samples,
# measurements and reports
from . import template
app.register_blueprint(template.bp)
# Register url_security submodule.
from . import url_security
app.register_blueprint(url_security.bp)
# Register the document submodule. Provides the wiki functionality
from . import documents
app.register_blueprint(documents.bp)
# Register the preview module. Here all file previews are defined
from . import preview
app.register_blueprint(preview.bp)
# Import the config submodule. All configuration of the running app like groups,
# places etc. are defined inside there.
from . import setup
app.register_blueprint(setup.bp)
from . import locations
app.register_blueprint(locations.bp)
from . import htmx
app.register_blueprint(htmx.bp)
from . import well_known
app.register_blueprint(well_known.bp)
# Import our test Blueprint. Currently, without work and not needed.
# from . import test
# app.register_blueprint(test.bp)
# Import and register our command line sub-module
from . import cli
app.register_blueprint(cli.bp)
app.cli.add_command(cli.init_db)
app.cli.add_command(cli.dump_database_json)
app.cli.add_command(cli.load_database_json)
app.cli.add_command(cli.database_maintenance)
app.cli.add_command(cli.update_search_index)
app.cli.add_command(cli.dump_meilisearch)
app.cli.add_command(cli.fix_filenames)
# Register the main submodule.
from . import main
app.register_blueprint(main.bp)
# Import the error handler blueprints
from . import error
# if not app.debug:
app.register_blueprint(error.bp)
# pylint: enable=import-outside-toplevel
return app