diff --git a/flaschengeist/config.py b/flaschengeist/config.py index ed74470..e06fcae 100644 --- a/flaschengeist/config.py +++ b/flaschengeist/config.py @@ -9,7 +9,7 @@ from flaschengeist import _module_path, logger # Default config: -config = {"DATABASE": {"port": 3306}} +config = {"DATABASE": {"engine": "mysql", "port": 3306}} def update_dict(d, u): @@ -65,17 +65,35 @@ def configure_app(app, test_config=None): else: app.config["SECRET_KEY"] = config["FLASCHENGEIST"]["secret_key"] - if test_config is None: - app.config["SQLALCHEMY_DATABASE_URI"] = "mysql{driver}://{user}:{passwd}@{host}:{port}/{database}".format( - driver="+pymysql" if os.name == "nt" else "", + if test_config is not None: + config["DATABASE"]["engine"] = "sqlite" + + if config["DATABASE"]["engine"] == "mysql": + engine = "mysql" + try: + # Try mysqlclient first + from MySQLdb import _mysql + except ModuleNotFoundError: + engine += "+pymysql" + options = "?charset=utf8mb4" + elif config["DATABASE"]["engine"] == "postgres": + engine = "postgresql+psycopg2" + options = "?client_encoding=utf8" + elif config["DATABASE"]["engine"] == "sqlite": + engine = "sqlite" + options = "" + host = "" + else: + logger.error(f"Invalid database engine configured. >{config['DATABASE']['engine']}< is unknown") + raise Exception + if config["DATABASE"]["engine"] in ["mysql", "postgresql"]: + host = "{user}:{password}@{host}:{port}".format( user=config["DATABASE"]["user"], - passwd=config["DATABASE"]["password"], + password=config["DATABASE"]["password"], host=config["DATABASE"]["host"], - database=config["DATABASE"]["database"], port=config["DATABASE"]["port"], ) - else: - app.config["SQLALCHEMY_DATABASE_URI"] = f"sqlite+pysqlite://{config['DATABASE']['file_path']}" + app.config["SQLALCHEMY_DATABASE_URI"] = f"{engine}://{host}/{config['DATABASE']['database']}{options}" app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False if "root" in config["FLASCHENGEIST"]: diff --git a/flaschengeist/flaschengeist.toml b/flaschengeist/flaschengeist.toml index 0ec50a0..ffbb51d 100644 --- a/flaschengeist/flaschengeist.toml +++ b/flaschengeist/flaschengeist.toml @@ -20,6 +20,7 @@ secret_key = "V3ryS3cr3t" level = "WARNING" [DATABASE] +# engine = "mysql" (default) # user = "user" # host = "127.0.0.1" # password = "password" diff --git a/flaschengeist/models/__init__.py b/flaschengeist/models/__init__.py index f63d173..1b503dd 100644 --- a/flaschengeist/models/__init__.py +++ b/flaschengeist/models/__init__.py @@ -2,7 +2,7 @@ import sys import datetime from sqlalchemy import BigInteger -from sqlalchemy.dialects import mysql +from sqlalchemy.dialects import mysql, sqlite from sqlalchemy.types import DateTime, TypeDecorator @@ -44,7 +44,7 @@ class ModelSerializeMixin: class Serial(TypeDecorator): """Same as MariaDB Serial used for IDs""" - impl = BigInteger().with_variant(mysql.BIGINT(unsigned=True), "mysql") + impl = BigInteger().with_variant(mysql.BIGINT(unsigned=True), "mysql").with_variant(sqlite.INTEGER, "sqlite") class UtcDateTime(TypeDecorator): diff --git a/flaschengeist/plugins/pricelist/__init__.py b/flaschengeist/plugins/pricelist/__init__.py index ef19807..b4df1ac 100644 --- a/flaschengeist/plugins/pricelist/__init__.py +++ b/flaschengeist/plugins/pricelist/__init__.py @@ -567,6 +567,7 @@ def get_columns(userid, current_session): userController.persist() return no_content() + @PriceListPlugin.blueprint.route("/users//pricecalc_columns_order", methods=["GET", "PUT"]) @login_required() def get_columns_order(userid, current_session): diff --git a/flaschengeist/plugins/users/__init__.py b/flaschengeist/plugins/users/__init__.py index e5b08a6..8a0db58 100644 --- a/flaschengeist/plugins/users/__init__.py +++ b/flaschengeist/plugins/users/__init__.py @@ -231,3 +231,21 @@ def notifications(current_session): def remove_notifications(nid, current_session): userController.delete_notification(nid, current_session.user_) return no_content() + + +@UsersPlugin.blueprint.route("/users//shortcuts", methods=["GET", "PUT"]) +@login_required() +def shortcuts(userid, current_session): + if userid != current_session.user_.userid: + raise Forbidden + + user = userController.get_user(userid) + if request.method == "GET": + return jsonify(user.get_attribute("users_link_shortcuts", [])) + else: + data = request.get_json() + if not isinstance(data, list) or not all(isinstance(n, dict) for n in data): + raise BadRequest + user.set_attribute("users_link_shortcuts", data) + userController.persist() + return no_content() diff --git a/readme.md b/readme.md index c10cf58..133a570 100644 --- a/readme.md +++ b/readme.md @@ -1,9 +1,16 @@ # Flaschengeist +This is the backend of the Flaschengeist. ## Installation ### Requirements -- mysql or mariadb -- python 3.6+ +- `mysql` or `mariadb` + - maybe `libmariadb` development files[1] +- python 3.7+ + +[1] By default Flaschengeist uses mysql as database backend, if you are on Windows Flaschengeist uses `PyMySQL`, but on +Linux / Mac the faster `mysqlclient` is used, if it is not already installed installing from pypi requires the +development files for `libmariadb` to be present on your system. + ### Install python files pip3 install --user . or with ldap support @@ -30,9 +37,21 @@ Configuration is done within the a `flaschengeist.toml`file, you can copy the on 1. `~/.config/` 2. A custom path and set environment variable `FLASCHENGEIST_CONF` -Change at least the database parameters! +Uncomment and change at least all the database parameters! ### Database installation +The user needs to have full permissions to the database. +If not you need to create user and database manually do (or similar on Windows): + + ( + echo "CREATE DATABASE flaschengeist;" + echo "CREATE USER 'flaschengeist'@'localhost' IDENTIFIED BY 'flaschengeist';" + echo "GRANT ALL PRIVILEGES ON 'flaschengeist'.* TO 'flaschengeist'@'localhost';" + echo "FLUSH PRIVILEGES;" + ) | sudo mysql + +Then you can install the database tables and initial entries: + run_flaschengeist install ### Run @@ -41,6 +60,8 @@ or with debug messages: run_flaschengeist run --debug +This will run the backend on http://localhost:5000 + ## Tests $ pip install '.[test]' $ pytest @@ -53,89 +74,4 @@ Or with html output (open `htmlcov/index.html` in a browser): $ coverage html ## Development -### Code Style -We enforce you to use PEP 8 code style with a line length of 120 as used by Black. -See also [Black Code Style](https://github.com/psf/black/blob/master/docs/the_black_code_style.md). - -#### Code formatting -We use [Black](https://github.com/psf/black) as the code formatter. - -Installation: - - pip install black -Usage: - - black -l 120 DIRECTORY_OR_FILE - -### Misc -#### Git blame -When using `git blame` use this to ignore the code formatting commits: - - $ git blame FILE.py --ignore-revs-file .git-blame-ignore-revs -Or if you just want to use `git blame`, configure git like this: - - $ git config blame.ignoreRevsFile .git-blame-ignore-revs - -#### Ignore changes on config - git update-index --assume-unchanged flaschengeist/flaschengeist.toml - -## Plugin Development -### File Structure - flaschengeist-example-plugin - |> __init__.py - |> model.py - |> setup.py - -### Files -#### \_\_init\_\_.py - from flask import Blueprint - from flaschengeist.modules import Plugin - - - example_bp = Blueprint("example", __name__, url_prefix="/example") - permissions = ["example_hello"] - - class PluginExample(Plugin): - def __init__(self, conf): - super().__init__(blueprint=example_bp, permissions=permissions) - - def install(self): - from flaschengeist.system.database import db - import .model - db.create_all() - db.session.commit() - - - @example_bp.route("/hello", methods=['GET']) - @login_required(roles=['example_hello']) - def __hello(id, **kwargs): - return "Hello" - -#### model.py -Optional, only needed if you need your own models (database) - - from flaschengeist.system.database import db - - - class ExampleModel(db.Model): - """Example Model""" - __tablename__ = 'example' - id = db.Column(db.Integer, primary_key=True) - description = db.Column(db.String(240)) - -#### setup.py - from setuptools import setup, find_packages - - setup( - name="flaschengeist-example-plugin", - version="0.0.0-dev", - packages=find_packages(), - install_requires=[ - "flaschengeist >= 2", - ], - entry_points={ - "flaschengeist.plugin": [ - "example = flaschengeist-example-plugin:ExampleModel" - ] - }, - ) +Please refer to our [development wiki](https://flaschengeist.dev/Flaschengeist/flaschengeist/wiki/Development). \ No newline at end of file diff --git a/run_flaschengeist b/run_flaschengeist index 26c10ec..e11271e 100644 --- a/run_flaschengeist +++ b/run_flaschengeist @@ -17,7 +17,7 @@ class PrefixMiddleware(object): def __call__(self, environ, start_response): if environ["PATH_INFO"].startswith(self.prefix): - environ["PATH_INFO"] = environ["PATH_INFO"][len(self.prefix):] + environ["PATH_INFO"] = environ["PATH_INFO"][len(self.prefix) :] environ["SCRIPT_NAME"] = self.prefix return self.app(environ, start_response) else: @@ -83,19 +83,19 @@ class InterfaceGenerator: import typing if ( - inspect.ismodule(module[1]) - and module[1].__name__.startswith(self.basename) - and module[1].__name__ not in self.known + inspect.ismodule(module[1]) + and module[1].__name__.startswith(self.basename) + and module[1].__name__ not in self.known ): self.known.append(module[1].__name__) for cls in inspect.getmembers(module[1], lambda x: inspect.isclass(x) or inspect.ismodule(x)): self.walker(cls) elif ( - inspect.isclass(module[1]) - and module[1].__module__.startswith(self.basename) - and module[0] not in self.classes - and not module[0].startswith("_") - and hasattr(module[1], "__annotations__") + inspect.isclass(module[1]) + and module[1].__module__.startswith(self.basename) + and module[0] not in self.classes + and not module[0].startswith("_") + and hasattr(module[1], "__annotations__") ): self.this_type = module[0] print("\n\n" + module[0] + "\n") @@ -156,12 +156,14 @@ def export(arguments): app = create_app() with app.app_context(): gen = InterfaceGenerator(arguments.namespace, arguments.file) - gen.run(models) - if arguments.plugins: + if not arguments.no_core: + gen.run(models) + if arguments.plugins is not None: for entry_point in pkg_resources.iter_entry_points("flaschengeist.plugin"): - plg = entry_point.load() - if hasattr(plg, "models") and plg.models is not None: - gen.run(plg.models) + if len(arguments.plugins) == 0 or entry_point.name in arguments.plugins: + plg = entry_point.load() + if hasattr(plg, "models") and plg.models is not None: + gen.run(plg.models) gen.write() @@ -183,7 +185,12 @@ if __name__ == "__main__": parser_export.set_defaults(func=export) parser_export.add_argument("--file", help="Filename where to save", default="flaschengeist.d.ts") parser_export.add_argument("--namespace", help="Namespace of declarations", default="FG") - parser_export.add_argument("--plugins", help="Also export plugins", action="store_true") + parser_export.add_argument( + "--no-core", + help="Do not export core declarations (only useful in conjunction with --plugins)", + action="store_true", + ) + parser_export.add_argument("--plugins", help="Also export plugins (none means all)", nargs="*") args = parser.parse_args() args.func(args)