hobo 0.6.1 → 0.6.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. data/bin/hobo +3 -2
  2. data/hobo_files/plugin/CHANGES.txt +299 -2
  3. data/hobo_files/plugin/Rakefile +12 -10
  4. data/hobo_files/plugin/generators/hobo/templates/guest.rb +1 -13
  5. data/hobo_files/plugin/generators/hobo_migration/hobo_migration_generator.rb +11 -7
  6. data/hobo_files/plugin/generators/hobo_rapid/hobo_rapid_generator.rb +1 -0
  7. data/hobo_files/plugin/generators/hobo_rapid/templates/hobo_rapid.js +1 -1
  8. data/hobo_files/plugin/generators/hobo_rapid/templates/lowpro.js +405 -0
  9. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/default/views/application.dryml +1 -1
  10. data/hobo_files/plugin/generators/hobo_user_model/templates/model.rb +1 -9
  11. data/hobo_files/plugin/init.rb +5 -0
  12. data/hobo_files/plugin/lib/active_record/has_many_association.rb +1 -1
  13. data/hobo_files/plugin/lib/extensions.rb +26 -5
  14. data/hobo_files/plugin/lib/extensions/test_case.rb +1 -1
  15. data/hobo_files/plugin/lib/hobo.rb +37 -11
  16. data/hobo_files/plugin/lib/hobo/authenticated_user.rb +7 -2
  17. data/hobo_files/plugin/lib/hobo/authentication_support.rb +7 -6
  18. data/hobo_files/plugin/lib/hobo/composite_model.rb +5 -0
  19. data/hobo_files/plugin/lib/hobo/controller.rb +4 -4
  20. data/hobo_files/plugin/lib/hobo/dryml.rb +5 -5
  21. data/hobo_files/plugin/lib/hobo/dryml/part_context.rb +3 -6
  22. data/hobo_files/plugin/lib/hobo/dryml/template.rb +16 -15
  23. data/hobo_files/plugin/lib/hobo/dryml/template_environment.rb +24 -20
  24. data/hobo_files/plugin/lib/hobo/email_address.rb +4 -0
  25. data/hobo_files/plugin/lib/hobo/field_spec.rb +2 -1
  26. data/hobo_files/plugin/lib/hobo/guest.rb +21 -0
  27. data/hobo_files/plugin/lib/hobo/hobo_helper.rb +42 -2
  28. data/hobo_files/plugin/lib/hobo/http_parameters.rb +225 -0
  29. data/hobo_files/plugin/lib/hobo/model.rb +55 -37
  30. data/hobo_files/plugin/lib/hobo/model_controller.rb +151 -151
  31. data/hobo_files/plugin/lib/hobo/model_queries.rb +30 -5
  32. data/hobo_files/plugin/lib/hobo/user_controller.rb +27 -16
  33. data/hobo_files/plugin/lib/hobo/where_fragment.rb +6 -1
  34. data/hobo_files/plugin/tags/rapid.dryml +88 -58
  35. data/hobo_files/plugin/tags/rapid_document_tags.dryml +5 -5
  36. data/hobo_files/plugin/tags/rapid_editing.dryml +3 -3
  37. data/hobo_files/plugin/tags/rapid_forms.dryml +35 -26
  38. data/hobo_files/plugin/tags/rapid_navigation.dryml +13 -12
  39. data/hobo_files/plugin/tags/rapid_pages.dryml +35 -31
  40. data/hobo_files/plugin/tags/rapid_plus.dryml +41 -0
  41. data/hobo_files/plugin/tags/rapid_support.dryml +18 -9
  42. data/hobo_files/plugin/tasks/dump_fixtures.rake +61 -0
  43. metadata +7 -11
  44. data/hobo_files/plugin/spec/fixtures/users.yml +0 -9
  45. data/hobo_files/plugin/spec/spec.opts +0 -6
  46. data/hobo_files/plugin/spec/spec_helper.rb +0 -28
  47. data/hobo_files/plugin/spec/unit/hobo/dryml/template_spec.rb +0 -650
@@ -194,11 +194,13 @@ module Hobo::Dryml
194
194
  def new_object_context(new_this)
195
195
  new_context do
196
196
  @_this_parent,@_this_field,@_this_type = if new_this.respond_to?(:proxy_reflection)
197
- refl = new_this.proxy_reflection
198
- [new_this.proxy_owner, refl.name, refl]
199
- else
200
- [nil, nil, new_this.class]
201
- end
197
+ refl = new_this.proxy_reflection
198
+ [new_this.proxy_owner, refl.name, refl]
199
+ else
200
+ # In dryml, TrueClass is the 'boolean' class
201
+ t = new_this.class == FalseClass ? TrueClass : new_this.class
202
+ [nil, nil, t]
203
+ end
202
204
  @_this = new_this
203
205
  yield
204
206
  end
@@ -207,27 +209,25 @@ module Hobo::Dryml
207
209
 
208
210
  def new_field_context(field_path, tag_this=nil)
209
211
  new_context do
210
- path = if field_path.is_a? Array
211
- field_path
212
- elsif field_path.is_a? String
213
- field_path.split('.')
214
- else
215
- [field_path]
216
- end
217
-
218
- obj = tag_this || this
219
- for field in path
220
- parent = obj
221
- obj = Hobo.get_field(parent, field)
222
- end
212
+ path = if field_path.is_a? Array
213
+ field_path
214
+ elsif field_path.is_a? String
215
+ field_path.split('.')
216
+ else
217
+ [field_path]
218
+ end
219
+ parent, field, obj = Hobo.get_field_path(tag_this || this, path)
223
220
 
224
221
  type = if (obj.nil? or obj.respond_to?(:proxy_reflection)) and
225
222
  parent.class.respond_to?(:field_type) and field_type = parent.class.field_type(field)
226
223
  field_type
224
+ elsif obj == false
225
+ # In dryml, TrueClass is the 'boolean' class
226
+ TrueClass
227
227
  else
228
228
  obj.class
229
229
  end
230
-
230
+
231
231
  @_this, @_this_parent, @_this_field, @_this_type = obj, parent, field, type
232
232
  @_form_field_path += path if @_form_field_path
233
233
  yield
@@ -409,7 +409,11 @@ module Hobo::Dryml
409
409
 
410
410
 
411
411
  def render_tag(tag_name, attributes)
412
- (send(tag_name, attributes) + part_contexts_storage_tag).strip
412
+ if respond_to?(tag_name)
413
+ (send(tag_name, attributes) + part_contexts_storage_tag).strip
414
+ else
415
+ false
416
+ end
413
417
  end
414
418
 
415
419
 
@@ -1,5 +1,9 @@
1
1
  class Hobo::EmailAddress < String
2
2
 
3
3
  COLUMN_TYPE = :string
4
+
5
+ def validate
6
+ "is not a valid email address" unless self.blank? || self =~ /^\s*([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\s*$/i
7
+ end
4
8
 
5
9
  end
@@ -23,7 +23,8 @@ module Hobo
23
23
  elsif options[:length]
24
24
  :string
25
25
  else
26
- Hobo.field_types[type]::COLUMN_TYPE or raise UnknownSqlTypeError, type
26
+ field_type = Hobo.field_types[type]
27
+ field_type && field_type::COLUMN_TYPE or raise UnknownSqlTypeError, [model, name, type]
27
28
  end
28
29
  end
29
30
  end
@@ -0,0 +1,21 @@
1
+ module Hobo
2
+
3
+ class Guest
4
+
5
+ alias_method :has_hobo_method?, :respond_to?
6
+
7
+ def to_s
8
+ "Guest"
9
+ end
10
+
11
+ def guest?
12
+ true
13
+ end
14
+
15
+ def super_user?
16
+ false
17
+ end
18
+
19
+ end
20
+
21
+ end
@@ -15,13 +15,13 @@ module Hobo
15
15
  @current_user = if session and id = session[:user]
16
16
  Hobo.object_from_dom_id(id)
17
17
  else
18
- Guest.new
18
+ ::Guest.new
19
19
  end
20
20
  end
21
21
 
22
22
 
23
23
  def logged_in?
24
- !(current_user.respond_to?(:guest?) && current_user.guest?)
24
+ !current_user.guest?
25
25
  end
26
26
 
27
27
 
@@ -307,6 +307,46 @@ module Hobo
307
307
  end
308
308
 
309
309
 
310
+ # Login url for a given user record or user class
311
+ def login_url(user_or_class)
312
+ c = user_or_class.is_a?(Class) ? user_or_class : user_or_class.class
313
+ send("#{c.name.underscore}_login_url") rescue nil
314
+ end
315
+
316
+
317
+ # Login url for a given user record or user class
318
+ def logout_url(user_or_class=nil)
319
+ c = if user_or_class.nil?
320
+ current_user.class
321
+ elsif user_or_class.is_a?(Class)
322
+ user_or_class
323
+ else
324
+ user_or_class.class
325
+ end
326
+ send("#{c.name.underscore}_logout_url") rescue nil
327
+ end
328
+
329
+
330
+ # Sign-up url for a given user record or user class
331
+ def signup_url(user_or_class)
332
+ c = user_or_class.is_a?(Class) ? user_or_class : user_or_class.class
333
+ send("#{c.name.underscore}_signup_url") rescue nil
334
+ end
335
+
336
+ def query_params
337
+ query = request.request_uri.match(/(?:\?(.+))/)._?[1]
338
+ if query
339
+ params = query.split('&')
340
+ pairs = params.map do |param|
341
+ pair = param.split('=')
342
+ pair.length == 1 ? pair + [''] : pair
343
+ end
344
+ Hash[*pairs.flatten]
345
+ else
346
+ {}
347
+ end
348
+ end
349
+
310
350
  # debugging support
311
351
 
312
352
  def abort_with(*args)
@@ -0,0 +1,225 @@
1
+ module Hobo
2
+
3
+ module HttpParameters
4
+
5
+ class PermissionDeniedError < RuntimeError; end
6
+ class InvalidError < RuntimeError; end
7
+
8
+ def initialize_record(record, params)
9
+ update_without_tracking(record, params)
10
+ record.set_creator(current_user)
11
+ (@to_create ||= []) << record
12
+ record
13
+ end
14
+
15
+
16
+ def update_record(record, params)
17
+ return if params.blank?
18
+
19
+ original = record.duplicate
20
+ # 'duplicate' can set these, but they can
21
+ # conflict with the changes so we clear them
22
+ @this.send(:clear_aggregation_cache)
23
+ @this.send(:clear_association_cache)
24
+
25
+ (@to_update ||= []) << [original, record]
26
+
27
+ update_without_tracking(record, params)
28
+ end
29
+
30
+
31
+ def update_without_tracking(record, params)
32
+ params && params.each_pair do |field_name, value|
33
+ field = if (create = field_name =~ /^\+/)
34
+ field_name[1..-1].to_sym
35
+ else
36
+ field_name.to_sym
37
+ end
38
+ refl = record.class.reflections[field]
39
+
40
+ if refl._?.macro == :belongs_to
41
+ if create
42
+ new_for_belongs_to(record, refl, value)
43
+ else
44
+ update_belongs_to(record, refl, value)
45
+ end
46
+
47
+ elsif Hobo.simple_has_many_association?(refl)
48
+ raise HoboError, "invalid HTTP parameter #{field_name}" if create
49
+ update_has_many(record, refl, value)
50
+
51
+ else
52
+ raise HoboError, "invalid HTTP parameter #{field_name}" if create
53
+ update_primitive(record, field, value)
54
+
55
+ end
56
+ end
57
+ end
58
+
59
+
60
+ def new_for_belongs_to(record, refl, fields)
61
+ # person[+home][address]=blah Create new home and set address (PUT POST)
62
+
63
+ target = refl.klass.new
64
+ initialize_record(target, fields)
65
+ record.send("#{refl.name}=", target)
66
+ end
67
+
68
+
69
+ def update_belongs_to(record, refl, value)
70
+ if value.is_a? String
71
+ # Update belongs_to to reference some existing record
72
+
73
+ target = if value.starts_with?('@')
74
+ # person[home]=@home_12 Reference different existing home (PUT POST)
75
+
76
+ Hobo.object_from_dom_id(value[1..-1])
77
+ elsif refl.klass.id_name?
78
+ # product[category]=garden Reference existing category with id or name (PUT POST)
79
+
80
+ refl.klass.find_by_id_name(value)
81
+ else
82
+ raise HoboError, "invalid HTTP parameter" if create
83
+ end
84
+ record.send("#{refl.name}=", target)
85
+
86
+ else
87
+ # Update state of current belongs_to target
88
+ # person[home][address]=blah Update existing home.address (PUT)
89
+ raise HoboError, "invalid HTTP parameter" unless params[:action] == "update"
90
+
91
+ target = record.send(refl.name)
92
+ raise HoboError, "invalid HTTP parameter" if target.nil?
93
+ update_record(target, value)
94
+ end
95
+ end
96
+
97
+
98
+ def update_has_many(record, refl, items)
99
+ new_items, changed_items = items.partition_hash {|k,v| k =~ /^\+/}
100
+
101
+ new_items.keys.sort_by{|k|k.to_i}.each do |k|
102
+ # home[people][+1][name]=blah Create new Person with fkey refing to this home and set name (PUT POST)
103
+ fields = new_items[k]
104
+ new_for_has_many(record, refl, fields)
105
+ end
106
+
107
+ changed_items.each_pair do |id, value|
108
+ # Change to existing record - only valid on PUTs
109
+ raise HoboError, "invalid HTTP parameter" unless params[:action] == "update"
110
+
111
+ target = id =~ /_/ ? Hobo.object_from_dom_id(id) : refl.klass.find(id)
112
+ # Ensure the target is actually in this has_many
113
+ raise HoboError, "invalid http parameter" unless target.send(refl.primary_key_name) == record.id
114
+
115
+ if value.is_a?(String) && value.downcase == "delete"
116
+ # home[people][45]=delete Delete Person[45] (PUT)
117
+ delete_record(target)
118
+
119
+ else
120
+ # home[people][45][name]=blah Update Person[45].name (PUT)
121
+ raise HoboError, "invalid http parameter" unless value.is_a?(Hash) # field/value pairs
122
+ update_record(target, value)
123
+
124
+ end
125
+ end
126
+ end
127
+
128
+
129
+ def new_for_has_many(record, refl, value)
130
+ # home[people][+1][name]=blah Create new Person with fkey refing to this home and set name (PUT POST)
131
+
132
+ new_record = record.send(refl.name).new
133
+ initialize_record(new_record, value)
134
+ record.send("#{refl.name}").target << new_record
135
+ end
136
+
137
+
138
+ def delete_record(record)
139
+ raise HoboError, "invalid HTTP parameter" unless params[:action] == "update"
140
+ (@to_delete ||= []) << record
141
+ end
142
+
143
+
144
+ def update_primitive(record, field, value)
145
+ # person[name]=fred (POST PUT)
146
+ field_type = record.class.field_type(field)
147
+ record.send("#{field}=", param_to_value(field_type, value))
148
+ end
149
+
150
+
151
+ def parse_datetime(s)
152
+ defined?(Chronic) ? Chronic.parse(s) : Time.parse(s)
153
+ end
154
+
155
+
156
+ def param_to_value(field_type, value)
157
+ if field_type.nil?
158
+ value
159
+ elsif field_type <= Date
160
+ if value.is_a? Hash
161
+ Date.new(*(%w{year month day}.map{|s| value[s].to_i}))
162
+ elsif value.is_a? String
163
+ dt = parse_datetime(value)
164
+ dt && dt.to_date
165
+ end
166
+ elsif field_type <= Time
167
+ if value.is_a? Hash
168
+ Time.local(*(%w{year month day hour minute}.map{|s| value[s].to_i}))
169
+ elsif value.is_a? String
170
+ parse_datetime(value)
171
+ end
172
+ elsif field_type <= TrueClass
173
+ (value.is_a?(String) && value.strip.downcase.in?(['0', 'false']) || value.blank?) ? false : true
174
+ else
175
+ # primitive field
176
+ value
177
+ end
178
+ end
179
+
180
+
181
+ def check_permissions_and_apply_changes
182
+ valid = true
183
+ for old, new in @to_update
184
+ raise PermissionDeniedError unless Hobo.can_update?(current_user, old, new)
185
+ new_valid = new.save
186
+ valid &&= new_valid
187
+ end if @to_update
188
+
189
+ for record in @to_create
190
+ raise PermissionDeniedError unless Hobo.can_create?(current_user, record)
191
+ # check if it's new because it might have already been saved as a result of the updates
192
+ record_valid = record.save if record.new_record?
193
+ valid &&= record_valid
194
+ end if @to_create
195
+
196
+ for record in @to_delete
197
+ raise PermissionDeniedError unless Hobo.can_delete?(current_user, record)
198
+ record.destroy
199
+ end if @to_delete
200
+
201
+ valid
202
+ ensure
203
+ @to_update = @to_create = @to_delete = nil
204
+ end
205
+
206
+
207
+ def secure_change_transaction
208
+ valid = nil
209
+ begin
210
+ ActiveRecord::Base.transaction do
211
+ yield
212
+ valid = check_permissions_and_apply_changes
213
+ raise InvalidError unless valid
214
+ end
215
+ rescue PermissionDeniedError
216
+ return :not_allowed
217
+ rescue InvalidError
218
+ return :invalid
219
+ end
220
+ :valid
221
+ end
222
+
223
+ end
224
+
225
+ end
@@ -28,6 +28,8 @@ module Hobo
28
28
  alias_method_chain :has_many, :defined_scopes
29
29
  alias_method_chain :belongs_to, :foreign_key_declaration
30
30
  end
31
+ # respond_to? is slow on AR objects, use this instead where possible
32
+ base.send(:alias_method, :has_hobo_method?, :respond_to_without_attributes?)
31
33
  end
32
34
 
33
35
  module ClassMethods
@@ -64,10 +66,13 @@ module Hobo
64
66
 
65
67
  def field(name, *args)
66
68
  type = args.shift
67
- options = extract_options_from_args!(args)
69
+ options = args.extract_options!
68
70
  @model.send(:set_field_type, name => type) unless
69
71
  type.in?(@model.connection.native_database_types.keys - [:text])
70
72
  @model.field_specs[name] = FieldSpec.new(@model, name, type, options)
73
+
74
+ @model.send(:validates_presence_of, name) if :required.in?(args)
75
+ @model.send(:validates_uniqueness_of, name) if :unique.in?(args)
71
76
  end
72
77
 
73
78
  def method_missing(name, *args)
@@ -102,6 +107,13 @@ module Hobo
102
107
  types.each_pair do |field, type|
103
108
  type_class = Hobo.field_types[type] || type
104
109
  field_types[field] = type_class
110
+
111
+ if "validate".in?(type_class.instance_methods)
112
+ self.validate do |record|
113
+ v = record.send(field).validate
114
+ record.errors.add(field, v) if v.is_a?(String)
115
+ end
116
+ end
105
117
  end
106
118
  end
107
119
 
@@ -129,15 +141,10 @@ module Hobo
129
141
  public :never_show?
130
142
 
131
143
  def set_creator_attr(attr)
132
- class_eval %{
133
- def creator
134
- #{attr};
135
- end
136
- def creator=(x)
137
- self.#{attr} = x;
138
- end
139
- }
144
+ @creator_attr = attr.to_sym
140
145
  end
146
+ attr_reader :creator_attr
147
+ public :creator_attr
141
148
 
142
149
  def set_search_columns(*columns)
143
150
  class_eval %{
@@ -225,27 +232,34 @@ module Hobo
225
232
  end
226
233
 
227
234
 
228
- def conditions(&b)
229
- ModelQueries.new(self).instance_eval(&b).to_sql
235
+ def conditions(*args, &b)
236
+ if args.empty?
237
+ ModelQueries.new(self).instance_eval(&b).to_sql
238
+ else
239
+ ModelQueries.new(self).instance_exec(*args, &b).to_sql
240
+ end
230
241
  end
231
-
242
+
232
243
 
233
244
  def find(*args, &b)
234
- if args.first.in?([:all, :first])
235
- if args.last.is_a? Hash
236
- options = args.last
237
- args[-1] = options = options.merge(:order => default_order) if options[:order] == :default
238
- else
239
- options = {}
240
- end
245
+ options = args.extract_options!
246
+ if args.first.in?([:all, :first]) && options[:order] == :default
247
+ options = if default_order.blank?
248
+ options - [:order]
249
+ else
250
+ options.merge(:order => "#{table_name}.#{default_order}")
251
+ end
252
+ end
241
253
 
242
- if b
243
- super(args.first, options.merge(:conditions => conditions(&b)))
244
- else
245
- super(*args)
246
- end
254
+ if b && !(block_conditions = conditions(&b)).blank?
255
+ c = if !options[:conditions].blank?
256
+ "(#{options[:conditons]}) and (#{block_conditions})"
257
+ else
258
+ block_conditions
259
+ end
260
+ super(args.first, options.merge(:conditions => c))
247
261
  else
248
- super(*args)
262
+ super(*args + [options])
249
263
  end
250
264
  end
251
265
 
@@ -271,8 +285,8 @@ module Hobo
271
285
  end
272
286
  end
273
287
 
274
- def has_creator?
275
- instance_methods.include?('creator=') and instance_methods.include?('creator')
288
+ def creator_type
289
+ reflections[@creator_attr]._?.klass
276
290
  end
277
291
 
278
292
  def search_columns
@@ -366,12 +380,13 @@ module Hobo
366
380
  if find_scope
367
381
  # Calling instance_variable_get directly causes self to
368
382
  # get loaded, hence this trick
369
- assoc = Kernel.instance_method(:instance_variable_get).bind(self).call("@#{name}")
383
+ assoc = Kernel.instance_method(:instance_variable_get).bind(self).call("@#{name}_scope")
370
384
 
371
385
  unless assoc
372
386
  options = proxy_reflection.options
373
387
  has_many_conditions = options.has_key?(:conditions)
374
- scope_conditions = find_scope.delete(:conditions)
388
+ source = proxy_reflection.source_reflection
389
+ scope_conditions = find_scope[:conditions]
375
390
  conditions = if has_many_conditions && scope_conditions
376
391
  "(#{scope_conditions}) AND (#{has_many_conditions})"
377
392
  else
@@ -379,23 +394,26 @@ module Hobo
379
394
  end
380
395
 
381
396
  options = options.merge(find_scope).update(:conditions => conditions,
382
- :class_name => proxy_reflection.klass.name,
383
- :foreign_key => proxy_reflection.primary_key_name)
397
+ :class_name => proxy_reflection.klass.name,
398
+ :foreign_key => proxy_reflection.primary_key_name)
399
+ options[:source] = source.name if source
400
+
384
401
  r = ActiveRecord::Reflection::AssociationReflection.new(:has_many,
385
402
  name,
386
403
  options,
387
- proxy_reflection.klass)
404
+ proxy_owner.class)
405
+
388
406
  @reflections ||= {}
389
407
  @reflections[name] = r
390
408
 
391
- assoc = if options.has_key?(:through)
409
+ assoc = if source
392
410
  ActiveRecord::Associations::HasManyThroughAssociation
393
411
  else
394
412
  ActiveRecord::Associations::HasManyAssociation
395
413
  end.new(self.proxy_owner, r)
396
-
414
+
397
415
  # Calling directly causes self to get loaded
398
- Kernel.instance_method(:instance_variable_set).bind(self).call("@#{name}", assoc)
416
+ Kernel.instance_method(:instance_variable_set).bind(self).call("@#{name}_scope", assoc)
399
417
  end
400
418
  assoc
401
419
  else
@@ -407,7 +425,7 @@ module Hobo
407
425
 
408
426
 
409
427
  def has_many_with_defined_scopes(name, *args, &block)
410
- options = extract_options_from_args!(args)
428
+ options = args.extract_options!
411
429
  if options.has_key?(:extend) || block
412
430
  # Normal has_many
413
431
  has_many_without_defined_scopes(name, *args + [options], &block)
@@ -420,7 +438,7 @@ module Hobo
420
438
 
421
439
 
422
440
  def set_creator(user)
423
- self.creator ||= user if self.class.has_creator? and not user.guest?
441
+ self.send("#{self.class.creator_attr}=", user) if (t = self.class.creator_type) && user.is_a?(t)
424
442
  end
425
443
 
426
444