hobo 0.5.3

Sign up to get free protection for your applications and to get access to all the features.
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