Notes from a Newbie 13: Create, Edit and Delete Blog Entries

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.

User Interface

Currently, the only way to create new blog entries is with AutoCRUD. This is okay for development purposes, but not for a production application. We need to give users the ability to create, edit and delete blog entries without using AutoCRUD, in a manner consistent with the overall design of the application. How would you do this? Please go to yardbirdfanclub.org and see for yourself how I did so. I gave logged-in users the ability to create, edit and delete blog entries from the blog/entries page.

Implementation

There are two ways for users to reach the blog/entries page:

1) Select "Blog" from the main menu to go to the blog page. From the blog page, select a member's name from the "Member Blogs" sidebar menu. After doing so, that member's most recent blog entry will appear in the main content area of the blog/entries page.

2) Select "Blog" from the main menu to go to the blog page. From the blog page, click on a title of one of the blog entries in the main content area. After doing so, that blog entry will appear in the main content area of the blog/entries page.

When a logged-in user's blog entry is displayed in the blog/entries page, a sidebar menu will give them the ability to edit or delete it, or to create a new one. Recall that titles of any other blog entries they created will also appear in a sidebar menu. Selecting one of these will make it appear in the main content area. Any blog entry belonging to a logged-in user, displayed in the main content area of the blog/entries page, may be edited or deleted by that user.

After implementing these features I realized I had a problem. The sidebar menu to create a new blog entry only appears for users who already have one or more blog entries. Since new members don't have any, they wouldn't be able to create any blog entries.

The solution? Automatically generate a generic blog entry for each new member when their membership is created. This would have the added benefit of providing an example of how to use HTML tags for style, which could be helpful to users unfamiliar with how to use them.

But what about users who delete their automatically generated blog entry and have no other entries? After doing so, they would be unable to create new blog entries.

The solution? For users with no blog entries, provide a sidebar menu with an item to create a first blog entry, but on which page? On all blog/entries pages, regardless of who's entries appear on the page.

Reflection

I question the effectiveness of my design. So far none of the members of yardbirdfanclub.org have created, edited or deleted any of their own blog entries without my help. Whether the interface is too confusing, they don't have enough technical skill, or are not interested enough to do so - I don't know.

Personally, I like the interface, but I would like to know what others think of it.

Create New Blog Entry

Create Sidebar Menu

Let's begin by creating a sidebar menu in our blog/entries page. This may be a little confusing, so I will explain this again. There are two situations we are looking for:

1) Logged-in users whose blog entries are being displayed will see a menu giving them the option to create a new blog entry. Later we will add the ability to edit or delete the currently displayed one.

2) Logged-in users who don't have any blog entries will see a menu giving them the option to create their first one.

We begin by putting a flag in the stash for our blog/entries/index.tt template to use. This will be used to create the sidebar menu for logged-in users to create their first blog entry. We add this code to our Blog/Entries.pm controller index action:

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

After adding the above code we 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'));

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

If a user is logged-in and the blog entries displayed on the page are not theirs, we look to see if they have any blog entries and set the flag accordingly.

We create our new sidebar menu in the template by adding this code to 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>
    </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 %]

We put our new sidebar menu above our existing one displaying blog titles. There is a reason for this. Suppose the user creates 25 blog entries. If our new sidebar menu went below the one displaying 25 blog entry titles, the user would not see it until scrolling down to the bottom of the page.

After adding the above code we 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">
    [% 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>
        </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 %]

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

Create Controller Actions and Templates

unauthorized_action

We need to protect against users entering URI's into their browser's address bar that would give them access to features we do not want them to have. For example, we don't want non-members to create blog entries. We will enforce this by requiring user's to be logged-in to create blog entries, and detach to a generic "unauthorized_action" page when necessary.

See:

  • Catalyst::Manual::Tutorial::06_Authorization

We add the following method to our Root.pm controller:

Yardbird/lib/Yardbird/Controller/Root.pm:

sub unauthorized_action :Chained('/') :PathPart('unauthorized_action') :Args(0) {
  my ( $self, $c ) = @_;

  $c->stash(template => 'unauthorized_action.tt');
}

Create a template for our new unauthorized_action method:

Yardbird/root/src/unauthorized_action.tt:

[% INCLUDE header.tt title = 'The YARDBIRD Fan Club: Error'; %]
<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>

  <div id="content_with_sidebar">
    <h3>Error</h3>
    <p>You are not authorized to perform this action.</p>
  </div>
</div>

[% INCLUDE footer.tt %]

create

Finally, we add our action method to create new blog entries to our Blog/Entries.pm controller:

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

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

We create our corresponding template:

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

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

<div id="content_wrapper">
  <div id="content_without_sidebar">
  <h3>Create New Blog Entry</h3>

  <form method="post" action="[% c.uri_for_action('/blog/entries/create') %]">
    <table>

      <tr>
        <td>Title:</td>
      </tr>
      <tr>
        <td><textarea name="title" rows="1" cols="88" wrap="virtual"></textarea></td>
      </tr>
      <tr>
        <td><p></p></td>
      </tr>
      <tr>
        <td>Content:</td>
      </tr>
      <tr>
        <td><textarea name="content" rows="20" cols="88" wrap="virtual"></textarea></td>
      </tr>

      <tr>
        <td><p>Do you really want to create this new blog entry?</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 %]

Run the application:

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

You should now be able to login a member who already has blog entries and create new ones:

yfc13a1.png

yfc13a2.png

You should also be able to login a member who has no blog entries and create their first one:

yfc13a4.png

yfc13a5.png

Don't forget to test your new unauthorized_action routine by entering a URI into your browser's address bar to create a new blog entry without being logged-in:

yfc13a3.png

Edit Blog Entry

Notice in our new controller action we not only make sure the user is logged-in, we also ensure the entry they want to edit belongs to them:

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

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

Yardbird/root/src/blog/entries/edit.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="content_without_sidebar">
  <h3>Edit Blog Entry</h3>

  <form method="post" action="[% c.uri_for_action('/blog/entries/edit', blog_entry.id) %]">
    <table>

      <tr>
        <td>Title:</td>
      </tr>
      <tr>
        <td><textarea name="title" rows="1" cols="88" wrap="virtual">[% blog_entry.title | html %]</textarea></td>
      </tr>
      <tr>
        <td><p></p></td>
      </tr>
      <tr>
        <td>Content:</td>
      </tr>
      <tr>
        <td><textarea name="content" rows="20" cols="88" wrap="virtual">[% blog_entry.content | html %]</textarea></td>
      </tr>

      <tr>
        <td><p>Do you really want to submit these changes?</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 %]

Don't forget to add a link to our new edit action in the sidebar menu:

<p class="sidebar_item_title"><a href="[% c.uri_for_action('/blog/entries/edit', blog_entry.id) %]">Edit Blog Entry</a></p>

After adding the link you should have the following:

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

Run the application and you should now be able to edit blog entries:

yfc13b1.png

yfc13b2.png

Delete Blog Entry

Deleting entries will look a little different than creating or editing them. We display the entry that will be deleted much like we do when viewing them, with comments (but without the ability to create them) and a prompt to delete at the bottom of the page.

Because we created our blog_comment table with blogid as a foreign key that cascade deletes, all comments belonging to the blog entry are automatically deleted (without our code having to delete them) when the blog entry is deleted.

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

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

Yardbird/root/src/blog/entries/delete.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="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>
      <h3 id="bottom">Delete Blog Entry</h3>
      <p>Do you really want to delete this blog entry and corresponding comments?</p>
    [% ELSE %]
      <div class="bottom_of_content"></div>
      <h3 id="bottom">Delete Blog Entry</h3>
      <p>Do you really want to delete this blog entry?</p>
    [% END %]

    <form method="post" action="[% c.uri_for_action('/blog/entries/delete', blog_entry.id) %]">
      <table>
        <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 %]

Don't forget to add a link to our new delete action in the sidebar menu:

<p class="sidebar_item_title"><a href="[% c.uri_for_action('/blog/entries/delete', blog_entry.id) _ '#bottom' %]">Delete Blog Entry</a></p>

After adding the link you should have the following:

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

Notice that we create a "bottom" id in the delete.tt template which we target in our sidebar menu link. When pages with long entries to delete load, the delete prompt would not be visible otherwise.

Run the application and you should now be able to delete blog entries:

yfc13c1.png

yfc13c2.png

yfc13c3.png

yfc13c4.png

Auto-Generate Blog Entry

This is what we now have to create a new member:

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

We auto-generate a new member's first blog entry after validating the form and inserting their user info into the database, before returning home:

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 and data inserted into database,
  # now auto-generate new member's first blog entry.

  # Get new member info.
  my $row = $c->model('DB::User')->most_recent->first; 
  my $userid = $row->id; 
  my $name = $row->name; 

  # Create new member's first blog entry.
  $row = $c->model('DB::Blog')->new_result({}); 
  $row->userid($userid);
  $row->title("Welcome ".$name."!");
  $row->content(
"<h3>You Are Now A Member Of The YARDBIRD Fan Club!</h3>
<p>You may now:</p>
<ul>
<li>Post blog entries and comments via the Blog pages.</li>
<li>Optionally provide your contact and other information to other users via the Members pages.</li>
<li>View other member's contact information via the Members pages.</li>
</ul>
<h3>About This Blog Entry</h3>
<p>This blog entry was automatically created when you became a member of the YARDBIRD Fan Club, you may edit or delete it as you wish.</p>
<p>It is suggested that you edit this blog entry to see how HTML Tags may be used to style blog entries and comments.</p>
<p>Happy blogging!</p>"
);
  $row->created(DateTime->now);
  $row->insert; 

  # Return to homepage.
  $c->res->redirect($c->uri_for_action('/index'));
}

Create a new ResultSet class to search for the most recently added new member:

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

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

sub most_recent {
  my ($self) = @_;

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

1;

Run the application and you should be able to create a new member with their first blog entry automatically generated for them:

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

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

    <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/src/blog/entries/create.tt

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

<div id="content_wrapper">
  <div id="content_without_sidebar">
  <h3>Create New Blog Entry</h3>

  <form method="post" action="[% c.uri_for_action('/blog/entries/create') %]">
    <table>

      <tr>
        <td>Title:</td>
      </tr>
      <tr>
        <td><textarea name="title" rows="1" cols="88" wrap="virtual"></textarea></td>
      </tr>
      <tr>
        <td><p></p></td>
      </tr>
      <tr>
        <td>Content:</td>
      </tr>
      <tr>
        <td><textarea name="content" rows="20" cols="88" wrap="virtual"></textarea></td>
      </tr>

      <tr>
        <td><p>Do you really want to create this new blog entry?</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/edit.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="content_without_sidebar">
  <h3>Edit Blog Entry</h3>

  <form method="post" action="[% c.uri_for_action('/blog/entries/edit', blog_entry.id) %]">
    <table>

      <tr>
        <td>Title:</td>
      </tr>
      <tr>
        <td><textarea name="title" rows="1" cols="88" wrap="virtual">[% blog_entry.title | html %]</textarea></td>
      </tr>
      <tr>
        <td><p></p></td>
      </tr>
      <tr>
        <td>Content:</td>
      </tr>
      <tr>
        <td><textarea name="content" rows="20" cols="88" wrap="virtual">[% blog_entry.content | html %]</textarea></td>
      </tr>

      <tr>
        <td><p>Do you really want to submit these changes?</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/delete.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="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>
      <h3 id="bottom">Delete Blog Entry</h3>
      <p>Do you really want to delete this blog entry and corresponding comments?</p>
    [% ELSE %]
      <div class="bottom_of_content"></div>
      <h3 id="bottom">Delete Blog Entry</h3>
      <p>Do you really want to delete this blog entry?</p>
    [% END %]

    <form method="post" action="[% c.uri_for_action('/blog/entries/delete', blog_entry.id) %]">
      <table>
        <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/lib/Yardbird/Controller/Root.pm

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

BEGIN { extends 'Catalyst::Controller' }

#
# Sets the actions in this controller to be registered with no prefix
# so they function identically to actions created in MyApp.pm
#
__PACKAGE__->config(namespace => '');

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

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

sub unauthorized_action :Chained('/') :PathPart('unauthorized_action') :Args(0) {
  my ( $self, $c ) = @_;

  $c->stash(template => 'unauthorized_action.tt');
}

sub default :Path {
    my ( $self, $c ) = @_;
    $c->response->body( 'Page not found' );
    $c->response->status(404);
}

sub end : ActionClass('RenderView') {}

__PACKAGE__->meta->make_immutable;

1;

Yardbird/root/src/unauthorized_action.tt

[% INCLUDE header.tt title = 'The YARDBIRD Fan Club: Error'; %]
<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>

  <div id="content_with_sidebar">
    <h3>Error</h3>
    <p>You are not authorized to perform this action.</p>
  </div>
</div>

[% INCLUDE footer.tt %]

Yardbird/lib/Yardbird/Controller/Member.pm

package Yardbird::Controller::Member;
use Moose;
use namespace::autoclean;
use Yardbird::Form::User; 

BEGIN { extends 'Catalyst::Controller'; }

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

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

    $c->response->body('Matched Yardbird::Controller::Member in Member.');
}

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

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

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 and data inserted into database,
  # now auto-generate new member's first blog entry.

  # Get new member info.
  my $row = $c->model('DB::User')->most_recent->first; 
  my $userid = $row->id; 
  my $name = $row->name; 

  # Create new member's first blog entry.
  $row = $c->model('DB::Blog')->new_result({}); 
  $row->userid($userid);
  $row->title("Welcome ".$name."!");
  $row->content(
"<h3>You Are Now A Member Of The YARDBIRD Fan Club!</h3>
<p>You may now:</p>
<ul>
<li>Post blog entries and comments via the Blog pages.</li>
<li>Optionally provide your contact and other information to other users via the Members pages.</li>
<li>View other member's contact information via the Members pages.</li>
</ul>
<h3>About This Blog Entry</h3>
<p>This blog entry was automatically created when you became a member of the YARDBIRD Fan Club, you may edit or delete it as you wish.</p>
<p>It is suggested that you edit this blog entry to see how HTML Tags may be used to style blog entries and comments.</p>
<p>Happy blogging!</p>"
);
  $row->created(DateTime->now);
  $row->insert; 

  # Return to homepage.
  $c->res->redirect($c->uri_for_action('/index'));
}

__PACKAGE__->meta->make_immutable;

1;

Yardbird/lib/Yardbird/Schema/ResultSet/User.pm

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

sub most_recent {
  my ($self) = @_;

  return $self->search({}, { 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.