Updating model after click event with JavaScript

Hello,

I have a to do app that I’m working on and I have 3 models, a custom user model, a list model, and an item model. The item model represents list items that can be on a specific list. The item has a foreign key relationship to a list and a list is mapped to a specific user.

In my item model I have a field for “completed”, which defaults to False, but I would like this to be updated to True if a user clicks a checkbox. I would like to do this in JavaScript, but I’m unsure about how to properly update a model with JS. As a note, I haven’t serialized my models, but if this would make it easier than just updating a model directly, then I would certainly look into that.

Here is my JS code that I’ve written, and you can see from the photo I’ve added at the bottom, I’m getting a response in the console, which is great, but the url route is not what I was expecting from my fetch request in my JS code.

// function to mark list item as completed.
document.addEventListener('DOMContentLoaded', function() {
    let checkboxes = document.querySelectorAll('#completed')
    checkboxes.forEach(checkbox => {
        checkbox.onclick = function() {
            const pk = checkbox.dataset.id
            fetch(`/${pk}/item`, {
                method: 'PUT',
                body: JSON.stringify({
                    completed: (completed ? false : true)
                })
            })
        }
    });
});

Here is my urls.py file. (I’ve excluded the imports, but i’ve imported my views)

urlpatterns = [
    path('signup/', SignUpView.as_view(), name='signup'),
    path('<int:pk>/todolists/', ToDoListView.as_view(), name='todolists'),
    path('list_new/', ToDoListCreateView.as_view(), name='list_new'),
    path('<int:pk>/item_new/', item_new_view, name='item_new'),
    path('<int:pk>/delete/', ToDoListDeleteView.as_view(), name='list_delete'),
    path('<int:pk>/item', mark_item_as_completed_view, name='item_completed')
]

Here is the specific view that is going to handle the fetch. I’ve worked on this bit the least:

def mark_item_as_completed_view(request, pk):
    if request.method == "PUT":
        return JsonResponse({
            "success: completed"
        })

G’day g’day,

Is the urls.py file in your post your app specific URLs or your project’s URLs? Is it possible for you two share both the project level and app level URLs? I ask this because the URL in your console debugger is quite long and the URL for what I assume is your app level URL should be <integer>/item whereas your URL appears to also include the URL for the ToDoListCreateView.

Am I right in guessing the your project level URLs has something along the lines of:

path("lists/", include("your_app_name.urls")),

From looking at the console debugger output and your urls.py, I’m guessing that the fetch() function in your JS is prepending to many paths to the URL of your request.

One thing I can suggest doing is to hardcode your URL into your JS as a test or use a tool like Postman to test your endpoints.

And almost forgot to ask, are you using Django by itself or are you also using Django Rest Framework. If the former, you’ll have to include the CSRF cookie

Hello Conor,

Your assumption is correct, I have a project level urls.py file and then what I shared in my first post is my app level urls.py file. Also I’m not using DRF, just traditional Django. Here are the project level urls again without my imports:

urlpatterns = [
    path('admin/', admin.site.urls),
    path('lists/', include('lists.urls')),
    path('lists/', include('django.contrib.auth.urls')),
    path('', TemplateView.as_view(template_name='home.html'),
        name='home')
]

I’ve tried previously to hardcode the route into the fetch request as well, but that hasn’t been successful. Perhaps the interaction between my project level urls and app level urls is the cause?

I tried the hardcoding again just to double check. Given the pk 31 for a specific item, when the checkbox is clicked it still prepends lists/23/todolists/lists/ so the full path would be: lists/23/todolists/lists/31/item.

Here’s the hardcoded JS:

// function to mark list item as completed.
document.addEventListener('DOMContentLoaded', function() {
    let checkboxes = document.querySelectorAll('#completed')
    checkboxes.forEach(checkbox => {
        checkbox.onclick = function() {
            const pk = checkbox.dataset.id
            fetch("/31/item", {
                method: 'PUT',
                body: JSON.stringify({
                    completed: (completed ? false : true)
                })
            })
        }
    });
});

I was more thinking of hardcoding the entire URL, for example, http:localhost:8000/lists/31/item

Please see my questions about your URLs and DRF.

Sorry, my latest response might have overshadowed my first. I answered the DRF and urls question above, but I’ve copied and pasted it below:

Hello Conor,

Your assumption is correct, I have a project level urls.py file and then what I shared in my first post is my app level urls.py file. Also I’m not using DRF, just traditional Django. Here are the project level urls again without my imports:

urlpatterns = [
    path('admin/', admin.site.urls),
    path('lists/', include('lists.urls')),
    path('lists/', include('django.contrib.auth.urls')),
    path('', TemplateView.as_view(template_name='home.html'),
        name='home')
]

I’ve tried previously to hardcode the route into the fetch request as well, but that hasn’t been successful. Perhaps the interaction between my project level urls and app level urls is the cause?

Thanks,
Jack

Mea culpa, I missed your first reply.

I’m guessing you’re developing this on your own, local machine? Can you navigate to the URL in your web browser? I know it’s not a PUT request, but this will help you determine if the URL is reachable. If so, then I’m leaning towards there being an issue with fetch()method in your JS. I’m not much a JS wizard, so I might be missing something obvious, but if you’re feeding "/31/item" into fetch() and getting an incorrect URL as a result, then I’d be inclined the look there.

If you want to test this programatically with python, the you can use requests to fire off a put request:

import requests  # pip install requests

url = "http:localhost:8000/lists/31/item"
data = {"completed": True}
response = requests.put(url, json=data)
print(response.content)  # this will print the raw bytes response. Handy for debugging when things go wrong
print(response.json())  # will print the JSON response if there is one

If a simple test program like the one above works, then I’d say fetch() if the culprit.

Also, if there is more than one attribute to the list model, and not just complete, you can consider using PATCH instead of PUT. PUT recreates the object whereas PATCH can modify just one or more attributes of a model.

I’ve made some progress here now. I’ve been using Chrome, but noticed that changes I was making in my fetch request were not being reflected in the console. I switched to Safari and noticed a difference immediately. I changed my fetch request to /lists/${pk}/item and noticed that this fixed the route it was looking for and it dropped that long prepended route. Chrome however is still showing me that long route I was seeing, maybe I need to clear cache and cookies?

One other issue was that my fetch request originally was /${pk}/item, so adding lists to the beginning of my request fixed some issues. I’ve also added a csrf_exempt decorator on top of the view for the time being.

So for right now it’s getting the right route and view, now I just need to figure out how to actually change my model from False to True, because that’s still not working.

Thank you for your help.

Good to hear. This is just going from the top of my head (I’m about to nod off for the night), so definitely sanity check this, but something along these lines would be the steps to be required to receive some JSON and modify a model instance. Note If working with Django and a frontend, Django Rest Framework is your very, very best friend - it is mine.

from django.shortcuts import get_object_or_404
from lists.models import List


def mark_item_as_completed_view(self, request):
    if request.method == "PUT":
        # get the ID KWARG
        list_id = request.kwargs.get("pk", None)
        if list_id is not None:
            try:
                list_obj = get_object_or_404(List, pk=list_id)
                completed = request.POST.get("completed", None)
                # good idea to do some more error checking here, but its getting late
                list_object.update(completed=completed)
                return JsonResponse({
                    "success": completed,
                    "list_id": list_obj.id
                })
            except List.DoesNotExist:
                # handle the exception
                pass
        return JsonResponse({"error": "something went wrong"}, status=401)

Hope this helps. Good luck!

Thank you, Conor! I really appreciate your time and help.