Source code for ResearchNotes

# -*- 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