Enhancing Streamlined Enumerations

Posted by blackrat on March 26, 2008

Recently, I’ve been looking at the Streamlined framework. For those of you who don’t know, Streamlined is an Ajaxified Scaffold currently under development. The edge version shows promise and is stable enough for my personal use as an administration tool.
One area which is particularly interesting is the way that they handle enumerations and the fact that they are called late in the process rather than being instantiated once and then used. This may appear as an inefficiency at first glance, but in tracing through the call progress, I realized that you could make them more dynamic and allow for dynamic changes to the enumeration on a per item basis.
This means that if you have an exclusive list, you can restrict the choices to only those items that haven’t yet been assigned to other rows in the database.
For example, in one of my projects, you can assign a unique number to each row, and my desire was to restrict the view so that only the numbers that are available can be chosen.
So if you have possible numbers of
[1,2,3,4,5,6,7,8]

Assign 1 to the first row and for new items, [2,3,4,5,6,7,8] should be available, but [1,2,3,4,5,6,7,8] would be available for editing the first row.

Assign 5 to the second row and for new items, [2,3,4,6,7,8] should be available with [1,2,3,4,6,7,8] available for editing the first row and [2,3,4,5,6,7,8] available for editing the second row.

Coding this for the model is fairly straightforward:

class DynamicTest < ActiveRecord::Base
  def available_nodes
    node_list=[1,2,3,4,5,6,7,8]
    nodes=DynamicTest.find(:all)
    nodes.each do |n|
      node_list-=[n.number] unless n.number==number
    end
    node_list
  end
end

The unfortunate thing is that Streamlined doesn’t support this call, you can perform a call to DynamicTest.available_nodes, but that wouldn’t let you know what the current item is and you wouldn’t be able to see it in the list or edit views. Not very useful. What is needed is a way to call this directly from the row rendering code when you have the item in scope.
Since this is new functionality for Streamlined, the guys who maintain the codebase may adopt it, but for those of you who want to monkeypatch your own version or just see my take on it, you can download this sample project.

The monkeypatch (in app/streamlined/dynamic_tests_ui.rb) overrides four of the streamlined functions and adds two more for handling dynamic enumerations. This means that in addition to the original

Streamlined.ui_for(DynamicTest) do
  user_columns :number, {:enumeration => Numbers::TYPES}
end

class Numbers
  TYPES = [1,2,3,4,5,6,7,8]
end

and its Hash and 2d array counterparts, you can now have:

Streamlined.ui_for(DynamicTest) do
  user_columns :number, {:enumeration => {:action=>:available_nodes}}
end

which will perform a late call to the DynamicTest#available_nodes scoped for the current row.

For those of you who just want to look at the code without downloading a full rails project, the relevant monkeypatched pieces are:


#Note: There is a bug in _enumeration.html that prevents non-Fixednum numeric
#indices. This should be updated in the template version
#<% value = item.send(relationship.name) -%>
#<% key_value_pair = relationship.enumeration_key_for(value) -%>
#<%= key_value_pair ? key_value_pair.first : relationship.unassigned_value %>

module Streamlined::Controller::EnumerationMethods
  def dynamic_enumeration
    dynamic_enumeration_method=nil
    @enumeration_name=params[:enumeration]
    rel_type=model_ui.scalars[@enumeration_name.to_sym]
    rel_type.enumeration.each { |k,v|
      dynamic_enumeration_method=v if k==:action
    }
    dynamic_enumeration_method.nil? ? rel_type.enumeration : instance.send(dynamic_enumeration_method).to_2d_array
  end

  # Shows the enumeration’s configured +Edit+ view, as defined in streamlined_ui
  # and Streamlined::Column.
  def edit_enumeration
    self.instance = model.find(params[:id])
    @enumeration_name = params[:enumeration]
    rel_type = model_ui.scalars[@enumeration_name.to_sym]
    @all_items=dynamic_enumeration
    @selected_item = instance.send(@enumeration_name)
    render(:partial => rel_type.edit_view.partial, :locals => {:item => instance, :relationship => rel_type})
  end

  # Show’s the enumeration’s configured +Show+ view,
  # as defined in streamlined_ui and Streamlined::Column.
  def show_enumeration
    self.instance = model.find(params[:id])
    rel_type = model_ui.scalars[params[:enumeration].to_sym]
    rel_type.enumeration=dynamic_enumeration
    render(:partial => rel_type.show_view.partial, :locals => {:item => instance, :relationship => rel_type})
  end
end

class Streamlined::Column::ActiveRecord < Streamlined::Column::Base
  def dynamic_enumeration(item)
    dynamic_enumeration_method=nil
    @enumeration.each { |k,v|
      dynamic_enumeration_method=v if k==:action
    }
    dynamic_enumeration_method.nil? ? @enumeration : item.send(dynamic_enumeration_method)
  end

  def render_td_show(view, item)
    if enumeration
      content = item.send(self.name)
      @enumeration=dynamic_enumeration(item)
      key_value_pair = enumeration_key_for(content) # call wraps enumeration to 2d array, so check unnecessary
      content = key_value_pair.first if key_value_pair
      content = content && !content.blank? ? content : self.unassigned_value
      content = wrap_with_link(content, view, item)
    else
      render_content(view, item)
    end
  end

  def render_enumeration_select(view, item)
    id = relationship_div_id(name, item)
    @enumeration=dynamic_enumeration(item)
    choices = enumeration  #enumeration call wraps to 2d array so extra call is redundant
    choices.unshift(unassigned_option) if column_can_be_unassigned?(parent_model, name.to_sym)
    args = [model_underscore, name, choices]
    args << {} << html_options unless html_options.empty?
    view.select(*args)
  end
end

Rails IDE - Komodo 4.1

Posted by blackrat on May 23, 2007

I’m a great believer in free software, most of my systems are run using Apache, MySQL, Linux, and Ruby, as I’m sure quite a lot of you are running also. I’m also a great believer in the right tool for the right job, even if that isn’t a free tool. After using several of the free offerings, I downloaded the 4.0 beta of Komodo IDE to see if it was the right tool for developing RubyOnRails apps.

It was a little clunky. The editor and syntax highlighting were fine, and the approach to extending language support was also great. (I use Haml and Sass, rather than RHtml and CSS) The debugger took up to a minute to hit breakpoints, however, and although it was possible to use it , it was a little more difficult than I would have liked.

4.1 has changed all of that for me. Currently in beta, it is fast and shows the great strides that they have achieved. The Pro version is simply the best IDE I have used for Rails, bar none. I purchased mine and use Komodo now almost exclusively on all of my projects. Version 4.1 is available as a Trial from here.

Update: 4.1 has become the official release version, and is no longer in Beta. I feel an upgrade coming on.

Faster approach to identifying duplicates in a Ruby array

Posted by blackrat on May 02, 2007

Not mine, this one. Came from bshow here

a = [1,1,5,5,2,3,99,54,54,3,7,54,54,3,19]
a.inject({}) {|h,v| h[v]=h[v].to_i+1; h}.reject{|k,v| v==1}.keys.inspect # => [1, 3, 5, 54]

Ruby Fnord Generator - Part Two

Posted by blackrat on April 29, 2007

In Part 1, I took you through the beginnings of the Fnord generator up to the point we could create Fnords using random words and optional parts of speech. This gave us a class Fnord which contained the following functions.

require "fnord_words.rb"  #Arrays of NOUNS, ADJECTIVES, PLACES etc.
class Fnord
 def self.true?(chance) (chance==0 or rand(chance)<1) end
 def self.build(string_array,chance=0) true?(chance) ? string_array[rand(string_array.length)] : "" end
 def self.in_place(chance=0) true?(chance) ? "in #{build(PLACES)}" : "" end
 def self.adjective(chance=0) build(ADJECTIVES, chance) end
 def self.name(chance=0) build(NAMES, chance) end
 def self.place(chance=0) build(PLACES, chance) end
 def self.preposition(chance=0) build(PREPOSITIONS, chance) end
 def self.action(chance=0) build(ACTIONS, chance) end
 def self.pronoun(chance=0) build(PRONOUNS, chance) end
 def self.intro(chance=0) build(INTROS, chance) end
 def self.noun(chance=0) build(NOUNS, chance) end
end

I moved the word lists into their own file “fnord_words.rb”. Since these are separately generated and updated by SJ Games, it made sense to have them as separate files. I thought of writing a quick Perl->Ruby conversion to allow for the files to be dropped in, but since manually updating requires making only small changes to the file, I decided to leave this as an exercise for a later date.
I was using the normal

msg=case rand(14) #Return generated Fnord as a string
when 0: "The #{adjective(2)} #{noun} #{in_place(5)} is #{adjective}."
when 1: "#{name} #{action} the #{adjective} #{noun} and the #{adjective} #{noun}."
when 2: "The #{noun} from #{place} will go to #{place}."
when 3: "#{name} must take the #{adjective} #{noun} from #{place}."
when 4: "#{place} is #{adjective} and the #{noun} is #{adjective}."
.
.
.
when 13: "A #{noun} from #{place} #{action} the #{adjective(2)} #{adjective(5)} #{noun}."
end

and I figured I could move all of the data including the SENTENCES into a template to make it even easier to update. This required two things to happen.

  1. All of the parts of speech required are embedded in the string.
  2. The parts of speech can only be evaluated at runtime when the method is called.

We’d already achieved the first, and in order to have the second all that was needed was to move the SENTENCE strings into single quotes, such as

'#{intro(5)} the #{adjective(2)} #{adjective(2)} #{noun} #{action} the #{adjective(2)} #{adjective(2)} #{noun} #{in_place(2)}.'

In order to execute this later, you use the eval function to perform substitutions. Ignore normalize for now, it’s only to remove extra spaces and fixup capitalization.

def self.sentence(chance=0)
  normalize(eval('"'+build(SENTENCES,chance)+'"'))
end

private
def self.normalize(msg)
  while msg.include?("  ")
    msg.gsub!(/  /," ")
  end
  msg.gsub!(/^ /,"")
  msg.gsub!(/ ./,".")
  msg.gsub!(/[s^]([aA])s([aeiouhy])/,' 1n 2')
  msg[0]=msg[0,1].upcase
  while msg[/([^A-Z][.!?:])s+([a-z])/]
    msg[/([^A-Z][.!?:])s+([a-z])/]="#{$1} #{$2.upcase}"
  end
  msg
end

and voila. Sentences can be constructed by calling

print Fnord.sentence

We are still missing the word lists. You can grab the latest updated Perl version from SJ Games, or, if you are feeling really lazy, download my updated Ruby code from my website and grab my personalized Rubyised word lists from here. They aren’t strictly the Fnords word list, so you may prefer the SJ Games ones, but they are in the correct format.
If you’ve gone down the Perl wordlist route, you won’t have found the SENTENCES array, which you need for my code to work. You can either modify my oroginal code, or use the expanded versions below.

#Note. Parts of speech are coded in the main application and MUST NOT be expanded here. Hence only ' rather then " can be used.

SENTENCES=[
	'#{intro(5)} #{name} #{action} #{name} and #{pronoun} #{adjective(2)} #{adjective(2)} #{noun}.',
	'#{intro(5)} #{name} #{action} the #{adjective(2)} #{adjective(2)} #{noun} and the #{adjective(2)} #{adjective(2)} #{noun} #{in_place(2)}.',
	'#{intro(5)} #{name} #{action} the #{adjective(2)} #{adjective(2)} #{noun} of #{place}.',
	'#{intro(5)} #{name} #{preposition} #{place} and #{action} the #{adjective(2)} #{adjective(2)} #{noun}.',
	'#{intro(5)} #{name} #{preposition} #{place} for the #{adjective(2)} #{adjective} #{noun}.',
	'#{intro(5)} #{name} is the #{adjective(2)} #{adjective(2)} #{noun}; #{name} #{preposition} #{place}.',
	'#{intro(5)} #{name} must take the #{adjective(2)} #{adjective(2)} #{noun} from #{place}.',
	'#{intro(5)} #{name} takes #{pronoun} #{adjective(2)} #{adjective(2)} #{noun} and #{preposition} #{place}.',
	'#{intro(5)} #{place} is #{adjective} and the #{noun} is #{adjective}.',
	'#{intro(5)} a #{adjective(2)} #{adjective(2)} #{noun} from #{place} #{action} the #{adjective(2)} #{adjective(2)} #{noun}.',
	'#{intro(5)} the #{adjective(2)} #{adjective(2)} #{noun} #{action} the #{adjective(2)} #{adjective(2)} #{noun} #{in_place(2)}.',
	'#{intro(5)} the #{adjective(2)} #{adjective(2)} #{noun} #{in_place(2)} is #{adjective}.',
	'#{intro(5)} the #{adjective(2)} #{adjective(2)} #{noun} from #{place} will go to #{place}.',
	'#{intro(5)} you must meet #{name} at #{place} and get the #{adjective(2)} #{adjective(2)} #{noun}.'
]

Enjoy

Ruby Fnord Generator - Part One

Posted by blackrat on April 28, 2007

I recently came across the Steve Jackson Games Fnords program and thought that there should be a really cool and easy way to generate this type of sentence using Ruby. The basic priciple is that you take arrays of NOUNS, PLACES, VERBS, and other parts of speech and generate syntacically correct nonsensical sentences. A little bit like a truncated form of madlibs.

Ruby embedded code in strings was something that I’d played around with, and I thought that it should be relatively easy to base sentence construction using methods embedded in the strings. So basically

"#{name} is a #{adjective} #{noun}."

could be run through the generator and create “Mac is a brown dog”, “Sigmund Freud is a coherent lightbulb”, type of sentences. Fairly basic stuff with NAME, ADJECTIVE and NOUN being arrays of appropriate words and the method, for example, of self.name being defined as

def self.name NAME[rand(NAME.length)] end

If you’ve taken a look at the other Fnords programs at SJ Games (or even my early one which is now downloadable from there), you will have noticed that the sentences themselves have a random chance of having some parts of speech included. It was this random element that first attracted me to solving the problem using Ruby.
Most code on the site uses logic of the following form.

msg="The"                           # The
if rand(2) == 0	               	    # adjective - (50% chance)
  msg+=adjecive
end
msg+=noun                          # noun
if rand(5) == 0                      # in place  - (20% chance)
  msg+="in #{place}"
end
msg+="is #{adjective}."         # is adjective.

That wasn’t DRY enough for me, and too much code when a little would do the same thing. Moving the (rand()) into the actual function that defined the part of speech and making the return from the call optional would do almost the same thing. Worst case I would have to fix up spaces at the end. (I actually decided to do space fixup as a final pass, since it meant I could just type the sentences spaced normally, which makes it easier to maintain.)
Ok so with the method for noun now as

def self.noun(chance=0)
  if (rand(chance)<1) then
     NOUN[rand(NOUN.length)]
  else
     ""
  end
end 

The way the Ruby rand works, passing it 0 gives a random number between 0 and 1 and passing a positive integer greater than 0 generates a random integer, so with this code rand(0)<1 is always true, so the next iteration changed this to

if (chance==0 or rand(chance)<1)

Finally to DRY it up even more, I removed the randomness into two separate methods and switched to the “? :” form of “if then”.

def self.true?(chance)
  (chance==0 or rand(chance)<1)
end
def self.build(string_array,chance=0)
  true?(chance) ? string_array[rand(string_array.length)] : ""
end

which meant that my noun method could now simplify again to

def self.noun(chance=0)
  build(NOUNS, chance)
end

One special case was required to allow for “in place” as optional rather than just “place”.

def self.in_place(chance=0)
  true?(chance) ? "in #{build(PLACES)}" : ""
end

Now I could create strings using my desired form.

msg="The #{adjective(2)} #{noun} #{in_place(5)} is #{adjective}."

Part 2 will examine how to take this and create the fully functional fnords program.

Fast extraction of duplicate array items into a new array

Posted by blackrat on April 18, 2007

Whilst creating a natural language parser, one of the things I was presented with was multiple merged dictionaries, which needed some processing. I was asked to supply a list of duplicated words back, and after trawling the web finding slow code, I decided that going the fast, but inefficient (in terms of space) was the way to go.
The object is to return an array of just those elements that are duplicated in as little time as possible, from a 50,000 word list. One sort and one lookup per element was what I came up with


array=["long","array","with","lots","and","lots","and","lots","of","array","duplicates","long","and","array"]
arr2=array.sort
arr3=[]
arr4=[]
arr2.each { |a| (arr3[-1]==a) ? (arr4[-1]!=a) ? arr4 << a : "" : arr3 << a }
arr3 => [”and”,”array”,”duplicates”,”long”,”lots”,”of”,”with”]
arr4 => [”and”,”array”,”long”,”lots”]

If you are sure that there are fewer duplicates in the returned array of just duplicated elements, you can remove the arr4 checks at the expense of a final .uniq! pass. i.e.


array=["long","array","with","lots","and","lots","and","lots","of","array","duplicates","long","and","array"]
arr2=array.sort
arr3=[]
arr4=[]
arr2.each { |a| (arr3[-1]==a) ? arr4 << a : arr3 << a }
arr4.uniq!

arr3 => [”and”,”array”,”duplicates”,”long”,”lots”,”of”,”with”]
arr4 => [”and”,”array”,”long”,”lots”]

Looks Quiet. Isn’t.

Posted by blackrat on April 16, 2007

Maintaining 4 separate blogs (2 project specific and intraneted, rather than external), doesn’t take any more time than 1, but it means that less gets added to this one than might otherwise be the case. However, one thing I can share is the work that’s been going on under the covers to get some Ajax code working on Blogger. (Yet another blog). This all came about when I had looked at some code and thought. Hmm. That would be so much easier to create, maintain and extend in Ruby.
I didn’t want the full blown features of Rails, but since I was experimenting with cutting down the code needed for yet another project, I thought that this would give me a good quick way of injecting Ajax code into a 3rd party site.
One of the rules of Ajax, is that you can’t go outside the domain for the call. So where an XMLHttpRequest.open(’GET’,'http://random.external.site.com/CGI_program’), would throw an error, you can do this by performing the call from an page on http://random.external.site.com.

For example, the header on Nofnords is an Ajaxified update from fnord.pqmf.com (a subdomain of this site), which has been embedded using an iframe which uses http://fnord.pqmf.com/fnord.html as the jumping off point. What this means is that the XMLHttpRequest comes from the same domain as the iframe’d page. Since this is a separate request, there is still a separate between the domains, so data isn’t accessible between the two pieces of content directly, but with some CSS, the appearance is (almost) seamless, and allows for some “personalized” customisation that would otherwise be difficult to achieve.

Displaying a subset of items from an association

Posted by blackrat on December 16, 2006

As of this post I have two classes joined by have_and_belongs_to_many (habtm), and I figured that I’d like to be able to click on an entry in one and have a subset index of the other appear. Not an uncommon task and used by tags and blogs everywhere. Rather than search for a snippet, I thought I’d give it a go myself first.What I came up with surprised me with it’s simplicity and may not be the most expedient manner, but seems to be a neat and easy extension of the index method.

The obvious way was to add an entry to the list of actions which can be applied to a tag item. A simple addition of a “Show Users” link pointing to the Users controller and passing a tag_id parameter was the starting point for this.

Opening up app/views/tags/index.rhtml, we can add a user_path with :tag_id=>tag as the parameter. This will translate to http://localhost:3000/users?tag_id=x.


<h1>Listing tags</h1>

<table>
  <tr>
    <th>Name</th>
  </tr>

<% for tag in @tags %>
  <tr>
    <td><%=h tag.name %></td>
    <td><%= link_to 'Users', user_path(:tag_id=>tag) %></td>
    <td><%= link_to 'Show', tag_path(tag) %></td>
    <td><%= link_to 'Edit', edit_tag_path(tag) %></td>
    <td><%= link_to 'Destroy', tag_path(tag), :confirm => 'Are you sure?', :method => :delete %></td>
  </tr>
  <% end %>
</table>
<br />
<%= link_to 'New tag', new_tag_path %>

This will then allow the following in the index view of the users controller.

  # GET /users
  # GET /users.xml
  def index
    @tag=Tag.find(params[:tag_id]) if params[:tag_id]
    @users = @tag.users || throw rescue User.find(:all)

    respond_to do |format|
      format.html # index.rhtml
      format.xml  { render : xml => @users.to_xml }
    end
  end

Note the unusual throw rescue construct. Since @tag.users would throw an exception, as well as having a nil possibility, this is the dryest form of getting the original User.find(:all) executed on error. I mainly wanted to see if this would work, and it does! I’m really starting to like what you can do with Ruby.

RESTful Rails with Associations explored

Posted by blackrat on December 16, 2006

I’ve recently switched to EdgeRails for Rails 1.2 RC1, and decided to try out the resource_scaffold to create RESTful classes. There’s not that much documentation on this so far, so I thought I’d combine using these with creating habtm associations. What follows is a walkthrough of the initial creation of a has_and_belongs_to_many association and the changes required to get it to do what I want within the scaffold_resource RESTful framework.

First of all, some MVC creation. I like the fact that tests are now generated by the scaffold_resource, but be warned that if you modify your migrations, you also need to change the test/fixtures/*.yml files to reflect the changes. Otherwise, your unit tests will start to error. We’re going to be using the extension to scaffold_resource and generate a string “name” for both tables, which will also generate the fixture unit test for that column.

rails –database=sqlite3 blog
cd blog
script/generate scaffold_resource user name:string
script/generate scaffold_resource tag name:string
script/generate migration create_tags_users
Change db/migration/003_create_tags_users.rb to the following:
class CreateTagsUsers < ActiveRecord::Migration
    def self.up
        create_table :tags_users, :id=>false do |t|
            t.column :tag_id, :integer
            t.column :user_id, :integer
        end
    end
    def self.down
        drop_table :tags_users
    end
end

Don’t forget the :id=>false (unless you know what you are doing, that is).
Next, modify the models to point to each other. app/models/tag.rb and app/models/user.rb become:

class Tag < ActiveRecord::Base
    has_and_belongs_to_many :users
end

and

class User < ActiveRecord::Base
    has_and_belongs_to_many :tags
end

At this point, I like to run the migrate and the tests which were created by the scaffold_resource.

rake db:migrate test
Now it’s time for a little association plumbing. I like to have a view to allow me to select the tags from the full list (we’ll ignore adding more from this view for this walkthrough) when in the user display, so the first port of call is the users_controller. I’d also like to be able to see from the index display which ones are selected, so I’ll be modifying the new, edit, create and update methods and all of the views for user.

Ok. For app/views/users the following become the new views

index.rhtml

<h1>Listing users</h1>
<table>
    <tr>
          <th>Name</th>
          <th>Tags</th>
    </tr>
    <% for user in @users %>
          <tr>
               <td><%=h user.name %></td>
               <td>
                    <% for tag in user.tags %>
                         <%= tag.name %><br />
                    <% end %>
               </td>
               <td><%= link_to 'Show', user_path(user) %></td>
               <td><%= link_to 'Edit', edit_user_path(user) %></td>
               <td><%= link_to 'Destroy', user_path(user), :confirm => 'Are you sure?', :method => :delete %></td>
        </tr>
    <% end %>
</table>
<br />
<%= link_to 'New user', new_user_path %>

new.rhtml

<h1>New user</h1>
<%= error_messages_for :user %>
<% form_for(:user, :url => users_path) do |f| %>
    <p>
          <b>Name</b><br />
          <%= f.text_field :name %>
     </p>
    <p>
        <b>Tags</b><br />
          <% for tag in @tags %>
               <br />
            <input type="checkbox"
                            id="<%=tag.id%>"
              name="tag_ids[]"
                            value="<%=tag.id%>"
                        >
            <%=tag.name%>
        <% end %>
    </p>
     <p>
          <%= submit_tag "Create" %>
     </p>
<% end %>
<%= link_to 'Back', users_path %>

show.rhtml

<p>
    <b>Name:</b>
     <%=h @user.name %>
</p>
<p>
    <b>Tags:</b>
     <% for tag in @user.tags %>
          <%= tag.name %><br />
     <% end %>
</p>
<%= link_to 'Edit', edit_user_path(@user) %> |
<%= link_to 'Back', users_path %>

and finally, edit.rhtml

<h1>Editing user</h1>
<%= error_messages_for :user %>
<% form_for(:user, :url => user_path(@user), :html => { :method => :put }) do |f| %>
    <p>
          <b>Name</b><br />
          <%= f.text_field :name %>
     </p>
     <p>
          <b>Tags</b><br />
          <% for tag in @tags %>
            <br />
               <input type="checkbox"
                   id="<%=tag.id%>"
                   name="tag_ids[]"
                   value="<%=tag.id%>"
                   <%if @user.tags.include? tag%>checked="checked"<%end%>
               >
               <%=tag.name%>
          <% end %>
     </p>
     <p>
          <%= submit_tag "Update" %>
     </p>
<% end %>
<%= link_to 'Show', user_path(@user) %> |
<%= link_to 'Back', users_path %>

So not that much different to the CRUD scaffolding. I’m going to experiment more with this, and also with the through :class approach of handling associations, which I believe looks to have some strong advantages (not the least of which being that it gives you a join class rather than a join table).

A Weekend on Rails - Day Two

Posted by blackrat on November 19, 2006

Last time, I built a standard scaffolded Rails application with a modified database, controller (admin) controlling a model named programme. The next step is to add a view and import mechanism. The standard scaffold mechanism uses a list.rhtml as the entry point, so we will create one of these to override the scaffold, and display a list of the current programmes, along with the import button.

<h1>Listing programmes</h1>
  <table>
    <tr>
      <% for column in Programme.content_columns %>
        <th><%= column.human_name %></th>
      <% end %>
    </tr>
    <% for programme in @programmes %>
      <tr>
        <% for column in Programme.content_columns %>
          <td><%=h programme.send(column.name) %></td>
        <% end %>
      </tr>
    <% end %>
  </table>
  <%= link_to ‘Previous page’, { :page => @programme_pages.current.previous } if @programme_pages.current.previous %>
  <%= link_to ‘Next page’, { :page => @programme_pages.current.next } if @programme_pages.current.next %><br />
  <%= link_to ‘Import XML’, :action => ‘load_xml’ %>

and add a new “load_xml.rhtml” template

<h1>Load XML<h1>
<%= form_tag({ :action=> ‘import_xml’}, {multipart => true }) %>
<%= file_field :document, :file %>
<%= submit_tag ‘Import’ %>
<%= end_form_tag ‘Import’ %>
<%= link_to ‘Back’, :action => ‘list’ %>

Ok.So much for the easy part. We need to add an entry to the controllerfor the ‘import_xml’ action that has just been declared. This will haveaccess to the embedded file uploaded using the file_field :document,:file instruction.

The decision to make the database table columns have exactly the same namesas the XML attributes allows for the following code to populate thedatabase. So the final app/controller/admin_controller.rb looks like:

class AdminController << ApplicationControllerscaffold :programme
  def import_xml
    require ‘rexml/document’
    file=params[:document][:file]
    doc=REXML::Document.new(file.read)
    doc.root.each_element(’//programme’) do |p|
      if not Programme.find(:first, :conditions => [ “name=?”, p.attibutes[:name] ]) then
        @programme=Programme.new
        @programme.update_attributes(p.attributes)
      end
    end
    redirect_to :action => ‘list’
  end
end

Ok. That’s it for now. I’ve scratched the rails surface and learnt something new. More discoveries soon.