2.17.2012

Nomination part 9 - routes and ajax


Version en español


This post is part of the series about creating an app with node.js, express for Facebook


Part 4
Part 5
Part 6
Part 7
Part 8
Part 9
Part 10

Ready the ui lets put this to work, first lets create our routes to handle all the possible request from our front-end, we have to handle the nominations, creation, erase, add, end, cancel and... i think its all, all this will be in "dashboard.js":

/**
 * Rutas dashboard
*/
var fb = require('facebook-js'),
    url = 'http://nomination.cloudno.de/';

function findIndexByKeyValue(obj, key, value)
{
    var l = obj.length;
    for (var i = 0; i < l; i++) {
        if (obj[i][key] == value) {
            return i;
  }
 }
 return -1;
}

module.exports = function(app, log){
    var nominator = require('../controllers/nominator.js');

    app.error(function(err, req, res, next){
        log.error('Error: ' + err);
        if (err.message === 'nli'){
            res.redirect('?error='+err.message);
        }else{
            res.send(err.message);
        }
    });

    function checkUser(req, res, next){
        if (req.session.user){
            next();
        }else{
            log.notice('someone try to go directly to dashboard on: ' +
                new Date() );
            next(new Error('nli'));
        }
    }
    /**
     * Dashboard landing
    */
    app.get('/dashboard', checkUser, function(req, res){
        log.notice('landed on dashboard user: ' +
            req.session.user.id + ' on: ' + new Date() );
        var invited = req.param('invited');
        res.render('dashboard', { user: req.session.user, error : req.param('error'), type: 'dashboard', invited: invited });
    });

    /**
     * lista de amigos de facebook
     */
    app.get('/friends', checkUser, function(req, res){
        fb.apiCall('GET', '/me/friends', {access_token: req.session.user.access_token}, function(error, response, body){
            if (error){
                log.debug('error getting friends list:' + error);
                throw new Error('Error getting friends list');
            }
            log.notice('getting friends from user:' + req.session.user.id);
            res.json(body);
        });
    });

    /**
     * lista de mis nominaciones
     */
    app.get('/nominations/mine', checkUser, function(req, res){
        nominator.findMyNominations(req.session.user.id,function(err, data){
            if (err) { log.debug('error getting nominations:' + err); res.json(null); return; }
            res.json(data);
        });
    });

    /**
     * lista de nominaciones donde vote
     */
    app.get('/nominations/voted', checkUser, function(req, res){
        nominator.findVoted(req.session.user.id,function(err, data){
            if (err) { log.debug('error getting nominations:' + err); res.json(null); return; }
            res.json(data);
        });
    });

    /**
     * lista de nominaciones donde estoy nominado
     */
    app.get('/nominations/appear', checkUser, function(req, res){
        nominator.findNominated(req.session.user.id,function(err, data){
            if (err) { log.debug('error getting nominations:' + err); res.json(null); return; }
            res.json(data);
        });
    });

    /**
     * regresar nominacion
     */
    app.get('/nominations/:id', checkUser, function(req, res){
        nominator.findNomination(req.params.id,function(err, data){
            if (err) { log.debug('error getting nominations:' + err); res.json(null); return; }
            res.json(data);
        });
    });

    /**
     * Crear nominacion
     */
    app.post('/nominations/create', checkUser, function(req, res){
        var nomination = {
            'name' : req.param('name'),
            'owner' : req.session.user.id,
            'endDate' : new Date(req.param('datep')),
            'category' : "cat1",
            'sub_cat' : "sub1",
            'active' : true
        };
        nominator.createNomination(nomination, function(err, doc){
            if (err) { log.debug(err); return; }
            log.notice('nomination '+ req.param('name') +' created by: ' + req.session.user.id );
            res.json(doc);
        });
    });

    /**
     * Borrar nominacion
     */
    app.post('/nominations/erase', checkUser, function(req, res){
        //TODO: erase nomination
        var id = req.param('id');
        nominator.eraseNomination(id, function(err){
            if (err) { log.debug(err); res.json(null); return; }
            log.notice('nomination '+ req.param('name') +' erased by: ' + req.session.user.id );
            res.json(true);
        });
    });

    /**
     * Votar en nominacion
     */
    app.post('/nominations/vote', checkUser, function(req, res){
        //TODO: vote nomination, write in own wall, try to write in other user wall
        var voterid = req.session.user.id;
        var id = req.param('id');
        var userid = req.param('userid');
        //add a friend, try to write to the user wall and local
        nominator.findNomination(id,function(err, doc){
            if (err) { log.debug('error getting nominations:' + err); res.json(null); return; }
            nominator.vote(doc, voterid, userid, function(err, nom){
                if (err) { log.debug('error getting nominations:' + err); res.json(null); return; }
                var index = findIndexByKeyValue(nom.users, "_id", req.param('userid'));
                res.json(nom.users[index].votes);
                //post to my wall that i voted
                fb.apiCall(
                    'POST',
                    '/'+voterid+'/feed',
                    {
                        access_token: req.session.user.access_token,
                        message: 'Vote por "' + nom.users[index].name + '" en "' +
                            nom.name + '" en nomi-nation, vota tu tambien',
                        name: "Votar",
                        link: url + '?invited=' + req.param('id')
                    },
                    function (error, response, body) {
                        if (error) { log.debug('error posting on my wall'); return; }
                        log.notice('posted on current user wall: ' + voterid);
                    }
                );
                //post to the victim
                fb.apiCall(
                    'POST',
                    '/'+nom.users[index]._id+'/feed',
                    {
                        access_token: req.session.user.access_token,
                        message: 'Vote por ti en "' + nom.name + '" en nomi-nation ' +
                            'vota tu tambien',
                        name: "Votar",
                        link: url + '?invited=' + req.param('id')
                    },
                    function (error, response, body) {
                        if (error) { log.debug('error posting on voted user'); return; }
                        log.notice('posted on the voted user wall: ' + nom.users[index]._id);
                    }
                );
                log.notice('nomination '+ req.param('id') +' voted by: ' + req.session.user.id );
            });
        });
    });

    /**
     * Agregar usuario a nominacion
     */
    app.post('/nominations/adduser', checkUser, function(req, res){
        var users = req.param('users');
        var id = req.param('id');
        //add a friend, try to write to the user wall and local
        nominator.findNomination(id,function(err, doc){
            if (err) { log.debug('error getting nominations:' + err); res.json(null); return; }
            nominator.addUser(doc, users, function(err){
                if (err) { log.debug('error adding users'); res.json(null); return; }
                res.json(true);
                var usersl = req.param('users');
                var userl = 0;
                if (usersl instanceof Array){
                    userl = usersl.length;
                    for (var i=0;i<userl;i++){
                        fb.apiCall(
                            'POST',
                            '/'+usersl[i]._id+'/feed',
                            {
                                access_token: req.session.user.access_token,
                                message: 'Te agregue a "' + doc.name + '" en nomi-nation ' +
                                    'agrega a tus amigos tambien',
                                name: "Votar",
                                link: url + '?invited=' + req.param('id')
                            },
                            function (error, response, body) {
                                if (error) { log.debug('error posting on voted user'); return; }
                                log.notice('posted on the added user wall: ' + usersl[i]._id);
                            }
                        );
                    }
                }else{
                    fb.apiCall(
                        'POST',
                        '/'+usersl._id+'/feed',
                        {
                            access_token: req.session.user.access_token,
                            message: 'Te agregue a "' + doc.name + '" en nomi-nation ' +
                                'agrega a tus amigos tambien',
                            name: "Votar",
                            link: url + '?invited=' + req.param('id')
                        },
                        function (error, response, body) {
                            if (error) { log.debug('error posting on voted user'); return; }
                            log.notice('posted on the added user wall: ' + usersl._id);
                        }
                    );
                }
            });
        });
    });

    /**
     * Borrar usuario de nominacion
     */
    app.post('/nominations/eraseuser', checkUser, function(req, res){
        //just erased, dont notice anyone :(
        var user = req.param('user');
        var id = req.param('id');
        //add a friend, try to write to the user wall and local
        nominator.findNomination(id,function(err, doc){
            if (err) { log.debug('error getting nominations'); res.json(null); return; }
            if (user === 'eraseme'){
                var index = findIndexByKeyValue(doc.users, "_id", req.session.user.id);
                user = doc.users[index];
            }
            nominator.eraseUser(doc, user, function(err){
                if (err) { log.debug('error erasing nominations'); res.json(null); return; }
                res.json(true);
            });
        });
    });

    /**
     * Terminar nominacion
     */
    app.post('/nominations/end', checkUser, function(req, res){
        //TODO: end the nomination and declare a winner
        var id = req.param('id');
        nominator.findNomination(id,function(err, doc){
            if (err) { log.debug('error getting nominations:' + err); res.json(null); return; }
            var users = doc.users;
            var usersl = doc.users.length;
            var voters = doc.voters;
            var votersl = doc.voters.length;
            var winner = users[0];
            for (var j=1; j<usersl;j++){
                if (winner.votes < users[j].votes){
                    winner = users[j];
                }
            }
            res.json(winner);
            fb.apiCall(
                'POST',
                '/'+req.session.user.id+'/feed',
                {
                    access_token: req.session.user.access_token,
                    message: winner.name + ' gano "' + doc.name + '" en nomi-nation ' +
                        'crea tu propia nominacion',
                    name: "Crear",
                    link: url + '?invited=' + req.param('id')
                },
                function (error, response, body) {
                    if (error) { log.debug('error posting on voted user'); return; }
                    log.notice('posted on the created user wall: ' + req.session.user.id);
                }
            );
            for (var i=0;i<usersl;i++){
                if (users[i]._id == req.session.user.id){ continue; }
                fb.apiCall(
                    'POST',
                    '/'+users[i]._id+'/feed',
                    {
                        access_token: req.session.user.access_token,
                        message: winner.name + ' gano "' + doc.name + '" en nomi-nation ' +
                            'crea tu propia nominacion',
                        name: "Crear",
                        link: url
                    },
                    function (error, response, body) {
                        if (error) { log.debug('error posting on voted user'); return; }
                        log.notice('posted on the user wall: ' + users[i]._id);
                    }
                );
            }
            nominator.eraseNomination(id, function(err){
                if (err) { log.debug('error erasing nomination'); return; }
                log.notice('nomination '+ req.param('name') +' erased by: ' + req.session.user.id );
            });
            for (var i=0;i<votersl;i++){
                if (voters[i]._id == req.session.user.id){ continue; }
                fb.apiCall(
                    'POST',
                    '/'+voters[i]._id+'/feed',
                    {
                        access_token: req.session.user.access_token,
                        message: winner.name + ' gano "' + doc.name + '" en nomi-nation ' +
                            'crea tu propia nominacion',
                        name: "Crear",
                        link: url
                    },
                    function (error, response, body) {
                        if (error) { log.debug('error posting on voted user'); return; }
                        log.notice('posted on the user wall: ' + voters[i]._id);
                    }
                );
            }
            nominator.eraseNomination(id, function(err){
                if (err) { log.debug('error erasing nomination'); return; }
                log.notice('nomination '+ req.param('name') +' erased by: ' + req.session.user.id );
            });
        });
    });

};


First lets see our middleware and a little helper function

Line 7-16: "findIndexByKeyValue" this function will help us to find the index of creation object in the array, since we use this a lot we put in his own function


21-28: "app.error" will help us to catch any error the app send and show it to the user

30-38: small middleware to check if the user its logged in, if he is we just call "next()" if not we send the user to the index page with the error


Note on middleware: check that the middleware gets behind of the function that handles the route, you can put any number of middleware divided by ",", for example lets imagine we have "checkotherstuff" too, then the route will be: "checkuser, checkstuff, function..."

Now the routes
42-61: The dashboard its simple, we use our middleware and if everything is good we render the "dashboard.jade", "get friends", here we use the Facebook api to bring the list of friends from the user logged in, if we don't have errors we return the json with the friends array.

66-91: We get the type of list the client is asking, we call the controller and we return the json with the list of nominations array, same thing with all the other functions... another way to do it could be add a ":type" to the route and we end with something like: "/nominations/:type" and with code decide what to call to the controller and send the corresponding json, but for this example we leave like this

138-185: vote for an user, we take the parameters send in the post, the voting user which its in session, nomination id and the user to vote for, we search the nomination, if everything its ok we vote for it, if everything its ok we return the number of votes this user have, we post in the user wall who voted and in the voted wall.


Remaining routes are similar, any doubt ask in the comments


Now lets see how to call all this from our client, all this goes in "script.js"

/* Author: mrpix
*/
var options = {
    ns: { namespaces: ['translation'], defaultNs: 'translation'},
    useLocalStorage: true,
    resGetPath: 'locales/resources.json?lng=__lng__&ns=__ns__',
    dynamicLoad: true
};
$.i18n.init(options, function() {
    //TODO: add more text
});
// expresion para buscar texto que contenga cierto termino
jQuery.expr[':'].Contains = function(a,i,m){
    return (a.textContent || a.innerText || "").toUpperCase().indexOf(m[3].toUpperCase())>=0;
};
// expresion para buscar texto que sea igual a otro
jQuery.extend(jQuery.expr[':'], {
     containsExactly: "$(a).text() == m[3]"
});
//mostrar mensajes modales
function showMsg(title, msg, extra){
    var dialog = $( "#dialog-modal" );
    dialog.attr('title', $.t(title));
    if (extra){
        dialog.find('#msg').text($.t(msg, {winner: extra}));
    }else{
        dialog.find('#msg').text($.t(msg));
    }
    dialog.dialog('open');
}
$(function() {
    //instanciar dialogo
    $( "#dialog-modal" ).dialog({
        autoOpen: false,
        modal: true,
        buttons: {
            Ok: function() {
                $( this ).dialog( "close" );
            }
        }
    });
    //guardamos la lista para futuras referencias
    var list = $('#selectable');
    //funcion para votar por un usuario
    var vote = function(ev){
        ev.preventDefault();
        var a = $(this);
        var tr = a.parent().parent();
        var id = tr.attr('id');
        var nid = $('.details').attr('nid');
        var name = $('.details').find('legend').text();
        $.post("/nominations/vote", { id: nid, userid: id },
            function(data) {
                if (data){
                    var votes = tr.find('.votes');
                    votes.html(data);
                }else{
                    showMsg('dashboard.error', 'dashboard.error_voting');
                }
            }
        ).error(function() {showMsg('dashboard.error', 'dashboard.error_voting'); });
        var list = $('#voted');
        var found = list.find('li:containsExactly('+name+')');
        if (found.length < 1){
            var li = $('<li id="'+nid+'" type="voted"><input type="checkbox" /><label>'+name+'</label></li>');
            list.append(li);
            li.find("label").click(checkOne);
            li.find("input").click(checkOne);
        }
    };
    //funcion para borrar usuario
    var erase = function(ev){
        ev.preventDefault();
        var a = $(this);
        var tr = a.parent().parent();
        var id = tr.attr('id');
        var name = tr.find('.name').text();
        var votes = tr.find('.votes').text();
        var nid = $('.details').attr('nid');
        var user = {
            _id : id,
            name : name,
            votes : votes
        };
        $.post("/nominations/eraseuser", { id: nid, user: user },
            function(data) {
                if (data){
                   tr.remove();
                }else{
                    showMsg('dashboard.error', 'dashboard.error_erasing_user');
                }
            }
        ).error(function() { showMsg('dashboard.error', 'dashboard.error_erasing_user'); });
    };
    //cargando amigos del usuario en session
    function loadUsers(next){
        $.getJSON(next || '/friends', function(data) {
            if (data.data.length > 0){
                $.each(data.data, function(key, value){
                    list.append('<li class="ui-state-default" id="'+value.id+'"><img width="30px" src="https://graph.facebook.com/'+value.id+'/picture"/><a>'+value.name+'</a></li>');
                });
                loadUsers(data.paging.next);
            }else{
                return;
            }
        }).error(function() { showMsg('dashboard.error', 'dashboard.error_friends'); });
    }
    //cargando nominaciones del usuario en session, depende del tipo
    function loadNominations(type){
        $.getJSON('/nominations/'+type, function(data) {
            var list = $('#'+type);
            if (data.length < 1 ){
                //showMsg('dashboard.warning', 'dashboard.warning_zarro');
            }else{
                $.each(data, function(key, value){
                    list.append('<li id="'+value._id+'" type="'+type+'"><input type="checkbox" /><label>'+value.name+'</label></li>');
                });
            }
            list.find("label").click(checkOne);
            list.find("input").click(checkOne);
        }).error(function() { showMsg('dashboard.error', 'dashboard.warning_nominations'); });
    }
    //para q solo se puede seleccionar una nominacion a la vez
    function checkOne(){
        var currentEl = $(this);
        $("input:checked").each(function(){
            $(this).attr('checked', false);
        });
        if (currentEl.is('input')){
            currentEl.attr('checked', true);
            showNomination(currentEl.parent().attr('id'), currentEl.parent().attr('type'));
            return;
        }
        var checkbox = currentEl.siblings('input');
        checkbox.attr('checked', !checkbox.attr('checked'));
        showNomination(currentEl.parent().attr('id'), currentEl.parent().attr('type'));
    }
    //cargar la nominacion y llenar details
    function showNomination(id, type, refresh){
        //TODO: show avatar
        $.getJSON('/nominations/'+id, function(data) {
            if (!data){
                //alert('Du! nominacion ya no existe o termino :(');
                showMsg('dashboard.warning', 'dashboard.warning_erased');
            }
            var details = $('.details');
            details.attr('nid', data._id);
            details.find('legend').html(data.name);
            var daten = new Date(''+data.endDate);
            details.find('.date').html($.t('dashboard.end_date')+' '+ daten.getDate()+'/'+(daten.getMonth()+1)+'/'+daten.getUTCFullYear());
            details.find('.refresh').attr('nid', data._id);
            details.find('.refresh').attr('type', type);
            var ntype = type;
            //console.log(data._id);
            if (ntype === 'appear'){
                $('#end').hide();
                $('#cancel').hide();
                $('#remove').show();
            }else if (type === 'mine'){
                $('#end').show();
                $('#cancel').show();
                $('#remove').hide();
            }else{
                $('#end').hide();
                $('#cancel').hide();
                $('#remove').hide();
            }
            var tbody = details.find('.userst').find('tbody');
            tbody.html('');
            var userl = data.users.length;
            for (var i=0; i<userl;i++){
                var id = data.users[i]._id;
                var name = data.users[i].name;
                var votes = data.users[i].votes;
                var tr = $('<tr id="'+id+'"></tr>');
                tr.append('<td class="pic"><img width="20px" src="https://graph.facebook.com/'+id+'/picture"/></td>');
                tr.append('<td class="name">'+name+'</td>');
                tr.append('<td class="votes">'+votes+'</td>');
                var avote = $('<a class=".vote" href="#">'+$.t('dashboard.vote')+'</a>');
                avote.click(vote);
                var aerase = $('<a class=".erase" href="#">'+$.t('dashboard.erase')+'</a>');
                aerase.click(erase);
                var menu = $('<td></td>');
                menu.append(avote);
                menu.append(' / ');
                menu.append(aerase);
                menu.appendTo(tr);
                tbody.append(tr);
            }
            if (!refresh){
                details.show('slide');
            }
        }).error(function() { showMsg('dashboard.error', 'dashboard.error_showing'); });
    }
    //actualizar la nominacion en contexto
    $('.refresh').click(function(ev){
        ev.preventDefault();
        showNomination($(this).attr('nid'), $(this).attr('type'), 'refresh');
    });
    //cargar las nominaciones y usuarios
    loadNominations('mine');
    loadNominations('appear');
    loadNominations('voted');
    loadUsers(null);
    //viene referenciado, mostrar esa nominacion
    var invited = $('.invited');
    if (invited[0]){
        showNomination(invited.attr('invited'), 'voted');
    }
    //para filtrar la lista de amigos
    $("#filterinput").change( function () {
        var filter = $(this).val();
        if(filter) {
            // this finds all links in a list that contain the input,
            // and hide the ones not containing the input while showing the ones that do
            list.find("a:not(:Contains(" + filter + "))").parent().hide();
            list.find("a:Contains(" + filter + ")").parent().show();
        } else {
            list.find("li").show();
        }
        return false;
    }).keyup( function () {
        // fire the above change event after every letter
        $(this).change();
    });
    //inicializar el seleccionador de fecha
    $( "#datep" ).datepicker();
    //hacer seleccionable la lista de nominaciones
    $( "#selectable" ).selectable();
    //abrir el dialog de nueva nominacion
    $("#nn").click(function(){
        $( "#dialog-new" ).dialog( "open" );
    });
    //abrir el dialogo de agregar usuarios
    $("#am").click(function(){
        $( "#dialog-add" ).dialog( "open" );
    });
    //creamos el dialogo de nueva nominacion
    $( "#dialog-new" ).dialog({
        autoOpen: false,
        height: 300,
  width: 450,
  modal: true,
  buttons: {
            //TODO: put a t string :S
   "Create a nomination": function() {
                var dialog = $(this);
                var name = dialog.find('#name').val();
                var datep = dialog.find('#datep').val();
                $.post("/nominations/create", { name: name, datep: datep },
                    function(data) {
                        var list = $('#mine');
                        var li = $('<li id="'+data._id+'" type="mine"><input type="checkbox" checked="true" /><label>'+data.name+'</label></li>');
                        list.append(li);
                        li.find("label").click(checkOne);
                        li.find("input").click(checkOne);
                        li.find("label").trigger('click');
                        dialog.dialog( "close" );
                    }
                ).error(function() { showMsg('dashboard.error', 'dashboard.warning_creating'); });
   },
   Cancel: function() {
    $( this ).dialog( "close" );
   }
  },
  close: function() {
      var dialog = $(this);
            dialog.find('#name').val('');
            dialog.find('#datep').val('');
  }
 });
    //creamos el dialog de agregar usuarios
    $( "#dialog-add" ).dialog({
        autoOpen: false,
  height: 300,
  width: 450,
  modal: true,
  buttons: {
   "Add friend(s)": function() {
                var dialog = $(this);
                var tbody = $('.details').find('.userst').find('tbody');
                var users = [];
                var userp;
                $('#selectable').find('.ui-selected').each(function(key, value){
                    users.push({
                        "_id" : $(value).attr('id'),
                        "name" : $(value).text(),
                        "votes" : 0
                    });
                });
                var ul = users.length;
                if (ul > 0 && ul <= 1){
                    userp = users[0];
                }else{
                    userp = users;
                }
                var nid = $('.details').attr('nid');
                $.post("/nominations/adduser", { id: nid, users: userp },
                    function(data) {
                        if (data){
                            $.each(users,function(key, value){
                                var id = value._id;
                                var name = value.name;
                                var tr = $('<tr id="'+id+'"></tr>');
                                tr.append('<td class="pic"><img width="20px" src="https://graph.facebook.com/'+id+'/picture"/></td>');
                                tr.append('<td class="name">'+name+'</td>');
                                tr.append('<td class="votes">0</td>');
                                var avote = $('<a class=".vote" href="#">'+$.t('dashboard.vote')+'</a>');
                                avote.click(vote);
                                var aerase = $('<a class=".erase" href="#">'+$.t('dashboard.erase')+'</a>');
                                aerase.click(erase);
                                var menu = $('<td></td>');
                                menu.append(avote);
                                menu.append(' / ');
                                menu.append(aerase);
                                menu.appendTo(tr);
                                tbody.append(tr);
                            });
                            dialog.dialog( "close" );
                        }else{
                            dialog.dialog( "close" );
                            showMsg('dashboard.error', 'dashboard.error_adduser');
                        }
                    }
                ).error(function() { showMsg('dashboard.error', 'dashboard.error_adduser'); });
   },
   Cancel: function() {
    $( this ).dialog( "close" );
   }
  },
  close: function() {
   //TODO:
  }
 });
    //para cancelar nominacion
    $('#cancel').click(function(ev){
        ev.preventDefault();
        var nid = $('.details').attr('nid');
        $.ajax({
            type: 'POST',
            url: '/nominations/erase',
            data: {id : nid},
            success: function(data){
                if (!data){ showMsg('dashboard.error', 'dashboard.error_erasing'); return;}
                showMsg('dashboard.warning', 'dashboard.warning_erasing');
                $('#'+nid).remove();
                while($('#'+nid).length > 0){
                    $('#'+nid).remove();
                }
                $('.details').hide();
            },
            error: function(){
                showMsg('dashboard.error', 'dashboard.error_erasing');
            },
            dataType: 'json'
        });
    });
    //para terminar nominacion
    $('#end').click(function(ev){
        ev.preventDefault();
        var nid = $('.details').attr('nid');
        $.ajax({
            type: 'POST',
            url: '/nominations/end',
            data: {id : nid},
            success: function(data){
                if (!data){ showMsg('dashboard.error', 'dashboard.error_ending'); return;}
                showMsg('dashboard.warning','dashboard.win', data.name);
                $('#'+nid).remove();
                while($('#'+nid).length > 0){
                    $('#'+nid).remove();
                }
                $('.details').hide();
            },
            error: function(){
                showMsg('dashboard.error', 'dashboard.error_ending');
            },
            dataType: 'json'
        });
    });
    //para remover al usuario actual de la nominacion
    $('#remove').click(function(ev){
        ev.preventDefault();
        var uid = $(this).attr('uid');
        var nid = $('.details').attr('nid');
        $.post("/nominations/eraseuser", { id: nid, user: 'eraseme' },
            function(data) {
                if (data){
                    //get the row of the user and erase it
                   $('.details').find('#'+uid).remove();
                }else{
                    showMsg('dashboard.error', 'dashboard.error_removing');
                }
            }
        ).error(function() { showMsg('dashboard.error', 'dashboard.error_removing'); });
    });
    //escuchar por los clicks en votar y borrar
    $('.vote').click(vote);
    $('.erase').click(erase);
});

Line 1-11: we start the i18next

12-19: lets extend jquery functionality to find text that contains or match the term passed

21-30: this will show a modal dialog with errors or announcements to the user so we done use an ugly alert

33-41: initialize our messages dialog

45-94: vote and erase user, first lets prevent the regular behavior with "preventDefault", we get the data from the dom and we make a post with jquery sending the data to "/nominations/vote" or erase, if everything goes ok we put the number of votes or erase the user from the list, in any error we show an error msg

96-107: loading the list of friends, i got an error in opera here, it loads the list anyway, im not sure how to fix this, any idea?

109-122: load the nominations by type and we add them to the left list

124-137: we are just letting the user to select one nomination at the time, once is selected we will show the details for it, we un-select first others selected

139-194: showing the details, load first the data and fill out the details with jquery, we generate the dom with jquery in the for to fill out the table

239-334: add new nomination and add user opens a dialog, in the first one just ask the name of the nomination and the end date, the other one show the list of friends of the user with a filter at the top to search quickly an user... the list load once the user log in to the page so we don't have delay while opening this dialog.

336-396: cancel, remove and end button, cancel just end the nomination and don't declare any winner, remove its to remove yourself for one nomination, in case you don't want to be listed there, if you erase yourself or another user it cannot be added again and end its to declare a winner and end the nomination, all the participants will be notified with a wall post

Cool, this post don't pretend to be a jquery tutorial but if you need help or want a better tutorial about this i can create a post for that.

Also if you need help creating an app in Facebook or how to deploy a node app i can explain that more in depth

With this we have an app with enough functionality, we still need to polish it but for now lets put it in a server and we start to notify to the users to test our beta version

Start nominating your friends in:


If you have feedback please comment it or put it on the facebook page


We are creating a logo, send us your examples we may put yours :)

Next time we will add just some things and we will create our github readme to declare it a beta version ready

Fork the code in github:


Greetings 

2.16.2012

Nomination part 8 - ui


Version en español


This post is part of the series about creating an app with node.js, express for Facebook




Lets see how the ui will end up, lets see the mocks i did on cacoo, this is the index


Dashboard


Good lets try to do them in jade


We are going to use the structure from html5boilerplate with the css, we are going to add some small things at the end i will show all the css added


Lets look at the "index.jade"

#header-container
    header.wrapper.clearfix
        h1.title= t('app.name')
#main-container
    #main.wrapper.clearfix
        table.att_table
            thead
                tr
                    th= t('index.features')
            tbody
                tr
                    td= t('index.feature1')
                tr
                    td= t('index.feature2')
                tr
                    td= t('index.feature3')
                tr
                    td= t('index.feature4')
                tr
                    td= t('index.feature5')
                    td
                        a(href="/login")
                            img(src="/images/cwf.png")
        - if (error)
            .error #{error}
#footer-container
    footer.wrapper
        .ficon
            img(src="/images/ipn.png")


We are adding our title a table with the features, an image to load to Facebook, a placeholder for show any error and a footer with our logo, will look like this:





"Dashboard.jade"

#header-container
    header.wrapper.clearfix
        .new
            a#nn(href="#")= t('dashboard.new_nomination')
        .loading
            img(src="/images/loading.png")
        .exit
            a(href="/logout")= t('dashboard.exit')
#main-container
    #main.wrapper.clearfix
        .nominations
            div.nheader.title #{t('dashboard.mine')}
            ul
                li
                    input(type="checkbox", checked="true")
                    label  Nomination 1
                li
                    input(type="checkbox")
                    label  Nomination 2
                li
                    input(type="checkbox")
                    label  Nomination 3
            div.title #{t('dashboard.appear')}
            ul
                li
                    input(type="checkbox")
                    label  Nomination a
                li
                    input(type="checkbox")
                    label  Nomination b
                li
                    input(type="checkbox")
                    label  Nomination d
                li
                    input(type="checkbox")
                    label  Nomination e
                li
                    input(type="checkbox")
                    label  Nomination f
                li
                    input(type="checkbox")
                    label  Nomination g
                li
                    input(type="checkbox")
                    label  Nomination h

            div.title #{t('dashboard.voted')}
            ul
                li
                    input(type="checkbox")
                    label  Nomination i
                li
                    input(type="checkbox")
                    label  Nomination ii
                li
                    input(type="checkbox")
                    label  Nomination iii
        .details
            fieldset.fs
                legend Nomination 1
                .header
                    .users #{t('dashboard.users')}
                    .add
                        a#am(href="#")#{t('dashboard.add_more')}
                    .date Date
                br
                .list
                    table.userst
                        thead
                            tr
                                th #{t('dashboard.name')}
                                th #{t('dashboard.votes')}
                                th
                        tbody
                            tr
                                td User 1
                                td 2
                                td
                                    a(href="#") #{t('dashboard.vote')}
                                    | /
                                    a(href="#") #{t('dashboard.erase')}
                            tr
                                td User 2
                                td 3
                                td
                                    a(href="#") #{t('dashboard.vote')}
                                    | /
                                    a(href="#") #{t('dashboard.erase')}
                            - for (var i =4; i                                tr
                                    td User #{i}
                                    td #{Math.floor(Math.random()*11)}
                                    td
                                        a(href="#") #{t('dashboard.vote')}
                                        | /
                                        a(href="#") #{t('dashboard.erase')}
                .btns
                    button.myButton#end #{t('dashboard.end_nomination')}
                    button.myButton#remove #{t('dashboard.remove_me')}
                    button.myButton#cancel #{t('dashboard.cancel')}
        .images
        .logo logo
        .ficon
            img(src="/images/ipn.png")
#footer-container
    footer.wrapper

#dialog-new(title="Create new nomination")
    p.validateTips All form fields are required
    form
        fieldset
            label.forml(for="name") Name:
            input#name.text.ui-widget-content.ui-corner-all(type="text", name="name")
            br
            label.forml(for="email") End date:
            input#datep.text.ui-widget-content.ui-corner-all(type="text", name="datep")

#dialog-add(title="Add friends")
    form
        fieldset
            ol#selectable
                li.ui-state-default 1
                li.ui-state-default 2
                li.ui-state-default 3
                li.ui-state-default 4
                li.ui-state-default 5
                li.ui-state-default 6


We are adding a lot of things that we are going to erase later, but its to see if this is looking ok, the only interesting thing here will be the "for" to add some users around lines 89-96, we will fill out this from the app later.


We have two lost forms at the end, but we will hide them because we will use them as dialogs with jquery-ui.


Lets update our layout to add our css and the js needed

link(rel="stylesheet", href="/stylesheets/jquery-ui.css", type="text/css")

script(defer, src="/javascripts/lib/jquery-ui.min.js")


Lets start our dialogs and a date picker, we add al this to "script.js"

    $( "#datep" ).datepicker();
    $( "#selectable" ).selectable();
    $("#nn").click(function(){
        $( "#dialog-new" ).dialog( "open" );
    });
    $("#am").click(function(){
        $( "#dialog-add" ).dialog( "open" );
    });
    $( "#dialog-new" ).dialog({
  autoOpen: false,
  height: 300,
  width: 450,
  modal: true,
  buttons: {
   "Create a nomination": function() {
    $( this ).dialog( "close" );
   },
   Cancel: function() {
    $( this ).dialog( "close" );
   }
  },
  close: function() {
   //TODO:
  }
 });
    $( "#dialog-add" ).dialog({
        autoOpen: false,
  height: 300,
  width: 450,
  modal: true,
  buttons: {
   "Add friend(s)": function() {
    $( this ).dialog( "close" );
   },
   Cancel: function() {
    $( this ).dialog( "close" );
   }
  },
  close: function() {
   //TODO:
  }
 });


Then we also one that only one checkbox can be selected at the time and also when they select a label the checkbox get selected ask, we use some query magic and add it also to "script.js"

    function checkOne(){
        var currentEl = $(this);
        $("input:checked").each(function(){
            $(this).attr('checked', false);
        });
        if (currentEl.is('input')){
            currentEl.attr('checked', true);
            return;
        }
        var checkbox = currentEl.siblings('input');
        checkbox.attr('checked', !checkbox.attr('checked'));
    }
    $(".nominations ul label").click(checkOne);
    $(".nominations ul input").click(checkOne);


Here we just listen to the click event in the checkbox and the label, we deselect the already selected and we select the current one

We end up with something like this:



and the dialogs







And the css that we are going to add is:

/* =============================================================================
   Primary styles
   Author: mrpix
   ========================================================================== */
.logo{
    text-align: center;
}
.list{
    margin-top: 20px;
}
.btns{
    margin-top: 5%;
}
#main-container{
    height: 500px;
}
#main{
    height: 100%;
}
.userst th, .userst td{
    border: 1px solid #ededed;
}
.userst th{
    background-color: #ededed
}
.userst{
    width: 95%;
}
.btns button{
    margin-left: 5%;
}
.users, .add{
    float: left;
    margin-right: 30%;
}
.date{
    float: right;
}
.nominations .title{
    background-color: #ededed;
    width: 60%;
    padding: 0 0 0 40px;
    border: 1px solid black;
    text-align: left;
}
.nominations ul{
    border: 1px solid black;
    width: 60%;
    height: 120px;
    overflow: auto;
    margin-bottom: 0;
}
.nheader{
    -webkit-border-top-left-radius: 13px;
    -webkit-border-top-right-radius: 13px;
    -moz-border-radius-topleft: 13px;
    -moz-border-radius-topright: 13px;
    border-top-left-radius: 13px;
    border-top-right-radius: 13px;
}
.nominations li{
    list-style:none;
    background-color: white;
    margin-left: 0;
    border-bottom: 1px solid #ededed;
    margin-left: -25px;
}
label{
    padding-left: 5px;
    padding-right: 5px;
}
.nominations{
    width: 30%;
    height: 90%;
    overflow: auto;
    float: left;
}
.details{
    width: 69%;
    height: 90%;
    overflow: auto;
    float: left;
}
.new{
    float: left;
}
.loading{
    text-align: center;
}
.exit{
    float: right;
    margin-top: -1.7em;
}

.copy {
    text-align: center;
}
.ficon{
 float: right;
}

.title{
 text-align: center;
}
.att_table
{

 background: #fff;
 margin: 0px 40px 3px 50px;
 width: 90%;
 border-collapse: collapse;
 text-align: left;
}
.att_table th
{

 color: #039;
 padding: 10px 8px;
 border-bottom: 2px solid #6678b1;
}
.att_table td
{
 border-bottom: 1px solid #ccc;
 color: #669;
 padding: 6px 8px;
}
.att_table tbody tr:hover td
{
 color: #00c;
}
.myButton {
    -moz-box-shadow:inset 0px 1px 0px 0px #ffffff;
 -webkit-box-shadow:inset 0px 1px 0px 0px #ffffff;
 box-shadow:inset 0px 1px 0px 0px #ffffff;
 background:-webkit-gradient( linear, left top, left bottom, color-stop(0.05, #ededed), color-stop(1, #dfdfdf) );
 background:-moz-linear-gradient( center top, #ededed 5%, #dfdfdf 100% );
 filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ededed', endColorstr='#dfdfdf');
 background-color:#ededed;
 -moz-border-radius:6px;
 -webkit-border-radius:6px;
 border-radius:6px;
 border:1px solid #dcdcdc;
 display:inline-block;
 color:#777777;
 font-family:arial;
 font-size:15px;
 font-weight:bold;
 padding:6px 24px;
 text-decoration:none;
 text-shadow:1px 1px 0px #ffffff;
}.myButton:hover {
 background:-webkit-gradient( linear, left top, left bottom, color-stop(0.05, #dfdfdf), color-stop(1, #ededed) );
 background:-moz-linear-gradient( center top, #dfdfdf 5%, #ededed 100% );
 filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#dfdfdf', endColorstr='#ededed');
 background-color:#dfdfdf;
}.myButton:active {
 position:relative;
 top:1px;
}
#dialog-add, #dialog-new{
    display: none;
}
#selectable .ui-selecting { background: #FECA40; }
#selectable .ui-selected { background: #F39814; color: white; }
#selectable { list-style-type: none; margin: 0; padding: 0; }
#selectable li { margin: 3px; padding: 1px; float: left; width: 50px; height: 40px; font-size: 2em; text-align: center; }


If you need help understanding the css let me know in the comments

The design is not the best but we are making progress, any suggestions is welcome


Fork the code in github:


Greetings 

2.15.2012

Nomination part 7 - connecting to facebook


Version en español


This post is part of the series about creating an app with node.js, express for Facebook




Lets connect to fcebook to get the user information and store in session the tokens for request, we will only save the userid and name in the db.


For this we will use a module called "facebook-j", this module its already installed, so lets add it to our "app.js" at the beginning of the file with the other modules like this:

var fb = require('facebook-js');


And then lets add some routes, first for the login that will send to the facebook page for the user login or if its already logged in he must accept the app in order to let the app get information from the profile

    /**
     * Login page
     */
    app.get('/login', function(req, res){
        log.notice('trying to login:' + new Date());
        res.redirect(fb.getAuthorizeUrl({
            client_id: '264644053569277', //put the client id
            redirect_uri: 'http://nomi-nation.pinguxx.c9.io/auth/fb', //cambiar si es necesario
            scope: 'offline_access,publish_stream,read_stream'
        }));
    });
    /**
     * FB return
    */
    app.get('/auth/fb', function(req, res){
        log.notice('response from fb: ' + new Date());
        fb.getAccessToken('264644053569277', //clientid
            '76ded2bf195073ce7a183a1ef1cd0b8a', //app secret
            req.param('code'),
            'http://nomi-nation.pinguxx.c9.io/auth/fb', //cambiar si es necesario
            function (error, access_token, refresh_token) {
                if (error){
                    log.debug('error getting access token:' + error);
                    throw new Error('Error getting the acccess token');
                }
                log.notice('trying to get the tokens:' + new Date());
                req.session.user = {};
                req.session.user.access_token = access_token;
                req.session.user.access_token_secret = refresh_token;
                fb.apiCall('GET', '/me/', {access_token: req.session.user.access_token}, function(error, response, body){
                    if (error){
                        log.debug('error getting user info:' + error);
                        throw new Error('Error getting user information');
                    }
                    log.notice('getting info from user:' + body.id);
                    req.session.user.name = body.username;
                    req.session.user.id = body.id;
                    res.redirect('/dashboard');
                });
            }
        );
    });


Line 4-5: got the login path and log that someone is trying to access


6: we redirect to whatever Facebook api returns


7: add the client id that we got from Facebook, if you need help creating and app in Facebook let me know in the comments section


8: the "redirect_uri" its where we are going to return once the user has accepted our ap


9: the scope of our app, meaning what things are going to see from the app, we will need publish and read, we may not need to read it but for know lets leave it like that, the offline access is to read the user profile.

15 y 16: path is needed to put where to return and then we log whatever Facebook returns


17-21: we ask an access token to Facebook, we pass the "client_id" and the secrecy key of the app


22-25: if something bad happens lets report it

26-30: logging and storing in session the access token and we make another call to the api but this time to get the user information from "/me", we send the access token that we just got

31-38: check errors again, log and save the name and we redirect to the "/dashboard" path
Bien para manejar la ruta y las próximas rutas de dashboard usaremos otro archivo que le llamaremos dashboard.js en este por el momento solo declararemos la ruta principal


Lets create a file called "dashboard.js" to hold all the paths for the actual app, it will look like this:

/**
 * Rutas dashboard
*/
module.exports = function(app, log){

    /**
     * Dashboard landing
    */
    app.get('/dashboard', function(req, res){
        log.notice('landed on dashboard user: ' +
            req.session.user.id + ' on: ' + new Date() );
        res.render('dashboard', { user: req.session.user });
    });
};


Simple, just log out and send the dashboard template with the user object


lets add this dashboard routes to the "app.js" just like before, add it after index.js

require('./routes/dashboard')(app, log);


OK, our new view will be called "dashboard.jade" and right now we will show only the data that we store from Facebook

.username #{t('dashboard.user_name')}: #{user.name}
.userid #{t('dashboard.user_id')}: #{user.id}


Using our i18next module we add the name and the id, so we just need to update the "translation.json" for all the languages

    "dashboard":{
        "user_name" : "Nombre del usuario",
        "user_id" : "Id del usuario"
    }


And we are set, our app is now connected to Facebook and downloading info from there, in later posts you will see how to get other information

Fork the code in github:


Greetings 

2.14.2012

Nomination part 6 - internationalization



Version en español


This post is part of the series about creating an app with node.js, express for Facebook



Lets add i18 to our app since the beginning so we dont end up with a lof of strings to translate.


To do it so we are going to add a list of strings in a local file that we load to the app, it depends on the language settings in the user browser the file we are going to send.


Lets start adding a new module to our package called "i18next" that will allow us to handle the localization files, our "package.json" will look like this:


{
    "name": "nomi-nation"
  , "version": "0.0.1"
  , "private": true
  , "dependencies": {
      "express": "2.5.2"
    , "express-messages" : ">= 0.0.2"
    , "facebook-js" : "1.0.1"
    , "jade": ">= 0.0.1"
    , "log" : "1.2.0"
    , "mongoose" : "2.4.8"
    , "vows" : "0.6.1"
    , "zombie" : "0.12.9"
    , "jsdom" : "0.2.10"
    , "i18next" : "0.5.1"
  }
}

And as always lets update the dependencies with "npm install -d"

We need to create a folder called "locals" and inside of it more folders depending on the languages you are going to support in this case will be en-US, es-MX, en, es, those are in their iso name, inside all this sub folders we are going to add a file called "translation.json" same for all folders, the way i18next works is to get the code from the user request for example "en-US", if doesn't find it then will look for "en" if that also doesn't exists it will go to a default route if doesn't exists anywhere then it just returns the string you used as identifier, got it?... our translation file will look like this:

{
    "app": {
        "name": "Nomi-nation",
         "welcome" : "Welcome to $t(app.name)"
    },
    "creator": {
        "name": "MrPix"
    }
}


and in spanish:

{
    "app": {
        "name": "Nomi-nation",
        "welcome" : "Bienvenido a $t(app.name)"
    },
    "creator": {
        "name": "MrPix"
    }
}

Its a simple json file with keys and values, we can do nice things like in line 2, we are adding the app name inside our welcome string so it will get app.name instead of the code.

We have our files so lets code...

In "app.js" around line 8 lets add this line to load the module

i18next = require('i18next')


After our variables we need to initialize our i18next:

i18next.init({
    ns: { namespaces: ['translation'], defaultNs: 'translation'},
    resSetPath: 'locales/__lng__/new.__ns__.json',
    saveMissing: true
});

Here we are telling i18next that the namespace to load is translation which is our file and we add some default, the "resSetPath" helps to set the path dynamically, the "__" indicates a variable and at the end "saveMissing" its just to save all the keys that i18 didn't find in a new file, we shouldn't use this in prod.


In our configure part, lets add a helper for our views so we can use this without problems in jade

app.use(i18next.handle);


And after the configure part, lets initialize the helper:

i18next.registerAppHelper(app)
    .serveClientScript(app)
    .serveDynamicResources(app)
    .serveMissingKeyRoute(app);


Cool we are done in "app.js", lets see the templates. First "layout.jade" will look now like this:

!!! 5
html(lang="en")
  head
    title= t('app.name')
    link(rel='stylesheet', href='/stylesheets/style.css')
  body!= body
  script(src="http://code.jquery.com/jquery-1.7.1.min.js", type="text/javascript")
  script(src='i18next/i18next.js', type='text/javascript')
  script(src='javascripts/script.js', type='text/javascript')

We are updating the title to now get "t('app.name')" which is the key "app" and then the sub-key "name".

Then we are adding "i18next.js" that i18next server this file, this will help us to initialize i18 in the client side.

"index.jade" will look like this:
h1= t('app.name')
p #{t('app.welcome')}
.by by: #{t('creator.name')}

We are loading the strings in different ways, first we are just adding and h1 with the app name and then we are printing as a variable inside p and the div ".by" the welcome msg and the creator string.

OK, lets initialize i18next in the client side, remember that we already have the js file that we add in the layout, lets see the js code to initialize it:

var options = {
    ns: { namespaces: ['translation'], defaultNs: 'translation'},
    useLocalStorage: false,
    resGetPath: 'locales/resources.json?lng=__lng__&ns=__ns__',
    dynamicLoad: true,
    sendMissing: true
};
$.i18n.init(options, function() {
    //TODO: add more text
});


First we are creating an object to add all the options, we pass the namespace like in the server, the flag to use the localstorage in the browser if present, the path to load the files and to send the missing keys, then we just start the functionality with jquery.


To use it everywhere we can just use the function t like this:

$('#appname').text($.t('app.name'));


or

$.t()


or use the jquery function with something like this:

// file with strings
"nav": {
    "home": "home",
    "1": "link1",
    "2": "link2"
}

// the html
<ul class="nav">
    <li class="active"><a href="#" data-i18n="nav.home">home</a></li>
    <li><a href="#" data-i18n="nav.1">link1</a></li>
    <li><a href="#" data-i18n="nav.2">link2</a></li>
</ul>

// traduce all the elements with "data-i18n"
$('.nav').i18n();


We are set, we have our app ready for i18, instead of hardcoding the strings lets use this functionality and we can present our app in several languages


Resources:


By the way, update the testing because we change the title from "Express" to "Nomi-nation"



Fork the code in github:


Greetings 

2.12.2012

Nomination part 5 - testing



Version en español


This post is part of the series about creating an app with node.js, express for Facebook





So lets not code everything without knowing if we are doing things right or what... lets start testing what we do or better yet lets try to do some TDD, or do the test cases before the actual code... for this part we need to install some modules to test, first one is vows which has a nice way to describe the test cases, zombie to browser without a browser :) and jsdom to manipulate the dom once the pages are loaded, "up an atom"...


Lets update our package.json adding vows, zombie and jsdom, it will look like this:

{
    "name": "nomi-nation"
  , "version": "0.0.1"
  , "private": true
  , "dependencies": {
      "express": "2.5.2"
    , "express-messages" : ">= 0.0.2"
    , "facebook-js" : "1.0.1"
    , "jade": ">= 0.0.1"
    , "log" : "1.2.0"
    , "mongoose" : "2.4.8"
    , "vows" : "0.6.1"
    , "zombie" : "0.12.9"
    , "jsdom" : "0.2.10"
  }
}

And then we install the new dependencies with:

npm install -d

Now that we have our modules, lets create a folder called "test" or whatever and we create 3 files, "main.js", "controller.js" and "routes.js".

Lets start with the controller, lets imagine we have this structure in our db:
3 nomination, test1 owner "1", test2 owner "2" y test3 owner "3"
Easy right, lets start our testing, first the controller code will be like this:

/**
 *
 * Controller testing
 *
*/
var vows = require('vows'),
    assert = require('assert'),
    nominator = require('../controllers/nominator.js');

exports.findN = vows.describe('find nominations').addBatch({
    'when finding where 2 voted' : {
        topic : function(){
            nominator.findVoted(2,this.callback);
        },
        'result its only one nomination' : function(err, docs){
            if (err) { console.log(err); return; }
            assert.lengthOf(docs, 1);
        },
        'and its test2' : function(err, docs){
            if (err) { console.log(err); return; }
            assert.equal('test2', docs[0].name);
        }
    },
    'when finding where 1 voted' : {
        topic : function(){
            nominator.findVoted(1,this.callback);
        },
        'result is 3 nominations' : function(err, docs){
            if (err) { console.log(err); return; }
            assert.lengthOf(docs, 3);
        },
        'and are test1 2 and 3' : function(err, docs){
            if (err) { console.log(err); return; }
            for (var i=0;i<3;i++){
                assert.equal('test'+(i+1), docs[i].name);
            }
        }
    },
    'when finding where 8 voted' : {
        topic : function(){
            nominator.findVoted(8,this.callback);
        },
        'result is 0' : function(err, docs){
            if (err) { console.log(err); return; }
            assert.lengthOf(docs, 0);
        }
    }
}).addBatch({
    'when finding 1 nominations' : {
        topic : function(){
            nominator.findMyNominations(1,this.callback);
        },
        'result its only one nomination' : function(err, docs){
            if (err) { console.log(err); return; }
            assert.lengthOf(docs, 1);
        },
        'and its test1' : function(err, docs){
            if (err) { console.log(err); return; }
            assert.equal('test1', docs[0].name);
        }
    },
    'when finding 9 nominations' : {
        topic : function(){
            nominator.findMyNominations(9,this.callback);
        },
        'result is 0' : function(err, docs){
            if (err) { console.log(err); return; }
            assert.lengthOf(docs, 0);
        }
    }
}).addBatch({
    'when finding where 8 is nominated' : {
        topic : function(){
            nominator.findNominated(8,this.callback);
        },
        'result its 3 nominations' : function(err, docs){
            if (err) { console.log(err); return; }
            assert.lengthOf(docs, 3);
        },
        'and its test1, 2 and 3' : function(err, docs){
            if (err) { console.log(err); return; }
            for (var i=0;i<3;i++){
                assert.equal('test'+(i+1), docs[i].name);
            }
        }
    },
    'when finding where 1 is nominated' : {
        topic : function(){
            nominator.findNominated(1,this.callback);
        },
        'result is 0 ' : function(err, docs){
            if (err) { console.log(err); return; }
            assert.lengthOf(docs, 0);
        }
    }
});

Line 1-8: Loading modules and our controller "nominator.js"


10: we export the function "findN" with exports and we start describing the test in this case we are going to name it 'find nominations' and we add a batch, this will hold all our test cases.


11-14: The first test is called 'when finding where 2 voted', eso which means we are going to search for nominations where the user with id "2" already voted, first we create the topic that is like the entrance to the test case where we call the function "findVoted" from the nominator controller, we send the id to search and something important here, we use the local callback "this.callback", this means that whatever the function returns it will go to all the next test we have inside this topic in an async way


15-18: The next test is: 'result its only one nomination', the result expected its only one nomination and to check it first lets make sure we didnt get any error from the callback and then the lenght of the docs returned, since we are searching this always return an array of docs, even if the result is only one, to check the lenght we do "assert.lengthOf" to docs and we compare to 1 that is the lenght we are expecting.


19-22: Next test its called 'and its test2', here we also get the results from "findVoted" and in this case we check that the name of the nomination is "test 2" which is the only one that user 2 has voted, first we check for an error and then we check the name asserting "doc[0].name" to "test2"

...: Well its the same again and again in this file just checking different functions from our nomination controller and checking other things, we just add them to the batch, review them, any questions let me know in the comments


This code is what i use to add users and nominations:

var nomination = {
    'name' : "test3",
    'owner' : "3", //who is the owner of the nomination
    'endDate' : new Date(), //when this nomination is going to end
    'category' : "cat1",
    'sub_cat' : "sub1",
    'active' : true //nomination finished
};
nominator.createNomination(nomination, function(err, doc){
    if (err) { console.log(err); return; }
    var users = [];
    for(var i = 4; i<12; i++ ){
        users.push({
            "_id" : i,
            "name" : i,
            "votes" : 0
        });
    }
    nominator.addUser(doc, users, function(err){
        if (err) {  console.log(err); return; }
        var user = {"_id":9, "name":9, "votes":0 };
        nominator.eraseUser(doc, user, function(err){
            if (err) {  console.log(err); return; }
            nominator.addUser(doc, user, function(err){
                if (err) {  console.log(err); return; }
            });
            nominator.addUser(doc, {"_id":19, "name":19, "votes":0 }, function(err){
                if (err) {  console.log(err); return; }
            });
        });
    });
});

In this case we are creating a nomination and after that we add some users with "adduser" and then we erase some and add more just for fun.

And to add votes we do:

nominator.findMyNominations(1, function(err, doc){
    if (err) { console.log(err); return; }
    for (var i=2;i<5;i++){
        nominator.vote(doc[0], 1, i, function(err, doc){
            if (err) {  console.log(err); return; }
        });
    }
});

We search for userid 1 nomination and we add voters. try to do some testing for this with vows...

Good, lets test our main route "/", zombie to the rescue. Lets add the following code to "routes.js":

/**
 *
 * Test index route
*/
var vows = require('vows'),
    assert = require('assert'),
    zombie = require('zombie'),
    jsdom = require('jsdom');

exports.index = vows.describe('Visiting index').addBatch({
    'when hitting the page': {
        topic: function(){
            zombie.visit('http://localhost:3000',this.callback);
        },
        'we got the page': function(err, browser, status) {
            if (err){
                throw(err.message);
            }
            assert.equal(status, 200);
        },
        'and try to get the title' : {
            topic : function(browser){
                var vow = this;
                jsdom.env({
                    html: browser.response[2],
                    scripts: [
                        'http://code.jquery.com/jquery-1.5.min.js'
                    ],
                    done: vow.callback
                });
            },
            'the title is Express' : function(errors, window) {
                //console.log(errors);
                if (errors){
                    throw(errors);
                }else{
                    var $ = window.$;
                    assert.equal('Express', $('title').html());
                }
            }
        }
    }
});

Line 1-8: loading modules

10: We add our test "visitin index" y we add our batch (set) of testcases

11-14: "when hitting the page" this test will fire zombie and will try to reach our page, as a callback we put our local callback so all the other test cases have the results

15-20: 'we got the page', this page takes the arguments error, browser and the status, first we check for errors and then that we have a 200 response

21-22: 'and try to get the title', here we are adding a new topic in this case we take the response from zombie minus the error and also this one have status but im not adding it

23-30: lets store vows in a var for future reference and we use jsdom, in  this case we are sending the html that we got from zombie, we inject it jquery and we call the local callback for the next test case

32-39: 'the title is Express', this test gets for arguments what jsdom returns that it may be a possible error or the window that holds all the page, its like our browser page, we check for page errors and if everything is good with jquery we check for the title as if you were in the browser.

Cooll, lets create or "main.js" to call our testing

#!/usr/bin/env node
/**
 * Test runner
 *
*/
var controllers = require('./controllers');
controllers.findN.run();

We just got our testing file and we run them, check the documentation for more great stuff