Home > Software design >  Django - second trigger of htmx tries to load an unexpected URL gives a 403 error
Django - second trigger of htmx tries to load an unexpected URL gives a 403 error

Time:03-07

I have a table of cells, each with a different object instance, and using htmx to update the objects. I've created a CBV that takes the request.post from the htmx and saves the modified object to the db. The htmx does a hx-swap and loads a new <input> tag into my form, along with some background-color style based on the saved object. I can click on many cells and update several objects this way. This is working as expected and I don't see any errors.

However, the second time that I try to update the same cell/object, I get a 403 error. This error does not show in the browser. The cell just seems unresponsive. I can continue updating other cells. The error shows up in my browser console.

views

asessmentdetail - view that loads template and retrieves all the objects to fill in the table

def assessmentdetail(request, assess_pk, class_pk):
    """List the current grades for the assessment"""
    user = request.user
    assessment = Assessment.objects.get(id=assess_pk)
    classblock = Classroom.objects.get(id=class_pk)
    course_pk = classblock.course.pk
    objective_list = Objective.objects.all().filter(
        assessment=assess_pk).order_by('objective_name')
    student_list = Student.objects.all().filter(
        classroom=class_pk).order_by('nickname')
    gra = Grade.objects.filter(
        assessment=assess_pk).filter(cblock=classblock.id)
    context = {'objective_list': objective_list}
    new_or_update = gra.exists()
    grade_report = [str(new_or_update)]
    grade_list = []
    grade_id_array = []
    grade_report.append(len(objective_list))
    # print(len(objective_list))
    comment_list = []
    n = 0
    if gra:
        for student in student_list:
            # grade_report.append(student.nickname)
            comms = AssessComment.objects.filter(
                student=student, assessment=assessment).first()
            if comms:
                comment_list.append(comms)
            else:
                new_c = AssessComment(user=user,
                                      student=student, assessment=assessment, comment="---")
                new_c.save()
                comment_list.append(new_c)
            for obj in objective_list:
                if gra.filter(objective=obj, student=student.id).last():
                    grade_report.append(q.score)
                    grade_id_array.append(q.id)
                    grade_list.append(q)
                    n = n   1

    context['grade_report'] = grade_report
    context['grade_list'] = grade_list
    context['grade_id_array'] = grade_id_array
    context['student_list'] = student_list
    context['assessment'] = assessment
    context['class_pk'] = class_pk
    context['assess_pk'] = assess_pk
    context['course_pk'] = course_pk
    context['comment_list'] = comment_list
    context['classblock'] = classblock
)
    return render(request, "gradebook/assessment_detail.html", context)

GradeChange - the view that modifies the object via htmx

class GradeChange(SingleObjectMixin, View):
    """ view to handle htmx grade change"""
    model = Grade

    def post(self, request, *args, **kwargs):
        grade = self.get_object()
        ns = request.POST.get('score')
        new_score = ns.upper()

        def get_color(grade):  # map background color to the score
            if grade == "EXT":
                convert_code = "rgba(153,102,255,0.4)"
            elif grade == "APP " or grade == "PRF ":
                convert_code = "rgba(75, 192, 192, 0.7)"
            elif grade == "APP" or grade == "PRF":
                convert_code = "rgba(75, 192, 192, 0.3)"
            elif grade == "DEV":
                convert_code = "rgba(255, 205, 86, 0.4)"
            elif grade == "EMG" or grade == "BEG":
                convert_code = "rgba(225, 99, 132, 0.4)"
            else:
                convert_code = "rgba(0, 0, 0, 0.1)"
            return (convert_code)

        score_list = ["EXT", "APP ", "PRF ", "APP", "PRF", "DEV", "BEG", "EMG", "I", "---"]
        if new_score in score_list:
            grade.score = new_score
            grade.save()
            grade_score=str(grade.score)
            bgcode = get_color(grade.score)
            input_string=f'<input type="text" hx-post="{{% url "gradebook:grade-change" { grade.pk} %}}" hx-swap="outerHTML" hx-trigger="keyup delay:2s"  style="background-color:{ bgcode }" title="{ grade_score }" name="score" id="input-{{ forloop.counter0 }}" placeholder="{ grade.score }" required>'
        else:
            bgcode = get_color(grade.score)
            input_string=f'<input type="text" hx-post="{{% url "gradebook:grade-change" { grade.pk} %}}" hx-swap="outerHTML" hx-trigger="keyup delay:2s"  style="background-color:{ bgcode }; border:solid rgb(255, 0, 0,.5);" title="xxx" name="score" id="input-{{ forloop.counter0 }}" placeholder="{ new_score }" required>'
            
        return HttpResponse(input_string)  

template

<div >
  <div >
    <div >
      <h5>{{ assessment.assessment_name }}</h5>
    </div>
    <div >Class: {{ classblock }}, Assessment Date: {{ assessment.date_created|date:'Y-m-d' }}
    </div>
    <div  id="edit">
      <p>Click an item to edit.</p>
    </div>
    <div >
      <a href="{% url 'gradebook:addassessment' course_pk %}"><button type="submit" >Return to Assessment List</button></a>
    </div>
    <hr/>
  </div>
  {% if objective_list %}
  <div  id="create">
    <div >
      <div>No grades entered yet.</div>
    </div>
    <div >
      <a href="{% url 'gradebook:addgrades' assess_pk class_pk %}"><button >Add Grades</button></a>
    </div>
  </div>
  <div  id = "grade-table">
    <table >
      <thead>
        <tr>
          <th  scope="col">Students</th>
          {% for obj in objective_list %}
          <th  scope="col">{{ obj.objective_name }}</th>
          {% endfor %}
          <th scope="col">Comments</th>
        </tr>
      </thead>
      <tbody>
        <form action="" method="post" >
        <div id="hxtarget">test</div>
        {% for student in student_list %}
        <tr>
          <td >{{ student.student_first }}&nbsp;{{ student.student_last }}</td>
          {% for g in grade_list %}
            {% if g.student.id == student.id %}
            <td>
              <input type="text" hx-post="{% url 'gradebook:grade-change' g.pk %}" hx-swap="outerHTML" hx-trigger="keyup delay:2s"  title={{ g.score }} name="score" id="input-{{ forloop.counter0 }}" placeholder={{ g.score }} required>
            </td>
            {% endif %}
          {% endfor %}
          <td>
            {% for comms in comment_list %}
              {% if comms.student == student %}
                <a  href="{% url 'gradebook:addcomment' comms.pk assess_pk class_pk %}">{{ comms.comment|truncatewords:10 }}</a>
              {% endif %}
            {% endfor %}
          </td>
        </tr>
        {% endfor %}
      </form>
      </tbody>
    </table>
  </div>
  
  {% else %}
  
  <p>No objectives. Please add an objective to this assignment.</p>
  {% endif %}
</div>

urls

path('assessmentdetail/<uuid:assess_pk>/<uuid:class_pk>/',
         views.assessmentdetail, name='assessdetail'),
path('grade-change/<uuid:pk>/', views.GradeChange.as_view(), name='grade-change'),

The error I get in the console is:

Request URL: http://127.0.0.1:8000/gradebook/assessmentdetail/3f4c7422-89d0-442c-a310-46aff8123949/be8ef548-25a7-4793-99d1-ff3cc4166921/{% url
Request Method: POST
Status Code: 404 Not Found```

The url for this page in my browser is http://127.0.0.1:8000/gradebook/assessmentdetail/3f4c7422-89d0-442c-a310-46aff8123949/be8ef548-25a7-4793-99d1-ff3cc4166921/. I don't know where the {% url comes from. I don't understand htmx well enough to know why it's calling this url.

CodePudding user response:

The problem is that the response is not HTML but unrendered Django Template code, that's obviously not gonna work on the frontend. You must return pure HTML (or use a frontend template system, but it's not the topic of your question.)

The problematic variable is the input_string:

input_string=f'<input type="text" hx-post="{{% url "gradebook:grade-change" { grade.pk} %}}" hx-swap="outerHTML" hx-trigger="keyup delay:2s"  style="background-color:{ bgcode }" title="{ grade_score }" name="score" id="input-{{ forloop.counter0 }}" placeholder="{ grade.score }" required>'

HTMX expects that this is valid HTML, so the hx-post attribute will be just: hx-post="{{% url ". That's because the Django Template's url is not evaluated on the backend. So there's the origin of the strange {% url string in your logs, the { is the URL-encoded version of { bracket.

The fix is very easy, just use the reverse() method on the backend:

from django.urls import reverse

input_string=f'<input type="text" hx-post="{reverse("gradebook:grade-change", args=[grade.pk])}" hx-swap="outerHTML" hx-trigger="keyup delay:2s"  style="background-color:{ bgcode }" title="{ grade_score }" name="score" placeholder="{ grade.score }" required>'

Note: I have removed the id="input-{{ forloop.counter0 }}" because it seems unnecessary and requires passing the loop counter from the frontend. It's possible of course with e.g. hx-vals if you need it.

  • Related