hobo 0.7.2 → 0.7.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (77) hide show
  1. data/bin/hobo +24 -7
  2. data/hobo_files/plugin/CHANGES.txt +501 -0
  3. data/hobo_files/plugin/generators/hobo/hobo_generator.rb +8 -6
  4. data/hobo_files/plugin/generators/hobo/templates/application.dryml +3 -0
  5. data/hobo_files/plugin/generators/hobo/templates/dryml-support.js +132 -0
  6. data/hobo_files/plugin/generators/hobo_front_controller/hobo_front_controller_generator.rb +4 -5
  7. data/hobo_files/plugin/generators/hobo_model_resource/hobo_model_resource_generator.rb +75 -0
  8. data/hobo_files/plugin/generators/hobo_model_resource/templates/controller.rb +7 -0
  9. data/hobo_files/plugin/generators/hobo_model_resource/templates/functional_test.rb +8 -0
  10. data/hobo_files/plugin/generators/hobo_model_resource/templates/helper.rb +2 -0
  11. data/hobo_files/plugin/generators/hobo_rapid/templates/hobo-rapid.js +30 -11
  12. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/clean/public/stylesheets/application.css +149 -92
  13. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/clean/public/stylesheets/rapid-ui.css +0 -48
  14. data/hobo_files/plugin/init.rb +45 -13
  15. data/hobo_files/plugin/lib/action_view_extensions/base.rb +4 -3
  16. data/hobo_files/plugin/lib/active_record/association_proxy.rb +18 -0
  17. data/hobo_files/plugin/lib/active_record/association_reflection.rb +5 -0
  18. data/hobo_files/plugin/lib/active_record/has_many_association.rb +7 -11
  19. data/hobo_files/plugin/lib/active_record/has_many_through_association.rb +8 -0
  20. data/hobo_files/plugin/lib/extensions/test_case.rb +1 -1
  21. data/hobo_files/plugin/lib/hobo.rb +38 -60
  22. data/hobo_files/plugin/lib/hobo/authentication_support.rb +1 -1
  23. data/hobo_files/plugin/lib/hobo/bundle.rb +131 -34
  24. data/hobo_files/plugin/lib/hobo/composite_model.rb +1 -1
  25. data/hobo_files/plugin/lib/hobo/controller.rb +7 -8
  26. data/hobo_files/plugin/lib/hobo/dev_controller.rb +21 -0
  27. data/hobo_files/plugin/lib/hobo/dryml/dryml_builder.rb +14 -8
  28. data/hobo_files/plugin/lib/hobo/dryml/dryml_support_controller.rb +13 -0
  29. data/hobo_files/plugin/lib/hobo/dryml/taglib.rb +6 -7
  30. data/hobo_files/plugin/lib/hobo/dryml/template.rb +207 -73
  31. data/hobo_files/plugin/lib/hobo/dryml/template_environment.rb +67 -55
  32. data/hobo_files/plugin/lib/hobo/dryml/template_handler.rb +53 -3
  33. data/hobo_files/plugin/lib/hobo/hobo_helper.rb +75 -107
  34. data/hobo_files/plugin/lib/hobo/model.rb +236 -429
  35. data/hobo_files/plugin/lib/hobo/model_controller.rb +277 -437
  36. data/hobo_files/plugin/lib/hobo/model_router.rb +62 -29
  37. data/hobo_files/plugin/lib/hobo/rapid_helper.rb +48 -9
  38. data/hobo_files/plugin/lib/hobo/scopes.rb +98 -0
  39. data/hobo_files/plugin/lib/hobo/scopes/association_proxy_extensions.rb +31 -0
  40. data/hobo_files/plugin/lib/hobo/scopes/automatic_scopes.rb +282 -0
  41. data/hobo_files/plugin/lib/hobo/scopes/defined_scope_proxy_extender.rb +88 -0
  42. data/hobo_files/plugin/lib/hobo/scopes/scope_reflection.rb +18 -0
  43. data/hobo_files/plugin/lib/hobo/scopes/scoped_proxy.rb +59 -0
  44. data/hobo_files/plugin/lib/hobo/undefined.rb +2 -0
  45. data/hobo_files/plugin/lib/hobo/user.rb +31 -14
  46. data/hobo_files/plugin/lib/hobo/user_controller.rb +41 -27
  47. data/hobo_files/plugin/taglibs/core.dryml +9 -11
  48. data/hobo_files/plugin/taglibs/rapid.dryml +51 -108
  49. data/hobo_files/plugin/taglibs/rapid_editing.dryml +25 -25
  50. data/hobo_files/plugin/taglibs/rapid_forms.dryml +111 -79
  51. data/hobo_files/plugin/taglibs/rapid_generics.dryml +74 -0
  52. data/hobo_files/plugin/taglibs/rapid_navigation.dryml +23 -21
  53. data/hobo_files/plugin/taglibs/rapid_pages.dryml +83 -169
  54. data/hobo_files/plugin/taglibs/rapid_plus.dryml +16 -2
  55. data/hobo_files/plugin/taglibs/rapid_support.dryml +3 -3
  56. data/hobo_files/plugin/taglibs/rapid_user_pages.dryml +104 -0
  57. metadata +60 -55
  58. data/hobo_files/plugin/generators/hobo_migration/hobo_migration_generator.rb +0 -276
  59. data/hobo_files/plugin/generators/hobo_migration/templates/migration.rb +0 -9
  60. data/hobo_files/plugin/lib/active_record/table_definition.rb +0 -34
  61. data/hobo_files/plugin/lib/extensions.rb +0 -375
  62. data/hobo_files/plugin/lib/hobo/email_address.rb +0 -12
  63. data/hobo_files/plugin/lib/hobo/enum_string.rb +0 -50
  64. data/hobo_files/plugin/lib/hobo/field_declaration_dsl.rb +0 -43
  65. data/hobo_files/plugin/lib/hobo/field_spec.rb +0 -68
  66. data/hobo_files/plugin/lib/hobo/html_string.rb +0 -7
  67. data/hobo_files/plugin/lib/hobo/lazy_hash.rb +0 -40
  68. data/hobo_files/plugin/lib/hobo/markdown_string.rb +0 -11
  69. data/hobo_files/plugin/lib/hobo/migrations.rb +0 -12
  70. data/hobo_files/plugin/lib/hobo/model_queries.rb +0 -117
  71. data/hobo_files/plugin/lib/hobo/password_string.rb +0 -7
  72. data/hobo_files/plugin/lib/hobo/percentage.rb +0 -14
  73. data/hobo_files/plugin/lib/hobo/predicate_dispatch.rb +0 -78
  74. data/hobo_files/plugin/lib/hobo/proc_binding.rb +0 -32
  75. data/hobo_files/plugin/lib/hobo/text.rb +0 -3
  76. data/hobo_files/plugin/lib/hobo/textile_string.rb +0 -25
  77. data/hobo_files/plugin/lib/hobo/where_fragment.rb +0 -28
@@ -2,33 +2,34 @@ module Hobo
2
2
 
3
3
  module Model
4
4
 
5
+ class PermissionDeniedError < RuntimeError; end
6
+ class NoNameError < RuntimeError; end
7
+
5
8
  NAME_FIELD_GUESS = %w(name title)
6
9
  PRIMARY_CONTENT_GUESS = %w(description body content profile)
7
10
  SEARCH_COLUMNS_GUESS = %w(name title body content profile)
8
11
 
9
- PLAIN_TYPES = { :boolean => TrueClass,
10
- :date => Date,
11
- :datetime => Time,
12
- :integer => Fixnum,
13
- :big_integer => BigDecimal,
14
- :float => Float,
15
- :string => String
16
- }
17
-
18
- Hobo.field_types.update(PLAIN_TYPES)
19
12
 
20
13
  def self.included(base)
21
- Hobo.register_model(base)
22
14
  base.extend(ClassMethods)
15
+
16
+ included_in_class_callbacks(base)
17
+
18
+ Hobo.register_model(base)
19
+
20
+ patch_will_paginate
21
+
23
22
  base.class_eval do
23
+ inheriting_cattr_reader :default_order
24
24
  alias_method_chain :attributes=, :hobo_type_conversion
25
- default_scopes
26
25
  end
26
+
27
27
  class << base
28
- alias_method_chain :has_many, :defined_scopes
29
- alias_method_chain :belongs_to, :foreign_key_declaration
30
- alias_method_chain :belongs_to, :hobo_metadata
31
- alias_method_chain :acts_as_list, :fields if defined?(ActiveRecord::Acts::List)
28
+ alias_method_chain :has_many, :defined_scopes
29
+ alias_method_chain :belongs_to, :creator_metadata
30
+
31
+ alias_method_chain :has_one, :new_method
32
+
32
33
  def inherited(klass)
33
34
  fields do
34
35
  Hobo.register_model(klass)
@@ -36,126 +37,136 @@ module Hobo
36
37
  end
37
38
  end
38
39
  end
40
+
41
+ end
42
+
43
+ def self.patch_will_paginate
44
+ if defined?(WillPaginate) && !WillPaginate::Collection.respond_to?(:member_class)
45
+
46
+ WillPaginate::Collection.class_eval do
47
+ attr_accessor :member_class, :origin, :origin_attribute
48
+ end
49
+
50
+ WillPaginate::Finder::ClassMethods.class_eval do
51
+ def paginate_with_hobo_metadata(*args, &block)
52
+ returning paginate_without_hobo_metadata(*args, &block) do |collection|
53
+ collection.member_class = self
54
+ collection.origin = try.proxy_owner
55
+ collection.origin_attribute = try.proxy_reflection._?.name
56
+ end
57
+ end
58
+ alias_method_chain :paginate, :hobo_metadata
59
+
60
+ end
61
+
62
+ end
39
63
  end
40
64
 
65
+
41
66
  module ClassMethods
42
67
 
43
68
  # include methods also shared by CompositeModel
44
- include ModelSupport::ClassMethods
69
+ #include ModelSupport::ClassMethods
45
70
 
46
71
  attr_accessor :creator_attribute
47
72
  attr_writer :name_attribute, :primary_content_attribute
48
73
 
49
- def default_scopes
50
- def_scope :recent do |*args|
51
- count = args.first || 3
52
- { :limit => count, :order => "#{table_name}.created_at DESC" }
53
- end
74
+ def named(*args)
75
+ raise NoNameError, "Model #{name} has no name attribute" unless name_attribute
76
+ send("find_by_#{name_attribute}", *args)
77
+ end
78
+
79
+ alias_method :[], :named
80
+
81
+
82
+ def field_added(name, type, args, options)
83
+ self.name_attribute = name.to_sym if options.delete(:name)
84
+ self.primary_content_attribute = name.to_sym if options.delete(:primary_content)
85
+ self.creator_attribute = name.to_sym if options.delete(:creator)
86
+ validate = options.delete(:validate) {true}
87
+
88
+ #FIXME - this should be in Hobo::User
89
+ send(:login_attribute=, name.to_sym, validate) if options.delete(:login) && respond_to?(:login_attribute=)
90
+ end
91
+
92
+
93
+ def user_find(user, *args)
94
+ record = find(*args)
95
+ raise PermissionDeniedError unless Hobo.can_view?(user, self)
96
+ record
54
97
  end
55
98
 
99
+
100
+ def user_new(user, attributes={})
101
+ record = new(attributes)
102
+ record.user_changes(user) and record
103
+ end
104
+
105
+
106
+ def user_new!(user, attributes={})
107
+ user_new(user, attributes) or raise PermissionDeniedError
108
+ end
109
+
110
+
111
+ def user_create(user, attributes={})
112
+ record = new(attributes)
113
+ record.user_save_changes(user)
114
+ record
115
+ end
116
+
117
+
118
+ def user_can_create?(user, attributes={})
119
+ record = new(attributes)
120
+ record.user_changes(user)
121
+ end
122
+
123
+
56
124
  def name_attribute
57
125
  @name_attribute ||= begin
58
- cols = columns.every :name
59
- NAME_FIELD_GUESS.detect {|f| f.in? columns.every(:name) }
126
+ cols = columns.*.name
127
+ NAME_FIELD_GUESS.detect {|f| f.in? columns.*.name }
60
128
  end
61
129
  end
62
130
 
63
131
 
64
132
  def primary_content_attribute
65
- @description_attribute ||= begin
66
- cols = columns.every :name
67
- PRIMARY_CONTENT_GUESS.detect {|f| f.in? columns.every(:name) }
68
- end
133
+ @primary_content_attribute ||= begin
134
+ cols = columns.*.name
135
+ PRIMARY_CONTENT_GUESS.detect {|f| f.in? columns.*.name }
136
+ end
69
137
  end
70
138
 
71
139
  def dependent_collections
72
140
  reflections.values.select do |refl|
73
141
  refl.macro == :has_many && refl.options[:dependent]
74
- end.every(:name)
142
+ end.*.name
75
143
  end
76
144
 
77
145
 
78
146
  def dependent_on
79
147
  reflections.values.select do |refl|
80
148
  refl.macro == :belongs_to && (rev = reverse_reflection(refl.name) and rev.options[:dependent])
81
- end.every(:name)
82
- end
83
-
84
- private
85
-
86
- def return_type(type)
87
- @next_method_type = type
88
- end
89
-
90
- def method_added(name)
91
- if @next_method_type
92
- set_field_type(name => @next_method_type)
93
- @next_method_type = nil
94
- end
149
+ end.*.name
95
150
  end
96
151
 
97
152
 
98
- def fields(&b)
99
- dsl = FieldDeclarationsDsl.new(self)
100
- if b.arity == 1
101
- yield dsl
102
- else
103
- dsl.instance_eval(&b)
104
- end
153
+ def default_dependent_on
154
+ dependent_on.first
105
155
  end
106
156
 
107
157
 
108
- def belongs_to_with_foreign_key_declaration(name, *args, &block)
109
- options = args.extract_options!
110
- res = belongs_to_without_foreign_key_declaration(name, *args + [options], &block)
111
- refl = reflections[name]
112
- fkey = refl.primary_key_name
113
- column_options = {}
114
- column_options[:null] = options[:null] if options.has_key?(:null)
115
- field_specs[fkey] ||= FieldSpec.new(self, fkey, :integer, column_options)
116
- if refl.options[:polymorphic]
117
- type_col = "#{name}_type"
118
- field_specs[type_col] ||= FieldSpec.new(self, type_col, :string, column_options)
119
- end
120
- res
121
- end
122
-
158
+ private
123
159
 
124
- def belongs_to_with_hobo_metadata(name, *args, &block)
125
- options = args.extract_options!
160
+
161
+ def belongs_to_with_creator_metadata(name, options={}, &block)
126
162
  self.creator_attribute = name.to_sym if options.delete(:creator)
127
- belongs_to_without_hobo_metadata(name, *args + [options], &block)
163
+ belongs_to_without_creator_metadata(name, options, &block)
128
164
  end
129
-
130
-
131
- def acts_as_list_with_fields(options = {})
132
- fields { |f| f.send(options._?[:column] || "position", :integer) }
133
- acts_as_list_without_fields(options)
134
- end
135
-
136
-
137
- def field_specs
138
- @field_specs ||= HashWithIndifferentAccess.new
139
- end
140
- public :field_specs
141
165
 
142
- def set_field_type(types)
143
- types.each_pair do |field, type|
144
- type_class = Hobo.field_types[type] || type
145
- field_types[field] = type_class
146
-
147
- if type_class && "validate".in?(type_class.instance_methods)
148
- self.validate do |record|
149
- v = record.send(field)._?.validate
150
- record.errors.add(field, v) if v.is_a?(String)
151
- end
152
- end
153
- end
154
- end
155
-
156
-
157
- def field_types
158
- @hobo_field_types ||= superclass.respond_to?(:field_types) ? superclass.field_types : {}
166
+
167
+ def has_one_with_new_method(name, options={}, &block)
168
+ has_one_without_new_method(name, options)
169
+ class_eval "def new_#{name}(attributes={}); build_#{name}(attributes, false); end"
159
170
  end
160
171
 
161
172
 
@@ -163,178 +174,62 @@ module Hobo
163
174
  @default_order = order
164
175
  end
165
176
 
166
- inheriting_attr_accessor :default_order, :id_name_options
167
-
168
177
 
169
178
  def never_show(*fields)
170
179
  @hobo_never_show ||= []
171
- @hobo_never_show.concat(fields.every(:to_sym))
172
- end
173
-
174
- def never_show?(field)
175
- (@hobo_never_show && field.to_sym.in?(@hobo_never_show)) || (superclass < Hobo::Model && superclass.never_show?(field))
180
+ @hobo_never_show.concat(fields.*.to_sym)
176
181
  end
177
- public :never_show?
178
182
 
183
+
179
184
  def set_search_columns(*columns)
180
185
  class_eval %{
181
186
  def self.search_columns
182
- %w{#{columns.every(:to_s) * ' '}}
187
+ %w{#{columns.*.to_s * ' '}}
183
188
  end
184
189
  }
185
190
  end
186
191
 
187
-
188
- def id_name(*args)
189
- @id_name_options = [] + args
190
-
191
- underscore = args.delete(:underscore)
192
- insenstive = args.delete(:case_insensitive)
193
- id_name_field = args.first || :name
194
- @id_name_column = id_name_field.to_s
195
-
196
- if underscore
197
- class_eval %{
198
- def id_name(underscore=false)
199
- underscore ? #{id_name_field}.gsub(' ', '_') : #{id_name_field}
200
- end
201
- }
202
- else
203
- class_eval %{
204
- def id_name(underscore=false)
205
- #{id_name_field}
206
- end
207
- }
208
- end
209
-
210
- key = "id_name#{if underscore; ".gsub('_', ' ')"; end}"
211
- finder = if insenstive
212
- "find(:first, options.merge(:conditions => ['lower(#{id_name_field}) = ?', #{key}.downcase]))"
213
- else
214
- "find_by_#{id_name_field}(#{key}, options)"
215
- end
216
-
217
- class_eval %{
218
- def self.find_by_id_name(id_name, options={})
219
- #{finder}
220
- end
221
- }
222
-
223
- model = self
224
- validate do
225
- erros.add id_name_field, "is taken" if model.find_by_id_name(name)
226
- end
227
- validates_format_of id_name_field, :with => /^[^_]+$/, :message => "cannot contain underscores" if
228
- underscore
229
- end
230
-
231
- public
232
192
 
233
- def id_name?
234
- respond_to?(:find_by_id_name)
235
- end
236
-
237
- attr_reader :id_name_column
193
+ public
238
194
 
239
195
 
240
- def field_type(name)
241
- name = name.to_sym
242
- field_types[name] or
243
- reflections[name] or begin
244
- col = column(name)
245
- return nil if col.nil?
246
- case col.type
247
- when :boolean
248
- TrueClass
249
- when :text
250
- Hobo::Text
251
- else
252
- col.klass
253
- end
254
- end
255
- end
256
-
257
-
258
- def column(name)
259
- columns.find {|c| c.name == name.to_s} rescue nil
260
- end
261
-
262
-
263
- def conditions(*args, &b)
264
- if args.empty?
265
- ModelQueries.new(self).instance_eval(&b)._?.to_sql
266
- else
267
- ModelQueries.new(self).instance_exec(*args, &b)._?.to_sql
268
- end
196
+ def never_show?(field)
197
+ (@hobo_never_show && field.to_sym.in?(@hobo_never_show)) || (superclass < Hobo::Model && superclass.never_show?(field))
269
198
  end
270
199
 
271
200
 
272
201
  def find(*args, &b)
273
202
  options = args.extract_options!
274
- if args.first.in?([:all, :first]) && options[:order] == :default
203
+ if options[:order] == :default
275
204
  options = if default_order.blank?
276
- options - [:order]
205
+ options.except :order
277
206
  else
278
207
  options.merge(:order => "#{table_name}.#{default_order}")
279
208
  end
280
209
  end
281
-
282
- res = if b && !(block_conditions = conditions(&b)).blank?
283
- c = if !options[:conditions].blank?
284
- "(#{sanitize_sql options[:conditions]}) AND (#{sanitize_sql block_conditions})"
285
- else
286
- block_conditions
287
- end
288
- super(args.first, options.merge(:conditions => c))
289
- else
290
- super(*args + [options])
291
- end
292
- if args.first == :all
293
- def res.member_class
294
- @member_class
295
- end
296
- res.instance_variable_set("@member_class", self)
297
- end
298
- res
210
+ result = super(*args + [options])
211
+ result.member_class = self if result.is_a?(Array)
212
+ result
299
213
  end
300
-
214
+
301
215
 
302
216
  def all(options={})
303
217
  find(:all, options.reverse_merge(:order => :default))
304
218
  end
305
219
 
306
220
 
307
- def count(*args, &b)
308
- if b
309
- sql = ModelQueries.new(self).instance_eval(&b).to_sql
310
- options = extract_options_from_args!(args)
311
- super(*args + [options.merge(:conditions => sql)])
312
- else
313
- super(*args)
314
- end
315
- end
316
-
317
-
318
- def subclass_associations(association, *subclass_associations)
319
- refl = reflections[association]
320
- for assoc in subclass_associations
321
- class_name = assoc.to_s.classify
322
- options = { :class_name => class_name, :conditions => "type = '#{class_name}'" }
323
- options[:source] = refl.source_reflection.name if refl.source_reflection
324
- has_many(assoc, refl.options.merge(options))
325
- end
326
- end
327
-
328
221
  def creator_type
329
222
  reflections[creator_attribute]._?.klass
330
223
  end
331
224
 
225
+
332
226
  def search_columns
333
- cols = columns.every(:name)
227
+ cols = columns.*.name
334
228
  SEARCH_COLUMNS_GUESS.select{|c| c.in?(cols) }
335
229
  end
336
230
 
337
- # This should really be a method on AssociationReflection
231
+
232
+ # FIXME: This should really be a method on AssociationReflection
338
233
  def reverse_reflection(association_name)
339
234
  refl = reflections[association_name]
340
235
  return nil if refl.options[:conditions]
@@ -353,153 +248,94 @@ module Hobo
353
248
  end
354
249
 
355
250
 
356
- class ScopedProxy
357
-
358
- def initialize(klass, scope)
359
- @klass = klass
360
-
361
- # If there's no :find, or :create specified, assume it's a find scope
362
- @scope = if scope.has_key?(:find) || scope.has_key?(:create)
363
- scope
364
- else
365
- { :find => scope }
366
- end
367
- end
368
-
369
-
370
- def method_missing(name, *args, &block)
371
- if name.to_sym.in?(@klass.defined_scopes.keys)
372
- proxy = @klass.send(name, *args)
373
- proxy.instance_variable_set("@parent_scope", self)
374
- proxy
375
- else
376
- _apply_scope { @klass.send(name, *args, &block) }
377
- end
378
- end
379
-
380
- def all
381
- self.find(:all)
382
- end
383
-
384
- def first
385
- self.find(:first)
386
- end
387
-
388
- def member_class
389
- @klass
390
- end
391
-
392
- private
393
- def _apply_scope
394
- if @parent_scope
395
- @parent_scope.send(:_apply_scope) do
396
- @scope ? @klass.send(:with_scope, @scope) { yield } : yield
397
- end
398
- else
399
- @scope ? @klass.send(:with_scope, @scope) { yield } : yield
400
- end
401
- end
402
-
403
- end
404
- (Object.instance_methods +
405
- Object.private_instance_methods +
406
- Object.protected_instance_methods).each do |m|
407
- ScopedProxy.send(:undef_method, m) unless
408
- m.in?(%w{initialize method_missing send instance_variable_set instance_variable_get puts}) || m.starts_with?('_')
251
+ def has_inheritance_column?
252
+ columns_hash.include?(inheritance_column)
409
253
  end
410
254
 
411
- attr_accessor :defined_scopes
412
255
 
413
-
414
- def def_scope(name, scope=nil, &block)
415
- @defined_scopes ||= {}
416
- @defined_scopes[name.to_sym] = block || scope
417
-
418
- meta_def(name) do |*args|
419
- ScopedProxy.new(self, block ? block.call(*args) : scope)
256
+ def method_missing(name, *args, &block)
257
+ name = name.to_s
258
+ if name =~ /\./
259
+ # FIXME: Do we need this now?
260
+ call_method_chain(name, args, &block)
261
+ elsif create_automatic_scope(name)
262
+ send(name, *args, &block)
263
+ else
264
+ super(name.to_sym, *args, &block)
420
265
  end
421
266
  end
267
+
268
+
269
+ def call_method_chain(chain, args, &block)
270
+ parts = chain.split(".")
271
+ s = parts[0..-2].inject(self) { |m, scope| m.send(scope) }
272
+ s.send(parts.last, *args)
273
+ end
422
274
 
423
275
 
424
- module DefinedScopeProxyExtender
276
+ def to_url_path
277
+ "#{name.underscore.pluralize}"
278
+ end
279
+
280
+ def typed_id
281
+ HoboFields.to_name(self) || name.underscore.gsub("/", "__")
282
+ end
283
+
284
+ end # --- of ClassMethods --- #
285
+
286
+
287
+ include Scopes
288
+
289
+
290
+ def to_url_path
291
+ "#{self.class.to_url_path}/#{to_param}" unless new_record?
292
+ end
293
+
294
+
295
+ def user_changes(user, changes={})
296
+ if new_record?
297
+ self.attributes = changes
298
+ set_creator(user)
299
+ Hobo.can_create?(user, self)
300
+ else
301
+ original = duplicate
302
+ # 'duplicate' can cause these to be set, but they can conflict
303
+ # with the changes so we clear them
304
+ clear_aggregation_cache
305
+ clear_association_cache
425
306
 
426
- attr_accessor :reflections
307
+ self.attributes = changes
427
308
 
428
- def method_missing(name, *args, &block)
429
- scope = (proxy_reflection.klass.respond_to?(:defined_scopes) and
430
- scopes = proxy_reflection.klass.defined_scopes and
431
- scopes[name.to_sym])
432
-
433
- scope = scope.call(*args) if scope.is_a?(Proc)
434
-
435
- # If there's no :find, or :create specified, assume it's a find scope
436
- find_scope = if scope && (scope.has_key?(:find) || scope.has_key?(:create))
437
- scope[:find]
438
- else
439
- scope
440
- end
441
-
442
- if find_scope
443
- scope_name = "@#{name.to_s.gsub('?','')}_scope"
444
-
445
- # Calling instance_variable_get directly causes self to
446
- # get loaded, hence this trick
447
- assoc = Kernel.instance_method(:instance_variable_get).bind(self).call(scope_name)
448
-
449
- unless assoc
450
- options = proxy_reflection.options
451
- has_many_conditions = options[:conditions]
452
- has_many_conditions = nil if has_many_conditions.blank?
453
- source = proxy_reflection.source_reflection
454
- scope_conditions = find_scope[:conditions]
455
- scope_conditions = nil if scope_conditions.blank?
456
- conditions = if has_many_conditions && scope_conditions
457
- "(#{sanitize_sql scope_conditions}) AND (#{sanitize_sql has_many_conditions})"
458
- else
459
- scope_conditions || has_many_conditions
460
- end
461
-
462
- options = options.merge(find_scope).update(:class_name => proxy_reflection.klass.name,
463
- :foreign_key => proxy_reflection.primary_key_name)
464
- options[:conditions] = conditions unless conditions.blank?
465
- options[:source] = source.name if source
309
+ Hobo.can_update?(user, original, self)
310
+ end
311
+ end
312
+
313
+
314
+ def user_changes!(user, changes={})
315
+ user_changes(user, changes) or raise PermissionDeniedError
316
+ end
317
+
318
+
319
+ def user_can_create?(user, attributes={})
320
+ raise ArgumentError, "Called #user_can_create? on existing record" unless new_record?
321
+ user_changes(user, attributes)
322
+ end
323
+
466
324
 
467
- r = ActiveRecord::Reflection::AssociationReflection.new(:has_many,
468
- name,
469
- options,
470
- proxy_owner.class)
471
-
472
- @reflections ||= {}
473
- @reflections[name] = r
474
-
475
- assoc = if source
476
- ActiveRecord::Associations::HasManyThroughAssociation
477
- else
478
- ActiveRecord::Associations::HasManyAssociation
479
- end.new(self.proxy_owner, r)
325
+ def user_save_changes(user, changes={})
326
+ user_changes!(user, changes)
327
+ save
328
+ end
329
+
480
330
 
481
- # Calling directly causes self to get loaded
482
- Kernel.instance_method(:instance_variable_set).bind(self).call(scope_name, assoc)
483
- end
484
- assoc
485
- else
486
- super
487
- end
488
- end
489
-
490
- end
491
-
492
-
493
- def has_many_with_defined_scopes(name, *args, &block)
494
- options = args.extract_options!
495
- if options.has_key?(:extend) || block
496
- # Normal has_many
497
- has_many_without_defined_scopes(name, *args + [options], &block)
498
- else
499
- options[:extend] = DefinedScopeProxyExtender
500
- has_many_without_defined_scopes(name, *args + [options], &block)
501
- end
502
- end
331
+ def user_view(user, field=nil)
332
+ raise PermissionDeniedError unless Hobo.can_view?(user, self, field)
333
+ end
334
+
335
+
336
+ def user_destroy(user)
337
+ raise PermissionDeniedError unless Hobo.can_delete?(user, self)
338
+ destroy
503
339
  end
504
340
 
505
341
 
@@ -509,13 +345,18 @@ module Hobo
509
345
 
510
346
 
511
347
  def attributes_with_hobo_type_conversion=(attributes, guard_protected_attributes=true)
512
- converted = attributes.map_hash { |k, v| convert_type_for_mass_assignment(self.class.field_type(k), v) }
348
+ converted = attributes.map_hash { |k, v| convert_type_for_mass_assignment(self.class.attr_type(k), v) }
513
349
  send(:attributes_without_hobo_type_conversion=, converted, guard_protected_attributes)
514
350
  end
515
351
 
516
352
 
517
353
 
518
354
  def set_creator(user)
355
+ set_creator!(user) unless get_creator
356
+ end
357
+
358
+
359
+ def set_creator!(user)
519
360
  attr = self.class.creator_attribute
520
361
  return unless attr
521
362
 
@@ -528,22 +369,30 @@ module Hobo
528
369
  self.send("#{attr}=", user.to_s) unless user.guest?
529
370
  end
530
371
  end
372
+
373
+
374
+ # We deliberately give this method an unconventional name to avoid
375
+ # polluting the application namespace too badly
376
+ def get_creator
377
+ self.class.creator_attribute && send(self.class.creator_attribute)
378
+ end
531
379
 
532
380
 
533
381
  def duplicate
534
- res = self.class.new
535
- res.instance_variable_set("@attributes", @attributes.dup)
536
- res.instance_variable_set("@new_record", nil) unless new_record?
382
+ copy = self.class.new
383
+ copy.copy_instance_variables_from(self, ["@attributes_cache"])
384
+ copy.instance_variable_set("@attributes", @attributes.dup)
385
+ copy.instance_variable_set("@new_record", nil) unless new_record?
537
386
 
538
387
  # Shallow copy of belongs_to associations
539
388
  for refl in self.class.reflections.values
540
389
  if refl.macro == :belongs_to and (target = self.send(refl.name))
541
- bta = ActiveRecord::Associations::BelongsToAssociation.new(res, refl)
390
+ bta = ActiveRecord::Associations::BelongsToAssociation.new(copy, refl)
542
391
  bta.replace(target)
543
- res.instance_variable_set("@#{refl.name}", bta)
392
+ copy.instance_variable_set("@#{refl.name}", bta)
544
393
  end
545
394
  end
546
- res
395
+ copy
547
396
  end
548
397
 
549
398
 
@@ -554,29 +403,25 @@ module Hobo
554
403
  fields.all?{|f| self.send(f) == other.send(f)}
555
404
  end
556
405
 
406
+
557
407
  def only_changed_fields?(other, *changed_fields)
558
408
  return true if other.nil?
559
409
 
560
- changed_fields = changed_fields.flatten.every(:to_s)
561
- all_cols = self.class.columns.every(:name) - []
410
+ changed_fields = changed_fields.flatten.*.to_s
411
+ all_cols = self.class.columns.*.name - []
562
412
  all_cols.all?{|c| c.in?(changed_fields) || self.send(c) == other.send(c) }
563
413
  end
564
414
 
415
+
565
416
  def compose_with(object, use=nil)
566
417
  CompositeModel.new_for([self, object])
567
418
  end
568
419
 
569
- def created_date
570
- created_at.to_date
571
- end
572
-
573
- def modified_date
574
- modified_at.to_date
575
- end
576
420
 
577
421
  def typed_id
578
- id ? "#{self.class.name.underscore}_#{self.id}" : nil
422
+ "#{self.class.name.underscore}_#{self.id}" if id
579
423
  end
424
+
580
425
 
581
426
  def to_s
582
427
  if self.class.name_attribute
@@ -586,15 +431,20 @@ module Hobo
586
431
  end
587
432
  end
588
433
 
434
+
589
435
  private
590
436
 
437
+
591
438
  def parse_datetime(s)
592
439
  defined?(Chronic) ? Chronic.parse(s) : Time.parse(s)
593
440
  end
594
441
 
442
+
595
443
  def convert_type_for_mass_assignment(field_type, value)
596
- if field_type.is_a?(ActiveRecord::Reflection::AssociationReflection) and field_type.macro.in?([:belongs_to, :has_one])
444
+ if field_type.is_a?(ActiveRecord::Reflection::AssociationReflection) &&
445
+ field_type.macro.in?([:belongs_to, :has_one])
597
446
  if value.is_a?(String) && value.starts_with?('@')
447
+ # TODO: This @foo_1 feature is rarely (never?) used - get rid of it
598
448
  Hobo.object_from_dom_id(value[1..-1])
599
449
  else
600
450
  value
@@ -618,62 +468,19 @@ module Hobo
618
468
  else
619
469
  value
620
470
  end
621
- elsif field_type <= TrueClass
471
+ elsif field_type <= Hobo::Boolean
622
472
  (value.is_a?(String) && value.strip.downcase.in?(['0', 'false']) || value.blank?) ? false : true
623
473
  else
624
474
  # primitive field
625
475
  value
626
476
  end
627
477
  end
628
-
629
- end
630
- end
631
-
632
-
633
- # Hack AR to get Hobo type wrappers in
634
-
635
- module ActiveRecord::AttributeMethods::ClassMethods
636
-
637
- # Define an attribute reader method. Cope with nil column.
638
- def define_read_method(symbol, attr_name, column)
639
- cast_code = column.type_cast_code('v') if column
640
- access_code = cast_code ? "(v=@attributes['#{attr_name}']) && #{cast_code}" : "@attributes['#{attr_name}']"
641
-
642
- unless attr_name.to_s == self.primary_key.to_s
643
- access_code = access_code.insert(0, "missing_attribute('#{attr_name}', caller) " +
644
- "unless @attributes.has_key?('#{attr_name}'); ")
645
- end
646
-
647
- # This is the Hobo hook - add a type wrapper around the field
648
- # value if we have a special type defined
649
- src = if connected? && respond_to?(:field_type) && (type_wrapper = field_type(symbol)) &&
650
- type_wrapper.is_a?(Class) && type_wrapper.not_in?(Hobo::Model::PLAIN_TYPES.values)
651
- "val = begin; #{access_code}; end; " +
652
- "if val.nil? || (val.respond_to?(:hobo_undefined?) && val.hobo_undefined?); val; " +
653
- "else; self.class.field_type(:#{attr_name}).new(val); end"
654
- else
655
- access_code
656
- end
657
-
658
- evaluate_attribute_method(attr_name,
659
- "def #{symbol}; @attributes_cache['#{attr_name}'] ||= begin; #{src}; end; end")
478
+
660
479
  end
661
480
 
662
- def define_write_method(attr_name)
663
- src = if connected? && respond_to?(:field_type) && (type_wrapper = field_type(attr_name)) &&
664
- type_wrapper.is_a?(Class) && type_wrapper.not_in?(Hobo::Model::PLAIN_TYPES.values)
665
- "if val.nil? || (val.respond_to?(:hobo_undefined?) && val.hobo_undefined?); val; " +
666
- "else; self.class.field_type(:#{attr_name}).new(val); end"
667
- else
668
- "val"
669
- end
670
- evaluate_attribute_method(attr_name, "def #{attr_name}=(val); " +
671
- "write_attribute('#{attr_name}', #{src});end", "#{attr_name}=")
672
-
673
- end
674
-
675
481
  end
676
482
 
483
+
677
484
  class ActiveRecord::Base
678
485
  alias_method :has_hobo_method?, :respond_to_without_attributes?
679
486
  end