I am wondering if there is any way to keep the indentation with jinja when adding a include or macro inside a file. I would like to use jinja to generating a code file. An example would be
File: class.html
class MyClass:
def someOp():
pass
{% include "someOp.html" %}
File: someOp.html
def someOp2():
pass
The result of the template should be:
class MyClass:
def someOp():
pass
def someOp2():
pass
If there any way to make jinja prepend the indent before the include tag for each line in the included file? Or is there any way to customize jinja to do this?
One way is to wrap the include in a macro, then because the macro is a function, its output can be passed through the indent filter:
class MyClass:
def someOp():
pass
{% macro someop() %}{% include "someOp.html" %}{% endmacro %}
{{ someop()|indent }}
By default 'indent' indents 4 spaces and does not indent the first line, you can use e.g. 'indent(8)' to indent further, see http://jinja.pocoo.org/docs/templates/#list-of-builtin-filters for more details.
If what you're including is defined as a macro to begin with then the further wrapper macro is not needed, and you can jump straight to using the indent filter.
I was looking in Jinja2 to achieve the same and got to conclusion aligning multi-line block indentation with the originating Jinja statement is not possible currently.
I've posted a small PR to Jinja to add a new syntax {%* ... %} and {{* ... }} that does exactly this. See the PR for details:
https://github.com/pallets/jinja/pull/919
It would be easier if Jinja provided the facility. It looks like some work was done on this but the issue is currently closed (20 Nov 2019) and the pull request hasn't yet been merged. It could be because things get tricky quite quickly with indents (think of tabs and spaces, for example.)
The following is a simple solution I've found effective for generating Python code which, of course, needs to handle indenting well. It copes with files that use spaces for indentation.
auto_indent() detects the indent level of a variable in a host template, then applies that indent to a piece of text.
import os
import itertools
import jinja2
def indent_lines(text_lines: list, indent: int):
return [' ' * indent + line for line in text_lines]
def matching_line(s, substring):
lineno = s[:s.index(substring)].count('\n')
return s.splitlines()[lineno]
def is_space(c):
return c == ' '
def indentation(line: str) -> int:
initial_spaces = ''.join(itertools.takewhile(is_space, line))
return len(initial_spaces)
def auto_indent(template: str, placeholder: str, content_to_indent: str):
placeholder_line = matching_line(template, '{{ ' + placeholder + ' }}')
indent_width = indentation(placeholder_line)
lines = content_to_indent.splitlines()
first_line = [lines[0]] # first line uses placeholder indent-- no added indent
rest = indent_lines(lines[1:], indent_width)
return os.linesep.join(first_line + rest)
Example:
action_class = """\
class Actions:
def __init__(self):
pass
def prequel(self):
pass
{{ methods }}
def sequel(self):
pass
"""
inserted_methods = """\
def create_branch():
pass
def merge_branch():
pass
"""
if __name__ == '__main__':
indented_methods = auto_indent(action_class, 'methods', inserted_methods)
print(jinja2.Template(action_class).render(methods=indented_methods))
Example output:
>>> python indent.py
class Actions:
def __init__(self):
pass
def prequel(self):
pass
def create_branch():
pass
def merge_branch():
pass
def sequel(self):
pass
I've written a jinja2 extension to work around this long standing issue. It automates the previously proposed solution of using {% filter indent(...) %} by hooking into the preproccess api provided by jinja.ext.Extension.
If you add the extension in your jinja.Environment you can use the following syntax to include templates that get indented correctly into the rest of your template. Notice the indent content directive.
class MyClass:
def someOp():
pass
{% include "someOp.html" indent content %}
The result of rendering then becomes
class MyClass:
def someOp():
pass
def someOp2():
pass
Related
I am building a search engine, which needs a custom filter that displays the text surrounding a keyword, like the excerpts on Google results page. I am using regex to identify the surrounding words. Here is my code for the filter:
#register.filter(needs_autoescape=True)
#stringfilter
def show_excerpt (value, search_term, autoescape=True):
# make the keyword put into the search engine case insensitive #
keywords = re.compile(re.escape(search_term), re.IGNORECASE)
# make excerpt return 300 characters before and after keyword #
excerpt_text = '.{300}' + str(keywords) + '.{300}'
# replace the original text with excerpt #
excerpt = value.sub(excerpt_text, value)
return mark_safe(excerpt)
Code for the search engine in view.py:
def query_search(request):
articles = cross_currents.objects.all()
search_term = ''
if 'keyword' in request.GET:
search_term = request.GET['keyword']
articles = articles.annotate(similarity=Greatest(TrigramSimilarity('Title', search_term), TrigramSimilarity('Content', search_term))).filter(similarity__gte=0.03).order_by('-similarity')
context = {'articles': articles, 'search_term': search_term}
return render(request, 'query_search.html', context)
HTML template (it includes a custom highlight filter that highlights the keyword put into search engine):
<ul>
{% for article in articles %}
<li>{{ article|highlight:search_term }}</li>
<p> {{ article.Content|highlight:search_term|show_excerpt:search_term }} </p>
{% endfor %}
</ul>
Error message: 'SafeText' object has no attribute 'sub'
I think I am doing .sub wrong. I just need the excerpt to replace the entire original text (the text that I am putting a filter on). The original text starts from the beginning of the data but I just want to display the data surrounding the keyword, with my highlight custom filter highlighting the keyword (just like on Google). Any idea?
EDIT: when I do re.sub(excerpt_text, value), I get the error message sub() missing 1 required positional argument: 'string'.
You need to call re.sub(), not value.sub(). You are calling sub on a SafeText object, .sub() is a regex function.
I haven't tested your code but if the remaining code is correct you should just change that line to re.sub(excerpt_text, value)
I decided to ditch regex and just do good old string slicing. Working code for the filter:
#register.filter(needs_autoescape=True)
#stringfilter
def show_excerpt(value, search_term, autoescape=True):
#make data into string and lower#
original_text = str(value)
lower_original_text = original_text.lower()
#make keyword into string and lower#
keyword_string = str(search_term)
lower_keyword_string = keyword_string.lower()
#find the position of the keyword in the data#
keyword_index = lower_original_text.find(lower_keyword_string)
#Specify the begining and ending positions of the excerpt#
start_index = keyword_index - 10
end_index = keyword_index + 300
#Define the position range of excerpt#
excerpt = original_text[start_index:end_index]
return mark_safe(excerpt)
For a long time now, I have been trying to figure out how to work with lists created on the fly in Django templates, meaning being able to:
create a list directly in a django template,
add new elements to that list,
concatenate 2 lists together.
Those lists should be able to handle django objects and not only simple strings or so. For instance, in my case, I wanted my lists to be able to store form fields (example to follow).
After many researches, I figured out that it was impossible to do that but with simple things, and that I had to create my own custom tags if I ever wanted to achieve my purpose. My custom tag is written below. Please notice that this post helped me to do so.
The issue I am facing with
The custom tag works, and I use it in a for loop. The list generated here is correctly evolving according to the loop, and I can call it like any variable while still being in the loop (because it was exported in Django context): {{ listName }}
But! Once I'm outside that loop, my list does not seem having being updated at all! Like if it was only existing inside the for loop... I thought at first that when something was defined into a Django template context, it was available anywhere inside the template, and not only inside the block where it was defined. Am I missing something? Is that the normal behaviour for Django? I have not been able to find the answer to that question.
Custom tag
#register.tag()
def setList(parser, token):
"""
Use : {% setList par1 par2 ... parN as listName %}
'par' can be a simple variable or a list
To set an empty list: {% setList '' as listName %}
"""
data = list(token.split_contents())
if len(data) >= 4 and data[-2] == "as":
listName = data[-1]
items = data[1:-2]
return SetListNode(items, listName)
else:
raise template.TemplateSyntaxError(
"Erreur ! L'utilisation de %r est la suivante : {%% setList par1 par2 ... parN as listName %%}" % data[0]
)
class SetListNode(template.Node):
def __init__(self, items, listName):
self.items = []
for item in items: self.items.append(template.Variable(item))
self.listName = listName
def render(self, context):
finalList = []
for item in self.items:
itemR = item.resolve(context)
if isinstance(itemR, list): finalList.extend(itemR)
elif itemR == '': pass
else: finalList.append(itemR)
context[self.listName] = list(finalList)
return "" # django doc : render() always returns a string
Use of my custom tag in a Django Template
{% setList '' as new_list %}
new_list value is: {{ new_list }} # shows me an empty list: OK!
# then I iter on a forms.RadioSelect field
{% for field in form.fields %}
{% if field.choice_label in some_other_list %}
{% setList new_list field as new_list %}
{% endif %}
{{ new_list }} # a new item is added to new_list when necessary: OK!
{% endfor %}
{{ new_list }} # just shows an empty list, the one from the begining: THE ISSUE!
So: it looks like that my initial list is just being updated locally in my for loop. What a disappointment! Any idea about how I could use my custom list outside the loop? Is it impossible?
Thank you so much for the time you will take to help me with that thing. First time I am posting something in here, so if one needs anything please tell me!
First of all, that's a great question.
Now to the business: Context object (a dictionary mapping variable names to variable values) is a stack. That is, you can push() it and pop() it.
With that knowledge let's look at the code once again:
# Enter scoped block.
# Push new_list as empty list onto the context stack.
{% setList '' as new_list %}
new_list value is: {{ new_list }}
# Enter for-loop scoped block.
{% for field in form.fields %}
{% if field.choice_label in some_other_list %}
# Push new new_list onto context.
{% setList new_list field as new_list %}
{% endif %}
# Print most current value named new_list (at the top)
{{ new_list }}
# Exit for-loop.
# Pop the loop variables pushed on to the context to avoid
# the context ending up in an inconsistent state when other
# tags (e.g., include and with) push data to context.
{% endfor %}
# new_list's value is again empty list,
# since all other values under that name were poped off the stack.
{{ new_list }}
# Pop any values left.
# Exit scoped block.
What's this all about?, from django.template.base source code:
How [Django template system] works:
The Lexer.tokenize() function converts a template string (i.e., a
string containing markup with custom template tags) to tokens, which
can be either plain text (TOKEN_TEXT), variables (TOKEN_VAR) or
block statements (TOKEN_BLOCK).
The Parser() class takes a list of tokens in its constructor, and
its parse() method returns a compiled template -- which is, under
the hood, a list of Node objects.
Each Node is responsible for creating some sort of output -- e.g.
simple text (TextNode), variable values in a given context
(VariableNode), results of basic logic (IfNode), results of
looping (ForNode), or anything else. The core Node types are
TextNode, VariableNode, IfNode and ForNode, but plugin modules
can define their own custom node types.
Each Node has a render() method, which takes a Context and
returns a string of the rendered node. For example, the render()
method of a VariableNode returns the variable's value as a string.
The render() method of a ForNode returns the rendered output of
whatever was inside the loop, recursively.
The Template class is a convenient wrapper that takes care of
template compilation and rendering.
So, in sum, what ForNode does is:
It takes some mark-up code (whatever is inside for tags), pushes some variables on the stack, compiles HTML with them, pops off introduced variables off the stack and returns said HTML.
*Additionally, you can have a look at ForNode's render implementation itself. It takes context as argument and returns mark_safe(''.join(nodelist)), which is a string.
Sadly, you cannot circumvent this mechanism. Unless you write your own completely.
Cheers.
There is a solution!
Well, after spending hard time understanding and printing everything I could to understand what was going on, I finally succeeded in doing so! Thanks a lot to #Siegmeyer who really helped me to see clear in what was really a Django Context object.
To begin with, you can have a look at Django Template Context source code over here.
As #Siegmeyer said, Context in Django works as a stack. You won't be able to use it as a classic dictionary, especially if you want to add variables to your context. Please read #Siegmeyer explanations, it was clear enough for me. Also, he/she told me the answer without giving me the one existing appropriate method dedicated to my need, but maybe that was on purpose to make me read harder the doc ;-) Which would have been a good thing after all.
Let's have a look at that method of the BaseContext (from django.template.context source code) I am writing about :
def set_upward(self, key, value):
"""
Set a variable in one of the higher contexts if it exists there,
otherwise in the current context.
"""
context = self.dicts[-1]
for d in reversed(self.dicts):
if key in d.keys():
context = d
break
context[key] = value
As you can see, set_upward method answers perfectly to my need (the custom list is in a higher context). Except that it seems it is only looking at the n-1 context, so if you define your custom variable too deep in for loops, you might not be able to access it at some higher level (>n-1, but I did not test that). Maybe a good thing to avoid that would be to define your needed variables in your context_processors.py file so it can be accessible anywhere in your template (not sure about that though).
Custom tag: the end
Finally, my custom tag allowing me to define lists on the fly is the one following:
#register.tag()
def setList(parser, token) [...] # no change, please see my question
class SetListNode(template.Node):
def __init__(self, items, listName):
self.items = []
for item in items: self.items.append(template.Variable(item))
self.listName = listName
def render(self, context):
finalList = []
for item in self.items:
itemR = item.resolve(context)
if isinstance(itemR, list): finalList.extend(itemR)
elif itemR == '': pass
else: finalList.append(itemR)
context.set_upward(self.listName, list(finalList))
return "" # django doc : render() always return a string
The usecase of my question works, the custom list is displaying what is required.
If anything is not clear, or if you think I might be missing something, please feel free to tell me! Anyway, thanks for reading!
I have a generic delete view that includes a confirmation question as a translated string containing one placeholder. I would like to interpolate it like this:
<p class="text-error">
{% message % object %}
</p>
Variable message contains a string like: "Do you want to remove user %s?".
How can I use string interpolation in templates?
You can use the following dictionary with strings:
strings = { 'object': 'word' }
as follows:
{{ strings|stringformat:message }}
with the stringformat filter. Note that the leading % is dropped from the string (see the documentation for more details).
Finally I made a custom filter:
from django.template.base import Library
register = Library()
#register.filter
def interpolate(value, arg):
"""
Interpolates value with argument
"""
try:
return value % arg
except:
return ''
Is there a way to use the {{date|timesince}} filter, but instead of having two adjacent units, only display one?
For example, my template is currently displaying "18 hours, 16 minutes". How would I get it to display "18 hours"? (Rounding is not a concern here.) Thank you.
I can't think of a simple builtin way to do this. Here's a custom filter I've sometimes found useful:
from django import template
from django.template.defaultfilters import stringfilter
register = template.Library()
#register.filter
#stringfilter
def upto(value, delimiter=None):
return value.split(delimiter)[0]
upto.is_safe = True
Then you could just do
{{ date|timesince|upto:',' }}
Since the timesince filter doesn't accept any arguments, you will have to manually strip off the hours from your date.
Here is a custom template filter you can use to strip off the minutes, seconds, and microseconds from your datetime object:
#this should be at the top of your custom template tags file
from django.template import Library, Node, TemplateSyntaxError
register = Library()
#custom template filter - place this in your custom template tags file
#register.filter
def only_hours(value):
"""
Filter - removes the minutes, seconds, and milliseconds from a datetime
Example usage in template:
{{ my_datetime|only_hours|timesince }}
This would show the hours in my_datetime without showing the minutes or seconds.
"""
#replace returns a new object instead of modifying in place
return value.replace(minute=0, second=0, microsecond=0)
If you haven't used a custom template filter or tag before, you will need to create a directory in your django application (i.e. at the same level as models.py and views.py) called templatetags, and create a file inside it called __init__.py (this makes a standard python module).
Then, create a python source file inside it, for example my_tags.py, and paste the sample code above into it. Inside your view, use {% load my_tags %} to get Django to load your tags, and then you can use the above filter as shown in the documentation above.
Well, you can make use of JS here. You need to add a script that splits the first and second units and then displays only the first one.
Say the part of your template looks like this:
<your_tag id="your_tag_id">{{date|timesince}}</your_tag>
Now, add the below script to your template.
<script>
let timesince = document.getElementById("your_tag_id").innerHTML.split(",");
document.getElementById("your_tag_id").innerHTML = timesince[0];
</script>
A quick and dirty way:
Change the django source file $PYTHON_PATH/django/utils/timesince.py #line51(django1.7) :
result = avoid_wrapping(name % count)
return result #add this line let timesince return here
if i + 1 < len(TIMESINCE_CHUNKS):
# Now get the second item
seconds2, name2 = TIMESINCE_CHUNKS[i + 1]
count2 = (since - (seconds * count)) // seconds2
if count2 != 0:
result += ugettext(', ') + avoid_wrapping(name2 % count2)
return result
Writing template tags isn't easy in Django and involves lots of boilerplate code.
What is the least painful way to do it?
Are there any libs for that?
Are there any tricks that doesn't involve third-party apps?
What is your way to do it?
(I will make this post a community wiki once I figure how to do it.)
Try django-classy-tags https://github.com/ojii/django-classy-tags
There are some libs for that:
django-templatetag-sugar
Used it before. Makes everythings simpler, but I couldn't figure how to handle lots of optional arguments with it.
Usage example::
''' {% example_tag for val as asvar %} '''
#tag(register, [
Constant("for"), Variable(),
Optional([Constant("as"), Name()]),
])
def example_tag(context, val, asvar=None):
if asvar:
context[asvar] = val
return ""
else:
return val
django-tagcon, abandoned by original author
django-ttag, fork of django-tagcon
These two look promising because of class-based approach and keyword arguments support (like {% output limit=some_limit|default:1 offset=profile.offset %})
Example usage::
class Welcome(ttag.Tag):
''' {% welcome current_user fallback "Hello, anonymous." %} '''
user = ttag.Arg(positional=True)
fallback = ttag.Arg(default='Hi!')
def output(self, data)
name = data['user'].get_full_name()
if name:
return 'Hi, %s!' % name
return data['fallback']
I use the fancy_tag decorator: http://pypi.python.org/pypi/fancy_tag
Very simple and powerful and it comes with good documentation.
Gets rid of all that boilerplate when you just want to assign the output of a template tag to a context variable. Also handles (variable length) arguments and keyword arguments.
Much easier to use than for example django-templatetag-sugar.
I haven't really needed anything else apart from the built in inclusion tag decorator.