1# Copyright (c) 2006-2008, The RubyCocoa Project. 2# Copyright (c) 2001-2006, FUJIMOTO Hisakuni. 3# All Rights Reserved. 4# 5# RubyCocoa is free software, covered under either the Ruby's license or the 6# LGPL. See the COPYRIGHT file for more information. 7 8require 'osx/cocoa' 9begin 10 # for testing old AR 11 # require '/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/gems/1.8/gems/activesupport-1.4.2/lib/active_support.rb' 12 # require '/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/gems/1.8/gems/activerecord-1.15.3/lib/active_record.rb' 13 14 require 'active_record' 15rescue LoadError 16 msg = "ActiveRecord was not found, if you have it installed as a gem you have to require 'rubygems' before you require 'osx/active_record'" 17 $stderr.puts msg 18 raise LoadError, msg 19end 20 21# --------------------------------------------------------- 22# Class additions 23# --------------------------------------------------------- 24 25class ActiveRecord::Base 26 class << self 27 alias_method :__inherited_before_proxy, :inherited 28 def inherited(klass) 29 proxy_klass = "#{klass.to_s}Proxy" 30 unless klass.parent.local_constants.include?(proxy_klass) 31 eval "class ::#{proxy_klass} < OSX::ActiveRecordProxy; end;" 32 # FIXME: This leads to a TypeError originating from: oc_import.rb:618:in `method_added' 33 # Object.const_set(proxy_klass, Class.new(OSX::ActiveRecordProxy)) 34 end 35 __inherited_before_proxy(klass) 36 end 37 end 38 39 # Returns a proxy for this instance. 40 def to_activerecord_proxy 41 if self.class.instance_variable_get(:@proxy_klass).nil? 42 self.class.instance_variable_set(:@proxy_klass, Object.const_get("#{self.class.to_s}Proxy")) 43 end 44 @record_proxy ||= self.class.instance_variable_get(:@proxy_klass).alloc.initWithRecord(self) 45 end 46 alias_method :to_activerecord_proxies, :to_activerecord_proxy 47end 48 49class Array 50 # Returns an array with proxies for all the original records in this array. 51 def to_activerecord_proxies 52 map { |rec| rec.to_activerecord_proxy } 53 end 54 alias_method :to_activerecord_proxy, :to_activerecord_proxies 55 56 # Returns an array with all the original records for the proxies in this array. 57 def original_records 58 map { |rec| rec.original_record } 59 end 60end 61 62module OSX 63 64 # --------------------------------------------------------- 65 # Subclasses of cocoa classes that add ActiveRecord support 66 # --------------------------------------------------------- 67 68 class ActiveRecordSetController < OSX::NSArrayController 69 # First tries to destroy the record(s) then lets the super method do it's work. 70 def remove(sender) 71 super_remove(sender) if selectedObjects.all? {|proxy| proxy.destroy } 72 end 73 74 # Directly saves the new record. 75 def newObject 76 objectClass.class.create 77 end 78 79 # Sets up the ActiveRecordSetController for a given model and sets the content if it's specified. 80 # 81 # <tt>:model</tt> should be the model that we'll be handling. 82 # <tt>:content</tt> is optional, if specified it should be an array with or without any proxies for the model that we're handling. 83 def setup_for(options) 84 raise ArgumentError, ":model was nil, expected a ActiveRecord::Base subclass" if options[:model].nil? 85 # FIXME: not DRY, duplicated from ActiveRecord::Base#to_activerecord_proxy 86 self.setObjectClass( Object.const_get("#{options[:model].to_s}Proxy") ) 87 self.setContent(options[:content]) unless options[:content].nil? 88 end 89 end 90 91 class BelongsToActiveRecordSetController < ActiveRecordSetController 92 # Doesn't save the record to the db in a belongs to association, 93 # because it will automatically be saved by ActiveRecord when added to the collection. 94 def newObject 95 objectClass.class.alloc.init 96 end 97 end 98 99 class ActiveRecordTableView < OSX::NSTableView 100 require 'active_support/core_ext/string/inflections' 101 include ActiveSupport::CoreExtensions::String::Inflections 102 103 # <tt>:model</tt> should be the model class that you want to scaffold. 104 # <tt>:bind_to</tt> should be the ActiveRecordSetController instance that you want the columns to be bound too. 105 # <tt>:except</tt> can be either a string or an array that says which columns should not be displayed. 106 # <tt>:validates_immediately</tt> set this to +true+ to add validation to every column. Defaults to +false+. 107 # 108 # ib_outlets :customersTableView, :customersRecordSetController 109 # 110 # def awakeFromNib 111 # @customersTableView.scaffold_columns_for :model => Customer, :bind_to => @customersRecordSetController, :validates_immediately => true, :except => "id" 112 # end 113 # 114 # You can also pass it a block which will yield 2 objects for every column, namely +table_column+ which is the new NSTableColumn 115 # and +column_opyions+ which is a hash that can be used to set additional options for the binding. 116 # 117 # ib_outlets :customersTableView, :customersRecordSetController 118 # 119 # def awakeFromNib 120 # @customersTableView.scaffold_columns_for :model => Customer, :bind_to => @customersRecordSetController do |table_column, column_options| 121 # p table_column 122 # p column_options 123 # column_options['NSValidatesImmediately'] = true 124 # end 125 # end 126 def scaffold_columns_for(options) 127 raise ArgumentError, ":model was nil, expected a ActiveRecord::Base subclass" if options[:model].nil? 128 raise ArgumentError, ":bind_to was nil, expected an instance of ActiveRecordSetController" if options[:bind_to].nil? 129 options[:except] ||= [] 130 options[:validates_immediately] ||= false 131 132 # if there are any columns already, first remove them. 133 cols = self.tableColumns 134 if cols.count > 0 135 # we create a temporary array because we do not want to mutate the 136 # original one during the enumeration 137 tmpCols = OSX::NSArray.arrayWithArray(cols) 138 tmpCols.each { |column| self.removeTableColumn(column) } 139 end 140 141 options[:model].column_names.each do |column_name| 142 # skip columns 143 next if options[:except].include?(column_name) 144 # setup new table column 145 table_column = OSX::NSTableColumn.alloc.init 146 table_column.setIdentifier(column_name) 147 table_column.headerCell.setStringValue(column_name.titleize) 148 # create a hash that will hold the options that will be passed as options to the bind method 149 column_options = {} 150 column_options['NSValidatesImmediately'] = options[:validates_immediately] 151 152 # FIXME: causes a bus error on my machine... 153 yield(table_column, column_options) if block_given? 154 155 # set the binding 156 table_column.bind_toObject_withKeyPath_options(OSX::NSValueBinding, options[:bind_to], "arrangedObjects.#{column_name}", column_options) 157 # and add it to the table view 158 self.addTableColumn(table_column) 159 end 160 end 161 end 162 163 class ActiveRecordProxy < OSX::NSObject 164 165 # class methods 166 class << self 167 # Use this class method to set any filters you need when a specific value for a key is requested. 168 # You can pass it a block, or a hash that contains either the key: 169 # - <tt>:return</tt> which needs an array that holds the class to be instantiated as the first element 170 # and the method to be called if the data isn't nil as the second element. 171 # If the data is nil the class will simply be instantiated with the normal alloc.init call. 172 # - <tt>:call</tt> which needs the method that it should call. When the method is called the data is sent as the argument. 173 # 174 # class EmailProxy < OSX::ActiveRecordProxy 175 # # on_get filter with: block 176 # on_get :body do |content| 177 # content ||= 'I'm so empty' 178 # OSX::NSAttributedString.alloc.initWithString(content) 179 # end 180 # 181 # # on_get filter with: return 182 # on_get :subject, :return => [OSX::NSAttributedString, :initWithString] 183 # 184 # # on_get filter with: call 185 # on_get :address, :call => :nsattributed_string_from_address 186 # # and the method to be called 187 # def nsattributed_string_from_address(address) 188 # address ||= 'Emptier than this isn't possible' 189 # OSX::NSAttributedString.alloc.initWithString(address) 190 # end 191 # end 192 def on_get(key, options={}, &block) 193 @on_get_filters ||= {} 194 @on_get_filters[key.to_sym] = ( block.nil? ? options : block ) 195 end 196 197 # This find class method passes the message on to the model, but it will return proxies for the returned records 198 def find(*args) 199 model_class.find(*args).to_activerecord_proxies 200 end 201 202 # This method_missing class method passes the find_by_ message on to the model, but it will return proxies for the returned records 203 def method_missing(method, *args) 204 if method.to_s.index('find_by_') == 0 205 model_class.send(method, *args).to_activerecord_proxies 206 else 207 super 208 end 209 end 210 211 # Returns the model class for this proxy 212 def model_class 213 @model_class ||= Object.const_get(self.to_s[0..-6]) 214 end 215 216 def create(attributes = {}) 217 alloc.initWithAttributes(attributes) 218 end 219 end 220 221 # Creates a new record and returns a proxy for it. 222 def init 223 if super_init 224 @record = self.record_class.send(:new) unless @record 225 define_record_methods! unless self.class.instance_variable_get(:@record_methods_defined) 226 self 227 end 228 end 229 230 # Takes an existing record as an argument and returns a proxy for it. 231 def initWithRecord(record) 232 @record = record 233 init 234 end 235 236 # Creates a new record with the given attributes and returns a proxy for it. 237 def initWithAttributes(attributes) 238 @record = record_class.send(:new, attributes) 239 return nil unless @record.save 240 init 241 end 242 243 # Returns the model class for this proxy 244 def record_class 245 self.class.model_class 246 end 247 248 # Returns an Array of all the available methods on the corresponding record object 249 def record_methods 250 @record.methods 251 end 252 253 # Returns the corresponding record object 254 def to_activerecord 255 @record 256 end 257 # Returns the corresponding record object 258 def original_record 259 @record 260 end 261 262 # Useful inspect method for use as: p(my_proxy) 263 def inspect 264 @record.inspect.sub(/#{record_class}/, "#{self.class.name.to_s} proxy_object_id: #{self.object_id} record_object_id: #{@record.object_id}") 265 end 266 267 # Compare two ActiveRecord proxies. They are compared by the record. 268 def ==(other) 269 if self.class == other.class 270 self.original_record == other.original_record 271 else 272 super 273 end 274 end 275 276 # Returns +true+ if the given key is an association, otherwise returns +false+ 277 def is_association?(key) 278 key_sym = key.to_s.to_sym 279 @record.class.reflect_on_all_associations.each { |assoc| return true if assoc.name == key_sym } 280 false 281 end 282 283 # KVC stuff 284 285 # Get the filter for a given key if it exists. 286 def on_get_filter_for_key(key) 287 filters = self.class.instance_variable_get(:@on_get_filters) 288 filters[key.to_sym] unless filters.nil? 289 end 290 291 # This method is called by the object that self is bound to, 292 # if the requested key is a association return proxies for the records. 293 def rbValueForKey(key) 294 if is_association?(key) 295 @record.send(key.to_s.to_sym).to_activerecord_proxies 296 else 297 if filter = self.on_get_filter_for_key(key) 298 if filter.is_a?(Hash) 299 case filter.keys.first 300 when :return 301 klass, method = filter[:return] 302 data = @record[key.to_s] 303 return (data.nil? ? klass.alloc.init : klass.alloc.send(method.to_sym, data)) 304 when :call # callback method 305 send(filter[:call], @record[key.to_s]) 306 end 307 elsif filter.is_a?(Proc) 308 filter.call(@record[key.to_s]) 309 end 310 else 311 # no filter, so simply return the data 312 @record[key.to_s] 313 end 314 end 315 end 316 317 # This method is called by the object that self is bound to, 318 # it's called when a update has occured. 319 def rbSetValue_forKey(value, key) 320 if is_association? key 321 # we are dealing with an association (only has_many for now) 322 if @record.send(key.to_s.to_sym).length < value.to_a.length 323 # add the newest record to the has_many association of the @record 324 return true if (@record.send(key.to_s.to_sym) << value.to_a.last.to_activerecord) 325 else 326 # reload the children to reflect the changes deletion of records 327 @record.reload 328 return true 329 end 330 else 331 @record[key.to_s] = value.to_ruby rescue nil 332 return @record.save 333 end 334 return false 335 end 336 337 # This method is called by the object that self is bound to, 338 # it passes the value on to the record object and returns the validation result. 339 def validateValue_forKeyPath_error(value, key, error) 340 original_value = @record[key.to_s] 341 @record[key.to_s] = value[0].to_s 342 @record.valid? 343 344 # we only want to check if the value for this attribute is valid and not every attribute 345 return true if @record.errors[key.to_s].nil? 346 347 @record[key.to_s] = original_value 348 # create a error message for each validation error on this attribute 349 error_msg = '' 350 @record.errors[key.to_s].each do |err| 351 error_msg += "#{self.record_class} #{key.to_s} #{err}\n" 352 end 353 # construct the NSError object 354 error.assign( OSX::NSError.alloc.initWithDomain_code_userInfo( OSX::NSCocoaErrorDomain, -1, { OSX::NSLocalizedDescriptionKey => error_msg } ) ) 355 false 356 end 357 358 private 359 360 def define_record_methods! 361 # define all the record attributes getters and setters 362 @record.attribute_names.each do |m| 363 self.class.class_eval do 364 define_method(m) do || 365 #return @record.send(m) 366 return rbValueForKey(m.to_s) 367 end 368 sym = "#{m}=".to_sym 369 define_method(sym) do |*args| 370 return @record.send(sym, *args) 371 end 372 end 373 end 374 # define the normal instance methods of the record 375 (@record.methods - self.methods).each do |m| 376 next if m == 'initialize' 377 self.class.class_eval do 378 define_method(m) do |*args| 379 if is_association?(m) 380 return rbValueForKey(m) 381 else 382 return @record.send(m, *args) 383 end 384 end 385 end 386 end 387 self.class.instance_variable_set(:@record_methods_defined, true) 388 end 389 end 390 391 # --------------------------------------------------------- 392 # Extra support classes/modules 393 # --------------------------------------------------------- 394 395 module ActiveRecordConnector 396 def connect_to_sqlite(dbfile, options = {}) 397 options[:log] ||= false 398 399 if options[:log] 400 ActiveRecord::Base.logger = Logger.new($stderr) 401 ActiveRecord::Base.colorize_logging = false 402 end 403 404 # Connect to the database 405 ActiveRecord::Base.establish_connection({ 406 :adapter => 'sqlite3', 407 :dbfile => dbfile 408 }) 409 end 410 module_function :connect_to_sqlite 411 412 # Connect to an SQLite database stored in the applications support directory ~/USER/Library/Application Support/APP/APP.sqlite. 413 # <tt>:always_migrate</tt> Always run migrations when this method is invoked, false by default. 414 # <tt>:migrations_dir</tt> The directory where migrations are stored, migrate/ by default. 415 # <tt>:log</tt> Log database activity, false by default. 416 # 417 # ActiveRecordConnector.connect_to_sqlite_in_application_support :log => true 418 # 419 # If you run this for the first time and haven't already created a migration to create your database 420 # tables, etc., you'll need to force the migration if :always_migrate isn't enabled. 421 def connect_to_sqlite_in_application_support(options = {}) 422 options[:always_migrate] ||= false 423 options[:migrations_dir] ||= 'migrate/' 424 425 dbfile = File.join(self.get_app_support_path, "#{self.get_app_name}.sqlite") 426 # connect 427 self.connect_to_sqlite(dbfile, options) 428 # do any necessary migrations 429 if not File.exists?(dbfile) or options[:always_migrate] 430 migrations_dir = File.join(OSX::NSBundle.mainBundle.resourcePath.fileSystemRepresentation.to_s, options[:migrations_dir]) 431 # do a migration to the latest version 432 ActiveRecord::Migrator.migrate(migrations_dir, nil) 433 end 434 end 435 module_function :connect_to_sqlite_in_application_support 436 437 def get_app_name 438 OSX::NSBundle.mainBundle.bundleIdentifier.to_s.scan(/\w+$/).first 439 end 440 module_function :get_app_name 441 442 def get_app_support_path 443 # get the path to the ~/Library/Application Support/ directory 444 user_app_support_path = File.join(OSX::NSSearchPathForDirectoriesInDomains(OSX::NSLibraryDirectory, OSX::NSUserDomainMask, true)[0].to_s, "Application Support") 445 # get the complete path to the directory that will hold the files for this app. 446 # e.g.: ~/Library/Application Support/SomeApp/ 447 path_to_this_apps_app_support_dir = File.join(user_app_support_path, self.get_app_name) 448 # and create it if necessary 449 unless File.exists?(path_to_this_apps_app_support_dir) 450 require 'fileutils' 451 FileUtils.mkdir_p(path_to_this_apps_app_support_dir) 452 end 453 return path_to_this_apps_app_support_dir 454 end 455 module_function :get_app_support_path 456 end 457 458end 459