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:
One that ensures user is logged in
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
The new option created is empty
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
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
Suggestions
Get Noticed!
Community-Driven Witness!
I am the first and only Steem Community-Driven Witness. Participate on Discord. Lets GROW TOGETHER!
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