views:

99

answers:

3

Hay,

i have an app which essentially a conversation system (much like reddits)

Where a post can have multiple replies, and a reply and have multiplies, and a reply to a reply can have multiple replies (etc)

I've made the model like this

class Discussion(models.Model):
    message = models.TextField()
    replies = models.ManyToManyField('self')

and the view

discussions = Discussions.objects.all()

and the template looks like this

{% for discussion in discussions %}
    {{ discussion.message }}
{% endfor %}

How would i go about making a system where i can output all replies like this

discussion
    reply
        reply
    reply
        reply
            reply
                reply

Which would go down as far as it needs to to ensure all replies are listed.

Thanks

A: 

Check out django-threadedcomments.

Also, the parent-reply relationship isn't really a ManyToMany—it's a a parent-child OneToMany, because a comment (in traditional threaded comment models, anyway) can only be a reply to, at most, one other comment.

Hank Gay
That link is way over my head. Any other solutions?
dotty
+1  A: 

The first step is fixing your model. Look up rather than looking down.

class Discussion(models.Model):
    message = models.TextField()
    parent = models.ForeignKey(Discussion, null=True, blank=True)

    def get_children(self):
        return Discussion.objects.filter(parent=self)

When something doesn't have a parent, it's a root thread. When it does, it's a reply.

Your display logic needs to change a bit. Instead of iterating all comments, iterate the top-level posts. Your comment.html file might look like this:

{{ comment.message }}
{% for comment in comment.get_children %}
    {% include comment.html %}
{% endfor %}

In your main template you'd have:

{% for comment in base_comments %}
    {% include 'comment.html' %}
{% endfor %}

And in your view, add 'base_comments':Discussion.objects.filter(parent=None) to your context.

There's of course a UI element to this where you need to format things and handle the replying process, but I'll leave that up to you.

And don't forget that you can outsource all this very easily. I use Disqus very effectively.

Oli
+2  A: 

Unless a reply can be a reply to multiple posts, a ManyToManyField isn't what you want. You just need a ForeignKey:

class Discussion(models.Model):
    message = models.TextField()
    reply_to = models.ForeignKey('self', related_name='replies', 
        null=True, blank=True)

Then you can get to a Discussion's replies with Discussion.replies.

Unfortunately, there's no way to do a recursion in Django's template language, so you have to either 1) run a recursive function to get a "flattened" list of replies, and put that in the context, or 2) write a function that can be called recursively that uses a template to generate each level, which would look something like:

_DiscussionTemplate = Template("""
<li>{{ discussion.message }}{% if replies %}
    <ul>
        {% for reply in replies %}
        {{ reply }}
        {% endfor %}
    </ul>
{% endif %}</li>
""".strip())

class Discussion(models.Model):
    message = models.TextField()
    reply_to = models.ForeignKey('self', related_name='replies', 
        null=True, blank=True)

    @property
    def html(self):
        return _DiscussionTemplate.render(Context({
            'discussion': self,
            'replies': [reply.html() for reply in self.replies.all()]
        }))

Then in your top-level template, you just need:

<ul>
    {% for d in discussions %}
    {{ d.html }}
    {% endfor %}
</ul>

Apply CSS as desired to make it look nice.

EDIT: Root discussions are those in Discussion.objects.filter(reply_to=None). And all the code, _DiscussionTemplate included, goes in your models.py. This way, _DiscussionTemplate is initialized once when the module loads.

EDIT 2: Putting the HTML in a template file is pretty straightforward. Change the view code that sets _DiscussionTemplate to:

_DiscussionTemplate = loader.get_template("discussiontemplate.html")

Then create discussiontemplate.html:

<li>{{ discussion.message }}{% if replies %}
    <ul>
        {% for reply in replies %}
        {{ reply }}
        {% endfor %}
    </ul>
{% endif %}</li>

Set the path to the template file as needed.

Mike DeSimone
Where does _DiscussionTemplate go? Is it in my model? Do i have to import template?
dotty
symmetrical=False throw up an error so i removed it, will this still work?
dotty
How can I tell which are root discussions?
dotty
Brilliant answer. Thank you very much.
dotty
Just a follow up, this works well. However, is there any way to move the template from the model into it's own template? So it can reside in templates/includes/discussiontemplate ?
dotty
I have another field (votes, which is an integer field, obviously mimicking reddit's noting system) i want to order by votes (desc). How would i do that?
dotty
Replace `self.replies.all()` with `self.replies.all().order_by('-votes')`.
Mike DeSimone
Theres a follow up discussion over at http://stackoverflow.com/questions/2950155/reddit-style-voting-with-djangoIf you fancy lending another helping hand :)
dotty
A follow up to thisWhat does the reply.html() in reply.html() for reply in self.replies.all() do? Im aware the second part is a for loop retrieving all the replies
dotty
It specifies what values you want in the list. List comprehensions can be generalized as `[f(k) for k in X] = [f(X[0]), f(X[1]), ... f(X[N-1])]` where `f(k)` is some function of `k`, `X` is your iterable (list, tuple, dict, file, whatever), and `N` is the number of items in `X`. In this case, `f(k) = k.html()`, `k = reply`, and `X = self.replies.all()`.
Mike DeSimone