Schedule appointments with Rails 3 and jQuery

February 21, 2012 § Leave a comment

Problem: I needed a dynamic appointment scheduler that will instantly run a date chosen by a user against all the available time slots for that date. The scheduler then needs to dynamically hide the time slots that have already been chosen.

Solution: jQuery’s datepicker plugin and .ajax() function.

I’ll demonstrate this in an MVC context. All code can be had on Github.

THE COMPONENTS

First, we need to create an appointment model. So in the terminal run:

rails generate model Appointment date:string hour:string

We use a string datatype for ‘date’ because we’ll want to present the ‘yy-mm-dd’ formatted date immediately to the user. Setting hour:string allows the records to have leading zeros which allows us to set four digits for any hour e.g. 1300 and 0900 which in turn makes string pattern matching easier. We’ll use military time to help facilitate this as well.

Now, for our appointment class:

#appointment.rb

class Appointment < ActiveRecord::Base
attr_accessible :date, :hour

validates :date,  :presence => true
validates :hour,  :presence => true,
                  :uniqueness => {:scope => :date}
end

Here, we ensure that no model is persisted unless its date and hour properties are set. We also declare a qualifier for the hour’s uniqueness key – forcing any duplicate hours under the same date to be rejected. This server-side filter is critical for cases when two users both clear the client-side JavaScript protection at the same time. If both attempt to submit the exact same hour and date, Active Record will prevent the more recent submission from persisting.

In the view, we load the necessary jQuery-ui css file at the top, followed by the calendar, hour_picker and appointment_form divs. These divs will switch their hidden/shown status throughout the scheduling workflow. JS files required by jQuery as well as our custom appointment.js file round out our view template. Here is the entire view template for reference.

The controller contains our three needed RESTful routes. If you are confused about REST and how it relates to Rails, read this first followed by this. Our controller:

#app/controllers/appointments_controller.rb
class AppointmentsController < ApplicationController

  def index
    date_from_ajax = params[:matched_date]
    reduce = Appointment.where(:date => date_from_ajax)
    hour_on_date = reduce.collect {|x| x.hour}
    @new_dates = hour_on_date
    render :layout => false
  end

  def new
    @appointments = Appointment.create
      respond_to do |format|
        format.html
        format.js
     end
  end
 

  def create
     @appointment = Appointment.create(params[:appointments])
      if @appointment.save
        redirect_to new_appointment_path
      else
        err = ''
        @appointment.errors.full_messages.each do |m|
        err << m
      end
        redirect_to new_appointment_path, :flash => { :alert => "#{err}, please try again" }
      end
    end
 end

Here, our ‘new’ route allows us to use ‘new_appointment_path’ in our views. The ‘index’ action, as we’ll see, allows us to match chosen dates to available hours. The ‘create’ action persists the data and I added some error handling as well.

For our routes, we need only add the following:

   
#config/routes.rb
resources :appointments

Here, no appointment-specific routes need to be hardcoded – a distinct advantage of RESTful architecture.

SAVING A DATE

Upon visiting ‘/appointments/new’, a user sees nothing but the interactive jQuery calendar because we’ve hidden the ‘hour_picker’ and ‘appointment_form’ divs using jQuery’s .hide() function which is triggered upon page load. Another anonymous, document.ready function calls the .datepicker() function rendering the calendar. Clicking a datebox in the calendar itself triggers the ‘onSelect:’ callback function:

$(function () {
    var currentTime = new Date()
    var month = currentTime.getMonth()
    var day = currentTime.getDate()
    var year = currentTime.getFullYear()

    $("#datepicker").datepicker({
        minDate: new Date(year, month, day),
        dateFormat: 'yy-mm-dd',
        onSelect: function(dateText) {
          $('#hour_picker').show();
          $('#datepicker').hide();
          findHours(dateText);
          $("th.selected_date").append("Date Chosen:" + " " + (dateText));
          pageNo = '2';
        }
        
});
}); 

The ‘onSelect:’ function reverses the user interface by hiding the calendar and showing the hour_picker div. ‘dateText’ is the date value chosen by our user – we pass this value to our ‘findHours’ function which does nothing for now because no dates have ever been chosen. We’ll revisit this when we try to save the same date twice.

The unveiled hour_picker div offers the user a list of standard business-day hours.

<div id='hour_picker'>
<table border="1">
<tr>
<th class = 'selected_date'></th>
</tr>
<tr class = 'cal_table'>
<td type ="button" onclick = "nextPage('0700', this.id)" class = '0700' id = "7:00 am">7:00 am</td>
</tr>
<tr class = "cal_table2">
<td type ="button" onclick = "nextPage('0800', this.id)" class = '0800' id = "8:00 am">8:00 am</td>
</tr>
<tr class = "cal_table">
<td type ="button" onclick = "nextPage('0900', this.id)" class = '0900' id = "9:00 am">9:00 am</td>
</tr>
<tr class = "cal_table" id = '5'>
<td type ="button" onclick = "nextPage('1100', this.id)" class = '1100' id = "11:00 am">11:00 am</td>
</tr>
<tr class = "cal_table2" id = '6'>
<td type ="button" onclick = "nextPage('1200', this.id)" class = '1200' id = "12:00 pm">12:00 pm</td>
</tr>
<tr class = "cal_table" id = '7'>
<td type ="button" onclick = "nextPage('1300', this.id)" class = '1300' id = "1:00 pm">1:00 pm</td>
</tr>
</table>
</div>

Here, each table row is a button which triggers the ‘nextPage’ function (seen below) with two parameters – the button’s id and a string that is a duplicate of the button’s class. Matching the button’s class to a ‘nextPage’ parameter will allow us to block, hide, or disable one or more of these buttons in the future in order to prevent duplication.

 function nextPage(hour, id){
        $('#hour_picker').hide();
        $("#appointment_form").show();
        document.getElementById('appointments_date').value = setFinalDate;
        document.getElementById('appointments_hour').value = hour;
        $('#cal_previous2').show();
        document.getElementById('subData').style.display = 'block'
        $('#final_date h2').append(setFinalDate);
        $('#final_hour h2').append(id);
        pageNo = '3';
        return true;
     }

Here, we continue our liberal use of jQuery’s .hide() and .show() methods. We also set input values to the user-chosen date and the user-chosen hour respectively.

Finally the user sees the form to submit. The values have been added to hidden fields to prevent the user from changing them at this point in the process. Instead, to allow the user to review their selections before submission, we use jQuery’s .append() to peg the chosen date and hour to specific divs.

TRYING TO SAVE A DUPLICATE DATE

So now a different use wants to book that exact same date and time. They hit a calendar date, say February 21, 2012, and, along with moving the user to the next phase, the onclick triggers the ‘findHours()’ function which is no longer useless:

//appointment.js
function findHours(chosen_date){
 $.ajax({
      url: "/../appointments",
      cache: false,
      data: {matched_date:chosen_date},
      success: function(html){
        var hours_array = [];
        $("#hidden_hour_div").append(html);
        var hours_string = $("#hidden_hour_div").html()
        var one = hours_string.substring(2, 6);
        var two = hours_string.substring(10, 14);
        var three = hours_string.substring(18, 22);
        var four = hours_string.substring(26, 30);
        var five = hours_string.substring(34, 38);
        var six = hours_string.substring(42, 46);

        hours_array.push(one,two,three,four,five,six);
        for (var j in hours_array) {
          (final_array = '\.'+ hours_array[j]);
          $(final_array).hide();
     }
    }
  });
 }

This function dynamically checks the user-chosen date for any of its hours that may have already been booked. We pass the chosen_date to jQuery’s wonderful .ajax() function which creates a GET request by attaching the chosen_date value to the URI string. We set the ‘cache’ property to false causing a current timestamp to automatically be attached to the URI – we don’t use the timestamp here but you might have a use for it later. Also, we don’t want to cache the data should the user revisit the scheduler in the same session.

.ajax() is a higher-order function, meaning that if certain conditions are met, it returns a ‘success’ callback function. This callback takes the hours of our user-chosen-date – returned as a comma-delimited sting to the index view (more on this later) – and converts the hours into the elements of a new array by using javascript string manipulation techniques.

By sending a GET request to /../appointments, we call the index method in our appointments_controller. This method…

#app/controllers/appointments_controller.rb
  def index
    date_from_ajax = params[:matched_date]
    reduce_date = Appointment.where(:date => date_from_ajax)
    hour_on_date = reduce_date.collect {|x| x.hour}
    @new_dates = hour_on_date
    render :layout => false
  end  

takes the GET request’s matched_date value and queries the database with it. We then reduce the date to a collection of hours and make the result viewable with the instance variable @new_dates. Finally, since we’ll be passing this html back to our new.html.erb view, we need to disable the Rails layout lest we send a huge string of unnecessary html wrapping the html that we want.

Speaking of new.html.erb, you’ll notice this snippet..

<div id = "hidden_hour_div"></div>

..is where we’ll peg the string returned from our index method. The second part of the findHours() function manipulates this string into an array. We then cycle through the array, attaching a ‘.’ in front of each element so that jQuery can recognize each element as a particular class in our view and hide those classes accordingly. Why do we add the ‘.’ after the fact? Because ID and class tokens must begin with a hexadecimal ([0-9A-Za-z]) so we can’t call our

classes ‘.0900’ for instance.

An alternate strategy for findHours() is to have the data converted to JSON in the controller and then parsed with jQuery.parseJSON() in appointment.js. But this would require increased steps in both the Rails initialization directory, and in your Appointment’s model.

So we’ve covered the scheduler’s main components as well as the workflow for creating a new date/hour record and the process of preventing the duplication of a date/hour record.

I packaged this code as a Ruby Gem but running the generator only provides a scaffold for you to build a more robust scheduling feature.

Ship generators with your ruby gems

February 3, 2012 § Leave a comment

Say I’m building a gem and I want it to do three things.  1) create a new route in the gem user’s routes.rb file  2) add some code that will be executed every time they start their app, and  3) place a controller in their rails controller directory.  How would I allow the gem user to do this with one command?

Let’s see by building a throwaway gem for a rails 3.0+ app.  This post assumes that you understand the basics of building a gem.

In the terminal run:

mkdir my_gem && cd my_gem
mkdir lib && cd lib
mkdir generators && cd generators
mkdir templates && mkdir install && cd templates
touch my_gem.rb && touch my_gem_actions_controller.rb
cd ../install
touch install_generator.rb

Here, when Rails sees the ‘generators’ directory, the generator we are going to create becomes available to rails generate command. The templates directory will store all the files and text we want to add to the user’s app. Now open install_generator.rb and add the following:

module My_Gem
  class InstallGenerator < Rails::Generators::Base
  source_root File.expand_path("../templates", __FILE__)
  def add_my_gem_routes
  route "match '/my_gem', :to => 'my_gem_actions#some_method_in_my_controller'"
  end

  def add_initializer
    template "my_gem.rb", "config/initializers/my_gem.rb"
  end

  def add_controller
    template "my_gem_actions_controller.rb", "app/controllers/my_gem_actions_controller.rb"
  end
 end
end

For the three methods above to work, you need to specify ‘thor’ as a dependency in your my_gem.gemspec file. There, you will also need to specify your ‘lib’ directory as an executable. For an example of a gemspec file go here.

These three methods will be executed when your user runs the generator.

I use the ‘template’ method instead of the better-sounding ‘copy_file’ method so that we can place all our files into a ‘templates’ directory. The ‘template’ function adds this directory to the generator’s PATH. The ‘template’ method also executes any ERB tags found in your template files, and, while this can be powerful, we need to escape them if we’re placing ‘html.erb’ files into the user’s ‘view’ directory lest Rails throws a nomethod error when trying to execute them – we do this by using a double percent (%%) like in this example:


<!DOCTYPE html></pre>
<html>

<head>

<title>my app</title>

<%%= stylesheet_link_tag "<%= file_name %>" %>

<%%= javascript_include_tag :defaults %>

<%%= csrf_meta_tag %>

</head>
<pre>

To review, in the templates directory, we’ve made the files we want to copy into our gem user’s rails app. In the install directory we’ve made the actual generator itself. And in the root of our gem we’ve ensured that our gemspec file lists ‘thor’ as a dependency and the ‘lib’ directory as an executable.

We’re ready to create the gem.

cd /my_gem
gem build my_gem.gemspec

Publish Adobe Captivate results with Ruby on Rails

October 27, 2011 § Leave a comment

Adobe allows you to use the results derived from Captivate quizzes without requiring each of your students to have a separate Adobe ID.  There are several tutorials showing how to publish these results using PHP as your Adobe installation has the necessary PHP scripts, but none that I could find concerning the increasingly popular Ruby on Rails framework.  So here we go…
First, we need to tell Captivate that we want to report quiz results.  In your Captivate quiz project, hit edit > Preferences > Reporting (located 4th from the bottom of the left-hand menu) then at the top check ‘Enable reporting for this project’.  After checking this, check ‘Internal Server’ and then hit ‘configure’.  In the popup window that comes up, you’ll see 4 mandatory fields.  The first field is the most important as this is where you’ll link Captivate to the familiar Rails routes – type http://www.example.com/internalreport.  PHP projects will have a ‘.php’ appended to the end of that URL but we’re trying to create a Rails route so do not append a ‘.rb’ to the end of this.
Second, in your Captivate installation you’ll find the PHP script in question at ‘whereYouInstalledCaptivate\Adobe Captivate 5.5\Templates\’.  Open this and it should look like:
<!--?php # InternalServerReporting.php<br ?--># Copyright 2000-2008 Adobe Systems Incorporated. All rights reserved.
#
print "
\n";

#
   foreach ($_POST as $k => $v)
   {
    if($k == "CompanyName")
  {
    $CompanyName = $v;
      }
      if($k == "DepartmentName")
  {
    $DepartmentName = $v;
      }
      if($k == "CourseName")
  {
    $CourseName = $v;
      }
      if($k == "Filename")
      {
       $Filename = $v;
      }
      if($k == "Filedata")
      {
       if(get_magic_quotes_gpc())
 $Filedata = stripslashes($v);
 else
 $Filedata = $v;
      }
   }

 $ResultFolder = "./"."CaptivateResults";
 mkdir($ResultFolder);
 $CompanyFolder = $ResultFolder."//".$CompanyName;
 mkdir($CompanyFolder);
 $DepartmentFolder = $CompanyFolder."//".$DepartmentName;
 mkdir($DepartmentFolder);
 $CourseFolder = $DepartmentFolder."//".$CourseName;
 mkdir($CourseFolder);
 $FilePath = $CourseFolder."//".$Filename;
 $Handle = fopen($FilePath, 'w');
 fwrite($Handle, $Filedata);
 fclose($Handle);

   print "
\n";
?>

Now we need to translate this PHP into Ruby for our Rails controller.  Create a Rails controller such as ‘captivate_controller.rb’ so that we have a place where ‘http://www.example.com/InternalServerReporting’ will be routed to.  Don’t forget to add:

match '/InternalServerReporting', :to => 'captivate#InternalServerReporting'

(or whatever you named your controller and data-processing method) to config/routes.rb.  Now for the Ruby translation of the PHP file:

 def InternalServerReporting
    company_name = params[:CompanyName]
    department_name = params[:DepartmentName]
    course_name = params[:CourseName]
    file_name = params[:Filename]
    file_data = params[:Filedata]
    file_path = File.join("#{Rails.root}/doc", "Results", company_name, department_name, course_name)
    FileUtils.mkdir_p(file_path)
    file_path = File.join(file_path, file_name)
    handle = File.open(file_path, 'w' )
    handle << file_data
    handle.close
end

As you can see, POSTing to InternalServerReporting creates a params hash and we’re going to create a directory of Captivate results with the values of these parameter keys. Now, go to your Rails root and right-click ‘doc’ and change the permissions for the directory to ‘read, write, and execute, – make sure the change is recursive so that it affects all the directories within the ‘doc’ directory.
Everything is finished so let’s test. Take a quiz on your site as if you were a student and on the last slide you’ll see a ‘Post Result’ button that was automatically added when you checked ‘Enable Reporting For This Project’. Use whatever username and email combo you want and then exit the quiz and look in your ‘doc’ folder of your Rails root and you should see “Results” directory followed by the three directories that you named (after you specified your site’s URL) followed by an XML file with the results of the quiz you just took. Or you can spec this out.

In future posts, I’ll discuss how to send an email with these results to the student who took the quiz.  I’ll also post about ensuring that the username and email submitted by your students matches the username and email that they used to sign up for your site.  And how to get these captivate file working in Rails.

EDIT: I’ve made a Ruby Gem of this.

WordPress.com and Blogger.com informal SEO and popularity study

October 9, 2011 § Leave a comment

Curious about SEO and the consensus that WordPress.com beats Blogger.com even though Google owns Blogger and Google is synonymous with SEO.  Since I’ve never blogged before and have no Internet presence outside of Facebook’s blue-walled vacuum, I’ll use my own content on both platforms as the sample.  My parameters:

I registered for both free blogging platforms close to the exact same time – wordpress followed by blogger a few minutes later.

I’ll use the free plan for both – no domain mapping.  The sub-domains leveton.wordpress.com and leveton.blogspot.com are comparable.

I’m using the same display name and even the same internal email address for both.

If I link to a post, I’ll link to both platforms.

I’ll be publishing both posts so close to simultaneously that the difference will be negligible.

And, of course, the content and the content’s title will always be the same.  I’ll be posting Ruby on Rails and  Node.js tutorials so I should see some hits.