Skip to content

Commit

Permalink
feat: add flash plugin (#3145)
Browse files Browse the repository at this point in the history
* fix minijinja.py to mimic what we do on other templates so that any regitered callable can access the scope ie _transform_state is in the wrong spot

* added a flash function and its pending get_flashes one to be used in templates

* can't use StrEnum

* can't use StrEnum in tests as well

* remove StrEnum entirely

* enum not iterable ?

* silence mypy on this one, python/mypy#16936 related ?

* same

* TIL about ScopeState

* Silence another mypy error that works in test_temmplate...

* semi-ugly "fix"

* another way of looking at minjinja implementation

* Yes I debug with print sometimes

* still fails because get_flashes is not decorted properly in minijinja implementation, there should be a way to detect

* another "working" option, no preference vs other options

* another "working" option, no preference vs other options

* Removed the transformed kwrd, would be breaking probably

* Removed debug

* Removed docstring leftover

* Mypy

* Demonstrated superior-mypy-foo-skills by using type ignore

* Removed FLashCategory from plugin, shouldn't the app_init get it passed in the config ??

* docs: add plugin docs for flash messages

* docs: add plugin docs for flash messages

* Update docs/usage/index.rst

* fix: update file path

* fix: make sphinx gods happy

* docs: add more documentation

* docs: move api doc plugins into directory

* revert pdm.lock to it's original state

* living the dangerous life of not testing code

* typo

---------

Co-authored-by: Janek Nouvertné <25355197+provinzkraut@users.noreply.github.com>
Co-authored-by: Jacob Coffee <jacob@z7x.org>
  • Loading branch information
3 people committed Mar 14, 2024
1 parent 02f107d commit 817f518
Show file tree
Hide file tree
Showing 17 changed files with 316 additions and 7 deletions.
Empty file.
9 changes: 9 additions & 0 deletions docs/examples/plugins/flash_messages/jinja.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from litestar import Litestar
from litestar.contrib.jinja import JinjaTemplateEngine
from litestar.plugins.flash import FlashConfig, FlashPlugin
from litestar.template.config import TemplateConfig

template_config = TemplateConfig(engine=JinjaTemplateEngine, directory="templates")
flash_plugin = FlashPlugin(config=FlashConfig(template_config=template_config))

app = Litestar(plugins=[flash_plugin])
9 changes: 9 additions & 0 deletions docs/examples/plugins/flash_messages/mako.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from litestar import Litestar
from litestar.contrib.mako import MakoTemplateEngine
from litestar.plugins.flash import FlashConfig, FlashPlugin
from litestar.template.config import TemplateConfig

template_config = TemplateConfig(engine=MakoTemplateEngine, directory="templates")
flash_plugin = FlashPlugin(config=FlashConfig(template_config=template_config))

app = Litestar(plugins=[flash_plugin])
9 changes: 9 additions & 0 deletions docs/examples/plugins/flash_messages/minijinja.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from litestar import Litestar
from litestar.contrib.minijinja import MiniJinjaTemplateEngine
from litestar.plugins.flash import FlashConfig, FlashPlugin
from litestar.template.config import TemplateConfig

template_config = TemplateConfig(engine=MiniJinjaTemplateEngine, directory="templates")
flash_plugin = FlashPlugin(config=FlashConfig(template_config=template_config))

app = Litestar(plugins=[flash_plugin])
26 changes: 26 additions & 0 deletions docs/examples/plugins/flash_messages/usage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from litestar import Litestar, Request, get
from litestar.contrib.jinja import JinjaTemplateEngine
from litestar.plugins.flash import FlashConfig, FlashPlugin, flash
from litestar.response import Template
from litestar.template.config import TemplateConfig

template_config = TemplateConfig(engine=JinjaTemplateEngine, directory="templates")
flash_plugin = FlashPlugin(config=FlashConfig(template_config=template_config))


@get()
async def index(request: Request) -> Template:
"""Example of adding and displaying a flash message."""
flash(request, "Oh no! I've been flashed!", category="error")

return Template(
template_str="""
<h1>Flash Message Example</h1>
{% for message in get_flashes() %}
<p>{{ message.message }} (Category:{{ message.category }})</p>
{% endfor %}
"""
)


app = Litestar(plugins=[flash_plugin], route_handlers=[index], template_config=template_config)
2 changes: 1 addition & 1 deletion docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ Litestar library documentation

Litestar is a powerful, flexible, highly performant, and opinionated ASGI framework.

The Litestar framework supports :doc:`/usage/plugins`, ships
The Litestar framework supports :doc:`/usage/plugins/index`, ships
with :doc:`dependency injection </usage/dependency-injection>`, :doc:`security primitives </usage/security/index>`,
:doc:`OpenAPI schema generation </usage/openapi>`, `MessagePack <https://msgpack.org/>`_,
:doc:`middlewares </usage/middleware/index>`, a great :doc:`CLI </usage/cli>` experience, and much more.
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ API reference
openapi/index
pagination
params
plugins
plugins/index
repository/index
response/index
router
Expand Down
7 changes: 7 additions & 0 deletions docs/reference/plugins/flash_messages.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
=====
flash
=====


.. automodule:: litestar.plugins.flash
:members:
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
=======
plugins
=======


.. automodule:: litestar.plugins
:members:

.. toctree::
:maxdepth: 1

flash_messages
2 changes: 1 addition & 1 deletion docs/usage/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Usage
metrics/index
middleware/index
openapi
plugins
plugins/index
responses
security/index
static-files
Expand Down
73 changes: 73 additions & 0 deletions docs/usage/plugins/flash_messages.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
==============
Flash Messages
==============

.. versionadded:: 2.7.0

Flash messages are a powerful tool for conveying information to the user,
such as success notifications, warnings, or errors through one-time messages alongside a response due
to some kind of user action.

They are typically used to display a message on the next page load and are a great way
to enhance user experience by providing immediate feedback on their actions from things like form submissions.

Registering the plugin
----------------------

The FlashPlugin can be easily integrated with different templating engines.
Below are examples of how to register the ``FlashPlugin`` with ``Jinja2``, ``Mako``, and ``MiniJinja`` templating engines.

.. tab-set::

.. tab-item:: Jinja2
:sync: jinja

.. literalinclude:: /examples/plugins/flash_messages/jinja.py
:language: python
:caption: Registering the flash message plugin using the Jinja2 templating engine

.. tab-item:: Mako
:sync: mako

.. literalinclude:: /examples/plugins/flash_messages/mako.py
:language: python
:caption: Registering the flash message plugin using the Mako templating engine

.. tab-item:: MiniJinja
:sync: minijinja

.. literalinclude:: /examples/plugins/flash_messages/minijinja.py
:language: python
:caption: Registering the flash message plugin using the MiniJinja templating engine

Using the plugin
----------------

After registering the FlashPlugin with your application, you can start using it to add and display
flash messages within your application routes.

Here is an example showing how to use the FlashPlugin with the Jinja2 templating engine to display flash messages.
The same approach applies to Mako and MiniJinja engines as well.

.. literalinclude:: /examples/plugins/flash_messages/usage.py
:language: python
:caption: Using the flash message plugin with Jinja2 templating engine to display flash messages

Breakdown
+++++++++

#. Here we import the requires classes and functions from the Litestar package and related plugins.
#. We then create our ``TemplateConfig`` and ``FlashConfig`` instances, each setting up the configuration for
the template engine and flash messages, respectively.
#. A single route handler named ``index`` is defined using the ``@get()`` decorator.

* Within this handler, the ``flash`` function is called to add a new flash message.
This message is stored in the request's context, making it accessible to the template engine for rendering in the response.
* The function returns a ``Template`` instance, where ``template_str``
(read more about :ref:`template strings <usage/templating:template files vs. strings>`)
contains inline HTML and Jinja2 template code.
This template dynamically displays any flash messages by iterating over them with a Jinja2 for loop.
Each message is wrapped in a paragraph (``<p>``) tag, showing the message content and its category.

#. Finally, a ``Litestar`` application instance is created, specifying the ``flash_plugin`` and ``index`` route handler in its configuration.
The application is also configured with the ``template_config``, which includes the ``Jinja2`` templating engine and the path to the templates directory.
8 changes: 7 additions & 1 deletion docs/usage/plugins.rst → docs/usage/plugins/index.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
=======
Plugins
=======

Expand Down Expand Up @@ -84,7 +85,7 @@ Example
The following example shows the actual implementation of the ``SerializationPluginProtocol`` for
`SQLAlchemy <https://www.sqlalchemy.org/>`_ models that is is provided in ``advanced_alchemy``.

.. literalinclude:: ../../litestar/contrib/sqlalchemy/plugins/serialization.py
.. literalinclude:: ../../../litestar/contrib/sqlalchemy/plugins/serialization.py
:language: python
:caption: ``SerializationPluginProtocol`` implementation example

Expand Down Expand Up @@ -123,3 +124,8 @@ signature (their :func:`__init__` method).
.. literalinclude:: /examples/plugins/di_plugin.py
:language: python
:caption: Dynamically generating signature information for a custom type

.. toctree::
:titlesonly:

flash_messages
2 changes: 1 addition & 1 deletion docs/usage/requests.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ The type of ``data`` an be any supported type, including
* :class:`TypedDicts <typing.TypedDict>`
* Pydantic models
* Arbitrary stdlib types
* Typed supported via :doc:`plugins </usage/plugins>`
* Typed supported via :doc:`plugins </usage/plugins/index>`

.. literalinclude:: /examples/request_data/request_data_2.py
:language: python
Expand Down
10 changes: 9 additions & 1 deletion litestar/contrib/minijinja.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,9 @@ def get_template(self, template_name: str) -> MiniJinjaTemplate:
return MiniJinjaTemplate(self.engine, template_name)

def register_template_callable(
self, key: str, template_callable: TemplateCallableType[StateProtocol, P, T]
self,
key: str,
template_callable: TemplateCallableType[StateProtocol, P, T],
) -> None:
"""Register a callable on the template engine.
Expand All @@ -172,6 +174,12 @@ def register_template_callable(
Returns:
None
"""

def is_decorated(func: Callable) -> bool:
return hasattr(func, "__wrapped__") or func.__name__ not in globals()

if not is_decorated(template_callable):
template_callable = _transform_state(template_callable) # type: ignore[arg-type] # pragma: no cover
self.engine.add_global(key, pass_state(template_callable))

def render_string(self, template_string: str, context: Mapping[str, Any]) -> str:
Expand Down
74 changes: 74 additions & 0 deletions litestar/plugins/flash.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
"""Plugin for creating and retrieving flash messages."""
from dataclasses import dataclass
from typing import Any, Mapping

from litestar.config.app import AppConfig
from litestar.connection import ASGIConnection
from litestar.contrib.minijinja import MiniJinjaTemplateEngine
from litestar.plugins import InitPluginProtocol
from litestar.template import TemplateConfig
from litestar.template.base import _get_request_from_context
from litestar.utils.scope.state import ScopeState


@dataclass
class FlashConfig:
"""Configuration for Flash messages."""

template_config: TemplateConfig


class FlashPlugin(InitPluginProtocol):
"""Flash messages Plugin."""

def __init__(self, config: FlashConfig):
"""Initialize the plugin.
Args:
config: Configuration for flash messages, including the template engine instance.
"""
self.config = config

def on_app_init(self, app_config: AppConfig) -> AppConfig:
"""Register the message callable on the template engine instance.
Args:
app_config: The application configuration.
Returns:
The application configuration with the message callable registered.
"""
if isinstance(self.config.template_config.engine_instance, MiniJinjaTemplateEngine):
from litestar.contrib.minijinja import _transform_state

self.config.template_config.engine_instance.register_template_callable(
"get_flashes", _transform_state(get_flashes)
)
else:
self.config.template_config.engine_instance.register_template_callable("get_flashes", get_flashes)
return app_config


def flash(connection: ASGIConnection, message: str, category: str) -> None:
"""Add a flash message to the request scope.
Args:
connection: The connection instance.
message: The message to flash.
category: The category of the message.
"""
scope_state = ScopeState.from_scope(connection.scope)
scope_state.flash_messages.append({"message": message, "category": category})


def get_flashes(context: Mapping[str, Any]) -> Any:
"""Get flash messages from the request scope, if any.
Args:
context: The context dictionary.
Returns:
The flash messages, if any.
"""
scope_state = ScopeState.from_scope(_get_request_from_context(context).scope)
return scope_state.flash_messages
3 changes: 3 additions & 0 deletions litestar/utils/scope/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ class ScopeState:
"csrf_token",
"dependency_cache",
"do_cache",
"flash_messages",
"form",
"headers",
"is_cached",
Expand All @@ -56,6 +57,7 @@ def __init__(self) -> None:
self.dependency_cache = Empty
self.do_cache = Empty
self.form = Empty
self.flash_messages = []
self.headers = Empty
self.is_cached = Empty
self.json = Empty
Expand All @@ -76,6 +78,7 @@ def __init__(self) -> None:
dependency_cache: dict[str, Any] | EmptyType
do_cache: bool | EmptyType
form: dict[str, str | list[str]] | EmptyType
flash_messages: list[dict[str, str]]
headers: Headers | EmptyType
is_cached: bool | EmptyType
json: Any | EmptyType
Expand Down

0 comments on commit 817f518

Please sign in to comment.