<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: arslonga</title>
    <description>The latest articles on DEV Community by arslonga (@arslonga).</description>
    <link>https://dev.to/arslonga</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F666301%2F17bcc963-86fa-4610-b053-549aebb142d0.png</url>
      <title>DEV Community: arslonga</title>
      <link>https://dev.to/arslonga</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/arslonga"/>
    <language>en</language>
    <item>
      <title>Like/Dislike system with Mojolicious</title>
      <dc:creator>arslonga</dc:creator>
      <pubDate>Fri, 29 Apr 2022 07:38:42 +0000</pubDate>
      <link>https://dev.to/arslonga/likedislike-system-with-mojolicious-42ph</link>
      <guid>https://dev.to/arslonga/likedislike-system-with-mojolicious-42ph</guid>
      <description>&lt;p&gt;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 &lt;code&gt;like-dislike&lt;/code&gt; demo can be downloaded at: &lt;a href="https://github.com/arslonga/like-dislike"&gt;https://github.com/arslonga/like-dislike&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So, in the start package &lt;strong&gt;Vote.pm&lt;/strong&gt; we create routers for the post with a certain ID in the section named &lt;code&gt;'first-section'&lt;/code&gt; (for example), and also for &lt;code&gt;like&lt;/code&gt; and &lt;code&gt;dislike&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sub startup {
my $self = shift;

my $r = $self-&amp;gt;routes;
...
$r-&amp;gt;any('/first-section/:id' =&amp;gt; [ id =&amp;gt; qr/[0-9]+/ ] )-&amp;gt;to('post#article');
...
$r-&amp;gt;any('/likeartcl')-&amp;gt;to('voting#like');
$r-&amp;gt;any('/dislikeartcl')-&amp;gt;to('voting#dislike');
...
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A snippet of code in the &lt;strong&gt;Post.pm&lt;/strong&gt; package that describes the &lt;code&gt;'article'&lt;/code&gt; method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;package Vote::Controller::Post;
use Mojo::Base 'Mojolicious::Controller';
use Session;
use SessCheck;
...
#---------------------------------
sub article {
#---------------------------------
my $self = shift;
my $id = $self-&amp;gt;stash('id');
my($nickname, $status, $client_check, $user_id, $title_alias);


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

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

&lt;/div&gt;



&lt;p&gt;Template &lt;strong&gt;article.html.ep&lt;/strong&gt; in &lt;strong&gt;/templates/post&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;% layout 'vote';

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

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

&amp;lt;%= include 'voting/vote' %&amp;gt;
&amp;lt;/div&amp;gt;

&amp;lt;script&amp;gt;
//---------- 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 + 
  '&amp;amp;article_id=' + 
  articleId + 
  '&amp;amp;vote_span=' + 
  vote_span + 
  '&amp;amp;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 + 
  '&amp;amp;article_id=' + 
  articleId + 
  '&amp;amp;vote_span=' + 
  vote_span + 
  '&amp;amp;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 ---
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Package &lt;strong&gt;Session.pm&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;package Session;
use Mojo::Base -base;

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

$c-&amp;gt;session( client =&amp;gt; [$login, $password, $id], expiration =&amp;gt; 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-&amp;gt;signed_cookie( $user_vote_id =&amp;gt; $title_alias_and_id, {expires =&amp;gt; time + 120});
return 1;
}#---------------

#---------------------------------
sub client_expire {
#---------------------------------
my($self, $c) = @_;
delete $c-&amp;gt;session-&amp;gt;{'client'};
return 1;
}#---------------
1;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Package &lt;strong&gt;Vote/Voting.pm&lt;/strong&gt;&lt;br&gt;
Here we render template &lt;code&gt;'voting/vote.html.ep'&lt;/code&gt; that describes code block with &lt;code&gt;'like'&lt;/code&gt; and &lt;code&gt;'dislike'&lt;/code&gt; buttons and &lt;code&gt;'like'&lt;/code&gt; and &lt;code&gt;'dislike'&lt;/code&gt; counts&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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-&amp;gt;session('client')-&amp;gt;[0];  
};
my $client_check = SessCheck-&amp;gt;client( $self, $nickname );

my $title_alias = $self-&amp;gt;param('title_alias');
my $article_id  = $self-&amp;gt;param('article_id');
my $user_id     = $self-&amp;gt;param('user_id');

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

my $like_count = $self-&amp;gt;db-&amp;gt;select( 'posts', 
                                  ['liked'], 
                                  {id =&amp;gt; $article_id} )
                                  -&amp;gt;hash-&amp;gt;{liked};
my $unlike_count = $self-&amp;gt;db-&amp;gt;select( 'posts', 
                                    ['unliked'], 
                                    {id =&amp;gt; $article_id} )
                                    -&amp;gt;hash-&amp;gt;{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-&amp;gt;signed_cookie( $like_cookie_name ) &amp;amp;&amp;amp; $client_check ){
    ++$like_count;
    $self-&amp;gt;db-&amp;gt;update( 'posts', 
                     {'liked' =&amp;gt; $like_count}, 
                     {'id' =&amp;gt; $article_id} );
}

if( $self-&amp;gt;signed_cookie( $unlike_cookie_name ) ){
    --$unlike_count;
    $self-&amp;gt;db-&amp;gt;update( 'posts', 
                     {'unliked' =&amp;gt; $unlike_count}, 
                     {'id' =&amp;gt; $article_id} );
    $self-&amp;gt;signed_cookie( $unlike_cookie_name =&amp;gt; '', {expires =&amp;gt; 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-&amp;gt;voting( $self, 
                 'like_user'.$user_id.'_'.$title_alias.'_'.$article_id, 
                 $title_alias.'_'.$article_id 
               );

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

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

my $title_alias = $self-&amp;gt;param('title_alias');
my $article_id  = $self-&amp;gt;param('article_id');
my $user_id     = $self-&amp;gt;param('user_id');

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

my $unlike_count = $self-&amp;gt;db-&amp;gt;select( 'posts', 
                                    ['unliked'], 
                                    {id =&amp;gt; $article_id} )
                                    -&amp;gt;hash-&amp;gt;{unliked};
my $like_count = $self-&amp;gt;db-&amp;gt;select( 'posts', 
                                  ['liked'], 
                                  {id =&amp;gt; $article_id} )
                                  -&amp;gt;hash-&amp;gt;{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-&amp;gt;signed_cookie( $unlike_cookie_name ) &amp;amp;&amp;amp; $client_check ){
    ++$unlike_count;
    $self-&amp;gt;db-&amp;gt;update( 'posts', 
                     {'unliked' =&amp;gt; $unlike_count}, 
                     {'id' =&amp;gt; $article_id} );
}

if( $self-&amp;gt;signed_cookie( $like_cookie_name ) ){
    --$like_count;
    $self-&amp;gt;db-&amp;gt;update( 'posts', 
                     {'liked' =&amp;gt; $like_count}, 
                     {'id' =&amp;gt; $article_id} );
    $self-&amp;gt;signed_cookie( $like_cookie_name =&amp;gt; '', {expires =&amp;gt; 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-&amp;gt;voting( $self, 
                 'unlike_user'.$user_id.'_'.$title_alias.'_'.$article_id, 
                 $title_alias.'_'.$article_id );

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

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Template &lt;strong&gt;voting/vote.html.ep&lt;/strong&gt;&lt;br&gt;
Use it for both rendering &lt;code&gt;'like'&lt;/code&gt; and &lt;code&gt;'dislike'&lt;/code&gt; actions&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;%# voting/vote.html.ep

&amp;lt;button type="button" name="&amp;lt;%= $like_btn_name %&amp;gt;" 
onclick="Voting('&amp;lt;%= $section_ident %&amp;gt;', 
                   '&amp;lt;%= $id %&amp;gt;', 
                   '&amp;lt;%= $like_id %&amp;gt;', 
                   '&amp;lt;%= $client_id %&amp;gt;'); 
                   this.disabled='disabled';" 
                   id="chevron_stl"&amp;lt;%= $status %&amp;gt;&amp;gt;
&amp;lt;span id="&amp;lt;%= $section_ident.'_'.$client_id.'-up' %&amp;gt;" 
class="glyphicon glyphicon-chevron-up" aria-hidden="true"&amp;gt;&amp;lt;/span&amp;gt;
&amp;lt;/button&amp;gt;
&amp;lt;span id="&amp;lt;%= $like_id %&amp;gt;" class="like_unlike"&amp;gt;&amp;lt;%= $liked_cnt %&amp;gt;&amp;lt;/span&amp;gt;

&amp;lt;button type="button" name="&amp;lt;%= $unlike_btn_name %&amp;gt;" 
onclick="Voting('&amp;lt;%= $section_ident %&amp;gt;', 
                     '&amp;lt;%= $id %&amp;gt;', 
                     '&amp;lt;%= $unlike_id %&amp;gt;', 
                     '&amp;lt;%= $client_id %&amp;gt;'); 
                     this.disabled='disabled';" 
                     id="chevron_stl"&amp;lt;%= $status %&amp;gt;&amp;gt;
&amp;lt;span id="&amp;lt;%= $section_ident.'_'.$client_id.'-down' %&amp;gt;" 
class="glyphicon glyphicon-chevron-down" aria-hidden="true"&amp;gt;&amp;lt;/span&amp;gt;
&amp;lt;/button&amp;gt;
&amp;lt;span id="&amp;lt;%= $unlike_id %&amp;gt;" class="like_unlike"&amp;gt;&amp;lt;%= $unliked_cnt %&amp;gt;&amp;lt;/span&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;A simplified like/dislike system implemented in my project &lt;strong&gt;MornCat CMS&lt;/strong&gt; at &lt;a href="https://github.com/arslonga/my_blog"&gt;https://github.com/arslonga/my_blog&lt;/a&gt; &lt;/p&gt;

</description>
      <category>mojolicious</category>
      <category>perl</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
