Getting Started¶
Installation¶
FeretUI needs some dependencies system:
sudo npm install -g less
Install released versions of FeretUI from the Python package index with pip or a similar tool:
pip install feretui (Not ready yet)
Installation via source distribution is via the pyproject.toml script:
pip install .
Installation will add the feretui commands to the environment.
Note
FeretUI use Python version >= 3.10
Install your favorite web server¶
FeretUI come without any web server. The web serving is not this job.
The routes to serve FeretUI¶
We need 3 routes :
[GET] client route
[GET] static files route
[GET, POST, PATCH, PUT, DELETE] action route.
Note
The GET, POST, DELETE is the minimum method to use the actions. The other is to add only if you need custom form with theses methods.
Request, Response and Session¶
The objects Request, Response and Session are wrapper class. The goal is to séparate and link the three object come from web server to FeretUI.
FeretUI can not know all the web server and can not be adapted for each. We need a link between them. The responsability to do the link is yours.
Some exemple exist and can copy it or adapt it for your projects.
Client route¶
The client route is the route that return the FeretUI client. the route (name and url) is free.
The route should
session = ... # get the feretui session
myferet = ... # get the instance client
request = Request(session=session, ...) # get the feretui request
response = myferet.render(request) # get the client page
# return the response formated for the web server
static files route¶
The static files route is the route that return the javascript, css, font, images.
the url is /feretui base url/static/filepath
The route should
filepath = ... # get the file path
myferet = ... # get the instance client
return myferet.get_static_file_path(filepath) # return the static file
Action route¶
The action route is used to do action on and with the FeretUI client.
render page
execute a page
…
the url is /feretui base url/static/action
The route should
action = ... # get the action to call
session = ... # get the feretui session
myferet = ... # get the instance client
request = Request(session=session, ...) # get the feretui request
response = myferet.execute_action(request, action) # execute the action
# return the response formated for the web server
Serve FeretUI with bottle¶
Bottle is a fast, simple and lightweight WSGI micro web-framework for Python. It is distributed as a single file module and has no dependencies other than the Python Standard Library.
See the bottle documentation.
For this example you need to install some additional package
pip install bottle BottleSessions
With feretui helper
import logging
from bottle import app, run
from BottleSessions import BottleSessions
from feretui import FeretUI, Request
from feretui.ext.bottle import declare_routes_for_feretui_client
logging.basicConfig(level=logging.DEBUG)
myferet = FeretUI()
# Here define your feretui stuff.
declare_routes_for_feretui_client(myferet)
if __name__ == "__main__":
app = app()
cache_config = {
'cache_type': 'FileSystem',
'cache_dir': './sess_dir',
'threshold': 2000,
}
BottleSessions(
app, session_backing=cache_config, session_cookie='appcookie')
run(host="localhost", port=8080, debug=True, reloader=1)
Without feretui helper
import logging
from contextlib import contextmanager
from os import path
from bottle import abort, app, request, response, route, run, static_file
from BottleSessions import BottleSessions
from multidict import MultiDict
from feretui import FeretUI, Request, Session
logging.basicConfig(level=logging.DEBUG)
@contextmanager
def feretui_session(cls):
session = None
try:
session = cls(**request.session)
yield session
finally:
if session:
request.session.update(session.to_dict())
def add_response_headers(headers) -> None:
for k, v in headers.items():
response.set_header(k, v)
myferet = FeretUI()
# Here define your feretui stuff.
@route('/')
def index():
with feretui_session(Session) as session:
frequest = Request(
method=Request.GET,
querystring=request.query_string,
headers=dict(request.headers),
session=session,
)
res = myferet.render(frequest)
add_response_headers(res.headers)
return res.body
@route('/feretui/static/<filepath:path>')
def feretui_static_file(filepath):
filepath = myferet.get_static_file_path(filepath)
if filepath:
root, name = path.split(filepath)
return static_file(name, root)
abort(404)
@route('/feretui/action/<action>', method=['DELETE', 'GET', 'POST'])
def call_action(action):
with feretui_session(Session) as session:
frequest = Request(
method=getattr(Request, request.method),
querystring=request.query_string,
form=MultiDict(request.forms),
params=MultiDict(request.params),
headers=dict(request.headers),
session=session,
)
res = myferet.execute_action(frequest, action)
add_response_headers(res.headers)
return res.body
if __name__ == "__main__":
app = app()
cache_config = {
'cache_type': 'FileSystem',
'cache_dir': './sess_dir',
'threshold': 2000,
}
BottleSessions(
app, session_backing=cache_config, session_cookie='appcookie')
run(host="localhost", port=8080, debug=True, reloader=1)
Serve FeretUI with flask¶
Flask is a lightweight WSGI web application framework. It is designed to make getting started quick and easy, with the ability to scale up to complex applications. It began as a simple wrapper around Werkzeug and Jinja, and has become one of the most popular Python web application frameworks.
See the flask documentation.
For this example you need to install some additional package
pip install flask
import logging
from wsgiref.simple_server import make_server
from flask import Flask, abort, make_response, request, send_file
from multidict import MultiDict
from feretui import FeretUI
from feretui.ext.flask import declare_routes_for_feretui_client
logging.basicConfig(level=logging.DEBUG)
app = Flask(__name__)
app.secret_key = b'secret'
myferet = FeretUI()
# Here define your feretui stuff.
declare_routes_for_feretui_client(app, myferet)
if __name__ == "__main__":
with make_server('', 8080, app) as httpd:
logging.info("Serving on port 8080...")
httpd.serve_forever()
Serve FeretUI with pyramid¶
Pyramid is a small, fast, down-to-earth, open source Python web framework. It makes real-world web application development and deployment more fun, more predictable, and more productive. Try Pyramid, browse its add-ons and documentation, and get an overview.
See the pyramid documentation.
For this example you need to install some additional package
pip install pyramid, pyramid_beaker
import logging
from contextlib import contextmanager
from wsgiref.simple_server import make_server
from multidict import MultiDict
from pyramid.config import Configurator
from pyramid.httpexceptions import exception_response
from pyramid.response import FileResponse, Response
from pyramid.view import view_config
from pyramid_beaker import session_factory_from_settings
from feretui import FeretUI, Request, Session
logging.basicConfig(level=logging.DEBUG)
@contextmanager
def feretui_session(feretui_session_cls, pyramid_session):
fsession = None
try:
fsession = feretui_session_cls(**pyramid_session)
yield fsession
finally:
if fsession:
pyramid_session.update(fsession.to_dict())
pyramid_session.save()
myferet = FeretUI()
# Here define your feretui stuff.
@view_config(route_name='feretui', request_method='GET')
def feretui(request):
with feretui_session(Session, request.session) as session:
frequest = Request(
method=Request.GET,
querystring=request.query_string,
headers=dict(request.headers),
session=session,
)
response = myferet.render(frequest)
return Response(
response.body,
headers=response.headers,
)
@view_config(route_name='feretui_static_file', request_method='GET')
def feretui_static_file(request):
filepath = myferet.get_static_file_path(
'/'.join(request.matchdict['filepath']),
)
if filepath:
return FileResponse(filepath)
raise exception_response(404)
@view_config(
route_name='call_action',
request_method=('DELETE', 'GET', 'POST'),
)
def call_action(request):
action = request.matchdict['action']
with feretui_session(Session, request.session) as session:
frequest = Request(
method=getattr(Request, request.method),
querystring=request.query_string,
form=MultiDict(request.POST),
params=request.params.dict_of_lists(),
headers=dict(request.headers),
session=session,
)
response = myferet.execute_action(frequest, action)
return Response(
response.body,
headers=response.headers,
)
if __name__ == "__main__":
session_factory = session_factory_from_settings({})
with Configurator() as config:
config.include('pyramid_beaker')
config.set_session_factory(session_factory)
config.add_route('feretui', '/')
config.add_route('feretui_static_file', '/feretui/static/*filepath')
config.add_route('call_action', '/feretui/action/{action}')
config.scan()
app = config.make_wsgi_app()
with make_server('', 8080, app) as httpd:
logging.info("Serving on port 8080...")
httpd.serve_forever()
Serve FeretUI with django¶
Django is a high-level Python web framework that encourages rapid development and clean, pragmatic design. Built by experienced developers, it takes care of much of the hassle of web development, so you can focus on writing your app without needing to reinvent the wheel. It’s free and open source.
See the django’s documentation.
For this example you need to install some additional package
pip install django
Create your app and install it.
Update the urls.py to add a new path:
from django.urls import path
from . import views
urlpatterns = [
path("", views.index, name="feretui-index"),
path("feretui/static/<filepath>", views.feretui_static_file, name="feretui-static"),
path("feretui/action/<action>", views.call_action, name="feretui-action"),
]
Updated the views.py:
from contextlib import contextmanager
from django.http import FileResponse, HttpResponse, HttpResponseNotFound
from feretui import Session, Request
from multidict import MultiDict
from .feret import myferet
class MySession(Session):
def __init__(self, **options) -> None:
options.setdefault('theme', 'minty')
options.setdefault('lang', 'fr')
super().__init__(**options)
@contextmanager
def feretui_session(request, cls):
session = None
try:
session = cls(**request.session)
yield session
finally:
if session:
request.session.update(session.to_dict())
def index(request):
with feretui_session(request, MySession) as session:
frequest = Request(
method=Request.GET,
querystring=request.GET.urlencode(),
headers=dict(request.headers),
session=session,
)
res = myferet.render(frequest)
return HttpResponse(res.body, headers=res.headers)
def feretui_static_file(request, filepath):
filepath = myferet.get_static_file_path(filepath)
if filepath:
return FileResponse(open(filepath, 'rb'))
return HttpResponseNotFound("404")
def call_action(request, action):
with feretui_session(request, MySession) as session:
params = dict()
params.update(request.GET)
params.update(request.POST)
frequest = Request(
method=getattr(Request, request.method),
querystring=request.GET.urlencode(),
form=MultiDict(request.POST),
params=params,
headers=dict(request.headers),
session=session,
)
res = myferet.execute_action(frequest, action)
return HttpResponse(res.body, headers=res.headers)
Serve FeretUI with starlette¶
Starlette is a lightweight ASGI framework/toolkit, which is ideal for building async web services in Python.
See the starlette documentation.
For this example you need to install some additional package
pip install starlette uvicorn python-multipart itsdangerous
import logging
from contextlib import contextmanager
from starlette.applications import Starlette
from starlette.responses import HTMLResponse, FileResponse, Response
from starlette.routing import Route
from starlette.middleware import Middleware
from starlette.middleware.sessions import SessionMiddleware
from multidict import MultiDict
import uvicorn
logging.basicConfig(level=logging.DEBUG)
@contextmanager
def feretui_session(request, cls):
session = None
try:
session = cls(**request.session)
yield session
finally:
if session:
request.session.update(session.to_dict())
myferet = FeretUI()
# Here define your feretui stuff.
async def index(request):
with feretui_session(request, MySession) as session:
frequest = Request(
method=Request.GET,
querystring=request.scope['query_string'].decode('utf-8'),
headers=request.headers,
session=session,
)
res = myferet.render(frequest)
return HTMLResponse(res.body, headers=res.headers)
async def feretui_static_file(request):
filepath = myferet.get_static_file_path(request.path_params['filepath'])
if filepath:
return FileResponse(filepath)
return Response('', status_code=404)
async def get_params(request):
res = {}
res.update({
key: request.query_params.getlist(key)
for key in request.query_params.keys()
if request.query_params.get(key)
})
form = await request.form()
res.update({
key: form.getlist(key)
for key in form.keys()
if form.get(key)
})
return res
async def call_action(request):
with feretui_session(request, MySession) as session:
form = await request.form()
frequest = Request(
method=getattr(Request, request.method),
querystring=request.scope['query_string'].decode('utf-8'),
form=MultiDict(form),
params=await get_params(request),
headers=request.headers,
session=session,
)
res = myferet.execute_action(frequest, request.path_params['action'])
return HTMLResponse(res.body, headers=res.headers)
if __name__ == "__main__":
app = Starlette(
debug=True,
routes=[
Route('/', index),
Route('/feretui/static/{filepath:path}', feretui_static_file),
Route(
'/feretui/action/{action:str}',
call_action,
methods=['GET', 'POST']),
],
middleware=[
Middleware(SessionMiddleware, secret_key="secret"),
],
)
uvicorn.run(app, port=8080, log_level="info")
Defined the content of your application¶
It is the FeretUI stuff for your project.
You may defined
Menu
page (with form or not)
action
resource (set of page and action for a database entries)
Page and action¶
Page¶
The page is a function who return html on a string
@myferet.register_page()
def my_page(feretui, session, options):
return "<div>My HTML.</div>"
feretui is the instance of the client
session is the feretui session
options is the querystring, because the page is only rendering in the GET call.
To display the page, rendere the feretui client with page attribute un the query string
http /?page=mypage
Javascript / CSS / picture¶
By defaut some lib are loaded and executed in the main page:
If you need some additionnal javascript, css or picture use the methods:
Note
All the statics must be on the host.
Templating¶
The template are writing in file(s) and register with feretui.feretui.FeretUI.register_template_file().
my/template/file:
<templates>
<template id="my-page">
<div>My HTML.</div>
</template>
</templates>
In the project:
myferet.register_template_file('my/template/file')
Update the page to renderer the template:
@myferet.register_page()
def my_page(feretui, session, options):
return feretui.render_template(session, 'my-page')
FeretUI use a templating compilation based on Jinja2.
my/template/file:
<templates>
<template id="my-page">
<div>Hello {{ name }}</div>
</template>
</templates>
Update the page to renderer the template:
@myferet.register_page()
def my_page(feretui, session, options):
return feretui.render_template(
session,
'my-page',
name=options.get('name', 'My feret'),
)
Warning
All the behaviours of jinja are available. The only limit is the if instruction can not be inside a node attribute.
The templating of FeretUI allow to update, include of copy an existing template. This behaviour is used to add modularity in the project.
<templates>
<template extend="my-page">
<xpath expression="//div" action="insertInside">
<include template="template id" />
</xpath>
</template>
</templates>
Warning
You can not use extend if the id does not exist and you can not use the id twice
To copy an existing template and modify the new template you
need to filled **extend** (existing id) and **id** (new id)
attributes.
The existing xpath action are:
insertInside
insertBefore
insertAfter
replace
remove
attributes
Static page¶
If you need to write a litle template without any form or control some helper can help you
Method 1:
from feretui.pages import static_page
myferet.register_page(name='my_page')(static_page('my-page'))
Method 2:
myferet.register_static_page(
'my_page',
'''
<div>My HTML.</div>
''',
)
Warning
The second method register the template in the template instance of feretui instance.
If the template id already exists then an error is raised. In this cas the method can not be overwritten.
Template directly in the register_page¶
The goal is to defined the page with the template in the same location in the project.
@myferet.register_page(
templates=['''
<template id="my-page">
<div>My HTML.</div>
</template>
'''],
)
def my_page(feretui, session, options):
return feretui.render_template(session, 'my-page')
Warning
The second method register the template in the template instance of feretui instance.
If the template id already exists then an error is raised. In this cas the method can not be overwritten.
Added form on your page¶
FeretUI implement a base class for wtforms.
from feretui import FeretUIForm
class MyForm(FeretUIForm):
...
The base class:
overwrite gettext and ngettext for the translation
overwrite the render of the field to add bulma class on the input
You use it directly in the page or the action.
@myferet.register_page(
templates=['''
<template id="my-page">
<form
hx-post="{{ feretui.base_url }}/action/my_form"
hx-swap="outerHTML"
hx-trigger="submit"
>
<div class="container content">
<h1>My form</h1>
{% for field in form %}
{{ field }}
{% endfor %}
<div class="buttons">
<button
class="button is-primary is-fullwidth"
type="submit"
>
Submit
</button>
</div>
</div>
</form>
</template>
'''],
)
def my_page(feretui, session, options):
form = option.get('form', MyForm())
return feretui.render_template(session, 'my-page', form=form)
You need to register the Form to export the translation.
Method 1:
@myferet.register_form()
class MyForm(FeretUIForm):
...
Method 2:
@myferet.register_page(
templates=['''
<template id="my-page">
<form
hx-post="{{ feretui.base_url }}/action/my_form"
hx-swap="outerHTML"
hx-trigger="submit"
>
<div class="container content">
<h1>My form</h1>
{% for field in form %}
{{ field }}
{% endfor %}
<div class="buttons">
<button
class="button is-primary is-fullwidth"
type="submit"
>
Submit
</button>
</div>
</div>
</form>
</template>
'''],
forms=[MyForm],
)
def my_page(feretui, session, options):
form = option.get('form', MyForm())
return feretui.render_template(session, 'my-page', form=form)
Visibility¶
Each page can be visible in function rules. If the condition of the visibility is not validate the a redirect to another page is done.
Some rules already exists:
By default they are no rule on the page, anybody can see them
from feretui import page_for_authenticated_user_or_goto, page_404
@myferet.register_page()
@page_for_authenticated_user_or_goto(page_404)
def my_page(feretui, session, options):
return feretui.render_template(session, 'my-page')
All the page can be choosen by the redirection, by default feretui give:
Note
The template of these pages can be overwritten. You also create and use your own page.
To create your own function to redirect:
def page_for_ ... _or_goto(
fallback_page: str | Callable,
) -> Callable:
def wrap_func(func: Callable) -> Callable:
@wraps(func)
def wrap_call(
feretui: "FeretUI",
session: Session,
options: dict,
) -> str:
if some_check_with_sesion(session):
return func(feretui, session, options)
page = fallback_page
if isinstance(fallback_page, str):
page = feretui.get_page(fallback_page)
return page(feretui, session, options)
return wrap_call
return wrap_func
The session can be overloaded and passed during the creation of the request. By default only the user attribute exist on the session.
Translation¶
The templates are always translated, No action is needed to translate them other that the standard translation of the project.
Action¶
An action is function call by the api at the url /{{ myferet.base_url }}/actions/{{ name of the action }}
from feretui import Response
@myferet.register_action
def my_action(feretui, request):
return Response(...)
Warning
The action have to return a response instance, need by the web server.
Validate the methods and the response.¶
By default the actions can be called by any http method. To filter and validate the return
you must used the feretui.helper.action_validator().
from feretui import action_validator, Response, RequestMethod
@myferet.register_action
@action_validator(methods=[RequestMethod.POST])
def my_action(feretui, request):
return Response(...)
Used form with your action¶
FeretUI implement a base class for wtforms.
from feretui import FeretUIForm
class MyForm(FeretUIForm):
...
The base class:
overwrite gettext and ngettext for the translation
overwrite the render of the field to add bulma class on the input
automatically group related fields (like
FormFieldorSelectMultipleFieldwith checkbox list) in a<fieldset>to comply with RGAA 11.5.
You use it directly in the page or the action.
from feretui import action_validator, Response, RequestMethod
@myferet.register_action
@action_validator(methods=[RequestMethod.POST])
def my_action(feretui, request):
form = MyForm(request.form)
if form.validate():
...
return Response(...)
return Response(my_page(feretui, request.session, {'form': form}))
Security¶
To protect the action and indicate if the action is callable you should use the decorator:
By default they are no rule on the action, anybody can call them
from feretui import action_validator, Response, RequestMethod, action_for_authenticated_user
@myferet.register_action
@action_validator(methods=[RequestMethod.POST])
@action_for_authenticated_user
def my_action(feretui, request):
return Response(...)
To create your own function to protect you action:
class MyActionException(ActionError):
pass
def action_for_...(func: Callable) -> Callable:
@wraps(func)
def wrapper_call(
feret: "FeretUI",
request: Response,
) -> Response:
if something_with_session(request.session):
raise MyActionException(...)
return func(feret, request)
return wrapper_call
Resource¶
The resource is a set of views to represente and manipulate the data to display and to modify.
from feretui import Resource
@myferet.register_resource()
class MyResource(Resource):
code: str = 'my-resource'
label: str = 'My resource'
...
Warning
The decorator not only register the resource in the feretui client. It also build the views in the resource.
Menus¶
To define a menu you could:
Method 1:
MyResource.menu
Method 2:
ToolBarMenu(
MyResource.label,
page="resource",
resource=MyResource.code
visible_callback=menu_for_authenticated_user,
)
The two methods give the same result.
Views¶
The views are the renders used by your application.
Existing views¶
Create / New :
feretui.resources.create.CResourceUpdate / Edit :
feretui.resources.update.UResourceDelete / Remove :
feretui.resources.delete.DResourceList + Create + Read + Update + Delete :
feretui.resources.LCRUDResource
You can update the configuration of the view in your resource:
from feretui import Resource, LResource
@myferet.register_resource()
class MyResource(LResource, Resource):
code: str = 'my-resource'
label: str = 'My resource'
class MetaViewList:
limit: int = 5 # display only 5 lines in the pagination.
Forms¶
The Forms are the base of the representation of the all views.
The declaration of the View is done in the :
Recource class
MetaView class
class MyResource(LResource, Resource):
class Form:
code = StringField()
class MetaViewList:
class Form:
label = StringField()
The Form class is a mixin not the final the Form. The MetaView’s Form’s class inherit also the Resource Form class.
In the previous example the Resource, the build form have got two fields:
code
label
The mecanism is not use full if you are only one MetaView’s type.
The build of the resource class :
transform the MetaView’s Form’s class as a Form
Add FeretUIForm in the inheritance
Declare in the FeretUI instance
Actions¶
Some views can declare actions:
The actions is declared in the MetaView’s class:
class MyResource(RResource, Resource):
class MetaViewRead:
actions = [
Actionset('Title of the set of actions', [
GotoViewAction('Title of the action', 'my_resource_method'),
]),
]
def my_resource_method(self, feretui, request, **kwargs):
...
The method called is defined on the resource the same method can be called by any view that declare the action set.
The action’s type are:
feretui.resources.actions.Action: call the methodferetui.resources.actions.GotoViewAction: call the goto main action to change pageferetui.resources.actions.SelectedRowsAction: call the method only if a row is selected. This action can be called only on a MetaView List type
Visibility and security¶
The resource take the visibility and the autorisation mecanism of the main object.
Menus¶
from feretui import Resource, menu_for_authenticated_user
@myferet.register_resource()
class MyResource(Resource):
code: str = 'my-resource'
label: str = 'My resource'
menu_visibility: Callable = staticmethod(menu_for_authenticated_user)
you also create your own method inside
from feretui import Resource
@myferet.register_resource()
class MyResource(Resource):
code: str = 'my-resource'
label: str = 'My resource'
@staticmethod
def menu_visibility(session: Session) -> bool:
return True # always displayed
Warning
You can use classmethod or static method, but not a method, because the menu is down with the class and not the instance.
Pages¶
from feretui import Resource, page_for_authenticated_user_or_goto, login
@myferet.register_resource()
class MyResource(Resource):
code: str = 'my-resource'
label: str = 'My resource'
page_visibility: Callable = staticmethod(
page_for_authenticated_user_or_goto(login))
Actions¶
from feretui import Resource, action_for_authenticated_user
@myferet.register_resource()
class MyResource(Resource):
code: str = 'my-resource'
label: str = 'My resource'
action_security: Callable = staticmethod(action_for_authenticated_user)
Create your own view’s type¶
To create a new view you need to create:
default class attribute defined in a class, this document the possibility of the configuration of your view.
Example for the list view:
class DefaultViewList: """Default value for the view list.""" label: str = None limit: int = 20 create_button_redirect_to: str = None delete_button_redirect_to: str = None do_click_on_entry_redirect_to: str = None
The class View, define the render and the actions Example for the list view:
class ListView(MultiView, LabelMixinForView, View): """List view.""" code: str = 'list' def render( self: "ListView", feretui: "FeretUI", session: Session, options: dict, ) -> str: """Render the view. :param feretui: The feretui client :type feretui: :class:`feretui.feretui.FeretUI` :param session: The Session :type session: :class:`feretui.session.Session` :param options: The options come from the body or the query string :type options: dict :return: The html page in :rtype: str. """
Th mixin class to build the view in the resource. Example for the list view:
class LResource: """LResource class.""" default_view: str = 'list' MetaViewList = DefaultViewList def build_view( self: "LResource", view_cls_name: str, ) -> Resource: """Return the view instance in fonction of the MetaView attributes. :param view_cls_name: name of the meta attribute :type view_cls_name: str :return: An instance of the view :rtype: :class:`feretui.resources.view.View` """ if view_cls_name.startswith('MetaViewList'): meta_view_cls = self.get_meta_view_class(view_cls_name) meta_view_cls.append(ListView) view_cls = type( 'ListView', tuple(meta_view_cls), {}, ) if not self.default_view: self.default_view = view_cls.code return view_cls(self) return super().build_view(view_cls_name)
The name of the MetaView should be MetaView`code`.
Translation¶
The decorateur register the forms and the templates in the client feretui. So no action is needed to translate the resource other that the standard translation of the project.
Examples¶
Example 2¶
This is an example with SQLAlchemy to manage the printer in the application.
DB model:
class Printer(Base):
__tablename__ = "device_printer"
pk: Mapped[int] = mapped_column(Integer, primary_key=True)
url: Mapped[str] = mapped_column(String(30), nullable=False)
label: Mapped[str] = mapped_column(String(20), nullable=False)
Resource:
from wtforms_components import read_only
@myferet.register_resource()
class RPrinter(LCRUDResource, Resource):
code = 'c2'
label = 'Printers'
class Form:
pk = IntegerField()
url = URLField(validators=[InputRequired()])
label = StringField(validators=[InputRequired()])
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
read_only(self.pk)
def create(self, form):
with SQLASession(engine) as session:
printer = session.get(Printer, form.pk.data)
if printer:
raise Exception('printer already exist')
printer = Printer()
form.populate_obj(printer)
session.add(printer)
session.commit()
return printer.pk
def read(self, form_cls, pk):
with SQLASession(engine) as session:
printer = session.get(Printer, pk)
if user:
return form_cls(MultiDict(printer.__dict__))
return None
def filtered_reads(self, form_cls, filters, offset, limit):
forms = []
total = 0
with SQLASession(engine) as session:
stmt = select(Printer).where()
stmt_count = select(func.count()).select_from(
stmt.subquery())
total = session.execute(stmt_count).scalars().first()
stmt = stmt.offset(offset).limit(limit)
for printer in session.scalars(stmt):
forms.append(form_cls(MultiDict(printer.__dict__)))
return {
'total': total,
'forms': forms,
}
def update(self, forms) -> None:
with SQLASession(engine) as session:
for form in forms:
printer = session.get(Printer, form.pk.data)
if printer:
form.populate_obj(printer)
session.commit()
def delete(self, pks) -> None:
with SQLASession(engine) as session:
for pk in pks:
session.delete(session.get(Printer, pk))
session.commit()
Example 2¶
This is an example with SQLAlchemy to manage the user in the application.
DB model:
class User(Base):
__tablename__ = "user_account"
login: Mapped[str] = mapped_column(
String(30), primary_key=True, nullable=False)
password: Mapped[str] = mapped_column(String(30), nullable=False)
name: Mapped[str] = mapped_column(String(20))
lang: Mapped[str] = mapped_column(String(2), default="fr")
theme: Mapped[str] = mapped_column(String(10), default="minthy")
Resource:
@myferet.register_resource()
class RUser(LCRUDResource, Resource):
code = 'c1'
label = 'User'
class Form:
login = StringField(validators=[InputRequired()])
name = StringField()
lang = RadioField(
label='Language',
choices=[('en', 'English'), ('fr', 'Français')],
validators=[InputRequired()],
render_kw={"vertical": False},
)
theme = RadioField(
choices=[
('journal', 'Journal'),
('minthy', 'Minthy'),
('darkly', 'Darkly'),
],
render_kw={"vertical": False},
)
@property
def pk(self):
return self.login
class MetaViewList:
class Form:
theme = SelectField(
choices=[
('journal', 'Journal'),
('minthy', 'Minthy'),
('darkly', 'Darkly'),
],
)
lang = None
class Filter:
lang = SelectField(choices=[('en', 'English'), ('fr', 'Français')])
class MetaViewCreate:
class Form:
password = PasswordField(validators=[Password()])
password_confirm = PasswordField(
validators=[InputRequired(), EqualTo('password')],
)
class MetaViewRead:
class Form:
theme = SelectField(
choices=[
('journal', 'Journal'),
('minthy', 'Minthy'),
('darkly', 'Darkly'),
],
)
lang = SelectField(choices=[('en', 'English'), ('fr', 'Français')])
actions = [
Actionset('Print', [
GotoViewAction('Update password', 'update_password'),
]),
]
class MetaViewUpdatePassword(DefaultViewUpdate):
code = 'update_password'
after_update_redirect_to = 'read'
cancel_button_redirect_to = 'read'
header_template = """
<h1>Update the password for {{ form.pk.data }}</h1>
"""
body_template = """
<div class="container mb-4">
{% if error %}
<div class="notification is-danger">
{{ error }}
</div>
{% endif %}
{{ form.password }}
{{ form.password_confirm }}
</div>
"""
class Form:
name = None
lang = None
theme = None
password = PasswordField(validators=[Password()])
password_confirm = PasswordField(
validators=[InputRequired(), EqualTo('password')],
)
class MetaViewDelete:
def get_label_from_pks(self, pks):
with SQLASession(engine) as session:
return [
session.get(User, pk).name
for pk in pks
]
def create(self, form):
with SQLASession(engine) as session:
user = session.get(User, form.login.data)
if user:
raise Exception('User already exist')
user = User()
form.populate_obj(user)
session.add(user)
session.commit()
return user.login
def read(self, form_cls, pk):
with SQLASession(engine) as session:
user = session.get(User, pk)
if user:
return form_cls(MultiDict(user.__dict__))
return None
def filtered_reads(self, form_cls, filters, offset, limit):
forms = []
total = 0
with SQLASession(engine) as session:
stmt = select(User).where()
for key, values in filters:
if len(values) == 1:
stmt = stmt.filter(
getattr(User, key).ilike(f'%{values[0]}%'),
)
elif len(values) > 1:
stmt = stmt.filter(getattr(User, key).in_(values))
stmt_count = select(func.count()).select_from(
stmt.subquery())
total = session.execute(stmt_count).scalars().first()
stmt = stmt.offset(offset).limit(limit)
for user in session.scalars(stmt):
forms.append(form_cls(MultiDict(user.__dict__)))
return {
'total': total,
'forms': forms,
}
def update(self, forms) -> None:
with SQLASession(engine) as session:
for form in forms:
user = session.get(User, form.pk.data)
if user:
form.populate_obj(user)
session.commit()
def delete(self, pks) -> None:
with SQLASession(engine) as session:
for pk in pks:
session.delete(session.get(User, pk))
session.commit()
Translation¶
Export the translation¶
The translation is saved in po file.
A console script exist to export the translation in the pofile template.
export-feretui-catalog --version 0.1.0 my/file.pot
All the entry with translation have addons named argument to give context. You can filter only on one of these addons.
Translate¶
to translate use poedit.
Import the translation¶
The import of the translation is done in the project and by instance. Two instance can live in the same project with two diferent translation.
Internal translations¶
myferet.load_internal_catalog('fr')
Warning
If your lang is not defined in the project, you can use the pot file save in the project.
Another translations¶
myferet.load_catalog('my/file.po', 'fr')
Install your favorite ORM¶
FeretUI come without any ORM. It is not this job, It not required to have an ORM. You can do without directly with SQL or just with full static page.
SQLAlchemy¶
This parts does not explain how to create and use a SQLAlchemy project, only how to link SQLalchemy with FeretUI.
To help you with SQLAlchemy see the documentation.
Simple case¶
The contextmanager sqlalchemy.Session create a session with the database. No need more to link them.
from sqlalchemy import create_engine
from sqlalchemy.orm import Session as SQLASession
engine = create_engine('sqlite:///mydb.db')
myferet = FeretUI()
@myferet.register_action
@action_validator(methods=[Request.Post])
def my_action(feretui, request):
with SQLASession(engine) as session:
DO stuff with the session
With a Form¶
WTForms add helper to work with SQLAlchemy see.
from ..mymodels import MyModel
from ..myforms import MyForm
@myferet.register_action
@action_validator(methods=[Request.Post])
def create_or_update(feretui, request):
with SQLASession(engine) as session:
mymodel = session.get(MyModel, request.form.pk)
if mymodel:
form = MyForm(request.form, mymodel)
else:
mymodel = MyForm()
session.add(mymodel)
form = MyForm(request.form)
if form.validate():
form.populate_obj(mymodel)
else:
raise Exception()
Note
They are not difference between action and resource.
Case with flask¶
In the Flask world, the flask_sqlalchemy’s project add helper to link perfectly Flask and Sqlalchemy, but the previous explanation works too.
If you want to do a project with any web server, use the previous explanation but if you use only flask so use the flask_sqlalchemy’s project.
from flask_sqlalchemy import SQLAlchemy
from .mymodels import MyModel
db = SQLAlchemy(...)
myferet = FeretUI()
@myferet.register_action
@action_validator(methods=[Request.Post])
def my_action(feretui, request):
mymodel = MyModel()
db.session.add(mymodel)
db.session.commit()
Case with Pyramid¶
In the Pyramid world, the zope.sqlalchemy’s project add helper to link perfectly the transaction between pyramid and Sqlalchemy, but the previous explanation works too.
If you want to do a project with any web server, use the previous explanation but if you use only pyramid so use the zope.sqlalchemy’s project.
from zope.sqlalchemy import register
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, scoped_session
from .mymodels import MyModel
engine = create_engine('sqlite:///mydb.db')
DBSession = scoped_session(sessionmaker(bind=engine))
mysession = DBSession()
register(DBSession)
myferet = FeretUI()
@myferet.register_action
@action_validator(methods=[Request.Post])
def my_action(feretui, request):
mymodel = MyModel()
mysession.add(mymodel)
Django¶
This parts does not explain how to create and use a django project, only how to link django orm with FeretUI.
To help you with SQLAlchemy see the documentation.
Simple case¶
See the ORM’s documentation to create and use Model.
from .models import MyModel
@myferet.register_action
@action_validator(methods=[Request.Post])
def my_action(feretui, request):
query = MyModel.objects.all()
With a Form¶
WTForms add helper to work with django see.
from ..models import MyModel
from ..myforms import MyForm
@myferet.register_action
@action_validator(methods=[Request.Post])
def create_or_update(feretui, request):
mymodel = MyModel.objects.get(pk=request.form.pk)
if mymodel:
form = MyForm(request.form, mymodel)
else:
mymodel = MyForm()
form = MyForm(request.form)
if form.validate():
form.populate_obj(mymodel)
mymodel.save()
else:
raise Exception()
Note
They are not difference between action and resource.
Warning
An issue exist to activate CSRF, for the moment you should stop to use the middleware
Examples¶
Some examples exist.