A voting app (CRUD) using Django Rest Framework and Vue.JS
django tutorial vuejs

Aug. 9, 2020, 12:49 a.m.

A voting app (CRUD) using Django Rest Framework and Vue.JS

  382

What are we building?

In this post, we will be making a very simple voting app with the following functionalities:
1. The admin is able to register new candidates
2. Normal users are able to view and vote for candidates (only once, tracked using the IP address of the request)

If you just want the code then: https://github.com/amartya-dev/vote_drf_vue (Do star the repo in case you find it useful :P)

The Backend with Django Rest Framework

The application architecture

The requirements are simple, we need a candidate table (model/entity or whatever you want to call it) which will contain the details about the candidates, and to track the votes and the IP addresses we would need another Vote table which contains the IP address and the candidate voted for.

We want to be able to get the votes directly with the candidate information for easy access thus, it might be a good idea to include the total number of votes there.

We would need to set up your Django project at this point, so let us quickly create a project and the main app inside it via:

django-admin startproject coda
cd coda/
python manage.py startapp main

As it is pretty clear at this point, our project is called coda, and the app is called main.

Let us code the models for our application according to the above constraints (the following code goes in coda/main/models.py):

class Candidate(models.Model):
    name = models.CharField(max_length=250)
    no_challenges_solved = models.IntegerField()
    votes = models.IntegerField(default=0)
    python_rating = models.IntegerField(default=1)
    dsa_rating = models.IntegerField(default=1)
    cplus_rating = models.IntegerField(default=1)
    java_rating = models.IntegerField(default=1)

    def __str__(self):
        return self.name


class Vote(models.Model):
    ip_address = models.CharField(
        max_length=50,
        default="None",
        unique=True
    )
    candidate = models.ForeignKey(
        to=Candidate,
        on_delete=models.CASCADE,
        related_name='vote'
    )

    def save(self, commit=True, *args, **kwargs):

        if commit:
            try:
                self.candidate.votes += 1
                self.candidate.save()
                super(Vote, self).save(*args, **kwargs)

            except IntegrityError:
                self.candidate.votes -= 1
                self.candidate.save()
                raise IntegrityError

        else:
            raise IntegrityError

    def __str__(self):
        return self.candidate.name

I have overridden the save() method of the Vote model to achieve the following:

  1. As we are maintaining the number of votes for each candidate, as soon as there is a request for a vote we append the number of votes of the associated candidate. The catch here is that in case there is a repeated request the incremented votes' value needs to be decremented again. Thus, we use the except block to do precisely that.
  2. We wrote the conditional to check the commit flag so that we can save an instance of the model without committing the transaction to the database.

The serializers

To be able to write the API and corresponding views we would need the serializers to parse the data to JSON and vice versa. Create a file called serializers.py inside coda/main/, we will be creating two serializers here:
1. The candidate serializer which we are going to use for the CRUD operations for candidates and 2. The Vote serializer which we are going to use to just allow the cast of a vote. Thus, we have overridden the create() method where we are just returning an object of the Vote class without committing the entry into our DB, reason: We would be adding the IP address in views for which we just need the object as sort of a baseline. Also, we are using candidate_name to easily send that data from the frontend and get the corresponding candidate instance. You might want to change that to id in case the uniqueness of candidate names is not guaranteed.

from rest_framework import serializers
from main.models import Candidate, Vote
from django.shortcuts import get_object_or_404
from django.db import IntegrityError


class CandidateSerializer(serializers.ModelSerializer):
    votes = serializers.ReadOnlyField()

    class Meta:
        model = Candidate
        fields = "__all__"


class VoteSerializer(serializers.ModelSerializer):
    candidate_name = serializers.CharField()

    def create(self, validated_data):
        candidate = get_object_or_404(Candidate, name=validated_data["candidate_name"])
        vote = Vote()
        vote.candidate = candidate
        try:
            vote.save(commit=False)
        except IntegrityError:
            return vote
        return vote

    class Meta:
        model = Vote
        exclude = ("id", "ip_address", "candidate")

Views

The time to finally write our logic for all the operations we need from this application, we are using generic viewsets and views provided by Django Rest Framework, we use a ModelViewSet for candidates CRUD operations and very generic APIView for casting the vote like so:

from rest_framework.viewsets import ModelViewSet
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework.permissions import IsAdminUser
from main.serializers import VoteSerializer, CandidateSerializer
from main.models import Candidate
from django.db import IntegrityError


def get_client_ip(request):
    x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
    if x_forwarded_for:
        ip = x_forwarded_for.split(',')[0]
    else:
        ip = request.META.get('REMOTE_ADDR')
    return ip


class CandidateViewSet(ModelViewSet):
    queryset = Candidate.objects.all().order_by('-votes')
    serializer_class = CandidateSerializer
    permission_classes = [IsAdminUser, ]


class CastVoteView(APIView):

    def post(self, request):
        serializer = VoteSerializer(data=request.data)
        if serializer.is_valid(raise_exception=ValueError):
            created_instance = serializer.create(validated_data=request.data)
            created_instance.ip_address = get_client_ip(request)

            try:
                created_instance.save()

            except IntegrityError:
                return Response(
                    {
                        "message": "Already voted"
                    },
                    status=status.HTTP_400_BAD_REQUEST
                )

            return Response(
                {
                    "message": "Vote cast successful"
                },
                status=status.HTTP_200_OK
            )

We use the uncommitted object we get from serializer's create() function and add the IP address from request to it before finally committing the entry to the database.

The URLS

Let us wrap this up by binding our views to URLs, create a file called coda/main/urls.py and add:

from django.urls import include, path
from rest_framework import routers
from main import views as main_views

router = routers.DefaultRouter()
router.register(r'candidate', main_views.CandidateViewSet)

app_name = 'api'
urlpatterns = [
    path('', include(router.urls)),
    path('vote/', main_views.CastVoteView.as_view(), name='vote')
]

Then add these to the main URLs i.e. coda/urls.py :

from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path('api/', include('main.urls', namespace='api')),
    path('admin/', admin.site.urls),
]

Finally, we would need to allow cross origins requests and add this app to the settings: So first install django-cors-headers by:

pip install django-cors-headers

Then modify coda/settings.py:

...
INSTALLED_APPS = [
    'main.apps.MainConfig',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
    'corsheaders'
]
...

CORS_ORIGIN_ALLOW_ALL = True

Time to make, run migrations, and run our server:

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

The vue frontend:

Let us quickly write the frontend for our app, thus:

vue create vote-app

Use the default settings then add the following packages:

yarn add axios router vuetify @mdi/font

The first thing to do is to set up our application to use Vuetify thus, create a folder called plugins in the src directory and create a file called vuetify.js inside it:

import Vue from 'vue'
import Vuetify from 'vuetify'
import 'vuetify/dist/vuetify.min.css'
import '@mdi/font/css/materialdesignicons.css'

Vue.use(Vuetify, {
  iconfont: 'md',
})

export default new Vuetify({})

Now we need to modify our main.js file in order to use Vuetify and Router with our application like so:

import Vue from 'vue'
import App from './App.vue'
import router from "./router";

import BootstrapVue from "bootstrap-vue";
// import VeeValidate from "vee-validate";
import vuetify from '@/plugins/vuetify' // path to vuetify export

Vue.config.productionTip = false

new Vue({
  router,
  vuetify,
  render: h => h(App),
}).$mount('#app')

Let us define the routes in our router, create a file called router.js in your src folder and add the following routes to it:

import Vue from "vue";
import Router from "vue-router";

Vue.use(Router);

export default new Router({
  routes: [
    {
      path: "/",
      redirect: '/index'
    },
    {
      path: "/register",
      name: "register",
      component: () => import("./components/Register.vue")
    },
    {
      path: "/index",
      name: "index",
      component: () => import("./components/Index.vue")
    },
  ]
});

Now that we are all set up it is time to create our components, let us start with index, create a file called Index.vue inside the components folder and add the following code:

<template>
  <v-card class="mx-auto">
    <v-row>
      <v-col v-for="(item, i) in candidates" :key="i" cols="10" style="margin: 2%">
        <v-card :color="white" light>
          <div class="d-flex flex-no-wrap justify-space-between">
            <div>
              <v-card-title class="headline" v-text="item.name"></v-card-title>
              <v-card-subtitle style="color:black">Votes: {{ item.votes }}</v-card-subtitle>
              <v-card-subtitle>
                <v-expansion-panels v-model="panel" :disabled="disabled">
                  <v-expansion-panel>
                    <v-expansion-panel-header>Details</v-expansion-panel-header>
                    <v-expansion-panel-content>
                      <b>Number of Challenges Solved:</b> {{ item.no_challenges_solved }}
                      <br />
                      <b>Python Rating:</b> {{ item.python_rating }}
                      <br />
                      <b>DSA Rating:</b> {{ item.dsa_rating }}
                      <br />
                      <b>Java Rating:</b> {{ item.java_rating }}
                      <br />
                      <b>C++ Rating:</b> {{ item.cplus_rating }}
                      <br />
                    </v-expansion-panel-content>
                  </v-expansion-panel>
                </v-expansion-panels>
              </v-card-subtitle>
              <v-card-actions>
                <v-btn class="btn-success" style="color:white" text v-on:click="vote(item)">Vote</v-btn>
              </v-card-actions>
            </div>
          </div>
        </v-card>
      </v-col>
    </v-row>
  </v-card>
</template>

<script>
import axios from "axios";
export default {
  data() {
    return {
      candidates: [],
    };
  },
  created() {
    console.log("Here");
    this.all();
  },
  methods: {
    vote: function (candidate) {
      if (confirm("Vote " + candidate.name)) {
        axios
          .post(`http://localhost:8000/api/vote/`, {
            candidate_name: candidate.name,
          })
          .then((response) => {
            console.log(response);
            alert("Voted for" + candidate.name)
            this.all()
          })
          .catch(function (error) {
            if (error.response) {
              console.log(error);
              alert("You are only allowed to vote once");
            }
          });
      }
    },
    all: function () {
      console.log("Getting data");
      axios.get("http://localhost:8000/api/candidate/", {
        auth: {
          username: "admin",
          password: "hello@1234"
        }
      }).then((response) => {
        this.candidates = response.data;
        console.log(response);
      });
    },
  },
};
</script>

We used axios to make a request for the available candidates since we have set up the django application to use basic authentication for allowing CRUD on the Candidates, you would need to hard code the admin id and password here. Also, we used a function vote to make a request to vote for a particular candidate after confirmation via an alert window, and if the response is successful create a corresponding alert and vice-versa.

Let us now create the other component called Register.Vue in order to allow for registering new candidates:

<template>
  <v-container>
    <v-form @submit="create" ref="form" lazy-validation>
      <v-text-field v-model="admin_id" :counter="250" label="Admin Id" required></v-text-field>
      <v-text-field v-model="admin_password" label="Admin Password" type="password" required></v-text-field>
      <v-text-field v-model="candidate.name" :counter="250" label="Name" required></v-text-field>
      <v-text-field
        v-model="candidate.no_challenges_solved"
        label="Number of challenges solved"
        type="number"
      ></v-text-field>
      <v-select
        v-model="candidate.python_rating"
        :items="ratings"
        :rules="[v => !!v || 'Python Rating is required']"
        label="Python Rating"
        required
      ></v-select>
      <v-select
        v-model="candidate.java_rating"
        :items="ratings"
        :rules="[v => !!v || 'Java Rating is required']"
        label="Java Rating"
        required
      ></v-select>
      <v-select
        v-model="candidate.dsa_rating"
        :items="ratings"
        :rules="[v => !!v || 'DSA Rating is required']"
        label="DSA Rating"
        required
      ></v-select>
      <v-select
        v-model="candidate.cplus_rating"
        :items="ratings"
        :rules="[v => !!v || 'C++ Rating is required']"
        label="C++ Rating"
        required
      ></v-select>
      <v-btn color="primary" type="submit">Submit</v-btn>
    </v-form>
  </v-container>
</template>

<script>
import axios from "axios";
export default {
  data() {
    return {
      ratings: [1, 2, 3, 4, 5],
      num: 1,
      candidate: {
        name: "",
        no_challenges_solved: 0,
        java_rating: 1,
        cplus_rating: 1,
        dsa_rating: 1,
        python_rating: 1,
      },
      admin_id: "",
      admin_password: "",
      submitted: false,
    };
  },
  methods: {
    create: function () {
      axios
        .post("http://127.0.0.1:8000/api/candidate/", this.candidate, {
          auth: {
            username: this.admin_id,
            password: this.admin_password,
          },
        })
        .then((response) => {
          console.log(response);
          alert("Registered Succesfuly");
          this.$router.push("/");
        })
        .catch((error) => {
          console.log(error);
        });
    },
  },
};
</script>

Last but not the least we would need to create the navigation drawer in the App.Vue file in order to create the navigation and link it with our router, thus, the router will exist with the navigation drawer of Vuetify:

<template>
  <v-app id="inspire">
    <v-navigation-drawer v-model="drawer" app>
      <v-list dense>
        <v-list-item link>
          <v-list-item-action>
            <v-icon>mdi-home</v-icon>
          </v-list-item-action>
          <v-list-item-content>
            <v-list-item-title>
              <router-link to="/index">Candidates</router-link>
            </v-list-item-title>
          </v-list-item-content>
        </v-list-item>
        <v-list-item link>
          <v-list-item-action>
            <v-icon>mdi-account-plus</v-icon>
          </v-list-item-action>
          <v-list-item-content>
            <v-list-item-title>
              <router-link to="/register">Register New Candidate<br> (Only Admins)</router-link>
            </v-list-item-title>
          </v-list-item-content>
        </v-list-item>
      </v-list>
    </v-navigation-drawer>

    <v-app-bar app color="indigo" dark>
      <v-app-bar-nav-icon @click.stop="drawer = !drawer"></v-app-bar-nav-icon>
      <v-toolbar-title>Application</v-toolbar-title>
    </v-app-bar>
    <v-main>
      <router-view />
    </v-main>
    <v-footer color="indigo" app>
      <span class="white--text">&copy; {{ new Date().getFullYear() }}</span>
    </v-footer>
  </v-app>
</template>
<script>
  export default {
    props: {
      source: String,
    },
    data: () => ({
      drawer: null,
    }),
  }
</script>

AND DONE...

Alt Text

You should be able to run the app via:

yarn serve

Enough talk just show me how it looks :P, sure here is how it looks:

Screenshots

Index

index

Detail View

details

Registering candidates

register

Voting

vote

Vote twice error (based on IP)

error

Share this article:

Comments

  • DeeynStei
    Sept. 15, 2022, 6:41 a.m.

    free gay chat phone numbers gay boy teen boys webcam chat rooms <a href="https://free-gay-sex-chat.com/">gay boy chat </a>

  • GennieStei
    Sept. 16, 2022, 2:32 p.m.

    one on one gay sex chat on camera for masterbation <a href=https://chatcongays.com>easy gay chat</a> gay chat room in nj

  • MarrisStei
    Sept. 20, 2022, 12:21 p.m.

    essay writer reviews <a href=https://au-bestessays.org>essay homework help</a> cheap custom essay writing

  • DorolisaStei
    Sept. 21, 2022, 7:33 a.m.

    best writing paper <a href=https://bestcampusessays.com>help writing a compare and contrast essay</a> best essay writing websites

  • MartyStei
    Sept. 22, 2022, 5:54 a.m.

    cheap essay writer <a href=https://besteasyessays.org>in an essay help you guide</a> write my essay 4 me

  • MerolaStei
    Sept. 23, 2022, 1:53 a.m.

    buy essay <a href=https://bestessayreviews.net>top 10 essay writing services</a> need someone to write my essay

  • AshlenStei
    Sept. 23, 2022, 9:39 p.m.

    buy college essays online <a href=https://bestessaysden.com>custom essay paper</a> essay writers toronto

  • CharitaStei
    Sept. 25, 2022, 11:14 a.m.

    need someone to write my essay <a href=https://bestsessays.org>best college application essay service</a> college admission essay service

  • NaniceStei
    Sept. 26, 2022, 6:13 a.m.

    reviews for essay writing services <a href=https://buyacademicessay.com>how to be a better essay writer</a> cheap essay writer

  • ChelsaeStei
    Sept. 27, 2022, 1:53 a.m.

    essay writing services us <a href=https://buy-eessay-online.com>english literature essay help</a> essay on social service

  • PennyStei
    Sept. 27, 2022, 9:03 p.m.

    order custom essays online <a href=https://buytopessays.com>buy cheap essays</a> buy essays cheap

  • TammieStei
    Sept. 28, 2022, 4:51 p.m.

    college application essay writing service <a href=https://cheapessaywritingservice1.com>order custom essay</a> law essay writing service

  • RhiamonStei
    Sept. 30, 2022, 8:26 a.m.

    english literature essay help <a href=https://customessays-writing.org>essay help writing</a> i need help with writing an essay

  • AntonieStei
    Sept. 29, 2022, 12:48 p.m.

    cheap custom essay writing service <a href=https://customcollegeessays.net>college essay community service</a> college essay writing company

  • CharoStei
    Oct. 1, 2022, 3:18 a.m.

    best college essay writing services <a href=https://customessaywwriting.com>online essay help chat</a> help writing a descriptive essay

  • DronaStei
    Oct. 1, 2022, 9:57 p.m.

    recommended essay writing service <a href=https://customs-essays-writing.org>best essay cheap</a> essay paper writing services

  • TwylaStei
    Oct. 2, 2022, 5:26 p.m.

    essays on community service <a href=https://firstessayservice.net>custom writing essays services</a> write my essay help

  • LeilahStei
    Oct. 3, 2022, 2:12 p.m.

    what are the best essay writing services <a href=https://geniusessaywriters.net>best essay writing website</a> help me with my essay

  • CthrineStei
    Oct. 4, 2022, 12:05 p.m.

    write my social work essay <a href=https://howtobuyanessay.com>good essay writing services</a> help write an essay online

  • GinnieStei
    Oct. 7, 2022, 12:11 p.m.

    help writing an essay <a href=https://lawessayhelpinlondon.com>professional college essay writers</a> cheap essays online

  • GinnieStei
    Oct. 9, 2022, 11:59 p.m.

    best cheap essay writing service <a href=https://lawessayhelpinlondon.com>best essay writing service</a> are essay writing services legal

  • EasterStei
    Oct. 13, 2022, 9:20 p.m.

    admissions essay help <a href=https://writemyessaycheap24h.com>custom essays writing</a> buy college essays

  • DorolisaStei
    Nov. 19, 2022, 7:22 p.m.

    custom essay meister review <a href=https://bestcampusessays.com>can someone write my essay</a> who can i pay to write my essay

  • VivieneStei
    Oct. 12, 2022, 6:18 a.m.

    the best essay writing services <a href=https://ukessayservice.net>sat essay writing help</a> custom essay company

  • ChelsaeStei
    Nov. 27, 2022, 9:19 p.m.

    essay writer website <a href=https://buy-eessay-online.com>essay writing services us</a> essay writing service reviews

  • MarrisStei
    Nov. 16, 2022, 4:43 p.m.

    persuasive essay writer <a href=https://au-bestessays.org>essay writer program</a> essays about service

  • PennyStei
    Nov. 29, 2022, 6:30 a.m.

    lord of the flies essay help <a href=https://buytopessays.com>write my essay for money</a> custom essay company

  • MartyStei
    Nov. 21, 2022, 2:13 a.m.

    i need help writing a descriptive essay <a href=https://besteasyessays.org>i need help with my college essay</a> custom college essay

  • MerolaStei
    Nov. 22, 2022, 10 a.m.

    essay cheap <a href=https://bestessayreviews.net>order custom essay</a> buy essays cheap

  • AshlenStei
    Nov. 23, 2022, 6:56 p.m.

    cheap essay online <a href=https://bestessaysden.com>best essay help</a> essay writing services

  • CharitaStei
    Nov. 25, 2022, 2:36 a.m.

    buying essays online <a href=https://bestsessays.org>essays writing service</a> college essay proofreading service

  • NaniceStei
    Nov. 26, 2022, 11:57 a.m.

    college application essay help <a href=https://buyacademicessay.com>someone write my essay for me</a> buy essay writing online

  • InessaStei
    Jan. 20, 2023, 11:12 p.m.

    coursework support <a href=https://brainycoursework.com>data analysis coursework</a> coursework papers

  • PrughPed
    April 7, 2023, 8:34 a.m.

    https://rus-lit.com/

  • Neheala
    April 9, 2023, 5:30 p.m.

    https://donvard.ru/

  • LieriJah
    April 10, 2023, 7:21 p.m.

    https://ukrtvory.ru/

  • AlonzoBonse
    April 22, 2023, 5:19 a.m.

    <a href="https://virtual-local-numbers.com/countries/1245-uae-toll-free-numbers.html">buy uae phone number - continent telecom</a>

  • Gehits
    April 24, 2023, 4:30 p.m.

    https://school-essay.ru/

  • Haroldges
    April 27, 2023, 12:37 a.m.

    <a href="http://ukrgo.com/view_subsection.php?id_subsection=107">продам ноутбук</a>

  • CesarCrulp
    April 29, 2023, 5:09 a.m.

    <a href=https://n1casino-top.com/>n1 casino New Zealand</a>

  • Davidkem
    May 1, 2023, 8:52 p.m.

    <a href="https://car-rental-barcelona.com/">What is the most popular rental car in Barcelona?</a>

  • QuentinJaigo
    May 5, 2023, 4:38 a.m.

    <a href="https://rent-a-car-istanbul.com/">Аренда автомобилей в Стамбуле Booking.com</a>

  • avenue17
    May 5, 2023, 11:24 p.m.

    It is remarkable, it is the valuable answer

  • Shaneirody
    May 11, 2023, 5:02 p.m.

    <a href="https://kukhareva.com">Екатерина</a>

  • MichaelCom
    May 21, 2023, 6:01 p.m.

    <a href=http://резцедержатель.рф/>Резцедержатели с доставкой по России</a>. Держатели D Держатели H Держатели T отрезных пластин Устройство резьбонарезное int. "FI" Устройство резьбонарезное ext. "FE" Держатели расточные J Втулки с КМ

  • win daddy
    May 23, 2023, 5:03 a.m.

    And it has analogue?

Leave a comment

Related articles

Converting any HTML template into a Django template

Converting any HTML template into a Django template

Djangify A Python script developed by Ohuru that converts HTML Files / Templates to Django compatible HTML Templates. This post …

Read Story
Flutter signup/login application with Django backend #1

Flutter signup/login application with Django backend #1

Introduction This series of posts intends to develop a flutter signup/login app working with API calls to Django backend. The …

Read Story
Flutter signup/login application with Django backend #2

Flutter signup/login application with Django backend #2

Next steps This post is in continuation of the other post that written on the same topic. If you have …

Read Story

Stay right up to date

Get great content to your inbox every week. No spam.
Only great content, we don’t share your email with third parties.