Notes from a Newbie 14: Edit and Delete Comments

Notes from a Newbie document the creation and deployment of yardbirdfanclub.org with Perl Catalyst on shared hosting. They are intended for a Perl Catalyst Newbie who would like to study the creation and deployment of a simple Perl Catalyst application.

Edit and Delete Comments

Before going any further you may want to go to yardbirdfanclub.org to see how to edit and delete blog comments in the application. Doing so may make it easier to understand what we will be doing.

We will create a Blog/Entries.pm controller action to both edit and delete blog comments:

Yardbird/lib/Yardbird/Controller/Blog/Entries.pm:

sub edit_comments :Path('/blog/comments/edit') :Args(1) {
  my ($self, $c, $uID) = @_;

  # Detach if user is not logged-in, or is trying to delete somebody else's comments.
  $c->detach('/unauthorized_action') if !$c->user_exists;
  $c->detach('/unauthorized_action') if ($c->user->id != $uID);

  # blog/entries/index.tt only makes the "Edit/Delete Comments" link available if user
  # has comments. The only way a user would get this far into this subroutine if they
  # had no comments would be if they entered a malicious uri, so detach if that occurs.
  my @rows = $c->model('DB::BlogComment')->all_user($uID);
  $c->detach('/unauthorized_action') if !@rows;

  $c->stash(blog_comments => [@rows]);

  my $submit = $c->req->params->{submit};
  if ($submit eq "Yes") {
    my $rs = $c->model('DB::BlogComment')->all_user($uID);
    my $cnt = 0;
    for my $row (@rows) {
      if ($c->req->params->{'checkbox'.$cnt} eq "Yes") {
        $rs->specific_comment($row->id)->delete_all;
      }
      else {
        $row->content($c->req->params->{'content'.$cnt});
        $row->update;
      }
      $cnt++;
    }
  }

  if ($submit eq "Yes" || $submit eq "No") {
    # Get the most recent blog entry if one exists and display it, else go to the /blog/index page.
    my $row = $c->model('DB::Blog')->all_user($uID)->first; 
    if ($row) {
      $c->res->redirect($c->uri_for_action('/blog/entries/index', $row->id));
    }
    else {
      $c->res->redirect($c->uri_for_action('/blog/index'));
    }
  }
}

The Path attribute in my controller action above allows me to map it to the explicit URI: '/blog/comments/edit'. I do this because I want the URI for this action to be consistent with the URI's to create, edit and delete blog entries, i.e. '/blog/entries/create', '/blog/entries/edit' and '/blog/entries/delete'. In other words, I don't want the path to this action to be '/blog/edit_comments', I want it to be '/blog/comments/edit'.

See:

  • Catalyst::Manual::Tutorial::03_MoreCatalystBasics

Create the corresponding template:

Yardbird/root/src/blog/entries/edit_comments.tt:

[% INCLUDE header.tt title = 'The YARDBIRD Fan Club: '_ blog_comments.0.userid.name %]
<div id="header">
  <div id="header_title">[% blog_comments.0.userid.name %]</div>
  <div id="header_subtitle">The YARDBIRD Fan Club</div>
</div>

<div id="content_wrapper">
  <div id="content_without_sidebar">
  <h3>Edit/Delete Comments</h3>

  <form method="post" action="[% c.uri_for_action('/blog/entries/edit_comments', blog_comments.0.userid.id) %]">
    <table>

      [% cnt = 0 %]
      [% FOREACH blog_comment IN blog_comments %]
        <tr>
          <td><textarea name="[% 'content' _ cnt %]" rows="10" cols="88" wrap="virtual">[% blog_comment.content | html %]</textarea></td>
          <td><input type="checkbox" name="[% 'checkbox' _ cnt %]" value="Yes"/>Delete</td>
        </tr>
        [% cnt = cnt + 1 %]
      [% END %]

      <tr>
        <td><p>Do you really want to edit/delete these comments?</p></td>
      </tr>
      <tr>
        <td><input type="submit" name="submit" value="Yes" /> <input type="submit" name="submit" value="No" /></td>
      </tr>

    </table>
  </form> 

  </div>
</div>

[% INCLUDE footer.tt %]

The link to edit and delete comments should appear in the blog/entries sidebar menu:

1) When a logged-in user with comments is viewing their own blog entries in the blog/entries page.

2) When a logged-in user with comments has no blog entries of their own and is viewing any blog/entries page.

If the above explanation is confusing, perhaps reviewing Notes from a Newbie 13: Create, Edit and Delete Blog Entries and using yardbirdfanclub.org may help clear this up.

This is the link we will put in our sidebar menu:

[% blog_comments_rs = blog_comments.all_user(c.user.id) %]
[% IF blog_comments_rs %]
  <p class="sidebar_item_title"><a href="[% c.uri_for_action('/blog/entries/edit_comments', c.user.id) %]">Edit/Delete Comments</a></p>
[% END %]

This is what we now have in the sidebar menu:

Yardbird/root/src/blog/entries/index.tt:

[% IF (c.user_exists && (c.user.id == blog_entry.userid.id)) %]
  <div class="sidebar_item">
    <div class="sidebar_item_content">
      <p class="sidebar_item_title"><a href="[% c.uri_for_action('/blog/entries/create') %]">Create New Blog Entry</a></p>
      <p class="sidebar_item_title"><a href="[% c.uri_for_action('/blog/entries/edit', blog_entry.id) %]">Edit Blog Entry</a></p>
      <p class="sidebar_item_title"><a href="[% c.uri_for_action('/blog/entries/delete', blog_entry.id) _ '#bottom' %]">Delete Blog Entry</a></p> 
    </div>
  </div>
[% ELSIF (c.user_exists && user_has_no_blog_entries) %]
  <div class="sidebar_item">
    <div class="sidebar_item_content">
      <p class="sidebar_item_title"><a href="[% c.uri_for_action('/blog/entries/create') %]">Create First Blog Entry</a></p>
    </div>
  </div>
[% END %]

This is what we should have after adding the links:

[% IF (c.user_exists && (c.user.id == blog_entry.userid.id)) %]
  <div class="sidebar_item">
    <div class="sidebar_item_content">
      <p class="sidebar_item_title"><a href="[% c.uri_for_action('/blog/entries/create') %]">Create New Blog Entry</a></p>
      <p class="sidebar_item_title"><a href="[% c.uri_for_action('/blog/entries/edit', blog_entry.id) %]">Edit Blog Entry</a></p>
      <p class="sidebar_item_title"><a href="[% c.uri_for_action('/blog/entries/delete', blog_entry.id) _ '#bottom' %]">Delete Blog Entry</a></p> 

      [% blog_comments_rs = blog_comments.all_user(c.user.id) %]
      [% IF blog_comments_rs %]
        <p class="sidebar_item_title"><a href="[% c.uri_for_action('/blog/entries/edit_comments', c.user.id) %]">Edit/Delete Comments</a></p>
      [% END %]

    </div>
  </div>
[% ELSIF (c.user_exists && user_has_no_blog_entries) %]
  <div class="sidebar_item">
    <div class="sidebar_item_content">
      <p class="sidebar_item_title"><a href="[% c.uri_for_action('/blog/entries/create') %]">Create First Blog Entry</a></p>

      [% blog_comments_rs = blog_comments.all_user(c.user.id) %]
      [% IF blog_comments_rs %]
        <p class="sidebar_item_title"><a href="[% c.uri_for_action('/blog/entries/edit_comments', c.user.id) %]">Edit/Delete Comments</a></p>
      [% END %]

    </div>
  </div>
[% END %]

Run the application:

Yardbird]$ script/yardbird_server.pl -d -r

Logged-in users with comments should be able to edit and delete their comments when viewing their own blog entries in the blog/entries page:

yfc14a1.png

yfc14a2.png

Logged-in users with comments that have no blog entries of their own should be able to edit and delete their comments when viewing any blog/entries page:

yfc14a3.png

Summary

Yardbird/lib/Yardbird/Controller/Blog/Entries.pm

package Yardbird::Controller::Blog::Entries;
use Moose;
use namespace::autoclean;
use Yardbird::Form::BlogComment;

BEGIN { extends 'Catalyst::Controller'; }

has 'comment_form' => (
  isa => 'Yardbird::Form::BlogComment',
  is => 'rw',
  lazy => 1,
  default => sub { Yardbird::Form::BlogComment->new }
); 

sub index :Path :Args(1) {
  my ($self, $c, $bID) = @_;

  my $row = $c->model('DB::Blog')->specific($bID)->first;
  $c->stash(blog_entry => $row);
  $c->stash(blog_entries => [$c->model('DB::Blog')->all_user($row->userid->id)]);
  $c->stash(blog_comments => $c->model('DB::BlogComment'));

  # Determine if logged-in user has created any blog entries and put
  # flag in stash. The template uses this to allow user to create
  # first blog entry if they have none.
  if ($c->user_exists) {
    if ($c->user->id != $row->userid->id) {
      $row = $c->model('DB::Blog')->all_user($c->user->id)->first;
      if ($row) {
        $c->stash(user_has_no_blog_entries => 0);
      }
      else {
        $c->stash(user_has_no_blog_entries => 1);
      }
    }
  }

  # Don't allow comments to be added if user is not logged in.
  if ($c->user_exists) {
    $c->stash(template => 'blog/entries/index.tt', form => $self->comment_form);

    $row = $c->model('DB::BlogComment')->new_result({});
    $row->blogid($bID);
    $row->userid($c->user->id);
    $row->created(DateTime->now);

    # Validate and add database row
    return unless $self->comment_form->process(
      item => $row,
      params => $c->req->params,
    );

    # Form validated, refresh the page.
    $c->res->redirect($c->uri_for_action('/blog/entries/index', $bID));
  }
  else {
    return;
  }
}

sub create :Local :Args(0) {
  my ($self, $c) = @_;

  # Detach if user is not logged-in.
  $c->detach('/unauthorized_action') if !$c->user_exists;

  my $row;

  my $submit = $c->req->params->{submit};
  if ($submit eq "Yes") {
    $row = $c->model('DB::Blog')->new_result({}); 
    $row->title($c->req->params->{title});
    $row->content($c->req->params->{content});
    $row->userid($c->user->id);
    $row->created(DateTime->now);
    return if !$row->title || !$row->content;
    $row->insert;
  }

  if ($submit eq "Yes" || $submit eq "No") {
    # Get the most recent blog entry if one exists and display it, else go to the /blog/index page.
    $row = $c->model('DB::Blog')->all_user($c->user->id)->first; 
    if ($row) {
      $c->res->redirect($c->uri_for_action('/blog/entries/index', $row->id));
    }
    else {
      $c->res->redirect($c->uri_for_action('/blog/index'));
    }
  }
}

sub edit :Local :Args(1) {
  my ($self, $c, $bID) = @_;

  # Detach if user is not logged-in.
  $c->detach('/unauthorized_action') if !$c->user_exists;

  # Prepare to edit blog entry.
  # Detach if user is attempting to edit an entry that doesn't belong to user.
  my $row = $c->model('DB::Blog')->specific($bID)->first; 
  $c->detach('/unauthorized_action') if ($c->user->id != $row->userid->id);

  $c->stash(blog_entry => $row);

  my $submit = $c->req->params->{submit};
  if ($submit eq "Yes") {
    $row->title($c->req->params->{title});
    $row->content($c->req->params->{content});
    return if !$row->title || !$row->content;
    $row->update;
  }

  if ($submit eq "Yes" || $submit eq "No") {
    # Get the most recent blog entry if one exists and display it, else go to the /blog/index page.
    $row = $c->model('DB::Blog')->all_user($c->user->id)->first; 
    if ($row) {
      $c->res->redirect($c->uri_for_action('/blog/entries/index', $row->id));
    }
    else {
      $c->res->redirect($c->uri_for_action('/blog/index'));
    }
  }
}

sub delete :Local :Args(1) {
  my ($self, $c, $bID) = @_;

  # Detach if user is not logged-in.
  $c->detach('/unauthorized_action') if !$c->user_exists;

  # Detach if user attempts to delete an entry that doesn't belong to them.
  my $row = $c->model('DB::Blog')->specific($bID)->first; 
  $c->detach('/unauthorized_action') if ($c->user->id != $row->userid->id);

  $c->stash(blog_entry => $row);
  $c->stash(blog_entries => [$c->model('DB::Blog')->all_user($c->user->id)]);
  $c->stash(blog_comments => $c->model('DB::BlogComment'));

  my $submit = $c->req->params->{submit};
  if ($submit eq 'Yes') {
    $row->delete; 

    # Get the most recent blog entry if one exists and display it, else go to the /blog/index page.
    $row = $c->model('DB::Blog')->all_user($c->user->id)->first; 
    if ($row) {
      $c->res->redirect($c->uri_for_action('/blog/entries/index', $row->id));
    }
    else {
      $c->res->redirect($c->uri_for_action('/blog/index'));
    }
  }
  elsif ($submit eq 'No') {
    $c->res->redirect($c->uri_for_action('/blog/entries/index', $bID));
  }
}

sub edit_comments :Path('/blog/comments/edit') :Args(1) {
  my ($self, $c, $uID) = @_;

  # Detach if user is not logged-in, or is trying to delete somebody else's comments.
  $c->detach('/unauthorized_action') if !$c->user_exists;
  $c->detach('/unauthorized_action') if ($c->user->id != $uID);

  # blog/entries/index.tt only makes the "Edit/Delete Comments" link available if user
  # has comments. The only way a user would get this far into this subroutine if they
  # had no comments would be if they entered a malicious uri, so detach if that occurs.
  my @rows = $c->model('DB::BlogComment')->all_user($uID);
  $c->detach('/unauthorized_action') if !@rows;

  $c->stash(blog_comments => [@rows]);

  my $submit = $c->req->params->{submit};
  if ($submit eq "Yes") {
    my $rs = $c->model('DB::BlogComment')->all_user($uID);
    my $cnt = 0;
    for my $row (@rows) {
      if ($c->req->params->{'checkbox'.$cnt} eq "Yes") {
        $rs->specific_comment($row->id)->delete_all;
      }
      else {
        $row->content($c->req->params->{'content'.$cnt});
        $row->update;
      }
      $cnt++;
    }
  }

  if ($submit eq "Yes" || $submit eq "No") {
    # Get the most recent blog entry if one exists and display it, else go to the /blog/index page.
    my $row = $c->model('DB::Blog')->all_user($uID)->first; 
    if ($row) {
      $c->res->redirect($c->uri_for_action('/blog/entries/index', $row->id));
    }
    else {
      $c->res->redirect($c->uri_for_action('/blog/index'));
    }
  }
}

__PACKAGE__->meta->make_immutable;

1;

Yardbird/root/src/blog/entries/edit_comments.tt

[% INCLUDE header.tt title = 'The YARDBIRD Fan Club: '_ blog_comments.0.userid.name %]
<div id="header">
  <div id="header_title">[% blog_comments.0.userid.name %]</div>
  <div id="header_subtitle">The YARDBIRD Fan Club</div>
</div>

<div id="content_wrapper">
  <div id="content_without_sidebar">
  <h3>Edit/Delete Comments</h3>

  <form method="post" action="[% c.uri_for_action('/blog/entries/edit_comments', blog_comments.0.userid.id) %]">
    <table>

      [% cnt = 0 %]
      [% FOREACH blog_comment IN blog_comments %]
        <tr>
          <td><textarea name="[% 'content' _ cnt %]" rows="10" cols="88" wrap="virtual">[% blog_comment.content | html %]</textarea></td>
          <td><input type="checkbox" name="[% 'checkbox' _ cnt %]" value="Yes"/>Delete</td>
        </tr>
        [% cnt = cnt + 1 %]
      [% END %]

      <tr>
        <td><p>Do you really want to edit/delete these comments?</p></td>
      </tr>
      <tr>
        <td><input type="submit" name="submit" value="Yes" /> <input type="submit" name="submit" value="No" /></td>
      </tr>

    </table>
  </form> 

  </div>
</div>

[% INCLUDE footer.tt %]

Yardbird/root/src/blog/entries/index.tt

[% INCLUDE header.tt title = 'The YARDBIRD Fan Club: '_ blog_entry.userid.name %]
<div id="header">
  <div id="header_title">[% blog_entry.userid.name %]</div>
  <div id="header_subtitle">The YARDBIRD Fan Club</div>
</div>

<div id="content_wrapper">
  <div id="sidebar_wrapper">
    [% IF (c.user_exists && (c.user.id == blog_entry.userid.id)) %]
      <div class="sidebar_item">
        <div class="sidebar_item_content">
          <p class="sidebar_item_title"><a href="[% c.uri_for_action('/blog/entries/create') %]">Create New Blog Entry</a></p>
          <p class="sidebar_item_title"><a href="[% c.uri_for_action('/blog/entries/edit', blog_entry.id) %]">Edit Blog Entry</a></p>
          <p class="sidebar_item_title"><a href="[% c.uri_for_action('/blog/entries/delete', blog_entry.id) _ '#bottom' %]">Delete Blog Entry</a></p> 

          [% blog_comments_rs = blog_comments.all_user(c.user.id) %]
          [% IF blog_comments_rs %]
            <p class="sidebar_item_title"><a href="[% c.uri_for_action('/blog/entries/edit_comments', c.user.id) %]">Edit/Delete Comments</a></p>
          [% END %]

        </div>
      </div>
    [% ELSIF (c.user_exists && user_has_no_blog_entries) %]
      <div class="sidebar_item">
        <div class="sidebar_item_content">
          <p class="sidebar_item_title"><a href="[% c.uri_for_action('/blog/entries/create') %]">Create First Blog Entry</a></p>

          [% blog_comments_rs = blog_comments.all_user(c.user.id) %]
          [% IF blog_comments_rs %]
            <p class="sidebar_item_title"><a href="[% c.uri_for_action('/blog/entries/edit_comments', c.user.id) %]">Edit/Delete Comments</a></p>
          [% END %]

        </div>
      </div>
    [% END %]

    <div class="sidebar_item">
      <div class="sidebar_item_content">
        [% FOREACH entry IN blog_entries %]
          <p class="sidebar_item_title"><a href="[% c.uri_for_action('/blog/entries/index', entry.id) %]">[% entry.title %]</a></p>
          <p class="sidebar_item_subtitle">[% entry.created.strftime('%B %e, %Y %l:%M %p') %]</p> 
        [% END %]
      </div>
    </div>
  </div>

  <div id="content_with_sidebar">
    <p class="content_title">[% blog_entry.title %]</p>
    <p class="content_subtitle">By <strong>[% blog_entry.userid.name %]</strong> on [% blog_entry.created.strftime('%B %e, %Y %l:%M %p') %]</p> 
    <p>[% blog_entry.content %]</p>

    [% blog_comments_rs = blog_comments.specific_blog(blog_entry.id) %]
    [% IF blog_comments_rs %]
      <div class="top_of_comments"></div>
      [% between_comments_flg = 0 %]
      [% FOREACH blog_comment IN blog_comments_rs %]
        [% IF between_comments_flg %]
          <div class="between_comments"></div>
        [% ELSE %]
          [% between_comments_flg = 1 %]
        [% END %]
        <p class="comment_subtitle">Comment by <strong>[% blog_comment.userid.name %]</strong> on [% blog_comment.created.strftime('%B %e, %Y %l:%M %p') %]</p> 
        <p class="comment_content">[% blog_comment.content %]</p>
      [% END %]
      <div class="bottom_of_comments"></div>
    [% ELSE %]
      <div class="bottom_of_content"></div>
    [% END %]

    <h3>Leave a comment</h3>
    [% IF c.user_exists %] 
      <p>(You may use HTML tags for style)</p> 
      [% form.render %] 
    [% ELSE %]
      <p><a href="[% c.uri_for_action('/login/index') %]">Login to comment.</a> | <a href="[% c.uri_for_action('/member/about') %]">Not a member?</a></p> 
    [% END %]
  </div>
</div>

[% INCLUDE footer.tt %]

Leave a comment

About j0e

user-pic I've been studying Catalyst when I can find time and will soon be applying for jobs. My experience is with embedded, realtime systems running DOS, Assembly Language and C. I live in Dover-Foxcroft, Maine but would welcome the opportunity to live anywhere I can work and develop my skills, especially overseas. I live by myself with an Australian Cattle Dog I call "Bird" for the great alto sax genius Charlie "Yardbird" Parker. I am looking forward to attending YAPC::NA in Austin, and hope to meet some of the people who have helped me at irc.perl.org.