Hacking Typo 5.1.2

Posted by Tom Willett Thu, 14 Aug 2008 18:26:00 GMT

I am creating a website that required some data tables.  Typo does not have any built in functionality to accomplish this, but since it is just a rails app, it is fairly easy to add such functionality.  So here is what I wanted to do and how I accomplished it.

1)  Add the tables I need to typo

2) Create the admin screens to add/update/delete data from these tables and integrate them into Typos admin interface.

3) Pull the data from these tables into blog posts in a semi dynamic fashion.

Read on for how I accomplished these tasks.

1)  Add tables.

Since Typo has not adopted the Rails 2 convention of using time stamped migrations, I just did this by hand – not a big deal.

2)  Create admin screens.

I will assume that you know how to create normal rails models/controllers and views to match your tables.  I just did it by using the built in rails script/generate scaffold routine.  They don’t need to be fancy, this is just for admin use.  Of course when you create these models/controllers and views they have no security and are not part of the Typo interface.  When you have them working normally to your satisfaction here is how to add them to Typo admin interface.

a) Move the controller to the admin app/controller/admin directory.  Edit the controller code and Change the class declaration from;

class MyController < ApplicationController

to

class Admin::MyController < Admin::BaseController

of course MyController is the name of your controller.

This controller now participates in the Typo admin security system.

To add this controller to the admin interface you will need to edit config/initializers/access_rules.rb

There you will find the AccessConrol.map and a series of map.project_modules below.  These modules correspond to the new AccessControl system and to the Admin Menus.

The data tables I added would be considered content so I added the controllers I created to the content map.  Just add a line for each controller you wish to access.  Now your controller is callable for the admin interface.  Note if you don’t add the controller here, the security system will not let you access the controller.

b)  Now we have to get the views into the admin area so the controller can find them.  Like the controller just move your view subdirectory to the view admin subdirectory.  Your views will now use the admin layout.  The final change you should make is to edit your views to further integrate them into the admin interface.  First set the @page_heading variable and add the code to pull in the appropriate submenu.  The beginning of each of my views looks like this:

<% @page_heading = "My Page Heading" %>
<% subtabs_for(:content) %>

You should change the :content to match the menu you placed your controller under.

All done.

3)  Pull the data from these tables into blog posts in a semi dynamic fashion.

Now for the harder part – of course since I already did it, it can be easy for you.  I created a textfilter macro that allows me to call rails methods in which I create the view code I want to display for the database record.  I called this macro RunCode.  So in the blog post I can simply enter <typo:runcode>some_method_name(args)</typo:runcode>.  When the post is published Typo calls the method and inserts the result into the blog post at that point.  If you are interested in the macro let me know and I will make it available. 

Have fun hacking Typo.

Ruby on Rails on Debian Etch with fastcgi and Phusion Passenger

Posted by Tom Willett Wed, 30 Jul 2008 12:13:00 GMT

Debian Etch has fairly current versions of Ruby and Rails that can be installed through the package management system.  However, the installation of the Gem package manager does not know anything about the Rails installation.  This will break software that checks Gem for Rails information.  So here is my method of getting Ruby on Rails running the way I want on Debian Etch.

I assume in the following that you have a Debian Etch server up and running with Apache2 and Mysql.  The bold lines can be copy and pasted into your terminal window.

1)  Install the normal Debian packages for ruby and rails.

aptitude install ruby libzlib-ruby rdoc irb rubygems rails eruby

2)  Now we will install fastcgi and the ruby mysql bindings.  To do this you will have to edit you /etc/apt/sources.list and add the contrib and non-free options.  Your main deb line should look like this:

deb http://ftp.debian.org/debian/ etch main contrib non-free

Now we do the install

aptitude update;aptitude install libapache2-mod-fastcgi libmysql-ruby apache2-prefork-dev

3) Next we update some apache modules and restart apache.

a2enmod ssl
a2enmod rewrite
a2enmod suexec
a2enmod include
a2enmod fastcgi
/etc/init.d/apache2 force-reload

4)  Now we get down to the business of updating ruby and rails.

First update gem

gem update –system

This will break gem.  To fix it do

cp /usr/bin/gem1.8 /usr/bin/gem

Now update rails

gem update -d rails

if you need an older version of rails to be available then install it like this

gem install -d -v1.1.6 rails

This will install rails 1.1.6 (the version installed by debian package manager) and make it know to gem.

4)  Now if you want the latest rails speed up install Phusion Passenger

gem install passenger

passenger-install-apache2-module

The passenger install is good about telling you if you don’t have everything it needs and how to get it.

Thats it you should have a fully updated and functioning Ruby on Rails system on Debian Etch.

 

 

Rails Date Parser 1

Posted by Tom Willett Fri, 27 Jun 2008 20:11:00 GMT

I became frustrated with date parsing in Rails and Ruby and so created a user friendly parser for dates. I wanted to avoid a big javascript library or using a date-picker widget. Quite frankly, when I am entering dates, I don't like to have to reach for the mouse and scroll through the dates.

So this is a simple to install simple to use extension to the ruby Date class. To use it simply copy the following code to a file named dateparse.rb and place it in the lib directory in your rails app. Then open up the application controller and and put require 'dateparse' at the top of the file. (Note: put it before the class declaration).

Then in your controller or view code use Date.parse_date() in place of Date.parse(). It recognizes the following formats
    YYYY-mm-dd  ISO Standard
    mm-dd-YYYY  American
    mm-dd      Assume current year
    mm/dd/YYYY American standard
    mm/dd 
    mm.dd.yyyy  European
    dd.mm.yyyy  
    dd mmmmm yyyy example: 22 may 1945 or 22nd may 1945
    dd mmmmm   example: 22 may  (current year)
    mmmm dd yyyy  example: may 22 1945 or may 22nd 1945
    mmmm dd, yyyy  example: may 22, 1945
    mmmm dd     example may 22 or may 22nd
    dd          example  4 or 4th
    today (tod) or now   common names for days and abbreviations
    tomorrow (tom)
    yesterday  (yes)
    Next Week, Last Week, Next Month, Last Month, Next Year, Last Year
    +n[units] or -n[units]: Date from today: examples: '+22', '+22 days',
       '+22d', '-4 weeks', '-4w', '-4week', '+6 months', '+6m', '+6month',  
       '-2 years', '-2y'
# This library adds the parse_date method to the Standard Ruby Date Class.
# Its purpose is to make the entry of user friendly date formates painless
# in Rails applications.
#
# My motivation for making this was the desire for user friendly date input
# in rails applications without resorting to javascript or some kind of widget.
#
# To use: drop this file into ./lib, require it into the application 
# controller.
#
# Written by Tom Willett 
# Version 1.0
#
# with a tip of the hat to
# Stuart Rackham 
# and Datetime ToolBocks
#
# Date Formats recognized by this method:
#    YYYY-mm-dd  ISO Standard
#    mm-dd-YYYY  American
#    mm-dd      Assume current year
#    mm/dd/YYYY American standard
#    mm/dd 
#    mm.dd.yyyy  European
#    dd.mm.yyyy  
#    dd mmmmm yyyy example: 22 may 1945 or 22nd may 1945
#    dd mmmmm   example: 22 may  (current year)
#    mmmm dd yyyy  example: may 22 1945 or may 22nd 1945
#    mmmm dd, yyyy  example: may 22, 1945
#    mmmm dd     example may 22 or may 22nd
#    dd          example  4 or 4th
#    today (tod) or now   common names for days and abbreviations
#    tomorrow (tom)
#    yesterday  (yes)
#    Next Week, Last Week, Next Month, Last Month, Next Year, Last Year
#    +n[units] or -n[units]: Date from today: examples: '+22', '+22 days',
#       '+22d', '-4 weeks', '-4w', '-4week', '+6 months', '+6m', '+6month',  
#       '-2 years', '-2y'
#
class Date

  # Parse date string with one of the following formats:
  #
  # The string argument is first converted to a string with #to_s.
  # Returns nil if passed nil or an empty string.
  # Raises ArgumentError if string can't be parsed.
  #
  def self.parse_date(org_string)
    string = org_string.to_s.strip.downcase
    return nil if org_string.empty?
    today = Date.today
    if string =~ /^(\d{4})-(\d{1,2})-(\d{1,2})$/
      # YYYY-mm-dd format.  ISO Standard
      year = $1.to_i
      month = $2.to_i
      day = $3.to_i
    elsif string =~ /^(\d{1,2})-(\d{1,2})-(\d{4})$/
      # mm-dd-yyyy format
      year = $3.to_i
      month = $1.to_i
      day = $2.to_i
    elsif string =~ /^(\d{1,2})\.(\d{1,2})\.(\d{4})$/
      # mm.dd.yyyy or dd.mm.yyyy format
      year = $3.to_i
      if $1.to_i > 12
        day = $1.to_i
        month = $2.to_i
      else
        day = $2.to_i
        month = $1.to_i
      end
    elsif string =~ /^(\d{1,2})-(\d{1,2})$/
      # mm-dd format
      year = today.year.to_i
      month = $1.to_i
      day = $2.to_i
    elsif string =~ /^(\d{1,2})\/(\d{1,2})\/(\d{4})$/
      # mm/dd/yyyy format
      year = $3.to_i
      month = $1.to_i
      day = $2.to_i
    elsif string =~ /^(\d{1,2})\/(\d{1,2})$/
      # mm/dd format
      year = today.year
      month = $1.to_i
      day = $2.to_i
    elsif string =~ /^(\d{1,2})(?:st|nd|rd|th)?,? (\w+) (\d{2,4})$/
      # 'd mmmm yyyy' format
      date_array = ParseDate.parsedate("#{$1} #{$2} #{$3}", true)
      year = date_array[0].to_i
      month = date_array[1].to_i
      day = date_array[2].to_i
    elsif string =~ /^(\w+) (\d{1,2})(?:st|nd|rd|th)?,? (\d{2,4})$/
      # 'mmmm d yyyy' format
      date_array = ParseDate.parsedate("#{$2} #{$1} #{$3}", true)
      year = date_array[0].to_i
      month = date_array[1].to_i
      day = date_array[2].to_i
    elsif string =~ /^(\d{1,2})(?:st|nd|rd|th)?,? (\w+)$/
      # 'd mmmm' format
      y = today.year.to_s
      date_array = ParseDate.parsedate("#{$1} #{$2} #{y}", true)
      year = date_array[0].to_i
      month = date_array[1].to_i
      day = date_array[2].to_i
    elsif string =~ /^(\w+) (\d{1,2})(?:st|nd|rd|th)?,?$/
      # 'mmmm d' format
      y = today.year.to_s
      date_array = ParseDate.parsedate("#{$2} #{$1} #{y}", true)
      year = date_array[0].to_i
      month = date_array[1].to_i
      day = date_array[2].to_i
    elsif string =~ /^(\d{1,2})(st|nd|rd|th)?$/
      # 'd' format
      year = today.year
      month = today.month
      day = $1.to_i
    elsif string =~ /^tod[\w]+$|^now$/
      # today or now
      year = today.year
      month = today.month
      day = today.day
    elsif string =~ /^tom/
      # tomorrow
      today += 1
      year = today.year
      month = today.month
      day = today.day
    elsif string =~ /^yes/
      # yesterday
      today -= 1
      year = today.year
      month = today.month
      day = today.day
    elsif string =~ /^(next|last)\s(week|month|year)$/
      # Next Week, Last Week, Next Month, Last Month, Next Year, Last Year
      year = today.year
      month = today.month
      day = today.day
      case $2
        when 'week'
          if $1 == 'next'
            today += 7
          else
            today -= 7
          end
          year = today.year
          month = today.month
          day = today.day
        when 'month'
          month = (($1 == 'next') ? (month + 1) : (month - 1))
          if month > 12
            month = 1
            year += 1
          elsif month < 1
            month = 12
            year -= 1
          end
        when 'year'
          year = (($1 == 'next') ? (year + 1) : (year - 1))
      end
    elsif string =~ /^([+-]\d+)(?:\s*(d|days?|w|weeks?|m|months?|y|years?))?$/
      # Date intervals.
      year = today.year
      month = today.month
      day = today.day
      n = $1.to_i
      units = $2 || 'days'
      case units.first
      when 'd'
        today = today + n
        year = today.year
        month = today.month
        day = today.day
      when 'w'
        today = today + n*7
        year = today.year
        month = today.month
        day = today.day
      when 'm'
        sign = n <=> 0
        month = today.month + sign * (n.abs % 12)
        year = today.year + sign * (n.abs / 12)
        if month <1
          month += 12
          year -= 1
        elsif month > 12
          month -= 12
          year += 1
        end
      when 'y'
        year = year + n
      end
    else
      raise ArgumentError.new("\"#{org_string}\" is not recognized as a Date")
    end
    
    begin
      result = Date.new(year,month,day)
    rescue
      raise ArgumentError.new("\"#{org_string}\" is not recognized as a Date")
    end
    result
  end
end
Hope this helps someone.

link_to_sort

Posted by Tom Willett Fri, 16 Feb 2007 16:46:00 GMT

I have created a little view helper that I use in my ROR applications. I call it link_to_sort. What it does is create a clickable header on a list page that sorts the list by that header.

I add it to the application helper and make minimal changes to the controller list action and add one routine to the application controller to make it work.

Here is the code for the helper:
  def link_to_sort(title, field_name)
    txt= '<a href="/' + @controller.controller_name + '/' + @controller.action_name + '?direction=' + @ndir + '&order=' + field_name + '">' + title + '</a>'
    if @order == field_name
      if @direction == 'ASC'
        txt += '<img alt="Arrow-up" class="dirimg" src="/images/arrow-up.png" />'
      else
        txt += '<img alt="Arrow-down" class="dirimg" src="/images/arrow-down.png" />'
      end
    end
    return txt
  end

As I mentioned, put this in app/helpers/application_helper.rb and it will be available to all your views. This assumes that you have two small images in you images directory called arrow-up.png and arrow-down.png to represent the direction of sort. I will leave that to the reader to supply.

To the application controller (app/controllers/application.rb) add the following routine:

  def get_order(default_order, default_direction = 'ASC')
    @order = params[:order] || default_order
    params[:direction] = 'DESC' if default_direction == 'DESC' and params[:direction] != 'ASC'
    if params[:direction] == 'ASC'
      @ndir = 'DESC'
    else
      @ndir = 'ASC'
    end
    @direction = params[:direction] || 'ASC'
    if @direction != 'ASC'
      @direction = 'DESC'
    end
    @od = @order.sub(',',' ' + @direction + ',') + ' ' + @direction
  end

With these two pieces of code in place, modifying your views and controllers to take advantage of sortable headers in the list view is simple

First modify the controller by adding:

:order => get_order('id')

To the end of the line that sets up paging for the list. So for example if you had a list of contacts the paging line would look like:

@contact_pages, @contacts = paginate :contacts, :per_page => 10, :order => get_order('id')

By the way the 'id' in the examples is the default sort column and can be changed to match your needs

Now in the view, replace your headings like:

<th>Name</th><th>City</th> ......

With

<th><%= link_to_sort 'Name', 'lastname, firstname' %></th>
<th><%= link_to_sort 'City', 'city' %></th> .....

As you might have guessed the two parameters that link_to_sort take are the Label for the column and the actual sort columns. You will notice in the first example you can give multiple columns for the sort. The first time you click the column will be sorted ascending and the second time you click it will be sorted descending.

A little arrow will appear beside the column when sort is active showing the direction of sort

Log4r and Rails 7

Posted by Tom Willett Thu, 01 Feb 2007 15:33:00 GMT

In a rails application I am working on I needed to set up logging for the events happening in the system. After a little perusing on the net I figured that log4r was the way to go. Then as with a lot of ruby apps, I found the documentation wanting. Yes I know the api and source is on line but a little more help to get started would be helpful. So I thought I would share what I had gleaned from the various sources and how I got it working.

Here are the problems I encountered and the solutions I came up with:

Install

Installation is easy using gem:

gem install log4r

Now you must include the log4r classes in your code with

require "log4r"

What is required for a simple logging setup?

First you must create a new logger. You can do that with code like:

MyLog = Log4r::Logger.new("mylog")

Now you must create an outputter for your log

I was interested in logging to a file so didn't bother with figuring out the ins and outs of logging to different devices.

To create a simple logger that logs to the file mylog.log in the rails log directory, you would create code like this:

Log4r::FileOutputter.new('my_log'
                       :filename=>"#{RAILS_ROOT}/log/mylog.log",
                       :trunc=>false)

This will create the log file named my_log which logs to the file mylog.log in the rails log directory and append to it. Note: Log4r has a log rotator, but since I am developing on linux, I elected to just let the system rotate my log files.

Now we must associate this file outputter with the logger we created earlier.

To do that we just user the following code:

myLog.add('my_log')

To put it all together

require "log4r"
MyLog = Log4r::Logger.new("mylog")
Log4r::FileOutputter.new('my_log'
                       :filename=>"#{RAILS_ROOT}/log/mylog.log",
                       :trunc=>false)
myLog.add('my_log')

Where do you put this code in a typical rails application?

I came up with two places, useful for different things.

First I placed it in config/environment.rb. This worked well, but I had to restart the web server every time I made a change. Debug messages were non-existent also.

Next I placed the setup in app/controllers/application.rb. This allowed me to make changes without restarting the webserver but slowed down the application.

So I did the initial setup and tweaking of the logging with the code in application.rb and moved it to environment.rb when it was done.

Now that we have the log setup how is it used?

Each log4r logger has default logging levels

DEBUG < INFO < WARN < ERROR < FATAL
To log something in our code you would use something like:
MyLog.info('My Log Message')
This will produce log output like
INFO MyLog: My Log Message

You log a message by giving the name of your logger and the log level.

What if I want to change the levels?

In the application I am writing I needed to log login/logouts and all activity of the users. To do this I wanted different log levels than the default. Here is how I did it: First I had to include the log4r configurator like this:

require "log4r/configurator"
Then I configured my custom levels:
Log4r::Configurator.custom_levels "Error","Access","Activity","Debug"
These two lines were placed immediately after the initial require, before creating the logger. Now I could create log messages with:
MyLog.access("User #{@user.name} logged in")
Producing a log message like:
Access MyLog: User tomw logged in

What about changing the log format?

Log4r has several different log format options built in Basic Formatter (the default formatter), Object Formatter, Pattern Formatter and Simple Formatter. None of these produced exactly the output I wanted and Log4r allows you to create your own formatters. The way you do this is to create a class of the type Log4r::Formatter and use it in place of the built in formatters. A Formatter class looks like this:

  class MyFormatter < Log4r::Formatter
    def format(event)
      buff = Time.now.strftime("%a %m/%d/%y %H:%M %Z")
      buff += " - #{Log4r::LNAMES[event.level]}"
      buff += " - #{event.data}\n"
    end
  end
This formatter produces log lines that look like this:
Thu 02/01/07 10:03 EST - Access - User Alfred E Newman From IP: 12.13.14.15 -- User Logged in.
Change the buff statements in your class to reflect how you want the log line to look. I have another log that I only wanted to contain the data I feed it. The formatter looks like this:
class UpFormatter < Log4r::Formatter
  def format(event)
      "#{event.data}\n"
  end
end
It puts a line in the log that only contain the data given it. Thus the log line:
MyLog.access("Some Log Event")
Will produce the following log line:
Some Log Event

How do you hook these new formatters into your log?

Its simple. When you are creating the log outputter simply add the formatter to the options hash, Thusly:

  Log4r::FileOutputter.new('my_log',
                          :filename=>"#{RAILS_ROOT}/log/mylog.log",
                          :trunc=>false,
                          :formatter=> MyFormatter)

This only scratches the surface of what you can do with log4r but should get you going.

Data Normalization in Rails 2

Posted by Tom Willett Wed, 20 Dec 2006 21:44:00 GMT

For a project I am working on (in Rails of course) I wanted to display currency like currency in a data field. This was easy just use the number_to_currency helper in the view. Ok maybe not so easy when you use the text_field helper. Some code will illus trate how to do it.

<%= text_field "invoice", 'total', :value => number_to_currency(@invoice['total']) %>

That only took a few minutes to figure out.

Now the problem comes trying to save the value back into the database. ActiveRecord typecasts $9.00 to 0.00 because $9.00 is con sidered a string. I knew it should be easy to do and after far too long looking for the answer I found it. In the model, use the b efore_save callback to make changes to the before_type_cast attribute. Some code will illustrate it:

class Invoice < ActiveRecord::Base
  before_save :fix_currency

  def fix_currency
    total_before_type_cast.tr!('$,','')
  end
end

You could, of course, add any other normalization routines here.

Hope this will save someone some time.

Install Ruby/Rails with FastCGI on Apache2 2

Posted by Tom Willett Thu, 21 Sep 2006 19:28:00 GMT

This is a basic howto on installing Ruby on Rails with FastCGI on Apache2. I assumes that you are using the Basic DebianServer with Apache2 with Perl and PHP already working.

Install Ruby with FastCGI on Apache2

apt-get install ruby ruby1.8-dev rdoc ri1.8 apache2-prefork-dev make

Install the FastCGI library

*Note 2.4.0 is the current version if it is not available visit www.fastcgi.com to get the latest.

cd /usr/local/src
wget http://www.fastcgi.com/dist/fcgi-2.4.0.tar.gz
tar xvzf fcgi-2.4.0.tar.gz
cd fcgi-2.4.0
./configure && make && make install

Installation of mod_fastcgi.so for Apache2

Download the mod_fastcgi sources from http://www.fastcgi.com/dist/mod_fastcgi-2.4.2.tar.gz. If the Link is not working, try to get the latest mod_fastcgi sources instead. Make the mod_fastcgi module.

cd /usr/local/src
wget http://www.fastcgi.com/dist/mod_fastcgi-2.4.2.tar.gz
tar xvzf mod_fastcgi-2.4.2.tar.gz
cd mod_fastcgi-2.4.2
cp Makefile.AP2 Makefile
make top_dir=/usr/share/apache2
make install top_dir=/usr/share/apache2

Enable mod_fastcgi in Apache2

echo 'LoadModule fastcgi_module /usr/lib/apache2/modules/mod_fastcgi.so' > /etc/apache2/mods-available/fastcgi.load
a2enmod fastcgi

Installation of ruby-fcgi bindings, Ruby GEM and Rails:

Download the ruby-fcgi bindings from http://sugi.nemui.org/pub/ruby/fcgi/ruby-fcgi-0.8.6.tar.gz. If the Link is not working, try to get the latest ruby-fcgi bindings instead.

cd /usr/local/src
wget http://sugi.nemui.org/pub/ruby/fcgi/ruby-fcgi-0.8.6.tar.gz
tar xvzf ruby-fcgi-0.8.6.tar.gz
cd ruby-fcgi-0.8.6
ruby install.rb config
ruby install.rb setup
ruby install.rb install
cd /usr/local/src
wget http://rubyforge.org/frs/download.php/5207/rubygems-0.8.11.tgz
tar zxvf rubygems-0.8.11.tgz
cd rubygems-0.8.11
ruby1.8 setup.rb
gem install rubygems-update
gem install rails --include-dependencies
gem install mysql

Answer 2 mysql 2.7(ruby)

Restart Apache:

/etc/init.d/apache2 restart

In the directories in which you want to run Ruby besure the following directive is listed in the .htaccess.

  AllowOverride all

Then setup you ruby environment in the .htaccess file