djxi 0.1.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- djxi/__init__.py +0 -0
- djxi/actions/__init__.py +4 -0
- djxi/actions/base.py +25 -0
- djxi/actions/parser.py +75 -0
- djxi/actions/router.py +103 -0
- djxi/actions/section.py +49 -0
- djxi/apps.py +27 -0
- djxi/conf.py +27 -0
- djxi/templates/htmx_headers.html +1 -0
- djxi/templates/htmx_script.html +5 -0
- djxi/templatetags/__init__.py +0 -0
- djxi/templatetags/djxi.py +16 -0
- djxi-0.1.2.dist-info/METADATA +119 -0
- djxi-0.1.2.dist-info/RECORD +24 -0
- djxi-0.1.2.dist-info/WHEEL +5 -0
- djxi-0.1.2.dist-info/licenses/LICENCE.md +21 -0
- djxi-0.1.2.dist-info/top_level.txt +2 -0
- tests/__init__.py +0 -0
- tests/conftest.py +32 -0
- tests/test_actions.py +50 -0
- tests/test_parser.py +38 -0
- tests/test_routing.py +83 -0
- tests/test_settings.py +19 -0
- tests/test_template_compat.py +119 -0
djxi/__init__.py
ADDED
|
File without changes
|
djxi/actions/__init__.py
ADDED
djxi/actions/base.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from django.http import HttpResponse
|
|
2
|
+
from django.template import Template, RequestContext
|
|
3
|
+
|
|
4
|
+
from .router import DXRouterMixin
|
|
5
|
+
from .section import DXSectionTemplateMixin
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DxActionRouter(DXSectionTemplateMixin, DXRouterMixin):
|
|
9
|
+
"""Unify the Main User Loop into one class with defined routed actions and a section library.
|
|
10
|
+
Main User Loop = Request->Route->Logic->Render->Response ...
|
|
11
|
+
|
|
12
|
+
1) define the section template:
|
|
13
|
+
a) inline_template = <dx-section name="identifier"> html for this section </dx-section>
|
|
14
|
+
b) template_name = path/to/template.html (app/template.html)
|
|
15
|
+
2) set up actions as class methods with the route decorator
|
|
16
|
+
3) hook the router into a url conf
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def render_section(self, request, section_name, context=None):
|
|
20
|
+
"""Render a dx-section as a full HTTP response."""
|
|
21
|
+
if context is None:
|
|
22
|
+
context = {}
|
|
23
|
+
raw_html = self.get_section(section_name)
|
|
24
|
+
template = Template(raw_html)
|
|
25
|
+
return HttpResponse(template.render(RequestContext(request, context)))
|
djxi/actions/parser.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from html.parser import HTMLParser
|
|
3
|
+
from django.template import loader
|
|
4
|
+
|
|
5
|
+
from djxi.conf import package_settings as djxi_settings
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SectionParser(HTMLParser):
|
|
9
|
+
def __init__(self):
|
|
10
|
+
super().__init__()
|
|
11
|
+
self.section_tag = getattr(djxi_settings, "DX_SECTION_TAG", None)
|
|
12
|
+
self.sections = {} # name -> inner HTML
|
|
13
|
+
self._current_name = None
|
|
14
|
+
self._inside = False
|
|
15
|
+
self._chunks = []
|
|
16
|
+
|
|
17
|
+
def handle_starttag(self, tag, attrs):
|
|
18
|
+
if tag == self.section_tag:
|
|
19
|
+
# Extract the 'name' attribute
|
|
20
|
+
attrs_dict = dict(attrs)
|
|
21
|
+
name = attrs_dict.get("name")
|
|
22
|
+
if name:
|
|
23
|
+
self._current_name = name
|
|
24
|
+
self._inside = True
|
|
25
|
+
self._chunks = []
|
|
26
|
+
return
|
|
27
|
+
if self._inside:
|
|
28
|
+
# Re‑emit the opening tag (including its attributes)
|
|
29
|
+
attrs_str = " " + " ".join(f'{k}="{v}"' for k, v in attrs) if attrs else ""
|
|
30
|
+
self._chunks.append(f"<{tag}{attrs_str}>")
|
|
31
|
+
|
|
32
|
+
def handle_endtag(self, tag):
|
|
33
|
+
if tag == self.section_tag and self._inside:
|
|
34
|
+
# End of section – store the collected inner HTML
|
|
35
|
+
self.sections[self._current_name] = "".join(self._chunks)
|
|
36
|
+
self._inside = False
|
|
37
|
+
self._current_name = None
|
|
38
|
+
self._chunks = []
|
|
39
|
+
elif self._inside:
|
|
40
|
+
# Re‑emit the closing tag for nested elements
|
|
41
|
+
self._chunks.append(f"</{tag}>")
|
|
42
|
+
|
|
43
|
+
def handle_data(self, data):
|
|
44
|
+
if self._inside:
|
|
45
|
+
self._chunks.append(data)
|
|
46
|
+
|
|
47
|
+
def handle_startendtag(self, tag, attrs):
|
|
48
|
+
# Self‑closing tags (like <br/> or <img ... />)
|
|
49
|
+
if self._inside:
|
|
50
|
+
attrs_str = " " + " ".join(f'{k}="{v}"' for k, v in attrs) if attrs else ""
|
|
51
|
+
self._chunks.append(f"<{tag}{attrs_str}/>")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def load_django_template(template_name: str) -> str:
|
|
55
|
+
"""Loads a Django template from the given name."""
|
|
56
|
+
template_string = ""
|
|
57
|
+
# If template_name references a filesystem path, read it directly.
|
|
58
|
+
# In that case template name has to be an absolute path (this is mainly for the test suite)
|
|
59
|
+
if os.path.exists(template_name):
|
|
60
|
+
with open(template_name, "r", encoding="utf-8") as f:
|
|
61
|
+
template_string = f.read()
|
|
62
|
+
else:
|
|
63
|
+
# Attempt to load via Django's template loader (e.g. app/template.html).
|
|
64
|
+
try:
|
|
65
|
+
tpl = loader.get_template(template_name)
|
|
66
|
+
origin = getattr(tpl, "origin", None)
|
|
67
|
+
if origin and hasattr(origin, "loader"):
|
|
68
|
+
template_string = origin.loader.get_contents(origin)
|
|
69
|
+
else:
|
|
70
|
+
template_string = (
|
|
71
|
+
getattr(getattr(tpl, "template", None), "source", "") or ""
|
|
72
|
+
)
|
|
73
|
+
except Exception:
|
|
74
|
+
template_string = ""
|
|
75
|
+
return template_string
|
djxi/actions/router.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
""" Djxi Routing
|
|
2
|
+
|
|
3
|
+
TODO: Potential Refactor
|
|
4
|
+
- dx_route -> make_route | dx_mark_view
|
|
5
|
+
- dx_router -> dx_collect_paths
|
|
6
|
+
"""
|
|
7
|
+
from functools import wraps
|
|
8
|
+
from django.http import HttpResponseNotAllowed
|
|
9
|
+
from django.urls import path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DXRouterMixin:
|
|
13
|
+
"""Use as a Mixin to call CLS.dx_router() to get a list of djxi routes"""
|
|
14
|
+
|
|
15
|
+
@classmethod
|
|
16
|
+
def dx_router(cls) -> list:
|
|
17
|
+
"""
|
|
18
|
+
Generate a list of Django URL patterns from all methods decorated with @route.
|
|
19
|
+
Use the class method to DxActionRouter.dx_router() in the url conf to hook the routes
|
|
20
|
+
"""
|
|
21
|
+
patterns = []
|
|
22
|
+
|
|
23
|
+
for attr_name in dir(cls):
|
|
24
|
+
attr = getattr(cls, attr_name)
|
|
25
|
+
if hasattr(attr, "_routes"):
|
|
26
|
+
for url_path, methods, name in attr._routes:
|
|
27
|
+
# Create a view that instantiates the class and calls the method
|
|
28
|
+
def make_view(method_name, allowed_methods, attr_func=attr):
|
|
29
|
+
@wraps(attr_func)
|
|
30
|
+
def view(request, *args, **kwargs):
|
|
31
|
+
if request.method not in allowed_methods:
|
|
32
|
+
return HttpResponseNotAllowed(allowed_methods)
|
|
33
|
+
instance = cls() # instantiate the view class
|
|
34
|
+
handler = getattr(instance, method_name)
|
|
35
|
+
return handler(request, *args, **kwargs)
|
|
36
|
+
|
|
37
|
+
return view
|
|
38
|
+
|
|
39
|
+
patterns.append(
|
|
40
|
+
path(url_path, make_view(attr_name, methods), name=name)
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
return patterns
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def dx_route(path: str, methods: [] = None, **kwargs):
|
|
47
|
+
"""
|
|
48
|
+
Decorator to mark a class method as a URL endpoint.
|
|
49
|
+
- path: URL path (can include Django path converters, e.g. '/items/<int:id>/')
|
|
50
|
+
- methods: list of allowed HTTP methods (defaults to ["GET"])
|
|
51
|
+
- kwargs:
|
|
52
|
+
- name: qualify the route with a name, default to func.__name__
|
|
53
|
+
"""
|
|
54
|
+
if methods is None:
|
|
55
|
+
methods = ["GET"]
|
|
56
|
+
|
|
57
|
+
def decorator(func):
|
|
58
|
+
name = kwargs.pop("name", func.__name__)
|
|
59
|
+
if not hasattr(func, "_routes"):
|
|
60
|
+
func._routes = []
|
|
61
|
+
func._routes.append((path, methods, name))
|
|
62
|
+
return func
|
|
63
|
+
|
|
64
|
+
return decorator
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
#
|
|
68
|
+
# Alias definitions
|
|
69
|
+
#
|
|
70
|
+
def dx_GET(path: str, **kwargs):
|
|
71
|
+
return dx_route(path, ["GET"], **kwargs)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def dx_HEAD(path: str, **kwargs):
|
|
75
|
+
return dx_route(path, ["HEAD"], **kwargs)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def dx_POST(path: str, **kwargs):
|
|
79
|
+
return dx_route(path, ["POST"], **kwargs)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def dx_PUT(path: str, **kwargs):
|
|
83
|
+
return dx_route(path, ["PUT"], **kwargs)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def dx_PATCH(path: str, **kwargs):
|
|
87
|
+
return dx_route(path, ["PATCH"], **kwargs)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def dx_DELETE(path: str, **kwargs):
|
|
91
|
+
return dx_route(path, ["DELETE"], **kwargs)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def dx_OPTIONS(path: str, **kwargs):
|
|
95
|
+
return dx_route(path, ["OPTIONS"], **kwargs)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def dx_ANY(path: str, **kwargs):
|
|
99
|
+
return dx_route(
|
|
100
|
+
path,
|
|
101
|
+
["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
|
|
102
|
+
**kwargs,
|
|
103
|
+
)
|
djxi/actions/section.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from django.core.exceptions import ImproperlyConfigured
|
|
2
|
+
|
|
3
|
+
from .parser import SectionParser, load_django_template
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class DXSectionTemplateMixin:
|
|
7
|
+
""""""
|
|
8
|
+
|
|
9
|
+
section_inline = None
|
|
10
|
+
section_template_name = None
|
|
11
|
+
|
|
12
|
+
def __init__(self, **kwargs):
|
|
13
|
+
"""Constructor builds the dx section cache."""
|
|
14
|
+
if self.section_inline is None and self.section_template_name is None:
|
|
15
|
+
raise ImproperlyConfigured(
|
|
16
|
+
"DxActionRouter requires a definition of sections via "
|
|
17
|
+
"'section_inline' or a path to a 'section_template_name'"
|
|
18
|
+
)
|
|
19
|
+
self.build_section_cache()
|
|
20
|
+
|
|
21
|
+
def build_section_cache(self) -> None:
|
|
22
|
+
"""On init builds the section cache.
|
|
23
|
+
A dictionary with the (k,v) = name-of-section, html_string.
|
|
24
|
+
"""
|
|
25
|
+
self._dx_section_cache = self.parse_section_dict()
|
|
26
|
+
|
|
27
|
+
def parse_section_dict(self) -> dict:
|
|
28
|
+
"""Parses the string and extract the dx-section parts."""
|
|
29
|
+
parser = SectionParser()
|
|
30
|
+
parser.feed(self.build_section_string())
|
|
31
|
+
return parser.sections
|
|
32
|
+
|
|
33
|
+
def build_section_string(self) -> str:
|
|
34
|
+
"""Concatenates the specified templates and returns the unmodified string.
|
|
35
|
+
In most cases you want to set one or the other, yet both is allowed.
|
|
36
|
+
Sections are read in order and later redefinition may override.
|
|
37
|
+
"""
|
|
38
|
+
section_string = ""
|
|
39
|
+
if self.section_template_name is not None:
|
|
40
|
+
section_string = load_django_template(self.section_template_name)
|
|
41
|
+
if self.section_inline is not None:
|
|
42
|
+
section_string += self.section_inline
|
|
43
|
+
return section_string
|
|
44
|
+
|
|
45
|
+
def get_section(self, name: str) -> str:
|
|
46
|
+
"""Returns the djxi section from the _dx_section_cache.
|
|
47
|
+
If name cannot be found, returns empty string.
|
|
48
|
+
"""
|
|
49
|
+
return self._dx_section_cache.get(name, "")
|
djxi/apps.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from django.apps import AppConfig
|
|
2
|
+
from django.core.exceptions import ImproperlyConfigured
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class DjxiAppConfig(AppConfig):
|
|
6
|
+
name = "djxi"
|
|
7
|
+
verbose_name = "Djxi"
|
|
8
|
+
|
|
9
|
+
def ready(self):
|
|
10
|
+
"""
|
|
11
|
+
Django calls this method when the application registry is fully loaded.
|
|
12
|
+
This is the safest place to import signals, validators, or run
|
|
13
|
+
one-time startup code.
|
|
14
|
+
"""
|
|
15
|
+
# Validate required settings exist and sane
|
|
16
|
+
from .conf import package_settings
|
|
17
|
+
|
|
18
|
+
htmx_version = package_settings.DX_HTMX_VERSION
|
|
19
|
+
if htmx_version not in ["2", "4"]:
|
|
20
|
+
raise ImproperlyConfigured("DX_HTMX_VERSION must be 2 or 4")
|
|
21
|
+
|
|
22
|
+
section_tag = package_settings.DX_SECTION_TAG
|
|
23
|
+
if section_tag in ["", None]:
|
|
24
|
+
raise ImproperlyConfigured("DX_SECTION_TAG can not be None or empty")
|
|
25
|
+
|
|
26
|
+
# Optional: Import signals if you have a signals.py file
|
|
27
|
+
# import mypackage.signals
|
djxi/conf.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from django.conf import settings
|
|
2
|
+
|
|
3
|
+
# Define your package's default settings here.
|
|
4
|
+
# Use ALL CAPS names, just like Django's own settings.
|
|
5
|
+
DEFAULTS = {
|
|
6
|
+
"DX_HTMX_VERSION": "4",
|
|
7
|
+
"DX_HTMX_MINIFIED": True,
|
|
8
|
+
"DX_SECTION_TAG": "dx-section",
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Settings:
|
|
13
|
+
"""
|
|
14
|
+
A proxy object that reads from django.conf.settings first,
|
|
15
|
+
and falls back to DEFAULTS if the user didn't define it.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __getattr__(self, name):
|
|
19
|
+
if name not in DEFAULTS:
|
|
20
|
+
raise AttributeError(f"Invalid setting: '{name}'")
|
|
21
|
+
|
|
22
|
+
# Return the user's value from settings.py, or fall back to the default
|
|
23
|
+
return getattr(settings, name, DEFAULTS[name])
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# Instantiate a single global object for easy import
|
|
27
|
+
package_settings = Settings()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
hx-headers{% if explicit_inheritance %}:inherited{% endif %}='{"X-CSRFTOKEN":"{{ csrf_token }}"}'
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
{% if version is "2" %}
|
|
2
|
+
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.10/dist/htmx{{minified}}.js" integrity="sha384-H5SrcfygHmAuTDZphMHqBJLc3FhssKjG7w/CeCpFReSfwBWDTKpkzPP8c+cLsK+V" crossorigin="anonymous"></script>
|
|
3
|
+
{% else %}
|
|
4
|
+
<script src="https://cdn.jsdelivr.net/npm/htmx.org@4.0.0-beta5/dist/htmx{{minified}}.js" integrity="sha384-RZoQSZlu2BAuZMuM5lTKAWXXSKC+7X6eVzP1pwkUBcyfPmOswexqVOsUqQMKbAFA" crossorigin="anonymous"></script>
|
|
5
|
+
{% endif %}
|
|
File without changes
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from django.template import Library
|
|
2
|
+
from djxi.conf import package_settings as djxi_settings
|
|
3
|
+
|
|
4
|
+
register = Library()
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@register.inclusion_tag("htmx_script.html")
|
|
8
|
+
def htmx_script_inclusion():
|
|
9
|
+
minified = ".min" if djxi_settings.DX_HTMX_MINIFIED is True else ""
|
|
10
|
+
return {"version": djxi_settings.DX_HTMX_VERSION, "minified": minified}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@register.inclusion_tag("htmx_headers.html")
|
|
14
|
+
def htmx_headers():
|
|
15
|
+
explicit_inheritance = djxi_settings.DX_HTMX_VERSION == "4"
|
|
16
|
+
return {"explicit_inheritance": explicit_inheritance}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: djxi
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: Django HTMX Integration
|
|
5
|
+
Author-email: Philipp Rollinger <philipp.rollinger@protonmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/rollinger/djxi
|
|
8
|
+
Project-URL: Repository, https://github.com/rollinger/djxi
|
|
9
|
+
Requires-Python: >=3.8
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENCE.md
|
|
12
|
+
Requires-Dist: django>=4.2
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: pytest>=7.4.0; extra == "dev"
|
|
15
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
16
|
+
Requires-Dist: black>=23.0.0; extra == "dev"
|
|
17
|
+
Requires-Dist: ruff>=0.0.275; extra == "dev"
|
|
18
|
+
Requires-Dist: pre-commit>=3.0; extra == "dev"
|
|
19
|
+
Requires-Dist: build>=1.0; extra == "dev"
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
# Djxi | **HTMX Integration for Django**
|
|
23
|
+

|
|
24
|
+
---
|
|
25
|
+
[](https://github.com/rollinger/djxi/actions/workflows/main.yml)
|
|
26
|
+
[](https://codecov.io/gh/rollinger/djxi)
|
|
27
|
+
[](https://pypi.org/project/djxi)
|
|
28
|
+
[](https://pypi.org/project/djxi)
|
|
29
|
+
[](https://pypi.org/project/djxi)
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## 📦 What is this?
|
|
33
|
+
|
|
34
|
+
**Stop hunting for HTMX endpoints.**
|
|
35
|
+
Djxi bundles the route, the view logic, and the HTML partial into a single **Endpoint Battery**.
|
|
36
|
+
Drop the `DxActionRouter` into your existing Django views or use it standalone. Keep every tiny `hx-*` swap exactly where it lives—without scattering your code across `urls.py`, `views/`, and `templates/`.
|
|
37
|
+
|
|
38
|
+
- **No more archaeology.** No more digging through three files just to tweak a button label.
|
|
39
|
+
- **LoB, restored.** Request → Logic → Render stays in one atomic, inline hub.
|
|
40
|
+
- **Scales cleanly.** Small partials stay manageable, without turning your project into spaghetti.
|
|
41
|
+
|
|
42
|
+
Just a prenup between Grandpa Django and his sexy new HTMX fling—keeping your repo clean, one battery at a time.
|
|
43
|
+
|
|
44
|
+
## Pre-Alpha Note
|
|
45
|
+
The package is not yet published and considered in experimental pre-alpha state.
|
|
46
|
+
- Watch out for updates and consider giving it a star.
|
|
47
|
+
- Checkout the djxi showcases in the example django app.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Getting Started
|
|
52
|
+
### Instalation
|
|
53
|
+
1) Install with pip:
|
|
54
|
+
|
|
55
|
+
`python -m pip install djxi`
|
|
56
|
+
|
|
57
|
+
2) Add django-htmx to your INSTALLED_APPS:
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
INSTALLED_APPS = [
|
|
61
|
+
...,
|
|
62
|
+
"djxi",
|
|
63
|
+
...,
|
|
64
|
+
]
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
3) Optional: Adjust your base template to get you up and running instantly
|
|
68
|
+
```html
|
|
69
|
+
{% load djxi %}
|
|
70
|
+
<!doctype html>
|
|
71
|
+
<html>
|
|
72
|
+
<head>
|
|
73
|
+
...
|
|
74
|
+
{% htmx_script_inclusion %}
|
|
75
|
+
</head>
|
|
76
|
+
<body {% htmx_headers %}>
|
|
77
|
+
...
|
|
78
|
+
</body>
|
|
79
|
+
</html>
|
|
80
|
+
```
|
|
81
|
+
The htmx_script_inclusion tag will pull the unminified v4 from CDN. Set DX_HTMX_VERSION="2" to pull in v2.
|
|
82
|
+
For prodution you likely want to serve your own minified htmx.js.
|
|
83
|
+
|
|
84
|
+
As there are significant syntax changes between v4 and v2 of htmx, keep DX_HTMX_VERSION in sync with
|
|
85
|
+
what htmx version you are serving.
|
|
86
|
+
|
|
87
|
+
### Configuration
|
|
88
|
+
In your settings file you can overide the following default values for Djxi:
|
|
89
|
+
- DX_HTMX_VERSION": "4" # Choose between 4 and 2
|
|
90
|
+
- DX_HTMX_MINIFIED": False # Load a minified source, recommended for production
|
|
91
|
+
|
|
92
|
+
### Quick start
|
|
93
|
+
Create and manage your HTMX Endpoint in a convenient Battery:
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
from djxi.actions import DxActionRouter, dx_route
|
|
97
|
+
|
|
98
|
+
INLINE_TEMPLATE = """
|
|
99
|
+
<dx-section name="confirm-button">
|
|
100
|
+
<button hx-post="/showcase/simple/confirm">
|
|
101
|
+
Confirm, {{ name }}
|
|
102
|
+
</button>
|
|
103
|
+
</dx-section>
|
|
104
|
+
|
|
105
|
+
<dx-section name="check-confirmed">
|
|
106
|
+
<button disabled>Confirmed!</button>
|
|
107
|
+
</dx-section>
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
class SimpleInlineActionRouter(DxActionRouter):
|
|
111
|
+
inline_template = INLINE_TEMPLATE
|
|
112
|
+
|
|
113
|
+
@dx_route("get-confirm-button", methods=["GET"])
|
|
114
|
+
def get_confirm_button(self, request):
|
|
115
|
+
context = {"name": "Phil"}
|
|
116
|
+
return self.render_section(
|
|
117
|
+
request, section_name="confirm-button", context=context
|
|
118
|
+
)
|
|
119
|
+
```
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
djxi/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
djxi/apps.py,sha256=VkuR7GCtN4bil6bdwnPQ0fnvKUH55AYDVMIEU7AbxGA,940
|
|
3
|
+
djxi/conf.py,sha256=pnfM8dBbpNUDjaXVy3CTEaY3UXKmun68nr4v7LUnqQc,754
|
|
4
|
+
djxi/actions/__init__.py,sha256=bGieYBqGmiM9TO4osylRgh0sXHHEWFoexwvozERyRJU,104
|
|
5
|
+
djxi/actions/base.py,sha256=FT7w7d3uk7X2zEDa0O5ZPfMEniG0sHueGwA2v6P4JD0,1055
|
|
6
|
+
djxi/actions/parser.py,sha256=sA6OlnE8snJmBw_rsVYpgDHPUMh_iA__r4iOYdhUa_8,2858
|
|
7
|
+
djxi/actions/router.py,sha256=YmWwcdLPChO4CkuUnXjCaVd0dsZdnnCOVCJD71pzUwU,3038
|
|
8
|
+
djxi/actions/section.py,sha256=RxB4-Lw1x3PRLPjGkhUUAtNq1LcASBkCmb_rrRr3aIs,1868
|
|
9
|
+
djxi/templates/htmx_headers.html,sha256=8I9e-IHH3dMyJ3unUxeYLP3lLeeKZDSBDcPX5K1E4Do,97
|
|
10
|
+
djxi/templates/htmx_script.html,sha256=tK9yc1IkV3HJXcYXNT0qq37wqqHAxp5wFUre9zqISgI,463
|
|
11
|
+
djxi/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
+
djxi/templatetags/djxi.py,sha256=e9zEWu68kQ4kd3ydvArZEYm9baRZuj7jTVeemU65FJk,526
|
|
13
|
+
djxi-0.1.2.dist-info/licenses/LICENCE.md,sha256=No1vF19rSru7Liq0pzUzfWaFLl0R0bd7D3aprQ6T9_M,1073
|
|
14
|
+
tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
|
+
tests/conftest.py,sha256=5KrxJ2VyLp8TMcw1pf-QUFnrcmsH2Mkxi1eEKw_zQ2Y,983
|
|
16
|
+
tests/test_actions.py,sha256=ltPcR9A9kOwXQ1IUSQb6IbnKoIlI79jfS1qIYj-0VtQ,1541
|
|
17
|
+
tests/test_parser.py,sha256=zr_6OQU3THLdUyu32kaVrguUrtgs35kny53-NOe3xMI,1237
|
|
18
|
+
tests/test_routing.py,sha256=Q1jD9M859Uz92XKo3hVx82FsWf5cGyuwLcvZ_L3NCXE,3185
|
|
19
|
+
tests/test_settings.py,sha256=nsQSFqVTuy8NJ7yE22tcKKBM46WxeL3wO04T2kLlPm4,678
|
|
20
|
+
tests/test_template_compat.py,sha256=TwFKVqzuc1do21CxsO3X64YhVl2EtdqWhPnsXXbBxnE,4477
|
|
21
|
+
djxi-0.1.2.dist-info/METADATA,sha256=EOBndSD_ItbBC5rt2iZcbgDRL3HU1vyjZzDVKuHFZUU,4083
|
|
22
|
+
djxi-0.1.2.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
23
|
+
djxi-0.1.2.dist-info/top_level.txt,sha256=iPqRhXpD7kKW5S5T7CoZaYG-WkV68sVourW1aSdd5gk,11
|
|
24
|
+
djxi-0.1.2.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Philipp Rollinger
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
tests/__init__.py
ADDED
|
File without changes
|
tests/conftest.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import django
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from django.conf import settings
|
|
4
|
+
|
|
5
|
+
TEST_DIR = Path(__file__).parent
|
|
6
|
+
TEMPLATE_DIR = str(TEST_DIR / "data") # absolute string path usable by Django
|
|
7
|
+
|
|
8
|
+
# Configure Django only once
|
|
9
|
+
if not settings.configured:
|
|
10
|
+
settings.configure(
|
|
11
|
+
SECRET_KEY="dummy-secret-for-testing",
|
|
12
|
+
ROOT_URLCONF=__name__, # will be overridden per test if needed
|
|
13
|
+
INSTALLED_APPS=[
|
|
14
|
+
"django.contrib.auth",
|
|
15
|
+
"django.contrib.contenttypes",
|
|
16
|
+
"djxi",
|
|
17
|
+
],
|
|
18
|
+
MIDDLEWARE=[], # empty for speed
|
|
19
|
+
TEMPLATES=[
|
|
20
|
+
{
|
|
21
|
+
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
|
22
|
+
"DIRS": [TEMPLATE_DIR],
|
|
23
|
+
"APP_DIRS": True,
|
|
24
|
+
"OPTIONS": {
|
|
25
|
+
"context_processors": [
|
|
26
|
+
"django.template.context_processors.request",
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
}
|
|
30
|
+
],
|
|
31
|
+
)
|
|
32
|
+
django.setup()
|
tests/test_actions.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from django.core.exceptions import ImproperlyConfigured
|
|
3
|
+
|
|
4
|
+
from djxi.actions import DxActionRouter
|
|
5
|
+
|
|
6
|
+
INLINE_TEMPLATE = """
|
|
7
|
+
<dx-section name="a-b">AB</dx-section><dx-section name="long spaced name">
|
|
8
|
+
Long spaced Name
|
|
9
|
+
</dx-section>
|
|
10
|
+
|
|
11
|
+
<dx-section name="b-c-d">
|
|
12
|
+
This should be good<br>
|
|
13
|
+
</dx-section>
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class EmptyActionRouter(DxActionRouter):
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class InlineActionRouter(DxActionRouter):
|
|
22
|
+
section_inline = INLINE_TEMPLATE
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TemplateActionRouter(DxActionRouter):
|
|
26
|
+
section_template_name = "template.html"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_djxi_empty_init():
|
|
30
|
+
# Raise error if inline_template and template_name is not configured
|
|
31
|
+
with pytest.raises(ImproperlyConfigured):
|
|
32
|
+
dx_action = EmptyActionRouter() # noqa: F841
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_djxi_inline_build_sections():
|
|
36
|
+
dx_action = InlineActionRouter()
|
|
37
|
+
assert len(dx_action._dx_section_cache) == 3
|
|
38
|
+
assert dx_action.get_section("a-b") == "AB"
|
|
39
|
+
assert dx_action.get_section("long spaced name") == "\nLong spaced Name\n"
|
|
40
|
+
assert dx_action.get_section("b-c-d") == "\n This should be good<br>\n"
|
|
41
|
+
assert dx_action.get_section("this-key-does-not_exist") == ""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_djxi_template_build_sections():
|
|
45
|
+
dx_action = TemplateActionRouter()
|
|
46
|
+
assert len(dx_action._dx_section_cache) == 3
|
|
47
|
+
assert dx_action.get_section("a-b") == "AB"
|
|
48
|
+
assert dx_action.get_section("long spaced name") == "\nLong spaced Name\n"
|
|
49
|
+
assert dx_action.get_section("b-c-d") == "\n This should be good<br>\n"
|
|
50
|
+
assert dx_action.get_section("this-key-does-not_exist") == ""
|
tests/test_parser.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from django.test import override_settings
|
|
2
|
+
|
|
3
|
+
from djxi.actions import DxActionRouter
|
|
4
|
+
|
|
5
|
+
DEFAULT = "dx-section"
|
|
6
|
+
CUSTOM = "my-section-tag"
|
|
7
|
+
|
|
8
|
+
INLINE_TEMPLATE = f"""
|
|
9
|
+
<unrelated-tag>
|
|
10
|
+
<{DEFAULT} name="section_01">Content 1</{DEFAULT}>
|
|
11
|
+
<{DEFAULT} name="section_02">Content 2</{DEFAULT}>
|
|
12
|
+
<unrelated-tag/ >
|
|
13
|
+
<{CUSTOM} name="section_03">Content 3</{CUSTOM}>
|
|
14
|
+
<{DEFAULT} name="section_04">Content 4</{DEFAULT}>
|
|
15
|
+
<unrelated-tag>XYZ</unrelated-tag>
|
|
16
|
+
<{CUSTOM} name="section_05">Content 5</{CUSTOM}>
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class InlineActionRouter(DxActionRouter):
|
|
21
|
+
section_inline = INLINE_TEMPLATE
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@override_settings(DX_SECTION_TAG=DEFAULT)
|
|
25
|
+
def test_default_section_tag():
|
|
26
|
+
default_router = InlineActionRouter()
|
|
27
|
+
assert len(default_router._dx_section_cache) == 3
|
|
28
|
+
assert default_router.get_section("section_01") == "Content 1"
|
|
29
|
+
assert default_router.get_section("section_02") == "Content 2"
|
|
30
|
+
assert default_router.get_section("section_04") == "Content 4"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@override_settings(DX_SECTION_TAG=CUSTOM)
|
|
34
|
+
def test_custom_section_tag():
|
|
35
|
+
custom_router = InlineActionRouter()
|
|
36
|
+
assert len(custom_router._dx_section_cache) == 2
|
|
37
|
+
assert custom_router.get_section("section_03") == "Content 3"
|
|
38
|
+
assert custom_router.get_section("section_05") == "Content 5"
|
tests/test_routing.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# fmt: off
|
|
2
|
+
from django.test import override_settings, RequestFactory
|
|
3
|
+
from django.urls import path, include, resolve, reverse
|
|
4
|
+
|
|
5
|
+
from djxi.actions import DxActionRouter, dx_route
|
|
6
|
+
|
|
7
|
+
INLINE_TEMPLATE = """
|
|
8
|
+
<dx-section name="section_01">Content 1</dx-section>
|
|
9
|
+
<dx-section name="section_02">Content 2</dx-section>
|
|
10
|
+
<dx-section name="section_03">Content 3</dx-section>
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class InlineActionRouter(DxActionRouter):
|
|
15
|
+
section_inline = INLINE_TEMPLATE
|
|
16
|
+
|
|
17
|
+
@dx_route("section/1", methods=["GET"], name="nifty_first_section")
|
|
18
|
+
def section_01(self, request):
|
|
19
|
+
return self.render_section(request, "section_01")
|
|
20
|
+
|
|
21
|
+
@dx_route("section/2/", methods=["GET"])
|
|
22
|
+
def section_02(self, request):
|
|
23
|
+
return self.render_section(request, "section_02")
|
|
24
|
+
|
|
25
|
+
@dx_route("section/3", methods=["GET", "POST"])
|
|
26
|
+
def section_03(self, request):
|
|
27
|
+
return self.render_section(request, "section_03")
|
|
28
|
+
|
|
29
|
+
@dx_route("section/<int:id>", methods=["GET"], name="get_the_section_you_want")
|
|
30
|
+
def section_by_id(self, request, id):
|
|
31
|
+
section_name = f"section_{id}"
|
|
32
|
+
return self.render_section(request, section_name)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# URL Patterns from .dx_router
|
|
36
|
+
urlpatterns = [
|
|
37
|
+
path("", include((InlineActionRouter.dx_router(), "djxi"), namespace="djxi"))
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@override_settings(ROOT_URLCONF=__name__)
|
|
42
|
+
def test_path_resolver():
|
|
43
|
+
true_paths = {
|
|
44
|
+
"/section/1": {"view": "section_01", "args": (), "kwargs": {}, "content": "Content 1"},
|
|
45
|
+
"/section/2/": {"view": "section_02", "args": (), "kwargs": {}, "content": "Content 2"},
|
|
46
|
+
"/section/3": {"view": "section_03", "args": (), "kwargs": {}, "content": "Content 3"},
|
|
47
|
+
"/section/4": {"view": "section_by_id", "args": (), "kwargs": {"id": 4}, "content": ""},
|
|
48
|
+
"/section/99": {"view": "section_by_id", "args": (), "kwargs": {"id": 99}, "content": ""},
|
|
49
|
+
}
|
|
50
|
+
rf = RequestFactory()
|
|
51
|
+
for request_path, truth in true_paths.items():
|
|
52
|
+
match = resolve(request_path)
|
|
53
|
+
assert match.func.__name__ == truth["view"]
|
|
54
|
+
assert match.args == truth["args"]
|
|
55
|
+
assert match.kwargs == truth["kwargs"]
|
|
56
|
+
req = rf.get(request_path)
|
|
57
|
+
assert match.func(req, *match.args, **match.kwargs).content.decode() == truth["content"]
|
|
58
|
+
|
|
59
|
+
@override_settings(ROOT_URLCONF=__name__)
|
|
60
|
+
def test_name_resolver():
|
|
61
|
+
request_path = reverse("djxi:nifty_first_section")
|
|
62
|
+
assert request_path == "/section/1"
|
|
63
|
+
match = resolve(request_path)
|
|
64
|
+
assert match.func.__name__ == "section_01"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@override_settings(ROOT_URLCONF=__name__)
|
|
68
|
+
def test_no_name_resolver():
|
|
69
|
+
request_path = reverse("djxi:section_02")
|
|
70
|
+
assert request_path == "/section/2/"
|
|
71
|
+
match = resolve(request_path)
|
|
72
|
+
assert match.func.__name__ == "section_02"
|
|
73
|
+
|
|
74
|
+
@override_settings(ROOT_URLCONF=__name__)
|
|
75
|
+
def test_name_override_resolver():
|
|
76
|
+
request_path = reverse("djxi:get_the_section_you_want", kwargs={"id": 3})
|
|
77
|
+
assert request_path == "/section/3"
|
|
78
|
+
match = resolve(request_path)
|
|
79
|
+
assert match.func.__name__ == "section_03"
|
|
80
|
+
request_path = reverse("djxi:get_the_section_you_want", kwargs={"id": 4})
|
|
81
|
+
assert request_path == "/section/4"
|
|
82
|
+
match = resolve(request_path)
|
|
83
|
+
assert match.func.__name__ == "section_by_id"
|
tests/test_settings.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from django.test import override_settings
|
|
2
|
+
|
|
3
|
+
from djxi.conf import package_settings as djxi_settings
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_conf_DX_HTMX_VERSION():
|
|
7
|
+
assert djxi_settings.DX_HTMX_VERSION == "4"
|
|
8
|
+
with override_settings(DX_HTMX_VERSION="2"):
|
|
9
|
+
assert djxi_settings.DX_HTMX_VERSION == "2"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_conf_DX_SECTION_TAG():
|
|
13
|
+
section_name = getattr(djxi_settings, "DX_SECTION_TAG", None)
|
|
14
|
+
assert section_name is not None
|
|
15
|
+
assert section_name == "dx-section"
|
|
16
|
+
with override_settings(DX_SECTION_TAG="my-section-tag"):
|
|
17
|
+
section_name = getattr(djxi_settings, "DX_SECTION_TAG", None)
|
|
18
|
+
assert section_name is not None
|
|
19
|
+
assert section_name == "my-section-tag"
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
""" Test compatibility of inline sections with Django Templating Tags and Filter"""
|
|
2
|
+
from django.test import override_settings, RequestFactory, modify_settings
|
|
3
|
+
from django.urls import path, include, resolve, reverse
|
|
4
|
+
|
|
5
|
+
from djxi.actions import DxActionRouter, dx_route
|
|
6
|
+
|
|
7
|
+
INLINE_TEMPLATE = """
|
|
8
|
+
<dx-section name="hello-world">
|
|
9
|
+
<p>Hello, {{ name }}!</p>
|
|
10
|
+
</dx-section>
|
|
11
|
+
<dx-section name="hello-many">
|
|
12
|
+
<p>Hello {% for name in names %}{{ name }}{% if not forloop.last %}, {% endif %}{% endfor %}!</p>
|
|
13
|
+
</dx-section>
|
|
14
|
+
<dx-section name="hello-filter">
|
|
15
|
+
<p>{{ number|floatformat:3 }}</p>
|
|
16
|
+
</dx-section>
|
|
17
|
+
<dx-section name="hello-humanize">
|
|
18
|
+
{% load humanize %}
|
|
19
|
+
<p>{{ number|intword }}</p>
|
|
20
|
+
</dx-section>
|
|
21
|
+
<dx-section name="hello-include">
|
|
22
|
+
<p>Hello {% for name in names %}{% include "partial.html" %}{% endfor %}!</p>
|
|
23
|
+
</dx-section>
|
|
24
|
+
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class InlineActionRouter(DxActionRouter):
|
|
29
|
+
section_inline = INLINE_TEMPLATE
|
|
30
|
+
|
|
31
|
+
@dx_route("hello/<str:name>", methods=["GET"], name="hello_world")
|
|
32
|
+
def hallo_du(self, request, name: str):
|
|
33
|
+
context = {"name": name}
|
|
34
|
+
return self.render_section(request, "hello-world", context)
|
|
35
|
+
|
|
36
|
+
@dx_route("hello-all", methods=["GET"], name="hello_many")
|
|
37
|
+
def hello_many(self, request):
|
|
38
|
+
context = {"names": ["Django", "World", "Python"]}
|
|
39
|
+
return self.render_section(request, "hello-many", context)
|
|
40
|
+
|
|
41
|
+
@dx_route("hello-filter/<str:number>", methods=["GET"])
|
|
42
|
+
def hello_filter(self, request, number: str):
|
|
43
|
+
context = {"number": number}
|
|
44
|
+
return self.render_section(request, "hello-filter", context)
|
|
45
|
+
|
|
46
|
+
@dx_route("hello-humanize/<str:number>", methods=["GET"])
|
|
47
|
+
def hello_humanize(self, request, number: str):
|
|
48
|
+
return self.render_section(request, "hello-humanize", {"number": number})
|
|
49
|
+
|
|
50
|
+
@dx_route("hello_include", methods=["GET"])
|
|
51
|
+
def hello_include(self, request):
|
|
52
|
+
context = {"names": ["Django", "World", "Python"]}
|
|
53
|
+
return self.render_section(request, "hello-include", context)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# URL Patterns from .dx_router
|
|
57
|
+
urlpatterns = [
|
|
58
|
+
path("", include((InlineActionRouter.dx_router(), "djxi"), namespace="djxi"))
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@override_settings(ROOT_URLCONF=__name__)
|
|
63
|
+
def test_value_interpolation():
|
|
64
|
+
rf = RequestFactory()
|
|
65
|
+
resolver_match = resolve(reverse("djxi:hello_world", kwargs={"name": "Django"}))
|
|
66
|
+
req = rf.get(resolver_match.url_name)
|
|
67
|
+
response = resolver_match.func(req, *resolver_match.args, **resolver_match.kwargs)
|
|
68
|
+
assert req.method == "GET"
|
|
69
|
+
assert response.status_code == 200
|
|
70
|
+
assert response.content.decode().strip() == "<p>Hello, Django!</p>"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@override_settings(ROOT_URLCONF=__name__)
|
|
74
|
+
def test_builtin_templatetags():
|
|
75
|
+
rf = RequestFactory()
|
|
76
|
+
resolver_match = resolve(reverse("djxi:hello_many"))
|
|
77
|
+
req = rf.get(resolver_match.url_name)
|
|
78
|
+
response = resolver_match.func(req, *resolver_match.args, **resolver_match.kwargs)
|
|
79
|
+
assert req.method == "GET"
|
|
80
|
+
assert response.status_code == 200
|
|
81
|
+
assert response.content.decode().strip() == "<p>Hello Django, World, Python!</p>"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@override_settings(ROOT_URLCONF=__name__)
|
|
85
|
+
def test_template_include():
|
|
86
|
+
rf = RequestFactory()
|
|
87
|
+
resolver_match = resolve(reverse("djxi:hello_include"))
|
|
88
|
+
req = rf.get(resolver_match.url_name)
|
|
89
|
+
response = resolver_match.func(req, *resolver_match.args, **resolver_match.kwargs)
|
|
90
|
+
assert req.method == "GET"
|
|
91
|
+
assert response.status_code == 200
|
|
92
|
+
assert response.content.decode().strip() == "<p>Hello Django & World & Python!</p>"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@override_settings(ROOT_URLCONF=__name__)
|
|
96
|
+
def test_buildin_filter():
|
|
97
|
+
rf = RequestFactory()
|
|
98
|
+
resolver_match = resolve(
|
|
99
|
+
reverse("djxi:hello_filter", kwargs={"number": "108.999212"})
|
|
100
|
+
)
|
|
101
|
+
req = rf.get(resolver_match.url_name)
|
|
102
|
+
response = resolver_match.func(req, *resolver_match.args, **resolver_match.kwargs)
|
|
103
|
+
assert req.method == "GET"
|
|
104
|
+
assert response.status_code == 200
|
|
105
|
+
assert response.content.decode().strip() == "<p>108.999</p>"
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@modify_settings(INSTALLED_APPS={"append": "django.contrib.humanize"})
|
|
109
|
+
@override_settings(ROOT_URLCONF=__name__)
|
|
110
|
+
def test_opt_in_tags():
|
|
111
|
+
rf = RequestFactory()
|
|
112
|
+
resolver_match = resolve(
|
|
113
|
+
reverse("djxi:hello_humanize", kwargs={"number": "1000000"})
|
|
114
|
+
)
|
|
115
|
+
req = rf.get(resolver_match.url_name)
|
|
116
|
+
response = resolver_match.func(req, *resolver_match.args, **resolver_match.kwargs)
|
|
117
|
+
assert req.method == "GET"
|
|
118
|
+
assert response.status_code == 200
|
|
119
|
+
assert response.content.decode().strip() == "<p>1.0 million</p>"
|