diff --git a/DOCS.md b/DOCS.md index 4ee18c5..bb1e0e9 100644 --- a/DOCS.md +++ b/DOCS.md @@ -4,8 +4,7 @@ ### App Factory -The default Flask app factory hasn't changed much. But the FlaskPP class can do a lot of repetitive work for you. -A sample factory is written into **project_root/main.py** by the `fpp init` command: +The default Flask app factory hasn't changed much. But the FlaskPP class can do a lot of repetitive work for you. A sample factory is written into **project_root/main.py** by the `fpp init` command: ```python from flaskpp import FlaskPP @@ -26,11 +25,9 @@ The FlaskPP class just extends Flask with basic factory tasks like setting up th ### Configuration -There are two ways of configuring your apps. The first and most important one is app configs, which you can find in **project_root/app_configs**. -They are named like that: **[app_name].conf** and used by the `fpp run` command to load your apps. (Config variables are passed as environment.) +There are two ways of configuring your apps. The first and most important one is app configs, which you can find in **project_root/app_configs**. They are named like that: **[app_name].conf** and used by the `fpp run` command to load your apps. (Config variables are passed as environment.) -With app configs you can control which extensions you want to use and pass secrets, defaults and every other data that you would usually write into your env files. -A basic Flask++ app.conf file looks like that: +With app configs you can control which features and extensions you want to use or pass secrets, defaults and every other data that you would usually write into your env files. A basic Flask++ **app.conf** file looks like that: ``` [core] @@ -64,7 +61,7 @@ JWT_SECRET_KEY = supersecret [extensions] EXT_SQLALCHEMY = 1 EXT_SOCKET = 1 -EXT_BABEL = 0 +EXT_BABEL = 1 EXT_FST = 0 EXT_AUTHLIB = 0 EXT_MAILING = 0 @@ -83,6 +80,7 @@ FRONTEND_ENGINE = 1 DB_AUTOUPDATE = 0 [modules] +module_id = 1 module_id_01 = 0 module_id_02 = 0 ... @@ -90,10 +88,9 @@ module_id_02 = 0 MODULE_HOME = module_id ``` -And can be generated and configured automatically by running `fpp setup` inside your project root. +It can be generated and configured automatically by running `fpp setup` inside your project root. -The second way of configuring your app is by using config classes. We provide a registration function, so you can plug in your own config classes with ease. -They will be extended with each other to one big config class by priority (values 1 to 10). In the end it will be extended with our default config class: +The second way of configuring your app is by using config classes. We provide a registration function, so you can plug in your own config classes with ease. They will be extended with each other to one big config class by priority (values 1 to 10). In the end it will be extended with our default config class: ```python class DefaultConfig: @@ -118,8 +115,8 @@ class DefaultConfig: # ------------------------------------------------- # Flask-Limiter (Rate Limiting) # ------------------------------------------------- - RATELIMIT = True - RATELIMIT_STORAGE_URL = f"{os.getenv('REDIS_URL', 'redis://localhost:6379')}/1" + RATELIMIT_ENABLED = True + RATELIMIT_STORAGE_URI = f"{os.getenv('REDIS_URL', 'redis://localhost:6379')}/1" RATELIMIT_DEFAULT = "500 per day; 100 per hour" RATELIMIT_STRATEGY = "fixed-window" @@ -203,11 +200,7 @@ So you can overwrite config keys by priority. This is especially useful to provi ### Lifecycle -You may have noticed the `app.start()` method inside our autogenerated **main.py** file shown in the ["App Factory" chapter](#app-factory). -This method is an automated lifecycle manager that replaces the `app.run()` method. It will automatically run itself inside -a daemon thread using Uvicorn. To handle SIGINT and SIGTERM signals, it uses a `threading.Event` flag. The FlaskPP class provides -decorators to register startup and shutdown hooks which will be executed before and after the app starts and stops. You can register -hooks like that: +You may have noticed the `app.start()` method inside our autogenerated **main.py** file shown in the ["App Factory" chapter](#app-factory). This method is an automated lifecycle manager that replaces the `app.run()` method. It will automatically run itself inside a daemon thread using Uvicorn. To handle SIGINT and SIGTERM signals, it uses a `threading.Event` flag. The FlaskPP class provides decorators to register startup and shutdown hooks which will be executed before and after the app starts and stops. You can register hooks like that: ```python @app.on_startup @@ -223,9 +216,7 @@ def shutdown_hook(): ### ASGI Wrapper -If you are running your app using the start method, you may ask how a wsgi app is running itself inside an asgi server. -Well, therefore the FlaskPP class provides a `app.to_asgi()` wrapper. It will map itself into an asgi compatible class, -but sensitive to the **EXT_SOCKET** switch: +If you are running your app using the start method, you may ask how a wsgi app is running itself inside an asgi server. Well, therefore the FlaskPP class provides a `app.to_asgi()` wrapper. It will map itself into an asgi compatible class, but sensitive to the **EXT_SOCKET** switch: ```python class FlaskPP(Flask): @@ -248,39 +239,36 @@ class FlaskPP(Flask): ## Modules -Modules are the most important feature of Flask++ apps. They work like little blueprint-based Flask apps, which can be -plugged into your main app using the module switches inside your app config files. Of course, you can turn off this feature -by setting the **FPP_MODULES** switch to 0. +Modules are the most important feature of Flask++ apps. They work like little blueprint-based Flask apps, which can be plugged into your main app using the module switches inside your app config files. Of course, you can turn off this feature by setting the **FPP_MODULES** switch to 0. ### Module Setup -The most easy way to create a module is to use the `fpp modules create` command. But you can also create modules manually. -It would be way more work, but we will explain what it would look like. Not because it is recommended but to provide a basic -understanding of how modules work. +The easiest way to create a module is to use the `fpp modules create` command. But you can also create modules manually. It would be way more work, but we will explain what it would look like. Not because it is recommended but to provide a basic understanding of how modules work. -At first, you would create a new python package inside **project_root/modules**. It is recommended to use the module id as -the package name, but you don't necessarily have to. +At first, you would create a new python package inside **project_root/modules**. It is recommended to use the module id as the package name, but you don't necessarily have to. #### Manifest -For your module to be recognized by Flask++ you need to create a **manifest.json** file inside your module package. It has -to contain at least one field named "version". The following version string formats are supported: +For your module to be recognized by Flask++ you need to create a **manifest.json** file inside your module package. It has to contain at least one field named "version". The following version string formats are supported (they are not case-sensitive): ``` x x alpha x beta +x rc x.x x.x alpha x.x beta +x.x rc x.x.x x.x.x alpha x.x.x beta +x.x.x rc ``` -A fully qualified manifest file would look like this: +A fully qualified manifest file would then look like this: ```json5 { @@ -289,8 +277,8 @@ A fully qualified manifest file would look like this: "description": "This module does ...", "version": "0.1", "requires": { - "fpp": ">=0.3.7", // the minimum required version of Flask++ - "modules": { // other modules that are required by this module + "fpp": ">=0.3.5", // the minimum required version of Flask++ + "modules": { // other modules that are required by your module "module_id_01": "==0.2", "module_id_02": "<0.7" } @@ -300,7 +288,7 @@ A fully qualified manifest file would look like this: #### Init File -Inside your **\_\_init__.py** file, you create `module: Module` variable and optionally register a `module.on_enable` hook: +Inside your **\_\_init__.py** file, you create a `module: Module` variable and optionally register a `module.on_enable` hook: ```python from flaskpp import Module @@ -342,7 +330,7 @@ def on_enable(app: FlaskPP): # And if you disabled automated route initialization: module.init_routes() - # The module has got its own context dict, which is used the modules context_processor. + # The module has got its own context dict, which is used by the modules context_processor. # So you can set up default variables which will be available inside your modules templates: module.context["my_variable"] = "Hello World!" @@ -352,10 +340,7 @@ def on_enable(app: FlaskPP): #### Handling -To abstract the handling of your module from its routes, the Module class provides a `module.handle_request(handler_name: str)` function. -You can combine this with a handling package inside your module. The handling feature is automated as well. For it to work, you need to -create a `init_handling(mod: Module)` function inside its **\_\_init__.py** file. The default init file created by the -`fpp modules create` command looks like this: +To abstract the handling of your module from its routes, the Module class provides a `module.handle_request(handler_name: str)` function. You can combine this with a handling package inside your module. The handling feature is automated as well. For it to work, you need to create an `init_handling(mod: Module)` function inside its **\_\_init__.py** file. The default init file created by the`fpp modules create` command looks like this: ```python from pathlib import Path @@ -377,7 +362,7 @@ def init_handling(mod: Module): mod.handler(handler_name)(handle_request) ``` -Handler files should look like this: +Handler files should then at least contain this: ```python def handle_request(mod: Module, *args): @@ -385,23 +370,19 @@ def handle_request(mod: Module, *args): pass ``` -But you can also build your own handling structure working with the `module.handler(handler_name: str)` decorator, -as long as you stick with the handle_request function signature shown above. The module parameter will be passed -by a wrapper inside the `module.hander(handler_name: str)` decorator. +But you can also build your own handling structure working with the `module.handler(handler_name: str)` decorator, as long as you stick with the handle_request function signature shown above. The module parameter will be passed by a wrapper inside the function's decorator. #### Routes -You will also need to create a **routes.py** file inside your module package. This file will have to contain an -`init_routes(mod: Module)` function, which will be imported and called automatically by the `module.init_routes()` function. -So your file should look something like this: +You will also need to create a **routes.py** file inside your module package. This file will have to contain an `init_routes(mod: Module)` function, which will be imported and called automatically by the `module.init_routes()` function. So your file should look something like this: ```python def init_routes(mod: Module): @mod.route("/my-route") def my_route(): - # Modules provide their own render template function, so you can easily target the templates of your module: + # Modules provide their own render_template function, so you can easily target the templates of your module: return mod.render_template("module_template.html") - # This automatically targets the templates folder inside your module package. + # This automatically renders the templates of your modules template folder. # Of course, you can work with your handlers, which we set up before: mod.route("/handle")( @@ -416,8 +397,7 @@ def init_routes(mod: Module): #### Config -You can optionally create a **config.py** file inside your module package. There you can create your modules config class -(like mentioned earlier in the ["Configuration" chapter](#configuration)) if you need: +You can optionally create a **config.py** file inside your module package. There you can create your modules config class (like mentioned earlier in the ["Configuration" chapter](#configuration)) if you need: ```python from flaskpp.app.config import register_config @@ -433,9 +413,7 @@ class ModuleConfig: #### Data Package -If your module requires **EXT_SQLALCHEMY** you need to create a **data** package inside your module. There you can create -your modules model classes. For this to work, you have to create an `init_models()` function inside its **\_\_init__.py** file. -The generated default looks like this: +If your module requires **EXT_SQLALCHEMY** you need to create a **data** package inside your module. There you can create your modules model classes. For this to work, you have to create an `init_models()` function inside its **\_\_init__.py** file. The generated default looks like this: ```python from pathlib import Path @@ -454,15 +432,11 @@ def init_models(mod: Module): ### Working with Modules -To simplify the work with modules, modules provide their own render_template (like mentioned above) and url_for functions. -So when you are handling a request inside a module, we recommend using these functions instead of the global ones. Modules -are sensitive to the **HOME_MODULE** configuration. Defining a home module is optional, but if you do so, the module will -be registered as if it was the main app when it gets enabled. +To simplify the work with modules, they provide their own render_template (like mentioned above) and url_for functions. So when you are handling a request inside a module, we recommend using these functions instead of the global ones. Modules are sensitive to the **HOME_MODULE** configuration. Defining a home module is optional, but if you do so, the module will be registered as if it was the main app when it gets enabled. #### Context -As shown earlier, modules do have their own context dict. It has a default value called "NAME", which you can use inside your -modules templates to work resolve its templates or urls: +As shown earlier, modules do have their own context dict. It has a default value called "NAME", which you can use inside your modules templates to resolve its templates or urls: ```html {% extends NAME ~ "/base.html" %} @@ -481,22 +455,15 @@ modules templates to work resolve its templates or urls: ## Features -When you take a look at our app config example, which we have shown in the ["Configuration" chapter](#configuration), you may have noticed that -there are some feature switches. Besides those switches, some of the extension switches are also enabling adopted Flask++ extension -classes, which extend the underlying base extension classes. We will show you which extensions are affected, so you can turn them off -if you would like to stick with Flask defaults. +When you take a look at our app config example, which we have shown in the ["Configuration" chapter](#configuration), you may have noticed that there are some feature switches. Besides those switches, some of the extension switches are also enabling adopted Flask++ extension classes and features, which extend the underlying Flask extension. We will show you which extensions are affected, so you can turn them off if you would like to stick with Flask defaults. -The FlaskPP class uses the `flaskpp.utils.enabled` function to check if a feature or extension switch is enabled. If so, it will -automatically set up the specific feature or initialize the extension. +The FlaskPP class uses the `flaskpp.utils.enabled` function to check if a feature or extension switch is enabled. If so, it will automatically set up the specific feature or initialize the extension. ### FPP_PROCESSING -The first feature switch is **FPP_PROCESSING**. This will cause the app to use the Flask++ processing utils to register request processors. -Therefore, it uses the `flaskpp.app.utils.processing` module. It provides its own decorators that replace the Flask ones, which is useful -if you want to use some of the Flask++ processing utility. The decorators will then overwrite the Flask++ default processors. +The first feature switch is **FPP_PROCESSING**. This will cause the app to use the Flask++ processing utils to register request processors. Therefore, it uses the `flaskpp.app.utils.processing` module. It provides its own decorators that replace the Flask ones, which is useful if you want to use some of the Flask++ processing utility. The decorators will then overwrite the Flask++ default processors. -Flask++ default processors provide some useful features like extended context processing (matching to the Flask++ environment), -request logging and error handling (rendering "404.html" on NotFound errors or else "error.html"). +Flask++ default processors provide some useful features like extended context processing (matching to the Flask++ environment), request logging and error handling (rendering "404.html" on NotFound errors or else "error.html"). To overwrite processors, use their matching decorators like this: @@ -514,8 +481,7 @@ def before_request(): pass ``` -The FlaskPP class uses the `set_default_handlers(app: Flask)` function its default processors. This util then uses the `get_handler(name: str) -> Callable` -function to get the matching handler from the processing utils. Both are part of the processing module and look like this: +The FlaskPP class uses the `set_default_handlers(app: Flask)` function to set its default processors. This util then uses the `get_handler(name: str) -> Callable` function to get the matching handler from the processing utils. Both are part of the processing module and look like this: ```python def get_handler(name: str) -> Callable: @@ -548,14 +514,9 @@ def set_default_handlers(app: Flask): ### EXT_SOCKET -When we talk about **FPP_PROCESSING** we should also mention the **EXT_SOCKET** extension switch. It extends SocketIOs AsyncServer -with some Flask++ specific features. Some of those features are default features of the FppSocket class, and others rely on the -default_processing switch. The sockets default processing features will either be turned on by the **FPP_PROCESSING** switch or -by setting the default_processing value of its init function to True. +When we talk about **FPP_PROCESSING** we should also mention the **EXT_SOCKET** extension switch. It extends SocketIOs AsyncServer with some Flask++ specific features. Some of those features are default features of the FppSocket class, and others rely on the default_processing switch. The sockets default processing features will either be turned on by the **FPP_PROCESSING** config switch or by setting its default_processing value to True (before you initialize the FlaskPP class). -The default features of the FppSocket class are its event_context (inspired by Flasks request_context) and the enable_sid_passing -value of its init function (default is True because it's the AsyncServers default too) or the `socket.sid_passing` switch itself -(if you are using the socket from `flaskpp.app.extensions` and want to turn it off): +The default features of the FppSocket class are its event_context (inspired by Flasks request_context) and the enable_sid_passing value of its init function (default is True because it's the AsyncServers default too) or the `socket.sid_passing` switch itself (if you are using the socket from `flaskpp.app.extensions` and want to turn it off): ```python from flaskpp.app.extensions import socket @@ -574,11 +535,7 @@ async def event( log("info", f"Current session data from socket: {socket.current_session}") ``` -If you enable default processing, you will get access to the full power of the FppSocket class. This includes default events (a socket -event called "default_event" or – if you initialize your own FppSocket – whatever default_event_name you pass to its init function), -a default on_connect handler (which writes the session and lang cookie into the session), error handling, HTML injectors (A default -event called "html". It can be used to return HTML strings that you can write into DOM blocks with your scripts.), decorators to -replace the default processing functions with you own ones and a get_handler function to fetch default handlers: +If you enable default processing, you will get access to the full power of the FppSocket class. This includes default events (a socket event called "default_event" or – if you initialize your own FppSocket – whatever default_event_name you pass to its init function), a default connect handler (which writes the session and lang cookie into the session), error handling, HTML injectors (A default_event called "html". It can be used to return HTML strings that you can write into DOM blocks with your scripts.), decorators to replace the default processing functions with your own ones and a get_handler function to fetch default handlers: ```python from flaskpp.app.extensions import socket @@ -613,12 +570,12 @@ async def handle_connect(sid: str, environ: dict): pass @socket.default_handler -def on_default(sid: str, data: dict): +def handle_default_event(sid: str, data: dict): # TODO: Handle every socket event called default_event (or whatever name you gave it) pass @socket.html_handler -def on_html(key: str): +def handle_html_request(key: str): # TODO: Handle every default_event called "html" pass @@ -643,20 +600,13 @@ handler = socket.get_handler( # handler_dict["*"]["*"] ``` -Be aware that default events should not contain any "@" inside their names, because the FppSocket resolves namespaces of -default events using the "@" character. +Be aware that default events should not contain any "@" inside their names, because the FppSocket resolves namespaces of default events using the "@" character. ### EXT_BABEL & FPP_I18N_FALLBACK -These two switches come together. The **FPP_I18N_FALLBACK** switch only takes effect if the **EXT_BABEL** switch is enabled too. -This is because Flask++ also provides its own Babel class called FppBabel, which extends `flask_babelplus.Babel`. Besides that, -Flask++ also changes the internationalization process to fit into the Flask++ environment. That's why **EXT_BABEL** requires -the **EXT_SQLALCHEMY** switch to be enabled. The Flask++ i18n system primarily stores translations inside the database -and only uses the message catalogs as fallback. +These two switches come together. The **FPP_I18N_FALLBACK** switch only takes effect if the **EXT_BABEL** switch is enabled too. This is because Flask++ also provides its own Babel class called FppBabel, which extends `flask_babelplus.Babel`. Besides that, Flask++ also changes the internationalization process to fit into the Flask++ environment. That's why **EXT_BABEL** requires the **EXT_SQLALCHEMY** switch to be enabled. The Flask++ i18n system primarily stores translations inside the database and only uses the message catalogs as fallback. -It also provides its own domain resolving system, which matches with the Flask++ module system. This is also where the -**FPP_I18N_FALLBACK** switch comes into play, because it adds a fallback domain called "flaskpp" which contains default -translations keys providing German and English translations, that are used by the Flask++ [app utility](#further-utilities). +It also provides its own domain resolving system, which matches with the Flask++ module system. This is also where the **FPP_I18N_FALLBACK** switch comes into play, because it adds a fallback domain called "flaskpp" which contains default translation keys providing German and English translations, that are used by the Flask++ [app utility](#further-utilities). Let's take a look at how you set up Babel for a specific module and how the fallback escalation works: @@ -709,7 +659,7 @@ And here is an example of what your **module_package/data/noinit_translations.py ```python from flaskpp.app.data import commit -from flaskpp.app.data.babel import add_entry, get_entries +from flaskpp.app.data.babel import add_entry, get_entries, get_entry _msg_keys = [ "EXAMPLE_TITLE", @@ -741,8 +691,11 @@ def setup_db(mod: Module): _add_entries(key, domain) for entry in entries: - if _translations_en[entry.key] != entry.text: - entry.text = _translations_en[entry.key] + key = entry.key + if _translations_en[key] != entry.text: + entry.text = _translations_en[key] + entry_de = get_entry(key, "de", domain) + entry_de.text = _translations_de[key] else: for key in _msg_keys: _add_entries(key, domain) @@ -750,14 +703,13 @@ def setup_db(mod: Module): commit() ``` +Like with our FppSockets default event system, our domain resolving system uses the "@" character as well. So when you are using our integrated i18n tooling, you should avoid using "@" characters inside your translation keys as well. + ### AUTOGENERATE_TAILWIND_CSS -Flask++ comes with its own Tailwind integration. Therefore, it uses Tailwinds standalone cli tool to generate your CSS files -on the fly. If you enable **AUTOGENERATE_TAILWIND_CSS**, Flask++ will automatically compile every **tailwind_raw.css** file -inside your apps or modules **static/css** folder and create a corresponding **tailwind.css** asset, when you run `app.start()`. +Flask++ comes with its own Tailwind integration. Therefore, it uses Tailwinds standalone cli tool to generate your CSS files on the fly. If you enable **AUTOGENERATE_TAILWIND_CSS**, Flask++ will automatically compile every **tailwind_raw.css** file inside your apps or modules **static/css** folder and create a corresponding **tailwind.css** asset, when you run `app.start()`. -And if you are using **FPP_PROCESSING**, you can integrate the generated assets into your section like this (assuming -you did not overwrite its default context_processor): +And if you are using **FPP_PROCESSING**, you can integrate the generated assets into your section like this (assuming you did not overwrite its default context_processor): ```html @@ -768,26 +720,17 @@ you did not overwrite its default context_processor): {{ tailwind }} - + ``` ### FRONTEND_ENGINE -The next thing a well-balanced full-stack framework should not miss is an integrated frontend tooling. That's why Flask++ -comes with its own Node.js integration. Therefore, it uses the standalone Node bundle (if you have not installed Node globally) -and uses it to integrate Vite into your project. So if you enable **FRONTEND_ENGINE**, Flask++ will automatically plug in a -blueprint-based Frontend class when you start your app. If you run your app in debug mode, it will also automatically start -a Vite server per module, otherwise it will use the production build. +The next thing a well-balanced full-stack framework should not miss is an integrated frontend tooling. That's why Flask++ comes with its own Node.js integration. Therefore, it uses the standalone Node bundle (if you have not installed Node globally) and then uses it to integrate Vite into your project. So if you enable **FRONTEND_ENGINE**, Flask++ will automatically plug in a blueprint-based Frontend class when you start your app. If you run your app in debug mode, it will also automatically start a Vite server per module (the framework automates the request resolving for you), otherwise it will use the production build. -The frontend tooling also uses the **tailwind_raw.css** file inside the **vite/src** folder to generate the **tailwind.css** asset. -In debug mode this will be regenerated on every request. For compatibility reasons and to reduce redundancy, it uses the integrated -Tailwind cli tool as well. When you would like to use a specific vite folder to use it for standalone Vite projects, you should be -aware of that. +The frontend tooling also uses the **tailwind_raw.css** file inside the **vite/src** folder to generate the **tailwind.css** asset. In debug mode this will be regenerated on every request. For compatibility reasons and to reduce redundancy, it uses the integrated Tailwind cli tool for that as well. When you would like to use a specific vite folder to export it into standalone Vite projects, you should be aware of that. -Next is that Flask++ orchestrates the Node and Vite configuration centrally inside the framework. So you won't find dozens of -config files and node_modules in every vite folder. Currently, you cannot modify the Vite configuration manually. So for now you can -only stick with the very basics when working with our frontend tooling. Besides Tailwind, this only includes working with TypeScript. +Next is that Flask++ orchestrates the Node and Vite configuration centrally inside the framework. So you won't find dozens of config files and node_modules in every vite folder. Currently, you cannot modify the Vite configuration manually. So for now you can only stick with the very basics when working with our frontend tooling. Besides Tailwind, this only includes working with TypeScript. To integrate vite into your templates, you can use: @@ -802,15 +745,13 @@ To integrate vite into your templates, you can use: ### EXT_FST -This extension switch enables the flask_security extension. And even if there's no whole class built on top of it, there's still -some tooling we should quickly mention. We are talking about user and role mixins, with which you can expand Flask Security Toos -default mixins. It works very similarly to how we build up our Config class. So you should be careful with overwriting FSTs -default security features and utilities. +This extension switch enables the flask_security extension. And even if there's no whole class built on top of it, there's still some tooling we should quickly mention. We are talking about user and role mixins, with which you can expand Flask Security Toos default mixins. It works very similarly to how we build up our Config class. So you should be careful with overwriting FSTs default security features and utilities. To plug in your own mixins, we provide decorators that you can use inside your modules data package, for example: ```python # module_package/data/noinit_fst.py +# -> Have to be named exactly like that, so that the app can see and initialize them when it sets up FST from flaskpp.app.data.fst_base import user_mixin #, role_mixin from flaskpp.app.extensions import db @@ -818,31 +759,24 @@ from flaskpp.app.extensions import db # priority=2 # -> Like config priority, it should be a value inclusively between 1 and 10 and defaults to 1. ) -class MyUserMixin: +class MyUserMixin(db.Model): bio = db.Column(db.String(512)) # ... ``` ## Further Utilities -To make your life even easier, Flask++ provides some additional utilities and templates. You can use them to play around with -the framework or integrate them into your own projects. In this chapter we will show you how to use them and how they work, so -you can abstract parts of them into your own code base. +To make your life even easier, Flask++ provides some additional utilities and templates. You can use them to play around with the framework or integrate them into your own projects. In this chapter we will show you how to use them and how they work, so you can abstract parts of them into your own code base. ### Example Base Template -The most important file we provide is the **example_base.html** file. As long as you do not have a similar named template inside -your apps or home modules **templates** folder, you can extend it by using `{% extends "example_base.html" %}`. Flask++ uses -a ChoiceLoader which automatically falls back to the frameworks templates if the template does not exist anywhere else. +The most important file we provide is the **example_base.html** file. As long as you do not have a similar named template inside your apps or home modules **templates** folder, you can extend it by using `{% extends "example_base.html" %}`. Flask++ uses a ChoiceLoader which automatically falls back to the frameworks templates if the template does not exist anywhere else. In the following chapters, we will introduce our further utilities and show you how they are integrated into our example base template. ### Socket and Base script -Of course, we provide utilities that close the circle when using our [Flask++ features](#features). Inside the frameworks **socket.js** -file, this includes a namespace-sensitive emit and emitAsync function for default events. And for the frameworks **base.js** file, -this includes further utilities like socket i18n, flashing, safe execution, info and confirm dialogs using the frameworks modal templates -(they are integrated into our example base as well) and a socketHtmlInject function matching the [FppSockets](#ext_socket) HTML injectors. +Of course, we provide utilities that close the circle when using our [Flask++ features](#features). Inside the frameworks **socket.js** file, this includes a namespace-sensitive emit and emitAsync function for default events. And for the frameworks **base.js** file, this includes further utilities like socket i18n, flashing, safe execution, info and confirm dialogs using the frameworks modal templates (they are integrated into our example base as well) and a socketHtmlInject function matching the [FppSockets](#ext_socket) HTML injectors. Our example base template integrates them like that: @@ -850,25 +784,24 @@ Our example base template integrates them like that: {% if enabled("EXT_SOCKET") %} - - - - - - + + + + + + {% endif %} ``` -The base script also writes all utils into window.FPP so you can use them inside your vite scripts as well. -You can also use our **base_example.html** and play around with those utils inside your browser console: +The base script also writes all utils into window.FPP so you can use them inside your vite scripts as well. You can also use our **base_example.html** and play around with those utils inside your browser console: ```javascript window.FPP = { @@ -895,18 +828,21 @@ window.FPP = { ### Navigation -We also provide an auto_nav module inside `flaskpp.app.utils`, which you can use to automatically generate your navigation bar. -Here is an example of what this would look like: +We do also provide an auto_nav module inside `flaskpp.app.utils`, which you can use to automatically generate your navigation bar. Here is an example of what this would look like: ```python # Assuming you are using it inside your modules routes.py file: from flaskpp.app.utils.auto_nav import autonav_route, DropdownBuilder def init_routes(mod: Module): + + _ = mod.t + # -> If you want the message catalogue resolver of default Babel to work as well + @autonav_route( mod, "/example", - mod.t("EXAMPLE_LABEL"), + _("EXAMPLE_LABEL"), # priority=2, # -> Same priority concept; defaults to 1. # additional_classes="text-green-400 hover:bg-green-500/10" @@ -916,8 +852,8 @@ def init_routes(mod: Module): return mod.render_template("example.html") builder = DropdownBuilder( - mod.t("DROPDOWN_LABEL"), - # dropdown_priority=1, + _("DROPDOWN_LABEL"), + # dropdown_priority=2, # -> Same priority concept. # additional_dropdown_classes="text-black-400 hover:bg-black-500/10" # -> To add additional classes to the dropdown item. @@ -925,7 +861,7 @@ def init_routes(mod: Module): @builder.dropdown_route( mod, "/dropdown-item-1", - mod.t("DROPDOWN_ITEM_1_LABEL"), + _("DROPDOWN_ITEM_1_LABEL"), # same as autonav_route # ... ) @@ -957,11 +893,11 @@ This is integrated into our example base template like this: {% for nav_type, nav_data in NAV.items() %} {% if nav_type == "links" %} {% for link in nav_data %} - {% include "nav_link.html" with context %} + {% include "nav_link.html" with context %} {% endfor %} {% elif nav_type == "dropdowns" %} {% for dropdown in nav_data %} - {% include "dropdown.html" with context %} + {% include "dropdown.html" with context %} {% endfor %} {% endif %} {% endfor %} @@ -1000,19 +936,14 @@ This is integrated into our example base template like this: ### Further Sources of Truth -As we already mentioned, there is a Tailwind-based modal system as well as flashing utility that can be used with our -natively provided [script utilities](#socket-and-base-script). And besides that, we do also provide a default **404.html** -and **error.html** template as well as a basic framework-specific **tailwind_raw.css** file. +As we already mentioned, there is a Tailwind-based modal system as well as flashing utility that can be used with our natively provided [script utilities](#socket-and-base-script). And besides that, we do also provide a default **404.html** and **error.html** template as well as a basic framework-specific **tailwind_raw.css** file. -If you are interested in what they look like and how they work, we highly recommend taking a look inside the frameworks -**templates** and **static** folders. You can find them inside **src/flaskpp/app** in the -[Flask++ repository](https://github.com/GrowVolution/FlaskPlusPlus). +If you are interested in what they look like and how they work, we highly recommend taking a look inside the frameworks **templates** and **static** folders. You can find them inside **src/flaskpp/app** in the [Flask++ repository](https://github.com/GrowVolution/FlaskPlusPlus). ## Get Help -You can simply use `fpp [-h/--help]` to get an overview on how to work with the Flask++ CLI. And if you still -have questions, which haven't been answered in this documentation **feel free to join the [discussions](https://github.com/GrowVolution/FlaskPlusPlus/discussions)**. +You can simply use `fpp [-h/--help]` to get an overview on how to work with the Flask++ CLI. And if you still have questions, which haven't been answered in this documentation **feel free to join the [discussions](https://github.com/GrowVolution/FlaskPlusPlus/discussions)**. --- -**Thank you for working with Flask++** +**© GrowVolution e.V. 2025 – Thank you for working with Flask++** diff --git a/README.md b/README.md index 2e36396..20b082e 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,10 @@ # 🧪 Flask++ -Tired of setting up Flask from scratch every single time? 🤯 -With **Flask++**, you can spin up and manage multiple apps in **under two minutes**. ⚡ +Tired of setting up Flask from scratch every single time? 🤯 With **Flask++**, you can spin up and manage multiple apps in **under two minutes**. ⚡ -And most important: This is **still Flask**. You won't have to miss the feeling of developing Flask. -You've got **full control** about how much magic you would like to use and how much this framework should just feel like Flask. -Not only that: If you experience something, which doesn't feel like Flask anymore... Please feel free to raise an issue and we'll fix that for you asap. ✌🏼️ +And most important: This is **still Flask**. You won't have to miss the feeling of developing Flask. You've got **full control** about how much magic you would like to use and how much this framework should just feel like Flask. Not only that: If you experience something, which doesn't feel like Flask anymore... Please feel free to raise an issue and we'll fix that for you asap. ✌🏼️ -It comes with the most common Flask extensions pre-wired and ready to go. -Configuration is dead simple – extensions can be bound or unbound with ease. -On top of that, it features a plug-&-play style **module system**, so you can just enable or disable functionality as needed. 🎚️ +It comes with the most common Flask extensions pre-wired and ready to go. Configuration is dead simple – extensions can be bound or unbound with ease. On top of that, it features a plug-&-play style **module system**, so you can just enable or disable functionality as needed. 🎚️ --- @@ -20,7 +15,7 @@ If not already done, just install Python 3.10 or higher on your system. Then ins pip install flaskpp ```` -After that you can simply setup your app with the Flask++ CLI: +After that you can simply set up your app with the Flask++ CLI: ```bash mkdir myproject @@ -48,11 +43,9 @@ fpp run [-a/--app] myapp [-p/--port] 5000 [-d/--debug] fpp --help ``` -The setup wizard will guide you through the configuration step by step. 🎯 -Once finished, your first app will be running – in less than the time it takes to make coffee. ☕🔥 +The setup wizard will guide you through the configuration step by step. 🎯 Once finished, your first app will be running – in less than the time it takes to make coffee. ☕🔥 -**Tip:** We recommend installing Flask++ globally. If your OS does not support installing PyPI packages outside virtual environments, -you can create a workaround like this: +**Tip:** We recommend installing Flask++ globally. If your OS does not support installing PyPI packages outside virtual environments, you can create a workaround like this: ```bash sudo su @@ -91,8 +84,7 @@ newgrp shared ## 🧩 Modules -To get started with modules, you can generate basic modules using the Flask++ CLI: `fpp modules create [module_name]` -Use as a starting point for your own modules. 😉 +To get started with modules, you can generate basic modules using the Flask++ CLI: `fpp modules create [module_name]`. Use it as a starting point for your own modules. 😉 --- @@ -146,15 +138,13 @@ For further information about this framework and how to use it, you may like to ### 🌱 Let it grow -If you like this project, feel free to **fork it, open issues, or contribute ideas**. -Every improvement makes life easier for the next developer. 💚 +If you like this project, feel free to **fork it, open issues, or contribute ideas**. Every improvement makes life easier for the next developer. 💚 --- ### 📜 License -Released under the [MIT License](LICENSE). -Do whatever you want with it – open-source, commercial, or both. Follow your heart. 💯 +Released under the [MIT License](LICENSE). Do whatever you want with it – open-source, commercial, or both. Follow your heart. 💯 --- diff --git a/src/flaskpp/app/data/noinit_translations.py b/src/flaskpp/app/data/noinit_translations.py index a69957b..e077f17 100644 --- a/src/flaskpp/app/data/noinit_translations.py +++ b/src/flaskpp/app/data/noinit_translations.py @@ -1,7 +1,7 @@ import json from flaskpp.app.data import commit, _package -from flaskpp.app.data.babel import add_entry, get_entries +from flaskpp.app.data.babel import add_entry, get_entries, get_entry from flaskpp.babel import valid_state from flaskpp.utils import enabled from flaskpp.utils.debugger import log @@ -78,8 +78,11 @@ def setup_db(domain: str = "flaskpp"): _add_entries(key, domain) for entry in entries: - if _translations_en[entry.key] != entry.text: - entry.text = _translations_en[entry.key] + key = entry.key + if _translations_en[key] != entry.text: + entry.text = _translations_en[key] + entry_de = get_entry(key, "de", domain) + entry_de.text = _translations_de[key] else: log("info", f"Setting up Flask++ translations...") diff --git a/src/flaskpp/flaskpp.py b/src/flaskpp/flaskpp.py index 9e803b8..da1876f 100644 --- a/src/flaskpp/flaskpp.py +++ b/src/flaskpp/flaskpp.py @@ -80,6 +80,9 @@ def __init__(self, import_name: str): socket.init_app(self) if enabled("EXT_BABEL"): + if not ext_database: + raise RuntimeError("EXT_BABEL requires EXT_SQLALCHEMY to be enabled.") + from flaskpp.app.extensions import babel from flaskpp.app.utils.translating import set_locale babel.init_app(self) @@ -91,7 +94,7 @@ def __init__(self, import_name: str): if enabled("EXT_FST"): if not ext_database: - raise RuntimeError("For EXT_FST EXT_SQLALCHEMY extension must be enabled.") + raise RuntimeError("EXT_FST requires EXT_SQLALCHEMY to be be enabled.") from flask_security import SQLAlchemyUserDatastore from flaskpp.app.extensions import security, db