feature/pricelist #15
|
@ -9,7 +9,7 @@ from flaschengeist import _module_path, logger
|
||||||
|
|
||||||
|
|
||||||
# Default config:
|
# Default config:
|
||||||
config = {"DATABASE": {"port": 3306}}
|
config = {"DATABASE": {"engine": "mysql", "port": 3306}}
|
||||||
|
|
||||||
|
|
||||||
def update_dict(d, u):
|
def update_dict(d, u):
|
||||||
|
@ -65,17 +65,35 @@ def configure_app(app, test_config=None):
|
||||||
else:
|
else:
|
||||||
app.config["SECRET_KEY"] = config["FLASCHENGEIST"]["secret_key"]
|
app.config["SECRET_KEY"] = config["FLASCHENGEIST"]["secret_key"]
|
||||||
|
|
||||||
if test_config is None:
|
if test_config is not None:
|
||||||
app.config["SQLALCHEMY_DATABASE_URI"] = "mysql{driver}://{user}:{passwd}@{host}:{port}/{database}".format(
|
config["DATABASE"]["engine"] = "sqlite"
|
||||||
driver="+pymysql" if os.name == "nt" else "",
|
|
||||||
|
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"],
|
user=config["DATABASE"]["user"],
|
||||||
passwd=config["DATABASE"]["password"],
|
password=config["DATABASE"]["password"],
|
||||||
host=config["DATABASE"]["host"],
|
host=config["DATABASE"]["host"],
|
||||||
database=config["DATABASE"]["database"],
|
|
||||||
port=config["DATABASE"]["port"],
|
port=config["DATABASE"]["port"],
|
||||||
)
|
)
|
||||||
else:
|
app.config["SQLALCHEMY_DATABASE_URI"] = f"{engine}://{host}/{config['DATABASE']['database']}{options}"
|
||||||
app.config["SQLALCHEMY_DATABASE_URI"] = f"sqlite+pysqlite://{config['DATABASE']['file_path']}"
|
|
||||||
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
|
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
|
||||||
|
|
||||||
if "root" in config["FLASCHENGEIST"]:
|
if "root" in config["FLASCHENGEIST"]:
|
||||||
|
|
|
@ -20,6 +20,7 @@ secret_key = "V3ryS3cr3t"
|
||||||
level = "WARNING"
|
level = "WARNING"
|
||||||
|
|
||||||
[DATABASE]
|
[DATABASE]
|
||||||
|
# engine = "mysql" (default)
|
||||||
# user = "user"
|
# user = "user"
|
||||||
# host = "127.0.0.1"
|
# host = "127.0.0.1"
|
||||||
# password = "password"
|
# password = "password"
|
||||||
|
|
|
@ -2,7 +2,7 @@ import sys
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
from sqlalchemy import BigInteger
|
from sqlalchemy import BigInteger
|
||||||
from sqlalchemy.dialects import mysql
|
from sqlalchemy.dialects import mysql, sqlite
|
||||||
from sqlalchemy.types import DateTime, TypeDecorator
|
from sqlalchemy.types import DateTime, TypeDecorator
|
||||||
|
|
||||||
|
|
||||||
|
@ -44,7 +44,7 @@ class ModelSerializeMixin:
|
||||||
class Serial(TypeDecorator):
|
class Serial(TypeDecorator):
|
||||||
"""Same as MariaDB Serial used for IDs"""
|
"""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):
|
class UtcDateTime(TypeDecorator):
|
||||||
|
|
|
@ -567,6 +567,7 @@ def get_columns(userid, current_session):
|
||||||
userController.persist()
|
userController.persist()
|
||||||
return no_content()
|
return no_content()
|
||||||
|
|
||||||
|
|
||||||
@PriceListPlugin.blueprint.route("/users/<userid>/pricecalc_columns_order", methods=["GET", "PUT"])
|
@PriceListPlugin.blueprint.route("/users/<userid>/pricecalc_columns_order", methods=["GET", "PUT"])
|
||||||
@login_required()
|
@login_required()
|
||||||
def get_columns_order(userid, current_session):
|
def get_columns_order(userid, current_session):
|
||||||
|
|
|
@ -231,3 +231,21 @@ def notifications(current_session):
|
||||||
def remove_notifications(nid, current_session):
|
def remove_notifications(nid, current_session):
|
||||||
userController.delete_notification(nid, current_session.user_)
|
userController.delete_notification(nid, current_session.user_)
|
||||||
return no_content()
|
return no_content()
|
||||||
|
|
||||||
|
|
||||||
|
@UsersPlugin.blueprint.route("/users/<userid>/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()
|
||||||
|
|
114
readme.md
114
readme.md
|
@ -1,9 +1,16 @@
|
||||||
# Flaschengeist
|
# Flaschengeist
|
||||||
|
This is the backend of the Flaschengeist.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
### Requirements
|
### Requirements
|
||||||
- mysql or mariadb
|
- `mysql` or `mariadb`
|
||||||
- python 3.6+
|
- 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
|
### Install python files
|
||||||
pip3 install --user .
|
pip3 install --user .
|
||||||
or with ldap support
|
or with ldap support
|
||||||
|
@ -30,9 +37,21 @@ Configuration is done within the a `flaschengeist.toml`file, you can copy the on
|
||||||
1. `~/.config/`
|
1. `~/.config/`
|
||||||
2. A custom path and set environment variable `FLASCHENGEIST_CONF`
|
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
|
### 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_flaschengeist install
|
||||||
|
|
||||||
### Run
|
### Run
|
||||||
|
@ -41,6 +60,8 @@ or with debug messages:
|
||||||
|
|
||||||
run_flaschengeist run --debug
|
run_flaschengeist run --debug
|
||||||
|
|
||||||
|
This will run the backend on http://localhost:5000
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
$ pip install '.[test]'
|
$ pip install '.[test]'
|
||||||
$ pytest
|
$ pytest
|
||||||
|
@ -53,89 +74,4 @@ Or with html output (open `htmlcov/index.html` in a browser):
|
||||||
$ coverage html
|
$ coverage html
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
### Code Style
|
Please refer to our [development wiki](https://flaschengeist.dev/Flaschengeist/flaschengeist/wiki/Development).
|
||||||
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"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
)
|
|
|
@ -17,7 +17,7 @@ class PrefixMiddleware(object):
|
||||||
def __call__(self, environ, start_response):
|
def __call__(self, environ, start_response):
|
||||||
|
|
||||||
if environ["PATH_INFO"].startswith(self.prefix):
|
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
|
environ["SCRIPT_NAME"] = self.prefix
|
||||||
return self.app(environ, start_response)
|
return self.app(environ, start_response)
|
||||||
else:
|
else:
|
||||||
|
@ -83,19 +83,19 @@ class InterfaceGenerator:
|
||||||
import typing
|
import typing
|
||||||
|
|
||||||
if (
|
if (
|
||||||
inspect.ismodule(module[1])
|
inspect.ismodule(module[1])
|
||||||
and module[1].__name__.startswith(self.basename)
|
and module[1].__name__.startswith(self.basename)
|
||||||
and module[1].__name__ not in self.known
|
and module[1].__name__ not in self.known
|
||||||
):
|
):
|
||||||
self.known.append(module[1].__name__)
|
self.known.append(module[1].__name__)
|
||||||
for cls in inspect.getmembers(module[1], lambda x: inspect.isclass(x) or inspect.ismodule(x)):
|
for cls in inspect.getmembers(module[1], lambda x: inspect.isclass(x) or inspect.ismodule(x)):
|
||||||
self.walker(cls)
|
self.walker(cls)
|
||||||
elif (
|
elif (
|
||||||
inspect.isclass(module[1])
|
inspect.isclass(module[1])
|
||||||
and module[1].__module__.startswith(self.basename)
|
and module[1].__module__.startswith(self.basename)
|
||||||
and module[0] not in self.classes
|
and module[0] not in self.classes
|
||||||
and not module[0].startswith("_")
|
and not module[0].startswith("_")
|
||||||
and hasattr(module[1], "__annotations__")
|
and hasattr(module[1], "__annotations__")
|
||||||
):
|
):
|
||||||
self.this_type = module[0]
|
self.this_type = module[0]
|
||||||
print("\n\n" + module[0] + "\n")
|
print("\n\n" + module[0] + "\n")
|
||||||
|
@ -156,12 +156,14 @@ def export(arguments):
|
||||||
app = create_app()
|
app = create_app()
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
gen = InterfaceGenerator(arguments.namespace, arguments.file)
|
gen = InterfaceGenerator(arguments.namespace, arguments.file)
|
||||||
gen.run(models)
|
if not arguments.no_core:
|
||||||
if arguments.plugins:
|
gen.run(models)
|
||||||
|
if arguments.plugins is not None:
|
||||||
for entry_point in pkg_resources.iter_entry_points("flaschengeist.plugin"):
|
for entry_point in pkg_resources.iter_entry_points("flaschengeist.plugin"):
|
||||||
plg = entry_point.load()
|
if len(arguments.plugins) == 0 or entry_point.name in arguments.plugins:
|
||||||
if hasattr(plg, "models") and plg.models is not None:
|
plg = entry_point.load()
|
||||||
gen.run(plg.models)
|
if hasattr(plg, "models") and plg.models is not None:
|
||||||
|
gen.run(plg.models)
|
||||||
gen.write()
|
gen.write()
|
||||||
|
|
||||||
|
|
||||||
|
@ -183,7 +185,12 @@ if __name__ == "__main__":
|
||||||
parser_export.set_defaults(func=export)
|
parser_export.set_defaults(func=export)
|
||||||
parser_export.add_argument("--file", help="Filename where to save", default="flaschengeist.d.ts")
|
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("--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 = parser.parse_args()
|
||||||
args.func(args)
|
args.func(args)
|
||||||
|
|
Loading…
Reference in New Issue