diff --git a/DOCS.md b/DOCS.md
index bb1e0e9..f64e6fe 100644
--- a/DOCS.md
+++ b/DOCS.md
@@ -1,4 +1,4 @@
-# Flask++ v0.3.x Documentation
+# Flask++ v0.4.x Documentation
## Core
@@ -35,7 +35,7 @@ SERVER_NAME = localhost
SECRET_KEY = supersecret
[database]
-DATABASE_URL = sqlite:///appdata.db
+DATABASE_URI = sqlite:///appdata.db
[redis]
REDIS_URL = redis://localhost:6379
@@ -109,7 +109,7 @@ class DefaultConfig:
# -------------------------------------------------
# Flask-SQLAlchemy & Flask-Migrate
# -------------------------------------------------
- SQLALCHEMY_DATABASE_URI = os.getenv("DATABASE_URL", "sqlite:///database.db")
+ SQLALCHEMY_DATABASE_URI = os.getenv("DATABASE_URI", "sqlite:///database.db")
SQLALCHEMY_TRACK_MODIFICATIONS = False
# -------------------------------------------------
@@ -249,7 +249,7 @@ At first, you would create a new python package inside **project_root/modules**.
#### 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 (they are not case-sensitive):
+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 two fields: "version" and "type". The following version string formats are supported (they are not case-sensitive):
```
x
@@ -276,11 +276,22 @@ A fully qualified manifest file would then look like this:
"name": "Module Name",
"description": "This module does ...",
"version": "0.1",
+ "type": "default", // can be either "default" or "base"
+ // -> If you set the module type to "base", your module can be used to extend other modules.
+ // Base modules are not allowed to be registered. Do not enable them in your app config.
"requires": {
"fpp": ">=0.3.5", // the minimum required version of Flask++
+ "packages": [ // PyPI packages that are required by your module
+ // e.g. "numpy", "pandas",
+ // ...
+
+ // packages will be installed when your module gets installed
+ ],
"modules": { // other modules that are required by your module
"module_id_01": "==0.2",
- "module_id_02": "<0.7"
+ "module_id_02": "<0.7",
+ // -> Base module ids are also allowed here.
+ // In this case the module loader will check if a parent module that extends it is enabled.
}
}
}
@@ -293,14 +304,16 @@ Inside your **\_\_init__.py** file, you create a `module: Module` variable and o
```python
from flaskpp import Module
from flaskpp.utils import enabled
+# from flaskpp.modules import import_base
from flaskpp.exceptions import ModuleError
module = Module(
__file__,
__name__,
-
- # Optional a list of required_extensions:
- [
+ # extends=import_base("base_module_id"),
+ # -> You can use base modules as extensions for your module.
+ # import_base will return None if the module is not installed or not a base module.
+ # required_extensions=[
# "sqlalchemy",
# "socket",
# "babel",
@@ -310,14 +323,19 @@ module = Module(
# "cache",
# "api",
# "jwt_extended"
- ],
-
- # And you can optionally turn off init_routes_on_enable (default is True):
- False
+ # ],
+ # -> You can set Flask++'s pre-wired extensions as required.
+ # init_routes_on_enable=False,
+ # -> You can optionally turn off automated route initialization when the module gets enabled.
+ # This could be especially useful, if you are working with socket i18n. (More in the later chapters.)
+ # allowed_for_home=False,
+ # -> You can disable the ability for your module to be the home module of the app.
+ # allow_frontend_engine=False,
+ # -> You can disable the frontend engine of your module if FRONTEND_ENGINE is set to 1.
)
# Now if you need to do stuff when your module gets enabled:
-@module.on_enable
+@module.on_enable # This hook cannot be used by base modules.
def on_enable(app: FlaskPP):
# Check for required features, for example:
if not enabled("FPP_PROCESSING"):
@@ -354,11 +372,15 @@ def init_handling(mod: Module):
for file in _package.rglob("*.py"):
if file.stem == "__init__" or file.stem.startswith("noinit"):
continue
- handler_name = file.stem
+
+ rel = file.relative_to(_package).with_suffix("")
+ handler_name = ".".join(rel.parts)
+
handler = import_module(f"{mod.import_name}.handling.{handler_name}")
handle_request = getattr(handler, "handle_request", None)
if not handle_request:
continue
+
mod.handler(handler_name)(handle_request)
```
@@ -397,7 +419,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)) and add a config function for the `fpp setup` command if you need:
```python
from flaskpp.app.config import register_config
@@ -409,6 +431,37 @@ from flaskpp.app.config import register_config
class ModuleConfig:
# TODO: Overwrite default config values or provide your own
pass
+
+# Or if you are working with base and extending modules, you might do something like this:
+# base_module_package/config.py
+
+class BaseModuleConfig:
+ # ...
+ pass
+
+# extended_module_package/config.py
+from modules.base_module_package.config import BaseModuleConfig
+
+@register_config()
+class ExtendingModuleConfig(BaseModuleConfig):
+ # ...
+ pass
+
+def module_config():
+ # return {
+ # TODO: Write required config data (will be prompted by the setup if module is set to 1)
+ # -> Base module configs will be prompted together with their extending modules (parent), when it gets enabled.
+
+ # "protected_MY_SECRET": token_hex(32),
+ # -> protected keys won't be prompted to the user
+
+ # "default_FEATURE_KEY": "Hello World!",
+ # -> default keys will be prompted with their default value shown (and written with if input left empty)
+
+ # "ADDITIONAL_DATA": "",
+ # -> simple config prompt without default value
+ # }
+ pass
```
#### Data Package
@@ -427,9 +480,16 @@ def init_models(mod: Module):
for file in _package.rglob("*.py"):
if file.stem == "__init__" or file.stem.startswith("noinit"):
continue
- import_module(f"{mod.import_name}.data.{file.stem}")
+ rel = file.relative_to(_package).with_suffix("")
+ import_module(f"{mod.import_name}.data.{".".join(rel.parts)}")
```
+This init file does not apply to base modules. Base modules can provide their own models, mixins or whatever data you would like to put into their data folder, but their data is meant to be used by their parents.
+
+#### Extracting
+
+The Module class provides a `module.extract()` function. This function is meant to be used by the `fpp modules install` command to extract the modules globals to the app's static / templates when the module gets installed. This is especially useful if your module needs to install global templates if it is not meant to be installed as a home module. To use this feature, you need to create an **extract** folder containing a **templates** and/or **static** folder inside your module package. Their contents will then be extracted to the app's static and templates folder.
+
### Working with Modules
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.
@@ -520,12 +580,13 @@ The default features of the FppSocket class are its event_context (inspired by F
```python
from flaskpp.app.extensions import socket
-from flaskpp.utils.debugger import log
+from flaskpp.utils.logging import log
+
@socket.on("my_event")
async def event(
- sid: str, # if you did not set sid_passing to False
- payload: Any
+ sid: str, # if you did not set sid_passing to False
+ payload: Any
):
# here you can access:
ctx = socket.event_context
@@ -606,7 +667,7 @@ Be aware that default events should not contain any "@" inside their names, beca
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 translation 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:
@@ -624,7 +685,10 @@ module = Module(
"sqlalchemy",
"socket",
"babel"
- ]
+ ],
+ False
+ # -> False to install translations before route setup.
+ # This is especially useful if you are using translations inside your route setup.
)
@module.on_enable
@@ -639,6 +703,9 @@ def enable(app: FlaskPP):
# -> default is module.name
) # This will register the domain_name as the modules translation domain, pass it as a variable called "DOMAIN" to the
# context processor and automatically cause _ and ngettext to primarily resolve translation keys from that domain.
+
+ # Now you can enable routes after you registered your translations
+ module.init_routes()
# If you now do something like this, for example:
from flask import render_template_string
@@ -658,8 +725,8 @@ def example():
And here is an example of what your **module_package/data/noinit_translations.py** file could look like:
```python
-from flaskpp.app.data import commit
-from flaskpp.app.data.babel import add_entry, get_entries, get_entry
+from flaskpp.app.data import commit, delete_model
+from flaskpp.app.data.babel import add_entry, get_entries
_msg_keys = [
"EXAMPLE_TITLE",
@@ -690,12 +757,15 @@ def setup_db(mod: Module):
if key not in keys:
_add_entries(key, domain)
+ from .. import data
for entry in entries:
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]
+ translations = getattr(data.noinit_translations, f"_translations_{entry.locale}", _translations_en)
+ try:
+ if translations[key] != entry.text:
+ entry.text = translations[key]
+ except KeyError:
+ delete_model(entry, False)
else:
for key in _msg_keys:
_add_entries(key, domain)
@@ -759,11 +829,29 @@ 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(db.Model):
+class MyUserMixin:
+ full_name = db.Column(db.String(64), nullable=False)
bio = db.Column(db.String(512))
# ...
```
+You can also create your own forms for FST. For that create a **forms.py** file inside your module package. If EXT_FST is set to 1, the FlaskPP class will load it automatically and like with the FST mixins create a combined form class that is passed to the `security.init_app()` function. So you can plug in FST forms like that:
+
+```python
+# module_package/forms.py
+from flaskpp.app.utils.fst import register_form #, login_form
+from flaskpp.app.utils.translating import t # this function is a promise that exists as dummy if EXT_BABEL = 0
+from wtforms import StringField, validators
+
+@register_form(
+ # priority=2
+ # -> you know the concept :)
+)
+class MyRegisterForm:
+ full_name = StringField(t("FULL_NAME_LABEL"), validators=validators.DataRequired())
+ # ...
+```
+
## 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.
diff --git a/pyproject.toml b/pyproject.toml
index 195003d..7700edb 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "flaskpp"
-version = "0.3.8"
+version = "0.4.8"
description = "A Flask based framework for fast and easy app creation. Experience the real power of Flask without boilerplate, but therefore a well balanced mix of magic and the default Flask framework."
authors = [
{ name = "Pierre", email = "pierre@growv-mail.org" }
@@ -24,9 +24,11 @@ dependencies = [
"pymysql",
"python-dotenv",
"python-socketio[asgi]",
+ "argon2_cffi",
"authlib",
"uvicorn",
"asgiref",
+ "immutables",
"requests",
"redis",
"pytz",
diff --git a/src/flaskpp/_init.py b/src/flaskpp/_init.py
index 2ce78e3..a045edf 100644
--- a/src/flaskpp/_init.py
+++ b/src/flaskpp/_init.py
@@ -1,4 +1,3 @@
-from pathlib import Path
import typer, subprocess, sys, os
from flaskpp import _fpp_root
@@ -10,12 +9,15 @@
def initialize(skip_defaults: bool, skip_babel: bool, skip_tailwind: bool, skip_node: bool, skip_vite: bool):
typer.echo(typer.style("Creating default structure...", bold=True))
- root = Path.cwd()
+ from flaskpp.cli import cwd
if not skip_defaults:
from flaskpp.utils.setup import conf_path
conf_path.mkdir(exist_ok=True)
+ from flaskpp.modules import setup_globals
+ setup_globals()
+
from flaskpp.modules import module_home
module_home.mkdir(exist_ok=True)
@@ -23,48 +25,49 @@ def initialize(skip_defaults: bool, skip_babel: bool, skip_tailwind: bool, skip_
service_path.mkdir(exist_ok=True)
- templates = root / "templates"
+ templates = cwd / "templates"
templates.mkdir(exist_ok=True)
- static = root / "static"
+ static = cwd / "static"
static.mkdir(exist_ok=True)
css = static / "css"
css.mkdir(exist_ok=True)
(static / "js").mkdir(exist_ok=True)
(static / "img").mkdir(exist_ok=True)
- with open(root / "main.py", "w") as f:
- f.write("""
-from flaskpp import FlaskPP
+ with open(cwd / "main.py", "w") as f:
+ f.write("""from flaskpp import FlaskPP
+from flask import render_template
def create_app():
app = FlaskPP(__name__)
-
+
+ app.add_app_url_rule(
+ "/", endpoint="index",
+ view_func=lambda: render_template("app/index.html")
+ )
+
# TODO: Extend the Flask++ default setup with your own factory
return app
if __name__ == "__main__":
app = create_app()
- app.start()
- """)
+ app.start()""")
- (templates / "index.html").write_text("""
-{% extends "base_example.html" %}
+ (templates / "index.html").write_text("""{% extends "base_example.html" %}
{# The base template is natively provided by Flask++. #}
{% block title %}{{ _('Home') }}{% endblock %}
+
+{% block head %}{{ tailwind_main }}{% endblock %}
+
{% block content %}
-
-
{{ _('My new Flask++ Project') }}
-
- {{ _('This is my brand new, super cool project.') }}
-
-
+
+
{{ _('My new Flask++ Project') }}
+
{{ _('This is my brand new, super cool project.') }}
-{% endblock %}
- """)
+{% endblock %}""")
- (css / "tailwind_raw.css").write_text("""
-@import "tailwindcss" source("../../");
+ (css / "tailwind_raw.css").write_text("""@import "tailwindcss" source("../../");
@source not "../../.venv";
@source not "../../venv";
@@ -74,13 +77,27 @@ def create_app():
@theme {
/* ... */
-}
- """)
+}""")
+
+ (cwd / ".gitignore").write_text("""[folders]
+__pycache__/
+app_configs/
+services/
+modules/
+instance/
+migrations/
+translations/
+dist/
+logs/
+
+[files]
+messages.pot
+tailwind.css""")
if not skip_babel:
typer.echo(typer.style("Generating default translations...", bold=True))
- translations = root / "translations"
+ translations = cwd / "translations"
translations.mkdir(exist_ok=True)
pot = "messages.pot"
diff --git a/src/flaskpp/app/__init__.py b/src/flaskpp/app/__init__.py
new file mode 100644
index 0000000..1b17fa5
--- /dev/null
+++ b/src/flaskpp/app/__init__.py
@@ -0,0 +1,7 @@
+from flask import Blueprint
+
+
+class App(Blueprint):
+ def __init__(self, import_name: str):
+ super().__init__("app", import_name)
+ self.name = ""
diff --git a/src/flaskpp/app/config/__init__.py b/src/flaskpp/app/config/__init__.py
index e37a707..d264200 100644
--- a/src/flaskpp/app/config/__init__.py
+++ b/src/flaskpp/app/config/__init__.py
@@ -2,8 +2,9 @@
from importlib import import_module
from typing import Callable, TYPE_CHECKING
+from flaskpp.modules import installed_modules
from flaskpp.app.config.default import DefaultConfig
-from flaskpp.utils import check_priority, build_sorted_tuple
+from flaskpp.utils import check_priority, build_sorted_tuple, enabled
if TYPE_CHECKING:
from flaskpp import FlaskPP
@@ -20,13 +21,15 @@ def init_configs(app: "FlaskPP"):
if not modules.exists() or not modules.is_dir():
return
- for module in modules.iterdir():
- if not module.is_dir():
+ for module_info in installed_modules(modules):
+ m, _, p = module_info
+ if not enabled(m):
continue
- config = module / "config.py"
- if config.exists():
- import_module(f"modules.{module.name}.config")
+ try:
+ import_module(f"modules.{p}.config")
+ except ModuleNotFoundError:
+ pass
def register_config(priority: int = 1) -> Callable:
diff --git a/src/flaskpp/app/config/default.py b/src/flaskpp/app/config/default.py
index b70db4e..92a7893 100644
--- a/src/flaskpp/app/config/default.py
+++ b/src/flaskpp/app/config/default.py
@@ -1,5 +1,7 @@
import os
+from flaskpp.utils import enabled
+
class DefaultConfig:
SERVER_NAME = os.getenv("SERVER_NAME")
@@ -11,7 +13,7 @@ class DefaultConfig:
PROXY_FIX = False
PROXY_COUNT = 1
- SQLALCHEMY_DATABASE_URI = os.getenv("DATABASE_URL", "sqlite:///database.db")
+ SQLALCHEMY_DATABASE_URI = os.getenv("DATABASE_URI", "sqlite:///database.db")
SQLALCHEMY_TRACK_MODIFICATIONS = False
RATELIMIT_ENABLED = True
@@ -37,8 +39,8 @@ class DefaultConfig:
MAIL_SERVER = os.getenv("MAIL_SERVER", "localhost")
MAIL_PORT = int(os.getenv("MAIL_PORT", 25))
- MAIL_USE_TLS = True
- MAIL_USE_SSL = False
+ MAIL_USE_TLS = enabled("MAIL_USE_TLS")
+ MAIL_USE_SSL = enabled("MAIL_USE_SSL")
MAIL_USERNAME = os.getenv("MAIL_USERNAME")
MAIL_PASSWORD = os.getenv("MAIL_PASSWORD")
MAIL_DEFAULT_SENDER = os.getenv("MAIL_DEFAULT_SENDER", "noreply@example.com")
diff --git a/src/flaskpp/app/data/__init__.py b/src/flaskpp/app/data/__init__.py
index 4ae6cb7..6ba1f24 100644
--- a/src/flaskpp/app/data/__init__.py
+++ b/src/flaskpp/app/data/__init__.py
@@ -4,7 +4,9 @@
from typing import TYPE_CHECKING
import os
-from flaskpp.utils.debugger import log
+from flaskpp.modules import installed_modules
+from flaskpp.utils import enabled
+from flaskpp.utils.logging import debug
if TYPE_CHECKING:
from flask import Flask
@@ -14,12 +16,29 @@
_package = Path(__file__).parent
-def init_models():
+def init_models(app: "FlaskPP"):
for file in _package.rglob("*.py"):
if file.stem == "__init__" or file.stem.startswith("noinit"):
continue
import_module(f"flaskpp.app.data.{file.stem}")
+ modules = Path(app.root_path) / "modules"
+ if not modules.exists() or not modules.is_dir():
+ return
+
+ for module_info in installed_modules(modules, False):
+ m, _, p = module_info
+ if not enabled(m):
+ continue
+
+ try:
+ mod = import_module(f"modules.{p}")
+ module = getattr(mod, "module", None)
+ if module and not module.is_base:
+ module.init_models()
+ except ModuleNotFoundError:
+ pass
+
def commit():
from ..extensions import db
@@ -70,4 +89,4 @@ def _fix_missing(migrations: str):
content = f"{import_str}\n{content}"
with open(latest_file, "w", encoding="utf-8") as f:
f.write(content)
- log("migrate", f"Fixed missing flask_security import in {latest_file}")
+ debug(f"[MIGRATE] Fixed missing flask_security import in {latest_file}")
diff --git a/src/flaskpp/app/data/fst_base.py b/src/flaskpp/app/data/fst.py
similarity index 75%
rename from src/flaskpp/app/data/fst_base.py
rename to src/flaskpp/app/data/fst.py
index 3eee996..4704f29 100644
--- a/src/flaskpp/app/data/fst_base.py
+++ b/src/flaskpp/app/data/fst.py
@@ -4,7 +4,8 @@
from typing import Callable, TYPE_CHECKING
import inspect
-from flaskpp.utils import check_priority, build_sorted_tuple
+from flaskpp.modules import installed_modules
+from flaskpp.utils import check_priority, build_sorted_tuple, enabled
from flaskpp.app.extensions import db
if TYPE_CHECKING:
@@ -27,13 +28,15 @@ def init_mixins(app: "FlaskPP"):
if not modules.exists() or not modules.is_dir():
return
- for module in modules.iterdir():
- if not module.is_dir():
+ for module_info in installed_modules(modules, False):
+ m, _, p = module_info
+ if not enabled(m):
continue
- fst_data = module / "data" / "noinit_fst.py"
- if fst_data.exists():
- import_module(f"modules.{module.name}.data.noinit_fst")
+ try:
+ import_module(f"modules.{p}.data.noinit_fst")
+ except ModuleNotFoundError:
+ pass
def user_mixin(priority: int = 1) -> Callable:
@@ -79,15 +82,9 @@ def build_role_model() -> type:
return type(
"Role",
- bases + (db.Model, fsqla.FsRoleMixin),
+ bases + (db.Model, fsqla.FsRoleMixinV2),
{}
)
-
-user_roles = db.Table(
- "user_roles",
- db.Column("user_id", db.Integer, db.ForeignKey("user.id"), primary_key=True),
- db.Column("role_id", db.Integer, db.ForeignKey("role.id"), primary_key=True)
-)
-
-fsqla.FsModels.set_db_info(db)
+if enabled("EXT_FST"):
+ fsqla.FsModels.set_db_info(db)
diff --git a/src/flaskpp/app/data/noinit_translations.py b/src/flaskpp/app/data/noinit_translations.py
index e077f17..cb0d501 100644
--- a/src/flaskpp/app/data/noinit_translations.py
+++ b/src/flaskpp/app/data/noinit_translations.py
@@ -1,12 +1,11 @@
-import json
-
-from flaskpp.app.data import commit, _package
-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
+from flaskpp.app.utils.i18n import update_translations
from flaskpp.exceptions import I18nError
+
+translations: dict[str, dict[str, str]] = {}
+
_msg_keys = [
"NAV_BRAND",
"NOT_FOUND_TITLE",
@@ -20,10 +19,12 @@
"NO",
"HINT",
"UNDERSTOOD",
+ "FORBIDDEN_TITLE",
+ "FORBIDDEN_MSG",
]
-_translations_en = {
+translations["en"] = {
_msg_keys[0]: "My Flask++ App",
_msg_keys[1]: "Not Found",
_msg_keys[2]: "We are sorry, but the requested page doesn't exist.",
@@ -36,10 +37,12 @@
_msg_keys[9]: "No",
_msg_keys[10]: "Hint",
_msg_keys[11]: "Understood",
+ _msg_keys[12]: "Access Denied",
+ _msg_keys[13]: "You are not authorized to access this page.",
}
-_translations_de = {
+translations["de"] = {
_msg_keys[0]: "Meine Flask++ App",
_msg_keys[1]: "Nicht Gefunden",
_msg_keys[2]: "Wir konnten die angefragte Seite leider nicht finden.",
@@ -52,58 +55,16 @@
_msg_keys[9]: "Nein",
_msg_keys[10]: "Hinweis",
_msg_keys[11]: "Verstanden",
+ _msg_keys[12]: "Zugriff Verweigert",
+ _msg_keys[13]: "Du bist nicht berechtigt auf diese Seite zuzugreifen."
}
-def _add_entries(key: str, domain: str):
- add_entry("en", key, _translations_en[key], domain, False)
- add_entry("de", key, _translations_de[key], domain, False)
-
-
def setup_db(domain: str = "flaskpp"):
if not (enabled("EXT_BABEL") and enabled("EXT_SQLALCHEMY")):
raise I18nError("To setup Flask++ base translations, you must enable EXT_BABEL and EXT_SQLALCHEMY.")
state = valid_state()
state.fpp_fallback_domain = domain
- entries = get_entries(domain=domain, locale="en")
-
- if entries:
- log("info", f"Updating Flask++ base translations...")
-
- keys = [e.key for e in entries]
- for key in _msg_keys:
- if key not in keys:
- _add_entries(key, domain)
-
- for entry in entries:
- 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...")
-
- for key in _msg_keys:
- _add_entries(key, domain)
-
- commit()
-
-
-def get_locale_data(locale: str) -> tuple[str, str]:
- if len(locale) != 2 and len(locale) != 5 or len(locale) == 5 and "_" not in locale:
- raise I18nError(f"Invalid locale code: {locale}")
-
- if "_" in locale:
- locale = locale.split("_")[0]
-
- try:
- locale_data = json.loads((_package / "locales.json").read_text())
- except json.JSONDecodeError:
- raise I18nError("Failed to parse locales.json")
-
- flags = locale_data.get("flags", {})
- names = locale_data.get("names", {})
- return flags.get(locale, "🇬🇧"), names.get(locale, "English")
+ update_translations("Flask++", __name__, domain)
diff --git a/src/flaskpp/app/static/css/tailwind_raw.css b/src/flaskpp/app/static/css/tailwind_raw.css
index f76c0ef..0cd70f0 100644
--- a/src/flaskpp/app/static/css/tailwind_raw.css
+++ b/src/flaskpp/app/static/css/tailwind_raw.css
@@ -44,7 +44,7 @@
}
.nav-link {
- @apply block rounded-md px-3 max-md:py-2 transition w-full;
+ @apply block rounded-md px-3 max-md:py-2 transition w-full md:w-fit whitespace-nowrap;
}
.nav-link.active {
diff --git a/src/flaskpp/app/static/js/base.js b/src/flaskpp/app/static/js/base.js
index 8d4bf00..961e01c 100644
--- a/src/flaskpp/app/static/js/base.js
+++ b/src/flaskpp/app/static/js/base.js
@@ -215,6 +215,8 @@ socket.on('error', async (message) => {
});
+export const initializedEvent = new Event("FPPBaseInitialized");
+
window.FPP = {
showModal: showModal,
hideModal: hideModal,
@@ -234,8 +236,9 @@ window.FPP = {
socket: socket,
emit: emit,
emitAsync: emitAsync,
-}
+ initializedEvent: initializedEvent
+}
document.addEventListener("DOMContentLoaded", () => {
document.querySelectorAll(".modal").forEach(modal => {
@@ -243,4 +246,6 @@ document.addEventListener("DOMContentLoaded", () => {
hideModal(modal);
bindModalCloseEvents(modal);
});
-});
\ No newline at end of file
+
+ document.dispatchEvent(initializedEvent);
+});
diff --git a/src/flaskpp/app/templates/403.html b/src/flaskpp/app/templates/403.html
new file mode 100644
index 0000000..9ad1d60
--- /dev/null
+++ b/src/flaskpp/app/templates/403.html
@@ -0,0 +1,18 @@
+{% extends "base_error.html" %}
+
+{% block error %}
+
+ 🚫 {{ _('FORBIDDEN_TITLE') }}
+
+
+
+ {{ _("FORBIDDEN_MSG") }}
+
+
+
+ {{ _("BACK_HOME") }}
+
+{% endblock %}
\ No newline at end of file
diff --git a/src/flaskpp/app/templates/404.html b/src/flaskpp/app/templates/404.html
index d106d90..54937ee 100644
--- a/src/flaskpp/app/templates/404.html
+++ b/src/flaskpp/app/templates/404.html
@@ -1,19 +1,18 @@
-{% extends "base_example.html" %}
-{% block title %}{{ _('NOT_FOUND_TITLE') }}{% endblock %}
-{% block content %}
-