Fixed an issue that prevented the password reset tokens from working. Added email templates for password reset success and new account creation. Added more dynamic email template support.
220 lines
7.3 KiB
Python
220 lines
7.3 KiB
Python
from __future__ import annotations
|
|
|
|
import typing as t
|
|
|
|
from jinja2 import BaseLoader
|
|
from jinja2 import Environment as BaseEnvironment
|
|
from jinja2 import Template
|
|
from jinja2 import TemplateNotFound
|
|
|
|
from .globals import _cv_app
|
|
from .globals import _cv_request
|
|
from .globals import current_app
|
|
from .globals import request
|
|
from .helpers import stream_with_context
|
|
from .signals import before_render_template
|
|
from .signals import template_rendered
|
|
|
|
if t.TYPE_CHECKING: # pragma: no cover
|
|
from .app import Flask
|
|
from .scaffold import Scaffold
|
|
|
|
|
|
def _default_template_ctx_processor() -> dict[str, t.Any]:
|
|
"""Default template context processor. Injects `request`,
|
|
`session` and `g`.
|
|
"""
|
|
appctx = _cv_app.get(None)
|
|
reqctx = _cv_request.get(None)
|
|
rv: dict[str, t.Any] = {}
|
|
if appctx is not None:
|
|
rv["g"] = appctx.g
|
|
if reqctx is not None:
|
|
rv["request"] = reqctx.request
|
|
rv["session"] = reqctx.session
|
|
return rv
|
|
|
|
|
|
class Environment(BaseEnvironment):
|
|
"""Works like a regular Jinja2 environment but has some additional
|
|
knowledge of how Flask's blueprint works so that it can prepend the
|
|
name of the blueprint to referenced templates if necessary.
|
|
"""
|
|
|
|
def __init__(self, app: Flask, **options: t.Any) -> None:
|
|
if "loader" not in options:
|
|
options["loader"] = app.create_global_jinja_loader()
|
|
BaseEnvironment.__init__(self, **options)
|
|
self.app = app
|
|
|
|
|
|
class DispatchingJinjaLoader(BaseLoader):
|
|
"""A loader that looks for templates in the application and all
|
|
the blueprint folders.
|
|
"""
|
|
|
|
def __init__(self, app: Flask) -> None:
|
|
self.app = app
|
|
|
|
def get_source( # type: ignore
|
|
self, environment: Environment, template: str
|
|
) -> tuple[str, str | None, t.Callable | None]:
|
|
if self.app.config["EXPLAIN_TEMPLATE_LOADING"]:
|
|
return self._get_source_explained(environment, template)
|
|
return self._get_source_fast(environment, template)
|
|
|
|
def _get_source_explained(
|
|
self, environment: Environment, template: str
|
|
) -> tuple[str, str | None, t.Callable | None]:
|
|
attempts = []
|
|
rv: tuple[str, str | None, t.Callable[[], bool] | None] | None
|
|
trv: None | (tuple[str, str | None, t.Callable[[], bool] | None]) = None
|
|
|
|
for srcobj, loader in self._iter_loaders(template):
|
|
try:
|
|
rv = loader.get_source(environment, template)
|
|
if trv is None:
|
|
trv = rv
|
|
except TemplateNotFound:
|
|
rv = None
|
|
attempts.append((loader, srcobj, rv))
|
|
|
|
from .debughelpers import explain_template_loading_attempts
|
|
|
|
explain_template_loading_attempts(self.app, template, attempts)
|
|
|
|
if trv is not None:
|
|
return trv
|
|
raise TemplateNotFound(template)
|
|
|
|
def _get_source_fast(
|
|
self, environment: Environment, template: str
|
|
) -> tuple[str, str | None, t.Callable | None]:
|
|
for _srcobj, loader in self._iter_loaders(template):
|
|
try:
|
|
return loader.get_source(environment, template)
|
|
except TemplateNotFound:
|
|
continue
|
|
raise TemplateNotFound(template)
|
|
|
|
def _iter_loaders(
|
|
self, template: str
|
|
) -> t.Generator[tuple[Scaffold, BaseLoader], None, None]:
|
|
loader = self.app.jinja_loader
|
|
if loader is not None:
|
|
yield self.app, loader
|
|
|
|
for blueprint in self.app.iter_blueprints():
|
|
loader = blueprint.jinja_loader
|
|
if loader is not None:
|
|
yield blueprint, loader
|
|
|
|
def list_templates(self) -> list[str]:
|
|
result = set()
|
|
loader = self.app.jinja_loader
|
|
if loader is not None:
|
|
result.update(loader.list_templates())
|
|
|
|
for blueprint in self.app.iter_blueprints():
|
|
loader = blueprint.jinja_loader
|
|
if loader is not None:
|
|
for template in loader.list_templates():
|
|
result.add(template)
|
|
|
|
return list(result)
|
|
|
|
|
|
def _render(app: Flask, template: Template, context: dict[str, t.Any]) -> str:
|
|
app.update_template_context(context)
|
|
before_render_template.send(
|
|
app, _async_wrapper=app.ensure_sync, template=template, context=context
|
|
)
|
|
rv = template.render(context)
|
|
template_rendered.send(
|
|
app, _async_wrapper=app.ensure_sync, template=template, context=context
|
|
)
|
|
return rv
|
|
|
|
|
|
def render_template(
|
|
template_name_or_list: str | Template | list[str | Template],
|
|
**context: t.Any,
|
|
) -> str:
|
|
"""Render a template by name with the given context.
|
|
|
|
:param template_name_or_list: The name of the template to render. If
|
|
a list is given, the first name to exist will be rendered.
|
|
:param context: The variables to make available in the template.
|
|
"""
|
|
app = current_app._get_current_object() # type: ignore[attr-defined]
|
|
template = app.jinja_env.get_or_select_template(template_name_or_list)
|
|
return _render(app, template, context)
|
|
|
|
|
|
def render_template_string(source: str, **context: t.Any) -> str:
|
|
"""Render a template from the given source string with the given
|
|
context.
|
|
|
|
:param source: The source code of the template to render.
|
|
:param context: The variables to make available in the template.
|
|
"""
|
|
app = current_app._get_current_object() # type: ignore[attr-defined]
|
|
template = app.jinja_env.from_string(source)
|
|
return _render(app, template, context)
|
|
|
|
|
|
def _stream(
|
|
app: Flask, template: Template, context: dict[str, t.Any]
|
|
) -> t.Iterator[str]:
|
|
app.update_template_context(context)
|
|
before_render_template.send(
|
|
app, _async_wrapper=app.ensure_sync, template=template, context=context
|
|
)
|
|
|
|
def generate() -> t.Iterator[str]:
|
|
yield from template.generate(context)
|
|
template_rendered.send(
|
|
app, _async_wrapper=app.ensure_sync, template=template, context=context
|
|
)
|
|
|
|
rv = generate()
|
|
|
|
# If a request context is active, keep it while generating.
|
|
if request:
|
|
rv = stream_with_context(rv)
|
|
|
|
return rv
|
|
|
|
|
|
def stream_template(
|
|
template_name_or_list: str | Template | list[str | Template],
|
|
**context: t.Any,
|
|
) -> t.Iterator[str]:
|
|
"""Render a template by name with the given context as a stream.
|
|
This returns an iterator of strings, which can be used as a
|
|
streaming response from a view.
|
|
|
|
:param template_name_or_list: The name of the template to render. If
|
|
a list is given, the first name to exist will be rendered.
|
|
:param context: The variables to make available in the template.
|
|
|
|
.. versionadded:: 2.2
|
|
"""
|
|
app = current_app._get_current_object() # type: ignore[attr-defined]
|
|
template = app.jinja_env.get_or_select_template(template_name_or_list)
|
|
return _stream(app, template, context)
|
|
|
|
|
|
def stream_template_string(source: str, **context: t.Any) -> t.Iterator[str]:
|
|
"""Render a template from the given source string with the given
|
|
context as a stream. This returns an iterator of strings, which can
|
|
be used as a streaming response from a view.
|
|
|
|
:param source: The source code of the template to render.
|
|
:param context: The variables to make available in the template.
|
|
|
|
.. versionadded:: 2.2
|
|
"""
|
|
app = current_app._get_current_object() # type: ignore[attr-defined]
|
|
template = app.jinja_env.from_string(source)
|
|
return _stream(app, template, context)
|