From 96e4e73f4becd6aa475e87377a3a265054e5d5ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Gr=C3=B6ger?= Date: Sun, 18 Apr 2021 23:28:05 +0200 Subject: [PATCH 1/6] [users] add dynamic shortcuts for users --- flaschengeist/plugins/users/__init__.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) 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() From 776332d5fe6a5c2fdc1c400047db39833a9f52fe Mon Sep 17 00:00:00 2001 From: groegert Date: Thu, 20 May 2021 15:37:17 +0000 Subject: [PATCH 2/6] added some hints to ease the installation --- readme.md | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/readme.md b/readme.md index c10cf58..5cef43d 100644 --- a/readme.md +++ b/readme.md @@ -1,8 +1,10 @@ # Flaschengeist +This is the backend of the Flaschengeist. ## Installation ### Requirements - mysql or mariadb + - including development files libmariadb-dev - python 3.6+ ### Install python files pip3 install --user . @@ -30,9 +32,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 +55,8 @@ or with debug messages: run_flaschengeist run --debug +This will run the backend on http://localhost:5000 + ## Tests $ pip install '.[test]' $ pytest @@ -55,7 +71,7 @@ Or with html output (open `htmlcov/index.html` in a browser): ## 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). +See also [Black Code Style](https://github.com/psf/black/blob/main/docs/the_black_code_style/current_style.md). #### Code formatting We use [Black](https://github.com/psf/black) as the code formatter. From 8696699ecb8e90dc3b4b95d7d3d3c6e8a9ba3a75 Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Wed, 26 May 2021 16:47:03 +0200 Subject: [PATCH 3/6] [run_flaschengeist] Improved export command Export now supports --no-core flag, if set no core models will get exported. Also --plugins was changed to support a list of plugins, if no list is given the old behavior is taken (export all plugins). If a list of plugins is given, only those plugins are exported. --- flaschengeist/plugins/pricelist/__init__.py | 1 + run_flaschengeist | 37 ++++++++++++--------- 2 files changed, 23 insertions(+), 15 deletions(-) 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/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) From 7b5f854d510c65c8b9932f48cf255a5dc257a47c Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Thu, 27 May 2021 01:27:53 +0200 Subject: [PATCH 4/6] [db] Support sqlite and postgresql as engine, fixes #5 mysql / mariadb still is the only tested configuration. This will break existing databases, as UTF8MB4 is enforced for mysql (real UTF8). --- flaschengeist/config.py | 29 +++++++++++++++++++++-------- flaschengeist/flaschengeist.toml | 1 + flaschengeist/models/__init__.py | 4 ++-- 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/flaschengeist/config.py b/flaschengeist/config.py index ed74470..9820e9e 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,30 @@ 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" + ("+pymysql" if os.name == "nt" else "") + 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): From 7928c16c07c72c0d6ec4833ec4beafd54160a44a Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Thu, 27 May 2021 01:52:30 +0200 Subject: [PATCH 5/6] [db] Try mysqlclient first, maybe the user managed to get it working on Windows --- flaschengeist/config.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/flaschengeist/config.py b/flaschengeist/config.py index 9820e9e..e06fcae 100644 --- a/flaschengeist/config.py +++ b/flaschengeist/config.py @@ -69,7 +69,12 @@ def configure_app(app, test_config=None): config["DATABASE"]["engine"] = "sqlite" if config["DATABASE"]["engine"] == "mysql": - engine = "mysql" + ("+pymysql" if os.name == "nt" else "") + 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" From 3fc04c4143564d3b1c678e23d73ec3a6b2a4f4a5 Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Thu, 27 May 2021 01:52:45 +0200 Subject: [PATCH 6/6] [docs] Moved some devel docs to the wiki --- readme.md | 98 +++++-------------------------------------------------- 1 file changed, 9 insertions(+), 89 deletions(-) diff --git a/readme.md b/readme.md index 5cef43d..133a570 100644 --- a/readme.md +++ b/readme.md @@ -3,9 +3,14 @@ This is the backend of the Flaschengeist. ## Installation ### Requirements -- mysql or mariadb - - including development files libmariadb-dev -- 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 @@ -69,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/main/docs/the_black_code_style/current_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