extjs-mvc 0.2.6 → 0.2.7

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -1,37 +1,47 @@
1
1
  = mvc
2
2
 
3
3
 
4
- A collection of helpers, MVC mixins and PORs (plain-old-ruby-object) to assist with auto-generating ExtJS javascript component definitions.
4
+ 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.
5
5
 
6
6
 
7
7
  ===Installation
8
-
9
- % gem sources -a http://gems.github.com (you only have to do this once)
8
+ % sudo gem install gemcutter
9
+ % gem tumble (only have to do this once, adds gemcutter as primary gem-source)
10
10
  % sudo gem install extjs-mvc
11
11
 
12
- In <tt>environment.rb</tt>
12
+ <p><b>Rails Installation:</b></p>
13
+ In <tt>environment.rb</tt>,
13
14
 
14
15
  Rails::Initializer.run do |config|
15
16
  config.gem "extjs-mvc"
16
17
  end
17
18
 
19
+ <p><b>Merb installation:</b></p>
20
+ In <tt>config/dependencies.rb</tt>, Add extjs-mvc as a new dependency
21
+
22
+ dependency "extjs-mvc"
18
23
 
19
- === An ActiveRecord mixin: ExtJS::Model
24
+ === An ORM Model mixin: ExtJS::Model
25
+ extjs-mvc contains Model mixin named <tt>ExtJS::Model</tt> which works for <b>three</b> popular ORM frameworks, ActiveRecord, DataMapper and MongoMapper. The API for each framework is identical.
20
26
 
21
- include it in your model. Use the class-method <tt>extjs_fields</tt> to specify those
27
+ Simply include the mixin into your model. Use the class-method <tt>extjs_fields</tt> to specify those
22
28
  fields with will be used to render the <tt>Ext.data.Record.create</tt> field-def'n.
29
+
23
30
  class User < ActiveRecord::Base
24
31
  include ExtJS::Model
25
32
 
26
33
  extjs_fields :exclude => [:password, :password_confirmation]
27
- # OR
34
+ # OR
28
35
  extjs_fields :name, :description
29
- # OR
36
+ # OR define a column as a Hash
37
+ extjs_fields :description, :name => {"sortDir" => "ASC"}, :created_at => {"dateFormat" => "c"}
38
+ #
30
39
  extjs_fields :name, :description, :parent => [:name, :date]
31
- # this would configure associated columns with names like "parent__name" and "parent__date")
40
+ # this would configure associated columns with names like:
41
+ [...{name: "parent_name", mapping: "parent.name"}, {name: "parent_date", mapping: "parent.date"}...]
32
42
  end
33
43
 
34
- In script/console
44
+ In <tt>irb</tt> console:
35
45
  >> User.extjs_fields
36
46
  >> User.extjs_record
37
47
  => { "idProperty"=>"id", "fields"=>[
@@ -42,13 +52,13 @@ In script/console
42
52
  ]}
43
53
 
44
54
  === An ActionController mixin: ExtJS::Controller
45
-
55
+ The <tt>extjs-mvc</tt> Gem includes a framework agnostic Controller mixin which works with both Rails and Merb.
46
56
  <b>usage:</b>
47
57
 
48
58
  class UsersController < ActionController::Base
49
59
  include ExtJS::Controller
50
60
  end
51
-
61
+
52
62
  === View Helper: ExtJS::Helpers::Component
53
63
 
54
64
  <b>usage:</b>
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.2.6
1
+ 0.2.7
@@ -1,9 +1,12 @@
1
+ ##
2
+ # ActiveRecord adapter to ExtJS::Model mixin.
3
+ #
1
4
  module ExtJS
2
5
  module Model
3
6
  module ClassMethods
4
7
 
5
8
  def extjs_primary_key
6
- self.primary_key
9
+ self.primary_key.to_sym
7
10
  end
8
11
 
9
12
  def extjs_column_names
@@ -14,7 +17,21 @@ module ExtJS
14
17
  self.columns_hash
15
18
  end
16
19
 
17
- def extjs_render_column(col)
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
+ col.null
27
+ end
28
+
29
+ ##
30
+ # determine datatype of supplied Column object
31
+ # @param {ActiveRecord::ConnectionAdapters::Column}
32
+ # @return {Symbol}
33
+ #
34
+ def extjs_type(col)
18
35
  type = col.type
19
36
  case type
20
37
  when :datetime || :date || :time || :timestamp
@@ -23,17 +40,28 @@ module ExtJS
23
40
  type = :string
24
41
  when :integer
25
42
  type = :int
43
+ when :decimal
44
+ type = :float
26
45
  end
27
- {:name => col.name, :allowBlank => (col.primary) ? true : col.null, :type => type}
46
+ type
28
47
  end
29
48
 
49
+ ##
50
+ # return a simple, normalized list of AR associations having the :name, :type and association class
51
+ # @return {Array}
52
+ #
30
53
  def extjs_associations
31
54
  if @extjs_associations.nil?
32
55
  @extjs_associations = {}
33
56
  self.reflections.keys.each do |key|
34
57
  assn = self.reflections[key]
35
58
  type = (assn.macro === :has_many) ? :many : assn.macro
36
- @extjs_associations[key.to_sym] = {:name => key, :type => type}
59
+ @extjs_associations[key.to_sym] = {
60
+ :name => key,
61
+ :type => type,
62
+ :class => assn.class_name.constantize,
63
+ :foreign_key => assn.association_foreign_key
64
+ }
37
65
  end
38
66
  end
39
67
  @extjs_associations
data/lib/model/base.rb CHANGED
@@ -5,9 +5,19 @@ module ExtJS
5
5
  model.send(:extend, ClassMethods)
6
6
  model.send(:include, InstanceMethods)
7
7
  model.class_eval do
8
+ ##
9
+ # @config {Array} List of DataReader fields to render. This should probably not be a cattr_accessor
10
+ # since it's only used internally. The user adds fields via the Class method extjs_fields instead.
11
+ #
8
12
  cattr_accessor :extjs_record_fields
13
+ ##
14
+ # @config {String} extjs_mapping_template This a template used to render mapped field-names.
15
+ # One could use the Rails standard "{association}[{property}]" as well.
16
+ #
17
+ cattr_accessor :extjs_mapping_template
9
18
  end
10
19
  model.extjs_record_fields = []
20
+ model.extjs_mapping_template = "_{property}"
11
21
  end
12
22
 
13
23
  ##
@@ -15,28 +25,39 @@ module ExtJS
15
25
  #
16
26
  module InstanceMethods
17
27
 
18
- def to_record
19
- pk = self.class.extjs_primary_key
20
- assns = self.class.extjs_associations
28
+ ##
29
+ # Converts a model instance to a record compatible with ExtJS
30
+ # @params {Mixed} params A list of fields to use instead of this Class's extjs_record_fields
31
+ #
32
+ def to_record(*params)
33
+
34
+ fields = (params.empty?) ? self.class.extjs_record_fields : self.class.process_fields(*params)
35
+ assns = self.class.extjs_associations
36
+ pk = self.class.extjs_primary_key
21
37
 
22
- data = {pk => self.send(pk)}
23
- self.class.extjs_record_fields.each do |f|
24
- if refl = assns[f]
38
+ # build the initial field data-hash
39
+ data = {pk => self.send(pk)}
40
+
41
+ fields.each do |field|
42
+ if refl = assns[field[:name]] || assns[field[:name].to_sym]
25
43
  if refl[:type] === :belongs_to
26
- assn = self.send(f)
27
- data[f] = (assn) ? assn.to_record : {} # <-- a thing was requested, give emtpy thing.
44
+ assn = self.send(field[:name])
45
+ if assn.respond_to?(:to_record)
46
+ data[field[:name]] = assn.send(:to_record, *[field[:fields]])
47
+ elsif (field[:fields])
48
+ data[field[:name]] = {}
49
+ field[:fields].each do |property|
50
+ data[field[:name]][property] = assn.send(property) if assn.respond_to?(property)
51
+ end
52
+ else
53
+ data[field[:name]] = {} # belongs_to assn that doesn't respond to to_record and no fields list
54
+ end
28
55
  elsif refl[:type] === :many
29
- data[f] = self.send(f).collect {|r| r.to_record} #CAREFUL!!!!!!!!!!!!1
56
+ data[field[:name]] = self.send(field[:name]).collect {|r| r.to_record} #CAREFUL!!!!!!!!!!!!1
30
57
  end
31
- elsif f.is_a? Array
32
- value = self
33
- f.each do |method|
34
- value = value.send(method)
35
- break if value.nil?
36
- end
37
- data[f.join('__')] = value
38
58
  else
39
- data[f] = self.send(f)
59
+ value = self.send(field[:name])
60
+ data[field[:name]] = value.respond_to?(:to_record) ? value.to_record : value
40
61
  end
41
62
  end
42
63
  data
@@ -51,66 +72,154 @@ module ExtJS
51
72
  # render AR columns to Ext.data.Record.create format
52
73
  # eg: {name:'foo', type: 'string'}
53
74
  #
54
- def extjs_record
55
-
75
+ def extjs_record(*fields)
56
76
  if self.extjs_record_fields.empty?
57
- self.extjs_record_fields = self.extjs_column_names
77
+ self.extjs_fields(*self.extjs_column_names)
58
78
  end
59
-
60
- pk = self.extjs_primary_key
61
- columns = self.extjs_columns_hash
62
- associations = self.extjs_associations
63
79
 
64
- return {
65
- "fields" => self.extjs_record_fields.collect {|f|
66
- if columns[f.to_sym] || columns[f.to_s]
67
- field = self.extjs_render_column(columns[f.to_sym] || columns[f.to_s])
68
- field[:dateFormat] = "c" if field[:type] === :date # <-- ugly hack for date
69
- field
70
- elsif assn = associations[f]
71
- field = {:name => f, :allowBlank => true, :type => 'auto'}
72
- elsif f.is_a? Array
73
- field = {:name => f.join('__'), :type => 'auto'}
74
- else # property is a method?
75
- field = {:name => f, :allowBlank => true, :type => 'auto'}
80
+ associations = self.extjs_associations
81
+ columns = self.extjs_columns_hash
82
+ fields = fields.empty? ? self.extjs_record_fields : self.process_fields(*fields)
83
+ pk = self.extjs_primary_key
84
+ rs = []
85
+
86
+ fields.each do |field|
87
+ field = field.dup
88
+
89
+ if col = columns[field[:name]] || columns[field[:name].to_sym] # <-- column on this model
90
+ rs << self.extjs_field(field, col)
91
+ elsif assn = associations[field[:name]] || associations[field[:name].to_sym]
92
+ assn_fields = field.delete(:fields) || nil
93
+ if assn[:class].respond_to?(:extjs_record) # <-- exec extjs_record on assn Model.
94
+ record = assn[:class].send(:extjs_record, *[assn_fields])
95
+ rs.concat(record["fields"].collect {|assn_field|
96
+ extjs_field(assn_field, :mapping => field[:name])
97
+ })
98
+ elsif assn_fields # <-- :parent => [:id, :name]
99
+ rs.concat(assn_fields.collect {|assn_field|
100
+ extjs_field(assn_field, :mapping => field[:name])
101
+ })
102
+ else
103
+ rs << extjs_field(field)
104
+ end
105
+
106
+ # attach association's foreign_key if not already included.
107
+ if (col = columns[assn[:foreign_key]] || columns[assn[:foreign_key].to_s]) && !rs.include?({:name => assn[:foreign_key].to_s})
108
+ rs << extjs_field({:name => assn[:foreign_key]}, col)
76
109
  end
77
- },
110
+
111
+ else # property is a method?
112
+ rs << extjs_field(field)
113
+ end
114
+ end
115
+
116
+ return {
117
+ "fields" => rs,
78
118
  "idProperty" => pk
79
119
  }
80
120
  end
81
121
 
122
+ ##
123
+ # meant to be used within a Model to define the extjs record fields.
124
+ # eg:
125
+ # class User
126
+ # extjs_fields :first, :last, :email => {"sortDir" => "ASC"}, :company => [:id, :name]
127
+ # end
128
+ #
82
129
  def extjs_fields(*params)
130
+ self.extjs_record_fields = self.process_fields(*params)
131
+ end
132
+
133
+ ##
134
+ # Prepare a field configuration list into a normalized array of Hashes, {:name => "field_name"}
135
+ # @param {Mixed} params
136
+ # @return Array
137
+ #
138
+ def process_fields(*params)
139
+ params = [] if params.first.nil?
83
140
  options = params.extract_options!
141
+
142
+ # Return immediately if pre-processed fields are detected.
143
+ # ie: [ [{:name => 'foo'}, {:name => 'bar'}] ]
144
+ # This is to handle the case where extjs_record and to_record are called recursively, in which case
145
+ # these fields have already been processed.
146
+ #
147
+ if params.length === 1 && params.first.kind_of?(Array) && !params.first.empty?
148
+ return params.first
149
+ end
150
+
151
+ fields = []
84
152
  if !options.keys.empty?
85
153
  if excludes = options.delete(:exclude)
86
- self.extjs_record_fields = self.extjs_column_names.reject {|c| excludes.find {|ex| c === ex.to_s}}.collect {|c| c}
154
+ fields = self.process_fields(self.extjs_column_names.reject {|c| excludes.find {|ex| c === ex.to_s}}.collect {|c| c})
87
155
  elsif only = options.delete(:only)
88
- self.extjs_record_fields = only
156
+ fields = self.process_fields(only)
157
+ else
158
+ options.keys.each do |k| # <-- :email => {"sortDir" => "ASC"}
159
+ if options[k].is_a? Hash
160
+ options[k][:name] = k.to_s
161
+ fields << options[k]
162
+ elsif options[k].is_a? Array # <-- :parent => [:id, :name]
163
+ fields << {
164
+ :name => k.to_s,
165
+ :fields => process_fields(*options[k])
166
+ }
167
+ end
168
+ end
89
169
  end
90
- self.extjs_record_fields.concat(process_association_fields(options))
91
170
  elsif params.empty?
92
171
  return self.extjs_record_fields
93
172
  end
94
-
95
- self.extjs_record_fields.concat(params) if !params.empty?
96
-
97
- #Append primary key if it's not included
98
- # I don't think we want to automatically include :id
99
- # Chris
100
- #self.extjs_record_fields << self.primary_key.to_sym if !self.extjs_record_fields.include?(self.primary_key.to_sym)
173
+
174
+ unless params.empty?
175
+ fields.concat(params.collect {|f|
176
+ {:name => f.to_s}
177
+ })
178
+ end
179
+ fields
101
180
  end
102
181
 
103
182
  ##
104
- # Prepare the config for fields with '.' in their names
183
+ # Render a column-config object
184
+ # @param {Hash/Column} field Field-configuration Hash, probably has :name already set and possibly Ext.data.Field options.
185
+ # @param {ORM Column Object from AR, DM or MM}
186
+ #
187
+ def extjs_field(field, config=nil)
188
+ if config.kind_of? Hash
189
+ if mapping = config.delete(:mapping)
190
+ field.update( # <-- We use a template for rendering mapped field-names.
191
+ :name => mapping + self.extjs_mapping_template.gsub(/\{property\}/, field[:name]),
192
+ "mapping" => "#{mapping.to_s}.#{field[:name]}"
193
+ )
194
+ end
195
+ field.update(config) unless config.keys.empty?
196
+ elsif !config.nil? # <-- Hopfully an ORM Column object.
197
+ field.update(
198
+ "allowBlank" => self.extjs_allow_blank(config),
199
+ "type" => self.extjs_type(config)
200
+ )
201
+ field["dateFormat"] = "c" if field["type"] === :date && field["dateFormat"].nil? # <-- ugly hack for date
202
+ end
203
+ field.update("type" => "auto") if field["type"].nil?
204
+ field
205
+ end
206
+
207
+ ##
208
+ # Returns an array of symbolized association names that will be referenced by a call to to_record
209
+ # i.e. [:parent1, :parent2]
105
210
  #
106
- def process_association_fields(options)
107
- results = []
108
- options.each do |assoc, fields|
109
- fields.each do |field|
110
- results << [assoc, field]
211
+ def extjs_used_associations
212
+ if @extjs_used_associations.nil?
213
+ assoc = []
214
+ self.extjs_record_fields.each do |f|
215
+ #This needs to be the first condition because the others will break if f is an Array
216
+ if extjs_associations[f[:name]]
217
+ assoc << f[:name]
218
+ end
111
219
  end
220
+ @extjs_used_associations = assoc.uniq
112
221
  end
113
- results
222
+ @extjs_used_associations
114
223
  end
115
224
  end
116
225
  end
@@ -1,3 +1,6 @@
1
+ ##
2
+ # DataMapper adapter for ExtJS::Model mixin
3
+ #
1
4
  module ExtJS
2
5
  module Model
3
6
  module ClassMethods
@@ -20,8 +23,11 @@ module ExtJS
20
23
  @extjs_columns_hash
21
24
  end
22
25
 
23
- def extjs_render_column(col)
24
- pk = self.key.first
26
+ def extjs_allow_blank(col)
27
+ (col === self.key.first) ? true : col.nullable?
28
+ end
29
+
30
+ def extjs_type(col)
25
31
  type = ((col.type.respond_to?(:primitive)) ? col.type.primitive : col.type).to_s
26
32
  case type
27
33
  when "DateTime", "Date", "Time"
@@ -34,8 +40,7 @@ module ExtJS
34
40
  type = :int
35
41
  else
36
42
  type = "auto"
37
- end
38
- {:name => col.name.to_s, :type => type, :allowBlank => (col === pk) ? true : col.nullable? }
43
+ end
39
44
  end
40
45
 
41
46
  def extjs_associations
@@ -43,8 +48,12 @@ module ExtJS
43
48
  @extjs_associations = {}
44
49
  self.relationships.keys.each do |key|
45
50
  assn = self.relationships[key]
46
- type = (assn.options[:max].nil? && assn.options[:min].nil?) ? :belongs_to : (assn.options[:max] > 1) ? :many : nil
47
- @extjs_associations[key.to_sym] = {:name => key, :type => type}
51
+ @extjs_associations[key.to_sym] = {
52
+ :name => key,
53
+ :type => type = (assn.options[:max].nil? && assn.options[:min].nil?) ? :belongs_to : (assn.options[:max] > 1) ? :many : nil ,
54
+ :class => assn.parent_model,
55
+ :foreign_key => assn.child_key.first.name
56
+ }
48
57
  end
49
58
  end
50
59
  @extjs_associations
@@ -1,3 +1,6 @@
1
+ ##
2
+ # MongoMapper adapter to ExtJS::Model mixin
3
+ #
1
4
  module ExtJS
2
5
  module Model
3
6
  ##
@@ -6,7 +9,7 @@ module ExtJS
6
9
  module ClassMethods
7
10
 
8
11
  def extjs_primary_key
9
- "id"
12
+ :_id
10
13
  end
11
14
 
12
15
  def extjs_column_names
@@ -21,13 +24,18 @@ module ExtJS
21
24
  if @extjs_associations.nil?
22
25
  @extjs_associations = {}
23
26
  self.associations.keys.each do |key|
24
- @extjs_associations[key.to_sym] = {:name => key, :type => self.associations[key].type}
27
+ @extjs_associations[key.to_sym] = {
28
+ :name => key,
29
+ :type => self.associations[key].type,
30
+ :class => self.associations[key].class_name.constantize,
31
+ :foreign_key => self.associations[key].foreign_key
32
+ }
25
33
  end
26
34
  end
27
35
  @extjs_associations
28
36
  end
29
37
 
30
- def extjs_render_column(col)
38
+ def extjs_type(col)
31
39
  type = col.type.to_s
32
40
  case type
33
41
  when "DateTime", "Date", "Time"
@@ -40,8 +48,11 @@ module ExtJS
40
48
  type = :int
41
49
  else
42
50
  type = "auto"
43
- end
44
- {:name => col.name, :type => type, :allowBlank => (col.name === '_id') ? true : (col.options[:required] === true) ? false : true }
51
+ end
52
+ end
53
+
54
+ def extjs_allow_blank(col)
55
+ (col.name === '_id') ? true : (col.options[:required] === true) ? false : true
45
56
  end
46
57
  end
47
58
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: extjs-mvc
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.6
4
+ version: 0.2.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Scott
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-11-17 00:00:00 -05:00
12
+ date: 2009-11-19 00:00:00 -05:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency