hobo 0.6.1 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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