Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Django include template tag in for loop only catches first iteration

I have a comments section on some pages on my site that I build with a {% for ... %} loop (and another nested loop for comment replies. The section was hacked together, and I am still learning web development and Django, so please forgive any frustrating sloppiness or weirdness. I am not concerned with efficiency at the moment, only efficacy, and right now it is not working quite right.

For each comment I have a Bootstrap dropdown button that will bring up the options Edit and Delete. Edit will open a modal to edit the comment. The modals are rendered with an {% include %} tag. Below I have included part of my code unmodified, rather than trying to simplify my example and risk leaving something crucial out:

<div class="panel panel-default">
    {% for comment in spot.ordered_comments %}
    <div class="panel-heading row">
        <div class="col-sm-10">
            <strong>{{ comment.poster.username }}</strong>
            <em style="margin-left: 2em">{{ comment.created|date:'M d \'y \a\t H:i' }}</em>
        </div>
        <div class="btn-group col-sm-2" role="group">

            {% if comment.poster == user %}
            <form id="delete-comment-form" class="form"
                  method="post" action="{% url 'delete_comment' spot.id comment.id %}">
                {% csrf_token %}
            </form>

            {% include 'topspots/editmodal.html' with edit_type='comment' %}

            <div class="btn-group">
                <button type="button" class="btn btn-default dropdown-toggle"
                        data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
                    <i class="fa fa-edit"></i> <span class="caret"></span>
                </button>
                <ul class="dropdown-menu">
                    <li><a href="#" data-toggle="modal" data-target="#editModal">Edit</a></li>
                    <li role="separator" class="divider"></li>
                    <li>
                        <a href="javascript:;" onclick="$('#delete-comment-form').submit();">Delete</a>
                    </li>
                </ul>
            </div>

            {% endif %}

            {% if user.is_authenticated %}
            {% include 'topspots/replymodal.html' %}
            <button type="button" class="btn btn-default" data-toggle="modal"
                    data-target="#replyModal">
                Reply
            </button>
            {% endif %}
        </div>
    </div>

    <div class="panel-body">
        <div class ="row">
            <div class="col-sm-8">
                {{ comment.comment_text }}
            </div>
        </div>
        <br/>

        <!-- Comment replies -->
        {% if comment.commentreply_set %}
        {% for reply in comment.commentreply_set.all %}
        <div class="row" style="padding-left: 1em">
            <div class="col-sm-8 well">
                <p>{{ reply.reply_text }}</p>
                <div class="row">
                    <div class="col-sm-4">
                        <p>
                            <strong>{{ reply.poster.username }}</strong>
                            <em style="margin-left: 2em">{{ comment.created|date:'M d \'y \a\t H:i' }}</em>
                        </p>
                    </div>
                    {% if reply.poster == user %}
                    {% include 'topspots/editmodal.html' with edit_type='reply' %}
                    <form id="delete-reply-form" class="form"
                          method="post" action="{% url 'delete_reply' spot.id reply.id %}">
                        {% csrf_token %}
                    </form>
                    <div class="col-sm-2">
                        <div class="btn-group">
                            <button type="button" class="btn btn-default dropdown-toggle"
                                    data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
                                <i class="fa fa-edit"></i> <span class="caret"></span>
                            </button>
                            <ul class="dropdown-menu">
                                <li><a href="#" data-toggle="modal"
                                       data-target="#editModal">Edit</a></li>
                                <li role="separator" class="divider"></li>
                                <li>
                                    <a href="javascript:;"
                                       onclick="$('#delete-reply-form').submit();">Delete</a>
                                </li>
                            </ul>
                        </div>
                    </div>
                    {% endif %}
                </div>
            </div>
        </div>
        {% endfor %}
        {% endif %}

    </div>
    {% endfor %}
</div>

Here is the edit modal:

<!-- editmodal.html -->
{% load static %}

<div class="modal fade" id="editModal" tabindex="-1" role="dialog" aria-labelledby="editModalLabel">
    <div class="modal-dialog" role="document">
        <div class="modal-content">
            <div class="modal-header">
                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                    <span aria-hidden="true">&times;</span></button>
                <h2 class="modal-title" id="editModalLabel">
                    Edit {{ edit_type }}:
                </h2>
            </div>
            <form action="{% url 'edit_comment' spot.id comment.id %}" method="post">
                <div class="modal-body">
                    <input class="form-control" name="text" value="{{ comment.comment_text }}" autofocus>
                    <input type="hidden" name="edit_type" value="{{ edit_type }}">
                    {% csrf_token %}
                </div>
                <div class="modal-footer">
                        <button type="submit" class="btn btn-default">Finish editing</button>
                </div>
            </form>
        </div>
    </div>
</div>
<script>

    $('.modal').on('shown.bs.modal', function() {
        $(this).find('[autofocus]').focus();
    });

</script>

and the reply modal:

<!-- replymodal.html -->
{% load static %}

<div class="modal fade" id="replyModal" tabindex="-1" role="dialog" aria-labelledby="replyModalLabel">
    <div class="modal-dialog" role="document">
        <div class="modal-content">
            <div class="modal-header">
                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                    <span aria-hidden="true">&times;</span></button>
                <h2 class="modal-title" id="replyModaLabel">
                    Reply to <strong>{{ comment.poster.username }}'s</strong> comment
                </h2>
            </div>
            <div class="modal-body">
                <form action="{% url 'reply_comment' spot.id comment.id %}" method="post">
                    <input class="form-control" name="reply_text" placeholder="Write a reply..." autofocus>
                    {% csrf_token %}
                </form>
            </div>
        </div>
    </div>
</div>
<script>

    $('.modal').on('shown.bs.modal', function() {
        $(this).find('[autofocus]').focus();
    });

</script>

The issue I am having is that my reply and edit modals (e.g. {% include 'topspots/editmodal.html' with edit_type='reply' %} or {% include 'topspots/replymodal.html' %} seem to be only rendered once with the context of the first iteration of my for loop. So even though all the questions are correctly rendered on the page, when I click reply, edit or delete, regardless of which button I click (i.e., whether I click the button for the first comment, or the fifth comment, etc.) I can only reply to, edit, or delete the very first comment. I have a feeling that this has something to do with closures and scope in a way I am not quite understanding (I have gotten into trouble in the past with unexpected results using lambda in Python loops because of this or this), but I am not sure.

I did a test with the following view:

def test(request):
    spots = Spot.objects.all()
    return render(request, 'test.html', {'spots': spots})

and templates:

<!-- test.html -->
<h1>Hello world</h1>
{% for spot in spots %}
    {% include 'testinclude.html' %}
{% endfor %}

and

<!-- testinclude.html -->
<h3>{{ spot.name }}</h3>

And it printed out a list of unique spot names, so why the difference with the modals?

like image 464
elethan Avatar asked Jul 07 '16 16:07

elethan


People also ask

What does {% %} mean in Django?

This tag can be used in two ways: {% extends "base.html" %} (with quotes) uses the literal value "base.html" as the name of the parent template to extend. {% extends variable %} uses the value of variable . If the variable evaluates to a string, Django will use that string as the name of the parent template.

How do I loop a list in a Django template?

One can loop over a list in reverse by using {% for obj in list reversed %} .

Which Django template syntax allows to use the for loop?

To create and use for loop in Django, we generally use the “for” template tag. This tag helps to loop over the items in the given array, and the item is made available in the context variable.

What is the use of template tags in Django?

The template tags are a way of telling Django that here comes something else than plain HTML. The template tags allows us to to do some programming on the server before sending HTML to the client.


2 Answers

As emulbreh postulates, a modal is in fact rendered for every comment. However, all of the modals have the same ID, so regardless of which comment’s edit button was clicked, the first modal gets triggered every time. IDs are supposed to be unique across an HTML document.

How can you fix this? You can make the IDs of the modals unique to each comment. You can get a unique identifier by writing id="editModal-{{ comment.id }}" or just id="editModal-{{ forloop.counter }} (documentation here).

But then your editModal.html template is coupled very tightly with your ‘master’ template. A better solution would be to use classes instead of IDs and put the identification where it belongs: the container of each comment. You can try:

  1. adding an ID to each comment’s container:

    <div class="panel panel-default">
    {% for comment in spot.ordered_comments %}
      <div class="panel-heading row" id="comment-{{ comment.id }}">
        ...
    
  2. using classes instead of IDs in your modal templates as so:

    <!-- editmodal.html -->
    {% load static %}
    
    <div class="modal fade editModal" tabindex="-1" ...>
      ...
    
  3. changing data-target in your buttons from:

    <li><a href="#" data-toggle="modal" data-target="#editModal">Edit</a></li>
    

    to:

    <li><a href="#" data-toggle="modal" data-target="#comment-{{ comment.id }} .editModal">Edit</a></li>
    
like image 173
Yatharth Agarwal Avatar answered Oct 06 '22 12:10

Yatharth Agarwal


It looks like all your edit modals will have the same id id="editModal" as well as your reply modals id="replyModal" if you are showing them based on id probably you will always open the first DOM element with that id. You could try appending a unique identifier like forloop.counter

https://docs.djangoproject.com/en/1.9/ref/templates/builtins/#for

like image 36
ed_ Avatar answered Oct 06 '22 11:10

ed_