Like/Dislike system with Mojolicious
Tags: mojolicious, perl, javascript, webdev
Published: 2022-04-29 07:38:42

This post describes the actual Like/Dislike mechanism in the program based on Mojolicious. Web pages style is created using the Bootstrap framework. The full version of the Mojolicious `like-dislike` demo can be downloaded at: [https://github.com/arslonga/like-dislike](https://github.com/arslonga/like-dislike)

So, in the start package **Vote.pm** we create routers for the post with a certain ID in the section named `'first-section'` (for example), and also for `like` and `dislike`: 

```
sub startup {
my $self = shift;

my $r = $self->routes;
...
$r->any('/first-section/:id' => [ id => qr/[0-9]+/ ] )->to('post#article');
...
$r->any('/likeartcl')->to('voting#like');
$r->any('/dislikeartcl')->to('voting#dislike');
...
}
```
A snippet of code in the **Post.pm** package that describes the `'article'` method:

```
package Vote::Controller::Post;
use Mojo::Base 'Mojolicious::Controller';
use Session;
use SessCheck;
...
#---------------------------------
sub article {
#---------------------------------
my $self = shift;
my $id = $self->stash('id');
my($nickname, $status, $client_check, $user_id, $title_alias);


eval{
$nickname = $self->session('client')->[0];
$user_id = $self->session('client')->[2]; 
};
$client_check = SessCheck->client( $self, $nickname );
if( !$client_check ){
    $status = ' disabled';
}
$title_alias = (split(/\//, $self->req->url))[1];
my $data = $self->db->select( 'posts', ['*'], {id => $id} )->hash;

$self->render( 
dat             => $data,
section_ident   => $title_alias,
id              => $id,
like_id         => 'like_'.$title_alias.'_'.$id,
unlike_id       => 'unlike_'.$title_alias.'_'.$id,
client_id       => $user_id,
unlike_btn_name => 'unlike'.$title_alias.'_'.$id,
like_btn_name   => 'like'.$title_alias.'_'.$id,
status          => $status,
liked_cnt       => $data->{liked} || 0,
unliked_cnt     => $data->{unliked} || 0
);
}#---------------
...
1;
```

Template **article.html.ep** in **/templates/post**

```
% layout 'vote';

<hr>
<a class="btn btn-info" href="/first-section" role="button">
<span class="glyphicon glyphicon-arrow-left"></span>
</a>
<hr>
<div>
    <h2><%= $dat->{title} %></h2>
    <%= $dat->{body} %>
</div>
<hr>
<div class="ld-box text-right">

%# Rendering a template that describes a block of code
%# that contains two buttons:
%# 'like' and 'dislike'

<%= include 'voting/vote' %>
</div>

<script>
//---------- Like/Dislike block ---
async function LikeArtcl(titleAlias, articleId, vote_span, user_id) {
let likedislikeBox = document.querySelector('.ld-box');

  let response = await fetch("/likeartcl?title_alias=" + 
  titleAlias + 
  '&article_id=' + 
  articleId + 
  '&vote_span=' + 
  vote_span + 
  '&user_id=' + 
  user_id);
  
  if (response.ok) {
	let respRendr = await response.text(); 
    likedislikeBox.innerHTML = respRendr;
  }else {
    alert("Error HTTP: " + response.status);
  }
}

async function UnlikeArtcl(titleAlias, articleId, vote_span, user_id) {
let likedislikeBox = document.querySelector('.ld-box');

  let response = await fetch("/dislikeartcl?title_alias=" + 
  titleAlias + 
  '&article_id=' + 
  articleId + 
  '&vote_span=' + 
  vote_span + 
  '&user_id=' + 
  user_id);

  if (response.ok) {
	let respRendr = await response.text(); 
    likedislikeBox.innerHTML = respRendr;
  }else {
    alert("Error HTTP: " + response.status);
  }
}
//---------- Like/Dislike block END ---
</script>
```
Package **Session.pm**:

```
package Session;
use Mojo::Base -base;

#---------------------------------
sub user {
#---------------------------------
my($self, $c, $login, $password, $id) = @_;

$c->session( client => [$login, $password, $id], expiration => 120);
return 1;
}#---------------

# 'voting' method of Session called in 'Voting.pm' package (see code fragment below)
#---------------------------------
sub voting {
#---------------------------------
my($self, $c, $user_vote_id, $title_alias_and_id) = @_;
$c->signed_cookie( $user_vote_id => $title_alias_and_id, {expires => time + 120});
return 1;
}#---------------

#---------------------------------
sub client_expire {
#---------------------------------
my($self, $c) = @_;
delete $c->session->{'client'};
return 1;
}#---------------
1;
```
Package **Vote/Voting.pm**
Here we render template `'voting/vote.html.ep'` that describes code block with `'like'` and `'dislike'` buttons and `'like'` and `'dislike'` counts

```
package Vote::Voting;
use Mojo::Base 'Mojolicious::Controller';
use Mojo::Util qw(trim encode decode);
use Mojo::Cookie;
use Session;

#---------------------------------
sub like {
#---------------------------------
my $self = shift;
my($nickname, $status);
eval{
$nickname = $self->session('client')->[0];  
};
my $client_check = SessCheck->client( $self, $nickname );

my $title_alias = $self->param('title_alias');
my $article_id  = $self->param('article_id');
my $user_id     = $self->param('user_id');

$status = !$client_check ? ' disabled' : '';

my $like_count = $self->db->select( 'posts', 
                                  ['liked'], 
                                  {id => $article_id} )
                                  ->hash->{liked};
my $unlike_count = $self->db->select( 'posts', 
                                    ['unliked'], 
                                    {id => $article_id} )
                                    ->hash->{unliked};
my $like_cookie_name = 'like_user'.$user_id.'_'.$title_alias.'_'.$article_id;
my $unlike_cookie_name = 'unlike_user'.$user_id.'_'.$title_alias.'_'.$article_id;

if( !$self->signed_cookie( $like_cookie_name ) && $client_check ){
    ++$like_count;
    $self->db->update( 'posts', 
                     {'liked' => $like_count}, 
                     {'id' => $article_id} );
}

if( $self->signed_cookie( $unlike_cookie_name ) ){
    --$unlike_count;
    $self->db->update( 'posts', 
                     {'unliked' => $unlike_count}, 
                     {'id' => $article_id} );
    $self->signed_cookie( $unlike_cookie_name => '', {expires => 1});
}

# Store session for 'like' action where key is 
#'like_user'.$user_id.'_'.$title_alias.'_'.$article_id
# and value is $title_alias.'_'.$article_id
Session->voting( $self, 
                 'like_user'.$user_id.'_'.$title_alias.'_'.$article_id, 
                 $title_alias.'_'.$article_id 
               );

$self->render(
template        => 'voting/vote',
section_ident   => $title_alias,
id              => $article_id,
like_id         => 'like_'.$title_alias.'_'.$article_id,
unlike_id       => 'unlike_'.$title_alias.'_'.$article_id,
client_id       => $user_id,
unlike_btn_name => 'unlike'.$title_alias.'_'.$article_id,
like_btn_name   => 'like'.$title_alias.'_'.$article_id,
status          => $status,
liked_cnt       => $like_count,
unliked_cnt     => $unlike_count
);
}#---------------

#---------------------------------
sub dislike {
#---------------------------------
my $self = shift;
my($nickname, $status);
eval{
$nickname = $self->session('client')->[0];  
};
my $client_check = SessCheck->client( $self, $nickname );

my $title_alias = $self->param('title_alias');
my $article_id  = $self->param('article_id');
my $user_id     = $self->param('user_id');

$status = !$client_check ? ' disabled' : '';

my $unlike_count = $self->db->select( 'posts', 
                                    ['unliked'], 
                                    {id => $article_id} )
                                    ->hash->{unliked};
my $like_count = $self->db->select( 'posts', 
                                  ['liked'], 
                                  {id => $article_id} )
                                  ->hash->{liked};
my $like_cookie_name = 'like_user'.$user_id.'_'.$title_alias.'_'.$article_id;
my $unlike_cookie_name = 'unlike_user'.$user_id.'_'.$title_alias.'_'.$article_id;

if( !$self->signed_cookie( $unlike_cookie_name ) && $client_check ){
    ++$unlike_count;
    $self->db->update( 'posts', 
                     {'unliked' => $unlike_count}, 
                     {'id' => $article_id} );
}

if( $self->signed_cookie( $like_cookie_name ) ){
    --$like_count;
    $self->db->update( 'posts', 
                     {'liked' => $like_count}, 
                     {'id' => $article_id} );
    $self->signed_cookie( $like_cookie_name => '', {expires => 1});
}

# Store session for 'dislike' action where key is 
#'unlike_user'.$user_id.'_'.$title_alias.'_'.$article_id
# and value is $title_alias.'_'.$article_id
Session->voting( $self, 
                 'unlike_user'.$user_id.'_'.$title_alias.'_'.$article_id, 
                 $title_alias.'_'.$article_id );

$self->render(
template        => 'voting/vote',
section_ident   => $title_alias,
id              => $article_id,
like_id         => 'like_'.$title_alias.'_'.$article_id,
unlike_id       => 'unlike_'.$title_alias.'_'.$article_id,
client_id       => $user_id,
unlike_btn_name => 'unlike'.$title_alias.'_'.$article_id,
like_btn_name   => 'like'.$title_alias.'_'.$article_id,
status          => $status,
unliked_cnt     => $unlike_count,
liked_cnt       => $like_count
);
}#---------------
1;

```

Template **voting/vote.html.ep**
Use it for both rendering `'like'` and `'dislike'` actions

```
%# voting/vote.html.ep

<button type="button" name="<%= $like_btn_name %>" 
onclick="Voting('<%= $section_ident %>', 
                   '<%= $id %>', 
                   '<%= $like_id %>', 
                   '<%= $client_id %>'); 
                   this.disabled='disabled';" 
                   id="chevron_stl"<%= $status %>>
<span id="<%= $section_ident.'_'.$client_id.'-up' %>" 
class="glyphicon glyphicon-chevron-up" aria-hidden="true"></span>
</button>
<span id="<%= $like_id %>" class="like_unlike"><%= $liked_cnt %></span>
  
<button type="button" name="<%= $unlike_btn_name %>" 
onclick="Voting('<%= $section_ident %>', 
                     '<%= $id %>', 
                     '<%= $unlike_id %>', 
                     '<%= $client_id %>'); 
                     this.disabled='disabled';" 
                     id="chevron_stl"<%= $status %>>
<span id="<%= $section_ident.'_'.$client_id.'-down' %>" 
class="glyphicon glyphicon-chevron-down" aria-hidden="true"></span>
</button>
<span id="<%= $unlike_id %>" class="like_unlike"><%= $unliked_cnt %></span>
```

## Conclusion
Use the combination of article ID, user ID, and kind of action (like or dislike) to create a unique voting ID. This voting ID can be a key of signed cookie.

A simplified like/dislike system implemented in my project **MornCat CMS** at [https://github.com/arslonga/my_blog](https://github.com/arslonga/my_blog)