Build a Voting App

in #utopian-io7 years ago (edited)

I am going to help you how to build a Voting App similar to this.
For the front-end we are using Bootstrap and jQuery.

Users and Passwords

I recommend you to do this tutorial first. It was very good and I built the entire voting app above a Login App similar to that one.
I did not make any major change to the back end of the tutorial's login app.
This tutorial suppose you have a way to store and authenticate users similar to that one. I am going to focus on the voting system.

Polls

Model

Schema

Similar to above tutorial, we are using MongoDB and Mongoose for wrapping:
const mongoose = require('mongoose');
Lets make a Poll Schema. It should have a title/question, a list of options and a list of users that have answered it. I also make an unique short ID for every poll, this is optional of course. That short id is stored as an URL (/polls/poll/:SHORTID) that goes to the poll's page. The options array will be in the form

[
    [option1: countOfVotesForOption1],
    [option2: countOfVotesForOption2],
    .
    .
    .
]

So here is the schema:

// Poll schema
var PollSchema = mongoose.Schema({
    title: {
        type: String
    },
    options: Array,
    username: {
        type: String
    },
    path: String,
    users: Array
});

Let's export the model constructor:
var Poll = module.exports = mongoose.model('polls', PollSchema);

Create New Poll

To make the unique short ID I choose a not null random number X of digits, with X less than or equal to the number of polls created. Then, from a not null random number less than or equal to 1e6 I choose first X digits. After that, I check if there is in the data base a poll with that subsequence as path.
Let's create a function that creates a new poll, receiving as argument a model and a callback. For counting polls in the collection we'll use Model.count()

module.exports.createPoll = function(newPoll, callback) {
    let cont;
        Poll.count({}, (err, size) => {
            if (err) throw err;
            do {
                let digits = Math.floor(Math.random()*size+1);
                let short = Math.floor(Math.random()*1000000+1).toString().slice(0,digits);
                let path = '/polls/poll/' + short.toString();
                Poll.count({'path': path}, (err, count) => {
                    if (err) throw err;
                    if (count > 0) cont = true;
                    else {
                        cont = false;
                        newPoll.path = path;
                        newPoll.save(callback);
                    }
                });
            } while(cont);
        });
}

Functions to get polls

This functions are for encapsulate the find operations.

module.exports.getPollsByUsername = function(username, callback) {
Poll.find({'username': username}, callback);
}
module.exports.getAllPolls = function(callback) {
Poll.find({}, callback);
}
module.exports.getPollByPath = function(path, callback) {
Poll.findOne({"path": path}, callback);
}
module.exports.getPollById = function(id, callback) {
Poll.findOne({"_id": id}, callback);
}

Routes

In our main script (i.e, app.js), we will add this variable: let polls = require('./routes/polls'); assuming your route file for polls is routes/polls.js.
To let the app use our route file every time client make a request to /polls, we add this line: app.use('/polls', polls);
At the end of routes/polls.js add module.exports = router;

Validate user is logged in

In tutorial for building a login app they use PassportJS for authentication. This module adds to request a function called .isAuthenticated() to check if a user is logged in. I made a exportable module called ensure that has two callbacks:

  1. One that ensures user is logged in

  2. One that ensures user is no logged in
    The second one is used in model for users for shorter code. For this tutorial we only need the first one. This is the full code of ensure.js file:

      export function ensureAuthenticated(req, res, next) {
         if (req.isAuthenticated()) {
             next();
         } else {
             req.flash('error_msg', 'You are not logged in.');
             res.redirect('/users/login');
         }
     }
     export function ensureNotAuthenticated(req, res, next) {
         if (req.isAuthenticated()) {
             req.flash('error_msg', 'You are already logged in.');
             res.redirect('/');
         } else {
             next();
             }
     }
    

Imports

In our route file (mine is routes/polls.js) for polls we are going to import express, our ensure module and our Polls model constructor (mine is models/polls.js):

import express from 'express';
var router = express.Router();
const ensure = require('../modules/ensure.js');
const Poll = require('../models/polls');

We will use package express-validator. According to the docs, this is the way to import it:

const { check, validationResult } = require('express-validator/check');
const { matchedData, sanitize } = require('express-validator/filter');

Consts

Remember I said that unique short IDs are stored as URLs in the form /polls/poll/:SHORTID? Well, for save code we'll store prefix const prefPath = '/polls/poll/';

View all polls

To view all polls, client will make a GET request to /polls. I suppose you are using a template engine like handlebars. Create a template file index.handlebars (or index.template engine you choose) with this code:

<h2 class="border border-top-0 border-left-0 border-right-0 border-muted page-header">Polls</h2>
<ul id="all-polls-list">
    {{#each polls}}
    <li>
        <a href={{path}} >{{title}}</a>
    </li>
    {{/each}}
</ul>

Now add this code to your route file:

// all polls
router.get('/', (req, res) => {
    Poll.getAllPolls((err, polls) => {
        if (err) throw err;
        res.render('index', {
            'polls': polls
        })
    });
});

Create New Poll

GET request

Remember from tutorial I posted above the app.js file sets up the default layout and view engine with code:

// view engine
app.set('views', path.join(__dirname, 'views'));
app.engine('handlebars', exphbs({
    defaultLayout: "layout",
    layoutsDir: path.join(__dirname, 'views/layouts')
}));
app.set('view engine', 'handlebars');
Template file

Create a template file new.handlebars that will have a form with Poll Information (suppose for now the poll has only two options). This is an example

<h2 class="border border-top-0 border-left-0 border-right-0 border-muted page-header">New Poll</h2>
{{#if errors}}
    {{#each errors}}
        <div class="alert alert-danger">{{msg}}</div>
    {{/each}}
{{/if}}
<form method="post" action="/polls/new">
    <div id="options">
        <div class="form-group">
            <label>Title / Question</label>
            <input type="text" class="form-control" placeholder="i.e, Who is stronger?" name="title">
        </div>
        <div class="form-group">
            <label>Option 1</label>
            <input type="text" class="form-control" placeholder="Saitama" name="option1">
        </div>
        <div class="form-group">
            <label>Option 2</label>
            <input type="text" class="form-control" placeholder="Son Goku" name="option2">
        </div>
    </div>
    <button type="button" class="btn btn-outline-secondary" id="btnAddOption">Add Option</button>
    <button type="submit" class="btn btn-primary">Submit</button>
</form>

The errors variable is for showing errors that occurred on validation.

To add scripts, add this code to your layout.handlebars

{{#if script}}
    {{#each script}}
    <script type="text/javascript" src="{{script}}"></script>
    {{/each}}
    {{/if}}
Script

We will make a script public/js/newOptionScript.js to let user add infinite new options to user's poll:

var optionsCount = 2;
$(document).ready(() => {
    $('#btnAddOption').click(() => {
        optionsCount += 1;
        $('#options').append('<div class="form-group">'+
            '<label>Option '+optionsCount+'</label><input type="text" '+
            'class="form-control" '+
            'name="option'+optionsCount+'"></div>');
    });
});
All set up

We are ready to handle GET request that lets user create new poll:

// new poll
router.get('/new/', (req, res) => {
    res.render('new', {
        script: {
            script1: {script: '/js/newOptionScript.js'} 
        }
    });
});

POST request

To validate every field is not empty we should make a middleware function. For consistency I will use check method from express-validator

// submit new poll
router.post('/new/', [
        // validation
        check('title').custom((value, {req, location, path}) => {
            for(let key in req.body) {
                if (!req.body[key]) {
                    return false;
                }
            }
            return true;
        }).withMessage('No empty fields are allowed')
    ], callback)

Now we will implement the callback function. Next code lines are supposed to be in this function.

Handle errors

The method validationResult from express-validator returns validation errors occurred using check method. If there are validation errors, we should reload page showing wich errors occured.

// handle errors
        const errors = validationResult(req);
        if (!errors.isEmpty()) {
            res.render('new', {
                "errors": errors.array(),
                'script': {
                    script1: {script: '/js/newOptionScript.js'}
                }
            });
        } else {...
Check all options are different

Remember from tutorial I posted above we import connect-flash and express-messages for flash messages and add this middleware:

app.use(function(req, res, next) {
    res.locals.success_msg = req.flash('success_msg');
    res.locals.error_msg = req.flash('error_msg');
    res.locals.error = req.flash('error');
    res.locals.user = req.user || null;
    next();
});

I will add to this middleware before next() line this lines:

    res.locals.warning_msg = req.flash('warning_msg');
    app.set('user', req.user || null);

We will have an aux array where we will store options from the poll and a boolean different that will tell us if all options are different. let aux = [], differents = true;
Let's iterate over options to check that:

for (let option in req.body) {
                if (option === 'title') continue;
                let value = req.body[option];
                if (aux.indexOf(value) !== -1) {
                    differents = false;
                    break;
                }
                aux.push(value);
            }

So, if they are not differents, return an error and reload the page:

if (!differents) {
                req.flash("error_msg", "Options should be different.");
                res.redirect('/polls/new');
            } else {...

If they are different we will create a new Poll:

let newPoll = Poll({
                    'title': req.body.title,
                    'options': aux.map((x) => [x, 0]),
                    'username': req.app.get('user').username,
                    'path': '0',
                    'users': []
                });
                Poll.createPoll(newPoll, (err, poll) => {
                    if (err) {
                        throw err;  
                    } else {
                        console.log('New poll: ' + poll.title);
                        req.flash('success_msg', 'New poll created');
                        res.redirect('/polls/user');
                    }
                });

Poll page

GET Request (view poll)

Template file

To view a Poll client should send a GET request to /polls/poll/:POLL, URL that is equal to a Poll.path.
We will find the poll that has that path and render template file view-poll.handlebars:

<h3 class="border border-top-0 border-left-0 border-right-0 border-muted page-header">{{title}}</h3>
<form method="post" action={{path}}>
    <div class="card my-2" style="width: 18rem;">
      <ul class="list-group list-group-flush" id="poll-options">
        {{#each options}}
        <li class="list-group-item input-group py-1 px-2">
            <input type="radio" name="poll" value='{{0}}' checked>
            <label class="mt-2">{{0}}</label>
            <small class="text-muted">{{1}}</small>
        </li>
        {{/each}}
        <div class="form-control d-none" id="new-option">
            <input type="text" class="form-control" placeholder="New Option">
        </div>
      </ul>
    </div>
    <button id="btnAddOption" class="btn btn-outline-secondary" type="button">Add Option</button>
    <button id="submit-button" class="btn btn-primary" type="submit">Submit</button>
</form>
Script

NOTE: Ignore this if you don't wanna let any user create a new option to any poll

Create the script public/js/submitNewOption.js:

$(document).ready(function() {
    let adding = false;
    $('#btnAddOption').click(function() {
        if (!adding) {
            $('#new-option').removeClass('d-none');
            $('#btnAddOption').html('Cancel');
            $('#submit-button').html('Vote for new option');
            adding = true;
        } else {
            $('#new-option').addClass('d-none');
            $('#btnAddOption').html('Add Option');
            $('#submit-button').html('Submit');
            adding = false;
        }   
    });
    $('#submit-button').click(function() {
        if (adding) {
            $('#poll-options').append('<li class="d-none list-group-item input-group py-1 px-2">'+
                '<input type="radio" name="poll" value="'+
                $('#new-option input').val()+
                '" checked></li>')
        }
    });
});

This script along with the template file basically toggles an input text that lets user type a new option if he is not fine with poll's options.

All set up

We are ready to handle GET request:

// View poll
router.get('/poll/:POLL', (req, res) => {
    Poll.getPollByPath(prefPath + req.params.POLL,
        (err, poll) => {
            if (err) {
                throw err;
            }
            if(poll) {
                res.render('view-poll', {
                    'title': poll.title,
                    'options': poll.options,
                    'path': poll.path,
                    'script': {
                        script1: {script: '/js/submitNewOption.js'}
                    }
                });
            } else {
                req.flash('error_msg', 'No such poll.');
                res.redirect('/polls');
            }
        });
});

POST request (submit answer)

this is the tricky part. My code is not necessary the best way.

Get IP

This is the basic way to get user's IP:

// get ip
            let ipAd = null;
            if (req.headers['x-forwarded-for']) {
                ipAd = req.headers['x-forwarded-for'].split(',')[0];
            }
            else {
                ipAd = req.ip;
            }
Check if user has already answered

We will show an error if

  1. The new option created is empty

  2. User has already answered
    If no of those conditions is true, this expression is true:

     (req.app.get('user') && 
                     poll.users.indexOf(req.app.get('user').username) == -1) ||
                     (!req.app.get('user') &&
                         poll.users.indexOf(ipAd) == -1) &&
                     req.body.poll)
    

So now you know more or less wich if else expression will use.
The following are inside the block where above boolean expression is true.

Add user to list of users that have already answered

So, if user is logged in we will store user's username, otherwise we will store IP address.

let newUser;
                if (req.app.get('user')) newUser = req.app.get('user').username;
                else newUser = ipAd;
let usersUpdated = poll.users;
usersUpdated.push(newUser);
Update Poll

We will use Model.findByIdAndUpdate to update Poll Model.

We pass to Poll.findByIdAndUpdate the poll id, the following object (to increment votes in option chosen):

{ 
                        $set: {
                            options: poll.options.map((arr) => {
                                if (arr[0] == req.body.poll) {
                                    arr[1]++;
                                    newOption = false;
                                }
                                return arr;
                            })
                        }
                    }

Also we pass the object { new: true } to return updated poll and alse a callback function.

In the callback function we will add new option (if there is) to array of options and add new user to array of users:

(err, poll) => {
                        if (err) throw err;
                        let aux = poll.options;
                        if (newOption) {
                            aux.push([req.body.poll, 1]);
                        }
                        Poll.findByIdAndUpdate(poll._id,
                            {
                                $set: {
                                    options: aux,
                                    users: usersUpdated
                                }
                            },
                            { new: true },
                            (err, poll) => {
                                if (err) throw err;
                                req.flash('success_msg', 'Submission Succeded');
                                res.redirect(poll.path);
                            }
                        );
                    }
All set up

Entire code of POST request handler would look like this:

// poll answer submission
router.post('/poll/:POLL', (req, res) => {
    console.log(req.body);
    let newOption = true;
    Poll.getPollByPath(prefPath + req.params.POLL,
        (err, poll) => {
            if (err) throw err;

            // get ip
            let ipAd = null;
            if (req.headers['x-forwarded-for']) {
                ipAd = req.headers['x-forwarded-for'].split(',')[0];
            }
            else {
                ipAd = req.ip;
            }
            if((req.app.get('user') && 
                poll.users.indexOf(req.app.get('user').username) == -1) ||
                (!req.app.get('user') &&
                    poll.users.indexOf(ipAd) == -1) &&
                req.body.poll) {
                let usersUpdated = poll.users;
                let newUser;
                if (req.app.get('user')) newUser = req.app.get('user').username;
                else newUser = ipAd;
                usersUpdated.push(newUser);
                Poll.findByIdAndUpdate(poll._id,
                    { 
                        $set: {
                            options: poll.options.map((arr) => {
                                if (arr[0] == req.body.poll) {
                                    arr[1]++;
                                    newOption = false;
                                }
                                return arr;
                            })
                        }
                    },
                    { new: true },
                    (err, poll) => {
                        if (err) throw err;
                        let aux = poll.options;
                        if (newOption) {
                            aux.push([req.body.poll, 1]);
                        }
                        Poll.findByIdAndUpdate(poll._id,
                            {
                                $set: {
                                    options: aux,
                                    users: usersUpdated
                                }
                            },
                            { new: true },
                            (err, poll) => {
                                if (err) throw err;
                                req.flash('success_msg', 'Submission Succeded');
                                res.redirect(poll.path);
                            }
                        );
                    }
                );
            } else if (!req.body.poll) {
                req.flash('error_msg', 'No empty fields are allowed');
                res.redirect(poll.path);
            } else {
                req.flash('error_msg', 'You can only vote once');
                res.redirect(poll.path);
            }
            
        });
});



Posted on Utopian.io - Rewarding Open Source Contributors

Sort:  

Free vote for you! Yay!

Holy..! Thanks a lot! :)

Thank you for the contribution. It has been approved.

You can contact us on Discord.
[utopian-moderator]

Hey @gustavoaca1997 I am @utopian-io. I have just upvoted you!

Achievements

  • You have less than 500 followers. Just gave you a gift to help you succeed!
  • This is your first accepted contribution here in Utopian. Welcome!

Suggestions

  • Contribute more often to get higher and higher rewards. I wish to see you often!
  • Work on your followers to increase the votes/rewards. I follow what humans do and my vote is mainly based on that. Good luck!

Get Noticed!

  • Did you know project owners can manually vote with their own voting power or by voting power delegated to their projects? Ask the project owner to review your contributions!

Community-Driven Witness!

I am the first and only Steem Community-Driven Witness. Participate on Discord. Lets GROW TOGETHER!

mooncryption-utopian-witness-gif

Up-vote this comment to grow my power and help Open Source contributions like this one. Want to chat? Join me on Discord https://discord.gg/Pc8HG9x

Coin Marketplace

STEEM 0.20
TRX 0.13
JST 0.030
BTC 65702.61
ETH 3485.24
USDT 1.00
SBD 2.51