2

I have a Django model which has a ForeignKey to the same class, effectively making a tree:

class Tag(models.Model):
    name = models.CharField(max_length=50)
    parent = models.ForeignKey('self', blank=True, null=True)

Playing around with a recursive in the Django shell (./manage.py shell), I am easily able to represent the tree as plain text:

def nodes(parent, level):
    children = Tag.objects.filter(parent=parent)
    for c in children:
        spaces = ""
        for i in xrange(0,level):
            spaces+="  "
        print "%s%s" % (spaces,c.name)
        nodes(c.pk,level+1)

nodes(None,0)

What I am unsure of is how to get the entire tree into a Django template. I've created a custom template tag to make this easier, but I can't figure out how to pass the data to the template to easily iterate over the tree for display in a template. Here's the basic template tag.

@register.inclusion_tag("core/tags.html")
def render_tags(**kwargs):
    tags = Tag.objects.all()
    return {"tags":tags}

I know the above is very basic, I just am not sure where to go from here. I thought it might be easier if the Tag class had a function to get its children, so I also have on the class:

    def children(self):
        return Tag.objects.filter(parent=self.pk)

I use self.pk there, then the root of the tree is simply rootTag=Tag(), since it has no pk since it is not saved, rootTag.children() will find any Tags which do not have a parent Tag, and any of these tags can then just continue to have their children() function called. But like I said, I do not know how to turn this into a single data structure of some sort to pass to my template.

Thoughts? I think I probably want to build a kind of dict, I'm just not able to follow through here.

Joseph
  • 12,678
  • 19
  • 76
  • 115
  • Why isn't passing the root good enough? You could still loop each nodes children in the template. You would just probably need to refactor your children method somehow so it doesn't do a new database call every time. – jproffitt Oct 05 '13 at 03:17
  • The fact that there's a database call doesn't matter so much that it requires recursion, not iteration, which AFAIK isn't something templates can really do. – Joseph Oct 05 '13 at 03:23
  • Ah. I understand your problem now. Would django-mptt (https://github.com/django-mptt/django-mptt) help? I've never used it myself, but I've heard it is good for this kind of thing – jproffitt Oct 05 '13 at 04:26

2 Answers2

9

As Jproffitt mentionned it Django MPTT is a nice way to achieve what you want.

Using it, you can then access children instance, recursively, in your template, like this:

{% load mptt_tags %}
<h1>Tags</h1>
<ul>
{% recursetree tags %}
    <li>{{ node.name }}
        {% if not node.is_leaf_node %}
            <ul>
                {{ children }}
            </ul>
        {% endif %}
    </li>
{% endrecursetree %}
</ul>

I have it for one of my projects and it's easy to set up and to use.

Alternative solution without a third-party app

If you don't want to use a dedicated app, I think these solution might work (untested):

# yourapp/tags_list.html
<ul>
{% for tag in tags %}
    <li>{{ tag.name }}</li>
    {% if tag.children.exists %}
       {% with tag.children.all as tags %}
            {% include "yourapp/tags_list.html" %}
        {% endwith %}    
    {% endif %}
{% endfor %}
</ul>

This way, the template should just call himself recursively until there is no more children tags. This solution needs that you specify a related name in your Tag model :

parent = models.ForeignKey('self', blank=True, null=True, related_name="children")

However, be careful, these solution will involve more database queries than using MPTT.

Agate
  • 3,152
  • 1
  • 19
  • 30
  • I'll check it out, thanks. Just seems like a whole app is overkill for what I'm trying to do, but maybe it will continue to be useful as this project goes on. Thanks again. I'm going to leave the question un-solved for now, but will come back if no one chimes in with anything else. – Joseph Oct 05 '13 at 14:42
  • I updated my answer to provide an alternative, can you tell me if it works ? – Agate Oct 05 '13 at 15:07
  • Seems to be giving me recursion errors, maximum depth exceeded. – Joseph Oct 05 '13 at 16:01
  • I've found that even those that do not have children, tag.children.exists is True because the relation manager does exist, so I'm trying to adjust it to be based off of length instead. – Joseph Oct 05 '13 at 16:07
  • I was able to fix this by using two "with" template tags, the first to specify a template name, the second to define `as tags` for that included template. I have proposed an edit to your solution but it needs to be reviewed to take effect. – Joseph Oct 05 '13 at 16:24
  • I've noted your solution as correct, django-mptt being the best option to take here. The template recursion also works, but probably isn't the next best solution. Instead, I've also provided an answer which explains an in the middle solution. – Joseph Oct 05 '13 at 16:45
6

The more I searched around the more I realized trying to do this with template recursion isn't the way to go, but I'd still like to avoid django-mptt for now because it just seems like too much for what I feel is a simple problem, and I don't plan on having very large trees.

An answer to this related question noted that template recursion really shouldn't be used and to instead handle serialization in the view. I had thought this would be better as I mentioned in the original question, assuming I needed to build a dict most likely.

This answer is basically what I was looking for. The serializable_object is something I can pass into a template, and if I decide I want to go with json later on it's quite easy to do as noted in that answer.

I am going to select Eliot's answer as correct since django-mptt is probably the "most correct" thing to do here, but this is an alternate solution for anyone looking to avoid another third party app.

class Tag(models.Model):
    name = models.CharField(max_length=50)
    description = models.CharField(max_length=100, blank=True)
    parent = models.ForeignKey('self', blank=True, null=True)

    # Not necessary, can use Tag.tag_set.all()
    # But this function uses pk which allows
    #    rootTag=Tag() 
    #    rootTag.children()
    # Because rootTag has no pk
    def children(self):
        return Tag.objects.filter(parent=self.pk)

    def serializable_object(self):
        obj = {'name': self.name, 'children': []}
        for child in self.children():
            obj['children'].append(child.serializable_object())
        return obj

With that model, you can either do:

rootTag=Tag()
rootTag.serializable_object()

Or:

tags = []
for t in Tag.objects.filter(parent=None):
    tags.append(t.serializable_object())

# And if you need JSON, 
from django.utils import simplejson
simplejson.dumps(tags)
Community
  • 1
  • 1
Joseph
  • 12,678
  • 19
  • 76
  • 115
  • I'd never imagine template recursion could cause a problem, so thank you for sharing your solution. – Agate Oct 06 '13 at 09:57