├── .document ├── .gitignore ├── LICENSE ├── README ├── README.rdoc ├── Rakefile ├── VERSION ├── demo-app.tar.gz ├── extjs-mvc.gemspec ├── lib ├── controller │ └── controller.rb ├── core_ext │ └── array │ │ └── extract_options.rb ├── extjs-mvc.rb ├── extjs │ ├── component.rb │ └── data │ │ └── store.rb ├── helpers │ ├── component.rb │ └── store.rb ├── model │ ├── active_record.rb │ ├── base.rb │ ├── data_mapper.rb │ └── mongo_mapper.rb └── test │ └── macros.rb ├── rails └── init.rb ├── shoulda_macros └── macros.rb └── test ├── active_record_test.rb ├── app ├── config │ ├── application.rb │ └── database.yml ├── db │ └── schema.rb └── models │ └── active_record │ ├── address.rb │ ├── data_type.rb │ ├── group.rb │ ├── house.rb │ ├── location.rb │ ├── person.rb │ ├── user.rb │ └── user_group.rb ├── component_test.rb ├── controller_test.rb ├── data_mapper_test.rb ├── model_test.rb ├── mongo_mapper_test.rb ├── store_test.rb └── test_helper.rb /.document: -------------------------------------------------------------------------------- 1 | README.rdoc 2 | lib/**/*.rb 3 | bin/* 4 | features/**/*.feature 5 | LICENSE 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.sw? 2 | .DS_Store 3 | coverage 4 | rdoc 5 | pkg 6 | *.log 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 Chris Scott 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | demo-app.tar.gz is the Rails app behind the tutorial at: 2 | 3 | unzip it then: 4 | >rake db:migrate 5 | >script/server 6 | 7 | In your browser, open: 8 | localhost/projects/step1 (through step9) 9 | 10 | 11 | http://www.extjs.com/blog/2009/09/30/ext-js-on-rails-a-comprehensivetutorial/ 12 | 13 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = mvc 2 | 3 | A collection of helpers, MVC mixins and PORs (plain-old-ruby-object) to assist with auto-generating ExtJS Stores (Ext.data.Store) including its associated DataReader (Ext.data.JsonReader, Ext.data.XmlReader) and DataWriter (Ext.data.JsonWriter, Ext.data.XmlWriter). Also contains a helper for rendering javascript component definitions via partials. 4 | 5 | See tutorial http://www.extjs.com/blog/2009/09/30/ext-js-on-rails-a-comprehensivetutorial/ 6 | 7 | ===Installation 8 | % sudo gem install gemcutter 9 | % gem tumble (only have to do this once, adds gemcutter as primary gem-source) 10 | % sudo gem install extjs-mvc 11 | 12 | Rails Installation: 13 | In environment.rb, 14 | 15 | Rails::Initializer.run do |config| 16 | config.gem "extjs-mvc" 17 | end 18 | 19 | Merb installation: 20 | In config/dependencies.rb, Add extjs-mvc as a new dependency 21 | 22 | dependency "extjs-mvc" 23 | 24 | === An ORM Model mixin: ExtJS::Model 25 | extjs-mvc contains Model mixin named ExtJS::Model which works for three popular ORM frameworks, ActiveRecord, DataMapper and MongoMapper. The API for each framework is identical. 26 | 27 | Simply include the mixin into your model. Use the class-method extjs_fields to specify those 28 | fields with will be used to render the Ext.data.Record.create field-def'n. 29 | 30 | class User < ActiveRecord::Base 31 | include ExtJS::Model 32 | 33 | extjs_fields :exclude => [:password, :password_confirmation] 34 | 35 | # OR 36 | extjs_fields :name, :description 37 | 38 | # OR 39 | extjs_fields :only => [:name, :description] # actually the same as above 40 | 41 | # OR 42 | extjs_fields :additional => [:computed] # includes all database columns and an additional computed field 43 | 44 | # OR define a column as a Hash 45 | extjs_fields :description, :name => {"sortDir" => "ASC"}, :created_at => {"dateFormat" => "c"} 46 | 47 | # OR render associations, association-fields will have their "mapping" property set automatically 48 | extjs_fields :name, :description, :company => [:name, :description] 49 | 50 | def computed 51 | name.blank? ? login : name 52 | end 53 | end 54 | 55 | After including the model mixin ExtJS::Model, try typing the following in irb console: 56 | >> User.extjs_record 57 | => { :idProperty=>"id", :fields=>[ 58 | {:type=>'int', :allowBlank=>true, :name=>"id"}, 59 | {:type=>'string', :allowBlank=>false, :name=>"first", :defaultValue => nil}, 60 | {:type=>'string', :allowBlank=>false, :name=>"last", :defaultValue => nil}, 61 | {:type=>'string', :allowBlank=>false, :name=>"email", :defaultValue => nil} 62 | ]} 63 | 64 | An auto-generated Ext.data.JsonReader configuration! 65 | 66 | 67 | You can also define different sets of fields for different representations of your model. 68 | 69 | E.g. with the following definition: 70 | 71 | class User < ActiveRecord::Base 72 | include ExtJS::Model 73 | 74 | extjs_fieldset :grid, fields => [:name, :description, :company => [:name, :description]] 75 | extjs_fieldset :combo, [:full_name] 76 | 77 | def full_name 78 | "#{first_name} #{name}" 79 | end 80 | end 81 | 82 | You can get store configs for both representations with 83 | User.extjs_record(:grid) 84 | or 85 | User.extjs_record(:combo) 86 | 87 | And the corresponding data for the representations with 88 | User.first.to_record(:grid) 89 | or 90 | User.first.to_record(:combo) 91 | 92 | 93 | === An ActionController mixin: ExtJS::Controller 94 | The extjs-mvc Gem includes a framework agnostic Controller mixin which works with both Rails and Merb. Include this mixin into any controller which will need to generate an Ext.data.Store. 95 | usage: 96 | 97 | class UsersController < ActionController::Base 98 | include ExtJS::Controller 99 | end 100 | 101 | === View Helper: ExtJS::Helpers::Component 102 | 103 | usage: 104 | 105 | class UserController < ActionController::Base 106 | include ExtJS::Controller 107 | helper ExtJS::Helpers::Component 108 | end 109 | 110 | Now render Ext components using helper method extjs_component 111 | 112 | @viewport = extjs_component( 113 | "xtype" => "viewport", 114 | "frame" => true, 115 | "layout" => "border") 116 | @viewport.add("xtype" => "panel", "contentEl" => "hd", "region" => "north", "height" => 30) 117 | @viewport.add(:partial => "/users/grid", "itemId" => "users-grid", "region" => "west") 118 | @viewport.add(:partial => "/tasks/grid", "itemId" => "tasks-grid", "region" => "center") 119 | @viewport.add("xtype" => "panel", "contentEl" => "ft", "region" => "south", "height" => 20) 120 | 121 | Note how it can also render partials. Partials will be invoked with a local-variable named "container", a reference to the 122 | parent Ext::Component instance which added the partial. If no "container" is specified, it would be expected that your partial 123 | would provide its own "renderTo" or "contentEl" property, just as in Ext.Component from ExtJS javascript library. 124 | 125 | 126 | === View Helper: ExtJS::Helpers::Store 127 | 128 | Renders an Ext.data.Store with helper method extjs_store 129 | 130 | class UserController < ActionController::Base 131 | include ExtJS::Controller 132 | helper ExtJS::Helpers::Store 133 | end 134 | 135 | Now render a store in an erb template: 136 | 137 | @store = extjs_store( 138 | :controller => "users", 139 | :fieldset => :grid, # <-- Specify a particular fieldset as defined in the Model (used to render DataReader) 140 | :proxy => "http" # <-- default 141 | :format => "json" # <-- default 142 | :model => "user", # <-- default: controller_name.singularize 143 | :writer => {:encode => false}, 144 | :config => { # <-- standard Ext.data.Store config-params 145 | "autoLoad" => true 146 | "autoSave" => true 147 | } 148 | ) 149 | 150 | %= @store.render % 151 | 152 | === A Testing Mixin: ExtJS::TestMacros 153 | The extjs-mvc Gem includes a small set of testing macros to help unit-test models. 154 | This requires the 'Shoulda' gem from thoughtbot. Include this mixin inside the 155 | ActiveSupport::TestCase class in test/test_helper.rb 156 | 157 | ==== Usage 158 | test/test_helper.rb 159 | class ActiveSupport::TestCase 160 | extend ExtJS::TestMacros 161 | #... 162 | end 163 | 164 | In individual model unit tests: 165 | class ModelTest < ActiveSupport::TestCase 166 | should_require_extjs_fields :name, :email, :city 167 | #... 168 | #other tests 169 | end 170 | 171 | 172 | == Note on Patches/Pull Requests 173 | 174 | * Fork the project. 175 | * Make your feature addition or bug fix. 176 | * Add tests for it. This is important so I don't break it in a 177 | future version unintentionally. 178 | * Commit, do not mess with rakefile, version, or history. 179 | (if you want to have your own version, that is fine but 180 | bump version in a commit by itself I can ignore when I pull) 181 | * Send me a pull request. Bonus points for topic branches. 182 | 183 | == Copyright 184 | 185 | Copyright (c) 2009 Chris Scott. See LICENSE for details. 186 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rake' 3 | 4 | begin 5 | require 'jeweler' 6 | Jeweler::Tasks.new do |gem| 7 | gem.name = "extjs-mvc" 8 | gem.summary = %Q{Ruby ORM tools to assist with rendering Ext.data.Store} 9 | gem.description = %Q{MVC tools to assist with ExtJS development in Rails and Merb} 10 | gem.email = "christocracy@gmail.com" 11 | gem.homepage = "http://github.com/extjs/mvc" 12 | gem.authors = ["Chris Scott"] 13 | gem.add_development_dependency "shoulda" 14 | gem.add_development_dependency "mocha" 15 | gem.add_development_dependency "extlib" 16 | 17 | gem.test_files = [] 18 | gem.files = FileList["[A-Z]*", "{bin,generators,lib,test}/**/*", 'lib/jeweler/templates/.gitignore'] 19 | 20 | # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings 21 | end 22 | Jeweler::GemcutterTasks.new 23 | rescue LoadError 24 | puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler" 25 | end 26 | 27 | 28 | require 'rake/testtask' 29 | Rake::TestTask.new(:test) do |test| 30 | test.libs << 'lib' << 'test' 31 | test.pattern = 'test/**/*_test.rb' 32 | test.verbose = true 33 | end 34 | 35 | begin 36 | require 'rcov/rcovtask' 37 | Rcov::RcovTask.new do |test| 38 | test.libs << 'test' 39 | test.pattern = 'test/**/*_test.rb' 40 | test.verbose = true 41 | end 42 | rescue LoadError 43 | task :rcov do 44 | abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov" 45 | end 46 | end 47 | 48 | task :test => :check_dependencies 49 | 50 | task :default => :test 51 | 52 | require 'rake/rdoctask' 53 | Rake::RDocTask.new do |rdoc| 54 | if File.exist?('VERSION') 55 | version = File.read('VERSION') 56 | else 57 | version = "" 58 | end 59 | 60 | rdoc.rdoc_dir = 'rdoc' 61 | rdoc.title = "mvc #{version}" 62 | rdoc.rdoc_files.include('README*') 63 | rdoc.rdoc_files.include('lib/**/*.rb') 64 | end 65 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.3.10 2 | -------------------------------------------------------------------------------- /demo-app.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extjs/mvc/b1972ca38457380e2acc1bfba113c74318881a69/demo-app.tar.gz -------------------------------------------------------------------------------- /extjs-mvc.gemspec: -------------------------------------------------------------------------------- 1 | # Generated by jeweler 2 | # DO NOT EDIT THIS FILE DIRECTLY 3 | # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command 4 | # -*- encoding: utf-8 -*- 5 | 6 | Gem::Specification.new do |s| 7 | s.name = %q{extjs-mvc} 8 | s.version = "0.3.9" 9 | 10 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 11 | s.authors = ["Chris Scott"] 12 | s.date = %q{2010-03-02} 13 | s.description = %q{MVC tools to assist with ExtJS development in Rails and Merb} 14 | s.email = %q{christocracy@gmail.com} 15 | s.extra_rdoc_files = [ 16 | "LICENSE", 17 | "README", 18 | "README.rdoc" 19 | ] 20 | s.files = [ 21 | "LICENSE", 22 | "README", 23 | "README.rdoc", 24 | "Rakefile", 25 | "VERSION", 26 | "lib/controller/controller.rb", 27 | "lib/core_ext/array/extract_options.rb", 28 | "lib/extjs-mvc.rb", 29 | "lib/extjs/component.rb", 30 | "lib/extjs/data/store.rb", 31 | "lib/helpers/component.rb", 32 | "lib/helpers/store.rb", 33 | "lib/model/active_record.rb", 34 | "lib/model/base.rb", 35 | "lib/model/data_mapper.rb", 36 | "lib/model/mongo_mapper.rb", 37 | "lib/test/macros.rb", 38 | "test/active_record_test.rb", 39 | "test/app/config/application.rb", 40 | "test/app/config/database.yml", 41 | "test/app/db/schema.rb", 42 | "test/app/models/active_record/address.rb", 43 | "test/app/models/active_record/data_type.rb", 44 | "test/app/models/active_record/group.rb", 45 | "test/app/models/active_record/house.rb", 46 | "test/app/models/active_record/location.rb", 47 | "test/app/models/active_record/person.rb", 48 | "test/app/models/active_record/user.rb", 49 | "test/app/models/active_record/user_group.rb", 50 | "test/component_test.rb", 51 | "test/controller_test.rb", 52 | "test/data_mapper_test.rb", 53 | "test/debug.log", 54 | "test/model_test.rb", 55 | "test/mongo_mapper_test.rb", 56 | "test/store_test.rb", 57 | "test/test_helper.rb" 58 | ] 59 | s.homepage = %q{http://github.com/extjs/mvc} 60 | s.rdoc_options = ["--charset=UTF-8"] 61 | s.require_paths = ["lib"] 62 | s.rubygems_version = %q{1.3.6} 63 | s.summary = %q{Ruby ORM tools to assist with rendering Ext.data.Store} 64 | 65 | if s.respond_to? :specification_version then 66 | current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION 67 | s.specification_version = 3 68 | 69 | if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then 70 | s.add_development_dependency(%q, [">= 0"]) 71 | s.add_development_dependency(%q, [">= 0"]) 72 | s.add_development_dependency(%q, [">= 0"]) 73 | else 74 | s.add_dependency(%q, [">= 0"]) 75 | s.add_dependency(%q, [">= 0"]) 76 | s.add_dependency(%q, [">= 0"]) 77 | end 78 | else 79 | s.add_dependency(%q, [">= 0"]) 80 | s.add_dependency(%q, [">= 0"]) 81 | s.add_dependency(%q, [">= 0"]) 82 | end 83 | end 84 | 85 | -------------------------------------------------------------------------------- /lib/controller/controller.rb: -------------------------------------------------------------------------------- 1 | module ExtJS::Controller 2 | 3 | def self.included(controller) 4 | controller.send(:extend, ClassMethods) 5 | end 6 | 7 | ## 8 | # Controller class methods 9 | # 10 | module ClassMethods 11 | 12 | def extjs_root(value=nil) 13 | ExtJS::MVC.root = value unless value.nil? 14 | ExtJS::MVC.root 15 | end 16 | 17 | def extjs_success_property(value=nil) 18 | ExtJS::MVC.success_property = value unless value.nil? 19 | ExtJS::MVC.success_property 20 | end 21 | 22 | def extjs_message_property(value=nil) 23 | ExtJS::MVC.message_property = value unless value.nil? 24 | ExtJS::MVC.message_property 25 | end 26 | 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/core_ext/array/extract_options.rb: -------------------------------------------------------------------------------- 1 | ## 2 | # add Rails-style Array#extract_options! method 3 | # 4 | module ExtJS 5 | module CoreExtensions 6 | module Array 7 | module ExtractOptions 8 | def extract_options! 9 | last.is_a?(::Hash) ? pop : {} 10 | end 11 | end 12 | end 13 | end 14 | end 15 | Array.send(:include, ExtJS::CoreExtensions::Array::ExtractOptions) -------------------------------------------------------------------------------- /lib/extjs-mvc.rb: -------------------------------------------------------------------------------- 1 | module ExtJS 2 | class MVC 3 | @@success_property = :success 4 | @@message_property = :message 5 | @@root = :data 6 | cattr_accessor :success_property 7 | cattr_accessor :message_property 8 | cattr_accessor :root 9 | 10 | require 'model/base' 11 | 12 | # Detect orm, include appropriate mixin. 13 | if defined?(ActiveRecord) 14 | require 'model/active_record' 15 | elsif defined?(DataMapper) 16 | require 'model/data_mapper' 17 | elsif defined?(MongoMapper) 18 | require 'model/mongo_mapper' 19 | else 20 | raise StandardError.new("extjs-mvc could not detect an ORM framework. Be sure to include your ORM framework before initializing extjs-mvc Gem.") 21 | end 22 | 23 | # Rails-style Array#extract_options! used heavily 24 | if defined?(Merb) 25 | require 'core_ext/array/extract_options' 26 | end 27 | 28 | # ExtJS Component and Store wrappers 29 | require 'extjs/component' 30 | require 'extjs/data/store' 31 | 32 | # Component/Store view-helpers 33 | require 'helpers/component' 34 | require 'helpers/store' 35 | 36 | # Controller mixin. Works for both Rails and Merb. 37 | require 'controller/controller' 38 | end 39 | end 40 | 41 | -------------------------------------------------------------------------------- /lib/extjs/component.rb: -------------------------------------------------------------------------------- 1 | ## 2 | # @class ExtJS::Component 3 | # 4 | class ExtJS::Component 5 | attr_accessor :config 6 | def initialize(params) 7 | @config = params#params.extract_options! 8 | @controller = @config.delete(:controller) unless @config[:controller].nil? 9 | 10 | @config["items"] = [] if config["items"].nil? 11 | 12 | if container = @config.delete(:container) 13 | container.add(self) 14 | end 15 | @partial_config = nil 16 | end 17 | 18 | def apply(params) 19 | @config.merge!(params) 20 | end 21 | 22 | ## 23 | # Adds a config {} or ExtJS::Component instance to this component's items collection. 24 | # NOTE: When :partial option is used a String will of course be returned. Otherwise an ExtJS::Component 25 | # instance will be returned. 26 | # @return {String/ExtJS::Component} 27 | def add(*config) 28 | 29 | options = config.extract_options! 30 | if !options.keys.empty? 31 | if url = options.delete(:partial) 32 | # rendering a partial, cache the config until partial calls #add method. @see else. 33 | @partial_config = options 34 | if (@controller.respond_to?(:partial)) 35 | # Merb 36 | return @controller.partial(url, :with => self, :as => :container) 37 | else 38 | # Rails 39 | return @controller.render(:partial => url, :locals => {:container => self}) 40 | end 41 | else 42 | options.merge!(@partial_config) unless @partial_config.nil? 43 | options[:controller] = @controller unless @controller.nil? 44 | cmp = ExtJS::Component.new(options) 45 | @partial_config = nil 46 | @config["items"] << cmp 47 | return cmp 48 | end 49 | elsif !config.empty? && config.first.kind_of?(ExtJS::Component) 50 | cmp = config.first 51 | cmp.apply(@partial_config) unless @partial_config.nil? 52 | @partial_config = nil 53 | @config["items"] << cmp.config 54 | return cmp 55 | end 56 | end 57 | 58 | def to_json 59 | config.to_json 60 | end 61 | 62 | def render 63 | @config.delete("items") if @config["items"].empty? 64 | 65 | # If there are any listeners attached in json, we have to get rid of double-quotes in order to expose 66 | # the javascript object. 67 | # eg: "listeners":"SomeController.listeners.grid" -> {"listeners":SomeController.listeners.grid, ...} 68 | json = @config.to_json.gsub(/\"(listeners|handler|scope)\":\s?\"([a-zA-Z\.\[\]\(\)]+)\"/, '"\1":\2') 69 | "Ext.ComponentMgr.create(#{json});" 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/extjs/data/store.rb: -------------------------------------------------------------------------------- 1 | ## 2 | # ExtJS::Data::Store 3 | # 4 | module ExtJS::Data 5 | class Store 6 | attr_accessor :id, :format, :type, :controller, :model 7 | 8 | def initialize(*params) 9 | options = params.extract_options! 10 | 11 | @config = options[:config] || {} 12 | @format = options[:format] || 'json' 13 | @fieldset = options[:fieldset] || :default 14 | @schema = options[:schema] 15 | @proxy = options[:proxy] || 'http' 16 | @writer = options[:writer] 17 | @type = (options[:type].nil?) ? @proxy === 'direct' ? 'Ext.data.DirectStore' : "Ext.data.#{@format.capitalize}Store" : options[:type] 18 | 19 | @controller = self.class.get_controller(options[:controller]) 20 | @model = self.class.get_model(options[:controller], options[:model]) 21 | 22 | # Merge Reader/Proxy config 23 | @config.merge!(reader) 24 | @config.merge!(proxy) 25 | 26 | @config["baseParams"] = {} if @config["baseParams"].nil? 27 | @config["baseParams"].update("fieldset" => @fieldset) 28 | 29 | @config["format"] = @format 30 | 31 | # Set storeId implicitly based upon Model name if not set explicitly 32 | @id = @config["storeId"] = @model.to_s.downcase unless @config["storeId"] 33 | end 34 | 35 | ## 36 | # pre-load a store with data. Not yet tested. In theory, this *should* work. 37 | # 38 | def load(*params) 39 | #@config["loadData"] = @model.all(params).collect {|rec| rec.to_record } 40 | end 41 | 42 | ## 43 | # renders the configured store 44 | # @param {Boolean} script_tag [true] Not yet implemented. Always renders tags. 45 | def render(script_tag = true) 46 | script = '' 47 | # ugly hack for DirectProxy API. Have to add an Ext.onReady() after the Store constructor to set API 48 | if @proxy === 'direct' 49 | auto_load = @config.delete("autoLoad") 50 | cname = @controller.controller_name.capitalize 51 | script = "Ext.onReady(function() { var s = Ext.StoreMgr.get('#{@config["storeId"]}');" 52 | if (@config["directFn"]) 53 | script += "s.proxy.directFn = #{cname}.#{@config["directFn"]};" 54 | else 55 | script += "s.proxy.setApi({create:#{cname}.#{@config["api"]["create"]},read:#{cname}.#{@config["api"]["read"]},update:#{cname}.#{@config["api"]["update"]},destroy:#{cname}.#{@config["api"]["destroy"]}});" 56 | end 57 | if auto_load 58 | script += "s.load();" 59 | end 60 | script += "});" 61 | end 62 | 63 | if @writer # <-- ugly hack because 3.0.1 can't deal with Writer as config-param 64 | json = @config.to_json 65 | json[json.length-1] = ',' 66 | json += "\"writer\":new Ext.data.#{@format.capitalize}Writer(#{@writer.to_json})}" 67 | "" 68 | else 69 | "" 70 | end 71 | end 72 | 73 | private 74 | 75 | def self.get_controller(name) 76 | begin 77 | if (defined?(Rails)) 78 | "#{name.to_s.camelize}Controller".constantize 79 | else 80 | Extlib::Inflection.constantize("#{Extlib::Inflection.camelize(name)}") 81 | end 82 | rescue NameError 83 | throw NameError.new("ExtJS::Store failed with an unknown controller named '#{name.to_s}'") 84 | end 85 | end 86 | 87 | def self.get_model(controller, model) 88 | unless model.class == Class 89 | begin 90 | if (defined?(Rails)) 91 | model = ((model) ? model : controller.singularize).camelize.constantize 92 | else 93 | model = Extlib::Inflection.constantize(Extlib::Inflection.camelize(((model) ? model : Extlib::Inflection.singularize(controller)))) 94 | end 95 | rescue NameError => e 96 | throw NameError.new("EXTJS::Store found an unknown model #{model.to_s})") 97 | end 98 | end 99 | model 100 | end 101 | 102 | def proxy 103 | proxy = {} 104 | if @proxy === 'direct' 105 | actions = ['create', 'read', 'update', 'destroy'] 106 | proxy["api"] = {} 107 | @controller.direct_actions.each_index do |n| 108 | proxy["api"][actions[n]] = @controller.direct_actions[n][:name] 109 | end 110 | else 111 | if @config["api"] 112 | proxy["api"] = {} 113 | @config["api"].each {|k,v| proxy["api"][k] = "/#{@controller.controller_name}/#{v}" } 114 | else 115 | proxy["url"] = "/#{@controller.controller_name}.#{@format.to_s}" 116 | end 117 | end 118 | proxy 119 | end 120 | 121 | def reader 122 | { 123 | "successProperty" => @controller.extjs_success_property, 124 | "root" => @controller.extjs_root, 125 | "messageProperty" => @controller.extjs_message_property 126 | }.merge(@schema || @model.extjs_record(@fieldset)) 127 | end 128 | 129 | 130 | end 131 | end 132 | -------------------------------------------------------------------------------- /lib/helpers/component.rb: -------------------------------------------------------------------------------- 1 | ## 2 | # ExtJS::Helpers::Component 3 | # 4 | module ExtJS::Helpers 5 | module Component 6 | ## 7 | # add class-var @@extjs_on_ready 8 | def self.included(helper) 9 | 10 | end 11 | 12 | def extjs_component(*params) 13 | options = params.extract_options! 14 | options[:controller] = self 15 | ExtJS::Component.new(options) 16 | end 17 | 18 | ## 19 | # Adds a script or ExtJS::Component instance to on_ready queue. The queue is emptied and rendered to 20 | # via #extjs_render 21 | # 22 | def extjs_onready(*params) 23 | @onready_queue = [] if @onready_queue.nil? 24 | params.each do |cmp| 25 | @onready_queue << cmp 26 | end 27 | end 28 | 29 | ## 30 | # Empties the on_ready queue. Renders within tags 31 | # 32 | def extjs_render 33 | @onready_queue = [] if @onready_queue.nil? 34 | "" 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/helpers/store.rb: -------------------------------------------------------------------------------- 1 | module ExtJS::Helpers 2 | module Store 3 | def extjs_store(*params) 4 | ExtJS::Data::Store.new(*params) 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/model/active_record.rb: -------------------------------------------------------------------------------- 1 | ## 2 | # ActiveRecord adapter to ExtJS::Model mixin. 3 | # 4 | module ExtJS 5 | module Model 6 | module ClassMethods 7 | 8 | def extjs_primary_key 9 | self.primary_key.to_sym 10 | end 11 | 12 | def extjs_column_names 13 | self.column_names.map(&:to_sym) 14 | end 15 | 16 | def extjs_columns_hash 17 | self.columns_hash.symbolize_keys 18 | end 19 | 20 | ## 21 | # determine if supplied Column object is nullable 22 | # @param {ActiveRecord::ConnectionAdapters::Column} 23 | # @return {Boolean} 24 | # 25 | def extjs_allow_blank(col) 26 | # if the column is the primary key always allow it to be blank. 27 | # Otherwise we could not create new records with ExtJS because 28 | # new records have no id and thus cannot be valid 29 | col.name == self.primary_key || col.null 30 | end 31 | 32 | ## 33 | # returns the default value 34 | # @param {ActiveRecord::ConnectionAdapters::Column} 35 | # @return {Mixed} 36 | # 37 | def extjs_default(col) 38 | col.default 39 | end 40 | 41 | ## 42 | # returns the corresponding column name of the type column for a polymorphic association 43 | # @param {String/Symbol} the id column name for this association 44 | # @return {Symbol} 45 | def extjs_polymorphic_type(id_column_name) 46 | id_column_name.to_s.gsub(/_id\Z/, '_type').to_sym 47 | end 48 | 49 | ## 50 | # determine datatype of supplied Column object 51 | # @param {ActiveRecord::ConnectionAdapters::Column} 52 | # @return {String} 53 | # 54 | def extjs_type(col) 55 | type = col.type.to_s 56 | case type 57 | when "datetime", "date", "time", "timestamp" 58 | type = "date" 59 | when "text" 60 | type = "string" 61 | when "integer" 62 | type = "int" 63 | when "decimal" 64 | type = "float" 65 | end 66 | type 67 | end 68 | 69 | ## 70 | # return a simple, normalized list of AR associations having the :name, :type and association class 71 | # @return {Array} 72 | # 73 | def extjs_associations 74 | @extjs_associations ||= self.reflections.inject({}) do |memo, (key, assn)| 75 | type = (assn.macro === :has_many || assn.macro === :has_and_belongs_to_many) ? :many : assn.macro 76 | memo[key.to_sym] = { 77 | :name => key.to_sym, 78 | :type => type, 79 | :class => assn.options[:polymorphic] ? nil : assn.class_name.constantize, 80 | :foreign_key => assn.association_foreign_key.to_sym, 81 | :is_polymorphic => !!assn.options[:polymorphic] 82 | } 83 | memo 84 | end 85 | end 86 | end 87 | end 88 | end 89 | 90 | -------------------------------------------------------------------------------- /lib/model/base.rb: -------------------------------------------------------------------------------- 1 | module ExtJS 2 | module Model 3 | 4 | def self.included(model) 5 | model.send(:extend, ClassMethods) 6 | model.send(:include, InstanceMethods) 7 | ## 8 | # @config {String} extjs_parent_trail_template This a template used to render mapped field-names. 9 | # Default is Proc.new{ |field_name| "_#{field_name}" } 10 | # You could also use the Rails standard 11 | # Proc.new{ |field_name| "[#{field_name}]" } 12 | # 13 | model.cattr_accessor :extjs_parent_trail_template 14 | model.extjs_parent_trail_template = Proc.new{ |field_name| "_#{field_name}" } if model.extjs_parent_trail_template.nil? 15 | end 16 | 17 | ## 18 | # InstanceMethods 19 | # 20 | module InstanceMethods 21 | 22 | ## 23 | # Converts a model instance to a record compatible with ExtJS 24 | # 25 | # The first parameter should be the fieldset for which the record will be returned. 26 | # If no parameter is provided, then the default fieldset will be choosen 27 | # Alternativly the first parameter can be a Hash with a :fields member to directly specify 28 | # the fields to use for the record. 29 | # 30 | # All these are valid calls: 31 | # 32 | # user.to_record # returns record for :default fieldset 33 | # # (fieldset is autmatically defined, if not set) 34 | # 35 | # user.to_record :fieldset # returns record for :fieldset fieldset 36 | # # (fieldset is autmatically defined, if not set) 37 | # 38 | # user.to_record :fields => [:id, :password] 39 | # # returns record for the fields 'id' and 'password' 40 | # 41 | # For even more valid options for this method (which all should not be neccessary to use) 42 | # have a look at ExtJS::Model::Util.extract_fieldset_and_options 43 | def to_record(*params) 44 | fieldset, options = Util.extract_fieldset_and_options params 45 | 46 | fields = [] 47 | if options[:fields].empty? 48 | fields = self.class.extjs_get_fields_for_fieldset(fieldset) 49 | else 50 | fields = self.class.process_fields(*options[:fields]) 51 | end 52 | 53 | assns = self.class.extjs_associations 54 | pk = self.class.extjs_primary_key 55 | 56 | # build the initial field data-hash 57 | data = {pk => self.send(pk)} 58 | 59 | fields.each do |field| 60 | next if data.has_key? field[:name] # already processed (e.g. explicit mentioning of :id) 61 | 62 | value = nil 63 | if association_reflection = assns[field[:name]] # if field is an association 64 | association = self.send(field[:name]) 65 | 66 | # skip this association if we already visited it 67 | # otherwise we could end up in a cyclic reference 68 | next if options[:visited_classes].include? association.class 69 | 70 | case association_reflection[:type] 71 | when :belongs_to, :has_one 72 | if association.respond_to? :to_record 73 | assn_fields = field[:fields] 74 | if assn_fields.nil? 75 | assn_fields = association.class.extjs_get_fields_for_fieldset(field.fetch(:fieldset, fieldset)) 76 | end 77 | 78 | value = association.to_record :fields => assn_fields, 79 | :visited_classes => options[:visited_classes] + [self.class] 80 | else 81 | value = {} 82 | (field[:fields]||[]).each do |sub_field| 83 | value[sub_field[:name]] = association.send(sub_field[:name]) if association.respond_to? sub_field[:name] 84 | end 85 | end 86 | if association_reflection[:type] == :belongs_to 87 | # Append associations foreign_key to data 88 | data[association_reflection[:foreign_key]] = self.send(association_reflection[:foreign_key]) 89 | if association_reflection[:is_polymorphic] 90 | foreign_type = self.class.extjs_polymorphic_type(association_reflection[:foreign_key]) 91 | data[foreign_type] = self.send(foreign_type) 92 | end 93 | end 94 | when :many 95 | value = association.collect { |r| r.to_record } # use carefully, can get HUGE 96 | end 97 | else # not an association -> get the method's value 98 | value = self.send(field[:name]) 99 | value = value.to_record if value.respond_to? :to_record 100 | end 101 | data[field[:name]] = value 102 | end 103 | data 104 | end 105 | end 106 | 107 | ## 108 | # ClassMethods 109 | # 110 | module ClassMethods 111 | ## 112 | # render AR columns to Ext.data.Record.create format 113 | # eg: {name:'foo', type: 'string'} 114 | # 115 | # The first parameter should be the fieldset for which the record definition will be returned. 116 | # If no parameter is provided, then the default fieldset will be choosen 117 | # Alternativly the first parameter can be a Hash with a :fields member to directly specify 118 | # the fields to use for the record config. 119 | # 120 | # All these are valid calls: 121 | # 122 | # User.extjs_record # returns record config for :default fieldset 123 | # # (fieldset is autmatically defined, if not set) 124 | # 125 | # User.extjs_record :fieldset # returns record config for :fieldset fieldset 126 | # # (fieldset is autmatically defined, if not set) 127 | # 128 | # User.extjs_record :fields => [:id, :password] 129 | # # returns record config for the fields 'id' and 'password' 130 | # 131 | # For even more valid options for this method (which all should not be neccessary to use) 132 | # have a look at ExtJS::Model::Util.extract_fieldset_and_options 133 | def extjs_record(*params) 134 | fieldset, options = Util.extract_fieldset_and_options params 135 | 136 | if options[:fields].empty? 137 | fields = self.extjs_get_fields_for_fieldset(fieldset) 138 | else 139 | fields = self.process_fields(*options[:fields]) 140 | end 141 | 142 | associations = self.extjs_associations 143 | columns = self.extjs_columns_hash 144 | pk = self.extjs_primary_key 145 | rs = [] 146 | 147 | fields.each do |field| 148 | 149 | field = Marshal.load(Marshal.dump(field)) # making a deep copy 150 | 151 | if col = columns[field[:name]] # <-- column on this model 152 | rs << self.extjs_field(field, col) 153 | elsif assn = associations[field[:name]] 154 | # skip this association if we already visited it 155 | # otherwise we could end up in a cyclic reference 156 | next if options[:visited_classes].include? assn[:class] 157 | 158 | assn_fields = field[:fields] 159 | if assn[:class].respond_to?(:extjs_record) # <-- exec extjs_record on assn Model. 160 | if assn_fields.nil? 161 | assn_fields = assn[:class].extjs_get_fields_for_fieldset(field.fetch(:fieldset, fieldset)) 162 | end 163 | 164 | record = assn[:class].extjs_record(field.fetch(:fieldset, fieldset), { :visited_classes => options[:visited_classes] + [self], :fields => assn_fields}) 165 | rs.concat(record[:fields].collect { |assn_field| 166 | self.extjs_field(assn_field, :parent_trail => field[:name], :mapping => field[:name], :allowBlank => true) # <-- allowBlank on associated data? 167 | }) 168 | elsif assn_fields # <-- :parent => [:id, :name, :sub => [:id, :name]] 169 | field_collector = Proc.new do |parent_trail, mapping, assn_field| 170 | if assn_field.is_a?(Hash) && assn_field.keys.size == 1 && assn_field.keys[0].is_a?(Symbol) && assn_field.values[0].is_a?(Array) 171 | field_collector.call(parent_trail.to_s + self.extjs_parent_trail_template.call(assn_field.keys.first), "#{mapping}.#{assn_field.keys.first}", assn_field.values.first) 172 | else 173 | self.extjs_field(assn_field, :parent_trail => parent_trail, :mapping => mapping, :allowBlank => true) 174 | end 175 | end 176 | rs.concat(assn_fields.collect { |assn_field| field_collector.call(field[:name], field[:name], assn_field) }) 177 | else 178 | rs << extjs_field(field) 179 | end 180 | 181 | # attach association's foreign_key if not already included. 182 | if columns.has_key?(assn[:foreign_key]) && !rs.any? { |r| r[:name] == assn[:foreign_key] } 183 | rs << extjs_field({:name => assn[:foreign_key]}, columns[assn[:foreign_key]]) 184 | end 185 | # attach association's type if polymorphic association and not alredy included 186 | if assn[:is_polymorphic] 187 | foreign_type = self.extjs_polymorphic_type(assn[:foreign_key]) 188 | if columns.has_key?(foreign_type) && !rs.any? { |r| r[:name] == foreign_type } 189 | rs << extjs_field({:name => foreign_type}, columns[foreign_type]) 190 | end 191 | end 192 | else # property is a method? 193 | rs << extjs_field(field) 194 | end 195 | end 196 | 197 | return { 198 | :fields => rs, 199 | :idProperty => pk 200 | } 201 | end 202 | 203 | ## 204 | # meant to be used within a Model to define the extjs record fields. 205 | # eg: 206 | # class User 207 | # extjs_fieldset :grid, [:first, :last, :email => {"sortDir" => "ASC"}, :company => [:id, :name]] 208 | # end 209 | # or 210 | # class User 211 | # extjs_fieldset :last, :email => {"sortDir" => "ASC"}, :company => [:id, :name] # => implies fieldset name :default 212 | # end 213 | # 214 | def extjs_fieldset(*params) 215 | fieldset, options = Util.extract_fieldset_and_options params 216 | var_name = :"@extjs_fieldsets__#{fieldset}" 217 | self.instance_variable_set( var_name, self.process_fields(*options[:fields]) ) 218 | end 219 | 220 | def extjs_get_fields_for_fieldset(fieldset) 221 | var_name = :"@extjs_fieldsets__#{fieldset}" 222 | super_value = nil 223 | unless self.instance_variable_get( var_name ) 224 | if self.superclass.respond_to? :extjs_get_fields_for_fieldset 225 | super_value = self.superclass.extjs_get_fields_for_fieldset(fieldset) 226 | end 227 | self.extjs_fieldset(fieldset, self.extjs_column_names) unless super_value 228 | end 229 | super_value || self.instance_variable_get( var_name ) 230 | end 231 | 232 | ## 233 | # shortcut to define the default fieldset. For backwards-compatibility. 234 | # 235 | def extjs_fields(*params) 236 | self.extjs_fieldset(:default, { 237 | :fields => params 238 | }) 239 | end 240 | 241 | ## 242 | # Prepare a field configuration list into a normalized array of Hashes, {:name => "field_name"} 243 | # @param {Mixed} params 244 | # @return {Array} of Hashes 245 | # 246 | def process_fields(*params) 247 | fields = [] 248 | if params.size == 1 && params.last.is_a?(Hash) # peek into argument to see if its an option hash 249 | options = params.last 250 | if options.has_key?(:additional) && options[:additional].is_a?(Array) 251 | return self.process_fields(*(self.extjs_column_names + options[:additional].map(&:to_sym))) 252 | elsif options.has_key?(:exclude) && options[:exclude].is_a?(Array) 253 | return self.process_fields(*(self.extjs_column_names - options[:exclude].map(&:to_sym))) 254 | elsif options.has_key?(:only) && options[:only].is_a?(Array) 255 | return self.process_fields(*options[:only]) 256 | end 257 | end 258 | 259 | params = self.extjs_column_names if params.empty? 260 | 261 | associations = extjs_associations 262 | 263 | params.each do |f| 264 | if f.kind_of?(Hash) 265 | if f.keys.size == 1 && f.keys[0].is_a?(Symbol) && f.values[0].is_a?(Array) # {:association => [:field1, :field2]} 266 | fields << { 267 | :name => f.keys[0], 268 | :fields => process_fields(*f.values[0]) 269 | } 270 | elsif f.keys.size == 1 && f.keys[0].is_a?(Symbol) && f.values[0].is_a?(Hash) # {:field => {:sortDir => 'ASC'}} 271 | fields << f.values[0].update(:name => f.keys[0]) 272 | elsif f.has_key?(:name) # already a valid Hash, just copy it over 273 | fields << f 274 | else 275 | raise ArgumentError, "encountered a Hash that I don't know anything to do with `#{f.inspect}:#{f.class}`" 276 | end 277 | else # should be a String or Symbol 278 | raise ArgumentError, "encountered a fields Array that I don't understand: #{params.inspect} -- `#{f.inspect}:#{f.class}` is not a Symbol or String" unless f.is_a?(Symbol) || f.is_a?(String) 279 | fields << {:name => f.to_sym} 280 | end 281 | end 282 | 283 | fields 284 | end 285 | 286 | ## 287 | # Render a column-config object 288 | # @param {Hash/Column} field Field-configuration Hash, probably has :name already set and possibly Ext.data.Field options. 289 | # @param {ORM Column Object from AR, DM or MM} 290 | # 291 | def extjs_field(field, config=nil) 292 | if config.kind_of? Hash 293 | if config.has_key?(:mapping) && config.has_key?(:parent_trail) 294 | field.update( # <-- We use a template for rendering mapped field-names. 295 | :name => config[:parent_trail].to_s + self.extjs_parent_trail_template.call(field[:name]), 296 | :mapping => "#{config[:mapping]}.#{field[:name]}" 297 | ) 298 | end 299 | field.update(config.except(:mapping, :parent_trail)) 300 | elsif !config.nil? # <-- Hopfully an ORM Column object. 301 | field.update( 302 | :allowBlank => self.extjs_allow_blank(config), 303 | :type => self.extjs_type(config), 304 | :defaultValue => self.extjs_default(config) 305 | ) 306 | field[:dateFormat] = "c" if field[:type] === "date" && field[:dateFormat].nil? # <-- ugly hack for date 307 | end 308 | field.update(:type => "auto") if field[:type].nil? 309 | # convert Symbol values to String values 310 | field.keys.each do |k| 311 | raise ArgumentError, "extjs_field expects a Hash as first parameter with all it's keys Symbols. Found key #{k.inspect}:#{k.class.to_s}" unless k.is_a?(Symbol) 312 | field[k] = field[k].to_s if field[k].is_a?(Symbol) 313 | end 314 | field 315 | end 316 | 317 | # ## 318 | # # Returns an array of symbolized association names that will be referenced by a call to to_record 319 | # # i.e. [:parent1, :parent2] 320 | # # 321 | # def extjs_used_associations 322 | # if @extjs_used_associations.nil? 323 | # assoc = [] 324 | # self.extjs_record_fields.each do |f| 325 | # #This needs to be the first condition because the others will break if f is an Array 326 | # if extjs_associations[f[:name]] 327 | # assoc << f[:name] 328 | # end 329 | # end 330 | # @extjs_used_associations = assoc.uniq 331 | # end 332 | # @extjs_used_associations 333 | # end 334 | end 335 | 336 | module Util 337 | 338 | ## 339 | # returns the fieldset from the arguments and normalizes the options. 340 | # @return [{Symbol}, {Hash}] 341 | def self.extract_fieldset_and_options arguments 342 | orig_args = arguments 343 | fieldset = :default 344 | options = { # default options 345 | :visited_classes => [], 346 | :fields => [] 347 | } 348 | if arguments.size > 2 || (arguments.size == 2 && !arguments[0].is_a?(Symbol)) 349 | raise ArgumentError, "Don't know how to handle #{arguments.inspect}" 350 | elsif arguments.size == 2 && arguments[0].is_a?(Symbol) 351 | fieldset = arguments.shift 352 | if arguments[0].is_a?(Array) 353 | options.update({ 354 | :fields => arguments[0] 355 | }) 356 | elsif arguments[0].is_a?(Hash) 357 | options.update(arguments[0]) 358 | end 359 | elsif arguments.size == 1 && (arguments[0].is_a?(Symbol) || arguments[0].is_a?(String)) 360 | fieldset = arguments.shift.to_sym 361 | elsif arguments.size == 1 && arguments[0].is_a?(Hash) 362 | fieldset = arguments[0].delete(:fieldset) || :default 363 | options.update(arguments[0]) 364 | end 365 | [fieldset, options] 366 | end 367 | end 368 | end 369 | end 370 | 371 | -------------------------------------------------------------------------------- /lib/model/data_mapper.rb: -------------------------------------------------------------------------------- 1 | ## 2 | # DataMapper adapter for ExtJS::Model mixin 3 | # 4 | 5 | module ExtJS 6 | module Model 7 | module ClassMethods 8 | 9 | def extjs_primary_key 10 | self.key.first.name 11 | end 12 | 13 | def extjs_column_names 14 | self.properties.collect {|p| p.name.to_s } 15 | end 16 | 17 | def extjs_columns_hash 18 | if @extjs_columns_hash.nil? 19 | @extjs_columns_hash = {} 20 | self.properties.each do |p| 21 | @extjs_columns_hash[p.name] = p 22 | end 23 | end 24 | @extjs_columns_hash 25 | end 26 | 27 | def extjs_allow_blank(col) 28 | (col === self.key.first) ? true : col.nullable? 29 | end 30 | 31 | def extjs_type(col) 32 | type = ((col.type.respond_to?(:primitive)) ? col.type.primitive : col.type).to_s 33 | case type 34 | when "DateTime", "Date", "Time" 35 | type = :date 36 | when "String" 37 | type = :string 38 | when "Float" 39 | type = :float 40 | when "Integer", "BigDecimal" 41 | type = :int 42 | else 43 | type = "auto" 44 | end 45 | end 46 | 47 | def extjs_associations 48 | if @extjs_associations.nil? 49 | @extjs_associations = {} 50 | self.relationships.keys.each do |key| 51 | assn = self.relationships[key] 52 | @extjs_associations[key.to_sym] = { 53 | :name => key, 54 | :type => type = (assn.options[:max].nil? && assn.options[:min].nil?) ? :belongs_to : (assn.options[:max] > 1) ? :many : nil , 55 | :class => assn.parent_model, 56 | :foreign_key => assn.child_key.first.name, 57 | :is_polymorphic => false # <-- No impl. for DM is_polymorphic. Anyone care to implement this? 58 | } 59 | end 60 | end 61 | @extjs_associations 62 | end 63 | end 64 | end 65 | end 66 | 67 | -------------------------------------------------------------------------------- /lib/model/mongo_mapper.rb: -------------------------------------------------------------------------------- 1 | ## 2 | # MongoMapper adapter to ExtJS::Model mixin 3 | # 4 | 5 | module ExtJS 6 | module Model 7 | ## 8 | # ClassMethods 9 | # 10 | module ClassMethods 11 | 12 | def extjs_primary_key 13 | :id 14 | end 15 | 16 | def extjs_column_names 17 | self.column_names 18 | end 19 | 20 | def extjs_columns_hash 21 | self.keys 22 | end 23 | 24 | def extjs_associations 25 | @extjs_associations ||= self.associations.inject({}) do |memo, (key, assn)| 26 | memo[key.to_sym] = { 27 | :name => key.to_sym, 28 | :type => assn.type, 29 | :class => assn.class_name.constantize, 30 | :foreign_key => assn.foreign_key, 31 | :is_polymorphic => false 32 | } 33 | memo 34 | end 35 | end 36 | 37 | def extjs_type(col) 38 | type = col.type.to_s 39 | case type 40 | when "DateTime", "Date", "Time" 41 | type = :date 42 | when "String" 43 | type = :string 44 | when "Float" 45 | type = :float 46 | when "Integer", "BigDecimal" 47 | type = :int 48 | else 49 | type = "auto" 50 | end 51 | end 52 | 53 | def extjs_allow_blank(col) 54 | (col.name == '_id') || (col.options[:required] != true) 55 | end 56 | 57 | def extjs_default(col) 58 | col.default_value 59 | end 60 | 61 | end 62 | end 63 | end 64 | 65 | -------------------------------------------------------------------------------- /lib/test/macros.rb: -------------------------------------------------------------------------------- 1 | module ExtJS 2 | module TestMacros 3 | ## 4 | # Asserts that the passed list of fields are specified in the extjs_fields call 5 | # in the model class. 6 | # @fields {Symbols} fields A list of fields 7 | # 8 | def should_have_extjs_fields *fields 9 | klass = model_class 10 | should "have the correct extjs_fields" do 11 | fields.each do |field| 12 | found_record = klass.extjs_record_fields.find do|record_field| 13 | record_field[:name] == field.to_s 14 | end 15 | assert_not_nil found_record, "extjs field #{field} isn't listed in the #{klass.name} model" 16 | end 17 | end 18 | end 19 | end 20 | end -------------------------------------------------------------------------------- /rails/init.rb: -------------------------------------------------------------------------------- 1 | require 'extjs-mvc' 2 | -------------------------------------------------------------------------------- /shoulda_macros/macros.rb: -------------------------------------------------------------------------------- 1 | class Test::Unit::TestCase 2 | ## 3 | # Asserts that the passed list of fields are specified in the extjs_fields call 4 | # in the model class. 5 | # @fields {Symbols} fields A list of fields 6 | # 7 | def self.should_have_extjs_fields *fields 8 | klass = described_type 9 | should "have the correct extjs_fields" do 10 | fields.each do |field| 11 | found_record = klass.extjs_record_fields.find do|record_field| 12 | record_field[:name] == field.to_s 13 | end 14 | assert_not_nil found_record, "extjs field #{field} isn't listed in the #{klass.name} model" 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/active_record_test.rb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extjs/mvc/b1972ca38457380e2acc1bfba113c74318881a69/test/active_record_test.rb -------------------------------------------------------------------------------- /test/app/config/application.rb: -------------------------------------------------------------------------------- 1 | # TODO: Figure out how to iterate each ORM framework AR, DM, MM and test each. 2 | require 'active_record' 3 | require 'active_support' 4 | require 'extjs-mvc' 5 | require 'extlib/inflection' 6 | 7 | gem 'sqlite3-ruby' 8 | 9 | class Test::App 10 | 11 | attr_reader :models 12 | 13 | def initialize(orm = :active_record) 14 | @orm = orm 15 | @config = YAML::load(IO.read("#{ROOT}/config/database.yml")) 16 | 17 | # Load ORM 18 | send("boot_#{orm.to_s}") 19 | 20 | load_models 21 | 22 | require 'db/schema' 23 | 24 | end 25 | 26 | ## 27 | # Reset a model's @extjs_fieldsets 28 | # 29 | def clean_all 30 | @models.map { |klass| clean klass } 31 | end 32 | 33 | 34 | private 35 | 36 | def boot_active_record 37 | ActiveRecord::Base.establish_connection(@config['test']) 38 | end 39 | 40 | def boot_mongo_mapper 41 | 42 | end 43 | 44 | def boot_data_mapper 45 | 46 | end 47 | 48 | ## 49 | # Do a dir on /models and constantize each filename 50 | # 51 | def load_models 52 | @models = [] 53 | # Load Models and Schema for corresponding orm 54 | re = /^.*\/(.*).rb$/ 55 | Dir["#{ROOT}/models/#{@orm.to_s}/*"].each { |c| 56 | require c 57 | match = c.match(re) 58 | @models << Extlib::Inflection.constantize(Extlib::Inflection.camelize(match[1])) if match 59 | } 60 | end 61 | 62 | def clean klass 63 | klass.instance_variables.each do |var_name| 64 | if /\A@extjs_fieldsets__/ =~ var_name.to_s 65 | klass.instance_variable_set( var_name.to_sym, nil ) 66 | end 67 | end 68 | end 69 | 70 | end -------------------------------------------------------------------------------- /test/app/config/database.yml: -------------------------------------------------------------------------------- 1 | test: 2 | adapter: sqlite3 3 | database: ":memory:" -------------------------------------------------------------------------------- /test/app/db/schema.rb: -------------------------------------------------------------------------------- 1 | 2 | ## 3 | # build simple database 4 | # 5 | # people 6 | # 7 | ActiveRecord::Base.connection.create_table :users, :force => true do |table| 8 | table.column :id, :serial 9 | table.column :person_id, :integer 10 | table.column :password, :string 11 | table.column :created_at, :date 12 | table.column :disabled, :boolean, :default => true 13 | end 14 | ## 15 | # people 16 | # 17 | ActiveRecord::Base.connection.create_table :people, :force => true do |table| 18 | table.column :id, :serial 19 | table.column :first, :string, :null => false 20 | table.column :last, :string, :null => false 21 | table.column :email, :string, :null => false 22 | end 23 | ## 24 | # user_groups, join table 25 | # 26 | ActiveRecord::Base.connection.create_table :user_groups, :force => true do |table| 27 | table.column :user_id, :integer 28 | table.column :group_id, :integer 29 | end 30 | 31 | ## 32 | # groups 33 | # 34 | ActiveRecord::Base.connection.create_table :groups, :force => true do |table| 35 | table.column :id, :serial 36 | table.column :title, :string 37 | end 38 | 39 | ## 40 | # locations 41 | # 42 | ActiveRecord::Base.connection.create_table :locations, :force => true do |table| 43 | table.column :id, :serial 44 | table.column :name, :string 45 | table.column :street, :string 46 | table.column :type, :string 47 | end 48 | 49 | ## 50 | # addresses 51 | # 52 | ActiveRecord::Base.connection.create_table :addresses, :force => true do |table| 53 | table.column :id, :serial 54 | table.column :addressable_type, :string 55 | table.column :addressable_id, :integer 56 | table.column :street, :string 57 | end 58 | 59 | ## 60 | # Mock a Model for testing data-types 61 | # 62 | ActiveRecord::Base.connection.create_table :data_types, :force => true do |table| 63 | table.column :id, :serial 64 | table.column :string_column, :string 65 | table.column :decimal_column, :decimal 66 | table.column :float_column, :float 67 | table.column :date_column, :date 68 | table.column :datetime_column, :datetime 69 | table.column :time_column, :time 70 | table.column :email, :string 71 | table.column :integer_column, :integer 72 | table.column :notnull_column, :string, :null => false 73 | table.column :default_column, :boolean, :default => true 74 | table.column :boolean_column, :boolean 75 | end 76 | -------------------------------------------------------------------------------- /test/app/models/active_record/address.rb: -------------------------------------------------------------------------------- 1 | class Address < ActiveRecord::Base 2 | belongs_to :addressable, :polymorphic => true 3 | include ExtJS::Model 4 | end -------------------------------------------------------------------------------- /test/app/models/active_record/data_type.rb: -------------------------------------------------------------------------------- 1 | class DataType < ActiveRecord::Base 2 | include ExtJS::Model 3 | end 4 | -------------------------------------------------------------------------------- /test/app/models/active_record/group.rb: -------------------------------------------------------------------------------- 1 | class Group < ActiveRecord::Base 2 | has_many :users 3 | include ExtJS::Model 4 | end -------------------------------------------------------------------------------- /test/app/models/active_record/house.rb: -------------------------------------------------------------------------------- 1 | require "#{File.dirname(__FILE__)}/location" 2 | 3 | class House < Location 4 | end -------------------------------------------------------------------------------- /test/app/models/active_record/location.rb: -------------------------------------------------------------------------------- 1 | class Location < ActiveRecord::Base 2 | has_one :address, :as => :addressable 3 | include ExtJS::Model 4 | end 5 | 6 | -------------------------------------------------------------------------------- /test/app/models/active_record/person.rb: -------------------------------------------------------------------------------- 1 | class Person < ActiveRecord::Base 2 | has_one :user 3 | include ExtJS::Model 4 | end -------------------------------------------------------------------------------- /test/app/models/active_record/user.rb: -------------------------------------------------------------------------------- 1 | class User < ActiveRecord::Base 2 | include ExtJS::Model 3 | belongs_to :person 4 | 5 | has_and_belongs_to_many :groups, :join_table => :user_groups 6 | end -------------------------------------------------------------------------------- /test/app/models/active_record/user_group.rb: -------------------------------------------------------------------------------- 1 | class UserGroup < ActiveRecord::Base 2 | belongs_to :user 3 | belongs_to :group 4 | end -------------------------------------------------------------------------------- /test/component_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class ComponentTest < Test::Unit::TestCase 4 | context "An ExtJS::Component Instance" do 5 | 6 | setup do 7 | @cmp = ExtJS::Component.new("title" => "A Component", "xtype" => "panel") 8 | end 9 | 10 | should "Render" do 11 | assert @cmp.render.match(/Ext.ComponentMgr.create/) 12 | end 13 | end 14 | end 15 | 16 | -------------------------------------------------------------------------------- /test/controller_test.rb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extjs/mvc/b1972ca38457380e2acc1bfba113c74318881a69/test/controller_test.rb -------------------------------------------------------------------------------- /test/data_mapper_test.rb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extjs/mvc/b1972ca38457380e2acc1bfba113c74318881a69/test/data_mapper_test.rb -------------------------------------------------------------------------------- /test/model_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | ## 4 | # create a couple of related instances. 5 | # 6 | p = Person.create(:first => "Chris", :last => "Scott", :email => "chris@scott.com") 7 | u = User.create(:password => "1234", :person => p) 8 | 9 | class BogusModel 10 | include ExtJS::Model 11 | def additional_attribute 12 | 'computed value' 13 | end 14 | class << self 15 | def extjs_allow_blank(col) 16 | true 17 | end 18 | 19 | def extjs_default(col) 20 | nil 21 | end 22 | 23 | def extjs_type(col) 24 | nil 25 | end 26 | 27 | def extjs_column_names 28 | [:one, :two, :three_id] 29 | end 30 | 31 | def extjs_columns_hash 32 | { 33 | :one => {}, 34 | :two => {}, 35 | :three_id => {} 36 | } 37 | end 38 | 39 | def extjs_polymorphic_type(id_column_name) 40 | id_column_name.to_s.gsub(/_id\Z/, '_type').to_sym 41 | end 42 | 43 | def extjs_primary_key 44 | :id 45 | end 46 | 47 | def extjs_associations 48 | { 49 | :three => { 50 | :name => :tree, 51 | :type => :belongs_to, 52 | :class => nil, 53 | :foreign_key => :three_id, 54 | :is_polymorphic => false 55 | } 56 | } 57 | end 58 | end 59 | end 60 | 61 | 62 | class BogusModelChild < BogusModel 63 | end 64 | 65 | class ModelTest < Test::Unit::TestCase 66 | context "Rendering DataReader configuration for Person and User" do 67 | 68 | setup do 69 | App.clean_all 70 | end 71 | 72 | should "Person and User should render a valid Reader config" do 73 | reader = Person.extjs_record 74 | assert reader.kind_of?(Hash) && reader.has_key?(:fields) && reader.has_key?(:idProperty) 75 | end 76 | should "Person instance should render with to_record, a Hash containing at least a primary_key" do 77 | rec = Person.first.to_record 78 | assert_kind_of(Hash, rec) 79 | assert_array_has_item(rec.keys, 'has primary key') { |i| i.to_s == Person.extjs_primary_key.to_s } 80 | end 81 | should "User should render a Reader config" do 82 | reader = User.extjs_record 83 | assert reader.kind_of?(Hash) && reader.has_key?(:fields) && reader.has_key?(:idProperty) 84 | end 85 | should "User instance should render with to_record, a Hash containing at least a primary_key" do 86 | rec = User.first.to_record 87 | assert rec.kind_of?(Hash) && rec.keys.include?(User.extjs_primary_key) 88 | end 89 | should "User instance should render to_record containing foreign_key of Person" do 90 | rec = User.first.to_record 91 | assn = User.extjs_associations[:person] 92 | assert rec.keys.include?(assn[:foreign_key]) 93 | end 94 | 95 | end 96 | 97 | context "A User with HABTM relationship with Group" do 98 | setup do 99 | App.clean_all 100 | UserGroup.destroy_all 101 | 102 | @user = User.first 103 | UserGroup.create(:user => @user, :group => Group.create(:title => "Merb")) 104 | UserGroup.create(:user => @user, :group => Group.create(:title => "Rails")) 105 | end 106 | 107 | should "Render to_record should return 2 groups" do 108 | User.extjs_fields(:groups) 109 | assert @user.to_record[:groups].length == 2 110 | end 111 | end 112 | 113 | context "A User with Person relationship: User.extjs_fields(:password, :person => [:first, {:last => {'sortDir' => 'ASC'}}])" do 114 | setup do 115 | App.clean_all 116 | User.extjs_fields(:password, {:person => [:first, {:last => {:sortDir => "ASC"}}]}) 117 | @fields = User.extjs_record[:fields] 118 | end 119 | 120 | should "User should render a Reader with 4 total fields" do 121 | assert @fields.count === 4 122 | end 123 | should "Reader fields should contain 'password' field" do 124 | assert_array_has_item(@fields, 'has password field') {|f| f[:name] === "password"} 125 | end 126 | should "Reader fields should contain person_id" do 127 | assns = User.extjs_associations 128 | assn = assns[:person] 129 | assert_array_has_item(@fields, 'has foreign key person_id') {|f| f[:name] === assns[:person][:foreign_key].to_s } 130 | end 131 | should "Reader fields should contain mapped field 'person.first'" do 132 | assert_array_has_item(@fields, 'has person_first') {|f| f[:name] === "person_first" and f[:mapping] === "person.first"} 133 | end 134 | should "Reader fields should contain mapped field 'person.last'" do 135 | assert_array_has_item(@fields, 'has person_last') {|f| f[:name] === "person_last" and f[:mapping] === "person.last"} 136 | end 137 | should "person.last should have additional configuration 'sortDir' => 'ASC'" do 138 | assert_array_has_item(@fields, 'has person.last with sortDir') {|f| f[:name] === "person_last" and f[:sortDir] === 'ASC' } 139 | end 140 | 141 | should "produce a valid to_record record" do 142 | person = Person.create!(:first => 'first', :last => 'last', :email => 'email') 143 | user = User.create!(:person_id => person.id, :password => 'password') 144 | record = user.to_record 145 | assert_equal(user.id, record[:id]) 146 | assert_equal(person.id, record[:person_id]) 147 | assert_equal('password', record[:password]) 148 | assert_equal('last', record[:person][:last]) 149 | assert_equal('first', record[:person][:first]) 150 | end 151 | end 152 | 153 | context "User with standard Person association" do 154 | setup do 155 | App.clean_all 156 | User.extjs_fields(:id, :password, :person) 157 | end 158 | should "produce a valid store config" do 159 | fields = User.extjs_record[:fields] 160 | assert_array_has_item(fields, 'has id') {|f| f[:name] === "id" } 161 | assert_array_has_item(fields, 'has person_id') {|f| f[:name] === "person_id" } 162 | assert_array_has_item(fields, 'has password') {|f| f[:name] === "password" } 163 | assert_array_has_item(fields, 'has person_last') {|f| f[:name] === "person_last" and f[:mapping] == "person.last" } 164 | assert_array_has_item(fields, 'has person_first') {|f| f[:name] === "person_first" and f[:mapping] == "person.first" } 165 | end 166 | should "produce a valid to_record record" do 167 | person = Person.create!(:first => 'first', :last => 'last', :email => 'email') 168 | user = User.create!(:person_id => person.id, :password => 'password') 169 | record = user.to_record 170 | assert_equal(user.id, record[:id]) 171 | assert_equal(person.id, record[:person_id]) 172 | assert_equal('password', record[:password]) 173 | assert_equal('last', record[:person][:last]) 174 | assert_equal('first', record[:person][:first]) 175 | end 176 | end 177 | 178 | context "Person with User association (has_one relationship)" do 179 | setup do 180 | App.clean_all 181 | User.extjs_fields(:id, :password) 182 | Person.extjs_fields(:id, :user) 183 | end 184 | should "produce a valid store config" do 185 | fields = Person.extjs_record[:fields] 186 | assert_array_has_item(fields, 'has id') {|f| f[:name] === "id" } 187 | assert_array_has_item(fields, 'has user_id') {|f| f[:name] === "user_id" and f[:mapping] == 'user.id' } 188 | assert_array_has_item(fields, 'has user_password') {|f| f[:name] === "user_password"and f[:mapping] == 'user.password' } 189 | end 190 | should "produce a valid to_record record" do 191 | person = Person.create!(:first => 'first', :last => 'last', :email => 'email') 192 | user = User.create!(:person_id => person.id, :password => 'password') 193 | record = person.reload.to_record 194 | assert_equal(person.id, record[:id]) 195 | assert_equal(user.id, record[:user][:id]) 196 | assert_equal('password', record[:user][:password]) 197 | end 198 | end 199 | 200 | context "Person with User association (has_one/belongs_to relationship) cyclic reference" do 201 | setup do 202 | App.clean_all 203 | User.extjs_fields(:id, :person) 204 | Person.extjs_fields(:id, :user) 205 | end 206 | should "produce a valid store config for Person" do 207 | fields = Person.extjs_record[:fields] 208 | assert_array_has_item(fields, 'has id') {|f| f[:name] === "id" } 209 | assert_array_has_item(fields, 'has user_id') {|f| f[:name] === "user_id" and f[:mapping] == 'user.id' } 210 | end 211 | should "produce a valid to_record record for Person" do 212 | person = Person.create!(:first => 'first', :last => 'last', :email => 'email') 213 | user = User.create!(:person_id => person.id, :password => 'password') 214 | record = person.reload.to_record 215 | assert_equal(person.id, record[:id]) 216 | assert_equal(user.id, record[:user][:id]) 217 | end 218 | end 219 | 220 | context "Fields should render with correct, ExtJS-compatible data-types" do 221 | setup do 222 | App.clean_all 223 | @fields = DataType.extjs_record[:fields] 224 | end 225 | 226 | should "Understand 'string'" do 227 | assert_array_has_item(@fields, 'has string_column with string') {|f| f[:name] == 'string_column' and f[:type] == 'string'} 228 | end 229 | should "Understand 'integer' as 'int'" do 230 | assert_array_has_item(@fields, 'has integer_column with int') {|f| f[:name] == 'integer_column' and f[:type] == 'int'} 231 | end 232 | should "Understand 'float'" do 233 | assert_array_has_item(@fields, 'has float_column with float') {|f| f[:name] == 'float_column' and f[:type] == 'float'} 234 | end 235 | should "Understand 'decimal' as 'float'" do # Is this correct?? 236 | assert_array_has_item(@fields, 'has decimal_column with float') {|f| f[:name] == 'decimal_column' and f[:type] == 'float'} 237 | end 238 | should "Understand 'date'" do 239 | assert_array_has_item(@fields, 'has date_column with date') {|f| f[:name] == 'date_column' and f[:type] == 'date'} 240 | end 241 | should "Understand 'datetime' as 'date'" do 242 | assert_array_has_item(@fields, 'has datetime_column with date') {|f| f[:name] == 'datetime_column' and f[:type] == 'date'} 243 | end 244 | should "Understand 'time' as 'date'" do 245 | assert_array_has_item(@fields, 'has time_column with date') {|f| f[:name] == 'time_column' and f[:type] == 'date'} 246 | end 247 | should "Understand 'boolean'" do 248 | assert_array_has_item(@fields, 'has boolean_column with boolean') {|f| f[:name] == 'boolean_column' and f[:type] == 'boolean'} 249 | end 250 | should "Understand NOT NULL" do 251 | assert_array_has_item(@fields, 'has notnull_column with allowBlank == false') {|f| f[:name] == 'notnull_column' and f[:allowBlank] === false} 252 | end 253 | should "Understand DEFAULT" do 254 | assert_array_has_item(@fields, 'has default_column with defaultValue == true') {|f| f[:name] == 'default_column' and f[:defaultValue] === true} 255 | end 256 | end 257 | 258 | context "polymorphic associations" do 259 | setup do 260 | App.clean_all 261 | end 262 | 263 | should "return nil as class for a polymorphic relation" do 264 | assert_equal(nil, Address.extjs_associations[:addressable][:class]) 265 | end 266 | 267 | should "create a proper default store config" do 268 | Address.extjs_fields 269 | fields = Address.extjs_record[:fields] 270 | assert_array_has_item(fields, 'has addressable_id') {|f| f[:name] === 'addressable_id' && !f[:mapping] } 271 | assert_array_has_item(fields, 'addressable_type') {|f| f[:name] === 'addressable_type' && !f[:mapping] } 272 | end 273 | 274 | should "create the right store config when including members of the polymorphic association" do 275 | Address.extjs_fields :street, :addressable => [:name] 276 | fields = Address.extjs_record[:fields] 277 | assert_array_has_item(fields, "has addressable_name") {|f| f[:name] === 'addressable_name' && f[:mapping] === 'addressable.name'} 278 | assert_array_has_item(fields, "has addressable_id") {|f| f[:name] === 'addressable_id' && !f[:mapping] } 279 | assert_array_has_item(fields, "has addressable_type") {|f| f[:name] === 'addressable_type' && !f[:mapping] } 280 | end 281 | 282 | should "fill in the right values for to_record" do 283 | Address.extjs_fields :street, :addressable => [:name] 284 | location = Location.create!(:name => 'Home') 285 | address = location.create_address(:street => 'Main Street 1') 286 | record = address.to_record 287 | assert_equal({:name => "Home", :id => location.id}, record[:addressable]) 288 | assert_equal("Location", record[:addressable_type]) 289 | assert_equal(location.id, record[:addressable_id]) 290 | assert_equal(address.id, record[:id]) 291 | assert_equal("Main Street 1", record[:street]) 292 | end 293 | end 294 | 295 | context "single table inheritance" do 296 | setup do 297 | App.clean_all 298 | end 299 | 300 | should "fieldsets should be accessible from descendants" do 301 | Location.extjs_fieldset :on_location, [:street] 302 | fields = House.extjs_record(:on_location)[:fields] 303 | assert_array_has_item(fields, 'has street') {|f| f[:name] === 'street' } 304 | assert_array_has_not_item(fields, 'has name') {|f| f[:name] === 'name' } 305 | end 306 | should "fieldsets should be overrideable from descendants" do 307 | Location.extjs_fieldset :override, [:street] 308 | House.extjs_fieldset :override, [:name] 309 | fields = House.extjs_record(:override)[:fields] 310 | assert_array_has_not_item(fields, 'has street') {|f| f[:name] === 'street' } 311 | assert_array_has_item(fields, 'has name') {|f| f[:name] === 'name' } 312 | end 313 | end 314 | 315 | context "ExtJS::Model::Util" do 316 | context "#extract_fieldset_and_options default" do 317 | setup do 318 | @fieldset, @options = ExtJS::Model::Util.extract_fieldset_and_options [:fields => [:one, :two, :three]] 319 | @fields = @options[:fields] 320 | end 321 | should "return :default when no fieldset provided" do 322 | assert_equal(:'default', @fieldset) 323 | end 324 | should "not alter the fields array" do 325 | assert_equal([:one, :two, :three], @fields) 326 | end 327 | end 328 | 329 | context "#extract_fieldset_and_options with explicit fieldset definition and array with fields" do 330 | setup do 331 | @fieldset, @options = ExtJS::Model::Util.extract_fieldset_and_options [:explicit, [:one, :two, :three]] 332 | @fields = @options[:fields] 333 | end 334 | should "return :default when no fieldset provided" do 335 | assert_equal(:'explicit', @fieldset) 336 | end 337 | should "not alter the fields array" do 338 | assert_equal([:one, :two, :three], @fields) 339 | end 340 | end 341 | 342 | context "#extract_fieldset_and_options with explicit fieldset definition and hash with fields" do 343 | setup do 344 | @fieldset, @options = ExtJS::Model::Util.extract_fieldset_and_options [:explicit, {:fields => [:one, :two, :three]}] 345 | @fields = @options[:fields] 346 | end 347 | should "return :default when no fieldset provided" do 348 | assert_equal(:'explicit', @fieldset) 349 | end 350 | should "not alter the fields array" do 351 | assert_equal([:one, :two, :three], @fields) 352 | end 353 | end 354 | 355 | context "#extract_fieldset_and_options with only a hash" do 356 | setup do 357 | @fieldset, @options = ExtJS::Model::Util.extract_fieldset_and_options [{:fieldset => :explicit, :fields => [:one, :two, :three]}] 358 | @fields = @options[:fields] 359 | end 360 | should "return :default when no fieldset provided" do 361 | assert_equal(:'explicit', @fieldset) 362 | end 363 | should "not alter the fields array" do 364 | assert_equal([:one, :two, :three], @fields) 365 | end 366 | end 367 | 368 | context "#extract_fieldset_and_options edge cases" do 369 | should "called without arguments" do 370 | @fieldset, @options = ExtJS::Model::Util.extract_fieldset_and_options [] 371 | @fields = @options[:fields] 372 | assert_equal(:'default', @fieldset) 373 | assert_equal([], @fields) 374 | end 375 | should "called with only the fieldset and no field arguments" do 376 | @fieldset, @options = ExtJS::Model::Util.extract_fieldset_and_options [:explicit] 377 | @fields = @options[:fields] 378 | assert_equal(:'explicit', @fieldset) 379 | assert_equal([], @fields) 380 | end 381 | should "raise error when called with more than 2 arguments" do 382 | assert_raise(ArgumentError) { ExtJS::Model::Util.extract_fieldset_and_options [:explicit, :some, {}] } 383 | end 384 | should "raise error when called with 2 arguments and the first one is no symbol" do 385 | assert_raise(ArgumentError) { ExtJS::Model::Util.extract_fieldset_and_options [{ :fields => [] }, :explicit] } 386 | end 387 | end 388 | end 389 | 390 | context "ExtJS::Model::ClassMethods" do 391 | 392 | context "#process_fields" do 393 | should "handle a simple Array of Symbols" do 394 | @fields = BogusModel.process_fields :one, :two, :three 395 | assert_equal([{:name => :one}, {:name => :two}, {:name => :three}], @fields) 396 | end 397 | should "handle a mixed Array where the last item is a Hash" do 398 | @fields = BogusModel.process_fields :one, :two, :three => [:three_one, :three_two] 399 | assert_equal([{:name => :one}, {:name => :two}, {:name => :three, :fields => [{:name => :three_one}, {:name => :three_two}]}], @fields) 400 | end 401 | should "handle a mixed Array where a middle item is a Hash" do 402 | @fields = BogusModel.process_fields :one, {:two => [:two_one, :two_two]}, :three 403 | assert_equal([ 404 | {:name => :one}, 405 | {:name => :two, :fields => [{:name => :two_one}, {:name => :two_two}]}, 406 | {:name => :three}], @fields) 407 | end 408 | should "handle option :only" do 409 | @fields = BogusModel.process_fields :only => [:one, :two, :three] 410 | assert_equal([{:name => :one}, {:name => :two}, {:name => :three}], @fields) 411 | end 412 | should "handle option :exclude" do 413 | @fields = BogusModel.process_fields :exclude => [:two] 414 | assert_equal([{:name => :one}, {:name => :three_id}], @fields) 415 | end 416 | should "handle option :additional" do 417 | @fields = BogusModel.process_fields :additional => [:additional_attribute] 418 | assert_equal([{:name => :one}, {:name => :two}, {:name => :three_id}, {:name => :additional_attribute}], @fields) 419 | 420 | end 421 | should "handle {:field => {:sortDir => 'ASC'}}" do 422 | @fields = BogusModel.process_fields({:field => {:sortDir => 'ASC'}}) 423 | assert_equal([{:name => :field, :sortDir => 'ASC'}], @fields) 424 | end 425 | should "handle recursive definition" do 426 | @fields = BogusModel.process_fields(:one, {:three => [{:one => [:one, :two]}, {:two => {:sortDir => "ASC"}}]}) 427 | assert_equal([{:name => :one}, {:name => :three, :fields => [{:name => :one, :fields => [{:name => :one}, {:name => :two}]}, {:name => :two, :sortDir => 'ASC'}]}], @fields) 428 | end 429 | should "not touch already correct fields" do 430 | @fields = BogusModel.process_fields(:one, {:name => :field,:sortDir => 'ASC'}) 431 | assert_equal([{:name => :one},{:name => :field, :sortDir => 'ASC'}], @fields) 432 | end 433 | should "raise ArgumentError when pass in bogus hash" do 434 | assert_raise(ArgumentError) { @fields = BogusModel.process_fields(:one, {:nme => :field,:sortDir => 'ASC'}) } 435 | end 436 | end 437 | 438 | context "#extjs_field" do 439 | should "type gets set to 'auto' when not present" do 440 | @field = BogusModel.extjs_field({:name => :test}) 441 | assert_equal('auto', @field[:type]) 442 | end 443 | should "not touch type when alredy present" do 444 | @field = BogusModel.extjs_field({:name => :test, :type => 'untouched'}) 445 | assert_equal('untouched', @field[:type]) 446 | end 447 | should "raise exception when bogus field config passed" do 448 | assert_raise(ArgumentError) { BogusModel.extjs_field({:name => :test, "type" => 'untouched'}) } 449 | end 450 | 451 | end 452 | 453 | context "#extjs_field with ORM config" do 454 | should "set allowBlank" do 455 | BogusModel.expects(:extjs_allow_blank).returns(false) 456 | @field = BogusModel.extjs_field({:name => :test}, stub()) 457 | assert_equal(false, @field[:allowBlank]) 458 | end 459 | should "set type" do 460 | BogusModel.expects(:extjs_type).returns('int') 461 | @field = BogusModel.extjs_field({:name => :test}, stub()) 462 | assert_equal('int', @field[:type]) 463 | end 464 | should "set defaultValue" do 465 | BogusModel.expects(:extjs_default).returns(true) 466 | @field = BogusModel.extjs_field({:name => :test}, stub()) 467 | assert_equal(true, @field[:defaultValue]) 468 | end 469 | should "set dateFormat to c it's a date" do 470 | BogusModel.expects(:extjs_type).returns('date') 471 | @field = BogusModel.extjs_field({:name => :test}, stub()) 472 | assert_equal('c', @field[:dateFormat]) 473 | end 474 | should "not touch dateFormat if it's already set" do 475 | BogusModel.expects(:extjs_type).returns('date') 476 | @field = BogusModel.extjs_field({:name => :test, :dateFormat => 'not-c'}, stub()) 477 | assert_equal('not-c', @field[:dateFormat]) 478 | end 479 | end 480 | 481 | context "#extjs_field with Hash config" do 482 | should "set correct name and mapping" do 483 | @field = BogusModel.extjs_field({:name => :son}, {:mapping => 'grandfather.father', :parent_trail => 'grandfather_father'}) 484 | assert_equal('grandfather_father_son', @field[:name]) 485 | assert_equal('grandfather.father.son', @field[:mapping]) 486 | end 487 | should "apply config to field" do 488 | @field = BogusModel.extjs_field({:name => :son}, {:sortDir => 'ASC'}) 489 | assert_equal('ASC', @field[:sortDir]) 490 | end 491 | end 492 | 493 | context "#extjs_get_fields_for_fieldset" do 494 | should "return full list of columns for fieldset that was not defined, yet" do 495 | @fields = BogusModel.extjs_get_fields_for_fieldset :not_there 496 | assert_equal(BogusModel.process_fields(*BogusModel.extjs_column_names), @fields) 497 | end 498 | should "return the right fields for a fieldset that was defined before in the same class" do 499 | BogusModel.extjs_fieldset :fieldset_was_defined, [:one] 500 | @fields = BogusModel.extjs_get_fields_for_fieldset :fieldset_was_defined 501 | assert_equal(BogusModel.process_fields(:one), @fields) 502 | end 503 | should "return the fieldset of the ancestor when it was only defined in the ancestor" do 504 | BogusModel.extjs_fieldset :fieldset_was_defined_in_ancestor, [:one] 505 | @fields = BogusModelChild.extjs_get_fields_for_fieldset :fieldset_was_defined_in_ancestor 506 | assert_equal(BogusModel.process_fields(:one), @fields) 507 | end 508 | should "return the fieldset of the child when it was defined in the child and the ancestor" do 509 | BogusModel.extjs_fieldset :fieldset_was_defined_in_both, [:one] 510 | BogusModelChild.extjs_fieldset :fieldset_was_defined_in_both, [:two] 511 | @fields = BogusModelChild.extjs_get_fields_for_fieldset :fieldset_was_defined_in_both 512 | assert_equal(BogusModel.process_fields(:two), @fields) 513 | end 514 | end 515 | end 516 | 517 | protected 518 | def assert_array_has_item array, item_description, &blk 519 | assert array.find {|i| blk.call(i) }, "The array #{array.inspect} should #{item_description} but it does not" 520 | end 521 | def assert_array_has_not_item array, item_description, &blk 522 | assert !array.find {|i| blk.call(i) }, "The array #{array.inspect} should not #{item_description} but it does" 523 | end 524 | 525 | end 526 | 527 | -------------------------------------------------------------------------------- /test/mongo_mapper_test.rb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extjs/mvc/b1972ca38457380e2acc1bfba113c74318881a69/test/mongo_mapper_test.rb -------------------------------------------------------------------------------- /test/store_test.rb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extjs/mvc/b1972ca38457380e2acc1bfba113c74318881a69/test/store_test.rb -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'test/unit' 3 | require 'shoulda' 4 | require 'mocha' 5 | 6 | begin 7 | require 'ruby-debug' 8 | rescue LoadError 9 | puts "ruby-debug not loaded" 10 | end 11 | 12 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 13 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 14 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), 'app')) 15 | 16 | ROOT = File.join(File.dirname(__FILE__), 'app') 17 | require "config/application" 18 | 19 | ## 20 | # Boot test app. 21 | # TODO, send orm as param from console 22 | # eg: >rake test data_mapper 23 | # >rake test mongo_mapper 24 | # 25 | App = Test::App.new(:active_record) 26 | 27 | #FIXTURES_DIR = File.join(File.dirname(__FILE__), "fixtures") 28 | 29 | 30 | class Test::Unit::TestCase 31 | end 32 | 33 | --------------------------------------------------------------------------------