Coder Social home page Coder Social logo

bckohan / django-render-static Goto Github PK

View Code? Open in Web Editor NEW
18.0 1.0 1.0 798 KB

Use Django's template engine(s) to render static files at deployment or package time. Includes transpilers for extending Django's url reversal and enums to JavaScript.

Home Page: https://django-render-static.readthedocs.io

License: MIT License

Python 98.67% HTML 0.91% Jinja 0.07% JavaScript 0.35%
django javascript python static-files templates defines enum enums reverse transpiler

django-render-static's Introduction

License: MIT PyPI version PyPI pyversions PyPI djversions PyPI status Documentation Status Code Cov Test Status Code Style

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:
pip install django-render-static
  1. Add 'render_static' to your INSTALLED_APPS :
INSTALLED_APPS = [
    'render_static',
]
  1. Add a STATIC_TEMPLATES configuration directive to your settings file:
STATIC_TEMPLATES = {
    'templates' : [
        ('path/to/template':, {'context' {'variable': 'value'})
    ]
}
  1. Run renderstatic preceding every run of collectstatic :
$> 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:

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:

{% 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:

STATIC_TEMPLATES = {
    'templates': [
        'examples/defines.js'
    ]
}

And then of course they would call renderstatic before collectstatic:

$> ./manage.py renderstatic
$> ./manage.py collectstatic

This would create the following file::

.
└── examples
    └── static
        └── examples
            └── defines.js

Which would look like this:

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:

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:


    {% enums_to_js enums="examples.models.ExampleModel.Color" %}

It will contain a javascript class transpilation of the Color enum that looks like this:

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:

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:

    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:

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/<int:arg1>', MyView.as_view(), name='simple'),
    path('different/<int:arg1>/<str:arg2>', 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:

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:

from render_static.placeholders import register_variable_placeholder

app_name = 'year_app'
urlpatterns = [
    re_path(r'^fetch/(?P<year>\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!):

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/<year:year>', YearView.as_view(), name='fetch_year')
]

django-render-static's People

Contributors

bckohan avatar dependabot[bot] avatar robbiefernandez avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

Forkers

robbiefernandez

django-render-static's Issues

urls_to_js should gracefully handle default kwargs supplied to path()

Django seems to support path resolution based on default kwargs that are not part of the named parameters on a path. See #65.

Should make a good faith effort to support path resolution based on these parameters. It would involve modifying match() in the javascript to look for named parameters with specific values. This can become tricky because type becomes important. '1' and 1 are treated differently by reverse in the context of a named default parameter value. Not all types will easily extend to Javascript, so some restriction and possible warning should be thrown.

This is a corner case - good patterns should not rely on this and should instead use named parameters with converters that restrict the values if necessary. This pattern has been seen in the wild tho and urls_to_js should "just work" as much as it can.

The key thing to note here is that these default kwargs are essentially named path() parameters for the purposes of reverse - but they lack converters. That this "works" in Django seems almost to be by accident.

Support batch rendering & glob patterns in template selectors

The static template engine is fundamentally different from the dynamic template engine because of the batch-render use case. In a batch-render the loader should return multiple matching files, that each get rendered with the given context.

This change would enable loaders to accept glob patterns and the like. File load should end when the first loader to return non-empty list of files returns.

This has implications for dest. Namely that when multiple files are returned dest must be a directory.

Support 'package' time rendering

Right now, render_static is meant to be run at deployment time, before collectstatic is run. This works best for apps that are part of source of the site being deployed.

Reusable apps packaged into repositories like pypi would want to generate their static files at package time. Is there anything that can be done in render_static to explicitly support this? It would be possible to bootstrap this currently.

Provide Pluggable Visitor Pattern to customize generated javascript

As the title says, currently the javascript generated can't be customized. Implement a visitor pattern that will allow users to customize the javascript generated by all the template filters. Also provide a visitor that generates es6 classes that are more minification friendly for urls_to_js.

Change templates config parameter to be a list of tuples.

Its currently impossible to render one template multiple times using different contexts and destinations using just the settings configuration -- this IS possible using multiple invocations of renderstatic. The constraint on settings however is unnecessary.

Change STATIC_TEMPLATES['templates'] to optionally be a list of (template name, {config}) tuples.

Document deployment time vs package time use cases.

Depending on the static templates rendered there are two major use cases for static rendering. Deployment time for things like urls.js and package time for self-contained app artifacts. The nuances involved deserve discussion in the docs.

Support backup destination to STATIC_ROOT on permissions error

If static_templates are delivered with a package downloaded from pypi into a repo, render_static might not have permissions to render those files into the app's static directories. Default behavior should be to use STATIC_ROOT as a backup in the case of permissions error.

Current workaround is to just provide a destination.

Bound complexity of URL Generation

For urls with large numbers of arguments for which specific placeholders are not registered, the guess and check mechanism could produce a complexity explosion. This is a O(n^p) operation where n is the number of potential placeholders to try and p is the number of arguments in the URL.

There should be a, potentially configurable, hard limit on the number of iterations of the guess and check mechanism. An exception should bound the number of iterations of each reversal attempt, which should indicate to the user that more specific placeholders should be registered to speed up reversal success.

Jinja2 include breaks Jinja2 as optional dependency

File "/Users/bkohan/Library/Caches/pypoetry/virtualenvs/genesis-ed7ge2WL-py3.6/lib/python3.6/site-packages/render_static/engine.py", line 11, in
from django.template.backends.jinja2 import Template as Jinja2Template
File "/Users/bkohan/Library/Caches/pypoetry/virtualenvs/genesis-ed7ge2WL-py3.6/lib/python3.6/site-packages/django/template/backends/jinja2.py", line 3, in
import jinja2
ModuleNotFoundError: No module named 'jinja2'

Add set of common placeholders to always try

Most url arguments are fairly simple and most cases can be handled with a common set of simple string and integer placeholders. Add these to the placeholders to try for url reversal to drastically increase the number of urls that work "out of the box".

Flexible context specifiers

Allow contexts to be loaded from:

  • json files
  • pickled dictionaries
  • python modules (locals)
  • yaml files
  • string file paths, path like objects or packaged resources of any of the above

Deprecate es5 support.

Deprecate all es5 specific code generation and move it into separate transpilers. This is now considered beyond the scope of django-render-static and rendered code should be run through an external transpiler like babel if es5 compat is required.

Unnamed/named urls of the same name sometimes fail

This is wrong:

urlpatterns = [
    re_path(r'^special1/(?P<choice>(:?first)|(:?second))$', TestView.as_view(), name='special'),
    re_path(r'^special1/(?P<choice>(:?first)|(:?second))/(?P<choice1>(:?first)|(:?second))$', TestView.as_view(), name='special'),
    re_path(r'^special2/((?:first)|(?:second))$', TestView.as_view(), name='special')
]
urls = {
  "special": (kwargs={}, args=[]) => {
	  if (this.match(kwargs, args, 1)) { return `/special2/${args[0]}`; }
  },
}

And when reversed, it throws an exception:

urlpatterns = [
    re_path(r'^special2/((?:first)|(?:second))$', TestView.as_view(), name='special'),
    re_path(r'^special1/(?P<choice>(:?first)|(:?second))$', TestView.as_view(), name='special'),
    re_path(r'^special1/(?P<choice>(:?first)|(:?second))/(?P<choice1>(:?first)|(:?second))$', TestView.as_view(), name='special'),
]

Rename render_static -> renderstatic

This might seem overly pedantic, but given the goal to work calls to render_static into deployment processes it would be nice if the casing type was consistent. i.e:

./manage.py renderstatic
./manage.py collectstatic

looks better than:

./manage.py render_static
./manage.py collectstatic

Should deprecate the snake case name and remove in version 2.0.

urls_to_js output is incorrect when default kwargs specified in path()

    path(
        'qr/<int:qr>/download',
        QRDownloadView.as_view(),
        kwargs={'format': 'png'},
        name='download_qr'
    ),
    path(
        'qr/<int:qr>/download/png',
        QRDownloadView.as_view(),
        kwargs={'format': 'png'},
        name='download_qr'
    ),
    path(
        'qr/<int:qr>/download/svg',
        QRDownloadView.as_view(),
        kwargs={'format': 'svg'},
        name='download_qr'
    ),
    path(
        'qr',
        QRDownloadView.as_view(),
        kwargs={'format': 'svg'},
        name='download_qr'
    )

produces this:

"download_qr": (kwargs={}, args=[]) => {
      kwargs={'qr': {'converter': <class 'django.urls.converters.IntConverter'>, 'app_name': 'demoply.qr'}} */
      kwargs={'qr': {'converter': <class 'django.urls.converters.IntConverter'>, 'app_name': 'demoply.qr'}} */
      if (this.match(kwargs, args, ['qr'])) { return `/qr/${kwargs["qr"]}/download/svg`; }
      if (this.match(kwargs, args)) { return "/qr"; }
},

Enum support

Provide a python enum based pattern and JS translation tags to single source Model fields with choices.

Require importlib-resources for python < 3.9

The resource() function will automatically work in python >3.9. In 3.7-3.8 it requires importlib_resources to be installed as an optional dependency. Change this dependency to required for python versions below 3.9.

Allow 'lazy' contexts built after Django bootstrapping

Right now, static files are limited to context's specifiable directly in settings. Richer behavior can be produced if implemented directly in template tags, but there should be a way to build a context off the Django stack post-initialization (i.e. have access to the database).

Test re_path nested arguments

From the Django docs, this should work:

from django.urls import re_path

urlpatterns = [
    re_path(r'^blog/(page-([0-9]+)/)?$', blog_articles),                  # bad
    re_path(r'^comments/(?:page-(?P<page_number>[0-9]+)/)?$', comments),  # good
]

Extension points for transpiled code.

Should be able to do something like this:

{% enums_to_js enums=enums %}
  
  {% override 'get' %}
  static get(value) {
     if ( < custom value mapping >) {
         return {{ class_name }}.ENUM_VALUE;
     }
     {{ default_impl }} <!-- transpiler puts default get function code here -->
  }
  {% endoverride %}

{% endenums_to_js %}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.