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 FormField or SelectMultipleField with 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

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:

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.