From 061fa28f652fc9214e9cf480d66870140181edef Mon Sep 17 00:00:00 2001 From: Erik Michaels-Ober Date: Mon, 26 Oct 2009 12:31:08 -0700 Subject: [PATCH] added Sequel support --- lib/abstract_model.rb | 14 + lib/merb-admin/slicetasks.rb | 25 +- lib/sequel_support.rb | 309 ++++++++++++++++++ .../sequel/001_create_divisions_migration.rb | 15 + .../sequel/002_create_drafts_migration.rb | 21 ++ .../sequel/003_create_leagues_migration.rb | 14 + .../sequel/004_create_players_migration.rb | 22 ++ .../sequel/005_create_teams_migration.rb | 24 ++ spec/models/sequel/division.rb | 15 + spec/models/sequel/draft.rb | 19 ++ spec/models/sequel/league.rb | 14 + spec/models/sequel/player.rb | 18 + spec/models/sequel/team.rb | 22 ++ spec/requests/main_spec.rb | 6 + spec/spec_helper.rb | 4 + 15 files changed, 540 insertions(+), 2 deletions(-) create mode 100644 lib/sequel_support.rb create mode 100644 spec/migrations/sequel/001_create_divisions_migration.rb create mode 100644 spec/migrations/sequel/002_create_drafts_migration.rb create mode 100644 spec/migrations/sequel/003_create_leagues_migration.rb create mode 100644 spec/migrations/sequel/004_create_players_migration.rb create mode 100644 spec/migrations/sequel/005_create_teams_migration.rb create mode 100644 spec/models/sequel/division.rb create mode 100644 spec/models/sequel/draft.rb create mode 100644 spec/models/sequel/league.rb create mode 100644 spec/models/sequel/player.rb create mode 100644 spec/models/sequel/team.rb diff --git a/lib/abstract_model.rb b/lib/abstract_model.rb index faf8a9c..95d6775 100644 --- a/lib/abstract_model.rb +++ b/lib/abstract_model.rb @@ -24,6 +24,15 @@ def self.all @models << new(model) if model end @models.sort!{|a, b| a.model.to_s <=> b.model.to_s} + when :sequel + Dir.glob(Merb.dir_for(:model) / Merb.glob_for(:model)).each do |filename| + # FIXME: This heuristic for finding Sequel models could be too strict + File.read(filename).scan(/^class ([\w\d_\-:]+) < Sequel::Model$/).flatten.each do |m| + model = lookup(m.to_s.to_sym) + @models << new(model) if model + end + end + @models.sort!{|a, b| a.model.to_s <=> b.model.to_s} else raise "MerbAdmin does not support the #{Merb.orm} ORM" end @@ -42,6 +51,8 @@ def self.lookup(model_name) return model if model.superclass == ActiveRecord::Base when :datamapper return model if model.include?(DataMapper::Resource) + when :sequel + return model if model.superclass == Sequel::Model end nil end @@ -59,6 +70,9 @@ def initialize(model) when :datamapper require 'datamapper_support' self.extend(DatamapperSupport) + when :sequel + require 'sequel_support' + self.extend(SequelSupport) else raise "MerbAdmin does not support the #{Merb.orm} ORM" end diff --git a/lib/merb-admin/slicetasks.rb b/lib/merb-admin/slicetasks.rb index 6162d29..c579dd4 100644 --- a/lib/merb-admin/slicetasks.rb +++ b/lib/merb-admin/slicetasks.rb @@ -51,6 +51,27 @@ end end + desc "Copies sample models, copies and runs sample migrations, and loads sample data" + task :sequel => ["sequel:copy_sample_models", "sequel:copy_sample_migrations", "sequel:migrate", "load_sample_data"] + namespace :sequel do + desc "Copies sample models into your app" + task :copy_sample_models do + copy_models(:sequel) + end + + desc "Copies sample migrations into your app" + task :copy_sample_migrations do + copy_migrations(:sequel) + end + + desc "Perform migration using migrations in schema/migrations" + task :migrate do + require 'sequel/extensions/migration' + Rake::Task["sequel:db:migrate"].reenable + Rake::Task["sequel:db:migrate"].invoke + end + end + desc "Loads sample data into your app" task :load_sample_data do load_data @@ -63,13 +84,13 @@ def load_data begin - require "mlb" + require "mlb" rescue LoadError => e puts "LoadError: #{e}" puts "gem install mlb -s http://gemcutter.org" return end - + puts "Loading current MLB leagues, divisions, teams, and players" MLB.teams.each do |mlb_team| unless league = MerbAdmin::AbstractModel.new("League").first(:conditions => ["name = ?", mlb_team.league]) diff --git a/lib/sequel_support.rb b/lib/sequel_support.rb new file mode 100644 index 0000000..2013ebb --- /dev/null +++ b/lib/sequel_support.rb @@ -0,0 +1,309 @@ +require 'sequel' +require 'sequel/extensions/pagination' + +class Sequel::Model +=begin + # Intialize each column to the default value for new model objects + def after_initialize + super + model.columns.each do |x| + if !@values.include?(x) && db_schema[x][:allow_null] + send("#{x}=", db_schema[x][:ruby_default]) + end + end + end +=end + + # Return an empty array for *_to_many association methods for new model objects + def _load_associated_objects(opts) + opts.returns_array? && new? ? [] : super + end +end + +module MerbAdmin + class AbstractModel + module SequelSupport + def get(id) + model.first(:id => id).extend(InstanceMethods) + end + + def count(options = {}) + if options[:conditions] && !options[:conditions].empty? + # If options[:conditions] isn't cloned, Sequel eats the first condition! + model.where(options[:conditions].clone).count + else + model.count + end + end + + def first(options = {}) + sort = options.delete(:sort) || :id + sort_order = options.delete(:sort_reverse) ? :desc : :asc + + if options[:conditions] && !options[:conditions].empty? + # If options[:conditions] isn't cloned, Sequel eats the first condition! + model.order(sort.to_sym.send(sort_order)).first(options[:conditions].clone).extend(InstanceMethods) + else + model.order(sort.to_sym.send(sort_order)).first.extend(InstanceMethods) + end + end + + def all(options = {}) + offset = options.delete(:offset) + limit = options.delete(:limit) + + sort = options.delete(:sort) || :id + sort_order = options.delete(:sort_reverse) ? :desc : :asc + + if options[:conditions] && !options[:conditions].empty? + # If options[:conditions] isn't cloned, Sequel eats the first condition! + model.where(options[:conditions].clone).order(sort.to_sym.send(sort_order)) + else + model.order(sort.to_sym.send(sort_order)) + end + end + + def paginated(options = {}) + page = options.delete(:page) || 1 + per_page = options.delete(:per_page) || MerbAdmin[:per_page] + page_count = (count(options).to_f / per_page).ceil + + sort = options.delete(:sort) || :id + sort_order = options.delete(:sort_reverse) ? :desc : :asc + + if options[:conditions] && !options[:conditions].empty? + # If options[:conditions] isn't cloned, Sequel eats the first condition! + [page_count, model.paginate(page.to_i, per_page).where(options[:conditions].clone).order(sort.to_sym.send(sort_order))] + else + [page_count, model.paginate(page.to_i, per_page).order(sort.to_sym.send(sort_order))] + end + end + + def create(params = {}) + model.create(params) + end + + def new(params = {}) + model.new(params).extend(InstanceMethods) + end + + def destroy_all! + model.all.each do |object| + object.destroy + end + end + + def has_many_associations + associations.select do |association| + association[:type] == :has_many + end + end + + def has_one_associations + associations.select do |association| + association[:type] == :has_one + end + end + + def belongs_to_associations + associations.select do |association| + association[:type] == :belongs_to + end + end + + def associations + model.all_association_reflections.map do |association| + { + :name => association_name_lookup(association), + :pretty_name => association_pretty_name_lookup(association), + :type => association_type_lookup(association), + :parent_model => association_parent_model_lookup(association), + :parent_key => association_parent_key_lookup(association), + :child_model => association_child_model_lookup(association), + :child_key => association_child_key_lookup(association), + } + end + end + + def properties + model.columns.map do |property| + { + :name => property, + :pretty_name => property.to_s.gsub(/_id$/, "").gsub("_", " ").capitalize, + :type => property_type_lookup(property), + :length => property_length_lookup(property), + :nullable? => model.db_schema[property][:allow_null], + :serial? => model.db_schema[property][:primary_key], + } + end + end + + private + + def property_type_lookup(property) + case model.db_schema[property][:db_type] + when /\A(?:medium|small)?int(?:eger)?(?:\((?:\d+)\))?\z/io + :integer + when /\Atinyint(?:\((\d+)\))?\z/io + :boolean + when /\Abigint(?:\((?:\d+)\))?\z/io + :integer + when /\A(?:real|float|double(?: precision)?)\z/io + :float + when 'boolean' + :boolean + when /\A(?:(?:tiny|medium|long|n)?text|clob)\z/io + :text + when 'date' + :date + when /\A(?:small)?datetime\z/io + :datetime + when /\Atimestamp(?: with(?:out)? time zone)?\z/io + :datetime + when /\Atime(?: with(?:out)? time zone)?\z/io + :time + when /\An?char(?:acter)?(?:\((\d+)\))?\z/io + :string + when /\A(?:n?varchar|character varying|bpchar|string)(?:\((\d+)\))?\z/io + :string + when /\A(?:small)?money\z/io + :big_decimal + when /\A(?:decimal|numeric|number)(?:\((\d+)(?:,\s*(\d+))?\))?\z/io + :big_decimal + when 'year' + :integer + else + :string + end + end + + def property_length_lookup(property) + case model.db_schema[property][:db_type] + when /\An?char(?:acter)?(?:\((\d+)\))?\z/io + $1 ? $1.to_i : 255 + when /\A(?:n?varchar|character varying|bpchar|string)(?:\((\d+)\))?\z/io + $1 ? $1.to_i : 255 + else + nil + end + end + + def association_name_lookup(association) + case association[:type] + when :one_to_many + if association[:one_to_one] + association[:name].to_s.singularize.to_sym + else + association[:name] + end + when :many_to_one + association[:name] + else + raise "Unknown association type" + end + end + + def association_pretty_name_lookup(association) + case association[:type] + when :one_to_many + if association[:one_to_one] + association[:name].to_s.singularize.gsub('_', ' ').capitalize + else + association[:name].to_s.gsub('_', ' ').capitalize + end + when :many_to_one + association[:name].to_s.gsub('_', ' ').capitalize + else + raise "Unknown association type" + end + end + + def association_type_lookup(association) + case association[:type] + when :one_to_many + if association[:one_to_one] + :has_one + else + :has_many + end + when :many_to_one + :belongs_to + else + raise "Unknown association type" + end + end + + def association_parent_model_lookup(association) + case association[:type] + when :one_to_many + association[:model] + when :many_to_one + Object.const_get(association[:class_name]) + else + raise "Unknown association type" + end + end + + def association_parent_key_lookup(association) + [:id] + end + + def association_child_model_lookup(association) + case association[:type] + when :one_to_many + Object.const_get(association[:class_name]) + when :many_to_one + association[:model] + else + raise "Unknown association type" + end + end + + def association_child_key_lookup(association) + case association[:type] + when :one_to_many + association[:keys] + when :many_to_one + ["#{association[:class_name].snake_case}_id".to_sym] + else + raise "Unknown association type" + end + end + + module InstanceMethods + def id + super + end + + def save + super + end + + def destroy + super + end + + def update_attributes(attributes) + # NOTE: Not sure why calling update(attributes) raises + # Argument Error: wrong number of arguments (1 for 0) + # but this seems to work: + set(attributes) + save + end + + def errors + super + end + + def clear_association(association) + association.clear # FIXME! + end + + def reset + super + end + end + + end + end +end diff --git a/spec/migrations/sequel/001_create_divisions_migration.rb b/spec/migrations/sequel/001_create_divisions_migration.rb new file mode 100644 index 0000000..06d0fc0 --- /dev/null +++ b/spec/migrations/sequel/001_create_divisions_migration.rb @@ -0,0 +1,15 @@ +class CreateDivisions < Sequel::Migration + def up + create_table(:divisions) do + primary_key(:id) + DateTime(:created_at) + DateTime(:updated_at) + foreign_key(:league_id, :table => :leagues) + String(:name, :limit => 50, :null => false) + end + end + + def down + drop_table(:divisions) + end +end diff --git a/spec/migrations/sequel/002_create_drafts_migration.rb b/spec/migrations/sequel/002_create_drafts_migration.rb new file mode 100644 index 0000000..d461cf8 --- /dev/null +++ b/spec/migrations/sequel/002_create_drafts_migration.rb @@ -0,0 +1,21 @@ +class CreateDrafts < Sequel::Migration + def up + create_table(:drafts) do + primary_key(:id) + DateTime(:created_at) + DateTime(:updated_at) + foreign_key(:player_id, :table => :players) + foreign_key(:team_id, :table => :teams) + Date(:date) + Integer(:round) + Integer(:pick) + Integer(:overall) + String(:college, :limit => 100) + String(:notes, :text=>true) + end + end + + def down + drop_table(:drafts) + end +end diff --git a/spec/migrations/sequel/003_create_leagues_migration.rb b/spec/migrations/sequel/003_create_leagues_migration.rb new file mode 100644 index 0000000..58ee8c9 --- /dev/null +++ b/spec/migrations/sequel/003_create_leagues_migration.rb @@ -0,0 +1,14 @@ +class CreateLeagues < Sequel::Migration + def up + create_table(:leagues) do + primary_key(:id) + DateTime(:created_at) + DateTime(:updated_at) + String(:name, :limit => 50, :null => false) + end + end + + def down + drop_table(:leagues) + end +end diff --git a/spec/migrations/sequel/004_create_players_migration.rb b/spec/migrations/sequel/004_create_players_migration.rb new file mode 100644 index 0000000..fb1566f --- /dev/null +++ b/spec/migrations/sequel/004_create_players_migration.rb @@ -0,0 +1,22 @@ +class CreatePlayers < Sequel::Migration + def up + create_table(:players) do + primary_key(:id) + DateTime(:created_at) + DateTime(:updated_at) + DateTime(:deleted_at) + foreign_key(:team_id, :table => :teams) + String(:name, :limit => 100, :null => false) + String(:position, :limit => 50) + Integer(:number, :null => false) + TrueClass(:retired, :default => false) + TrueClass(:injured, :default => false) + Date(:born_on) + String(:notes, :text=>true) + end + end + + def down + drop_table(:players) + end +end diff --git a/spec/migrations/sequel/005_create_teams_migration.rb b/spec/migrations/sequel/005_create_teams_migration.rb new file mode 100644 index 0000000..771c9d2 --- /dev/null +++ b/spec/migrations/sequel/005_create_teams_migration.rb @@ -0,0 +1,24 @@ +class CreateTeams < Sequel::Migration + def up + create_table(:teams) do + primary_key(:id) + DateTime(:created_at) + DateTime(:updated_at) + foreign_key(:league_id, :table => :leagues) + foreign_key(:division_id, :table => :divisions) + String(:name, :limit => 50, :null => false) + String(:logo_url, :limit => 255) + String(:manager, :limit => 100, :null => false) + String(:ballpark, :limit => 100) + String(:mascot, :limit => 100) + Integer(:founded) + Integer(:wins) + Integer(:losses) + Float(:win_percentage) + end + end + + def down + drop_table(:teams) + end +end diff --git a/spec/models/sequel/division.rb b/spec/models/sequel/division.rb new file mode 100644 index 0000000..007e137 --- /dev/null +++ b/spec/models/sequel/division.rb @@ -0,0 +1,15 @@ +class Division < Sequel::Model + set_primary_key(:id) + plugin(:timestamps, :update_on_create => true) + plugin(:validation_helpers) + + many_to_one(:league) + one_to_many(:teams) + + self.raise_on_save_failure = false + self.raise_on_typecast_failure = false + def validate + validates_numeric(:league_id, :only_integer => true) + validates_presence(:name) + end +end diff --git a/spec/models/sequel/draft.rb b/spec/models/sequel/draft.rb new file mode 100644 index 0000000..25c4f82 --- /dev/null +++ b/spec/models/sequel/draft.rb @@ -0,0 +1,19 @@ +class Draft < Sequel::Model + set_primary_key(:id) + plugin(:timestamps, :update_on_create => true) + plugin(:validation_helpers) + + many_to_one(:team) + many_to_one(:player) + + self.raise_on_save_failure = false + self.raise_on_typecast_failure = false + def validate + validates_numeric(:player_id, :only_integer => true, :allow_blank => true) + validates_numeric(:team_id, :only_integer => true, :allow_blank => true) + validates_presence(:date) + validates_numeric(:round, :only_integer => true) + validates_numeric(:pick, :only_integer => true) + validates_numeric(:overall, :only_integer => true) + end +end diff --git a/spec/models/sequel/league.rb b/spec/models/sequel/league.rb new file mode 100644 index 0000000..a9263b6 --- /dev/null +++ b/spec/models/sequel/league.rb @@ -0,0 +1,14 @@ +class League < Sequel::Model + set_primary_key(:id) + plugin(:timestamps, :update_on_create => true) + plugin(:validation_helpers) + + one_to_many(:divisions) + one_to_many(:teams) + + self.raise_on_save_failure = false + self.raise_on_typecast_failure = false + def validate + validates_presence(:name) + end +end diff --git a/spec/models/sequel/player.rb b/spec/models/sequel/player.rb new file mode 100644 index 0000000..9d2b3bb --- /dev/null +++ b/spec/models/sequel/player.rb @@ -0,0 +1,18 @@ +class Player < Sequel::Model + set_primary_key(:id) + plugin(:timestamps, :update_on_create => true) + plugin(:validation_helpers) + + many_to_one(:team) + one_to_many(:drafts, :one_to_one => true) + + self.raise_on_save_failure = false + self.raise_on_typecast_failure = false + def validate + validates_numeric(:number, :only_integer => true) + validates_unique(:number, :message => "There is already a player with that number on this team") do |dataset| + dataset.where("team_id = ?", team_id) + end + end +end + diff --git a/spec/models/sequel/team.rb b/spec/models/sequel/team.rb new file mode 100644 index 0000000..193aef9 --- /dev/null +++ b/spec/models/sequel/team.rb @@ -0,0 +1,22 @@ +class Team < Sequel::Model + set_primary_key(:id) + plugin(:timestamps, :update_on_create => true) + plugin(:validation_helpers) + + many_to_one(:league) + many_to_one(:division) + one_to_many(:players) + + self.raise_on_save_failure = false + self.raise_on_typecast_failure = false + def validate + validates_numeric(:league_id, :only_integer => true) + validates_numeric(:division_id, :only_integer => true) + validates_presence(:name) + validates_presence(:manager) + validates_numeric(:founded, :only_integer => true) + validates_numeric(:wins, :only_integer => true) + validates_numeric(:losses, :only_integer => true) + validates_numeric(:win_percentage) + end +end diff --git a/spec/requests/main_spec.rb b/spec/requests/main_spec.rb index 060ed4b..552778f 100644 --- a/spec/requests/main_spec.rb +++ b/spec/requests/main_spec.rb @@ -449,6 +449,7 @@ end it "should be associated with the correct object" do + @draft.reload MerbAdmin::AbstractModel.new("Player").first.draft.should == @draft end end @@ -463,7 +464,9 @@ end it "should be associated with the correct objects" do + @teams[0].reload MerbAdmin::AbstractModel.new("League").first.teams.should include(@teams[0]) + @teams[1].reload MerbAdmin::AbstractModel.new("League").first.teams.should include(@teams[1]) end @@ -544,6 +547,7 @@ end it "should be associated with the correct object" do + @draft.reload MerbAdmin::AbstractModel.new("Player").first.draft.should == @draft end end @@ -558,7 +562,9 @@ end it "should be associated with the correct objects" do + @teams[0].reload MerbAdmin::AbstractModel.new("League").first.teams.should include(@teams[0]) + @teams[1].reload MerbAdmin::AbstractModel.new("League").first.teams.should include(@teams[1]) end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index f923840..238b943 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -64,6 +64,10 @@ def setup_orm(orm = nil) end when :sequel require 'sequel' + require 'sequel/extensions/blank' + require 'sequel/extensions/migration' + Sequel::Migrator.apply(Sequel.sqlite, File.join(File.dirname(__FILE__), "migrations", "sequel")) + require_models(orm) else raise "MerbAdmin does not support the #{orm} ORM" end