How to use django-compressor to compress form media offline - django

A Django template (containing a form with media) that includes a snippet like the one below would throw an error when using django-compressor with COMPRESS_OFFLINE=True, because form is not available when offline compression is executed:
# Template snippet
{% compress js %}
{{ form.media.js }}
{% endcompress %}
Generally speaking, django-compressor provides the COMPRESS_OFFLINE_CONTEXT setting to handle similar situations. However, if a site contains many such forms or widgets with media, this solution isn't ideal.
For example, currently, I do something like this in settings.py (for each widget's media):
# settings.py
...
from my_app1.widgets import Widget1
from my_app1.widgets import Widget2
from my_app1.widgets import Widget3
...
widgets = {
'my_widget1': Widget1(),
'my_widget2': Widget2(),
'my_widget3': Widget3(),
...
}
for name, widget in widgets.items():
COMPRESS_OFFLINE_CONTEXT['{}_css'.format(name)] = widget.media['css']
COMPRESS_OFFLINE_CONTEXT['{}_js'.format(name)] = widget.media['js']
And then in templates, I do this:
{% compress js %}
{{ my_widget1_js }}
{% endcompress %}
Is there a way to handle this situation in a manner that more closely resembles Django's {{ form.media }} method, or perhaps without needing to enumerate the specific media to every widget or every form (containing media) in the site?

You can simplify your settings by using some custom class with an __getattr__ method. That way if you will pass that object into template and try to call
{{ obj.my_widget1_js }}
logic inside __getattr__ can search for specified widget in your project and return it's media.
Example of class that will auto-discover each widget:
class WidgetImporter(object):
def __getattr__(self, name):
path = name.split('__') # this will split path on double underscore, so you can define submodule here
path = [(node, "".join(part.capitalize() for part in node.split('_'))) for node in path] # this will convert underscore_names to CamelCase names.
# determining for each module if it's name is CamelCased or underscored
real_path = []
for underscored, cameled in path:
try:
__import__(".".join(real_path + [underscored])) # first trying to import umderscored module
except ImportError:
try:
__import__(".".join(real_path + [cameled])) # now try with cameled
except ImportError:
return "" # or raise some exception here if you like
else:
real_path.append(cameled)
else:
real_path.append(underscored)
last_node = real_path[len(real_path) - 1]
return getattr(__import__(".".join(real_path), fromlist=[last_node]), last_node)() # returning actual class instance
That way you can use it inside your settings.py file:
COMPRESS_OFFLINE_CONTEXT = {
'widgets': WidgetImporter(),
}
and use in your template:
{{ widgets.my_app1__widgets__widget1.media.css }}
Class will try to resolve my_app1__widgets__widget1 to actual path to your class and will try to import it. If it succeeds, it's media['css'] will be printed. It's not 100% optimal code and 100% safe (there can be some vulnerabilities if someone has access to your templates), but it will do the job.

Related

How to access server data in a javascript file that is loaded by Django admin pages

I need to access server data in a javascript file that gets loaded with an admin page. In my case, I'll need a json variable that is set in the settings.py file (in the production version, this json-variable gets loaded from env-variable which means that the content of the variable is known at startup-time but is not hard coded).
I use the javascript to validate and populate some fields in an admin-page - that is a better UX than doing it server side.
Is it possible to add script-tag to the admin html-pages' head section, that contains server data, i.e. non-static data?
If I understood your question correctly, you need to send some data to the admin from the website, and yes there are multiple ways to do so
make an API that serves the data and fetch it using ajax from the admin
add the data in the context of the page and use it directly in the admin
in both ways you'll need to edit the admin and add some js code to it so that's what I'll show you here
Editing the admin
First Approch
here we gonna create a js file in the static files and then import it using the js variable in the Media class in the admin
class TestAdmin(admin.ModelAdmin):
class Media:
js = ('js/some_js_file.js',)
and then write an API to serve your data and consume it from the js
Second Approach
we will extend the add form template and write the js code directly in the template
{% extends 'admin/change_form.html' %}
{% block extrahead %}{{ block.super }}
# write code here
{% endblock %}
and in the admin
class TestAdmin(admin.ModelAdmin):
add_form_template = 'path_to_template'
note: you will need to enable the templates in the settings
now if you don't like the API approach, you could overwrite the admin add_form function and add the context you need
class TestAdmin(admin.ModelAdmin):
add_form_template = 'admin/test_form_Change.html'
def changeform_view(self, request, object_id=None, form_url='', extra_context=None):
extra_context = extra_context or {}
extra_context['some_data'] = 'value'
return super(TestAdmin, self).changeform_view(request, object_id, extra_context=extra_context)
and you could use this tag
to add the data to the template
The answer from Mina Atef helped me to get this working. Thanks Mina Atef.
Here is the exact code that I used, in case if it helps someone. This works in Django 4.1. I needed this in list view but you can also use this approach in detail view.
Adding to the admin class:
class PlaceNameAdmin(admin.ModelAdmin):
# A lof of code not shown
class Media:
# Adding the javascript file
js = ('multi_line_list_edit.js',)
def changelist_view(self, request, extra_context=None):
extra_context = extra_context or {}
# Adding the extra context. It comes from settings but it can be any dynamic data.
# The REGISTER_PLACES_SETTINGS is actually not static, since it comes
# from env-variable that is set in production docker environment.
extra_context['register_places_settings'] = settings.REGISTER_PLACES_SETTINGS
return super(PlaceNameAdmin, self).changelist_view(request, extra_context)
A file named change_list.html needs to be added to the templates/admin/ directory. See the django doc for more info on how to override admin templates.
{% extends "admin/change_list.html" %}
{% block extrahead %}
{{ block.super }}
{% comment %}
See docs.djangoproject.com/en/4.1/ref/templates/builtins/#json-script for doc on how to use json_script
{% endcomment %}
{{ register_places_settings|json_script:"register_places_settings" }}
{% endblock %}
Here is how the data get used in multi_line_list_edit.js. This javascript file needs to be placed in the static/ directory.
$(document).ready(function() {
const register_places_settings = JSON.parse(document.getElementById('register_places_settings').textContent);
// A lof of code not shown
});

Get non-hashed urls when using Django's ManifestStaticFilesStorage

When using ManifestStaticFilesStorage the static template function always returns the 'hashed' file name (when DEBUG=False). Is there any way to get the non-hashed regular file name in the template? It seems odd that there's no obvious way since collectstatic always includes both hashed and non-hashed files, but the non-hashed ones would never be used.
I'm guessing what I need to do is create my own templatetag, but was wondering if I missed something.
EDIT to clarify what I want...
Right now {% static 'css/style.css' %} outputs something like /static/css/style.a163843f12bc.css while I'd prefer it to result in /static/css/style.css which should always be the latest version.
I suppose another solution is to use {{ STATIC_URL }}css/style.css by adding 'django.template.context_processors.static' to the list of context processors.
If you don't want to use STATIC_URL but continue to use the url template tag, you could override url() in your custom subclass of ManifestStaticFileStorage:
# add kwarg "clean"
def url(self, name, force=False, clean=False):
if clean:
return self._url(self.clean_name, name, force)
return self._url(self.stored_name, name, force)
While stored_name(name) returns the hashed version clean_name(name) (same method signature) just replaces backslashes with slashes and returns the plain name.
This is untested but from the code in django/contrib/staticfiles/storage.py it looks like this could work.
To activate your custom FileStorage class you need to point settings.STATICFILES_STORAGE to it.
In your templates, whereever you want to use the unhashed name, you'd write:
{% static 'css/style.css' clean=True %}
Some insights on how static works:
#classmethod
def handle_simple(cls, path):
if apps.is_installed('django.contrib.staticfiles'):
from django.contrib.staticfiles.storage import staticfiles_storage
return staticfiles_storage.url(path)
else:
return urljoin(PrefixNode.handle_simple("STATIC_URL"), quote(path))
So, this means that using {{ STATIC_URL }} is not far from what {% static %} without the staticfiles app does. The staticfiles storage class adds a layer of abstraction, checks for URL fragment and does URL quoting.
Essentially what I want is just concatenating the STATIC_URL with the relative file path. However, the static template tag adds a little more functionality like proper escaping spaces and allowing assigning to template variables so I derived the following from the Django source and added it in an app as 'templatetags/simple_static.py':
from django import template
from django.templatetags.static import PrefixNode
from django.utils.six.moves.urllib.parse import quote, urljoin
register = template.Library()
#register.simple_tag
def simple_static(path):
"""
Simple concatenation of path with STATIC_URL
Usage::
{% simple_static path [as varname] %}
Examples::
{% simple_static "myapp/css/base.css" %}
{% simple_static variable_with_path %}
{% simple_static "myapp/css/base.css" as admin_base_css %}
{% simple_static variable_with_path as varname %}
"""
return urljoin(PrefixNode.handle_simple("STATIC_URL"), quote(path))
Then in the template I can do:
{% load simple_static %}{% simple_static 'css/style.css' %}
And it will output '/static/css/style.css' if STATIC_URL is '/static/'

How to put an object into template via Jinja2 in Django app using Coffin

I use Coffin to integrate Jinja2 with Django application. I want to use sorl application in my Jinja2 template. So i decided to write my own Extension for tag {% thumbnail %}. I decided to use a great WithExtension as an example which come out from a box with Coffin.
My extension:
class ThumbnailExtension(Extension):
tags = set(['thumbnail'])
def parse(self, parser):
lineno = parser.stream.next().lineno
value = parser.parse_expression()
im = get_thumbnail(value.value, "100x100")
parser.stream.expect('name:as')
name = parser.stream.expect('name')
body = parser.parse_statements(['name:endthumbnail'], drop_needle=True)
# Use a local variable instead of a macro argument to alias
# the expression. This allows us to nest "with" statements.
body.insert(0, nodes.Assign(nodes.Name(name.value, 'store'), im))
return nodes.CallBlock(
self.call_method('_render_block'), [], [], body).\
set_lineno(lineno)
def _render_block(self, caller=None):
return caller()
My template:
{% thumbnail "jinja.png" as img %}
{{ img.url }}
{% endthumbnail %}
But i get an AttributeError: 'ImageFile' object has no attribute 'iter_child_nodes'
It`s seems that i should pass a jinja2.nodes.Node object as a second parameter to nodes.Assign( ). How should i do this?
Problem solved by sending a get_thumbnail function into template:
from sorl.thumbnail.shortcuts import get_thumbnail
from coffin.template import Library
register = Library()
#register.object()
def thumbnail(file_, geometry_string, **options):
try:
im = get_thumbnail(file_, geometry_string, **options)
except IOError:
im = None
return im
And now I can call it directly from template:
{% set image = thumbnail(image_object, params.size|default("100x100")) %}
There is no necessary in custom tag or filter.
Here's something similar, suitable for use in 2016, using coffin's successor, django-jinja --
from sorl.thumbnail.shortcuts import get_thumbnail
from django_jinja import library
#library.filter
def thumbnail(path, geometry, **options):
return get_thumbnail(path, geometry, **options)

Form labels not rendering with Django & WTForms

I'm trying to use WTForms with Django & a MongoEngine/MongoDB database backend. The forms are outputting properly, but I can't for the life of me get the labels to show up.
Here is my template code:
{% load wtforms %}
<form>
{% for f in form %}
{{ f.label }}: {% form_field f %}<br/>
{% endfor %}
</form>
This is what I am passing in the view:
form = StrandForm()
return render_to_response('create_strand.html', locals(), context_instance = RequestContext(request))
The StrandForm class I have tried both creating from the WTForm mongoengine extension's model_form class, and from WTForm's Form class. The label exists in the view, I can print it to the console and it shows the rendered form label, but somehow it gets lost when transferring to the template. Am I doing something wrong?
Django 1.4 has a new feature: do_not_call_in_templates attribute.
If you set it on wtforms.Field class, every child class inherits and all fields will work fine in django templates.
import wtforms
wtforms.Field.do_not_call_in_templates = True
Now following code works as expected:
{% load wtforms %}
{{ f.label }}: {% form_field f %}
I encountered the same problem today. It has to do with the way WTForms is programmed so that it will work with many different templating libraries. Django 1.3 will only see f as it's HTML string even though it has other attributes.
In order to fix this you must add a template tag to retrieve the attribute.
Add the following to your projects hierarchy:
templatetags
templatetags / init.py
templatetags / templatetags
templatetags / templatetags / init.py
templatetags / templatetags / getattribute.py
Then in your settings.py file, add the following line to INSTALLED_APPS
'templatetags',
Open up getattribute.py and paste the following code:
from django import template
from django.conf import settings
register = template.Library()
#register.tag
def getattribute(parser, token):
try:
tag_name, tag_object, tag_function = token.split_contents()
except ValueError:
raise template.TemplateSyntaxError("%r tag requires two arguments" % token.contents.split()[0])
return getattrNode(tag_object, tag_function)
class getattrNode(template.Node):
def __init__(self, tag_object, tag_function):
self.tag_object = tag_object
self.tag_function = tag_function
def render(self, context):
return getattr(context[self.tag_object], self.tag_function)()
This will allow you to use the follow code whenever you're inside a template and need an attribute that won't show up:
{% load getattribute %}
{% getattribute OBJECT ATTRIBUTE %}
In your case:
{% getattribute f label %}
Hope that helped!

Django Widget Media doesn't work

I need a widget, which should only display a block, because i will add functionality with jQuery. I am trying to include the javascript and stylesheet trough the "Media" class of Widget and it doesn't work for me.
Here is the code:
class StarsWidget(Widget):
"""
Widget with stars for rating
"""
class Media:
js = (
settings.MEDIA_URL + 'js/rating.js',
)
css = {
'screen': (
settings.MEDIA_URL + 'css/rating.css',
)
}
def render(self, name, value, attrs=None):
if value is None: value = ''
attrs = {'id':'ratingX', 'class':'rating'}
final_attrs = self.build_attrs(attrs, name=name)
if value != '':
# Only add the 'value' attribute if a value is non-empty.
final_attrs['value'] = force_unicode(value)
return mark_safe(u'<div %s ></div>' % flatatt(final_attrs))
Any suggestions or help will be appreciated
Are you actually including the form media in your template anywhere?
{% block extrahead %}
{{ form.media }}
{% endblock %}
Paths in "class Media" are automatically prepended with settings.MEDIA_URL. So try like this:
class Media:
js = ('js/rating.js',)
css = {'screen': ('css/rating.css',),}
If it will not work, I suggest using FireBug or something like it and checking if browser is able to load css and javascript.
Based on comment:
It appears, that you are not loading your form media at all. Try adding in the template:
{{ my_form.media }}
in {% block subheader %} (or wherever you are loading scripts and css), where "my_form" is the name of your form instance, created in the view.
Minor optimization: You don't need to prepend settings.MEDIA_URL to your paths, it's done automatically when it's printed out.