Django Do Once Custom Template Tag - python

I'm trying to create a custom template tag that only renders a block of code once, regardless of how many times the tag/partial that contains it is executed.
This is how I've implemented it, but as you can see, it's a bit hackish:
my_partial.html:
{% once mycontent %}
this will only show once
{% endonce%}
my_template.html:
{% load my_tags %}
{% for i in list %}
{% my_partial %}
{% endfor %}
my_tags.py:
#register.inclusion_tag('my_partial.html',takes_context=True)
def my_partial(context):
return dict(arbitrary extra data)
#register.tag(name="once")
def do_once(parser, token):
try:
# Splitting by None == splitting by spaces.
tag_name, var_name = token.contents.split(None, 1)
except ValueError:
raise template.TemplateSyntaxError("%r tag requires arguments" % token.contents.split()[0])
nodelist = parser.parse(('endonce',))
parser.delete_first_token()
return DoOnceNode(nodelist, var_name)
class DoOnceNode(template.Node):
def __init__(self, nodelist, var_name):
self.nodelist = nodelist
self.var_name = '_do_once_'+var_name
def render(self, context):
request = context['request']
# Make request.GET mutable.
request.GET = dict(request.GET)
if self.var_name in request.GET:
return ''
else:
request.GET[self.var_name] = 1
return self.nodelist.render(context)
Specifically, I'm using the request.GET dictionary as a mutable global scope. It's hackish and obviously not what the request object is designed to do, but it works.
Ideally, I'd like to use something like the context, but I found that it isn't shared between calls to this tag. i.e. self.var_name in context is always False, rendering it useless as a global scope.
Why isn't context shared the same way request is shared? Is there someway to make it truly shared, or is there some other object I can use to store globally accessible variables within a request?

I'm not exactly sure what you need to accomplish or if your approach is indeed the best approach, but I'd suggest your look into the forloop.first variable before you go too far down this road. Your approach seems awkward at best at a glance, but I could be wrong since I don't know the specifics of the situation
django for template tag
Most likely you should be able to make this work to your needs, however if it falls short I'd suggest that the source for the for template tag (and it's forloop variable) would likely be very illustrative on how you might implement what you're looking to do.

What I ended up doing is saving a variable within the context and checking for that in the Node's render method:
class CustomNode(template.Node):
def render(self, context: dict) -> str:
context['already_rendered'] = context.get('already_rendered', set())
if self.__class__ in context['already_rendered']:
return ''
context['already_rendered'].add(self.__class__)
...

Related

Django: Include template with constructed name if available

I have a one template say:
default_form.html
which receives in its context a variable, say variant.
I can of course render the contents of the variable with {{ variant }}
And here:
https://stackoverflow.com/a/28076380/4002633
I see a wonderful way to load a template if it exists. It works nicely if I have a template called variant as follows:
{% try_include variant %}
But I am hoping to go one step further. I would like to use a constructed template name, in the example above and include if it exists a template named default_form_variant.html.
I imagine something like this:
{% try_include "default_form_{{variant}}.html" %}
but even more general rather than encoding the current template's name, getting that, if possible, from the context itself or in a custom tag from the parser.
I am struggling to see how and where in the custom tag definition I'd have access to three things at once:
The name of the current template: default_form.html in this case.
The argument to the tag: variant in this case
The ability to construct a template name from the first two (easy enough with os.path) and inject it into the do_include handler.
Well, as no-one was rushing to answer this I experimented with the wonderful solution to another problem posted here:
https://stackoverflow.com/a/28076380/4002633
at some length and have implemented the following with success:
class IncludeVariant(template.Node):
'''
A Template Node that tries to include a template file named as a variant
of the file it's included in. That is if it's in a template named:
form_data.html
as:
{% include_variant context_var %}
it will try and include:
form_data_context_var.html
where context_var is a context variable.
For help on custom template tags:
https://docs.djangoproject.com/en/3.1/howto/custom-template-tags/#writing-the-compilation-function
'''
def __init__(self, parser, token):
self.parser = parser
self.token = token
def render(self, context):
try:
words = self.token.split_contents()
variant = context.get(self.token.contents.split()[1], self.token.contents.split()[1])
path = context.template_name
parts = os.path.splitext(path)
words[1] = f"'{parts[0]}_{variant}{parts[1]}'"
self.token.contents = " ".join(words)
include = do_include(self.parser, self.token)
return include.render(context)
except template.TemplateDoesNotExist:
return ''
#register.tag('include_variant')
def include_variant(parser, token):
'''
Include the specified variant on this template but only if it exists.
:param parser:
:param token:
'''
return IncludeVariant(parser, token)
It is a mildly risky approach in that it rests on premises that stood up to empirical testing but I can't find clear documentation for. Namely that we can rebuild the passed-in token as I have done (a re-join of a token.split_contents()) and that this is robust. It certainly seems to be and inspecting do_include() I am satisfied it's functional.
This permits me to include variants of the current template based on the contents of a context variable.

Single View with Dynamic template

I've got modules that if enabled/disabled would require a different arrangement of tables and iframes on the frontend webpage. In order to do this I set up a function to check which modules are enabled/disabled and assigned a number to each 'set' and will be created separate templates for each "set1.html", "set2.html", "set3.html", etc...
I'd like to utilize one single view where I can pass the set number from from function I created but I can't seem to figure out how.
def homeset(request):
return render(request, 'app/set1.html', {})
Looking to figure out some way to make the "1" the return of the function I created to determine which set# to load as template and would prefer to not have to create a view for every single template needed.
Just taking a stab at this, do you have a problem with setting the template name before rendering?
def homeset(request):
# define 'my_set'
# define num
...
my_template_name = None
if num in my_set:
my_template_name = 'app/set{}.html'.format(num)
return render(request, my_template_name)
Looks like Scott Skiles answered your question, but an alternative way is to use a single base template and the "include" templatetag.
You can pass the set number to the template and use the "if" templatetag" to include the content from appropriate template(s) for each set number.
{% if set_num == 1 %}
{% include "foo/bar.html" %}
{% elif set_num == 2 %}
{% include "foo/two.html" %}
{% endif %}

Calling a method with the multiple arguments in a template

How can I call a method, pass multiple parameters to it and render the result in a html template? I can't find a simple solution which works in all circumstantials. I'm aware about #register.filter and #register.simple_tag but there's alway an error being thrown. If it's not thing, it's another. For example, simple_tag works when I render it, but throws an error when I call if.
So how can I call a method from an html template in the following ways:
{{method1(a, b)}}
{% if method1(a, b) == some_value %} {{method1(a, b)}} {%endif%}
Note I don't need advice like "you're doing something wrong, you should change your architecture" because that's not the case here. I just a simple way to do what I want. In Rails, for example, I can do it very, very easily.
You can use an assignment tag to assign the value received from the template tag into a variable which then can be used anywhere in the template.
#register.assignment_tag
def method1(a,b):
...
Then in your template you can call this template tag and assign its value to a variable method_value.
{% method1 a b as method_value %}
Then use this variable in the template.
{{method_value}}
{% if method_value == some_value %} {{method_value}} {%endif%}

Access Model Instances from Template without custom filter

I have yet to see anybody implement this pattern and am eager to learn if it's even technically viable. Let me provide an example of what the pattern would look like using a custom filter:
In this example, the "get_widget" filter will look for MyWidget objects with the name or key passed in as the first argument.
Template Logic
{% get_widget "whizbang" as item %}
<h1>{{item.name}}</h1>
{% get_widget "1234" as item %}
<h1>{{item.name}}</h1>
Custom Filter
#register.assignment_tag(takes_context=True)
def get_widget(context, widget_name):
try:
return MyWidget.objects.get(name=widget_name)
except MyWidget.DoesNotExist:
return None
But that seems rather hideous.
What I'd like to see is something a little more dyanmic:
Example:
Retrieve an instance of MyWidget based on its name being "whizbang" or, alternatively, using it's key.
In the template:
<h1>{{MyWidget.whizbang}}</h1>
<h1>{{MyWidget.1234}}</h1>
The question is twofold:
Would it be possible to pass in a singleton/factory to the
request_context
Is there a Python mechanism to "intercept" a method
call and interpret it before it's executed?
After sifting through the Python docs it looks like a combination of __getattrr__() and passing in a class name is all that was required. My apologies for answering my own question. Hopefully this will be useful for someone else.
Template
{{ Menus.HeyDude.DoSomething }}
Class
from mycode import Menu
class MenuFactory():
def __getattr__(self, name):
try:
return Menu.object.get(name=name)
except Menu.DoesNotExist:
raise AttributeError
Middlewear context processor
from mycode import MenuFactory
def context_processor(request):
return_val = {}
# Add all of the factories
return_val['Menus'] = MenuFactory
return return_val

tornado render argument in template

I feel like I hacked this into tornado and it is in poor form. The goal was to get an error message down into a template. This error message would only need to be within one handler (responsible for that same page).
The template line:
{% if errormsg is not None %}
<div class="alert-warning">{{ errormsg }}</div>
{% end %}
The relevant handler section:
if auth:
self.set_current_user(username)
self.redirect(self.get_argument("next",u"/"))
else:
self.errormsg = "Login Failed"
self.render("login.html", errormsg=self.errormsg)
At this point I was getting global namespace error messages on the page when errormsg was not set to something.
NameError: global name 'errormsg' is not defined
The workaround I found was to muck around with the global render function within my BaseHandler (I do not like this one bit):
def render(self, template, **kwargs):
if hasattr(self, 'errormsg'):
kwargs['errormsg'] = self.errormsg
else:
kwargs['errormsg'] = None
super(BaseHandler, self).render(template, **kwargs)
This basically adds the errormsg to every render now. Is there a correct way to do this that doesn't mess with the global render function?
Thanks!
Edit:
Because what I'm actually trying to do is pass different/multiple, non-standard kwargs parameters into inherited handlers, I actually really think I was looking for a better way to test, in this case errormsg, within the template context.
{% if 'errormsg' in globals() %}
This still feels pretty hacked into place since this issue is the first time globals actually showed up at all while working with tornado.
I do like extending render for setting kwargs default values for all inherited handlers (what it is actually for). I think this may also be similar to how self.current_user works.
Overriding render() is officially supported, but it's a bit cleaner to override get_template_namespace instead: http://www.tornadoweb.org/en/stable/web.html#tornado.web.RequestHandler.get_template_namespace
Or, if errormsg is an attribute of the RequestHandler, you can just access handler.errormsg in the template - the handler variable is always set to the current RequestHandler.

Categories

Resources