Notes from a Newbie 12: Create and View 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.

Create Comments

Recall that we already have a table in our database that will allow us to create and view comments:

yardbird.sql:

CREATE TABLE blog_comment (
  id      INT UNSIGNED NOT NULL AUTO_INCREMENT,
  blogid  INT UNSIGNED NOT NULL,
  -- userid of the commenter (NOT THE userid OF THE BLOG ENTRY)
  userid  INT UNSIGNED NOT NULL,
  content TEXT NOT NULL,
  created TIMESTAMP NOT NULL,
  FOREIGN KEY (blogid) REFERENCES blog(id) ON DELETE CASCADE ON UPDATE CASCADE,
  FOREIGN KEY (userid) REFERENCES `user`(id) ON DELETE CASCADE ON UPDATE CASCADE,
  PRIMARY KEY (id) 
) ENGINE=InnoDB;

We will create comments with an HTML::FormHandler form, much like we created new members. You may want to review Notes from a Newbie 10: Authentication/Authorization and these files you already created:

  • Yardbird/lib/Yardbird/Controller/Member.pm
  • Yardbird/root/src/member/create.tt
  • Yardbird/lib/Yardbird/Form/User.pm

Create HTML::FormHandler Form

Let's start by creating the form. Recall that Catalyst provides no helper script to create the form, you create it yourself:

Yardbird/lib/Yardbird/Form/BlogComment.pm:

package Yardbird::Form::BlogComment;
use HTML::FormHandler::Moose;
extends 'HTML::FormHandler::Model::DBIC';

has '+item_class' => ( default => 'BlogComment' );

has_field 'content' => (
  type => 'TextArea',
  cols => 88,
  rows => 10,
  required => 1,
  do_label => 0,
  tags => { wrapper_tag => 'p' },
);

has_field 'submit' => (
  type => 'Submit',
  value => 'Submit',
);

no HTML::FormHandler::Moose;
1;

Edit Controller

We need to setup the Blog/Entries.pm controller to use the form by adding this to it:

use Yardbird::Form::BlogComment;

We will also need to add this to it:

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

After adding the above code you should have this:

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 }
);

Then we can use the form in our index action with this code:

# 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;
}

After making the above change you should have this:

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

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)]);

  # 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;
  }
}

Compare your controller action for creating new members to the one you just edited to create comments:

Yardbird/lib/Yardbird/Controller/Member.pm:

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

  $c->stash(template => 'member/create.tt', form => $self->form );

  # Validate and insert data into database
  return unless $self->form->process(
    item_id => $user_id,
    params => $c->req->parameters,
    schema => $c->model('DB')->schema
  );

  # Form validated, return to home
  $c->res->redirect($c->uri_for_action('/index'));
}

Unlike when creating new members, we only allow logged-in users to add comments.

Notice also that we create a new row object and pass it to the process method, rather than passing a row id as we did when creating new members. For this reason it is unnecessary for our Blog/Entries.pm index method to pass the schema to the process method.

Edit Template

Since our form is so simple, we can display it to appear as we wish with much less work than was needed to display our form to create new members.

This is what we now do to display content:

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

<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>
</div>

Add the following below where we display blog_entry.content:

<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 %]

After adding the above code you should have this:

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">
    <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>
    <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 %]

Run the application and view one of the blog entries (by clicking on it's title in the content area) and you should now be able to create a comment:

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

When not logged-in you should have options to login to comment or become a member:

yfc12a1.png

When logged-in you should see your new form and be able to submit a comment:

yfc12a2.png

We haven't added the feature to view comments yet, but you can use AutoCRUD to do so by giving your browser the correct URI:

http://0.0.0.0:3000/autocrud

Select the "Blog Comment" table and you should see the new comment you created:

yfc12a3.png

View Comments

Create ResultSet Class

We are going to need some ResultSet methods to search for comments, so I'll give them all to you at once. You may want to go back to Notes from a Newbie Experiment 02: DBIx::Class and review how we use ResultSets. You will need to create the following file:

Yardbird/lib/Yardbird/Schema/ResultSet/BlogComment.pm:

package Yardbird::Schema::ResultSet::BlogComment;
use strict;
use warnings;
use base 'DBIx::Class::ResultSet';

sub specific_comment {
  my ($self, $id) = @_;

  return $self->search({id => $id});
} 

sub specific_blog {
  my ($self, $bID) = @_;

  return $self->search({blogid => $bID});
} 

sub all_user {
  my ($self, $uID) = @_;

  return $self->search({userid => $uID}, {order_by => {-desc => ['id']}});
} 

1;

Edit Blog.pm Controller

We need to put a ResultSet in the stash for our template:

$c->stash(blog_comments => $c->model('DB::BlogComment'));

After adding the above code you should have this:

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

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

  $c->stash(blog_entries => [$c->model('DB::Blog')->most_recent]);
  $c->stash(blog_comments => $c->model('DB::BlogComment')); 

  my @members;
  for my $user ($c->model('DB::User')->all) {
    my $blog_entry = $c->model('DB::Blog')->all_user($user->id)->first;
    push @members, { name => $user->name, bid => $blog_entry->id } if $blog_entry; 
  }
  my @sorted_members = sort { "\L$a->{name}" cmp "\L$b->{name}" } @members; 
  $c->stash(sorted_members => [@sorted_members]);

  $c->detach($c->view("TT")); 
}

Edit blog/index.tt Template

This is what we now do to display content:

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

<div id="content_with_sidebar">
  [% FOREACH blog_entry IN blog_entries %]
    <p class="content_title" id="[% 'bid' _ blog_entry.id %]"><a href="[% c.uri_for_action('/blog/entries/index', blog_entry.id) %]">[% blog_entry.title %]</a></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>
  [% END %]
</div>

Add the following below where we display blog_entry.content:

[% 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 %]

After adding the above code you should have this:

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

[% INCLUDE header.tt title = 'The YARDBIRD Fan Club: Blog'; %]
<div id="header">
  <div id="header_title">The YARDBIRD Fan Club</div>
  <div id="header_subtitle">Celebrating the NFL Rookie Sensation!</div>
</div>

<div id="content_wrapper">
  <div id="sidebar_wrapper">

    <div class="sidebar_item">
      <div class="sidebar_item_content">
        <p><strong>Recent Blog Entries</strong></p>
        [% FOREACH blog_entry IN blog_entries %]
          <p class="sidebar_item_title"><a href="[% c.uri_for_action('/blog/index') _ '#bid' _ blog_entry.id %]">[% blog_entry.title %]</a></p>
          <p class="sidebar_item_subtitle">By <strong>[% blog_entry.userid.name %]</strong></p> 
          <p class="sidebar_item_sub_subtitle">[% blog_entry.created.strftime('%B %e, %Y %l:%M %p') %]</p> 
        [% END %]
      </div>
    </div>

    <div class="sidebar_item">
      <div class="sidebar_item_content">
        <p><strong>Member Blogs</strong></p>
        [% FOREACH member IN sorted_members %]
          <p class="sidebar_item_title"><a href="[% c.uri_for_action('/blog/entries/index', member.bid) %]">[% member.name %]</a></p>
        [% END %]
      </div>
    </div>

  </div>

  <div id="content_with_sidebar">
    [% FOREACH blog_entry IN blog_entries %]
      <p class="content_title" id="[% 'bid' _ blog_entry.id %]"><a href="[% c.uri_for_action('/blog/entries/index', blog_entry.id) %]">[% blog_entry.title %]</a></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 %]

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

[% INCLUDE footer.tt %]

Edit Stylesheet

Our template needs a few new styles added to our stylesheet:

Yardbird/root/static/css/main.css:

.bottom_of_content {
  border-bottom: 2px solid #87ceeb;
  margin-top: 1.5em;
  margin-bottom: 1.3em;
}

.top_of_comments {
  margin-top: 1.3em;
  border-bottom: 2px solid #87ceeb;
  margin-bottom: -0.5em;
}

.between_comments {
  margin-top: -0.6em;
  border-bottom: 2px solid #87ceeb;
  margin-bottom: -0.5em;
}

.bottom_of_comments {
  border-bottom: 2px solid #87ceeb;
  margin-top: -0.6em;
  margin-bottom: 1.3em;
}

.comment_content {
  margin-bottom: 1em;
}

.comment_subtitle {
  font-size: 0.7em;
  color: #004080; 
  margin-top: 1.2em;
}

Run the application and go to the Blog page and you should see decent looking comments, with horizontal blue lines separating blog entries and comments:

yfc12b1.png

Edit Blog/Entries.pm Controller

Again like we did above, we need to put a ResultSet in the stash for our template:

$c->stash(blog_comments => $c->model('DB::BlogComment'));

After adding the above code you should have this:

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

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'));

  # 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;
  }
}

Edit blog/entries/index.tt Template

This is what we now do to display content:

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

<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>
  <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>

Again, similar to what we did above, we add the following between where we display blog_entry.content and "Leave a comment":

[% 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 %]

After adding the above code you should have this:

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">
    <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 %]

Run the application and you should now be able to see comments in the blog/entries pages:

yfc12b2.png

Summary

Yardbird/lib/Yardbird/Controller/Blog.pm

package Yardbird::Controller::Blog;
use Moose;
use namespace::autoclean;

BEGIN { extends 'Catalyst::Controller'; }

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

  $c->stash(blog_entries => [$c->model('DB::Blog')->most_recent]);
  $c->stash(blog_comments => $c->model('DB::BlogComment')); 

  my @members;
  for my $user ($c->model('DB::User')->all) {
    my $blog_entry = $c->model('DB::Blog')->all_user($user->id)->first;
    push @members, { name => $user->name, bid => $blog_entry->id } if $blog_entry; 
  }
  my @sorted_members = sort { "\L$a->{name}" cmp "\L$b->{name}" } @members; 
  $c->stash(sorted_members => [@sorted_members]);

  $c->detach($c->view("TT")); 
}

__PACKAGE__->meta->make_immutable;

1;

Yardbird/root/src/blog/index.tt

[% INCLUDE header.tt title = 'The YARDBIRD Fan Club: Blog'; %]
<div id="header">
  <div id="header_title">The YARDBIRD Fan Club</div>
  <div id="header_subtitle">Celebrating the NFL Rookie Sensation!</div>
</div>

<div id="content_wrapper">
  <div id="sidebar_wrapper">

    <div class="sidebar_item">
      <div class="sidebar_item_content">
        <p><strong>Recent Blog Entries</strong></p>
        [% FOREACH blog_entry IN blog_entries %]
          <p class="sidebar_item_title"><a href="[% c.uri_for_action('/blog/index') _ '#bid' _ blog_entry.id %]">[% blog_entry.title %]</a></p>
          <p class="sidebar_item_subtitle">By <strong>[% blog_entry.userid.name %]</strong></p> 
          <p class="sidebar_item_sub_subtitle">[% blog_entry.created.strftime('%B %e, %Y %l:%M %p') %]</p> 
        [% END %]
      </div>
    </div>

    <div class="sidebar_item">
      <div class="sidebar_item_content">
        <p><strong>Member Blogs</strong></p>
        [% FOREACH member IN sorted_members %]
          <p class="sidebar_item_title"><a href="[% c.uri_for_action('/blog/entries/index', member.bid) %]">[% member.name %]</a></p>
        [% END %]
      </div>
    </div>

  </div>

  <div id="content_with_sidebar">
    [% FOREACH blog_entry IN blog_entries %]
      <p class="content_title" id="[% 'bid' _ blog_entry.id %]"><a href="[% c.uri_for_action('/blog/entries/index', blog_entry.id) %]">[% blog_entry.title %]</a></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 %]

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

[% INCLUDE footer.tt %]

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'));

  # 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;
  }
}

__PACKAGE__->meta->make_immutable;

1;

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">
    <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 %]

Yardbird/root/static/css/main.css

#wrapper {
  width: 960px;
  margin-top: 0px;
  margin-right: auto;
  margin-left: auto;
}

#header {
  height: 100px;
  margin-left: 20px;
  background: #004060;
  background-image: url(../images/bird/bird.gif);
  background-position: left bottom;
  background-repeat: no-repeat; 
}

#header_title {
  margin-left: 70px;
  padding-top: 22px;
  color: #87ceeb;
  font-size: 2.2em;
  font-weight: bold;
}

#header_subtitle {
  margin-left: 70px;
  margin-top: 2px;
  color: #87ceeb;
  font-size: 0.9em;
}

#footer {
  padding-top: 0.4em;
  padding-bottom: 1.5em;
  background: #004060;
  color: #FFFFFF;
  font-size: 0.9em;
}

body {
  font-family: arial;
  margin-top: 0px;
  background: #004060;
}

#main_menu {
  border-left: 1px solid #FFFFFF;
  border-right: 1px solid #FFFFFF;
  border-bottom: 1px solid #FFFFFF;
  border-bottom-left-radius: 0.3em; 
  border-bottom-right-radius: 0.3em; 
  background: #003050;
  font-size: 0.9em;

  color: #FFFFFF;
}

#main_menu ul {
  padding-top: 0.275em;
  padding-bottom: 0.18em;
  margin: auto; /* center horizontally */
  list-style: none; 

  /* Menu background/border has no vertical height without
  this, I don't know why it works. */
  overflow: hidden;
}

#main_menu a {
  color: #87ceeb;
  text-decoration: none; 
}

#main_menu a:hover {
  color: #FFFFFF;
  text-decoration: underline; 
}

#main_menu_left li {
  padding-right: 5em;
  /*padding-left: 0em;*/
  margin-left: -1.5em;
  float: left;
  /*width: 1em;*/
}

#main_menu_right li {
  /*padding-right: 0em;*/
  /*padding-left: 40px;*/
  margin-right: 1em;
  float: right;
  /*width: 12.5em;*/
}

#content_wrapper {
  border: 1px solid #000000;
  border-radius: 0.3em; 
  background: #FFFFFF;
  /*background: #A4D3EE;*/

  /* Expand content area when sidebar is vertically longer than content. */
  overflow: auto; 
}

/* formhandler generates html with id selectors named 'content' for it's
 * 'content' fields in the blog and blogcomments forms. For this reason I
 * renamed the 'content' id style 'content_with_sidebar'. */

#content_with_sidebar {
  margin-right: 225px;
  margin-left: 10px;
  margin-top: 0px;
  margin-bottom: 20px;
}

#content_without_sidebar {
  margin-right: 10px;
  margin-left: 10px;
  margin-top: 0px;
  margin-bottom: 20px;
}

#sidebar_wrapper {
  float: right;
}

.sidebar_item {
  width: 200px;
  margin-right: 10px;
  margin-top: 13px;
  margin-bottom: 10px;
  border: 1px solid #000000;
  border-radius: 0.3em; 
  /*background: #FFFFFF;*/
}

.sidebar_item_content {
  padding-left: 12px;
  padding-right: 12px;
}

#user_form_button {
  margin-left: 140px;
}

#user_form .label {
  float: left;
  width: 140px;
} 

#user_form_textarea {
  margin-top: -1.15em;
  margin-left: 140px;
}

.content_title {
  font-size: 1.4em;
  font-weight: bold;
  margin-top: 0.3em;
}

.content_subtitle {
  font-size: 0.8em;
  margin-top: -1.6em;
}

.content_title a {
  color: #004080;
  text-decoration: none; 
}

.content_title a:hover {
  color: #004080;
  text-decoration: underline; 
}

.sidebar_item_title {
  font-size: 0.9em;
  font-weight: bold;
}

.sidebar_item_title a {
  color: #004080;
  text-decoration: none; 
}

.sidebar_item_title a:hover {
  color: #004080;
  text-decoration: underline; 
}

.sidebar_item_subtitle {
  font-size: 0.65em;
  margin-top: -1.5em;
}

.sidebar_item_sub_subtitle {
  font-size: 0.65em;
  margin-top: -1.0em;
}

.bottom_of_content {
  border-bottom: 2px solid #87ceeb;
  margin-top: 1.5em;
  margin-bottom: 1.3em;
}

.top_of_comments {
  margin-top: 1.3em;
  border-bottom: 2px solid #87ceeb;
  margin-bottom: -0.5em;
}

.between_comments {
  margin-top: -0.6em;
  border-bottom: 2px solid #87ceeb;
  margin-bottom: -0.5em;
}

.bottom_of_comments {
  border-bottom: 2px solid #87ceeb;
  margin-top: -0.6em;
  margin-bottom: 1.3em;
}

.comment_content {
  margin-bottom: 1em;
}

.comment_subtitle {
  font-size: 0.7em;
  color: #004080; 
  margin-top: 1.2em;
}

Yardbird/lib/Yardbird/Form/BlogComment.pm

package Yardbird::Form::BlogComment;
use HTML::FormHandler::Moose;
extends 'HTML::FormHandler::Model::DBIC';

has '+item_class' => ( default => 'BlogComment' );

has_field 'content' => (
  type => 'TextArea',
  cols => 88,
  rows => 10,
  required => 1,
  do_label => 0,
  tags => { wrapper_tag => 'p' },
);

has_field 'submit' => (
  type => 'Submit',
  value => 'Submit',
);

no HTML::FormHandler::Moose;
1;

Yardbird/lib/Yardbird/Schema/ResultSet/BlogComment.pm

package Yardbird::Schema::ResultSet::BlogComment;
use strict;
use warnings;
use base 'DBIx::Class::ResultSet';

sub specific_comment {
  my ($self, $id) = @_;

  return $self->search({id => $id});
} 

sub specific_blog {
  my ($self, $bID) = @_;

  return $self->search({blogid => $bID});
} 

sub all_user {
  my ($self, $uID) = @_;

  return $self->search({userid => $uID}, {order_by => {-desc => ['id']}});
} 

1;

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.