hobo 0.5.3

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 (125) hide show
  1. data/LICENSE.txt +22 -0
  2. data/README.txt +18 -0
  3. data/bin/hobo +81 -0
  4. data/hobo_files/plugin/CHANGES.txt +963 -0
  5. data/hobo_files/plugin/LICENSE.txt +22 -0
  6. data/hobo_files/plugin/README +4 -0
  7. data/hobo_files/plugin/Rakefile +11 -0
  8. data/hobo_files/plugin/generators/hobo/hobo_generator.rb +37 -0
  9. data/hobo_files/plugin/generators/hobo/templates/application.dryml +2 -0
  10. data/hobo_files/plugin/generators/hobo/templates/guest.rb +31 -0
  11. data/hobo_files/plugin/generators/hobo_front_controller/USAGE +11 -0
  12. data/hobo_files/plugin/generators/hobo_front_controller/hobo_front_controller_generator.rb +90 -0
  13. data/hobo_files/plugin/generators/hobo_front_controller/templates/controller.rb +51 -0
  14. data/hobo_files/plugin/generators/hobo_front_controller/templates/functional_test.rb +18 -0
  15. data/hobo_files/plugin/generators/hobo_front_controller/templates/helper.rb +2 -0
  16. data/hobo_files/plugin/generators/hobo_front_controller/templates/index.dryml +43 -0
  17. data/hobo_files/plugin/generators/hobo_front_controller/templates/login.dryml +44 -0
  18. data/hobo_files/plugin/generators/hobo_front_controller/templates/search.dryml +18 -0
  19. data/hobo_files/plugin/generators/hobo_front_controller/templates/signup.dryml +45 -0
  20. data/hobo_files/plugin/generators/hobo_model/USAGE +26 -0
  21. data/hobo_files/plugin/generators/hobo_model/hobo_model_generator.rb +38 -0
  22. data/hobo_files/plugin/generators/hobo_model/templates/fixtures.yml +11 -0
  23. data/hobo_files/plugin/generators/hobo_model/templates/migration.rb +13 -0
  24. data/hobo_files/plugin/generators/hobo_model/templates/model.rb +24 -0
  25. data/hobo_files/plugin/generators/hobo_model/templates/unit_test.rb +10 -0
  26. data/hobo_files/plugin/generators/hobo_model_controller/USAGE +30 -0
  27. data/hobo_files/plugin/generators/hobo_model_controller/hobo_model_controller_generator.rb +43 -0
  28. data/hobo_files/plugin/generators/hobo_model_controller/templates/controller.rb +5 -0
  29. data/hobo_files/plugin/generators/hobo_model_controller/templates/functional_test.rb +18 -0
  30. data/hobo_files/plugin/generators/hobo_model_controller/templates/helper.rb +2 -0
  31. data/hobo_files/plugin/generators/hobo_model_controller/templates/view.rhtml +2 -0
  32. data/hobo_files/plugin/generators/hobo_rapid/hobo_rapid_generator.rb +51 -0
  33. data/hobo_files/plugin/generators/hobo_rapid/templates/hobo_rapid.js +436 -0
  34. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/default/default_mapping.rb +11 -0
  35. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/default/public/images/banner.gif +0 -0
  36. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/default/public/images/bkg_bodytop.gif +0 -0
  37. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/default/public/images/bkg_corner_01.gif +0 -0
  38. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/default/public/images/bkg_corner_02.gif +0 -0
  39. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/default/public/images/bkg_corner_03.gif +0 -0
  40. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/default/public/images/bkg_corner_04.gif +0 -0
  41. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/default/public/images/bkg_shadow_bottom.gif +0 -0
  42. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/default/public/images/bkg_shadow_left.gif +0 -0
  43. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/default/public/images/bkg_shadow_right.gif +0 -0
  44. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/default/public/images/bkg_shadow_top.gif +0 -0
  45. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/default/public/images/header_blue.gif +0 -0
  46. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/default/public/images/header_dblue.gif +0 -0
  47. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/default/public/images/header_green.gif +0 -0
  48. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/default/public/images/header_purple.gif +0 -0
  49. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/default/public/images/header_red.gif +0 -0
  50. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/default/public/images/logo.gif +0 -0
  51. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/default/public/images/spinner.gif +0 -0
  52. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/default/public/images/txt_list_img_dblue.gif +0 -0
  53. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/default/public/images/txt_list_img_green.gif +0 -0
  54. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/default/public/images/txt_list_img_purple.gif +0 -0
  55. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/default/public/images/txt_list_img_red.gif +0 -0
  56. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/default/public/images/window_corner_01.gif +0 -0
  57. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/default/public/images/window_corner_02.gif +0 -0
  58. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/default/public/images/window_corner_03.gif +0 -0
  59. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/default/public/images/window_corner_04.gif +0 -0
  60. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/default/public/images/window_shadow_bottom.gif +0 -0
  61. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/default/public/images/window_shadow_left.gif +0 -0
  62. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/default/public/images/window_shadow_right.gif +0 -0
  63. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/default/public/images/window_shadow_top.gif +0 -0
  64. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/default/public/stylesheets/application.css +390 -0
  65. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/default/views/application.dryml +104 -0
  66. data/hobo_files/plugin/generators/hobo_user_model/USAGE +26 -0
  67. data/hobo_files/plugin/generators/hobo_user_model/hobo_user_model_generator.rb +38 -0
  68. data/hobo_files/plugin/generators/hobo_user_model/templates/fixtures.yml +11 -0
  69. data/hobo_files/plugin/generators/hobo_user_model/templates/migration.rb +15 -0
  70. data/hobo_files/plugin/generators/hobo_user_model/templates/model.rb +58 -0
  71. data/hobo_files/plugin/generators/hobo_user_model/templates/unit_test.rb +10 -0
  72. data/hobo_files/plugin/init.rb +44 -0
  73. data/hobo_files/plugin/lib/action_view_extensions/base.rb +14 -0
  74. data/hobo_files/plugin/lib/active_record/has_many_association.rb +54 -0
  75. data/hobo_files/plugin/lib/active_record/has_many_through_association.rb +22 -0
  76. data/hobo_files/plugin/lib/active_record/table_definition.rb +34 -0
  77. data/hobo_files/plugin/lib/extensions.rb +245 -0
  78. data/hobo_files/plugin/lib/extensions/test_case.rb +130 -0
  79. data/hobo_files/plugin/lib/hobo.rb +353 -0
  80. data/hobo_files/plugin/lib/hobo/HtmlString +3 -0
  81. data/hobo_files/plugin/lib/hobo/authenticated_user.rb +106 -0
  82. data/hobo_files/plugin/lib/hobo/authentication_support.rb +108 -0
  83. data/hobo_files/plugin/lib/hobo/composite_model.rb +66 -0
  84. data/hobo_files/plugin/lib/hobo/controller.rb +134 -0
  85. data/hobo_files/plugin/lib/hobo/controller_helpers.rb +135 -0
  86. data/hobo_files/plugin/lib/hobo/core.rb +475 -0
  87. data/hobo_files/plugin/lib/hobo/define_tags.rb +56 -0
  88. data/hobo_files/plugin/lib/hobo/dryml.rb +161 -0
  89. data/hobo_files/plugin/lib/hobo/dryml/dryml_builder.rb +126 -0
  90. data/hobo_files/plugin/lib/hobo/dryml/tag_module.rb +9 -0
  91. data/hobo_files/plugin/lib/hobo/dryml/taglib.rb +57 -0
  92. data/hobo_files/plugin/lib/hobo/dryml/template.rb +586 -0
  93. data/hobo_files/plugin/lib/hobo/dryml/template_environment.rb +302 -0
  94. data/hobo_files/plugin/lib/hobo/dryml/template_handler.rb +19 -0
  95. data/hobo_files/plugin/lib/hobo/generator.rb +25 -0
  96. data/hobo_files/plugin/lib/hobo/html_string.rb +3 -0
  97. data/hobo_files/plugin/lib/hobo/lazy_hash.rb +28 -0
  98. data/hobo_files/plugin/lib/hobo/mapping_tags.rb +262 -0
  99. data/hobo_files/plugin/lib/hobo/markdown_string.rb +7 -0
  100. data/hobo_files/plugin/lib/hobo/model.rb +391 -0
  101. data/hobo_files/plugin/lib/hobo/model_controller.rb +676 -0
  102. data/hobo_files/plugin/lib/hobo/model_queries.rb +92 -0
  103. data/hobo_files/plugin/lib/hobo/model_support.rb +44 -0
  104. data/hobo_files/plugin/lib/hobo/password_string.rb +3 -0
  105. data/hobo_files/plugin/lib/hobo/predicate_dispatch.rb +78 -0
  106. data/hobo_files/plugin/lib/hobo/proc_binding.rb +32 -0
  107. data/hobo_files/plugin/lib/hobo/rapid.rb +447 -0
  108. data/hobo_files/plugin/lib/hobo/static_tags +92 -0
  109. data/hobo_files/plugin/lib/hobo/text.rb +3 -0
  110. data/hobo_files/plugin/lib/hobo/textile_string.rb +13 -0
  111. data/hobo_files/plugin/lib/hobo/undefined.rb +41 -0
  112. data/hobo_files/plugin/lib/hobo/undefined_access_error.rb +5 -0
  113. data/hobo_files/plugin/lib/hobo/where_fragment.rb +23 -0
  114. data/hobo_files/plugin/lib/rexml.rb +345 -0
  115. data/hobo_files/plugin/tags/core.dryml +6 -0
  116. data/hobo_files/plugin/tags/rapid.dryml +177 -0
  117. data/hobo_files/plugin/tags/rapid_editing.dryml +168 -0
  118. data/hobo_files/plugin/tags/rapid_navigation.dryml +95 -0
  119. data/hobo_files/plugin/tags/rapid_pages.dryml +175 -0
  120. data/hobo_files/plugin/tasks/environments.rake +19 -0
  121. data/hobo_files/plugin/tasks/hobo_tasks.rake +4 -0
  122. data/hobo_files/plugin/test/hobo_dryml_template_test.rb +7 -0
  123. data/hobo_files/plugin/test/hobo_test.rb +7 -0
  124. data/hobo_files/plugin/uninstall.rb +1 -0
  125. metadata +206 -0
@@ -0,0 +1,7 @@
1
+ class Hobo::MarkdownString < String
2
+
3
+ def to_html
4
+ self.blank? ? "" : BlueCloth.new(self).to_html
5
+ end
6
+
7
+ end
@@ -0,0 +1,391 @@
1
+ module Hobo
2
+
3
+ module Model
4
+
5
+ def self.included(base)
6
+ Hobo.register_model(base)
7
+ base.extend(ClassMethods)
8
+ base.set_field_type({})
9
+ class << base
10
+ alias_method_chain :has_many, :defined_scopes
11
+ end
12
+ end
13
+
14
+ module ClassMethods
15
+
16
+ # include methods also shared by CompositeModel
17
+ include ModelSupport::ClassMethods
18
+
19
+ def method_added(name)
20
+ # avoid error when running model generators before
21
+ # db exists
22
+ return unless connected?
23
+
24
+ aliased_name = "#{name}_without_hobo_type"
25
+ return if name.to_s.ends_with?('without_hobo_type') or aliased_name.in?(instance_methods)
26
+
27
+ type_wrapper = self.field_type(name)
28
+ if type_wrapper && type_wrapper.is_a?(Class) && type_wrapper < String
29
+ aliased_name = "#{name}_without_hobo_type"
30
+ alias_method aliased_name, name
31
+ define_method name do
32
+ res = send(aliased_name)
33
+ if res.nil?
34
+ nil
35
+ elsif res.respond_to?(:hobo_undefined?) && res.hobo_undefined?
36
+ res
37
+ else
38
+ type_wrapper.new(res)
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+ def set_field_type(types)
45
+ types.each_pair do |field, type|
46
+
47
+ # TODO: Make this extensible
48
+ type_class = case type
49
+ when :html; HtmlString
50
+ when :markdown; MarkdownString
51
+ when :textile; TextileString
52
+ when :password; PasswordString
53
+ else type
54
+ end
55
+
56
+ field_types[field] = type_class
57
+ end
58
+ end
59
+
60
+
61
+ def field_types
62
+ @hobo_field_types ||= superclass.respond_to?(:field_types) ? superclass.field_types : {}
63
+ end
64
+
65
+
66
+ def set_default_order(order)
67
+ @default_order = order
68
+ end
69
+
70
+ inheriting_attr_accessor :default_order, :id_name_options
71
+
72
+
73
+ def never_show(*fields)
74
+ @hobo_never_show ||= []
75
+ @hobo_never_show.concat(fields.omap{to_sym})
76
+ end
77
+
78
+
79
+ def never_show?(field)
80
+ @hobo_never_show and field.to_sym.in?(@hobo_never_show)
81
+ end
82
+
83
+ def set_creator_attr(attr)
84
+ class_eval %{
85
+ def creator
86
+ #{attr};
87
+ end
88
+ def creator=(x)
89
+ self.#{attr} = x;
90
+ end
91
+ }
92
+ end
93
+
94
+ def set_search_columns(*columns)
95
+ class_eval %{
96
+ def self.search_columns
97
+ %w{#{columns.omap{to_s} * ' '}}
98
+ end
99
+ }
100
+ end
101
+
102
+
103
+ def id_name(*args)
104
+ @id_name_options = [] + args
105
+
106
+ underscore = args.delete(:underscore)
107
+ insenstive = args.delete(:case_insensitive)
108
+ id_name_field = args.first || :name
109
+ @id_name_column = id_name_field.to_s
110
+
111
+ if underscore
112
+ class_eval %{
113
+ def id_name(underscore=false)
114
+ underscore ? #{id_name_field}.gsub(' ', '_') : #{id_name_field}
115
+ end
116
+ }
117
+ else
118
+ class_eval %{
119
+ def id_name(underscore=false)
120
+ #{id_name_field}
121
+ end
122
+ }
123
+ end
124
+
125
+ key = "id_name#{if underscore; ".gsub('_', ' ')"; end}"
126
+ finder = if insenstive
127
+ "find(:first, :conditions => ['lower(#{id_name_field}) = ?', #{key}.downcase])"
128
+ else
129
+ "find_by_#{id_name_field}(#{key})"
130
+ end
131
+
132
+ class_eval %{
133
+ def self.find_by_id_name(id_name)
134
+ #{finder}
135
+ end
136
+ }
137
+
138
+ model = self
139
+ validate do
140
+ erros.add id_name_field, "is taken" if model.find_by_id_name(name)
141
+ end
142
+ validates_format_of id_name_field, :with => /^[^_]+$/, :message => "cannot contain underscores" if
143
+ underscore
144
+ end
145
+
146
+
147
+ def id_name?
148
+ respond_to?(:find_by_id_name)
149
+ end
150
+
151
+
152
+ attr_reader :id_name_column
153
+
154
+
155
+
156
+ def field_type(name)
157
+ name = name.to_sym
158
+ field_types[name] or
159
+ reflections[name] or
160
+ ((col = columns.find {|c| c.name == name.to_s}) and case col.type
161
+ when :boolean
162
+ TrueClass
163
+ when :text
164
+ Hobo::Text
165
+ else
166
+ col.klass
167
+ end)
168
+ end
169
+
170
+
171
+ def conditions(&b)
172
+ ModelQueries.new(self).instance_eval(&b).to_sql
173
+ end
174
+
175
+
176
+ def find(*args, &b)
177
+ if args.first.in?([:all, :first])
178
+ if args.last.is_a? Hash
179
+ options = args.last
180
+ args[-1] = options = options.merge(:order => default_order) if options[:order] == :default
181
+ else
182
+ options = {}
183
+ end
184
+
185
+ if b
186
+ super(args.first, options.merge(:conditions => conditions(&b)))
187
+ else
188
+ super(*args)
189
+ end
190
+ else
191
+ super(*args)
192
+ end
193
+ end
194
+
195
+
196
+ def count(*args, &b)
197
+ if b
198
+ sql = ModelQueries.new(self).instance_eval(&b).to_sql
199
+ options = extract_options_from_args!(args)
200
+ super(*args + [options.merge(:conditions => sql)])
201
+ else
202
+ super(*args)
203
+ end
204
+ end
205
+
206
+
207
+ def subclass_associations(association, *subclass_associations)
208
+ refl = reflections[association]
209
+ for assoc in subclass_associations
210
+ class_name = assoc.to_s.classify
211
+ options = { :class_name => class_name, :conditions => "type = '#{class_name}'" }
212
+ options[:source] = refl.source_reflection.name if refl.source_reflection
213
+ has_many(assoc, refl.options.merge(options))
214
+ end
215
+ end
216
+
217
+ def has_creator?
218
+ instance_methods.include?('creator=') and instance_methods.include?('creator')
219
+ end
220
+
221
+ def search_columns
222
+ cols = columns.omap{name}
223
+ %w{name title body content}.select{|c| c.in?(cols) }
224
+ end
225
+
226
+ # This should really be a method on AssociationReflection
227
+ def reverse_reflection(association_name)
228
+ refl = reflections[association_name]
229
+ return nil if refl.options[:conditions]
230
+
231
+ reverse_macro = if refl.macro == :has_many
232
+ :belongs_to
233
+ elsif refl.macro == :belongs_to
234
+ :has_many
235
+ end
236
+ refl.klass.reflections.values.find do |r|
237
+ r.macro == reverse_macro and
238
+ r.klass == self and
239
+ !r.options[:conditions] and
240
+ r.primary_key_name == refl.primary_key_name
241
+ end
242
+ end
243
+
244
+
245
+ class ScopedProxy
246
+ def initialize(klass, scope={})
247
+ @klass, @scope = klass, scope
248
+ end
249
+
250
+ def method_missing(name, *args, &block)
251
+ klass.with_scope(@scope) do
252
+ @klass.send(name, *args, &block)
253
+ end
254
+ end
255
+ end
256
+ (Object.instance_methods +
257
+ Object.private_instance_methods +
258
+ Object.protected_instance_methods).each do |m|
259
+ ScopedProxy.send(:undef_method, m) unless
260
+ m.in?(%w{initialize method_missing}) || m.starts_with?('_')
261
+ end
262
+
263
+ attr_accessor :defined_scopes
264
+
265
+
266
+ def def_scope(name, scope=nil, &block)
267
+ @defined_scopes ||= {}
268
+ @defined_scopes[name.to_sym] = block || scope
269
+
270
+ meta_def(name) do |*args|
271
+ ScopedProxy.new(self, block ? block.call(*args) : scope)
272
+ end
273
+ end
274
+
275
+
276
+ module DefinedScopeProxyExtender
277
+
278
+ attr_accessor :reflections
279
+
280
+ def method_missing(name, *args, &block)
281
+ scopes = proxy_reflection.klass.defined_scopes
282
+ scope = scopes && scopes[name.to_sym]
283
+ if scope
284
+ scope = scope.call(*args) if scope.is_a?(Proc)
285
+
286
+ # Calling directly causes self to get loaded
287
+ assoc = Kernel.instance_method(:instance_variable_get).bind(self).call("@#{name}")
288
+ unless assoc
289
+ options = proxy_reflection.options
290
+ has_many_conditions = options.has_key?(:condition)
291
+ scope_conditions = scope.delete(:conditions)
292
+ conditions = if has_many_conditions && scope_conditions
293
+ "(#{scope_conditions}) AND (#{has_many_conditions})"
294
+ else
295
+ scope_conditions || has_many_conditions
296
+ end
297
+
298
+ options = options.merge(scope).update(:conditions => conditions,
299
+ :class_name => proxy_reflection.klass.name,
300
+ :foreign_key => proxy_reflection.primary_key_name)
301
+ r = ActiveRecord::Reflection::AssociationReflection.new(:has_many,
302
+ name,
303
+ options,
304
+ proxy_reflection.klass)
305
+ @reflections ||= {}
306
+ @reflections[name] = r
307
+
308
+ assoc = if options.has_key?(:through)
309
+ ActiveRecord::Associations::HasManyThroughAssociation
310
+ else
311
+ ActiveRecord::Associations::HasManyAssociation
312
+ end.new(self.proxy_owner, r)
313
+
314
+ # Calling directly causes self to get loaded
315
+ Kernel.instance_method(:instance_variable_set).bind(self).call("@#{name}", assoc)
316
+ end
317
+ assoc
318
+ else
319
+ super
320
+ end
321
+ end
322
+
323
+ end
324
+
325
+
326
+ def has_many_with_defined_scopes(name, *args, &block)
327
+ options = extract_options_from_args!(args)
328
+ if options.has_key?(:extend) || block
329
+ # Normal has_many
330
+ has_many_without_defined_scopes(name, *args + [options], &block)
331
+ else
332
+ options[:extend] = DefinedScopeProxyExtender
333
+ has_many_without_defined_scopes(name, *args + [options], &block)
334
+ end
335
+ end
336
+ end
337
+
338
+
339
+ def method_missing(name, *args, &b)
340
+ val = super
341
+ if val.nil?
342
+ nil
343
+ else
344
+ type_wrapper = self.class.field_type(name)
345
+ (type_wrapper && type_wrapper.is_a?(Class) && type_wrapper < String) ? type_wrapper.new(val) : val
346
+ end
347
+ end
348
+
349
+
350
+ def created_by(user)
351
+ self.creator ||= user if self.class.has_creator? and not user.guest?
352
+ end
353
+
354
+
355
+ def duplicate
356
+ res = self.class.new
357
+ res.instance_variable_set("@attributes", @attributes.dup)
358
+ res.instance_variable_set("@new_record", nil) unless new_record?
359
+
360
+ # Shallow copy of belongs_to associations
361
+ for refl in self.class.reflections.values
362
+ if refl.macro == :belongs_to and (target = self.send(refl.name))
363
+ bta = ActiveRecord::Associations::BelongsToAssociation.new(res, refl)
364
+ bta.replace(target)
365
+ res.instance_variable_set("@#{refl.name}", bta)
366
+ end
367
+ end
368
+ res
369
+ end
370
+
371
+
372
+ def same_fields?(other, *fields)
373
+ fields.all?{|f| self.send(f) == other.send(f)}
374
+ end
375
+
376
+ def changed_fields?(other, *fields)
377
+ fields.all?{|f| self.send(f) != other.send(f)}
378
+ end
379
+
380
+ def compose_with(object, use=nil)
381
+ CompositeModel.new_for([self, object])
382
+ end
383
+
384
+
385
+ def typed_id
386
+ id ? "#{self.class.name.underscore}_#{self.id}" : nil
387
+ end
388
+
389
+ end
390
+ end
391
+
@@ -0,0 +1,676 @@
1
+ module Hobo
2
+
3
+ module ModelController
4
+
5
+ include Hobo::Controller
6
+
7
+ class PermissionDeniedError < RuntimeError; end
8
+
9
+ VIEWLIB_DIR = "hobolib"
10
+
11
+ GENERIC_PAGE_TAGS = [:index, :show, :new, :edit, :show_collection, :new_in_collection]
12
+
13
+ class << self
14
+
15
+ def included(base)
16
+ base.extend(ClassMethods)
17
+ base.helper_method(:find_partial, :model, :current_user)
18
+
19
+ Hobo::ControllerHelpers.public_instance_methods.each {|m| base.hide_action(m)}
20
+
21
+ for collection in base.collections
22
+ add_collection_actions(base, collection.to_sym)
23
+ end
24
+
25
+ base.before_filter :set_no_cache_headers
26
+
27
+ base.class_eval do
28
+ alias_method_chain :redirect_to, :object_url
29
+ end
30
+ end
31
+
32
+ def find_partial(klass, as)
33
+ find_model_template(klass, as, true)
34
+ end
35
+
36
+
37
+ def template_path(dir, name, is_partial)
38
+ fileRx = is_partial ? /^_#{name}\.[^.]+/ : /^#{name}\.[^.]+/
39
+ unless Dir.entries("#{RAILS_ROOT}/app/views/#{dir}").grep(fileRx).empty?
40
+ return "#{dir}/#{name}"
41
+ end
42
+ end
43
+
44
+
45
+ def find_model_template(klass, name, is_partial=false)
46
+ while klass and klass != ActiveRecord::Base
47
+ dir = klass.name.underscore.pluralize
48
+ path = template_path(dir, name, is_partial)
49
+ return path if path
50
+
51
+ klass = klass.superclass
52
+ end
53
+ nil
54
+ end
55
+
56
+
57
+ def add_collection_actions(controller_class, name)
58
+ defined_methods = controller_class.instance_methods
59
+
60
+ show_collection_method = "show_#{name}".to_sym
61
+ unless show_collection_method.in?(defined_methods)
62
+ controller_class.send(:define_method, show_collection_method) do
63
+ hobo_show_collection(name)
64
+ end
65
+ end
66
+
67
+ if Hobo.simple_has_many_association?(controller_class.model.reflections[name])
68
+ new_method = "new_#{name.to_s.singularize}"
69
+ if new_method.not_in?(defined_methods)
70
+ controller_class.send(:define_method, new_method) do
71
+ hobo_new_in_collection(name)
72
+ end
73
+ end
74
+ end
75
+ end
76
+
77
+ end
78
+
79
+ module ClassMethods
80
+
81
+ attr_writer :model
82
+
83
+ def web_methods
84
+ @web_methods ||= superclass.respond_to?(:web_methods) ? superclass.web_methods : []
85
+ end
86
+
87
+ def show_actions
88
+ @show_actions ||= superclass.respond_to?(:show_actions) ? superclass.show_actions : []
89
+ end
90
+
91
+ def collections
92
+ # By default, all has_many associations are published
93
+ @collections ||= if superclass.respond_to?(:collections)
94
+ superclass.collections
95
+ else
96
+ model.reflections.values.map {|r| r.name if r.macro == :has_many}.compact
97
+ end
98
+ end
99
+
100
+ def model
101
+ @model ||= name.sub(/Controller$/, "").singularize.constantize
102
+ end
103
+
104
+
105
+ def autocomplete_for(attr, options={})
106
+ opts = { :limit => 15 }.update(options)
107
+ @completers ||= {}
108
+ @completers[attr.to_sym] = opts
109
+ end
110
+
111
+
112
+ def autocompleter(name)
113
+ (@completers && @completers[name]) ||
114
+ (superclass.respond_to?(:autocompleter) && superclass.autocompleter(name))
115
+ end
116
+
117
+
118
+ def def_data_filter(name, &b)
119
+ @data_filters ||= {}
120
+ @data_filters[name] = b
121
+ end
122
+
123
+
124
+ def web_method(web_name, method_name=nil)
125
+ method_name ||= web_name
126
+ web_methods << web_name.to_sym
127
+ before_filter(:only => [web_name]) {|controller| controller.send(:prepare_web_method, method_name)}
128
+ end
129
+
130
+
131
+ def show_action(*names)
132
+ show_actions.concat(names)
133
+ for name in names
134
+ class_eval "def #{name}; show; end"
135
+ end
136
+ end
137
+
138
+
139
+ def publish_collection(*names)
140
+ collections.concat(names)
141
+ names.each {|n| ModelController.add_collection_actions(self, n)}
142
+ end
143
+
144
+
145
+ def data_filter(name)
146
+ (@data_filters && @data_filters[name]) ||
147
+ (superclass.respond_to?(:data_filter) && superclass.data_filter(name))
148
+ end
149
+
150
+
151
+ def find_instance(id)
152
+ if model.id_name? and id !~ /^\d+$/
153
+ model.find_by_id_name(id)
154
+ else
155
+ model.find(id)
156
+ end
157
+ end
158
+
159
+ end
160
+
161
+ # --- ACTIONS --- #
162
+
163
+ def index; hobo_index; end
164
+ def show; hobo_show; end
165
+ def new; hobo_new; end
166
+ def create; hobo_create; end
167
+ def edit; hobo_edit; end
168
+ def update; hobo_update; end
169
+ def destroy; hobo_destroy; end
170
+
171
+ def completions
172
+ attr = params[:for]
173
+ opts = attr && self.class.autocompleter(attr.to_sym)
174
+ if opts
175
+ q = params[:query]
176
+ items = find_with_data_filter(opts) { send("#{attr}_contains", q) }
177
+
178
+ render :text => "<ul>\n" + items.map {|i| "<li>#{i.send(attr)}</li>\n"}.join + "</ul>"
179
+ else
180
+ render :text => "No completer for #{attr}", :status => 404
181
+ end
182
+ end
183
+
184
+
185
+ ###### END OF ACTIONS ######
186
+
187
+ protected
188
+
189
+ def overridable_response(options, key)
190
+ if options.has_key?(key)
191
+ options[key]
192
+ true
193
+ else
194
+ yield if block_given?
195
+ false
196
+ end
197
+ end
198
+
199
+ # --- action implementations --- #
200
+
201
+ def hobo_index(options = {})
202
+ options = LazyHash.new(options)
203
+ @model = model
204
+ @this = options[:collection] || paginated_find
205
+
206
+ instance_variable_set("@#{@model.name.pluralize.underscore}", @this)
207
+ if block_given?
208
+ yield
209
+ else
210
+ hobo_render
211
+ end
212
+ end
213
+
214
+
215
+ def paginated_find(*args, &b)
216
+ options = extract_options_from_args!(args)
217
+
218
+ total_number = options.delete(:total_number)
219
+ if args.empty?
220
+ total_number ||= count_with_data_filter
221
+ else
222
+ owner, collection_name = args
223
+ @association = collection_name.to_s.split(".").inject(owner) { |m, name| m.send(name) }
224
+ total_number ||= @association.size
225
+ @reflection = @association.proxy_reflection
226
+ end
227
+
228
+ page_size = options.delete(:page_size) || 20
229
+ page = options.delete(:page) || params[:page]
230
+ @pages = ::ActionController::Pagination::Paginator.new(self, total_number, page_size, page)
231
+
232
+ options = {
233
+ :limit => @pages.items_per_page,
234
+ :offset => @pages.current.offset,
235
+ }.merge(options)
236
+
237
+ if @association
238
+ @association.find(:all, options, &b)
239
+ else
240
+ options[:order] = :default
241
+ find_with_data_filter(options, &b)
242
+ end
243
+ end
244
+
245
+
246
+ def find_instance_or_not_found(options, this_option)
247
+ x = begin
248
+ options[this_option] || find_instance
249
+ rescue ActiveRecord::RecordNotFound
250
+ nil
251
+ end
252
+
253
+ not_found unless x
254
+ x
255
+ end
256
+
257
+
258
+ def hobo_show(options={})
259
+ options = LazyHash.new(options)
260
+
261
+ @this = find_instance_or_not_found(options, :this)
262
+ if @this
263
+ if Hobo.can_view?(current_user, @this)
264
+ set_named_this!
265
+ block_given? ? yield : hobo_render
266
+ else
267
+ permission_denied(options)
268
+ end
269
+ end
270
+ end
271
+
272
+
273
+ def hobo_new(options={})
274
+ options = LazyHash.new(options)
275
+ @this = options[:this] || model.new
276
+ @this.created_by(current_user) unless options.has_key?(:set_creator) && !options[:set_creator]
277
+
278
+ if Hobo.can_create?(current_user, @this)
279
+ set_named_this!
280
+ block_given? ? yield : hobo_render
281
+ else
282
+ permission_denied(options)
283
+ end
284
+ end
285
+
286
+
287
+ def hobo_create(options={})
288
+ options = LazyHash.new(options)
289
+
290
+ @this = (options[:this] ||
291
+ begin
292
+ attributes = params[model.name.underscore]
293
+ type_attr = params['type']
294
+ create_model = if 'type'.in?(model.column_names) and
295
+ type_attr and type_attr.in?(model.send(:subclasses).omap{name})
296
+ type_attr.constantize
297
+ else
298
+ model
299
+ end
300
+ this = create_model.new
301
+ @check_create_permission = [this]
302
+ initialize_from_params(this, attributes)
303
+ for obj in @check_create_permission
304
+ permission_denied(options) and return unless Hobo.can_create?(current_user, obj)
305
+ end
306
+ @check_create_permission = nil
307
+ this
308
+ end)
309
+
310
+ set_named_this!
311
+ if @this.save
312
+ if block_given?
313
+ yield
314
+ else
315
+ respond_to do |wants|
316
+ wants.html { overridable_response(options, :html_response) || redirect_to(object_url(@this)) }
317
+ wants.js { overridable_response(options, :js_response) || hobo_ajax_response || render(:text => "") }
318
+ end
319
+ end
320
+ else
321
+ # Validation errors
322
+ unless options[:invalid_response]
323
+ respond_to do |wants|
324
+ wants.html { overridable_response(options, :invalid_html_response) || hobo_render(:new) }
325
+ wants.js do
326
+ (overridable_response(options, :invalid_js_response) ||
327
+ render(:status => 500,
328
+ :text => ("There was a problem creating that #{create_model.name}.\n" +
329
+ @this.errors.full_messages.join("\n"))))
330
+ end
331
+ end
332
+ end
333
+ end
334
+ end
335
+
336
+ def hobo_edit(options={})
337
+ hobo_show(options)
338
+ end
339
+
340
+
341
+ def hobo_update(options={})
342
+ options = LazyHash.new(options)
343
+
344
+ @this = find_instance_or_not_found(options, :this)
345
+ return unless @this
346
+
347
+ original = @this.duplicate
348
+
349
+ changes = params[model.name.underscore]
350
+
351
+ if changes
352
+ # The 'duplicate' call above can set these, but they can
353
+ # conflict with the changes so we clear them
354
+ @this.send(:clear_aggregation_cache)
355
+ @this.send(:clear_association_cache)
356
+
357
+ update_with_params(@this, changes)
358
+ permission_denied(options) and return unless Hobo.can_update?(current_user, original, @this)
359
+ end
360
+
361
+ set_named_this!
362
+ if changes.nil? || @this.save
363
+ # Ensure current_user isn't out of date
364
+ @current_user = @this if @this == current_user
365
+
366
+ if block_given?
367
+ yield
368
+ else
369
+ respond_to do |wants|
370
+ wants.html do
371
+ overridable_response(options, :html_response) || redirect_to(object_url(@this))
372
+ end
373
+
374
+ wants.js do
375
+ overridable_response(options, :js_response) do
376
+ if changes.size == 1
377
+ # Decreasingly hacky support for the scriptaculous in-place-editor
378
+ new_val = Hobo::Dryml.render_tag(@template, "show",
379
+ :obj => @this, :attr => changes.keys.first, :no_span => true)
380
+ hobo_ajax_response(@this, :new_field_value => new_val)
381
+ else
382
+ hobo_ajax_response(@this)
383
+ end
384
+
385
+ # Maybe no ajax requests were made
386
+ render :nothing => true unless performed?
387
+ end
388
+ end
389
+ end
390
+ end
391
+
392
+ else
393
+ # Validation errors
394
+ respond_to do |wants|
395
+ wants.html do
396
+ overridable_response(options, :invalid_html_response) || render(:action => :edit)
397
+ end
398
+
399
+ wants.js do
400
+ overridable_response(options, :invalid_js_response) do
401
+ render(:status => 500,
402
+ :text => ("There was a problem with that change.\n" +
403
+ @this.errors.full_messages.join("\n")))
404
+ end
405
+ end
406
+ end
407
+ end
408
+ end
409
+
410
+
411
+ def hobo_destroy(options={})
412
+ options = LazyHash.new(options)
413
+ @this = find_instance_or_not_found(options, :this)
414
+ return unless @this
415
+
416
+ set_named_this!
417
+ permission_denied(options) and return unless Hobo.can_delete?(current_user, @this)
418
+
419
+ @this.destroy
420
+
421
+ if block_given?
422
+ yield
423
+ else
424
+ respond_to do |wants|
425
+ wants.html { overridable_response(options, :html_response) || redirect_to(:action => "index") }
426
+ wants.js { overridable_response(options, :js_response) || hobo_ajax_response || render(:text => "") }
427
+ end
428
+ end
429
+ end
430
+
431
+ def hobo_show_collection(collection, options={})
432
+ options = LazyHash.new(options)
433
+
434
+ @owner = find_instance_or_not_found(options, :owner)
435
+ return unless @owner
436
+
437
+ toplevel_collection = collection.to_s.split(".").first
438
+ if Hobo.can_view?(current_user, @owner, toplevel_collection)
439
+ @this = options[:collection] || @this = paginated_find(@owner, collection, options)
440
+
441
+ if block_given?
442
+ yield
443
+ else
444
+ hobo_render(params[:action]) or hobo_render(:show_collection, @reflection.klass)
445
+ end
446
+ else
447
+ permission_denied(options)
448
+ end
449
+ end
450
+
451
+
452
+ def hobo_new_in_collection(collection, options={})
453
+ options = LazyHash.new(options)
454
+ @owner = find_instance_or_not_found(options, :owner)
455
+ return unless @owner
456
+
457
+ permission_denied(options) and return unless Hobo.can_view?(current_user, @owner, collection)
458
+
459
+ @association = options[:collection] || @owner.send(collection)
460
+ @this = options[:this] || @association.new_without_appending
461
+ @this.created_by(current_user) unless options.has_key?(:set_creator) && !options[:set_creator]
462
+
463
+ permission_denied(options) and return unless Hobo.can_create?(current_user, @this)
464
+
465
+ if block_given?
466
+ yield
467
+ else
468
+ hobo_render("new_#{collection.to_s.singularize}") or hobo_render(:new_in_collection, @this.class)
469
+ end
470
+ end
471
+
472
+
473
+ # --- end action implementations --- #
474
+
475
+ # --- filters --- #
476
+
477
+ def prepare_web_method(method)
478
+ @this = find_instance
479
+ permission_denied unless Hobo.can_call?(current_user, @this, method)
480
+ end
481
+
482
+ # --- end filters --- #
483
+
484
+
485
+ def set_no_cache_headers
486
+ headers["Pragma"] = "no-cache"
487
+ #headers["Cache-Control"] = ["must-revalidate", "no-cache", "no-store"]
488
+ #headers["Cache-Control"] = "no-cache"
489
+ headers["Cache-Control"] = "no-store"
490
+ headers["Expires"] ='0'
491
+ end
492
+
493
+
494
+ def permission_denied(options=nil)
495
+ if options and options[:permission_denied_response]
496
+ # do nothing (callback handled by LazyHash)
497
+ elsif respond_to? :permission_denied_response
498
+ permission_denied_response
499
+ else
500
+ render :text => "Permission Denied", :status => 403
501
+ end
502
+ end
503
+
504
+ def not_found(options=nil)
505
+ if options && options[:not_found_response]
506
+ # do nothing (callback handled by LazyHash)
507
+ elsif respond_to? :not_found_response
508
+ not_found_response
509
+ else
510
+ render(:text => "Can't find #{model.name.titleize}: #{params[:id]}", :status => 404)
511
+ end
512
+ end
513
+
514
+ def find_instance(id=nil)
515
+ res = self.class.find_instance(id || params[:id])
516
+ instance_variable_set("@#{model.name.underscore}", res)
517
+ res
518
+ end
519
+
520
+ def set_named_this!
521
+ instance_variable_set("@#{model.name.underscore}", @this)
522
+ end
523
+
524
+
525
+ def hobo_render(page_kind = nil, page_model=nil)
526
+ page_kind ||= params[:action].to_sym
527
+ page_model ||= model
528
+
529
+ template = Hobo::ModelController.find_model_template(page_model, page_kind)
530
+
531
+ if template
532
+ render :template => template
533
+ true
534
+ else
535
+ if page_kind.in? GENERIC_PAGE_TAGS
536
+ render_tag("#{page_kind}_page", :obj => @this)
537
+ true
538
+ else
539
+ false
540
+ end
541
+ end
542
+ end
543
+
544
+
545
+ def model
546
+ self.class.model
547
+ end
548
+
549
+
550
+ def find_template
551
+ Hobo::ModelController.find_model_template(model, params[:action])
552
+ end
553
+
554
+
555
+ def with_data_filter(operation, *args, &block)
556
+ filter_param = params.keys.ofind {starts_with? "where_"}
557
+ proc = filter_param && self.class.data_filter(filter_param[6..-1].to_sym)
558
+ if proc
559
+ filter_args = params[filter_param]
560
+ filter_args = [filter_args] unless filter_args.is_a? Array
561
+ model.send(operation, *args) do
562
+ if block
563
+ instance_eval(&block) & instance_exec(*filter_args, &proc)
564
+ else
565
+ instance_exec(*filter_args, &proc)
566
+ end
567
+ end
568
+ else
569
+ if block
570
+ model.send(operation, *args) { instance_eval(&block) }
571
+ else
572
+ model.send(operation, *args)
573
+ end
574
+ end
575
+ end
576
+
577
+ def find_with_data_filter(opts={}, &b)
578
+ with_data_filter(:find, :all, opts, &b)
579
+ end
580
+
581
+
582
+ def count_with_data_filter(opts={}, &b)
583
+ with_data_filter(:count, opts, &b)
584
+ end
585
+
586
+
587
+ def initialize_from_params(obj, params)
588
+ update_with_params(obj, params)
589
+ obj.created_by(current_user)
590
+ (@check_create_permission ||= []) << obj
591
+ obj
592
+ end
593
+
594
+
595
+ def update_with_params(object, params)
596
+ return unless params
597
+
598
+ params.each_pair do |field,value|
599
+ field = field.to_sym
600
+ refl = object.class.reflections[field]
601
+ ar_value = if refl
602
+ if refl.macro == :belongs_to
603
+ associated_record(object, refl, value)
604
+
605
+ elsif Hobo.simple_has_many_association?(refl) and object.new_record?
606
+ # only populate has_many relationships for new records. For existing
607
+ # records, AR updates the DB immediately, bypassing Hobo's permission check
608
+ if value.is_a? Array
609
+ value.map {|x| associated_record(object, refl, x) }
610
+ else
611
+ value.keys.every(:to_i).sort.map{|i| associated_record(object, refl, value[i.to_s]) }
612
+ end
613
+ else
614
+ raise HoboError.new("association #{refl.name} is not settable via parameters")
615
+ end
616
+ else
617
+ param_to_value(object.class.field_type(field), value)
618
+ end
619
+ object.send("#{field}=".to_sym, ar_value)
620
+ end
621
+ end
622
+
623
+
624
+ def parse_datetime(s)
625
+ defined?(Chronic) ? Chronic.parse(s) : Time.parse(s)
626
+ end
627
+
628
+
629
+ def param_to_value(field_type, value)
630
+ if field_type <= Date
631
+ if value.is_a? Hash
632
+ Date.new(*(%w{year month day}.map{|s| value[s].to_i}))
633
+ elsif value.is_a? String
634
+ dt = parse_datetime(value)
635
+ dt && dt.to_date
636
+ end
637
+ elsif field_type <= Time
638
+ if value.is_a? Hash
639
+ Time.local(*(%w{year month day hour minute}.map{|s| value[s].to_i}))
640
+ elsif value.is_a? String
641
+ parse_datetime(value)
642
+ end
643
+ else
644
+ # primitive field
645
+ value
646
+ end
647
+ end
648
+
649
+ def associated_record(owner, refl, value)
650
+ if value.is_a? String
651
+ if value.starts_with?('@')
652
+ Hobo.object_from_dom_id(value[1..-1])
653
+ elsif refl.klass.id_name?
654
+ refl.klass.find_by_id_name(value)
655
+ else
656
+ nil
657
+ end
658
+ else
659
+ if refl.macro == :belongs_to
660
+ new_from_params(refl.klass, value)
661
+ else
662
+ obj = owner.send(refl.name).new
663
+ initialize_from_params(obj, value)
664
+ obj
665
+ end
666
+ end
667
+ end
668
+
669
+
670
+ def object_from_param(param)
671
+ Hobo.object_from_dom_id(param)
672
+ end
673
+
674
+ end
675
+
676
+ end