Posted by Jim Morris
Thu, 25 May 2006 06:20:00 GMT
I had a database with about 60 tables in it, most where simple lookup
tables with simple has_many and belongs_to relationships, and I didn't
want to manually create all the models, with the associations by hand.
I googled around and came across
Bill Katz's dbmodel,
which takes the output of dbdesigner and creates the models with the
relationships. However I already had the Databases and schema setup,
and I didn't have (and couldn't find) a copy of dbdesigner to use. So I
hacked Bills dbmodel to read a DDL file that was created from the
command rake db:structure:dump, as I was using Postgresql this file had
all the relevant relationship info in it plus a bit extra.
I added the ability to create the relationships that had non standard
table names and foreign keys, and also added some validations to the
created models.
By default all relationships are created as has_many (and belongs_to),
and tables can have multiple belongs_to referencing the same table so
long as the foreign keys are different. Also models are created for
every table found in the DDL.
I also added an yaml file as an override so you can specify habtm and
has_one relationships too.
There are two files to this modification, a hacked version of the
original dbmodel.rb and a new file which encapsulates the parsing of
the DDL file. The main changes to the original dbmodel.rb consists of
removing the parsing of the original xml file and reading the changes
from a modified hash of the tables and associations.
The new ddl.rb file handles the parsing of the DDL file, and building
a hash of data for the tables, with their associations and
validations. It also reads the YAML file that overides the association
types.
You can get a zip of the two files from this link ddl2model.zip.
Both files need to be in the same directory and it is run from the
command line, the yaml override being in the same directory as the
dbmodel.rb script. The ddl file to be processed is given on the
command line.
The YAML file assocs.yml is used to tell the ddl.rb script about
relations that are anything other than has_many, so:
extable1:
- {:assoc: has_one, :ref: extable2, :column: extable1_id}
- {:assoc: has_one, :ref: extable3, :column: extable1_id}
- {:assoc: habtm, :ref: extable1_extable2, :column: extable1_id}
extable2:
- {:assoc: habtm, :ref: extable1_extable2, :column: extable2_id}
Tells the parser that the model for a table named extable1 should create a has_one
relation for extable2 using the foreign key extable1_id. Ditto for extable3.
It also specifies that the model for extable2 has a habtm relationship
using the join table extable1_extable2 using foreign key extable1_id.
Note that the habtm override needs to be specified for both tables, in
this case extable2 also has a habtm override.
You can also ignore a table with this:
table3: []
will ignore the table called table3.
Lastly the generator will add validate_presence_of based on any NOT
NULL constraints found on the column DDL.
Update
A better solution to this problem has been provided here http://db-discovery.rubyforge.org/
Posted in Rails, Ruby | Tags activerecord, ddl, rails | 5 comments | no trackbacks
Posted by Jim Morris
Sat, 20 May 2006 20:19:00 GMT
This is an example of an admin page for the Rails Recipes book Role Based Authentication, using a
tree control and checkboxes for HABTM.
I used the Silverstripe tree control
to render two trees, on the left a list of all Roles and what Rights
they have. On the right a tree with all controllers and a checkbox for
each action for each controller, under that a set of checkboxes that
allow you to apply the selected rights to the selected Roles.
This technique seems a good match where the lists are hierarchical so
it can cut down on the screen real-estate for very large lists.
Example code...
In the model:
- The Rights class has_and_belongs_to_many :roles
- The Roles class has_and_belongs_to_many :rights
I setup the data by creating hashes in the controller which are available to the view...
In roles_controller.rb...
def edit
@role_tree= {}
roles= Role.find_all
roles.each do |r|
ph= {}
rights= r.rights
rights.each do |p|
ph[p.controller] ||= []
ph[p.controller] << p.action
end
@role_tree[r.name] = ph
end
@rights_tree= {}
rights= Right.find_all
rights.each do |r|
name= r.controller
@rights_tree[name] ||= []
@rights_tree[name] << r.action
end
end
The view (edit.rhtml) is simple...
<h1>Role and Rights Editor</h1>
<table cellspacing="10" cellpadding="20%">
<tr>
<th align="left">Roles</th>
<th align="left">Rights</th>
<tr>
<td valign="top"><%= render_role_tree(@role_tree) %></td>
<td><%= start_form_tag :action => 'edit' %>
<%= render_rights_tree(@rights_tree) %>
<p>
The checked items above will replace the Rights of the following
Roles:<br>
<% @role_tree.each_key do |role| %>
<%= check_box_tag "role", role %> <%= role %>
<% end %>
<p>
<%= submit_tag "Allocate to selected Roles" %>
<%= end_form_tag %>
</td>
</table>
This part goes in helpers, and creates the html for the two trees this is specific for
the silverstripe tree control...
module RoleHelper
def render_role_tree(tree)
ret = ''
ret += "<ul class='tree'>"
tree.each_key do |r|
ret += '<li><a href="#">' + r + '</a>'
h= tree[r]
unless h.empty?
ret += '<ul>'
h.each_key do |c|
ret += '<li><a href="#">' + c + '</a>'
a= h[c]
unless a.empty?
ret += '<ul>'
a.each do |an|
ret += '<li><a href="#">' + an + '</a></li>'
end
ret += '</ul>'
end
ret += '</li>'
end
ret += '</ul>'
end
ret += '</li>'
end
ret += '</ul>'
ret
end
def render_rights_tree(tree)
ret = ''
ret += "<ul class='tree'>"
tree.each_key do |r|
ret += "<li><a href=\"#\">#{r}</a>"
aa= tree[r]
unless aa.empty?
ret += '<ul>'
ret += "<li><a href=\"#\" onclick=\"checkAll('right[#{r}][]'); return false;\">Check All</a>"
aa.each do |a|
ret += '<li>' + "<input type=\"checkbox\" name=\"right[#{r}][]\" value=\"#{a}\" />" + a + '</li>'
end
ret += '</ul>'
end
ret += '</li>'
end
ret += '</ul>'
ret
end
end
the action handler in the controller for the post basically goes
through all the selected checkboxes and sets the relevant habtms.
def edit
if request.post?
unless params['role'].nil?
a_roles= params['role']
a_rights= params['right']
unless a_rights.nil?
a_roles.each { |ar|
role= Role.find_by_name(ar)
unless role.nil?
role.rights.clear
a_rights.each { |controller, aa|
aa.each { |action|
right= Right.find_by_controller_and_action(controller, action)
unless right.nil?
role.rights << right
else
puts "Right #{controller}/#{action} not found"
end
}
}
else
puts "Role #{ar} not found"
end
}
flash[:notice]= 'rights updated'
else
flash[:warning]= 'no rights were selected'
end
else
flash[:warning]= 'You need to select the role to allocate to'
end
end
end
Checking and unchecking all the boxes
This is done with a little bit of javascript.
The view code looks like...
<a href="#" onclick="checkAll('permissions_<%= controller_id %>[]'); return false;"\>all</a>
<a href="#" onclick="uncheckAll('permissions_<%= controller_id %>[]'); return false;"\>none</a>
The java script is...
function checkAll(name)
{
boxes = document.getElementsByName(name)
for (i = 0; i < boxes.length; i++)
boxes[i].checked = true ;
}
function uncheckAll(name)
{
boxes = document.getElementsByName(name)
for (i = 0; i < boxes.length; i++)
boxes[i].checked = false ;
}
Automatically adding Rights.
Lastly the question of how to get all those Rights into the table? I
liked the approach of the UserEngine so I borrowed it from them
(thanks:), Add this to the app/models/right.rb file, then whenever you
need to get all the new actions and controllers populated call
Right.synchronize_with_controllers from the console, (or create a
button on your admin page to do it). Here is the code...
class Right < ActiveRecord::Base
has_and_belongs_to_many :roles
validates_presence_of :controller, :action, :name
validates_uniqueness_of :name
def self.synchronize_with_controllers
require RAILS_ROOT + "/app/controllers/application"
controller_files = Dir[RAILS_ROOT + "/app/controllers/**/*_controller.rb"]
controller_files.each do |file_name|
require file_name
end
subclasses_of(ApplicationController).each do |controller|
controller.public_instance_methods(false).each do |action|
next if action =~ /return_to_main|component_update|component/
if find_all_by_controller_and_action(controller.controller_path, action).empty?
self.new(:name => "#{controller}.#{action}", :controller => controller.controller_path, :action => action).save!
logger.info "added: #{controller} - #{controller.controller_path}, #{action}"
end
end
self.find(:all, :conditions => ['controller = ?', controller.controller_path]).each do |right_to_go|
unless controller.public_instance_methods(false).include?(right_to_go.action)
right_to_go.destroy
end
end
end
end
end
Posted in Rails | Tags rails, RBAC, ruby | 10 comments | no trackbacks