active_scaffold 3.0.0

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 (272) hide show
  1. data/.autotest +27 -0
  2. data/CHANGELOG +152 -0
  3. data/Gemfile +4 -0
  4. data/MIT-LICENSE +20 -0
  5. data/README +51 -0
  6. data/Rakefile +24 -0
  7. data/active_scaffold.gemspec +24 -0
  8. data/environment.rb +22 -0
  9. data/frontends/default/images/add.gif +0 -0
  10. data/frontends/default/images/arrow_down.gif +0 -0
  11. data/frontends/default/images/arrow_up.gif +0 -0
  12. data/frontends/default/images/close.gif +0 -0
  13. data/frontends/default/images/cross.png +0 -0
  14. data/frontends/default/images/indicator-small.gif +0 -0
  15. data/frontends/default/images/indicator.gif +0 -0
  16. data/frontends/default/images/magnifier.png +0 -0
  17. data/frontends/default/javascripts/jquery/active_scaffold.js +957 -0
  18. data/frontends/default/javascripts/jquery/jquery.editinplace.js +726 -0
  19. data/frontends/default/javascripts/prototype/active_scaffold.js +954 -0
  20. data/frontends/default/javascripts/prototype/dhtml_history.js +867 -0
  21. data/frontends/default/javascripts/prototype/form_enhancements.js +117 -0
  22. data/frontends/default/javascripts/prototype/rico_corner.js +370 -0
  23. data/frontends/default/stylesheets/stylesheet-ie.css +35 -0
  24. data/frontends/default/stylesheets/stylesheet.css +858 -0
  25. data/frontends/default/views/_add_existing_form.html.erb +30 -0
  26. data/frontends/default/views/_base_form.html.erb +41 -0
  27. data/frontends/default/views/_create_form.html.erb +6 -0
  28. data/frontends/default/views/_create_form_on_list.html.erb +5 -0
  29. data/frontends/default/views/_field_search.html.erb +32 -0
  30. data/frontends/default/views/_form.html.erb +24 -0
  31. data/frontends/default/views/_form_association.html.erb +14 -0
  32. data/frontends/default/views/_form_association_footer.html.erb +40 -0
  33. data/frontends/default/views/_form_attribute.html.erb +15 -0
  34. data/frontends/default/views/_form_hidden_attribute.html.erb +2 -0
  35. data/frontends/default/views/_form_messages.html.erb +5 -0
  36. data/frontends/default/views/_horizontal_subform.html.erb +19 -0
  37. data/frontends/default/views/_horizontal_subform_header.html.erb +10 -0
  38. data/frontends/default/views/_horizontal_subform_record.html.erb +37 -0
  39. data/frontends/default/views/_human_conditions.html.erb +1 -0
  40. data/frontends/default/views/_list.html.erb +18 -0
  41. data/frontends/default/views/_list_actions.html.erb +16 -0
  42. data/frontends/default/views/_list_calculations.html.erb +16 -0
  43. data/frontends/default/views/_list_column_headings.html.erb +12 -0
  44. data/frontends/default/views/_list_header.html.erb +12 -0
  45. data/frontends/default/views/_list_inline_adapter.html.erb +10 -0
  46. data/frontends/default/views/_list_messages.html.erb +32 -0
  47. data/frontends/default/views/_list_pagination.html.erb +11 -0
  48. data/frontends/default/views/_list_pagination_links.html.erb +9 -0
  49. data/frontends/default/views/_list_record.html.erb +14 -0
  50. data/frontends/default/views/_list_record_columns.html.erb +8 -0
  51. data/frontends/default/views/_list_with_header.html.erb +32 -0
  52. data/frontends/default/views/_messages.html.erb +10 -0
  53. data/frontends/default/views/_render_field.js.rjs +13 -0
  54. data/frontends/default/views/_row.html.erb +12 -0
  55. data/frontends/default/views/_search.html.erb +34 -0
  56. data/frontends/default/views/_search_attribute.html.erb +10 -0
  57. data/frontends/default/views/_show.html.erb +8 -0
  58. data/frontends/default/views/_show_columns.html.erb +12 -0
  59. data/frontends/default/views/_update_actions.html.erb +9 -0
  60. data/frontends/default/views/_update_form.html.erb +5 -0
  61. data/frontends/default/views/_vertical_subform.html.erb +12 -0
  62. data/frontends/default/views/_vertical_subform_record.html.erb +38 -0
  63. data/frontends/default/views/add_existing.js.rjs +17 -0
  64. data/frontends/default/views/add_existing_form.html.erb +5 -0
  65. data/frontends/default/views/create.html.erb +5 -0
  66. data/frontends/default/views/delete.html.erb +13 -0
  67. data/frontends/default/views/destroy.js.rjs +5 -0
  68. data/frontends/default/views/edit_associated.js.rjs +11 -0
  69. data/frontends/default/views/field_search.html.erb +5 -0
  70. data/frontends/default/views/form_messages.js.rjs +1 -0
  71. data/frontends/default/views/list.html.erb +1 -0
  72. data/frontends/default/views/list.js.rjs +1 -0
  73. data/frontends/default/views/on_action_update.js.rjs +8 -0
  74. data/frontends/default/views/on_create.js.rjs +24 -0
  75. data/frontends/default/views/on_update.js.rjs +15 -0
  76. data/frontends/default/views/search.html.erb +5 -0
  77. data/frontends/default/views/show.html.erb +5 -0
  78. data/frontends/default/views/update.html.erb +8 -0
  79. data/frontends/default/views/update_column.js.rjs +13 -0
  80. data/frontends/default/views/update_row.js.rjs +1 -0
  81. data/init.rb +1 -0
  82. data/install_assets.rb +44 -0
  83. data/lib/active_record_permissions.rb +134 -0
  84. data/lib/active_scaffold.rb +279 -0
  85. data/lib/active_scaffold/actions/common_search.rb +22 -0
  86. data/lib/active_scaffold/actions/core.rb +150 -0
  87. data/lib/active_scaffold/actions/create.rb +152 -0
  88. data/lib/active_scaffold/actions/delete.rb +72 -0
  89. data/lib/active_scaffold/actions/field_search.rb +82 -0
  90. data/lib/active_scaffold/actions/list.rb +128 -0
  91. data/lib/active_scaffold/actions/mark.rb +50 -0
  92. data/lib/active_scaffold/actions/nested.rb +241 -0
  93. data/lib/active_scaffold/actions/search.rb +47 -0
  94. data/lib/active_scaffold/actions/show.rb +54 -0
  95. data/lib/active_scaffold/actions/subform.rb +17 -0
  96. data/lib/active_scaffold/actions/update.rb +134 -0
  97. data/lib/active_scaffold/attribute_params.rb +207 -0
  98. data/lib/active_scaffold/bridges/ancestry/bridge.rb +5 -0
  99. data/lib/active_scaffold/bridges/ancestry/lib/ancestry_bridge.rb +38 -0
  100. data/lib/active_scaffold/bridges/bridge.rb +52 -0
  101. data/lib/active_scaffold/bridges/calendar_date_select/bridge.rb +16 -0
  102. data/lib/active_scaffold/bridges/calendar_date_select/lib/as_cds_bridge.rb +79 -0
  103. data/lib/active_scaffold/bridges/carrierwave/bridge.rb +7 -0
  104. data/lib/active_scaffold/bridges/carrierwave/lib/carrierwave_bridge.rb +38 -0
  105. data/lib/active_scaffold/bridges/carrierwave/lib/carrierwave_bridge_helpers.rb +26 -0
  106. data/lib/active_scaffold/bridges/carrierwave/lib/form_ui.rb +35 -0
  107. data/lib/active_scaffold/bridges/carrierwave/lib/list_ui.rb +17 -0
  108. data/lib/active_scaffold/bridges/date_picker/bridge.rb +22 -0
  109. data/lib/active_scaffold/bridges/date_picker/lib/datepicker_bridge.rb +225 -0
  110. data/lib/active_scaffold/bridges/date_picker/public/javascripts/date_picker_bridge.js +22 -0
  111. data/lib/active_scaffold/bridges/file_column/bridge.rb +11 -0
  112. data/lib/active_scaffold/bridges/file_column/lib/as_file_column_bridge.rb +46 -0
  113. data/lib/active_scaffold/bridges/file_column/lib/file_column_helpers.rb +59 -0
  114. data/lib/active_scaffold/bridges/file_column/lib/form_ui.rb +37 -0
  115. data/lib/active_scaffold/bridges/file_column/lib/list_ui.rb +26 -0
  116. data/lib/active_scaffold/bridges/file_column/test/functional/file_column_keep_test.rb +43 -0
  117. data/lib/active_scaffold/bridges/file_column/test/mock_model.rb +9 -0
  118. data/lib/active_scaffold/bridges/file_column/test/test_helper.rb +15 -0
  119. data/lib/active_scaffold/bridges/paperclip/bridge.rb +12 -0
  120. data/lib/active_scaffold/bridges/paperclip/lib/form_ui.rb +27 -0
  121. data/lib/active_scaffold/bridges/paperclip/lib/list_ui.rb +16 -0
  122. data/lib/active_scaffold/bridges/paperclip/lib/paperclip_bridge.rb +38 -0
  123. data/lib/active_scaffold/bridges/paperclip/lib/paperclip_bridge_helpers.rb +26 -0
  124. data/lib/active_scaffold/bridges/semantic_attributes/bridge.rb +5 -0
  125. data/lib/active_scaffold/bridges/semantic_attributes/lib/semantic_attributes_bridge.rb +20 -0
  126. data/lib/active_scaffold/bridges/shared/date_bridge.rb +187 -0
  127. data/lib/active_scaffold/bridges/tiny_mce/bridge.rb +5 -0
  128. data/lib/active_scaffold/bridges/tiny_mce/lib/tiny_mce_bridge.rb +45 -0
  129. data/lib/active_scaffold/bridges/validation_reflection/bridge.rb +8 -0
  130. data/lib/active_scaffold/bridges/validation_reflection/lib/validation_reflection_bridge.rb +21 -0
  131. data/lib/active_scaffold/config/base.rb +54 -0
  132. data/lib/active_scaffold/config/core.rb +229 -0
  133. data/lib/active_scaffold/config/create.rb +43 -0
  134. data/lib/active_scaffold/config/delete.rb +25 -0
  135. data/lib/active_scaffold/config/field_search.rb +74 -0
  136. data/lib/active_scaffold/config/form.rb +46 -0
  137. data/lib/active_scaffold/config/list.rb +174 -0
  138. data/lib/active_scaffold/config/mark.rb +22 -0
  139. data/lib/active_scaffold/config/nested.rb +43 -0
  140. data/lib/active_scaffold/config/search.rb +68 -0
  141. data/lib/active_scaffold/config/show.rb +34 -0
  142. data/lib/active_scaffold/config/subform.rb +35 -0
  143. data/lib/active_scaffold/config/update.rb +38 -0
  144. data/lib/active_scaffold/configurable.rb +29 -0
  145. data/lib/active_scaffold/constraints.rb +179 -0
  146. data/lib/active_scaffold/data_structures/action_columns.rb +133 -0
  147. data/lib/active_scaffold/data_structures/action_link.rb +162 -0
  148. data/lib/active_scaffold/data_structures/action_links.rb +59 -0
  149. data/lib/active_scaffold/data_structures/actions.rb +45 -0
  150. data/lib/active_scaffold/data_structures/column.rb +348 -0
  151. data/lib/active_scaffold/data_structures/columns.rb +75 -0
  152. data/lib/active_scaffold/data_structures/error_message.rb +24 -0
  153. data/lib/active_scaffold/data_structures/nested_info.rb +108 -0
  154. data/lib/active_scaffold/data_structures/set.rb +62 -0
  155. data/lib/active_scaffold/data_structures/sorting.rb +168 -0
  156. data/lib/active_scaffold/finder.rb +333 -0
  157. data/lib/active_scaffold/helpers/association_helpers.rb +40 -0
  158. data/lib/active_scaffold/helpers/controller_helpers.rb +40 -0
  159. data/lib/active_scaffold/helpers/country_helpers.rb +352 -0
  160. data/lib/active_scaffold/helpers/form_column_helpers.rb +343 -0
  161. data/lib/active_scaffold/helpers/human_condition_helpers.rb +59 -0
  162. data/lib/active_scaffold/helpers/id_helpers.rb +131 -0
  163. data/lib/active_scaffold/helpers/list_column_helpers.rb +363 -0
  164. data/lib/active_scaffold/helpers/pagination_helpers.rb +55 -0
  165. data/lib/active_scaffold/helpers/search_column_helpers.rb +238 -0
  166. data/lib/active_scaffold/helpers/show_column_helpers.rb +46 -0
  167. data/lib/active_scaffold/helpers/view_helpers.rb +315 -0
  168. data/lib/active_scaffold/locale/de.rb +113 -0
  169. data/lib/active_scaffold/locale/en.rb +118 -0
  170. data/lib/active_scaffold/locale/es.yml +112 -0
  171. data/lib/active_scaffold/locale/fr.rb +113 -0
  172. data/lib/active_scaffold/locale/hu.yml +63 -0
  173. data/lib/active_scaffold/locale/ja.yml +64 -0
  174. data/lib/active_scaffold/locale/ru.yml +62 -0
  175. data/lib/active_scaffold/marked_model.rb +38 -0
  176. data/lib/dhtml_confirm.rb +54 -0
  177. data/lib/extensions/action_controller_rendering.rb +20 -0
  178. data/lib/extensions/action_view_rendering.rb +113 -0
  179. data/lib/extensions/action_view_resolver.rb +7 -0
  180. data/lib/extensions/active_record_offset.rb +12 -0
  181. data/lib/extensions/array.rb +7 -0
  182. data/lib/extensions/localize.rb +10 -0
  183. data/lib/extensions/name_option_for_datetime.rb +12 -0
  184. data/lib/extensions/nil_id_in_url_params.rb +7 -0
  185. data/lib/extensions/paginator_extensions.rb +26 -0
  186. data/lib/extensions/reverse_associations.rb +62 -0
  187. data/lib/extensions/routing_mapper.rb +34 -0
  188. data/lib/extensions/to_label.rb +8 -0
  189. data/lib/extensions/unsaved_associated.rb +61 -0
  190. data/lib/extensions/unsaved_record.rb +20 -0
  191. data/lib/extensions/usa_state.rb +46 -0
  192. data/lib/generators/active_scaffold/USAGE +29 -0
  193. data/lib/generators/active_scaffold/active_scaffold_generator.rb +20 -0
  194. data/lib/generators/active_scaffold_controller/USAGE +19 -0
  195. data/lib/generators/active_scaffold_controller/active_scaffold_controller_generator.rb +28 -0
  196. data/lib/generators/active_scaffold_controller/templates/controller.rb +4 -0
  197. data/lib/generators/active_scaffold_setup/USAGE +10 -0
  198. data/lib/generators/active_scaffold_setup/active_scaffold_setup_generator.rb +53 -0
  199. data/lib/paginator.rb +136 -0
  200. data/lib/responds_to_parent.rb +70 -0
  201. data/public/blank.html +33 -0
  202. data/shoulda_macros/macros.rb +136 -0
  203. data/test/bridges/bridge_test.rb +47 -0
  204. data/test/config/base_test.rb +15 -0
  205. data/test/config/create_test.rb +55 -0
  206. data/test/config/list_test.rb +74 -0
  207. data/test/config/show_test.rb +43 -0
  208. data/test/config/update_test.rb +17 -0
  209. data/test/const_mocker.rb +36 -0
  210. data/test/data_structures/action_columns_test.rb +113 -0
  211. data/test/data_structures/action_link_test.rb +78 -0
  212. data/test/data_structures/action_links_test.rb +78 -0
  213. data/test/data_structures/actions_test.rb +25 -0
  214. data/test/data_structures/association_column_test.rb +42 -0
  215. data/test/data_structures/column_test.rb +185 -0
  216. data/test/data_structures/columns_test.rb +69 -0
  217. data/test/data_structures/error_message_test.rb +28 -0
  218. data/test/data_structures/set_test.rb +86 -0
  219. data/test/data_structures/sorting_test.rb +126 -0
  220. data/test/data_structures/standard_column_test.rb +24 -0
  221. data/test/data_structures/virtual_column_test.rb +23 -0
  222. data/test/extensions/active_record_test.rb +45 -0
  223. data/test/extensions/array_test.rb +12 -0
  224. data/test/helpers/form_column_helpers_test.rb +31 -0
  225. data/test/helpers/list_column_helpers_test.rb +31 -0
  226. data/test/helpers/pagination_helpers_test.rb +55 -0
  227. data/test/misc/active_record_permissions_test.rb +154 -0
  228. data/test/misc/attribute_params_test.rb +110 -0
  229. data/test/misc/configurable_test.rb +96 -0
  230. data/test/misc/constraints_test.rb +193 -0
  231. data/test/misc/finder_test.rb +93 -0
  232. data/test/misc/lang_test.rb +12 -0
  233. data/test/mock_app/.gitignore +2 -0
  234. data/test/mock_app/app/controllers/application_controller.rb +10 -0
  235. data/test/mock_app/app/helpers/application_helper.rb +3 -0
  236. data/test/mock_app/config/boot.rb +110 -0
  237. data/test/mock_app/config/database.yml +16 -0
  238. data/test/mock_app/config/environment.rb +43 -0
  239. data/test/mock_app/config/environments/development.rb +17 -0
  240. data/test/mock_app/config/environments/production.rb +28 -0
  241. data/test/mock_app/config/environments/test.rb +28 -0
  242. data/test/mock_app/config/initializers/backtrace_silencers.rb +7 -0
  243. data/test/mock_app/config/initializers/inflections.rb +10 -0
  244. data/test/mock_app/config/initializers/mime_types.rb +5 -0
  245. data/test/mock_app/config/initializers/new_rails_defaults.rb +19 -0
  246. data/test/mock_app/config/initializers/session_store.rb +15 -0
  247. data/test/mock_app/config/locales/en.yml +5 -0
  248. data/test/mock_app/config/routes.rb +43 -0
  249. data/test/mock_app/db/test.sqlite3 +1 -0
  250. data/test/mock_app/public/blank.html +33 -0
  251. data/test/mock_app/public/images/active_scaffold/DO_NOT_EDIT +2 -0
  252. data/test/mock_app/public/images/active_scaffold/default/add.gif +0 -0
  253. data/test/mock_app/public/images/active_scaffold/default/arrow_down.gif +0 -0
  254. data/test/mock_app/public/images/active_scaffold/default/arrow_up.gif +0 -0
  255. data/test/mock_app/public/images/active_scaffold/default/close.gif +0 -0
  256. data/test/mock_app/public/images/active_scaffold/default/cross.png +0 -0
  257. data/test/mock_app/public/images/active_scaffold/default/indicator-small.gif +0 -0
  258. data/test/mock_app/public/images/active_scaffold/default/indicator.gif +0 -0
  259. data/test/mock_app/public/images/active_scaffold/default/magnifier.png +0 -0
  260. data/test/mock_app/public/javascripts/active_scaffold/DO_NOT_EDIT +2 -0
  261. data/test/mock_app/public/javascripts/active_scaffold/default/active_scaffold.js +532 -0
  262. data/test/mock_app/public/javascripts/active_scaffold/default/dhtml_history.js +867 -0
  263. data/test/mock_app/public/javascripts/active_scaffold/default/form_enhancements.js +117 -0
  264. data/test/mock_app/public/javascripts/active_scaffold/default/rico_corner.js +370 -0
  265. data/test/mock_app/public/stylesheets/active_scaffold/DO_NOT_EDIT +2 -0
  266. data/test/mock_app/public/stylesheets/active_scaffold/default/stylesheet-ie.css +35 -0
  267. data/test/mock_app/public/stylesheets/active_scaffold/default/stylesheet.css +839 -0
  268. data/test/model_stub.rb +55 -0
  269. data/test/run_all.rb +8 -0
  270. data/test/test_helper.rb +39 -0
  271. data/uninstall.rb +13 -0
  272. metadata +478 -0
@@ -0,0 +1,75 @@
1
+ module ActiveScaffold::DataStructures
2
+ class Columns
3
+ include Enumerable
4
+ include ActiveScaffold::Configurable
5
+
6
+ # The motivation for this collection is that this Columns data structure fills two roles: it provides
7
+ # the master list of all known columns, and it provides an inheritable list for all other actions (e.g.
8
+ # Create and Update and List). Well we actually want to *know* about as many columns as possible, so
9
+ # we don't want people actually removing columns from @set. But at the same time, we want to be able to
10
+ # manage which columns get inherited. Tada!
11
+ #
12
+ # This collection is referenced by other parts of ActiveScaffold and by methods within this DataStructure.
13
+ # IT IS NOT MEANT FOR PUBLIC USE (but if you know what you're doing, go ahead)
14
+ def _inheritable=(value)
15
+ @sorted = true
16
+ @_inheritable = value
17
+ end
18
+
19
+ # This accessor is used by ActionColumns to create new Column objects without adding them to this set
20
+ attr_reader :active_record_class
21
+
22
+ def initialize(active_record_class, *args)
23
+ @active_record_class = active_record_class
24
+ @_inheritable = []
25
+ @set = []
26
+
27
+ self.add *args
28
+ end
29
+
30
+ # the way to add columns to the set. this is primarily useful for virtual columns.
31
+ # note that this also makes columns inheritable
32
+ def add(*args)
33
+ args.flatten! # allow [] as a param
34
+ args = args.collect{ |a| a.to_sym }
35
+
36
+ # make the columns inheritable
37
+ @_inheritable.concat(args)
38
+ # then add columns to @set (unless they already exist)
39
+ args.each { |a| @set << ActiveScaffold::DataStructures::Column.new(a.to_sym, @active_record_class) unless find_by_name(a) }
40
+ end
41
+ alias_method :<<, :add
42
+
43
+ def exclude(*args)
44
+ # only remove columns from _inheritable. we never want to completely forget about a column.
45
+ args.each { |a| @_inheritable.delete a }
46
+ end
47
+
48
+ # returns an array of columns with the provided names
49
+ def find_by_names(*names)
50
+ @set.find_all { |column| names.include? column.name }
51
+ end
52
+
53
+ # returns the column of the given name.
54
+ def find_by_name(name)
55
+ # this works because of `def column.=='
56
+ column = @set.find { |c| c == name }
57
+ column
58
+ end
59
+ alias_method :[], :find_by_name
60
+
61
+ def each
62
+ @set.each {|i| yield i }
63
+ end
64
+
65
+ def _inheritable
66
+ if @sorted
67
+ @_inheritable
68
+ else
69
+ @_inheritable.sort do |a, b|
70
+ self[a] <=> self[b]
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,24 @@
1
+ module ActiveScaffold::DataStructures
2
+ # Wrapper for error strings so that they may be exported using to_xxx
3
+ class ErrorMessage
4
+ def initialize(error)
5
+ @error = error
6
+ end
7
+
8
+ def public_attributes
9
+ { :error => @error }
10
+ end
11
+
12
+ def to_xml
13
+ public_attributes.to_xml(:root => "errors")
14
+ end
15
+
16
+ def to_yaml
17
+ public_attributes.to_yaml
18
+ end
19
+
20
+ def to_s
21
+ @error
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,108 @@
1
+ module ActiveScaffold::DataStructures
2
+ class NestedInfo
3
+ def self.get(model, session_storage)
4
+ if session_storage[:nested].nil?
5
+ nil
6
+ else
7
+ session_info = session_storage[:nested].clone
8
+ session_info[:association] = session_info[:parent_model].reflect_on_association(session_info[:name])
9
+ unless session_info[:association].nil?
10
+ ActiveScaffold::DataStructures::NestedInfoAssociation.new(model, session_info)
11
+ else
12
+ ActiveScaffold::DataStructures::NestedInfoScope.new(model, session_info)
13
+ end
14
+ end
15
+ end
16
+
17
+ attr_accessor :association, :child_association, :parent_model, :parent_id, :constrained_fields, :scope
18
+
19
+ def initialize(model, session_info)
20
+ @parent_model = session_info[:parent_model]
21
+ @parent_id = session_info[:parent_id]
22
+ end
23
+
24
+ def new_instance?
25
+ result = @new_instance.nil?
26
+ @new_instance = false
27
+ result
28
+ end
29
+
30
+ def parent_scope
31
+ parent_model.find(parent_id)
32
+ end
33
+
34
+ def habtm?
35
+ false
36
+ end
37
+
38
+ def belongs_to?
39
+ false
40
+ end
41
+
42
+ def readonly?
43
+ false
44
+ end
45
+
46
+ def sorted?
47
+ false
48
+ end
49
+ end
50
+
51
+ class NestedInfoAssociation < NestedInfo
52
+ def initialize(model, session_info)
53
+ super(model, session_info)
54
+ @association = session_info[:association]
55
+ iterate_model_associations(model)
56
+ end
57
+
58
+ def habtm?
59
+ association.macro == :has_and_belongs_to_many
60
+ end
61
+
62
+ def belongs_to?
63
+ association.belongs_to?
64
+ end
65
+
66
+ def readonly?
67
+ if association.options.has_key? :readonly
68
+ association.options[:readonly]
69
+ else
70
+ association.options.has_key? :through
71
+ end
72
+ end
73
+
74
+ def sorted?
75
+ association.options.has_key? :order
76
+ end
77
+
78
+ def default_sorting
79
+ association.options[:order]
80
+ end
81
+
82
+ protected
83
+
84
+ def iterate_model_associations(model)
85
+ @constrained_fields = []
86
+ @constrained_fields << association.primary_key_name.to_sym unless association.belongs_to?
87
+ model.reflect_on_all_associations.each do |current|
88
+ if !current.belongs_to? && association.primary_key_name == current.association_foreign_key
89
+ constrained_fields << current.name.to_sym
90
+ @child_association = current
91
+ end
92
+ if association.primary_key_name == current.primary_key_name
93
+ # show columns for has_many and has_one child associationes
94
+ constrained_fields << current.name.to_sym if current.belongs_to?
95
+ @child_association = current
96
+ end
97
+ end
98
+ end
99
+ end
100
+
101
+ class NestedInfoScope < NestedInfo
102
+ def initialize(model, session_info)
103
+ super(model, session_info)
104
+ @scope = session_info[:name]
105
+ @constrained_fields = []
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,62 @@
1
+ module ActiveScaffold::DataStructures
2
+ class Set
3
+ include Enumerable
4
+ include ActiveScaffold::Configurable
5
+
6
+ attr_writer :label
7
+ def label
8
+ as_(@label)
9
+ end
10
+
11
+ def initialize(*args)
12
+ @set = []
13
+ self.add *args
14
+ end
15
+
16
+ # the way to add items to the set.
17
+ def add(*args)
18
+ args.flatten! # allow [] as a param
19
+ args.each { |arg|
20
+ arg = arg.to_sym if arg.is_a? String
21
+ @set << arg unless @set.include? arg # avoid duplicates
22
+ }
23
+ end
24
+ alias_method :<<, :add
25
+
26
+ # the way to remove items from the set.
27
+ def exclude(*args)
28
+ args.flatten! # allow [] as a param
29
+ args.collect! { |a| a.to_sym } # symbolize the args
30
+ # check respond_to? :to_sym, ActionColumns doesn't respond to to_sym
31
+ @set.reject! { |c| c.respond_to? :to_sym and args.include? c.to_sym } # reject all items specified
32
+ end
33
+ alias_method :remove, :exclude
34
+
35
+ # returns an array of items with the provided names
36
+ def find_by_names(*names)
37
+ @set.find_all { |item| names.include? item }
38
+ end
39
+
40
+ # returns the item of the given name.
41
+ def find_by_name(name)
42
+ # this works because of `def item.=='
43
+ item = @set.find { |c| c == name }
44
+ item
45
+ end
46
+ alias_method :[], :find_by_name
47
+
48
+ def each
49
+ @set.each {|i| yield i }
50
+ end
51
+
52
+ # returns the number of items in the set
53
+ def length
54
+ @set.length
55
+ end
56
+
57
+ def empty?
58
+ @set.empty?
59
+ end
60
+
61
+ end
62
+ end
@@ -0,0 +1,168 @@
1
+ module ActiveScaffold::DataStructures
2
+ # encapsulates the column sorting configuration for the List view
3
+ class Sorting
4
+ include Enumerable
5
+
6
+ def initialize(columns)
7
+ @columns = columns
8
+ @clauses = []
9
+ end
10
+
11
+ def set_default_sorting(model)
12
+ model_scope = model.send(:current_scoped_methods)
13
+ order_clause = model_scope.arel.order_clauses.join(",") if model_scope
14
+
15
+ # If an ORDER BY clause is found set default sorting according to it, else
16
+ # fallback to setting primary key ordering
17
+ if order_clause
18
+ set_sorting_from_order_clause(order_clause, model.table_name)
19
+ @default_sorting = true
20
+ else
21
+ set(model.primary_key, 'ASC') if model.column_names.include?(model.primary_key)
22
+ end
23
+ end
24
+
25
+ def set_nested_sorting(table_name, order_clause)
26
+ clear
27
+ set_sorting_from_order_clause(order_clause, table_name)
28
+ end
29
+
30
+ # add a clause to the sorting, assuming the column is sortable
31
+ def add(column_name, direction = nil)
32
+ direction ||= 'ASC'
33
+ direction = direction.to_s.upcase
34
+ column = get_column(column_name)
35
+ raise ArgumentError, "Could not find column #{column_name}" if column.nil?
36
+ raise ArgumentError, "Sorting direction unknown" unless [:ASC, :DESC].include? direction.to_sym
37
+ @clauses << [column, direction] if column.sortable?
38
+ raise ArgumentError, "Can't mix :method- and :sql-based sorting" if mixed_sorting?
39
+ end
40
+
41
+ # an alias for +add+. must accept its arguments in a slightly different form, though.
42
+ def <<(arg)
43
+ add(*arg)
44
+ end
45
+
46
+ # clears the sorting before setting to the given column/direction
47
+ def set(*args)
48
+ clear
49
+ add(*args)
50
+ end
51
+
52
+ # clears the sorting
53
+ def clear
54
+ @default_sorting = false
55
+ @clauses = []
56
+ end
57
+
58
+ # checks whether the given column (a Column object or a column name) is in the sorting
59
+ def sorts_on?(column)
60
+ !get_clause(column).nil?
61
+ end
62
+
63
+ def direction_of(column)
64
+ clause = get_clause(column)
65
+ return if clause.nil?
66
+ clause[1]
67
+ end
68
+
69
+ # checks whether any column is configured to sort by method (using a proc)
70
+ def sorts_by_method?
71
+ @clauses.any? { |sorting| sorting[0].sort.is_a? Hash and sorting[0].sort.has_key? :method }
72
+ end
73
+
74
+ def sorts_by_sql?
75
+ @clauses.any? { |sorting| sorting[0].sort.is_a? Hash and sorting[0].sort.has_key? :sql }
76
+ end
77
+
78
+ # iterate over the clauses
79
+ def each
80
+ @clauses.each { |clause| yield clause }
81
+ end
82
+
83
+ # provides quick access to the first (and sometimes only) clause
84
+ def first
85
+ @clauses.first
86
+ end
87
+
88
+ # builds an order-by clause
89
+ def clause
90
+ return nil if sorts_by_method? || default_sorting?
91
+
92
+ # unless the sorting is by method, create the sql string
93
+ order = []
94
+ each do |sort_column, sort_direction|
95
+ sql = sort_column.sort[:sql]
96
+ next if sql.nil? or sql.empty?
97
+
98
+ order << "#{sql} #{sort_direction}"
99
+ end
100
+
101
+ order.join(', ') unless order.empty?
102
+ end
103
+
104
+ protected
105
+
106
+ # retrieves the sorting clause for the given column
107
+ def get_clause(column)
108
+ column = get_column(column)
109
+ @clauses.find{ |clause| clause[0] == column}
110
+ end
111
+
112
+ # possibly converts the given argument into a column object from @columns (if it's not already)
113
+ def get_column(name_or_column)
114
+ # it's a column
115
+ return name_or_column if name_or_column.is_a? ActiveScaffold::DataStructures::Column
116
+ # it's a name
117
+ name_or_column = name_or_column.to_s.split('.').last if name_or_column.to_s.include? '.'
118
+ return @columns[name_or_column]
119
+ end
120
+
121
+ def mixed_sorting?
122
+ sorts_by_method? and sorts_by_sql?
123
+ end
124
+
125
+ def default_sorting?
126
+ @default_sorting
127
+ end
128
+
129
+ def set_sorting_from_order_clause(order_clause, model_table_name = nil)
130
+ clear
131
+ order_clause.split(',').each do |criterion|
132
+ unless criterion.blank?
133
+ order_parts = extract_order_parts(criterion)
134
+ add(order_parts[:column_name], order_parts[:direction]) unless different_table?(model_table_name, order_parts[:table_name])
135
+ end
136
+ end
137
+ end
138
+
139
+ def extract_order_parts(criterion_parts)
140
+ column_name_part, direction_part = criterion_parts.strip.split(' ')
141
+ column_name_parts = column_name_part.split('.')
142
+ order = {:direction => extract_direction(direction_part),
143
+ :column_name => remove_quotes(column_name_parts.last)}
144
+ order[:table_name] = remove_quotes(column_name_parts[-2]) if column_name_parts.length >= 2
145
+ order
146
+ end
147
+
148
+ def different_table?(model_table_name, order_table_name)
149
+ !order_table_name.nil? && model_table_name != order_table_name
150
+ end
151
+
152
+ def remove_quotes(sql_name)
153
+ if sql_name.starts_with?('"') || sql_name.starts_with?('`')
154
+ sql_name[1, (sql_name.length - 2)]
155
+ else
156
+ sql_name
157
+ end
158
+ end
159
+
160
+ def extract_direction(direction_part)
161
+ if direction_part.to_s.upcase == 'DESC'
162
+ 'DESC'
163
+ else
164
+ 'ASC'
165
+ end
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,333 @@
1
+ module ActiveScaffold
2
+ module Finder
3
+ module ClassMethods
4
+ # Takes a collection of search terms (the tokens) and creates SQL that
5
+ # searches all specified ActiveScaffold columns. A row will match if each
6
+ # token is found in at least one of the columns.
7
+ def create_conditions_for_columns(tokens, columns, text_search = :full)
8
+ # if there aren't any columns, then just return a nil condition
9
+ return unless columns.length > 0
10
+ like_pattern = like_pattern(text_search)
11
+
12
+ tokens = [tokens] if tokens.is_a? String
13
+
14
+ where_clauses = []
15
+ columns.each do |column|
16
+ where_clauses << ((column.column.nil? || column.column.text?) ? "LOWER(#{column.search_sql}) LIKE ?" : "#{column.search_sql} = ?")
17
+ end
18
+ phrase = "(#{where_clauses.join(' OR ')})"
19
+
20
+ sql = ([phrase] * tokens.length).join(' AND ')
21
+ tokens = tokens.collect do |value|
22
+ columns.collect {|column| (column.column.nil? || column.column.text?) ? like_pattern.sub('?', value.downcase) : column.column.type_cast(value)}
23
+ end.flatten
24
+
25
+ [sql, *tokens]
26
+ end
27
+
28
+ # Generates an SQL condition for the given ActiveScaffold column based on
29
+ # that column's database type (or form_ui ... for virtual columns?).
30
+ # TODO: this should reside on the column, not the controller
31
+ def condition_for_column(column, value, text_search = :full)
32
+ like_pattern = like_pattern(text_search)
33
+ return unless column and column.search_sql and not value.blank?
34
+ search_ui = column.search_ui || column.column.type
35
+ begin
36
+ if self.respond_to?("condition_for_#{column.name}_column")
37
+ self.send("condition_for_#{column.name}_column", column, value, like_pattern)
38
+ elsif self.respond_to?("condition_for_#{search_ui}_type")
39
+ self.send("condition_for_#{search_ui}_type", column, value, like_pattern)
40
+ else
41
+ unless column.search_sql.instance_of? Proc
42
+ case search_ui
43
+ when :boolean, :checkbox
44
+ ["#{column.search_sql} = ?", column.column.type_cast(value)]
45
+ when :integer, :decimal, :float
46
+ condition_for_numeric(column, value)
47
+ when :string, :range
48
+ condition_for_range(column, value, like_pattern)
49
+ when :date, :time, :datetime, :timestamp
50
+ condition_for_datetime(column, value)
51
+ when :select, :multi_select, :country, :usa_state
52
+ ["#{column.search_sql} in (?)", Array(value)]
53
+ else
54
+ if column.column.nil? || column.column.text?
55
+ ["LOWER(#{column.search_sql}) LIKE ?", like_pattern.sub('?', value.downcase)]
56
+ else
57
+ ["#{column.search_sql} = ?", column.column.type_cast(value)]
58
+ end
59
+ end
60
+ else
61
+ column.search_sql.call(value)
62
+ end
63
+ end
64
+ rescue Exception => e
65
+ logger.error Time.now.to_s + "#{e.inspect} -- on the ActiveScaffold column :#{column.name}, search_ui = #{search_ui} in #{@controller.class}"
66
+ raise e
67
+ end
68
+ end
69
+
70
+ def condition_for_numeric(column, value)
71
+ if !value.is_a?(Hash)
72
+ ["#{column.search_sql} = ?", condition_value_for_numeric(column, value)]
73
+ elsif value[:from].blank? or not ActiveScaffold::Finder::NumericComparators.include?(value[:opt])
74
+ nil
75
+ elsif value[:opt] == 'BETWEEN'
76
+ ["#{column.search_sql} BETWEEN ? AND ?", condition_value_for_numeric(column, value[:from]), condition_value_for_numeric(column, value[:to])]
77
+ else
78
+ ["#{column.search_sql} #{value[:opt]} ?", condition_value_for_numeric(column, value[:from])]
79
+ end
80
+ end
81
+
82
+ def condition_for_range(column, value, like_pattern = nil)
83
+ if !value.is_a?(Hash)
84
+ if column.column.nil? || column.column.text?
85
+ ["LOWER(#{column.search_sql}) LIKE ?", like_pattern.sub('?', value.downcase)]
86
+ else
87
+ ["#{column.search_sql} = ?", column.column.type_cast(value)]
88
+ end
89
+ elsif value[:from].blank?
90
+ nil
91
+ elsif ActiveScaffold::Finder::StringComparators.values.include?(value[:opt])
92
+ ["#{column.search_sql} LIKE ?", value[:opt].sub('?', value[:from])]
93
+ elsif value[:opt] == 'BETWEEN'
94
+ ["#{column.search_sql} BETWEEN ? AND ?", value[:from], value[:to]]
95
+ elsif ActiveScaffold::Finder::NumericComparators.include?(value[:opt])
96
+ ["#{column.search_sql} #{value[:opt]} ?", value[:from]]
97
+ else
98
+ nil
99
+ end
100
+ end
101
+
102
+ def condition_value_for_datetime(value, conversion = :to_time)
103
+ if value.is_a? Hash
104
+ Time.zone.local(*[:year, :month, :day, :hour, :minute, :second].collect {|part| value[field][part].to_i}) rescue nil
105
+ elsif value.respond_to?(:strftime)
106
+ value.send(conversion)
107
+ else
108
+ Time.zone.parse(value).in_time_zone.send(conversion) rescue nil
109
+ end unless value.nil? || value.blank?
110
+ end
111
+
112
+ def condition_value_for_numeric(column, value)
113
+ return value if value.nil?
114
+ value = i18n_number_to_native_format(value) if [:i18n_number, :currency].include?(column.options[:format])
115
+ case (column.search_ui || column.column.type)
116
+ when :integer then value.to_i rescue value ? 1 : 0
117
+ when :float then value.to_f
118
+ when :decimal then ActiveRecord::ConnectionAdapters::Column.value_to_decimal(value)
119
+ else
120
+ value
121
+ end
122
+ end
123
+
124
+ def i18n_number_to_native_format(value)
125
+ native = '.'
126
+ delimiter = I18n.t('number.format.delimiter')
127
+ separator = I18n.t('number.format.separator')
128
+
129
+ unless delimiter == native && !value.include?(separator) && value !~ /\.\d{3}$/
130
+ value.gsub(/[^0-9\-#{I18n.t('number.format.separator')}]/, '').gsub(I18n.t('number.format.separator'), native)
131
+ else
132
+ value
133
+ end
134
+ end
135
+
136
+ def condition_for_datetime(column, value, like_pattern = nil)
137
+ conversion = column.column.type == :date ? :to_date : :to_time
138
+ from_value = condition_value_for_datetime(value[:from], conversion)
139
+ to_value = condition_value_for_datetime(value[:to], conversion)
140
+
141
+ if from_value.nil? and to_value.nil?
142
+ nil
143
+ elsif !from_value
144
+ ["#{column.search_sql} <= ?", to_value.to_s(:db)]
145
+ elsif !to_value
146
+ ["#{column.search_sql} >= ?", from_value.to_s(:db)]
147
+ else
148
+ ["#{column.search_sql} BETWEEN ? AND ?", from_value.to_s(:db), to_value.to_s(:db)]
149
+ end
150
+ end
151
+
152
+ def condition_for_record_select_type(column, value, like_pattern = nil)
153
+ if value.is_a?(Array)
154
+ ["#{column.search_sql} IN (?)", value]
155
+ else
156
+ ["#{column.search_sql} = ?", value]
157
+ end
158
+ end
159
+
160
+ def condition_for_null_type(column, value, like_pattern = nil)
161
+ case value.to_sym
162
+ when :null
163
+ ["#{column.search_sql} is null"]
164
+ when :not_null
165
+ ["#{column.search_sql} is not null"]
166
+ else
167
+ nil
168
+ end
169
+ end
170
+
171
+ def like_pattern(text_search)
172
+ case text_search
173
+ when :full then '%?%'
174
+ when :start then '?%'
175
+ when :end then '%?'
176
+ else '?'
177
+ end
178
+ end
179
+ end
180
+
181
+ NumericComparators = [
182
+ '=',
183
+ '>=',
184
+ '<=',
185
+ '>',
186
+ '<',
187
+ '!=',
188
+ 'BETWEEN'
189
+ ]
190
+ StringComparators = {
191
+ :contains => '%?%',
192
+ :begins_with => '?%',
193
+ :ends_with => '%?'
194
+ }
195
+ NullComparators = [
196
+ :null,
197
+ :not_null
198
+ ]
199
+
200
+
201
+
202
+ def self.included(klass)
203
+ klass.extend ClassMethods
204
+ end
205
+
206
+ protected
207
+
208
+ attr_writer :active_scaffold_conditions
209
+ def active_scaffold_conditions
210
+ @active_scaffold_conditions ||= []
211
+ end
212
+
213
+ attr_writer :active_scaffold_includes
214
+ def active_scaffold_includes
215
+ @active_scaffold_includes ||= []
216
+ end
217
+
218
+ attr_writer :active_scaffold_habtm_joins
219
+ def active_scaffold_habtm_joins
220
+ @active_scaffold_habtm_joins ||= []
221
+ end
222
+
223
+ def all_conditions
224
+ merge_conditions(
225
+ active_scaffold_conditions, # from the search modules
226
+ conditions_for_collection, # from the dev
227
+ conditions_from_params, # from the parameters (e.g. /users/list?first_name=Fred)
228
+ conditions_from_constraints, # from any constraints (embedded scaffolds)
229
+ active_scaffold_session_storage[:conditions] # embedding conditions (weaker constraints)
230
+ )
231
+ end
232
+
233
+ # returns a single record (the given id) but only if it's allowed for the specified action.
234
+ # accomplishes this by checking model.#{action}_authorized?
235
+ # TODO: this should reside on the model, not the controller
236
+ def find_if_allowed(id, crud_type, klass = beginning_of_chain)
237
+ record = klass.find(id)
238
+ raise ActiveScaffold::RecordNotAllowed, "#{klass} with id = #{id}" unless record.authorized_for?(:crud_type => crud_type.to_sym)
239
+ return record
240
+ end
241
+
242
+ # returns a Paginator::Page (not from ActiveRecord::Paginator) for the given parameters
243
+ # options may include:
244
+ # * :sorting - a Sorting DataStructure (basically an array of hashes of field => direction, e.g. [{:field1 => 'asc'}, {:field2 => 'desc'}]). please note that multi-column sorting has some limitations: if any column in a multi-field sort uses method-based sorting, it will be ignored. method sorting only works for single-column sorting.
245
+ # * :per_page
246
+ # * :page
247
+ # TODO: this should reside on the model, not the controller
248
+ def find_page(options = {})
249
+ options.assert_valid_keys :sorting, :per_page, :page, :count_includes, :pagination
250
+
251
+ search_conditions = all_conditions
252
+ full_includes = (active_scaffold_includes.blank? ? nil : active_scaffold_includes)
253
+ options[:per_page] ||= 999999999
254
+ options[:page] ||= 1
255
+ options[:count_includes] ||= full_includes unless search_conditions.nil?
256
+
257
+ klass = beginning_of_chain
258
+
259
+ # create a general-use options array that's compatible with Rails finders
260
+ finder_options = { :order => options[:sorting].try(:clause),
261
+ :where => search_conditions,
262
+ :joins => joins_for_finder,
263
+ :includes => options[:count_includes]}
264
+
265
+ finder_options.merge! custom_finder_options
266
+
267
+ # NOTE: we must use :include in the count query, because some conditions may reference other tables
268
+ count_query = append_to_query(klass, finder_options.reject{|k, v| [:select, :order].include?(k)})
269
+ count = count_query.count unless options[:pagination] == :infinite
270
+
271
+ # Converts count to an integer if ActiveRecord returned an OrderedHash
272
+ # that happens when finder_options contains a :group key
273
+ count = count.length if count.is_a? ActiveSupport::OrderedHash
274
+ finder_options.merge! :includes => full_includes
275
+
276
+ # we build the paginator differently for method- and sql-based sorting
277
+ if options[:sorting] and options[:sorting].sorts_by_method?
278
+ pager = ::Paginator.new(count, options[:per_page]) do |offset, per_page|
279
+ sorted_collection = sort_collection_by_column(append_to_query(klass, finder_options).all, *options[:sorting].first)
280
+ sorted_collection = sorted_collection.slice(offset, per_page) if options[:pagination]
281
+ sorted_collection
282
+ end
283
+ else
284
+ pager = ::Paginator.new(count, options[:per_page]) do |offset, per_page|
285
+ finder_options.merge!(:offset => offset, :limit => per_page) if options[:pagination]
286
+ append_to_query(klass, finder_options).all
287
+ end
288
+ end
289
+ pager.page(options[:page])
290
+ end
291
+
292
+ def append_to_query(query, options)
293
+ options.assert_valid_keys :where, :select, :group, :order, :limit, :offset, :joins, :includes, :lock, :readonly, :from
294
+ options.reject{|k, v| v.blank?}.inject(query) do |query, (k, v)|
295
+ query.send((k.to_sym), v)
296
+ end
297
+ end
298
+
299
+ def joins_for_finder
300
+ case joins_for_collection
301
+ when String
302
+ [ joins_for_collection ]
303
+ when Array
304
+ joins_for_collection
305
+ else
306
+ []
307
+ end + active_scaffold_habtm_joins
308
+ end
309
+
310
+ def merge_conditions(*conditions)
311
+ segments = []
312
+ conditions.each do |condition|
313
+ unless condition.blank?
314
+ sql = active_scaffold_config.model.send(:sanitize_sql, condition)
315
+ segments << sql unless sql.blank?
316
+ end
317
+ end
318
+ "(#{segments.join(') AND (')})" unless segments.empty?
319
+ end
320
+
321
+ # TODO: this should reside on the column, not the controller
322
+ def sort_collection_by_column(collection, column, order)
323
+ sorter = column.sort[:method]
324
+ collection = collection.sort_by { |record|
325
+ value = (sorter.is_a? Proc) ? record.instance_eval(&sorter) : record.instance_eval(sorter)
326
+ value = '' if value.nil?
327
+ value
328
+ }
329
+ collection.reverse! if order.downcase == 'desc'
330
+ collection
331
+ end
332
+ end
333
+ end