RESTful Rails with Associations explored
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.
cd blog
script/generate scaffold_resource user name:string
script/generate scaffold_resource tag name:string
script/generate migration create_tags_users
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.
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
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.
A Weekend on Rails - Day One
Since I’ve been laid up and not supposedto be working, Ruby on Rails seemed like an interesting, low-impact thing to do. I’m a complete n00b when it comes to Ruby, so it was pretty much a jump in at the deep-end for me. I’d like to think that I was swimming with Ruby rather than sinking within a matter of hours,and I’d recoded a file autostore/autorename utility in a couple of days.
I picked up the basics of array handling, regular expression manipulation, and some of the syntax of Ruby learnt, I still hadn’t quite made the leap of faith into the “Convention rather than Configuration” paradigm, so I figured it was time to get more deeply immersed in the full Ruby/Rails methodologies by building a Rails based utility.
I like to do something useful when trying out something new. Not production ready coding, but enought to move a personal project ahead and I was writing a TV programme recording prioritisation module for my next stage refinement of the PVR project.
Requirements for this were to have an input which contains the Programme name, a single aka (if appropriate), imdb and tv.com id number, and a recording priority. At this top level, I didn’t care about episodes, season information or anything else. Next extension was going to be for Genres, but I didn’t want to worry about that yet.
I already have XML files which contain the information, and I’d decided to use SQLite V3 as the database, so the Rails task I set myself was to convert the XML to the database.
As I have complete control over the XML file, I didn’t require any validation. If there was a problem, I could trash the database, correct the source file and run again.
The form of the xml file was:
<programmes>
<programme priority=”3″ name=”Time Gentlemen Please”>
<programme priority=”4″ name=”Time Trumpet”>
<programme priority=”1″ name=”Torchwood”>
<programme priority=”1″ name=”Torchwood Declassified”>
</programmes>
with possible additional attributes of aka, imdb_com, tv_com which contain other programme names, and the imdb.com and tv.com ids for the programme.
I’d read a number of Rails articles about moving image files into the database, and decided that I would do this as an import file from a webform.
Since this is my first Rails program, I’ve decided to post it here so I can point and snigger at it later. Feel free to do the same. Assumption is that all of the build prerequisites have already been installed, and I can skip straight to code generation and development work.
First step is to create the rails application framework for this, which goes by the name of BorgTest. Since I’m using SQLite3, I have a slightly modified commandline for the creation.
Checking inside the config/database.yml shows that the correct database is being used
# SQLite version 3.x
# gem install sqlite3-ruby
development:
adapter: sqlite3
database: db/development.sqlite3
# Warning: The database defined as ‘test’ will be erased and
# re-generated from your development database when you run ‘rake’.
# Do not set this db to the same as development or production.
test:
adapter: sqlite3
database: db/test.sqlite3
production:
adapter: sqlite3
database: db/production.sqlite3
Next thing to do is generate a model and controller for the database and associated views.
script/generate model programme admin
Since I already know the exact format of the XML file, I’m going to create a database table with exactly the same columns as the names of the XML attributes.
db/migrate/001_create_programmes.rb
class CreateProgrammes < ActiveRecord::Migration
def self.up
create_table :programmes do |t|
t.column :name, :string
t.column :aka, :string, :default => “”
t.column :priority, :integer, :default => 0
t.column :tv_com, :string, :default => “”
t.column :imdb_com, :string, :default => “”
end
end
def self.down
drop_table :programmes
end
end
Save the above file and run
to create the initial table. Add the scaffold :programme to the app/controllers/admin_controller.rb file
class AdminController << ApplicationController
scaffold :programme
end
Fire up script/server and point the webrowser to http://localhost:3000/admin and check that it is working.
Ok. Initial build completed and I had the basic scaffolded CRUD application. Next post will cover the addition of the controller and model to actually upload the file and perform the import.
PVR Progress
I’ve decided that MythTV is still too bitty for the average user to set up and keep running. This is a shame, since it is obvious that a lot of time and effort has gone into building it, designing the plugin architecture etc.. I’m going to persevere with MythTV for the live home system for now, but I’ve decided to build an alternative - “The Borg Box”.
Mplayer and Xine are easy to use and have good LIRC support (remote controls are desirable in the McKibbin household), so this will start as a manager and wrapper for those rather than trying to implement all of their functionality. Weather I use occasionally, but doesn’t warrant the effort I would spend on it for a first pass, so I’ve decided to implement something similar to MythVideo first. As most code I write is cross platform, and development takes place equally on my Mac notebook and my office Debian machine, it was pot luck to see where I was going to start on this.
Mac notebook won, since I was waiting for a MythTV listings update, and wanted to be near the backend. Since I use MPlayer on the Mac, I decided to see how difficult it would be to pick up a file from the MythVideo database and play it.
MythTV stores all of its configuration data in a MySQL database called mythconverg. I picked up the username/password combination from ~/.mythtv/mysql.txt; logged in and started to examine the data. The MythTV box has been running for some time now and I’ve managed to accumulate a number of AVI files. Native Myth storage is in an MPG ring buffer, and conversion to XVID AVI’s is pretty well documented elsewhere, but if there are enough comments, I’ll produce more detail on how to do this.
All of the MythVideo converted files are referenced in the videometadata table, and you can pick up the path of the video episode from “filename”. All NFS mounted paths are the same across all of my computers, so I could run the file directly using the “open” command on the Mac. All that was really required was to set up access permissions to allow any computer on my network to access the database. A quick re-compile on a Debian machine, with the “xine -pqhf -n” instruction replacing “open” and it was running in Debian under X.
So I now have a program that will pick a random file from the MythTV database and play it on either Max or Debbie. Good enough for one night. I’m looking at making the final “Borg Box” commercial, once I have full integration.
Not that I would ship it on a Pentium II, but The Eden + Hauppauge combination works well, and Debian machines built on this platform may become my chosen distribution.
Hardware Purchases
I decided, after some examination of systems and costs, to explore two options. MythTV on Via EPIA boards and, mainly because I like the footprint, the mini Mac as clients. So the system running currently is:
- MythTV backend running on an EPIA M1000 with a 400 GB drive
- MythTV frontend running on an EPIA M1000 with a 2.5″ laptop boot drive
- Two MAC mini’s (Intel) currently running Apple’s frontend, but in the process of being converted to MythTV frontends.
PVR
About a year ago, I started looking into systems for home video and other media systems. The aim was to have a consumer friendly box to replace a plethora of others including DVD/CD player/recorder, DAB radio, VHS deck and possibly enable telephony (VoIP). The options: buy a consumer box, find an existing open-source or commercial solution, build a HTPC running Windows/Linux/Other, etc.
This category will chart recent and past progress and may serve somewhat as a warning how not to do this.