Petit système de vote en AJAX

Le système que je vous propose est assez simpliste : il s’agit d’un vote type « j’aime / j’aime pas ».

On dispose d’un ensemble d’éléments (ici des messages) que l’on nous propose de noter.

Dans une première étape, le principe est de créer le système de vote, dans une deuxième étape on limitera les votes : un visiteur ne peut voter sur un élément qu’une fois par jour.
(Attention, ne pas pas oublier d’ajouter les include et use_javascript dans le layout de votre appli)

Etape 1 :

Schéma :

Message:
  columns:
    text:
      type: string(255)
    relations:
      Ratings:
        type: many
        class: Rating
        local: id
        foreign: message_id

Rating:
  actAs: { Timestampable: ~ } //permet d'avoir les champs "created_at" et "updated_at", pour limiter le nombre de vote à 1 par jour
  columns:
    value:
      type: boolean
    message_id:
      type: integer
  relations:
    Message:
      onDelete: CASCADE
      local: message_id
      foreign: id

On a donc une table contenant nos messages (ici on n’a besoin que du texte) et une table contenant les votes : un vote est donc décrit par une id, et un message, donc à chaque click un nouveau vote est inséré dans la base de données.

Pour éviter de se retrouver avec une BDD sur-remplie, on pourra rajouter une 3° étape qui consiste à créer une tâche qui compile chaque vote sur un message pour en faire un résumé (qui fait la somme par exemple) et qui ensuite supprime les votes compilés.

Bref, les fonctionnalités utiles maintenant sont : afficher la note du message, afficher le nombre de votes (pour pouvoir faire la différence entre un message qui a une bonne note mais 3 votes et un message qui a une bonne note mais 1000 votes, par exemple), et afficher les liens permettant de voter, tout ça à coté du message en question, bien évidemment.

Voici donc le code de l’index des message (apps/*votre appli*/modules/message/templates/indexSuccess.php)



Message List

<div id ="ratinggetId() ?>"> $message)) ?>
gettext() ?>

Partial message/rating (apps/*votre appli*/module/template/_rating.php) :

Note : getGlobalRating(); ?>/,
Nombre de votes : getRatingsCount() ?>

La syntaxe utilisée ici pour les fonction link_to_remote est :
link_to_remote(‘texte à afficher pour le lien’, tableau des options(‘update’ => id de la div a update, ‘url’ => adresse de l’action))

Vous noterez aussi que les url des link_to_remote utilisent des routes, voici les routes utilisées(apps/*votre appli*/config/routes.yml) :

message_vote_plus:
  url:      /message-vote-plus/:id
  param:    { module: message, action: votePlus }

message_vote_moins:
  url:      /message-vote-moins/:id
  param:    { module: message, action: voteMoins }

Un click sur un lien va donc engendrer l’action votePlus ou voteMoins de l’objet message en question, spécifié par l’id.
Voici les actions (apps/*votre appli*/modules/message/actions.class.php) :

  //execute le vote "j'aime"
  public function executeVotePlus(sfWebRequest $request)
  {
    //on récupère le message courant
    $this->message = Doctrine::getTable('Message')->find($request->getParameter('id'));
    //on effectue le vote positif
    $this->message->votePlus();
    //on génère le partial du message, c'est ce résultat qui sera utilisé par le link_to_remote pour mettre à jour la div spécifiée, cela évite également le passage par le template votePlusSuccess.php (qui donc n'est pas à créer)
    return $this->renderPartial('message/rating',array('message' => $this->message));
  }

  //execute le vote "j'aime pas"
  public function executeVoteMoins(sfWebRequest $request)
  {
    //on récupère le message courant
    $this->message = Doctrine::getTable('Message')->find($request->getParameter('id'));
    //on effectue le vote négatif
    $this->message->voteMoins();
    //on génère le partial du message, c'est ce résultat qui sera utilisé par le link_to_remote pour mettre à jour la div spécifiée, cela évite également le passage par le template voteMoinsSuccess.php (qui donc n'est pas à créer)
    return $this->renderPartial('message/rating',array('message' => $this->message));
  }

L’action utilise donc naturellement des méthodes du controlleur (lib/model/doctrine/Message.class.php) :
-récupération du nombres de vote sur 1 message :

  //retourne le nombre de votes sur ce message
  public function getRatingsCount()
  {
    return Doctrine::getTable('Rating')->getMessageRatingsQuery($this->getId())->count();
  }

-récupération de la note d’un message :

  //retourne la note globale du message courant
  public function getGlobalRating()
  {
    //on récupère tous les votes sur ce message
    $ratings = Doctrine::getTable('Rating')->getMessageRatingsQuery($this->getId())->execute();
    //on initialise la note a 0
    $note = 0;
    //pour chaque vote positif on incrémente la note (rappel : la note est stockée sous forme de boolean dans la base)
    foreach ($ratings as $rating)
    {
      if ($rating->getValue())
      {
        $note++;
      }
    }
    //on récupère le nombre de votes, pour calculer la moyenne
    $nbvotes = $this->getRatingsCount();
    //si il n'y pas encore eu de vote sur ce message, on est obligé de donner la valeur 1 à nbvotes, pour éviter une division par 0
    if($nbvotes == 0){ $nbvotes = 1; }

    //on retourne la moyenne : (note/nb_votes)*"valeur sur laquelle on rapporte la note" et on arrondi (round)
    //la valeur sur laqelle on rapporte la note est stockée dans la config (app.yml), ainsi que le nombre de chiffres apres la virgule
    return round(($note/$nbvotes)*sfConfig::get('app_moyenne_sur'), sfConfig::get('app_round'));
  }

-votes :

  //ajoute un vote positif relatif à ce message
  public function votePlus()
  {
    return $this->vote('1');
  }

  //ajoute un vote négatif relatif à ce message
  public function voteMoins()
  {
    return $this->vote('0');
  }

  //effectur le vote
  public function vote($value)
  {
    $rating = new Rating(); //nouveau vote
    //on set ses champs
    $rating->setMessageId($this->getId()); //message_id
    $rating->setValue($value); //value
    $rating->save(); //on l'enregistre dans la base
  }

Voilà pour la première étape, le système de votes est opérationnel, reste plus qu’à limiter les votes.

Etape 2
De quoi a-t-on besoin pour la suite ?
1) Modifier le schéma pour ajouter l’attribut ip_user, correspondant à l’adresse ip de l’utilisateur
2) De vérifier au moment du vote dans le modèle, s’il existe déjà un vote récent pour ce message et pour cette adresse ip
3) Donc, on a aussi besoin d’une méthode dans la classe RatingTable.class.php qui récupère les votes récents
4) On ajoutera également un message à l’utilisateur pour le remercier de son vote ou lui indiquer qu’il n’a plus le droit de voter.

Mise à jour de la table Rating dans le schéma :

Rating:
  actAs: { Timestampable: ~ }
  columns:
    value:
      type: boolean
    message_id:
      type: integer
    user_ip:
      type: string(12)
      notnull: true
  relations:
    Message:
      onDelete: CASCADE
      local: message_id
      foreign: id

1) Modification de la méthode vote()

public function vote(sfWebRequest $request, $value)
  {
    //on récupère l'adresse ip du visiteur/utilisateur
    $user_ip = $request->getRemoteAddress();
    //on récupère les votes effectués par cet utilisateur sur ce message
    $recent_rating = Doctrine::getTable('Rating')->getRecentRating(array('user_ip' => $user_ip, 'message_id' => $this->getId()));
    //si il n'existe pas de vote trop récent
    if(!($recent_rating->count() > 0))
    {
      $rating = new Rating(); //nouveau vote
      //on set ses champs
      $rating->setUserIp($user_ip); //user_ip
      $rating->setMessageId($this->getId()); //message_id
      $rating->setValue($value); //value
      $rating->save(); //on l'enregistre dans la base et on retourne vrai
      return true;
    }
    else //si le vote est trop récent, pas de vote, on retourne faux
    {
      return false;
    }
  }

Ce qui implique évidemment la création de la méthode getRecentRating() (dans lib/model/doctrine/RatingTable.class.php) :

  //retourne les votes récents dans la query
  public function getRecentRating($params, Doctrine_Query $query = NULL)
  {
    if(is_null($query)) //si pas de requete, on récupère tous les votes de la base
    {
      $query = $this->createQuery('c');
    }
    //on récupère tous les votes faits avec cette ip
    $query = $this->getRatingsByUserIp($params['user_ip'], $query);
    //on récupère tous les votes sur ce message
    $query = $this->getMessageRatingsQuery($params['message_id'], $query);
    //on sélectionne uniquement les message récents
    $query->andWhere('c.created_at >= ?', date('Y-m-d h:i:s', time() - 86400 * (sfConfig::get('app_vote_time_limit') / 24)));
    return $query;
  }

La valeur de la limite de temps pour le vote et stockée dans la config, de la même manière que round et moyenne_sur dans la première partie

All:
  moyenne_sur: 20
  round: 1
  #limite de temps pour le vote (en heures)
  vote_time_limit: 24

Vous noterez sûrement la présences des fonctions getRatingsByUserIpQuery() et getMessageRatingsQuery() :

  //retourne la requête permettant de récupérer tous les votes sur un message donné, parmis ceux récupérés par la requete $query
  public function getMessageRatingsQuery($message_id, Doctrine_Query $query = NULL)
  {
    if(is_null($query)) //si aucune requête passée, on génère la requête pour récup tous les votes
    {
      $query = $this->createQuery('c');
    }
    $query->andWhere('c.message_id = ?', $message_id); //on ajoute le where pour filtrer par message_id

    return $query;
  }

  //retourne la requete permettant de récupérer tous les votes passé par une adresse ip, parmis ceux récupérés par la requete $query
  public function getRatingsByUserIp($user_ip, Doctrine_Query $query = NULL)
  {
    if(is_null($query)) //si aucune requête passée, on génère la requête pour récup tous les votes
    {
      $query = $this->createQuery('c');
    }
    $query->andWhere('c.user_ip = ?', $user_ip); //on ajoute le where pour filtrer par user_ip

    return $query;
  }

Il ne reste plus qu’à afficher un petit message à notre utilisateur (dans l’action) :

  //execute le vote "j'aime"
  public function executeVotePlus(sfWebRequest $request)
  {
    //on récupère le message courant
    $this->message = Doctrine::getTable('Message')->find($request->getParameter('id'));
    //on effectue le vote positif
    if($this->message->votePlus($request))
    {
      $this->getUser()->setFlash('vote', "Merci d'avoir voté", false);
    }
    else
    {
      $this->getUser()->setFlash('vote', "vous ne pouvez plus voter", false);
    }
    //on génère le partial du message, c'est ce résultat qui sera utilisé par le link_to_remote pour mettre à jour la div spécifiée
    //cela évite également le passage par le template votePlusSuccess.php (qui donc n'est pas à créer)
    return $this->renderPartial('message/rating',array('message' => $this->message));
  }

  //execute le vote "j'aime pas"
  public function executeVoteMoins(sfWebRequest $request)
  {
    //on récupère le message courant
    $this->message = Doctrine::getTable('Message')->find($request->getParameter('id'));
    //on effectue le vote négatif
    if($this->message->voteMoins($request))
    {
      $this->getUser()->setFlash('vote', "Merci d'avoir voté", false);
    }
    else
    {
      $this->getUser()->setFlash('vote', "vous ne pouvez plus voter", false);
    }
    //on génère le partial du message, c'est ce résultat qui sera utilisé par le link_to_remote pour mettre à jour la div spécifiée, cela évite également le passage par le template voteMoinsSuccess.php (qui donc n'est pas à créer)
    return $this->renderPartial('message/rating',array('message' => $this->message));
  }

On utilise la méthode sfUser::setFlash(). Comme la mise à jour de l’affichage sera le partial rendu, il faut donc intégrer encore quelques lignes dans le template correspondant (apps/*votre appli*/module/message/template/_rating.php :

Note : getGlobalRating(); ?>/,
Nombre de votes : getRatingsCount() ?>

hasFlash('vote')): ?>
getFlash('vote') ?>

Et voilà !!

Partagez cet article