City, State, County Selection With Bootstrap 2 Typeahead

This is a demonstration of how I implemented an Ajax call to a Perl Dancer2 Application using jQuery and the Bootstrap Typeahead function.

As part of my TravelTime App , I wanted to create and easy way to select the start and end locations using the jQuery Autocomplete component. Bootstrap 2 has its own version of this which they call Typeahead.
I just found out that the latest version of Bootstrap will not include this Typeahead functionality. Instead it will be replaced with typeahead.js I hope to check this out in the future.
Anyway, I will be using Perl, Dancer2 with Template Toolkit to render this form.
I also needed to customise the Typeahead to complete an Ajax call to my Perl Dancer2 application. The Dancer2 application will then provide the sorted list of City,States and Counties based on the search parameters passed to it.

The Template::Toolkit Form

Exerpt from travel_time_quick.tt

<div class="span11">
        <span class="text-info">
            <abbr title='Enter the starting location'>Start Location</abbr>
        </span>
        <div class="control-group">
        <!-- Bootstrap Typeahead -->
            <label class="control-label" for="address-[% loop.count %]">[% warning_message %]</label>
            <div class="controls">
                <input type="text"
                    class="cityStatesTypeahead span9 input-append"
                    name="address-[% loop.count %]"
                    id="address-[% loop.count %]" value="[% address %]"
                    placeholder="City/Town, State" 
                    autocomplete="off"
                    data-provide="typeahead"
                    required="required" 
                    autofocus
                    />
                    <span class="addQmark"><i class="iconic-question-mark"></i></span>
            </div>
            <input type="hidden"
                name="h-address-[% loop.count %]"
                id="h-address-[% loop.count %]"
                value=""
                />
            </div>
        </div> <!-- /span11 -->

The entire template can be seen here on GitHub.
The highlighted fields are the ones that are pertinent to the activation of the Bootstrap Typeahead.
In order to customise the Bootstrap Typeahead, I found this very useful website by FusionGrokker, for which I am thankful (You could also check out the jQuery docs for the original concept).

Here is my JavaScript to implement the Typeahead Ajax call.

The jQuery

Excerpt from city-states.js

var hiddenId;
// Get the hidden address field associated with the current
// address field
$('.cityStatesTypeahead').bind('focus',  function(){
   hiddenId =  $(this).attr('id'); 
});

$(".cityStatesTypeahead").typeahead({
  items: 10, 
  minLength : 3, 
  source:  function(query, process) {
      //Call Perl App to find the sorted list of City, States and Counties
      return searchForPlaces(query, process);
 },  
  highlighter: function(item){
    return highLightSelect(item);
},  

matcher: function () { return true; }, 
 
updater: function (item) {
    return updateField(item);
}, 

});
// Ajax call to Perl Dancer Script, which returns an Array Ref
// of sorted City, State and County data.
var debounceWait = 100;
// Use Underscore JS function '_debounce' to ensure that the
// search waits for specific number of milliseconds before running again.
var cityStates = {};
var cityNames = [];
var searchForPlaces = _.debounce(function( query,  process ){
    $.post("/city_states",{ find : query}, function(data){
    cityStates = {};
    cityNames = [];
        var counter = 0;
        _.each( data.city_states,  function( item,  ix,  list ){
        item.label = item + '-' + (counter++);
         // console.log('Got this item from AJAX : ' + item);
        cityStates[item.label] = {
                city : item[0],   
                state : item[1],  
                county : item[2]
        };
        //add selected items to diaplay array
        // console.log('Add this to the array ' + item.label);
        cityNames.push(item.label);

    });
    // Let Bootstrap and jQuery process the list
    // for display in the input box
    process( cityNames );
});
},  debounceWait);

//Highlighter Function
var highLightSelect = function(item){
    // console.log("      Item inside highlighter " + item);
   var c = cityStates[item];
   return ''
       + "<div class='typeahead_wrapper'>"
       + "<div class='typeahead_labels'>"
       + "<div class='typeahead_primary'>" + c.city + ', ' + c.state + "</div>"
       + "<div class='typeahead_secondary'><strong>County: </strong>"  + c.county + "</div>"
       + "</div>"
       + "</div>";
};

// Updater Function
// Add the required data into the input field
var updateField = function(item){
   var c = cityStates[item];
   //Hidden field will contain all the valuable data
    $( "#h-" + hiddenId ).val( c.city + ',' + c.state + ',' + c.county );
   //This is the data for display on input box
   return c.city + ', ' + c.state + "  County: " + c.county ;
};

//      END BOOTSTRAP TYPEAHEAD


Have a look at line 34 for the actual Ajax call to the Perl module. It sends a request to TravelTime.pm’s ‘/city_state’ route. As the Typeahead attribute ‘minLength’ is set to 3, it passes at least the first three letters of the requested City to the Dancer2 application. The city_states database is then searched for a city beginning with this search string. The Dancer2 application will return a sorted list of these cities with their corresponding state and county names. The application can also accept two search strings separated by a comma. This will search the database for cities beginning with the first string and corresponding states beginning with the second string.
“city-states.js” receives this sorted list of cities, states and counties.
The Typeahead displays (items: 10) items in the dropdown list.The “highLightSelect” highlighter function formats this list into a more readable format.
When an item is selected “theupdateField” populates the hidden fields.

More details of what is happening in TravelTime.pm will be left for another post.
Have a look at the TravelTime App in action.
I hope that this blog post may useful to some developer out there.

Perl Google Distance Matrix API Interface

This is my Perl Module Google::Travel::Matrix, which will be an interface to the Google Distance Matrix API. I will use this for calculating distance and travel times between multiple locations.

Google Distance Matrix API Interface

Google::Travel::Matrix

This module provides an interface with the Google Distance Matrix API. It will return the distance between given locations in JSON format.
It will accept one or many “Origin” addresses in String, Array or Hash format.It will also accept many Destination addresses. The Google Matrix configuration attributes can be set here, but it may be easier just to accept the defaults that I built in unless you have other specific requirements.
I used the Perl Moose Object framework as it does a really great job of helping set up the configuration attributes using Moose Types and coercion. It will also make it easier for me to build my sub classes.
The module is available here on GitHub, Google::Travel::Matrix


=head2 get_all_elements
 Given the Google Distance Matrix output as a scalar reference to a Perl data
 structure, returns an ArrayRef of Matrix elements or undef.
 If no Google Output data passed, then it will create one using the Origin and
 destination addresses;
 It would be a good idea to check that the Google Matrix Return is 'OK' before calling
 this method.
=cut

sub get_all_elements {
    my $self = shift;
    my $matrix = shift || $self->get_google_matrix_data_as_scalar_ref();

    my $google_status = $self->get_matrix_status_message($matrix);

    if ( ( not $google_status ) || ( $google_status ne $VALID_REQ ) ) {

        $log->debug( 'Google return status message is: '
              . ( $google_status // 'No Google Status' ) );
        return $FAIL;
    }

    #------ Preserve the Original address sent to Google
    #       as it may be needed later.
    #       Yet again,  this is something I would prefer 
    #       to put into a Child Class
    my $original_origins      = $self->_get_array_of_origins();
    my $original_destinations = $self->_get_array_of_destinations();

    #------ Get each combination for the origin destination addresses
    my $origin_ct = 0;
    my @elements_array;
    foreach my $origin_addr ( @{ $self->get_matrix_origin_addresses($matrix) } )
    {
        my $original_origin_addr = $original_origins->[ $origin_ct++ ];
        $log->debug( 'Original origin address is : ' . $original_origin_addr );

        #---Get results for current origination address
        my $row = shift @{ $matrix->{rows} };

        #------ Match origination address with all destination addresses
        my $dest_ct = 0;
        foreach my $destination_addr (
            @{ $self->get_matrix_destination_addresses($matrix) } )
        {

            my $original_destination_addr =
              $original_destinations->[ $dest_ct++ ];

            $log->debug( 'Original destination address is : '
                  . $original_destination_addr );

           #----- get the result for the current Origination -> Destination pair
            my $element = shift @{ $row->{elements} };

            push @elements_array,
              {
                origin_address               => $origin_addr,
                destination_address          => $destination_addr,
                original_origin_address      => $original_origin_addr,
                original_destination_address => $original_destination_addr,
                element_status               => $element->{status},
                element_duration_text        => $element->{duration}{text},
                element_duration_value       => $element->{duration}{value},
                element_distance_text        => $element->{distance}{text},
                element_distance_value       => $element->{distance}{value},
              };
        }
    }
    $log->debug(
        'Array of all elements returned by Google:  ' . dump @elements_array );
    return \@elements_array;
}

This is far from being a completed masterpiece, but for now it will get me what I need. I intentionally didn’t allow for receiving the data in XML format, as Google recommends JSON format.I may add XML processing at a later date.I may even consider refactoring it from Moose to Moo, to make it lighter and nimbler.
I also plan to create a subclass class to Google::Travel::Time that will do the actual conversion from distance to a travel time acceptable to the NYC moving and storage industry. Until then, I don’t consider this module to be CPAN worthy.

Upload and Email a photo using Dancer2 with Twitter Bootstrap

This is just a short demonstration on uploading a photograph, previewing the photo, and then emailing the photo to a user defined email address.

I will use the Perl, Dancer2 a lightweight web application framework, Template Toolkit, and Bootstrap.

You can set up Gmail or Hotmail to act as your mail server. See here or here for useful guides on setting up sSMTP on your linux laptop.

Configuration:

First, set up the Dancer2 configuration file. The original Dancer comes with a helper script which will set up libraries, config files etc. I used this and converted what I needed to Dancer2(All is explained here. I prefer Config::General format to YMAL so I changed the config file to Config::General format and the file suffix from “.yml” to “.conf”.

Main Configuration File.

This config file, written in Config::General format, provides the application with the main view layout( or “wrapper” ) name, “main.tt”. It also notifies the application that we intend to use Template::Toolkit for our views as opposed to the Dancer2::Template::Simple. I specify that the Template will use “[%” style tags as opposed to the Dancer default of “<%” tags. This may no longer be necessary with Dancer2.
We also include UTF-8 encoding for good measure.

appname = "PhotoUp"
layout = "main"
charset = "UTF-8"
<engines>
  <template_toolkit>
    ENCODING = utf8
    start_tag = '[%'
    end_tag = '%]'
 </template_toolkit>
</engines>

Development Configuration File

Most of this, except the Input File and Email sections are set up automatically for you using the Dancer helper script. I changed the logging to output to a file (logs/development.log). I also added some configuration for sending the email via sSMTP.
The input file max size and other information can be specified here too. The more constraints that are added here, the less hard coding of data will be needed in the Perl Module.


logger = "file"
log = "debug"
warnings = 1
show_errors = 1
<InputFile>
  max_file_size = 1000000 # Kbs
  file_suffix = .jpeg
</InputFile>

<Email>
  transport = SMTP-TLS
 <SMP-TLS>
    host = smtp.live.com # hotmail
    username = xxxxxxxn@xxxxxxxxx.com
    password = "password"
    port = 587
  </SMP-TLS>
  from = xxxxxxxxxxxx@hotmail.com
  to = xxxxxx@xxxxxxxxx.com
  cc = xxx@xxxxxxxx.com
  subject = Here are your photos.
  signed = The Boss
</Email>

 

 

Views:

I use Template Toolkit for my templating. Dancer2 uses a “layout” as a type of wrapper for your template. Here is mine located at views/layouts/main.tt.

Layout.


<!DOCTYPE html>
<html lang="en">    
<meta charset="utf-8">
<head>
[% title %]
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Photo Sender" />
<meta name="author" content="austin kenny" />
  <!-- Bootstrap File Upload CSS -->
  <link href="[% request.uri_base %]/css/bootstrap/bootstrap.css" rel="stylesheet" /> 
  <link href="[% request.uri_base %]/css/bootstrap/bootstrap-responsive.css" rel="stylesheet" /> 
  <link href="[% request.uri_base %]/css/bootstrap/bootstrap-fileupload.css" rel="stylesheet" /> 
</head>
<body>
  [% content %]
<div class="text-center" id="footer">
 <small> Powered by <a href="http://perl.org/">Perl</a>
 <a href="https://metacpan.org/module/Dancer2">Dancer2</a>
 <a href="http://jasny.github.io/bootstrap/index.html">Bootstrap</a> </small></div>
</body>
<!-- Grab Google CDN's jQuery. fall back to local if necessary --><script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script><script type="text/javascript">// <![CDATA[
!window.jQuery &#038;& document.write('<script type="text/javascript" src="[% request.uri_base %]/javascripts/jquery.js"><\/script>')
// ]]></script><!-- Bootstrap File Upload Js --><script type="text/javascript" src="[% request.uri_base %]/javascripts/bootstrap-fileupload.js"></script>

I have included all the Bootstrap CSS links along with the Bootstrap JavaScript to provide the fancy photo/image preview etc. It is also necessary to provide links to JQuery in order for the Bootstrap code to work. Notice the [% content %] placeholder on line 14. This is where the page specific content(photo_upload.tt) goes.

Upload Page View.

This template view (photo_upload.tt), it the first page that is rendered when the application is called.It contains the “Browse” button for searching for the file/image to be uploaded on the client’s device. It also has an email to field, along with the necessary send button.
Most of the initial heavy lifting will be done by the jQuery upload and preview script, provided by Twitter Bootstrap. When the file/image is previewed, it can be emailed, or the user can opt to discard it in favour of another file/image.


<div class="container">
    <!-- Browse and upload file form: -->
    <form class="form-file-upload file-upload " enctype="multipart/form-data"
    name="photoUploadForm"  action="/photo_upload" method="POST">
    <fieldset>

        <legend class="form-file-upload-heading">Email Your Photos</legend>
        <!-- Start Bootstrap Template -->
        <div class="fileupload fileupload-new" data-provides="fileupload">
            <div class="fileupload-new thumbnail" style="width: 200px; height: 150px;">
              <img src="http://www.placehold.it/200x150/EFEFEF/AAAAAA&text=no+image" />
            </div>
            <div class="fileupload-preview fileupload-exists thumbnail"
             style="max-width: 200px; max-height: 150px; line-height: 20px;">
            </div>
            <div>
               <span class="btn btn-file"><span class="fileupload-new">Select image</span>
               <span class="fileupload-exists">Change</span>
               <input type="file"  name="inPhoto"/>
               </span>
               <a href="in_photo" class="btn fileupload-exists" data-dismiss="fileupload">Remove</a> 
                <span class="text-error">[%- upload_error -%]</span>
            </div> 
        </div>
        <!-- End Bootstrap Template -->
        <div class="input-append">
          <!-- Mail-to Email address -->
          <input class="input-large" type="email" name="emailTo"
          placeholder="Email to" maxlength="60" />
          <button class="btn btn" type="submit" >Email Photo</button>
        </div>
        <div>
           <span class="text-error">[%- email_error -%]</span>
        </div>

    </fieldset>
    </form>
    <!--   <div id="progress"></div> -->
    <div id="messages">
    <p class="text-success">[%- success_message -%]</p>
    <p class="text-info">[%- info_message -%]</p>
    <p class="text-warning">[%- warning_message -%]</p>
    <p class="text-error">[%- error_message -%]</p>
    </div>

</div> <!-- /container -->

 

Most of this comes straight from the Jasny Bootstrap documentation.

Now it is just a matter of writing theĀ  Dancer2 script.

 

Dancer2 Module


package PhotoUp;
# ABSTRACT: Upload and email photo(s) demo using ssmpt and Hotmail
use Dancer2;
use Dancer2::Core::Error;

our $VERSION = '0.1';

use Data::Dump qw/dump/;
use Carp qw/croak/;
use Try::Tiny;
use IO::All;
use File::LibMagic;

#------Email
use Email::Valid;
use Email::Sender::Simple qw(sendmail);
use MIME::Entity;
use Email::Sender::Transport::SMTP::TLS;

#------ Globals
my $file_upload_route    = '/photo_upload';
my $uploaded_file        = 'inPhoto';
my $email_to             = 'emailTo';
my $image_file_suffix_rx = qr/\.(gif|png|jpe?g)$/;
my $image_file_type_rx   = qr/image\/(jpeg|gif|png)/;

=head2 get $file_upload_route
  Render the file upload form.
=cut

The first part of the module took care of incorporating all the necessary modules, frameworks and package “global” variables. The most important modules being Dancer2 and Template modules.
The next method is the “GET” ‘/photo_upload’ route method. It will render the photo/image upload page with the image viewer. As you can see, there if very little coding here. Just specifying the template file and in this case, populating one template parameter, the page “title”. Dancer takes care of the rest.


get $file_upload_route => sub {
    debug 'Got to GET photo_upload page';

    #------ Render the upload page.
    template 'photo_upload.tt', { title => 'Upload Photos', };
};

The next method uses the same HTTP route, this time using “POST”. It is called after the file has been uploaded and the ‘Email Photo’ button has been clicked. This method takes care of passing the input parameters(uploaded file object and email address) to a validation method. The CPAN module Email::Valid is used to validate the email address while File::LibMagic has the necessary power to validate the uploaded image. From here, the Email Message building and sending methods are called. And finally, a results page is rendered with the details as to the success or failure of the operation.

=head2 post $file_upload_route
  Upload the photograph or image file. Will accept files of type .jpg, .jpeg, 
  .png, .gif.
  Validates the file type and size.
  Will email them to the user provided email address.
=cut

post $file_upload_route => sub {
    debug 'Got to POST photo_upload page';

    debug "Params are : " . dump( request->params );
    my $validation_results = {
        in_photo => upload($uploaded_file) // undef,
        email_to => params->{$email_to},
    };

    _validate_user_input($validation_results);
    debug 'These are the validation results: ' . dump($validation_results);

    #------ Display errors
    if (   ( exists $validation_results->{email_error} )
        or ( exists $validation_results->{upload_error} ) )
    {
        debug 'Got to display some Errors.';
        return template 'photo_upload.tt', {
            title => 'Upload Photo Errors',
            %$validation_results,
            warning_message => 'You must upload the file again and enter a
            correct email address!',
        };
    }

    debug 'Good, passed the error checks.';
    my $in_photo = $validation_results->{in_photo};

    #------ Rename the Base of the temporary file to the original File Base
    my $photo_fq_name = _rename_uploaded_file($in_photo);

    my $message = _build_email_message(
        {
            email_to => $validation_results->{email_to},
            type     => $in_photo->type,
            path     => $photo_fq_name,
            encoding => 'base64',
        }
    );

    return _process_error('Unable to build MIME::Entity')
      unless $message;

    my $transport = _build_email_transport();

    return _process_error('Unable to build Email transport!')
      unless $transport;

    _send_email_msg( $message, $transport );

    template 'photo_sent.tt',
      {
        title              => 'Emailed Photos',
        sent_files_heading => 'Emailed Photo(s)',
        success_message    => $in_photo->basename
          . ' was emailed to '
          . $validation_results->{email_to},
        in_photos   => [$in_photo],
        have_photos => ( $in_photo ? 1 : 0 ),
        return_to   => uri_for($file_upload_route),
      };

};

The input validation method using Email::Valid and File::Lib::Magic to do the heavy lifting here.

=head _validate_user_input
 Validate the uploaded file and the 'to' Email address.
 Pass a HashRef with the Dancer Request Upload Object and
 the email address.
 Populates the HashRef with validation information.
 {
    in_photo  =>  # the Dancer Request Upload object or not exists
    email_to => email@emails.com or not exists if invalid
    upload_error =>  'Error Msg....'  or not exists if ok
    email_error =>  'Error Msg....'  or not exists if ok
 }
=cut

sub _validate_user_input {
    my $validation_report = shift;

    if ( defined $validation_report->{in_photo} ) {
        debug 'At least there is a file uploaded.';

        #------ Check that the file is a valid Image or Photo
        $validation_report->{upload_error} = 'Not a valid photo or image type!'
          unless _validate_file( $validation_report->{in_photo} );
    }
    else {
        $validation_report->{upload_error} = 'No photo uploaded!';
    }

    #------ Validate the 'to' Email Address
    $validation_report->{email_error} = 'Invalid or no email address entered!'
      unless ( $validation_report->{email_to} =
        _validate_email_address( $validation_report->{email_to} ) );
}

=head2 _validate_email_address
  Validates a given Email Address.
  Uses Email::Valid
  Returns undef if not valid.
=cut

sub _validate_email_address {
    my $email_address_in = shift;
    my $valid_email_addr;
    try {
        $valid_email_addr = Email::Valid->address($email_address_in);
    }
    catch {
        error 'Problems with Email::Valid!: ' . $_;
    };
    return $valid_email_addr;
}

=head2 _validate_file 
  Validate the file type by first checking the file suffix,
  then validating the file type with File::Lib::Magic
  Also checks that the file is smaller than the maximum allowed 
  size from the config file.
  Returns the validated file or undef.
=cut

sub _validate_file {
    my $in_file = shift;
    return
      unless ( $in_file
        && ( lc( $in_file->basename ) =~ /$image_file_suffix_rx/ )
        && ( $in_file->size <= config->{InputFile}{max_file_size} ) );
    my $FileMagic;
    try {
        $FileMagic = File::LibMagic->new();
    }
    catch {
        error $_;
    };
    return $in_file
      if ( $FileMagic->checktype_filename( $in_file->tempname ) =~
        /$image_file_type_rx/ );
}

=head2 _rename_uploaded_file
 Renames the temporary file basename back to its original name.
=cut

sub _rename_uploaded_file {
    my $file_upload = shift;
    my $io_photo    = io( $file_upload->tempname );
    my $filepath    = $io_photo->filepath;
    return $io_photo->rename( $filepath . $file_upload->basename );
}

The Email Message is build using various CPAN modules.
MIME::Entity; # To build the Email Message
Email::Sender::Transport::SMTP::TLS; # To set up the Email transport
Email::Sender::Simple qw(sendmail); # Sends the Email
You can check out their documentation on meta::cpan if you are not already familiar with them.

And then there is the Error processing. Just in case …….

#-------------------------------------------------------------------------------
#  Render Error Page
#  Pass a message and a URL to return to.
#-------------------------------------------------------------------------------
sub _process_error {
    my $error_msg = shift // 'Something really bad must have happened.';
    error $error_msg;
    return Dancer2::Core::Error->new(
        response => response(),
        status   => 500,
        message  => $error_msg,
    )->throw;
}

#-------------------------------------------------------------------------------
1
__END__

=pod
 
=head1 NAME
 
 PhotoUp - Upload and Email Photo(s) Demo
  
=head1 VERSION
 
 version 0.1
  
=head1 SYNOPSIS
        Just a demo.                                         
=head1 DESCRIPTION
  This is just a short demo to Upload a use inputted photograph or image. The
  photo will be emailed to a user inputted email address using Dancer2. It will take
  advantage or Bootstrap's photo upload and preview JavaScript/jQuery
  component. It will also use Template::Toolkit.
=cut

This pretty much takes care of uploading the photograph, validating that it is in fact a photo or some other image type and validating the ‘to’ email address. It also checks to make sure that the file is not larger than the maximum size specified in the configuration file. It then emails the photo to the requested email address. Along the way it also logs some debug and error messages. If everything runs as planned, a page will be rendered notifying the user of the operations success.

Views (cont):

Display the results of our actions in photo_sent.tt.

This is the final template view rendered. Only a few things to note here for those of you who are not familiar with Template::Toolkit. Template::Toolkit allows lots of data processing and manipulation. It has many Plugins that can be installed to filter data or to use various CPAN modules like DateTime.pm right inside the template.
I just use a little manipulation of the uploaded file size data so as to figure out the best way to represent the size units of the file. I also threw in some error and status message fields at the foot of the view, which I haven’t fully utilized yet.


[% USE two_dec = format('%.2f') -%]

<div class="container">
<!-- Sent Photos Form -->
    <p class="lead">[%- sent_files_heading -%] </p>
    <table  class="table table-hover">
    <thead>
       <tr>
          <th>File Name</th> <th>Type</th> <th>Size</th>
        </tr>
    </thead>
    [% FOREACH file IN in_photos %]
    <tbody><tr>
            <td>[%- file.basename -%]</td>
            <td>[%- file.type -%]</td>
            [%# 'Display the size in most appropriate unit size.' %]
            [% IF file.size < 1024 %]
               <td>
                [% file.size _ "Bytes" %] 
               </td>
            [% ELSIF file.size < 1048576 %]
               <td>
                [% two_dec(file.size / 1024) _ 'K' %]
               </td>
            [% ELSIF file.size < 1073741824 %]
               <td>
                [% two_dec(file.size / 1048576 ) _ 'M'  %] 
               </td>
            [% ELSE %]
               <td>
                [% two_dec(file.size / 1073741824 ) _ 'G' %]
               </td>
             [% END %]


    </tr></tbody>
    </table>
    [% END %]

    <a  href="[% return_to %]">
        <button class="btn btn-large" >Back</button></a>
    <div id="messages">
    <p class="text-success">[%- success_message -%]</p>
    <p class="text-info">[%- info_message -%]</p>
    <p class="text-warning">[%- warning_message -%]</p>
    <p class="text-error">[%- error_message -%]</p>
    </div>
</div> <!-- /container -->

Sample Web Pages

Photo Upload Page
This is the initial photo upload page.

Initial Upload Photos Page

Upload Photos Page

View Uploaded Photo
This is what the web page looks like when a the photograph has been uploaded.

Photo preview page

Photo preview page

Results Page
This page is displayed after the photo is emailed.

Emailed photo results page

Emailed photo results page

Todo

Ok, thats it for this one for now. There are a few other things that I could do here. Test scripts need to be setup to really test this one out. Also I could add some more client side validation to the existing validation provided by Bootstrap. I have used this in the past and found it a very useful client side validation library. Also I plan to check out this more elaborate jQuery file upload library in the future.

Summary

Dancer2 provides a nice framework for building small to medium sized Web based applications. It is not as powerful, or as well documented as Catalyst. On the other hand, the learning curve is somewhat less steep than Catalyst. I still think that Catalyst is a much better framework, but I do plan to use Dancer2 for some more small applications. Plack::Builder is also another option worth checking out for very small web apps.

The code for this can be found in my GitHub Repo.