class Kiss # This class adds functionality to Sequel::Model and automatically loads # model class definitions from model_dir. It also uses Kiss#file_cache # to cache database model classes, unless no model_dir is specified. class Model < Sequel::Model class << self dsl_accessor :value_column, :display_column def set_dataset(source) super(source) end def has_many(*args) one_to_many(*args) end def belongs_to(*args) many_to_one(*args) end def has_and_belongs_to_many(*args) many_to_many(*args) end def alias_association(new_name, old_name) alias_method new_name, old_name alias_method :"#{new_name}_dataset", :"#{old_name}_dataset" end alias_method :alias_assoc, :alias_association # This method is called by Sequel::Model's association def methods. # Must return singularized table name for correct association key names. def name @_table.to_s.singularize end def controller db.kiss_controller end def request db.kiss_request || controller end def table @_table.to_sym end def table=(table) @_table = table end # Name symbol for default foreign key def default_foreign_key :"#{name.singularize.demodulize.underscore}_id" end def find_or_new(cond) find(cond) || new(cond) end # TODO: Fix has_many and many_to_many associations def associate(type, name, opts = {}, &block) opts = opts.clone unless opts[:class] || opts[:class_name] opts[:class_name] = name.to_s.pluralize end super(type, name, opts, &block) association_reflections[name] end include Kiss::ControllerAccessors include Kiss::RequestAccessors alias_method :database, :db end def deferred_associations @deferred_associations ||= {} end def deferred_association_method_calls @deferred_association_method_calls ||= [] end def set_associated_object(opts, o) if o && !o.pk if (da = deferred_associations[opts[:name]]) != o if da da.deferred_association_method_calls.delete_if {|c| c[1] == [self, opts.setter_method, da] } remove_deferred_reciprocal_object(opts, o) end deferred_associations[opts[:name]] = o o.deferred_association_method_calls << [false, [self, opts.setter_method, o]] add_deferred_reciprocal_object(opts, o) end o else deferred_associations.delete(opts[:name]) super end end def add_associated_object(opts, o) need_missing_opk = opts.need_associated_primary_key? && !o.pk if !pk || need_missing_opk double_defer = !pk && need_missing_opk # TODO: add :uniq check here (and patch this in Sequel as well) (deferred_associations[opts[:name]] ||= []).push(o) method_call = [double_defer, [self, opts.add_method, o]] deferred_association_method_calls << method_call unless pk o.deferred_association_method_calls << method_call if need_missing_opk add_deferred_reciprocal_object(opts, o) o else deferred_associations[opts[:name]].delete(o) if deferred_associations[opts[:name]] super end end # Add/Set the current object to/as the given object's reciprocal association. def add_deferred_reciprocal_object(opts, o) return unless reciprocal = opts.reciprocal if opts.reciprocal_array? if array = o.deferred_associations[reciprocal] and !array.include?(self) array.push(self) end else o.deferred_associations[reciprocal] = self end end def load_associated_objects(opts, reload=false) if d = deferred_associations[opts[:name]] opts.returns_array? ? super + d : d else super || set_associated_object(opts, opts.associated_class.new) end end # Remove all associated objects from the given association def remove_all_associated_objects(opts) ret = super if deferred_associations.include?(opts[:name]) def_ret = deferred_associations[opts[:name]].each do |o| method_call = [self, opts.add_method, o] deferred_association_method_calls.delete_if {|c| c[1] == method_call } o.deferred_association_method_calls.delete_if {|c| c[1] == method_call } remove_deferred_reciprocal_object(opts, o) end deferred_associations[opts[:name]] = [] (ret || []) + def_ret else ret end end # Remove the given associated object from the given association def remove_associated_object(opts, o) if (array = deferred_associations[opts[:name]]) and array.delete(o) method_call = [self, opts.add_method, o] deferred_association_method_calls.delete_if {|c| c[1] == method_call } o.deferred_association_method_calls.delete_if {|c| c[1] == method_call } remove_deferred_reciprocal_object(opts, o) else super end o end # Remove/unset the current object from/as the given object's reciprocal association. def remove_deferred_reciprocal_object(opts, o) return unless reciprocal = opts.reciprocal if opts.reciprocal_array? if array = o.deferred_associations[reciprocal] array.delete_if{|x| self === x} end else o.deferred_associations[reciprocal] = nil end end def after_create deferred_association_method_calls.each do |call| if call[0] # call[0] (double_defer) is true # now set it to false, do nothing else # (defer again until other side is created) call[0] = false else m = call[1] m.shift.send(*m) if m end end @deferred_association_method_calls = [] end def method_missing(meth, *args, &block) if meth.to_s =~ /(\w+)\?\Z/ column = $1.to_sym if respond_to?(column) val = self.send(column, *args, &block) return !(val.nil? || val.zero?) end end raise NoMethodError, "undefined method `#{meth}' for database model `#{self.class.name.pluralize}'" end def controller self.class.controller end def request self.class.request end def name self[:name] || self.class.table.to_s.singularize.titleize end # Creates and invokes new Kiss::Mailer instance to send email message via SMTP. def new_email(options = {}) request.new_email({ :data => self }.merge(options)) end def send_email(options = {}) new_email(options).send end def to_hash result = {} keys.each do |key| result[key] = values[key] end result end include Kiss::ControllerAccessors include Kiss::RequestAccessors alias_method :database, :db end class ModelCache def initialize(database, model_dir) @_db = database @_model_dir = model_dir @_cache = {} # TODO: Fix this to use file cache and subclass hierarchy, so models can be # reloaded if _model.rb changes. @_root_class = Class.new(Kiss::Model) if File.exist?(filename = "#{@_model_dir}/_model.rb") @_root_class.class_eval(File.read(filename), filename) end end def new_model_class(database, source) klass = Class.new(@_root_class) klass.set_dataset(database[source]) klass.table = source klass.class_eval do @_value_column = :id @_display_column = :name end klass end def [](source) raise 'argument to model cache must be symbol of database table name' unless source.is_a?(Symbol) database = @_db @_model_dir ? begin # TODO: use request's file_cache model_path = "#{@_model_dir}/#{source}.rb" db.kiss_controller.file_cache(model_path) do |src| klass = new_model_class(database, source) klass.class_eval(src, model_path) if src klass end end : @_cache[source] ||= new_model_class(database, source) end def database @_db end alias_method :db, :database def literal(*args) Sequel::Model.dataset.literal(*args) end alias_method :quote, :literal end end Sequel::Model::Associations::AssociationReflection.class_eval do def associated_class self[:class] ||= (self[:model].request || self[:model].controller).dbm[self[:class_name].to_s.pluralize.to_sym] end def default_left_key :"#{self[:model].name.singularize.underscore}_id" end end class Sequel::Model def inherited(subclass) self.instance_variables.each do |var| subclass.instance_variable_set(var, instance_variable_get(var)) end end end class Date class << self alias_method :old_parse, :parse def parse(*args, &block) return SequelZeroTime.new(args[0]) if args[0] =~ /0000-00-00/ old_parse(*args, &block) end end # comparision operators def ==(value) (value == 0 || value.is_a?(SequelZeroTime)) ? false : super(value) end def >(value) (value == 0 || value.is_a?(SequelZeroTime)) ? true : super(value) end def >=(value) (value == 0 || value.is_a?(SequelZeroTime)) ? true : super(value) end def <(value) (value == 0 || value.is_a?(SequelZeroTime)) ? false : super(value) end def <=(value) (value == 0 || value.is_a?(SequelZeroTime)) ? false : super(value) end end class Time class << self alias_method :old_parse, :parse def parse(*args, &block) return SequelZeroTime.new(args[0]) if args[0] =~ /0000/ old_parse(*args, &block) end end # comparision operators def ==(value) (value == 0 || value.is_a?(SequelZeroTime)) ? false : super(value) end def >(value) (value == 0 || value.is_a?(SequelZeroTime)) ? true : super(value) end def >=(value) (value == 0 || value.is_a?(SequelZeroTime)) ? true : super(value) end def <(value) (value == 0 || value.is_a?(SequelZeroTime)) ? false : super(value) end def <=(value) (value == 0 || value.is_a?(SequelZeroTime)) ? false : super(value) end end class BigDecimal # Formats number with comma-separated thousands. def format_thousands(value = to_f.to_s) integer, decimal = value.split(/\./, 2) integer.reverse.gsub(/(\d{3})/, '\1,').sub(/\,(-?)$/, '\1').reverse + '.' + decimal end end class SequelZeroTime < String def initialize(value = '0000-00-00 00:00', *args, &block) super(value, *args, &block) end # arithmetic operators def +(*args) self end def -(*args) self end # comparision operators def ==(value) (value == 0 || value.is_a?(SequelZeroTime)) ? true : false end def >(value) return false end def >=(value) (value == 0 || value.is_a?(SequelZeroTime)) ? true : false end def <(value) return true end def <=(value) return true end # format conversion def to_f; 0; end def to_i; 0; end def to_s; ''; end # timezone conversion def set_timezone!(zone, utc) self end def from_timezone(zone) self end def to_timezone(zone) self end def to_utc self end def zone '' end # string representations def strftime(*args); ''; end def md; ''; end def md_full; ''; end alias_method :md_long, :md_full def mdy; ''; end def mdy_full; ''; end alias_method :mdy_long, :mdy_full def mdy_hm; ''; end def mdy_hmz; ''; end def mdy_hms; ''; end def mdy_hmsz; ''; end def ymd_hm; ''; end def ymd_hmz; ''; end def ymd_hms; ''; end def ymd_hmsz; ''; end def sql; '0000-00-00 00:00'; end def mdy_hm_full; ''; end alias_method :mdy_hm_long, :mdy_hm_full def mdy_hmz_full; ''; end alias_method :mdy_hmz_long, :mdy_hmz_full def hm; ''; end def hmz; ''; end def hms; ''; end def hmsz; ''; end def hm_mdy; ''; end def hmz_mdy; ''; end def hms_mdy; ''; end def hmsz_mdy; ''; end def hm_mdy_full; ''; end alias_method :hm_mdy_long, :hm_mdy_full def hmz_mdy_full; ''; end alias_method :hmz_mdy_long, :hmz_mdy_full def zero?; true; end def in_between?(*args); false; end end IrregularInflection = proc do def self.irregular(singular, plural) singular(Regexp.new("(#{singular[0,1]})#{singular[1..-1]}$", "i"), '\1' + singular[1..-1]) plural(Regexp.new("(#{singular[0,1]})#{singular[1..-1]}$", "i"), '\1' + plural[1..-1]) singular(Regexp.new("(#{plural[0,1]})#{plural[1..-1]}$", "i"), '\1' + singular[1..-1]) plural(Regexp.new("(#{plural[0,1]})#{plural[1..-1]}$", "i"), '\1' + plural[1..-1]) end end Sequel::Inflections.class_eval &IrregularInflection String::Inflections.class_eval &IrregularInflection