Building an Election Portal for Student Elections - Part 1

This post is a walkthrough and code analysis of the Student Election Portal that was used by the IIT Madras BS Students. Part 1

4 mins read • Mon Apr 03 2023

I joined a meet where the Students Affairs Coordinator and the Placement Council of the IIT Madras BS Degree were discussing on conducting the House Leaders Elections (You can read about the Governance | IIT Madras BS Students to know more about the Student Government structure)

They were pouring in their technical requirements for voting, with a Google Form. The requirements are:

  1. The students should be able to see the candidates and read their manifestos.
  2. Each student should vote for 1 Secretary, 1 Deputy Secretary and 1 Web Admin.
  3. The students should vote only for his or her house candidates.

Writing App Script for such requirements seemed to be impossible at that time. Hence I started to create a website with ReactJS and Firebase with existing student database I had in Firebase.

Let's begin with the app

Setting up the React App

// Creating a React app inside the folder iitmbs-elections
npx create-react-app@latest iitmbs-elections

// Move into the directory
cd iitmbs-elections

// Install other dependencies
npm install firebase react-bootstrap

Setting up Firebase

I already had a Firebase project setup with the required data. So I used that project's Firestore. Before, I had to setup Firebase app

import { initializeApp } from "firebase/app";

export const initFirebase = () => {
  initializeApp({
    apiKey: process.env.REACT_APP_API_KEY,
    authDomain: process.env.REACT_APP_AUTH_DOMAIN,
    projectId: process.env.REACT_APP_PROJECT_ID,
    storageBucket: process.env.REACT_APP_STORAGE_BUCKET,
    messagingSenderId: process.env.REACT_APP_MESSAGE_SENDER_ID,
    appId: process.env.REACT_APP_APP_ID,
    measurementId: process.env.REACT_APP_MEASUREMENT_ID,
  });
};

Creating API functions

I created all the Firebase related functions which handles authentication and database fetches in the file src/apis/firebase.js

Student Profile

import { getFirestore, collection, getDocs, getDoc, doc, query, setDoc, orderBy, updateDoc } from "@firebase/firestore";
import { getAuth, signInWithPopup, GoogleAuthProvider, signOut } from "firebase/auth";
import { addToCache, getFromCache } from "./cache";

const db = getFirestore();

export const getProfileDetails = async () => {
  var res = await getFromCache("user");
  if (res!=false) {
    return res
  } else {
    const auth = getAuth();
    const user = auth.currentUser;
    const { displayName: userName, email } = user;
    try {
      const userDoc = await getDoc(doc(db, "users", email));
      const data = userDoc.data();
      addToCache("user", {userName, email, ...data })
      return {userName, email, ...data};
    } catch (err) {
      signOut(auth);
      alert("Sign in using your student login email");
      return {};
    }
  }
};

If you're wondering what are those addToCache and getFromCache, Firestore has limits on the number of reads and writes. Reading from the database everytime the student loads the website will increase the number of reads beyond the limit. Hence these functions first checks whether the data is in cache, if not it fetches from the database. Only after the development I found that the Firestore itself has cache management functions for this (read more about this here)

Authentication

export const signInFirebase = async () => {  
  const auth = getAuth();
  const provider = new GoogleAuthProvider();
  provider.setCustomParameters({ hd: "ds.study.iitm.ac.in" });
  return signInWithPopup(auth, provider)
    .then((result) => {
      const credential = GoogleAuthProvider.credentialFromResult(result);
      const token = credential.accessToken;
      const user = result.user;
      return { status: "success", token, user };
    })
    .catch((error) => {
      const credential = GoogleAuthProvider.credentialFromError(error);
      return { status: "error", error.code, error.message, error.email, credential };
    });
};

This elections are strictly internal to the students. Hence this website should be restricted only to the users with the @ds.study.iitm.ac.in.

This is done by the 4th line of the above code where I set custom parameters to allow only the email IDs which are from the domain ds.study.iitm.ac.in. You can read more about the custom auth parameters here

Fetching the candidates

// Get Secretary candidates of the house
const getSec = async (house) => {
  const q = query(collection(db, `elections/${house}/secretary`), orderBy("name"))
  const secDoc = await getDocs(q);
  let sec = [];
  secDoc.forEach(doc => sec.push(doc.data()))
  return sec;
}

// Get Deputy Secretary of the house
const getDySec = async (house) => {
  const q = query(collection(db, `elections/${house}/deputy-secretary`), orderBy("name"))
  const dySecDoc = await getDocs(q);
  let dySec = [];
  dySecDoc.forEach(doc => dySec.push(doc.data()))
  return dySec;
}

// Get Web Admin candidates of the house
const getWebAd = async (house) => {
  const q = query(collection(db, `elections/${house}/web-admin`), orderBy("name"))
  const webAdDoc = await getDocs(q);
  let webad = [];
  webAdDoc.forEach(doc => webad.push(doc.data()))
  return webad;
}

export const getElectionCandidates = async () => {
  // checks the cache for nomination data
  var res = await getFromCache("nominations");
  if (res!=false) {return res}
  const user = await getProfileDetails();
  const house = user.house.split(" ")[0].toLowerCase();
  const sec = await getSec(house);
  const dysec = await getDySec(house);
  const webad = await getWebAd(house);
  const mentor = await getMentors(house);
  addToCache("nominations", {...user, sec, dysec, webad, mentor})
  return {...user, sec, dysec, webad, mentor};
}

The functions here are straight forward. I have 3 functions: getSec, getDySec, getWebAd which fetches the secretary, deputy secretary and web admin candidates of the house. Did you notice how I manage to get the house name of the student? (If you did, comment below)

Updating Votes

export const updateVote = async (r) => {
  const voteDoc = doc(db, 'elections', 'votes')
  const userDoc = doc(db, 'users', r.email)
  var data = {}
  data[r.email.split("@")[0]] = r
  updateDoc(userDoc, {voted: true}).then(() => {
    updateDoc(voteDoc, data).then(async (r) => {
      var user = await getFromCache("user")
      var nominations = await getFromCache("nominations")
      nominations.voted = true
      user.voted = true;
      addToCache("user", user)
      addToCache("nominations", nominations)
      return true;
    }).catch((e) => {
      updateDoc(userDoc, {voted: false});
      return false
    })
  }).catch(e => false)
}

Every student has to vote exactly 1 time. So I'm adding a voted flag to the user collection. If you can understand the code, you can easily find a way to manipulate the voted flag which is updated in the cache. Well even if you try to change the cache data, you cannot vote again since it checks with the database before storing the votes. I maintain the voted variable in the cache only to restrict the view.

Wrap up

That is all for the Part 1. I have explained everything about the backend and data handling in this part. Stay tuned for Part 2 where I'll explain how I integrated the data with the frontend. I'll take leave now!

Be the first to know.

© 2024 All rights reserved | v3.0