Building a todo app using Vue.js and Django as the backend

Building a todo app using Vue.js and Django as the backend

Published 02. jul 2018 05:41 by Stein Ove Helset

Building a todo app is a trivial thing to do when you know a language, but it's also a great way to learn many of the key concepts of programming.

Setting up Django

We'll be starting of this tutorial by installing and setting up Django. First we'll create a virtual enviroment named "vuengo_env" (Vue + Django = Vuengo):


$ virtualenv -p vuengo_env
Running virtualenv with interpreter /Users/username/.pyenv/versions/3.5.0/bin/python3.5
Using base prefix '/Users/username/.pyenv/versions/3.5.0'
New python executable in djitter_env/bin/python3.5
Also creating executable in djitter_env/bin/python
Installing setuptools, pip, wheel...done.

The virtual environment should now be set up and you can go in and activate it:


$ cd vuengo_env
$ source bin/activate

The virtual environment is now activated and python command you run now only affects code inside the virtual environment. So we'll now install django and django-cors-headers.


$ pip install django
Collecting django
Collecting pytz (from django)
Installing collected packages: pytz, django
Successfully installed django-2.0.5 pytz-2018.4

$ pip install djangorestframework
Collecting djangorestframework
Installing collected packages: djangorestframework
Successfully installed djangorestframework

$ pip install django-cors-headers
Collecting django-cors-headers
Installing collected packages: django-cors-headers
Successfully installed django-cors-headers-2.2.0

The reason we install django-cors-headers is to allow request done via Ajax. Without this we would get an error in our browser. If you want to read more about django-cors-header you can check out their github page. We're also installing djangorestframework which is used to handle the API requests, serialization and so on. There are few more steps before the Django setup is finished. First thing you can do is to create the project and go in to it's directory by running:


$ django-admin.py startproject vuengo
$ cd vuengo

Inside the vuengo folder is a manage.py which is used to run the development server, connect to the shell and some more tasks. Right now you don't need to worry about this. We need to make a few changes to the settings file, so open vuengo/settings.py in your favorite editor. Add the following lines:


# Add 'corsheader' to the INSTALLED_APPS block:
INSTALLED_APPS = (
    ...
    'corsheaders',
    'rest_framework',
    ...
)
# Add this lines where you want CORS_ORIGIN_ALLOW_ALL = True # Add 127.0.0.1 to ALLOWED_HOSTS ALLOWED_HOSTS = ['127.0.0.1'] # Add 'corsheaders.middleware.CorsMiddleware', over 'django.middleware.common.CommonMiddleware' in the MIDDLEWARE block: MIDDLEWARE = [     ...     'corsheaders.middleware.CorsMiddleware',     'django.middleware.common.CommonMiddleware',     ...

We add 'corsheader' to the INSTALLED_APPS because Django needs to know that we want to use this app. CORS_ORIGIN_ALLOW_ALL = True is there to tell Django that we accept connections from everywhere. We can now try to run the django development server:


$ python manage.py makemigrations
$ python manage.py migrate
$ python manage.py runserver

Go to your browser and open http://127.0.0.1:8000 and you will see something very similar to this. We need to create a admin user so we can communicate with the database. Run this command:


$ python manage.py createsuperuser
Username (leave blank to use 'username'): admin
Email address: admin@example.com
Password: password123
Password (again): password123
Superuser created successfully.

Now Django should be working as we want it and we can continue to the next part.

Django - Welcome

Setting up Vue

In this part of the tutorial we're going to set up Vue by installing it and setting up a project using Vue CLI. We are also going to be using this webpack boilerplate https://github.com/vuejs-templates/webpack to make the set up as easy as possible. Write the following code in your command line:


$ npm install -g vue-cli
$ vue init webpack vuengo
$ cd vuengo
$ npm install
$ npm run dev

? Project name vuengo
? Project description A Vue.js project
? Author Stein Ove Helset <steinove@ahackersday.com>
? Vue build standalone
? Install vue-router? Yes
? Use ESLint to lint your code? No
? Set up unit tests No
? Setup e2e tests with Nightwatch? No
? Should we run `npm install` for you after the project has been created? (recommended) npm

Once you have created the project, opened the folder and run "npm run dev" you can open http://localhost:8080/ in you browser and you will see something like this.

Vuengo - vue welcome

To communicate with the backend we are going to use Axios. Install it by running:


npm install axios

The last part of setting up Vue is to open up index.html in your editor and modify it to look like this:


<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <title>vuengo</title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.1/css/bulma.min.css">
  </head>
  <body>
    <section class="section">
      <div class="container">
        <div id="app"></div>
      </div>
    </section>
    <!-- built files will be auto injected -->
  </body>
</html>

I just made some changes to include Bulma css framework and added a container to wrap around our app. The Vue part of Vuengo is finished set up now and we're ready to go to the next part where we will start setting up the Django apps and models.


Setting up django apps and models

To keep things simple we just need one app in our Django project. We can call it "task", run this command:


$ python manage.py startapp task

Inside the new task folder are a couple of files. The two files we're going to use now is models.py, serializers.py and views.py. Open those two files in your editor and modify them to look like this:


# models.py

from django.db import models

class Task(models.Model): # Our database model is called Task
    TODO = 0
    DONE = 1

    STATUS_CHOICES = ( # We create a tuple of status choices
        (TODO, 'To do'),
        (DONE, 'Done')
    )

    description = models.CharField(max_length=255) # The tasks description is limited to 255 characters
    status = models.IntegerField(choices=STATUS_CHOICES, default=TODO) # The task's status, default status = TODO

# serializers.py

from rest_framework import serializers

from .models import Task

class TaskSerializer(serializers.HyperlinkedModelSerializer):
    class Meta:
        model = Task
        fields = ('id', 'description', 'status')

# views.py

from .models import Task # Import our Task model
from .serializers import TaskSerializer # Import the serializer we just created

# Import django rest framework functions

from rest_framework import viewsets 
from rest_framework.authentication import BasicAuthentication
from rest_framework.permissions import IsAuthenticated

class TaskViewSet(viewsets.ModelViewSet): # Create a class based view
    """
    API endpoint that allows tasks to be viewed or edited.
    """
    authentication_classes = (BasicAuthentication,)
    permission_classes = (IsAuthenticated,)
    queryset = Task.objects.all() # Select all taks
    serializer_class = TaskSerializer # Serialize data

The model for describing our data to the database is finished and we've created a view for returning our tasks. We need to tell our Django project about our task app, we do this by adding 'task' to settings.py just like we did when we added 'corsheaders'.
Next step will be to add a couple of tasks to the database.
But first you need to add "task" to the INSTALLED_APPS inside "vuengo/settings.py".


$ python manage.py makemigrations # We need to create the database
$ python manage.py migrate # We need to create the database
$ python manage.py shell # Opens up a shell
Inside the shell that we just opened you can write the following commands:

$ from task.models import Task # Import the model
$ task1 = Task.objects.create(description='Description 1')
$ task2 = Task.objects.create(description='Description 2')
$ task3 = Task.objects.create(description='Description 3', status=1)
$ Task.objects.all() # Check if the tasks has been created
<QuerySet [<Task: Task object (1)>, <Task: Task object (2)>, <Task: Task object (3)>]> # Our three tasks

You can close the shell now. The last step to make the api/view accessible for the Vue app is to add api_get_tasks to the urls config. So go ahead and open vuengo/urls.py in your editor and modify it to look like this:


from django.urls import path, include

from rest_framework import routers # Import the router

from task.views import TaskViewSet # Import the view we just created

router = routers.DefaultRouter() # Define the router with our view
router.register(r'tasks', TaskViewSet)

urlpatterns = [
    path('', include(router.urls)), # Add the view to the patterns
]

To test this we can run a CURL command. Test for your self by running this command:

$ curl -H 'Accept: application/json; indent=4' -u admin:password123 http://127.0.0.1:8000/tasks/
[
    {
        "id": 1,
        "description": "Description 1",
        "status": 1
    },
    {
        "id": 2,
        "description": "Description 2",
        "status": 1
    },
    {
        "id": 3,
        "description": "Description 3",
        "status": 1
    }
]

There you'll now got the json code for our tasks. This part is finished and we're now heading back to the Vue app again.


The basic structure of our vue app

Open up src/components/HelloWorld.vue in your editor and strip away some of the boilerplate code to make it look like this:


<template>
  <div class="hello">

  </div>
</template>

<script>
export default {
  name: 'HelloWorld',
  data () {
    return {
    }
  }
}
</script>



This is just some basic html and a component initializer. We'll jump right to next step which is to create the html structure. Modify the contents of HelloWorld.vue to look like this:


<template>
  <div class="hello">
    <h1 class="title">Vuengo</h1> <!-- Page title -->

    <hr>

    <div class="columns">
      <div class="column is-one-third is-offset-one-third"> <!-- Narrow centered column -->
        <form><!-- Form for adding tasks -->
          <h2 class="subtitle">Add task</h2>

          <div class="field"> <!-- Normal input field for the description -->
            <label class="label">Description</label>
            <div class="control">
              <input class="input" type="text">
            </div>
          </div>

          <div class="field"> <!-- Select field for choosing the status (0 and 1 as value, same as in the django status choices) -->v
            <label class="label">Status</label>
            <div class="control">
              <div class="select">
                <select>
                  <option value="0">To do</option>
                  <option value="1">Done</option>
                </select>
              </div>
            </div>
          </div>

          <div class="field is-grouped"> <!-- Submit button -->
            <div class="control">
              <button class="button is-link">Submit</button>
            </div>
          </div>
        </form>
      </div>
    </div>

    <hr>

    <div class="columns">
      <div class="column is-half"> <!-- Half of the column for todo tasks -->
        <h2 class="subtitle">Todo</h2>

        <div class="todo">
          <div class="card">
            <div class="card-content">
              <div class="content">
                Task description
              </div>
            </div>

            <footer class="card-footer">
              <a class="card-footer-item">Done</a> <!-- Button for setting a task to done -->
            </footer>
          </div>
        </div>
      </div>

      <div class="column is-half"> <!-- Half of the column for done tasks -->
        <h2 class="subtitle">Done</h2>

        <div class="done">
          <div class="card">
            <div class="card-content">
              <div class="content">
                Task description
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'HelloWorld',
  data () {
    return {
    }
  }
}
</script>

<style scoped>
.select, select { /* 100% width for the select */
  width: 100%;
}

.card { /* Adding some air under the tasks */
  margin-bottom: 25px;
}

.done { /* Make the done tasks a little bit transparent */
  opacity: 0.3;
}
</style>

Now we're ready to start pulling the tasks from the server and showing them. Head over to the next part and get started.


Getting the tasks

In the top of <script> inside HelloWorld.vue add this line to import Axios:


import axios from 'axios'

Replace the 'export default' with this one:


export default {
  name: 'HelloWorld',
  data () {
    return {
      tasks: [] // Array for holding the tasks
    }
  },
  mounted () { // This will be called when HelloWorld is loaded
    this.getTasks(); // Call our getTasks function below
  },
  methods: {
    getTasks() {
        axios({
            method:'get',
            url: 'http://127.0.0.1:8000/tasks/',
            auth: {
                username: 'admin',
                password: 'password123'
            }
        }).then(response => this.tasks = response.data);
    }
  }
}

That was all we needed to get our tasks from the backend and load them into an array. Now we need to make a few changes to the template in order for it to show the tasks:


<div class="columns">
  <div class="column is-half">
    <h2 class="subtitle">Todo</h2>

    <div class="todo">
      <div class="card" v-for="task in tasks" v-if="task.status == 0"> <!-- Loop through the tasks array, if status is 0 (to do) then we'll show it. -->
        <div class="card-content">
          <div class="content">
            {{ task.description }} <!-- Print the task's description here -->
          </div>
        </div>

        <footer class="card-footer">
          <a class="card-footer-item">Done</a>
        </footer>
      </div>
    </div>
  </div>

  <div class="column is-half">
    <h2 class="subtitle">Done</h2>

    <div class="done">
      <div class="card" v-for="task in tasks" v-if="task.status == 1"> <!-- Loop through the tasks array, if status is 1 (done) then we'll show it. -->
        <div class="card-content">
          <div class="content">
            {{ task.description }}
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

If you open up the site (http://localhost:8080) in your browser now you should se two tasks to the left (to do) and one task in the right column which is done. Perfect :-)


Adding a task

First step is going to be a change to the Vue template. Change the to column to look like this:


<div class="columns">
  <div class="column is-one-third is-offset-one-third">
    <form v-on:submit.prevent="addTask"> <!-- v-on:submit.prevent="addTask" calls the function addTask on submit -->
      <h2 class="subtitle">Add task</h2>

      <div class="field">
        <label class="label">Description</label>
        <div class="control">
          <input class="input" type="text" v-model="description"> <!-- Connects this field to the description variable -->
        </div>
      </div>

      <div class="field">
        <label class="label">Status</label>
        <div class="control">
          <div class="select">
            <select v-model="status"> <!-- Connects this field to the status variable -->v
              <option value="0">To do</option>
              <option value="1">Done</option>
            </select>
          </div>
        </div>
      </div>

      <div class="field is-grouped">
        <div class="control">
          <button class="button is-link">Submit</button>
        </div>
      </div>
    </form>
  </div>
</div>

Inside the 'export default' we need to add two more properties to the data array. Make it look like this:


data () {
  return {
    tasks: [],
    description: '',
    status: 0
  }
},

Last but not least, we need to create the addTask function that communicates with the server:


addTask() { // Function
  if (this.description) { // Check if the description is empty
    axios({
      method:'post',
      url: 'http://127.0.0.1:8000/tasks/',
      data: { // Send description and status to the server
        description: this.description,
        status: this.status
      },
      auth: { // Basic authentication
        username: 'admin',
        password: 'password123'
      }
    }).then((response) => {
      let newTask = {'id': response.data.id, 'description': this.description, 'status': parseInt(this.status)}

      this.tasks.push(newTask)

      this.description = '' // Reset description
      this.status = 0 // Reset status
    })
    .catch((error) => {
      console.log(error);
    });
  }
}

Vuengo is starting to look like a really simple task manager now. If you try to add a task it should be appended to the bottom of the tasks list.


Setting a task as done

Open HelloWorld.vue and hange the following line:


<!-- From -->
<a class="card-footer-item">Done</a>
<!-- To -->
<a class="card-footer-item" v-on:click="setStatus(task.id)">Done</a>

When you click 'Done', setStatus will be called and the task's id will be passed as a parameter to the function. Next you can add a new function to the methods:


setStatus(task_id) {
  let description = '';

  for (let i = 0; i < this.tasks.length; i++) {
    if (this.tasks[i].id === task_id) {
      this.tasks[i].status = 1
      description = this.tasks[i].description

      break
    }
  }

  axios({
    method:'put',
    url: 'http://127.0.0.1:8000/tasks/' + task_id + '/',
    headers: {
      'Content-Type': 'application/json'
    },
    data: {
      description: description,
      status: 1
    },
    auth: {
      username: 'admin',
      password: 'password123'
    }
  })
}

If you now click 'Done' on one of the tasks a request will be sent to the server where the status will be updated. The task will also automatically be moved to the other column.


Summary

That was it for this time. You should now have a basic grasp on how to set up a django project, a vue project and how to communicate between them. There are now a ton of things you can do to improve this task manager and here are a few ideas:
-Undo a task
-Structuring code with components
-Delete a task
-Change description
-Better sorting
-More statuses
-Security

I hope you had a great time going through this tutorial and building it for your self. If you've got any questions, feel free to leave a reply.

Share this post

Comments

Lumenified

25. aug 2018 21:15

Thanks for a great tutorial :thumbsup: it taught me a lot about django, vue, vue-route, vuex and axios. I hope you continue your tutorial serie :) Have a great day.

Hilario

26. aug 2018 21:04

Good tutorial. It's just I need.
┬┐But is secure pass in axios call user/password?

Jack

02. sep 2018 10:38

Awesome tutorial! By far the best tutorial on the web for django, django-rest and vue integration!

Stein Ove Helset

04. sep 2018 11:20

@Lumenified
Thank you!

@Hilario
Sounds good!
Yeah, it's safe as long as you use https/SSL. By doing this the traffic will be encrypted.

@Jack
Thank you!
Nice to hear that you liked it!

Dillon

12. sep 2018 16:22

Hi, why did you use a for loop for setStatus? Is that how it's commonly done? I'm new to vue, curious as to why...

tp

06. nov 2018 18:00

This may be due to the version of python (3.7) or django (2.1) that I am using, but I had to explicitly add 'task' to the INSTALLED_APPS section of settings.py before running makemigrate.

Stein Ove Helset

07. nov 2018 11:33

tp: No. It's necessary to add no matter which version you're using. It's just a mistake from me. I have fixed it now :-)

Add comment