From 97c611b2be0040c7a10147631466dce34ba66fdf Mon Sep 17 00:00:00 2001 From: Brian Kohan Date: Thu, 11 Apr 2024 15:33:19 -0700 Subject: [PATCH 1/4] fix #141 --- doc/source/changelog.rst | 5 ++ pyproject.toml | 2 +- render_static/__init__.py | 2 +- render_static/transpilers/urls_to_js.py | 110 ++++++++++++------------ 4 files changed, 61 insertions(+), 58 deletions(-) diff --git a/doc/source/changelog.rst b/doc/source/changelog.rst index 7255800..5fba417 100644 --- a/doc/source/changelog.rst +++ b/doc/source/changelog.rst @@ -2,6 +2,11 @@ Change Log ========== +v2.2.1 +====== + +* Fixed `Custom URL converts may expect reversal kwargs to be of a given type. `_ + v2.2.0 ====== diff --git a/pyproject.toml b/pyproject.toml index 74c2cec..74663b9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "django-render-static" -version = "2.2.0" +version = "2.2.1" description = "Use Django's template engine to render static files at deployment or package time. Includes transpilers for extending Django's url reversal and enums to JavaScript." authors = ["Brian Kohan "] license = "MIT" diff --git a/render_static/__init__.py b/render_static/__init__.py index 13d0209..e980a9b 100755 --- a/render_static/__init__.py +++ b/render_static/__init__.py @@ -12,7 +12,7 @@ from .transpilers.enums_to_js import EnumClassWriter from .transpilers.urls_to_js import ClassURLWriter, SimpleURLWriter -VERSION = (2, 2, 0) +VERSION = (2, 2, 1) __title__ = "Django Render Static" __version__ = ".".join(str(i) for i in VERSION) diff --git a/render_static/transpilers/urls_to_js.py b/render_static/transpilers/urls_to_js.py index deb3fd9..e137754 100644 --- a/render_static/transpilers/urls_to_js.py +++ b/render_static/transpilers/urls_to_js.py @@ -490,68 +490,66 @@ def get_params(pattern: Union[RoutePattern, RegexPattern]) -> Dict[str, Any]: placeholder_url = reverse( qname, kwargs={**kwargs, **(endpoint.default_args or {})} ) + except (NoReverseMatch, TypeError, AttributeError): + continue - replacements = [] - - mtch = composite_regex.search(placeholder_url.lstrip("/")) - - if mtch: - # there might be group matches that aren't part of - # our kwargs, we go through this extra work to - # make sure we aren't subbing spans that aren't - # kwargs - grp_mp = { - idx: var for var, idx in composite_regex.groupindex.items() - } - - for idx, value in enumerate( # pylint: disable=W0612 - mtch.groups(), start=1 - ): - if unnamed: + replacements = [] + + mtch = composite_regex.search(placeholder_url.lstrip("/")) + + if mtch: + # there might be group matches that aren't part of + # our kwargs, we go through this extra work to + # make sure we aren't subbing spans that aren't + # kwargs + grp_mp = { + idx: var for var, idx in composite_regex.groupindex.items() + } + + for idx, value in enumerate( # pylint: disable=W0612 + mtch.groups(), start=1 + ): + if unnamed: + replacements.append((mtch.span(idx), Substitute(idx - 1))) + else: + # if the regex has non-capturing groups we + # need to filter those out + if idx in grp_mp: replacements.append( - (mtch.span(idx), Substitute(idx - 1)) + (mtch.span(idx), Substitute(grp_mp[idx])) ) - else: - # if the regex has non-capturing groups we - # need to filter those out - if idx in grp_mp: - replacements.append( - (mtch.span(idx), Substitute(grp_mp[idx])) - ) - - url_idx = 0 - path = [] - for rpl in replacements: - while url_idx <= rpl[0][0]: - path.append(placeholder_url[url_idx]) - url_idx += 1 - path.append(rpl[1]) - url_idx += rpl[0][1] - rpl[0][0] - if url_idx < len(placeholder_url): - path.append(placeholder_url[url_idx:]) - - yield from self.visit_path( - path, - list(kwargs.keys()), - endpoint.default_args if num_patterns > 1 else None, - ) - else: - # if we're here it means this path was overridden - # further down the tree - yield ( - f"/* Path {composite_regex.pattern} overruled " - "with: " - + ( - f"args={unnamed} */" - if unnamed - else f"kwargs={list(params.keys())} */" - ) + url_idx = 0 + path = [] + for rpl in replacements: + while url_idx <= rpl[0][0]: + path.append(placeholder_url[url_idx]) + url_idx += 1 + path.append(rpl[1]) + url_idx += rpl[0][1] - rpl[0][0] + if url_idx < len(placeholder_url): + path.append(placeholder_url[url_idx:]) + + yield from self.visit_path( + path, + list(kwargs.keys()), + endpoint.default_args if num_patterns > 1 else None, + ) + + else: + # if we're here it means this path was overridden + # further down the tree + yield ( + f"/* Path {composite_regex.pattern} overruled " + "with: " + + ( + f"args={unnamed} */" + if unnamed + else f"kwargs={list(params.keys())} */" ) - return + ) + return - except NoReverseMatch: - continue else: # this is a simple url with no params if not composite_regex.search(reverse(qname).lstrip("/")): From cca7b713919df7c908ef9fc15b7ad82d7c560b0a Mon Sep 17 00:00:00 2001 From: Brian Kohan Date: Thu, 11 Apr 2024 15:57:04 -0700 Subject: [PATCH 2/4] fix #140 --- .github/workflows/test.yml | 2 +- CONTRIBUTING.md | 83 ++++++++ CONTRIBUTING.rst | 95 --------- README.md | 354 +++++++++++++++++++++++++++++++++ README.rst | 393 ------------------------------------- doc/source/changelog.rst | 1 + pyproject.toml | 2 +- 7 files changed, 440 insertions(+), 490 deletions(-) create mode 100644 CONTRIBUTING.md delete mode 100644 CONTRIBUTING.rst create mode 100644 README.md delete mode 100644 README.rst diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 853adaa..48d9524 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -49,7 +49,7 @@ jobs: poetry check poetry run pip check poetry export --without-hashes --format=requirements.txt | poetry run safety check --stdin - poetry run python -m readme_renderer ./README.rst -o /tmp/README.html + poetry run python -m readme_renderer ./README.md -o /tmp/README.html cd ./doc poetry run doc8 --ignore-path build --max-line-length 100 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..8c1b64a --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,83 @@ +# Contributing + +Contributions are encouraged and welcome!! Please use the issue page to submit feature requests or +bug reports. Issues with attached PRs will be given priority and have a much higher likelihood of +acceptance. Please also open an issue and associate it with any submitted PRs. + +We are actively seeking additional maintainers. If you're interested, please +[contact me](https://github.com/bckohan). + + +## Installation + +`django-render-static` uses [Poetry](https://python-poetry.org/) for environment, package and +dependency management. [Poetry](https://python-poetry.org/) greatly simplifies environment +bootstrapping. Once it's installed. + +```shell +poetry install -E all +``` + +### External Dependencies + +Some of the tests require [npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) +to be installed. + +## Documentation + +`django-render-static` documentation is generated using +[Sphinx](https://www.sphinx-doc.org/en/master/) with the [readthedocs](https://readthedocs.org/) +theme. Any new feature PRs must provide updated documentation for the features added. To build +the docs run: + +```shell +cd ./doc +poetry run doc8 --ignore-path build --max-line-length 100 +poetry run make html +``` + +## Static Analysis + +`django-render-static` uses [Pylint](https://www.pylint.org/) for Python linting and +[mypy](http://mypy-lang.org/) for type checking. Header imports are also standardized using +[isort](https://pycqa.github.io/isort/). Before any PR is accepted the following must be run, and +static analysis tools should not produce any errors or warnings. Disabling certain errors or +warnings where justified is acceptable: + +```shell +poetry run isort render_static +poetry run black render_static +poetry run mypy render_static +poetry run pylint render_static +poetry check +poetry run pip check +poetry run python -m readme_renderer ./README.md +``` + +## Running Tests + +`django-render-static` is setup to use +[django-pytest](https://pytest-django.readthedocs.io/en/latest/) to allow +[pytest](https://docs.pytest.org/en/stable/) to run Django unit tests. All the tests are housed in +render_static/tests/tests.py. Before a PR is accepted, all tests must be passing and the code +coverage must be at 100%. + +To run the full suite: + +```shell +poetry run pytest +``` + +To run a single test, or group of tests in a class: + +```shell +poetry run pytest ::ClassName::FunctionName +``` + +For instance to run all tests in DefinesToJavascriptTest, and then just the test_classes_to_js test +you would do: + +```shell +poetry run pytest render_static/tests/tests.py::DefinesToJavascriptTest +poetry run pytest render_static/tests/tests.py::DefinesToJavascriptTest::test_classes_to_js +``` diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst deleted file mode 100644 index 3553e03..0000000 --- a/CONTRIBUTING.rst +++ /dev/null @@ -1,95 +0,0 @@ -.. _Poetry: https://python-poetry.org/ -.. _Pylint: https://www.pylint.org/ -.. _isort: https://pycqa.github.io/isort/ -.. _mypy: http://mypy-lang.org/ -.. _django-pytest: https://pytest-django.readthedocs.io/en/latest/ -.. _pytest: https://docs.pytest.org/en/stable/ -.. _Sphinx: https://www.sphinx-doc.org/en/master/ -.. _readthedocs: https://readthedocs.org/ -.. _me: https://github.com/bckohan -.. _npm: https://docs.npmjs.com/downloading-and-installing-node-js-and-npm - -Contributing -############ - -Contributions are encouraged and welcome!! Please use the issue page to submit feature requests or -bug reports. Issues with attached PRs will be given priority and have a much higher likelihood of -acceptance. Please also open an issue and associate it with any submitted PRs. - -We are actively seeking additional maintainers. If you're interested, please contact me_. - - -Installation ------------- - -`django-render-static` uses Poetry_ for environment, package and dependency management. Poetry_ -greatly simplifies environment bootstrapping. Once it's installed. - -.. code-block:: - - poetry install -E all - -External Dependencies -~~~~~~~~~~~~~~~~~~~~~ - -Some of the tests require npm_ to be installed. - -Documentation -------------- - -`django-render-static` documentation is generated using Sphinx_ with the readthedocs_ theme. Any -new feature PRs must provide updated documentation for the features added. To build the docs run: - -.. code-block:: - - cd ./doc - poetry run doc8 --ignore-path build --max-line-length 100 - poetry run make html - - -Static Analysis ---------------- - -`django-render-static` uses mypy_ for static type analysis, and Pylint_ for python linting. -Header imports are also standardized using isort_. Before any PR is accepted the following must be -run, and static analysis tools should not produce any errors or warnings. Disabling certain errors -or warnings where justified is acceptable: - -.. code-block:: - - poetry run isort render_static - poetry run black render_static - poetry run mypy render_static - poetry run pylint render_static - poetry check - poetry run pip check - poetry run python -m readme_renderer ./README.rst - - -Running Tests -------------- - -`django-render-static` is setup to use django-pytest_ to allow pytest_ to run Django unit tests. -All the tests are housed in render_static/tests/tests.py. Before a PR is accepted, all -tests must be passing and the code coverage must be at 100%. - -To run the full suite: - -.. code-block:: - - poetry run pytest - -To run a single test, or group of tests in a class: - -.. code-block:: - - poetry run pytest ::ClassName::FunctionName - -For instance to run all tests in DefinesToJavascriptTest, and then just the test_classes_to_js test -you would do: - -.. code-block:: - - poetry run pytest render_static/tests/tests.py::DefinesToJavascriptTest - poetry run pytest render_static/tests/tests.py::DefinesToJavascriptTest::test_classes_to_js - diff --git a/README.md b/README.md new file mode 100644 index 0000000..6720bee --- /dev/null +++ b/README.md @@ -0,0 +1,354 @@ +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) +[![PyPI version](https://badge.fury.io/py/django-render-static.svg)](https://pypi.python.org/pypi/django-render-static/) +[![PyPI pyversions](https://img.shields.io/pypi/pyversions/django-render-static.svg)](https://pypi.python.org/pypi/django-render-static/) +[![PyPI djversions](https://img.shields.io/pypi/djversions/django-render-static.svg)](https://pypi.org/project/django-render-static/) +[![PyPI status](https://img.shields.io/pypi/status/django-render-static.svg)](https://pypi.python.org/pypi/django-render-static) +[![Documentation Status](https://readthedocs.org/projects/django-render-static/badge/?version=latest)](http://django-render-static.readthedocs.io/?badge=latest/) +[![Code Cov](https://codecov.io/gh/bckohan/django-render-static/branch/main/graph/badge.svg?token=0IZOKN2DYL)](https://codecov.io/gh/bckohan/django-render-static) +[![Test Status](https://github.com/bckohan/django-render-static/workflows/test/badge.svg)](https://github.com/bckohan/django-render-static/actions/workflows/test.yml) +[![Code Style](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) + +# django-render-static + +Use Django's template engines to render static files that are collected +during the ``collectstatic`` routine and likely served above Django at runtime. +Files rendered by django-render-static are immediately available to participate +in the normal static file collection pipeline. + +For example, a frequently occurring pattern that violates the +[DRY principle](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself) is the presence of defines, +or enum like structures in server side Python code that are simply replicated in client side +JavaScript. Another example might be rebuilding Django URLs from arguments in a +[Single Page Application](https://en.wikipedia.org/wiki/Single-page_application). Single-sourcing +these structures by transpiling client side code from the server side code keeps the stack bone DRY. + +**`django-render-static` includes Python to Javascript transpilers for:** + +* Django's `reverse` function (`urls_to_js`) +* PEP 435 style Python enumerations (`enums_to_js`) +* Plain data define-like structures in Python classes and modules + (`defines_to_js`) + +Transpilation is extremely flexible and may be customized by using override blocks or extending the provided +transpilers. + +`django-render-static` also formalizes the concept of a package-time or deployment-time +static file rendering step. It piggybacks off the existing templating engines and configurations +and should therefore be familiar to Django developers. It supports both standard Django templating +and Jinja templates and allows contexts to be specified in python, json or YAML. + +You can report bugs and discuss features on the +[issues page](https://github.com/bckohan/django-render-static/issues). + +[Contributions](https://github.com/bckohan/django-render-static/blob/main/CONTRIBUTING.rst) are +encouraged! + +[Full documentation at read the docs.](https://django-render-static.readthedocs.io/en/latest/) + +## Installation + +1. Clone django-render-static from [GitHub](http://github.com/bckohan/django-render-static) or install a release off [PyPI](http://pypi.python.org/pypi/django-render-static): + +```shell +pip install django-render-static +``` + +2. Add 'render_static' to your ``INSTALLED_APPS`` : + +```python +INSTALLED_APPS = [ + 'render_static', +] +``` + +3. Add a ``STATIC_TEMPLATES`` configuration directive to your settings file: + +```python + +STATIC_TEMPLATES = { + 'templates' : [ + ('path/to/template':, {'context' {'variable': 'value'}) + ] +} +``` + +4. Run ``renderstatic`` preceding every run of ``collectstatic`` : + +```shell +$> manage.py renderstatic +$> manage.py collectstatic +``` + +## Usage + +### Transpiling Model Field Choices + +You have an app with a model with a character field that has several valid choices defined in an +enumeration type way, and you'd like to export those defines to JavaScript. You'd like to include +a template for other's using your app to use to generate a defines.js file. Say your app structure +looks like this:: + + . + └── examples + ├── __init__.py + ├── apps.py + ├── defines.py + ├── models.py + ├── static_templates + │   └── examples + │   └── defines.js + └── urls.py + + +Your defines/model classes might look like this: + +```python +class ExampleModel(Defines, models.Model): + + DEFINE1 = 'D1' + DEFINE2 = 'D2' + DEFINE3 = 'D3' + DEFINES = ( + (DEFINE1, 'Define 1'), + (DEFINE2, 'Define 2'), + (DEFINE3, 'Define 3') + ) + + define_field = models.CharField(choices=DEFINES, max_length=2) +``` + +And your defines.js template might look like this: + +```js+django +{% defines_to_js modules="examples.models" %} +``` + +If someone wanted to use your defines template to generate a JavaScript version of your Python +class their settings file might look like this: + +```python +STATIC_TEMPLATES = { + 'templates': [ + 'examples/defines.js' + ] +} +``` + + +And then of course they would call `renderstatic` before `collectstatic`: + +```shell +$> ./manage.py renderstatic +$> ./manage.py collectstatic +``` + +This would create the following file:: + + . + └── examples + └── static + └── examples + └── defines.js + +Which would look like this: + +```javascript +const defines = { + ExampleModel: { + DEFINE1: "D1", + DEFINE2: "D2", + DEFINE3: "D3", + DEFINES: [["D1", "Define 1"], ["D2", "Define 2"], ["D3", "Define 3"]] + } +}; +``` + +### Transpiling Enumerations + +Say instead of the usual choices tuple you're using PEP 435 style python enumerations as model +fields using [django-enum](http://pypi.python.org/pypi/django-enum) and +[enum-properties](http://pypi.python.org/pypi/enum-properties). For example we might define a +simple color enumeration like so: + +```python +from django.db import models +from django_enum import EnumField, TextChoices +from enum_properties import p, s + +class ExampleModel(models.Model): + + class Color(TextChoices, s('rgb'), s('hex', case_fold=True)): + + # name value label rgb hex + RED = 'R', 'Red', (1, 0, 0), 'ff0000' + GREEN = 'G', 'Green', (0, 1, 0), '00ff00' + BLUE = 'B', 'Blue', (0, 0, 1), '0000ff' + + color = EnumField(Color, null=True, default=None) +``` + +If we define an enum.js template that looks like this: + +```js+django + + {% enums_to_js enums="examples.models.ExampleModel.Color" %} +``` + +It will contain a javascript class transpilation of the Color enum that looks +like this: + +```javascript + +class Color { + + static RED = new Color("R", "RED", "Red", [1, 0, 0], "ff0000"); + static GREEN = new Color("G", "GREEN", "Green", [0, 1, 0], "00ff00"); + static BLUE = new Color("B", "BLUE", "Blue", [0, 0, 1], "0000ff"); + + constructor (value, name, label, rgb, hex) { + this.value = value; + this.name = name; + this.label = label; + this.rgb = rgb; + this.hex = hex; + } + + toString() { + return this.value; + } + + static get(value) { + switch(value) { + case "R": + return Color.RED; + case "G": + return Color.GREEN; + case "B": + return Color.BLUE; + } + throw new TypeError(`No Color enumeration maps to value ${value}`); + } + + static [Symbol.iterator]() { + return [Color.RED, Color.GREEN, Color.BLUE][Symbol.iterator](); + } +} +``` + +We can now use our enumeration like so: + +```javascript +Color.BLUE === Color.get('B'); +for (const color of Color) { + console.log(color); +} +``` + +### Transpiling URL reversal + +You'd like to be able to call something like `reverse` on path names from your client JavaScript +code the same way you do from Python Django code. + +Your settings file might look like: + +```python + STATIC_TEMPLATES={ + 'ENGINES': [{ + 'BACKEND': 'render_static.backends.StaticDjangoTemplates', + 'OPTIONS': { + 'loaders': [ + ('render_static.loaders.StaticLocMemLoader', { + 'urls.js': '{% urls_to_js %}' + }) + ] + }, + }], + 'templates': ['urls.js'] + } +``` + +Then call `renderstatic` before `collectstatic`: + +```shell +$> ./manage.py renderstatic +$> ./manage.py collectstatic +``` + +If your root urls.py looks like this: + +```python +from django.contrib import admin +from django.urls import path + +from .views import MyView + +urlpatterns = [ + path('admin/', admin.site.urls), + path('simple', MyView.as_view(), name='simple'), + path('simple/', MyView.as_view(), name='simple'), + path('different//', MyView.as_view(), name='different'), +] +``` + +So you can now fetch paths like this, in a way that is roughly API-equivalent +to Django's `reverse` function: + +```javascript +import { URLResolver } from '/static/urls.js'; + +const urls = new URLResolver(); + +// /different/143/emma +urls.reverse('different', {kwargs: {'arg1': 143, 'arg2': 'emma'}}); + +// reverse also supports query parameters +// /different/143/emma?intarg=0&listarg=A&listarg=B&listarg=C +urls.reverse( + 'different', + { + kwargs: {arg1: 143, arg2: 'emma'}, + query: { + intarg: 0, + listarg: ['A', 'B', 'C'] + } + } +); +``` + +### URLGenerationFailed Exceptions & Placeholders + +If you encounter a ``URLGenerationFailed`` exception you most likely need to register a placeholder for the argument in question. A placeholder is just a string or object that can be coerced to a string that matches the regular expression for the argument: + +```python +from render_static.placeholders import register_variable_placeholder + +app_name = 'year_app' +urlpatterns = [ + re_path(r'^fetch/(?P\d{4})/$', YearView.as_view(), name='fetch_year') +] + +register_variable_placeholder('year', 2000, app_name=app_name) +``` + +Users should typically use a path instead of re_path and register their own custom converters when needed. Placeholders can be directly registered on the converter (and are then conveniently available to users of your app!): + +```python +from django.urls.converters import register_converter + +class YearConverter: + regex = '[0-9]{4}' + placeholder = 2000 # this attribute is used by `url_to_js` to reverse paths + + def to_python(self, value): + return int(value) + + def to_url(self, value): + return str(value) + + +register_converter(YearConverter, 'year') + +urlpatterns = [ + path('fetch/', YearView.as_view(), name='fetch_year') +] +``` diff --git a/README.rst b/README.rst deleted file mode 100644 index cdd1601..0000000 --- a/README.rst +++ /dev/null @@ -1,393 +0,0 @@ -|MIT license| |PyPI version fury.io| |PyPI pyversions| |PyPi djversions| |PyPI status| |Documentation Status| -|Code Cov| |Test Status| |Code Style| - -.. |MIT license| image:: https://img.shields.io/badge/License-MIT-blue.svg - :target: https://lbesson.mit-license.org/ - -.. |PyPI version fury.io| image:: https://badge.fury.io/py/django-render-static.svg - :target: https://pypi.python.org/pypi/django-render-static/ - -.. |PyPI pyversions| image:: https://img.shields.io/pypi/pyversions/django-render-static.svg - :target: https://pypi.python.org/pypi/django-render-static/ - -.. |PyPI djversions| image:: https://img.shields.io/pypi/djversions/django-render-static.svg - :target: https://pypi.org/project/django-render-static/ - -.. |PyPI status| image:: https://img.shields.io/pypi/status/django-render-static.svg - :target: https://pypi.python.org/pypi/django-render-static - -.. |Documentation Status| image:: https://readthedocs.org/projects/django-render-static/badge/?version=latest - :target: http://django-render-static.readthedocs.io/?badge=latest/ - -.. |Code Cov| image:: https://codecov.io/gh/bckohan/django-render-static/branch/main/graph/badge.svg?token=0IZOKN2DYL - :target: https://codecov.io/gh/bckohan/django-render-static - -.. |Test Status| image:: https://github.com/bckohan/django-render-static/workflows/test/badge.svg - :target: https://github.com/bckohan/django-render-static/actions - -.. |Code Style| image:: https://img.shields.io/badge/code%20style-black-000000.svg - :target: https://github.com/psf/black - - -django-render-static -####################### - -Use Django's template engines to render static files that are collected -during the ``collectstatic`` routine and likely served above Django at runtime. -Files rendered by django-render-static are immediately available to participate -in the normal static file collection pipeline. - -For example, a frequently occurring pattern that violates the `DRY principle `_ -is the presence of defines, or enum like structures in server side Python code that are simply replicated in client -side JavaScript. Another example might be rebuilding Django URLs from arguments in a `Single Page Application `_. -Single-sourcing these structures by transpiling client side code from the server side code keeps the stack bone DRY. - -`django-render-static` includes Python to Javascript transpilers for: - - Django's `reverse` function (`urls_to_js`) - - PEP 435 style Python enumerations (`enums_to_js`) - - Plain data define-like structures in Python classes and modules - (`defines_to_js`) - -Transpilation is extremely flexible and may be customized by using override blocks or extending the provided -transpilers. - -`django-render-static` also formalizes the concept of a package-time or deployment-time -static file rendering step. It piggybacks off the existing templating engines and configurations -and should therefore be familiar to Django developers. It supports both standard Django templating -and Jinja templates and allows contexts to be specified in python, json or YAML. - -You can report bugs and discuss features on the -`issues page `_. - -`Contributions `_ are -encouraged! - -`Full documentation at read the docs. `_ - -Installation ------------- - -1. Clone django-render-static from GitHub_ or install a release off PyPI_ : - -.. code:: bash - - pip install django-render-static - - -2. Add 'render_static' to your ``INSTALLED_APPS`` : - -.. code:: python - - INSTALLED_APPS = [ - 'render_static', - ] - - -3. Add a ``STATIC_TEMPLATES`` configuration directive to your settings file: - -.. code:: python - - STATIC_TEMPLATES = { - 'templates' : [ - ('path/to/template':, {'context' {'variable': 'value'}) - ] - } - - -4. Run ``renderstatic`` preceding every run of ``collectstatic`` : - -.. code:: bash - - $> manage.py renderstatic - $> manage.py collectstatic - - -.. _GitHub: http://github.com/bckohan/django-render-static -.. _PyPI: http://pypi.python.org/pypi/django-render-static -.. _django-enum: http://pypi.python.org/pypi/django-enum -.. _enum-properties: http://pypi.python.org/pypi/enum-properties - - -Usage ------ - -Transpiling Model Field Choices -------------------------------- - -You have an app with a model with a character field that has several valid choices defined in an -enumeration type way, and you'd like to export those defines to JavaScript. You'd like to include -a template for other's using your app to use to generate a defines.js file. Say your app structure -looks like this:: - - . - └── examples - ├── __init__.py - ├── apps.py - ├── defines.py - ├── models.py - ├── static_templates - │   └── examples - │   └── defines.js - └── urls.py - - -Your defines/model classes might look like this: - -.. code:: python - - class ExampleModel(Defines, models.Model): - - DEFINE1 = 'D1' - DEFINE2 = 'D2' - DEFINE3 = 'D3' - DEFINES = ( - (DEFINE1, 'Define 1'), - (DEFINE2, 'Define 2'), - (DEFINE3, 'Define 3') - ) - - define_field = models.CharField(choices=DEFINES, max_length=2) - - -And your defines.js template might look like this: - -.. code:: js+django - - {% defines_to_js modules="examples.models" %} - - -If someone wanted to use your defines template to generate a JavaScript version of your Python -class their settings file might look like this: - -.. code:: python - - STATIC_TEMPLATES = { - 'templates': [ - 'examples/defines.js' - ] - } - - -And then of course they would call `renderstatic` before `collectstatic`: - -.. code:: bash - - $> ./manage.py renderstatic - $> ./manage.py collectstatic - - -This would create the following file:: - - . - └── examples - └── static - └── examples - └── defines.js - -Which would look like this: - -.. code:: javascript - - const defines = { - ExampleModel: { - DEFINE1: "D1", - DEFINE2: "D2", - DEFINE3: "D3", - DEFINES: [["D1", "Define 1"], ["D2", "Define 2"], ["D3", "Define 3"]] - } - }; - - -Transpiling Enumerations ------------------------- - -Say instead of the usual choices tuple you're using PEP 435 style python -enumerations as model fields using django-enum_ and enum-properties_. For example -we might define a simple color enumeration like so: - -.. code:: python - - from django.db import models - from django_enum import EnumField, TextChoices - from enum_properties import p, s - - class ExampleModel(models.Model): - - class Color(TextChoices, s('rgb'), s('hex', case_fold=True)): - - # name value label rgb hex - RED = 'R', 'Red', (1, 0, 0), 'ff0000' - GREEN = 'G', 'Green', (0, 1, 0), '00ff00' - BLUE = 'B', 'Blue', (0, 0, 1), '0000ff' - - color = EnumField(Color, null=True, default=None) - -If we define an enum.js template that looks like this: - -.. code:: js+django - - {% enums_to_js enums="examples.models.ExampleModel.Color" %} - -It will contain a javascript class transpilation of the Color enum that looks -like this: - -.. code:: javascript - - class Color { - - static RED = new Color("R", "RED", "Red", [1, 0, 0], "ff0000"); - static GREEN = new Color("G", "GREEN", "Green", [0, 1, 0], "00ff00"); - static BLUE = new Color("B", "BLUE", "Blue", [0, 0, 1], "0000ff"); - - constructor (value, name, label, rgb, hex) { - this.value = value; - this.name = name; - this.label = label; - this.rgb = rgb; - this.hex = hex; - } - - toString() { - return this.value; - } - - static get(value) { - switch(value) { - case "R": - return Color.RED; - case "G": - return Color.GREEN; - case "B": - return Color.BLUE; - } - throw new TypeError(`No Color enumeration maps to value ${value}`); - } - - static [Symbol.iterator]() { - return [Color.RED, Color.GREEN, Color.BLUE][Symbol.iterator](); - } - } - -We can now use our enumeration like so: - -.. code:: javascript - - Color.BLUE === Color.get('B'); - for (const color of Color) { - console.log(color); - } - - -Transpiling URL reversal ------------------------- - -You'd like to be able to call something like `reverse` on path names from your client JavaScript -code the same way you do from Python Django code. - -Your settings file might look like: - -.. code:: python - - STATIC_TEMPLATES={ - 'ENGINES': [{ - 'BACKEND': 'render_static.backends.StaticDjangoTemplates', - 'OPTIONS': { - 'loaders': [ - ('render_static.loaders.StaticLocMemLoader', { - 'urls.js': '{% urls_to_js %}' - }) - ] - }, - }], - 'templates': ['urls.js'] - } - - -Then call `renderstatic` before `collectstatic`:: - - $> ./manage.py renderstatic - $> ./manage.py collectstatic - -If your root urls.py looks like this: - -.. code:: python - - from django.contrib import admin - from django.urls import path - - from .views import MyView - - urlpatterns = [ - path('admin/', admin.site.urls), - path('simple', MyView.as_view(), name='simple'), - path('simple/', MyView.as_view(), name='simple'), - path('different//', MyView.as_view(), name='different'), - ] - - -So you can now fetch paths like this, in a way that is roughly API-equivalent -to Django's `reverse` function: - -.. code:: javascript - - import { URLResolver } from '/static/urls.js'; - - const urls = new URLResolver(); - - // /different/143/emma - urls.reverse('different', {kwargs: {'arg1': 143, 'arg2': 'emma'}}); - - // reverse also supports query parameters - // /different/143/emma?intarg=0&listarg=A&listarg=B&listarg=C - urls.reverse( - 'different', - { - kwargs: {arg1: 143, arg2: 'emma'}, - query: { - intarg: 0, - listarg: ['A', 'B', 'C'] - } - } - ); - - -URLGenerationFailed Exceptions & Placeholders ---------------------------------------------- - -If you encounter a ``URLGenerationFailed`` exception you most likely need to register a placeholder for the argument in question. A placeholder is just a string or object that can be coerced to a string that matches the regular expression for the argument: - -.. code:: python - - from render_static.placeholders import register_variable_placeholder - - app_name = 'year_app' - urlpatterns = [ - re_path(r'^fetch/(?P\d{4})/$', YearView.as_view(), name='fetch_year') - ] - - register_variable_placeholder('year', 2000, app_name=app_name) - -Users should typically use a path instead of re_path and register their own custom converters when needed. Placeholders can be directly registered on the converter (and are then conveniently available to users of your app!): - -.. code:: python - - from django.urls.converters import register_converter - - class YearConverter: - regex = '[0-9]{4}' - placeholder = 2000 # this attribute is used by `url_to_js` to reverse paths - - def to_python(self, value): - return int(value) - - def to_url(self, value): - return str(value) - - - register_converter(YearConverter, 'year') - - urlpatterns = [ - path('fetch/', YearView.as_view(), name='fetch_year') - ] - - diff --git a/doc/source/changelog.rst b/doc/source/changelog.rst index 5fba417..829dbb4 100644 --- a/doc/source/changelog.rst +++ b/doc/source/changelog.rst @@ -6,6 +6,7 @@ v2.2.1 ====== * Fixed `Custom URL converts may expect reversal kwargs to be of a given type. `_ +* Fixed `Switch README and CONTRIBUTING to markdown. `_ v2.2.0 ====== diff --git a/pyproject.toml b/pyproject.toml index 74663b9..70cf1c5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ version = "2.2.1" description = "Use Django's template engine to render static files at deployment or package time. Includes transpilers for extending Django's url reversal and enums to JavaScript." authors = ["Brian Kohan "] license = "MIT" -readme = "README.rst" +readme = "README.md" repository = "https://github.com/bckohan/django-render-static" homepage = "https://django-render-static.readthedocs.io" keywords = ["django", "static", "templates", "javascript", "url", "reverse", "defines", "transpiler", "transpile", "enum"] From e4fb60ee8da7317baf70aecede895063cb11a10a Mon Sep 17 00:00:00 2001 From: Brian Kohan Date: Thu, 11 Apr 2024 15:57:36 -0700 Subject: [PATCH 3/4] add ValueError to the list of accepable intercepted exceptions for placeholder testing #141 --- render_static/transpilers/urls_to_js.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/render_static/transpilers/urls_to_js.py b/render_static/transpilers/urls_to_js.py index e137754..d70b4a1 100644 --- a/render_static/transpilers/urls_to_js.py +++ b/render_static/transpilers/urls_to_js.py @@ -490,7 +490,7 @@ def get_params(pattern: Union[RoutePattern, RegexPattern]) -> Dict[str, Any]: placeholder_url = reverse( qname, kwargs={**kwargs, **(endpoint.default_args or {})} ) - except (NoReverseMatch, TypeError, AttributeError): + except (NoReverseMatch, TypeError, AttributeError, ValueError): continue replacements = [] From 579d09b22832fbcb7769b2f29fabde07c1c2a526 Mon Sep 17 00:00:00 2001 From: Brian Kohan Date: Thu, 11 Apr 2024 15:59:41 -0700 Subject: [PATCH 4/4] add readme_renderer markdown dependency extras #140 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 70cf1c5..f39cbe9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,7 +68,7 @@ pytest-cov = "^4.1.0" pylint = "^3.0.0" deepdiff = "^6.7.0" safety = "^2.3.0" -readme-renderer = ">=42" +readme-renderer = {extras = ["md"], version = "^43.0"} types-PyYAML = "^6.0" coverage = "^7.3.0" importlib-metadata = "^7.0.0"