How to create an "iffirst" tag?

5 kali dilihat
Langsung ke pesan pertama yang belum dibaca

Ned Batchelder

belum dibaca,
28 Des 2007, 14.46.3828/12/07
kepadadjango...@googlegroups.com
I'm making a list of objects, and only including some of them in the output:

{% for thing in mylist %}
{% if thing.test %}
{{thing}}
{% endif %}
{% endfor %}

Now if I want to comma-separate the list, how do I do it? The natural
thing to my mind is an "iffirst" tag:

{% for thing in mylist %}
{% if thing.test %}
{% iffirst %}Things: {% else %}, {% endiffirst %}
{{thing}}
{% endif %}
{% endfor %}

{% iffirst %} is true the first time it is evaluated in each loop.
Because of the "if thing.test", it could be evaluated first on the third
time through the loop, for example. That's why forloop.first isn't
sufficient.

I've tried creating such a tag, but can't find the contextual
information I need to know when the for loop has been restarted.

(Also: the whitespace will be messed up even if I do have this tag: the
comma will have space before it).

So, two questions: 1) How would I create a tag like this, and 2) is
there some simpler way to achieve my purpose?

--Ned.

--
Ned Batchelder, http://nedbatchelder.com

Empty

belum dibaca,
28 Des 2007, 14.56.2328/12/07
kepadadjango...@googlegroups.com

Todd O'Bryan

belum dibaca,
28 Des 2007, 15.06.4128/12/07
kepadadjango...@googlegroups.com
On Dec 28, 2007 2:46 PM, Ned Batchelder <n...@nedbatchelder.com> wrote:
>
> I'm making a list of objects, and only including some of them in the output:
>
> {% for thing in mylist %}
> {% if thing.test %}
> {{thing}}
> {% endif %}
> {% endfor %}
>

Is there a compelling reason not to do the filtering in the view
before you even hit the template?

Todd

Ned Batchelder

belum dibaca,
28 Des 2007, 15.36.0828/12/07
kepadadjango...@googlegroups.com
Just a matter of taste.  I suppose filtering in the view would be the simplest way to achieve what I'm looking for.

For my own edification: is there a way to write an iffirst tag?  I often wish for it.

--Ned.

Ned Batchelder

belum dibaca,
28 Des 2007, 15.39.1028/12/07
kepadadjango...@googlegroups.com
Just after I sent the last reply, I went to change to filtering in the view.  Then I realized why I didn't want to.

This loop is actually nested inside another (let's say it's an outer loop over blog posts, then an inner loop over tags for each post).  To filter the tags, I have to loop over the posts in the view, and annotate each with a filtered list of tags.  I'm already looping in the template, and don't need to loop in the view at all except to perform this filtering.

To me, this is kind of ugly.  Do-able, but unfortunate.


--Ned.

Todd O'Bryan wrote:

Malcolm Tredinnick

belum dibaca,
28 Des 2007, 15.45.0628/12/07
kepadadjango...@googlegroups.com

On Fri, 2007-12-28 at 15:36 -0500, Ned Batchelder wrote:
> Just a matter of taste. I suppose filtering in the view would be the
> simplest way to achieve what I'm looking for.
>
> For my own edification: is there a way to write an iffirst tag? I
> often wish for it.

In the tag code, test to see if context['forloop']['first'] is True or
not. That's the Python equivalent of {% if forloop.first %} in a
template.

But, seriously, use {% if forloop.first %}. It even works for nested
for-loops:

{% for x1 in some_list %}
{% for x2 in some_other_list %}
{% if forloop.parentloop.first %}
x1 is still on it's first iteration.


{% endif %}
{% endfor %}

{% endfor %}

"parentloop" always points to the parent loop or is {} when there's no
nesting above the current loop.

(And, yes, the wheels will come fall off if you ever create your own
context variable called "forloop", so don't do that.)

Regards,
Malcolm

--
Tolkien is hobbit-forming.
http://www.pointy-stick.com/blog/

Ned Batchelder

belum dibaca,
28 Des 2007, 16.01.0828/12/07
kepadadjango...@googlegroups.com
I appreciate the advice, but forloop.first won't work for what I'm trying to do.  If the first thing chosen is in the second iteration of the for loop, then forloop.first is never true.  I don't want to know if I'm on the first iteration of the loop.  I want to know if it's the first time in the if clause:
    {% for thing in mylist %}
       {% if thing.test %}
          {% iffirst %}Things: {% else %}, {% endiffirst %}
          {{thing}}
       {% endif %}
    {% endfor %}
--Ned.
-- 
Ned Batchelder, http://nedbatchelder.com

Malcolm Tredinnick

belum dibaca,
28 Des 2007, 16.27.0228/12/07
kepadadjango...@googlegroups.com

On Fri, 2007-12-28 at 16:01 -0500, Ned Batchelder wrote:
> I appreciate the advice, but forloop.first won't work for what I'm
> trying to do. If the first thing chosen is in the second iteration of
> the for loop, then forloop.first is never true. I don't want to know
> if I'm on the first iteration of the loop. I want to know if it's the
> first time in the if clause:
> {% for thing in mylist %}
> {% if thing.test %}
> {% iffirst %}Things: {% else %}, {% endiffirst %}
> {{thing}}
> {% endif %}
> {% endfor %}

Presumably, it's even more subtle than that. Don't you want to know if
it's the first time in the "if" clause for this particular execution of
the for-loop?

In any case, change the syntax slightly. The "iffirst" tag should take a
variable name, which is what will be set in the context to indicate if
it's been executed before. So the render code for

{% iffirst foo %}

will check to see if context['foo'] is equal to id(context['forloop']).
If not, set it to that value and return the "true" branch. Otherwise,
return the "false" branch. Untested code follows:

def render(self, context):
if context[self.varname] == id(context['forloop']):
return self.nodelist_false.render(context)
context[self.varname] = id(context['forloop'])
return self.nodelist_true.render(context0

Here. nodelist_true and nodelist_false could be similar to the same
variables in django.template.defaulttags.IfNode (and do the setup the
same way). I've been awake all night, so my brain is completely straight
at the moment ... there might be some subtlety that means you need to
use Variable.resolve() or something instead of just
context[self.varname], but it should be clear with a bit of
experimentation. You certainly won't need anything as complicated as
what the IfNode class does, since you aren't testing a user-supplied
condition.

The only reason to use the variable name is so that you can have
multiple "iffirst" calls in a single template without collisions.
Clearly extending to allow another parameter that says which for-loop
level of nesting ("parent", etc) would be another extension and not too
difficult.

Regards,
Malcolm

--
I just got lost in thought. It was unfamiliar territory.
http://www.pointy-stick.com/blog/

Malcolm Tredinnick

belum dibaca,
29 Des 2007, 05.25.1529/12/07
kepadadjango...@googlegroups.com
Hey Ned,

On Sat, 2007-12-29 at 08:27 +1100, Malcolm Tredinnick wrote:
[...]


> def render(self, context):
> if context[self.varname] == id(context['forloop']):
> return self.nodelist_false.render(context)
> context[self.varname] = id(context['forloop'])

> return self.nodelist_true.render(context)

This guess is rubbish. The problem is that the context['forloop']
dictionary is a new instance for every iteration of the for-loop, so
there's nothing constant about that outer structure we can use.

After a bit of sleep, some food and some time spent thinking about this
whilst doing other stuff today, there's a better solution (a.k.a one
that works) attached to this mail. I've even tested this one.

During any for-loop, the only thing that remains constant is the
"parentloop" dictionary. That dictionary is created anew every time the
*parent* loop steps forwards, but it is referred to by reference for
each iteration of the inner loop. So we can stick a marker into the
parent loop's data structure (even when there's no parent loop, that
data structure is an empty dictionary).

So, this implementation does depend a bit on the internals of the
ForNode, but that isn't a complete crime. Plus it's been hard to come up
with an alternative. Every time I thought of some tricky data structure
that seemed to do the trick, I was able to break it. A template fragment
such as

{% for x in data %}
{% if x %}
{% iffirst %} yes {% else %} no {% endfirst %}
{% else %} fail {% endif %}
{% endfor %}

given data = (0, 1, 1, 0) twice in succession (without recompiling), or
giving data = (0, 1, 1, 0) and data = (0, 0, 1, 0) on successive runs
(again, without recompiling) was enough to break most of my attempts.

It's also not too hard to extend this to be check whether this is the
first time around the parent loop using a similar method, although
nested "iffirst" tags seems hard.

Best wishes,

xyzzy.py

Ned Batchelder

belum dibaca,
31 Des 2007, 20.30.5031/12/07
kepadadjango...@googlegroups.com
Malcolm, thanks for continuing this (I was away for a few days).  I was thinking about the forloop internals as well, and it seems to me that there's no reason the forloop dictionary needs to be completely new each time around.  Why not change forloop to create a new dictionary before the first iteration, then update the dictionary on each iteration rather than replace it?

BTW: this problem also plagues ifchanged: the docs say "check if a value has changed from the last iteration of a loop", which I guess is accurate, but I was surprised to find that it really only works if the ifchanged tag is evaluated on every iteration of the loop.

--Ned.
-- 
Ned Batchelder, http://nedbatchelder.com

Ned Batchelder

belum dibaca,
1 Jan 2008, 15.28.3201/01/08
kepadadjango...@googlegroups.com
I tried out this change, and it works well.  I've created a ticket (http://code.djangoproject.com/ticket/6295) with a patch.

--Ned.
http://nedbatchelder.com

Ned Batchelder

belum dibaca,
2 Jan 2008, 06.48.0902/01/08
kepadadjango...@googlegroups.com
I may be the only one still interested in this, but the patch got incorporated yesterday (http://code.djangoproject.com/changeset/6981), so Malcolm, your original idea will now work.

--Ned.
Balas ke semua
Balas ke penulis
Teruskan
0 pesan baru