Skip to content

Commit f95930e

Browse files
committed
Refactored template rendering function and create a TemplateRenderingService
1 parent 3fddd81 commit f95930e

File tree

7 files changed

+164
-60
lines changed

7 files changed

+164
-60
lines changed

ellar/common/decorators/html.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
MODULE_DECORATOR_ITEM,
88
NOT_SET,
99
RESPONSE_OVERRIDE_KEY,
10+
TEMPLATE_CONTEXT_PROCESSOR_KEY,
1011
TEMPLATE_FILTER_KEY,
1112
TEMPLATE_GLOBAL_KEY,
1213
)
@@ -146,3 +147,24 @@ def decorator(f: TemplateGlobalCallable) -> TemplateGlobalCallable:
146147
return f
147148

148149
return decorator
150+
151+
152+
def template_context() -> t.Callable[[TemplateGlobalCallable], TemplateGlobalCallable]:
153+
"""
154+
Registers a template context processor function. These functions run before
155+
rendering a template. The keys of the returned dict are added as variables
156+
available in the template.
157+
158+
Example::
159+
160+
@template_context()
161+
def add_my_context(cls) -> dict:
162+
return dict(extra_item="extra_item", ...)
163+
"""
164+
165+
def decorator(f: TemplateGlobalCallable) -> TemplateGlobalCallable:
166+
setattr(f, TEMPLATE_CONTEXT_PROCESSOR_KEY, f)
167+
setattr(f, MODULE_DECORATOR_ITEM, TEMPLATE_CONTEXT_PROCESSOR_KEY)
168+
return f
169+
170+
return decorator

ellar/common/templating/__init__.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44
from .loader import JinjaLoader, JinjaLoaderType
55
from .model import ModuleTemplating
66
from .renderer import (
7-
get_template_name,
8-
process_view_model,
97
render_template,
108
render_template_string,
119
)
@@ -20,6 +18,4 @@
2018
"TemplateResponse",
2119
"render_template",
2220
"render_template_string",
23-
"process_view_model",
24-
"get_template_name",
2521
]

ellar/common/templating/loader.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from ellar.pydantic import as_pydantic_validator
44
from jinja2 import TemplateNotFound
55
from jinja2.loaders import BaseLoader
6+
from uvicorn.importer import import_from_string
67

78
if t.TYPE_CHECKING: # pragma: no cover
89
from ellar.app.main import App
@@ -56,6 +57,9 @@ def list_templates(self) -> t.List[str]: # pragma: no cover
5657
class JinjaLoaderType(BaseLoader):
5758
@classmethod
5859
def __validate_input__(cls, v: t.Any, _: t.Any) -> t.Any:
60+
if isinstance(v, str):
61+
v = import_from_string(v)
62+
5963
if not isinstance(v, BaseLoader):
6064
raise ValueError(f"Expected {BaseLoader} object, received: {type(v)}")
6165
return v
Lines changed: 19 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,86 +1,49 @@
11
import typing as t
2-
from functools import lru_cache
32

4-
import jinja2
5-
from ellar.common.templating import Environment
3+
from ellar.common.interfaces import ITemplateRenderingService
64
from starlette.background import BackgroundTask
75
from starlette.templating import _TemplateResponse as TemplateResponse
86

9-
if t.TYPE_CHECKING: # pragma: no cover
10-
from ellar.core.connection import Request
117

12-
13-
@lru_cache(1200)
14-
def get_template_name(template_name: str) -> str:
15-
if not template_name.endswith(".html"):
16-
return template_name + ".html"
17-
return template_name
18-
19-
20-
def process_view_model(view_response: t.Any) -> t.Dict:
21-
if isinstance(view_response, dict):
22-
return view_response
23-
return {"model": view_response}
24-
25-
26-
def _get_jinja_and_template_context(
27-
template_name: str, request: "Request", **context: t.Any
28-
) -> t.Tuple["jinja2.Template", t.Dict]:
29-
jinja_environment = request.service_provider.get(Environment)
30-
jinja_template = jinja_environment.get_template(get_template_name(template_name))
31-
template_context = dict(context)
32-
template_context.update(request=request)
33-
return jinja_template, template_context
34-
35-
36-
def render_template_string(
37-
template_string: str, request: "Request", **template_context: t.Any
38-
) -> str:
8+
def render_template_string(template_string: str, **template_context: t.Any) -> str:
399
"""Renders a template to string.
40-
:param request: Request instance
4110
:param template_string: Template String
4211
:param template_context: variables that should be available in the context of the template.
4312
"""
44-
try:
45-
jinja_template, template_context_ = _get_jinja_and_template_context(
46-
template_name=template_string,
47-
request=request,
48-
**process_view_model(template_context),
49-
)
50-
return jinja_template.render(template_context_)
51-
except jinja2.TemplateNotFound:
52-
jinja_environment = request.service_provider.get(Environment)
53-
jinja_template = jinja_environment.from_string(template_string)
13+
from ellar.core.execution_context import current_injector
5414

55-
_template_context = dict(template_context)
56-
_template_context.update(request=request)
57-
58-
return jinja_template.render(_template_context)
15+
rendering_service: ITemplateRenderingService = current_injector.get(
16+
ITemplateRenderingService
17+
)
18+
return rendering_service.render_template_string(
19+
template_string=template_string, **template_context
20+
)
5921

6022

6123
def render_template(
6224
template_name: str,
63-
request: "Request",
6425
background: t.Optional[BackgroundTask] = None,
26+
headers: t.Union[t.Mapping[str, str], None] = None,
6527
status_code: int = 200,
6628
**template_kwargs: t.Any,
6729
) -> TemplateResponse:
6830
"""Renders a template from the template folder with the given context.
6931
:param status_code: Template Response status code
70-
:param request: Request instance
7132
:param template_name: the name of the template to be rendered
33+
:param headers: Response Headers
7234
:param template_kwargs: variables that should be available in the context of the template.
7335
:param background: any background task to be executed after render.
7436
:return TemplateResponse
7537
"""
76-
jinja_template, template_context = _get_jinja_and_template_context(
77-
template_name=template_name,
78-
request=request,
79-
**process_view_model(template_kwargs),
38+
from ellar.core.execution_context import current_injector
39+
40+
rendering_service: ITemplateRenderingService = current_injector.get(
41+
ITemplateRenderingService
8042
)
81-
return TemplateResponse(
82-
template=jinja_template,
83-
context=template_context,
43+
return rendering_service.render_template(
44+
template_name=template_name,
45+
template_context=template_kwargs,
8446
background=background,
8547
status_code=status_code,
48+
headers=headers,
8649
)

ellar/core/templating/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .service import TemplateRenderingService
2+
3+
__all__ = ["TemplateRenderingService"]
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import typing as t
2+
3+
from ellar.core.connection import Request
4+
5+
6+
def request_context(request: Request) -> t.Dict[str, t.Any]:
7+
return {"request": request}
8+
9+
10+
def user(request: Request) -> t.Dict[str, t.Any]:
11+
"""
12+
Return context variables for current request user. This could be AnonymousIdentity or a real user Identity
13+
"""
14+
15+
return {
16+
"user": request.user,
17+
}
18+
19+
20+
def request_state(request: Request) -> t.Dict[str, t.Any]:
21+
"""Adds request state variable to template context"""
22+
23+
return {
24+
"request_state": request.state,
25+
}

ellar/core/templating/service.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import typing as t
2+
from functools import lru_cache
3+
4+
import jinja2
5+
from ellar.common import IHostContext
6+
from ellar.common.interfaces import ITemplateRenderingService
7+
from ellar.common.templating import TemplateResponse
8+
from ellar.core import Config
9+
from ellar.di import injectable, request_scope
10+
from jinja2 import Environment
11+
from starlette.background import BackgroundTask
12+
13+
14+
@lru_cache(1200)
15+
def get_template_name(template_name: str) -> str:
16+
if not template_name.endswith(".html"):
17+
return template_name + ".html"
18+
return template_name
19+
20+
21+
@injectable(scope=request_scope)
22+
class TemplateRenderingService(ITemplateRenderingService):
23+
def __init__(self, config: Config, context: IHostContext) -> None:
24+
self.config = config
25+
self.context = context
26+
self.jinja_env = self.context.get_service_provider().get(Environment)
27+
28+
def process_view_model(self, view_response: t.Any) -> t.Dict:
29+
if isinstance(view_response, dict):
30+
return view_response
31+
return {"model": view_response}
32+
33+
def _compute_template_context(self, template_context: t.Dict) -> t.Dict:
34+
request = self.context.switch_to_http_connection().get_request()
35+
36+
org_context = template_context.copy()
37+
for processor in self.config.APP_CONTEXT_PROCESSORS or []:
38+
res = processor(request)
39+
assert isinstance(
40+
res, dict
41+
), f"{processor} is expected to return a dict object"
42+
template_context.update(res)
43+
44+
template_context.update(org_context)
45+
46+
return template_context
47+
48+
def _get_jinja_and_template_context(
49+
self, template_name: str, **context: t.Any
50+
) -> t.Tuple["jinja2.Template", t.Dict]:
51+
jinja_template = self.jinja_env.get_template(get_template_name(template_name))
52+
template_context = dict(context)
53+
return jinja_template, template_context
54+
55+
def render_template(
56+
self,
57+
template_name: str,
58+
template_context: t.Dict[str, t.Any],
59+
background: t.Optional[BackgroundTask] = None,
60+
headers: t.Union[t.Mapping[str, str], None] = None,
61+
status_code: int = 200,
62+
response_type: t.Type[TemplateResponse] = TemplateResponse,
63+
) -> TemplateResponse:
64+
jinja_template, template_context_ = self._get_jinja_and_template_context(
65+
template_name=template_name,
66+
**self.process_view_model(template_context),
67+
)
68+
template_context = self._compute_template_context(template_context_)
69+
return response_type(
70+
template=jinja_template,
71+
context=template_context,
72+
status_code=status_code,
73+
background=background,
74+
headers=headers,
75+
)
76+
77+
def render_template_string(
78+
self, template_string: str, **template_context: t.Any
79+
) -> str:
80+
try:
81+
jinja_template, template_context_ = self._get_jinja_and_template_context(
82+
template_name=template_string,
83+
**self.process_view_model(template_context),
84+
)
85+
except jinja2.TemplateNotFound:
86+
jinja_template = self.jinja_env.from_string(template_string)
87+
template_context_ = template_context
88+
89+
template_context = self._compute_template_context(template_context_)
90+
91+
return jinja_template.render(template_context)

0 commit comments

Comments
 (0)