In my app, I have the following :
- "Userskill" allows a given user to link 1 to n skills thanks to a
Foreign Key to "Skill" model
- In Skill model there is a foreign key to "Skilldomain".
I would like to display all the skills for a given user by skill domains.
I have such models (simplified) :
class Skilldomain(models.Model):
name = models.CharField(_('Skill domain'), max_length=50)
class Skill(models.Model):
name = models.CharField(_('Skill'), max_length=50)
domain = models.ForeignKey(Skilldomain, verbose_name=_('Skill domain'))
class Userskill(models.Model):
who = models.ForeignKey(User, verbose_name=_('Person'))
name = models.ForeignKey(Skill, verbose_name=_('Skill'))
I tried a lot of things but should miss a point somewhere. My last
attempt is something like based on :
user = User.objects.get(pk=1)
user_skill = user.userskill_set.all().select_related()
for k in user_skill:
p = k.name
print p.domain
{# -- Skills -- #}
{% if user_skill %}
<div id="skill">
<strong>{% trans "Skills" %}</strong>
<ul>
{% for item in user_skill %}
{% ifchanged %}
<li>{{ item.name.domain }}
{% endifchanged %}
<ul>
<li class="{{ item.level }}"><img
src="/static/img/{{item.progress}}.png" alt="{{
item.get_progress_display }}" /><a href="{{ item.name.url }}"
class="skill" rel="tag">{{ item.name }}</a></li>
</ul>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
This solution works except that I have a side effect with a <ul></ul>
that I do not wish.
I tried to do the same with "regroup" in template but could not manage
to make it work in my case.
You don't mention which of the "ul" elements is coming out empty, but I
guess this happens in the case where user_skill is empty. So wrap that
section in an {% if user_skill %} template tag (wrap it around the <ul>
and close it after the closing </ul>).
Regards,
Malcolm
--
I've got a mind like a... a... what's that thing called?
http://www.pointy-stick.com/blog/
> You don't mention which of the "ul" elements is coming out empty, but I
> guess this happens in the case where user_skill is empty. So wrap that
> section in an {% if user_skill %} template tag (wrap it around the <ul>
> and close it after the closing </ul>).
Oups I was not clear enough :
My code generates so far (a little bit simplified) :
<ul>
<li>Framework
<ul>
<li>Django</li>
</ul>
<ul>
<li>Symfony</li>
</ul>
</li>
<li>Language
<ul>
<li>Python</li>
</ul>
<ul>
<li>PHP</li>
</ul>
</li>
</ul>
Where as I would like to have :
<ul>
<li>Framework
<ul>
<li>Django</li>
<li>Symfony</li>
</ul>
</li>
<li>Language
<ul>
<li>Python</li>
<li>PHP</li>
</ul>
</li>
</ul>
I admit that with "ifchanged" and my initial query, I may be wrong for
what I want to do. That's why I query some help :))
Nicolas
Ah, ok. So one solution is to twist your initial template a little bit.
Normally, whenever you insert the outer "li" element (the headings), you
really want to insert "</ul></li><li>New Heading<ul>" -- closing the
previous inner section, displaying a heading and then starting a new
inner section. The exception is the very first time around the loop when
there's no previous section to close.
So this should be close to what you're after:
<ul>
{% for item in user_skill %}
{% ifchanged %}
{% ifnotequal forloop.counter 1 %}
</ul></li>
{% endifnotequal %}
<li>{{ item.name.domain }}
<ul>
{% endifchanged %}
<li class="{{ item.level }}">...</li>
{% endfor %}
</ul>
</li>
</ul>
This will give slightly odd results if user_skill is empty, so you might
want to test that first (or maybe you know otherwise that it's always
going to contain content).
Regards,
Malcolm
--
The hardness of butter is directly proportional to the softness of the
bread.
http://www.pointy-stick.com/blog/
> Ah, ok. So one solution is to twist your initial template a little bit.
> Normally, whenever you insert the outer "li" element (the headings), you
> really want to insert "</ul></li><li>New Heading<ul>" -- closing the
> previous inner section, displaying a heading and then starting a new
> inner section. The exception is the very first time around the loop when
> there's no previous section to close.
Ok, i see the point.
> So this should be close to what you're after:
>
> <ul>
> {% for item in user_skill %}
> {% ifchanged %}
> {% ifnotequal forloop.counter 1 %}
> </ul></li>
> {% endifnotequal %}
> <li>{{ item.name.domain }}
> <ul>
> {% endifchanged %}
> <li class="{{ item.level }}">...</li>
> {% endfor %}
> </ul>
> </li>
> </ul>
>
> This will give slightly odd results if user_skill is empty, so you might
> want to test that first (or maybe you know otherwise that it's always
> going to contain content).
I test before if user_skill is empty or not.
At code level, it looks great but one bug. The first domain is repeated
twice whereas for the rest it works like a charm. I will try to see if
it"s a grouping issue or a template one.
Nicolas
[...]
> At code level, it looks great but one bug. The first domain is repeated
> twice whereas for the rest it works like a charm. I will try to see if
> it"s a grouping issue or a template one.
Aah, bother. Yeah, that's a consequence of the ifchanged tag: it
tests against the previous content and since the first time we
don't include "</ul></li>" and the second time we do, it's
considered to have changed. I think I've fallen for that trick
before, now that I think about it. It's going to be fiddly to
work around.
Okay, so maybe this approach is not going to work and we need to look at
the regroup approach. This means you need to create a list of
dictionaries, where each dictionary has, say, a "domain" key and an
"item" key. Something like
results = [{'domain': o.name.domain, 'item': o}
for o in user_skill]
results.sort(key=lambda x: x['domain'])
The last step is important, because regroup only works if the original
list of dictionaries is sorted by the thing you're going to be grouping
by. Then, in your template, you can do:
{% regroup results by domain as domain_list %}
{% for d in domain_list %}
<li>{{ d.grouper }}
<ul>
{% for item in d.list %}
<li>...</li>
{% endfor %}
</ul>
</li>
{% endfor %}
That looks, to my eye at least, a bit neater than the original solution,
too. You *might* (completely untested) be able to get away with making
your results list be:
results = list(user_skill)
results.sort(key=lambda x: x.getattr('domain').name))
and then grouping by "domain.name", but I'm not 100% certain that will
work.
Regards,
Malcolm
--
Atheism is a non-prophet organization.
http://www.pointy-stick.com/blog/
So the solution is :
In views.py :
user_skill = user.userskill_set.all().select_related()
results = [{'domain': o.name.domain, 'item': o} for o in user_skill]
(exactly as said by Malcolm)
And in the template :
> {% regroup results by domain as domain_list %}
> {% for d in domain_list %}
> <li>{{ d.grouper }}
> <ul>
> {% for item in d.list %}
> <li>...</li>
> {% endfor %}
> </ul>
> </li>
> {% endfor %}
The only change I made was :
{% regroup results|dictsort:"domain.name" by domain as domain_list %}
otherwise, regroup was not always correct.
and a small notice, to access values of the list, it is by using
item.item.<name_of_your_field> : ex : item.item.name
> That looks, to my eye at least, a bit neater than the original solution,
> too. You *might* (completely untested) be able to get away with making
> your results list be:
>
> results = list(user_skill)
> results.sort(key=lambda x: x.getattr('domain').name))
>
> and then grouping by "domain.name", but I'm not 100% certain that will
> work.
I did not test this one - I keep it in mind anyway :)
Thanks a lot for your help Malcom !
Nicolas