fat_free_crm 0.18.2 → 0.19.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of fat_free_crm might be problematic. Click here for more details.

Files changed (251) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +61 -160
  3. data/.travis.yml +27 -11
  4. data/CHANGELOG.md +40 -24
  5. data/CONTRIBUTORS.md +1 -0
  6. data/Dockerfile +45 -14
  7. data/Gemfile +16 -10
  8. data/Gemfile.lock +230 -222
  9. data/Procfile +1 -1
  10. data/README.md +2 -2
  11. data/Rakefile +1 -1
  12. data/app/assets/stylesheets/common.scss +1 -1
  13. data/app/controllers/admin/application_controller.rb +1 -1
  14. data/app/controllers/admin/field_groups_controller.rb +1 -3
  15. data/app/controllers/admin/tags_controller.rb +1 -3
  16. data/app/controllers/admin/users_controller.rb +5 -8
  17. data/app/controllers/application_controller.rb +11 -45
  18. data/app/controllers/comments_controller.rb +2 -5
  19. data/{config/initializers/authlogic.rb → app/controllers/confirmations_controller.rb} +4 -2
  20. data/app/controllers/emails_controller.rb +0 -2
  21. data/app/controllers/entities/accounts_controller.rb +1 -3
  22. data/app/controllers/entities/campaigns_controller.rb +1 -3
  23. data/app/controllers/entities/contacts_controller.rb +4 -24
  24. data/app/controllers/entities/leads_controller.rb +7 -10
  25. data/app/controllers/entities/opportunities_controller.rb +4 -14
  26. data/app/controllers/entities_controller.rb +21 -7
  27. data/app/controllers/home_controller.rb +2 -4
  28. data/app/controllers/passwords_controller.rb +3 -59
  29. data/{spec/features/support/maintain_sessions.rb → app/controllers/registrations_controller.rb} +12 -5
  30. data/{lib/development_tasks/gem.rake → app/controllers/sessions_controller.rb} +6 -6
  31. data/app/controllers/tasks_controller.rb +8 -17
  32. data/app/controllers/users_controller.rb +8 -29
  33. data/app/helpers/admin/users_helper.rb +1 -1
  34. data/app/helpers/application_helper.rb +27 -32
  35. data/app/helpers/campaigns_helper.rb +1 -1
  36. data/app/helpers/contacts_helper.rb +1 -3
  37. data/app/helpers/opportunities_helper.rb +4 -12
  38. data/app/helpers/tasks_helper.rb +1 -1
  39. data/app/helpers/users_helper.rb +1 -3
  40. data/{config/initializers/paper_trail.rb → app/mailers/devise_mailer.rb} +5 -1
  41. data/app/mailers/user_mailer.rb +0 -9
  42. data/app/models/entities/account.rb +10 -10
  43. data/app/models/entities/campaign.rb +4 -6
  44. data/app/models/entities/contact.rb +24 -12
  45. data/app/models/entities/lead.rb +7 -7
  46. data/app/models/entities/opportunity.rb +7 -9
  47. data/app/models/fields/custom_field.rb +1 -0
  48. data/app/models/fields/custom_field_date_pair.rb +2 -0
  49. data/app/models/fields/field.rb +1 -3
  50. data/app/models/list.rb +1 -1
  51. data/app/models/observers/entity_observer.rb +3 -7
  52. data/app/models/observers/lead_observer.rb +2 -4
  53. data/app/models/observers/opportunity_observer.rb +2 -4
  54. data/app/models/observers/task_observer.rb +1 -1
  55. data/app/models/polymorphic/email.rb +2 -2
  56. data/app/models/polymorphic/task.rb +13 -9
  57. data/app/models/polymorphic/version.rb +3 -2
  58. data/app/models/setting.rb +2 -0
  59. data/app/models/users/permission.rb +3 -3
  60. data/app/models/users/preference.rb +2 -1
  61. data/app/models/users/user.rb +67 -42
  62. data/app/views/accounts/_top_section.html.haml +1 -1
  63. data/app/views/accounts/edit.js.haml +1 -1
  64. data/app/views/accounts/update.js.haml +2 -2
  65. data/app/views/admin/users/_user.html.haml +4 -4
  66. data/app/views/contacts/_index_brief.html.haml +1 -1
  67. data/app/views/contacts/_index_full.html.haml +1 -1
  68. data/app/views/contacts/_index_long.html.haml +1 -1
  69. data/app/views/devise/confirmations/new.html.haml +9 -0
  70. data/app/views/devise/mailer/confirmation_instructions.html.haml +4 -0
  71. data/app/views/devise/mailer/password_change.html.haml +3 -0
  72. data/app/views/devise/mailer/reset_password_instructions.html.haml +6 -0
  73. data/app/views/devise/passwords/edit.html.haml +18 -0
  74. data/app/views/devise/passwords/new.html.haml +10 -0
  75. data/app/views/devise/registrations/new.html.haml +21 -0
  76. data/app/views/devise/sessions/new.html.haml +32 -0
  77. data/app/views/layouts/_about.html.haml +5 -5
  78. data/app/views/layouts/_header.html.haml +3 -3
  79. data/app/views/layouts/admin/_header.html.haml +1 -1
  80. data/app/views/shared/_address.html.haml +5 -5
  81. data/app/views/shared/_paginate_with_per_page.html.haml +1 -0
  82. data/app/views/users/_avatar.html.haml +1 -1
  83. data/bin/bundle +1 -1
  84. data/bin/rails +1 -1
  85. data/bin/setup +38 -0
  86. data/bin/update +33 -0
  87. data/bin/yarn +13 -0
  88. data/config/application.rb +8 -6
  89. data/config/boot.rb +1 -1
  90. data/config/brakeman.ignore +2 -2
  91. data/config/database.postgres.docker.yml +5 -5
  92. data/config/environment.rb +1 -1
  93. data/config/environments/development.rb +1 -0
  94. data/config/environments/test.rb +7 -0
  95. data/config/initializers/action_mailer.rb +1 -3
  96. data/config/initializers/application_controller_renderer.rb +9 -0
  97. data/config/initializers/assets.rb +6 -11
  98. data/config/initializers/backtrace_silencers.rb +0 -6
  99. data/config/initializers/content_security_policy.rb +26 -0
  100. data/config/initializers/cookies_serializer.rb +3 -6
  101. data/config/initializers/devise.rb +289 -0
  102. data/config/initializers/filter_parameter_logging.rb +0 -5
  103. data/config/initializers/gravatar.rb +0 -1
  104. data/config/initializers/inflections.rb +0 -6
  105. data/config/initializers/mime_types.rb +1 -9
  106. data/config/initializers/new_framework_defaults_5_2.rb +40 -0
  107. data/config/initializers/relative_url_root.rb +1 -3
  108. data/config/initializers/session_store.rb +1 -3
  109. data/config/initializers/wrap_parameters.rb +4 -9
  110. data/config/locales/fat_free_crm.en-GB.yml +5 -5
  111. data/config/locales/fat_free_crm.en-US.yml +5 -5
  112. data/config/locales/fat_free_crm.fr.yml +1 -1
  113. data/config/locales/fat_free_crm.ru.yml +1 -0
  114. data/config/routes.rb +20 -9
  115. data/db/demo/users.yml +62 -81
  116. data/db/migrate/20100928030620_remove_uuid.rb +1 -2
  117. data/db/migrate/20120316045804_activities_to_versions.rb +1 -0
  118. data/db/migrate/20120510025219_add_not_null_constraints_for_timestamp_columns.rb +1 -0
  119. data/db/migrate/20180107082701_authlogic_to_devise.rb +58 -0
  120. data/db/schema.rb +48 -43
  121. data/docker-compose.yml +10 -0
  122. data/fat_free_crm.gemspec +11 -13
  123. data/lib/development_tasks/license.rake +2 -2
  124. data/lib/fat_free_crm/callback.rb +2 -2
  125. data/lib/fat_free_crm/comment_extensions.rb +2 -4
  126. data/lib/fat_free_crm/core_ext/string.rb +1 -1
  127. data/lib/fat_free_crm/engine.rb +1 -1
  128. data/lib/fat_free_crm/errors.rb +1 -1
  129. data/lib/fat_free_crm/export_csv.rb +1 -0
  130. data/lib/fat_free_crm/exportable.rb +1 -1
  131. data/lib/fat_free_crm/fields.rb +1 -1
  132. data/lib/fat_free_crm/gem_dependencies.rb +1 -1
  133. data/lib/fat_free_crm/gem_ext/simple_form/action_view_extensions/form_helper.rb +1 -3
  134. data/lib/fat_free_crm/i18n.rb +2 -2
  135. data/lib/fat_free_crm/mail_processor/base.rb +4 -10
  136. data/lib/fat_free_crm/mail_processor/dropbox.rb +5 -15
  137. data/lib/fat_free_crm/permissions.rb +7 -4
  138. data/lib/fat_free_crm/sortable.rb +1 -1
  139. data/lib/fat_free_crm/tabs.rb +2 -2
  140. data/lib/fat_free_crm/version.rb +2 -2
  141. data/lib/gravatar_image_tag.rb +7 -8
  142. data/lib/missing_translation_detector.rb +1 -0
  143. data/lib/tasks/ffcrm/missing_translations.rake +1 -0
  144. data/lib/tasks/ffcrm/setup.rake +10 -1
  145. data/lib/tasks/ffcrm/update_data.rake +2 -2
  146. data/script/rails +2 -2
  147. data/spec/controllers/admin/users_controller_spec.rb +0 -56
  148. data/spec/controllers/comments_controller_spec.rb +6 -6
  149. data/spec/controllers/entities/campaigns_controller_spec.rb +1 -1
  150. data/spec/controllers/entities/contacts_controller_spec.rb +2 -1
  151. data/spec/controllers/entities/leads_controller_spec.rb +2 -2
  152. data/spec/controllers/entities/opportunities_controller_spec.rb +1 -1
  153. data/spec/controllers/entities_controller_spec.rb +5 -0
  154. data/spec/controllers/home_controller_spec.rb +5 -5
  155. data/spec/controllers/tasks_controller_spec.rb +6 -4
  156. data/spec/controllers/users_controller_spec.rb +28 -98
  157. data/spec/factories/account_factories.rb +5 -5
  158. data/spec/factories/campaign_factories.rb +3 -3
  159. data/spec/factories/contact_factories.rb +8 -8
  160. data/spec/factories/field_factories.rb +4 -3
  161. data/spec/factories/lead_factories.rb +5 -5
  162. data/spec/factories/list_factories.rb +2 -2
  163. data/spec/factories/opportunity_factories.rb +3 -3
  164. data/spec/factories/setting_factories.rb +2 -2
  165. data/spec/factories/shared_factories.rb +11 -9
  166. data/spec/factories/task_factories.rb +7 -7
  167. data/spec/factories/user_factories.rb +16 -19
  168. data/spec/features/admin/groups_spec.rb +1 -1
  169. data/spec/features/admin/users_spec.rb +3 -1
  170. data/spec/features/campaigns_spec.rb +1 -1
  171. data/spec/features/contacts_spec.rb +1 -1
  172. data/spec/features/dashboard_spec.rb +1 -1
  173. data/spec/features/devise/sign_in_spec.rb +58 -0
  174. data/spec/features/devise/sign_up_spec.rb +36 -0
  175. data/spec/features/leads_spec.rb +1 -1
  176. data/spec/features/opportunities_overview_spec.rb +1 -1
  177. data/spec/features/opportunities_spec.rb +3 -3
  178. data/spec/features/support/browser.rb +2 -1
  179. data/spec/features/tasks_spec.rb +1 -1
  180. data/spec/helpers/admin/field_groups_helper_spec.rb +1 -1
  181. data/spec/helpers/users_helper_spec.rb +4 -4
  182. data/spec/lib/comment_extensions_spec.rb +10 -4
  183. data/spec/lib/errors_spec.rb +2 -2
  184. data/spec/lib/mail_processor/dropbox_spec.rb +1 -1
  185. data/spec/lib/mail_processor/sample_emails/dropbox.rb +8 -8
  186. data/spec/lib/permissions_spec.rb +8 -3
  187. data/spec/mailers/devise_mailer_spec.rb +35 -0
  188. data/spec/mailers/user_mailer_spec.rb +0 -26
  189. data/spec/models/entities/account_spec.rb +27 -0
  190. data/spec/models/entities/contact_spec.rb +96 -1
  191. data/spec/models/fields/custom_field_date_pair_spec.rb +4 -2
  192. data/spec/models/fields/custom_field_spec.rb +4 -2
  193. data/spec/models/observers/entity_observer_spec.rb +1 -1
  194. data/spec/models/polymorphic/version_spec.rb +7 -7
  195. data/spec/models/users/user_spec.rb +22 -26
  196. data/spec/routing/users_routing_spec.rb +30 -8
  197. data/spec/shared/controllers.rb +3 -9
  198. data/spec/spec_helper.rb +10 -2
  199. data/spec/support/assert_select.rb +1 -0
  200. data/spec/support/devise_helpers.rb +28 -0
  201. data/spec/{features/support/helpers.rb → support/feature_helpers.rb} +10 -10
  202. data/spec/support/macros.rb +4 -1
  203. data/spec/views/contacts/update.js.haml_spec.rb +1 -1
  204. data/spec/views/opportunities/update.js.haml_spec.rb +1 -1
  205. data/vendor/gems/ransack_ui-1.3.4/.gitignore +17 -0
  206. data/vendor/gems/ransack_ui-1.3.4/Gemfile +7 -0
  207. data/vendor/gems/ransack_ui-1.3.4/LICENSE.txt +22 -0
  208. data/vendor/gems/ransack_ui-1.3.4/README.md +57 -0
  209. data/vendor/gems/ransack_ui-1.3.4/Rakefile +1 -0
  210. data/vendor/gems/ransack_ui-1.3.4/app/assets/images/ransack_ui/calendar.png +0 -0
  211. data/vendor/gems/ransack_ui-1.3.4/app/assets/images/ransack_ui/delete.png +0 -0
  212. data/vendor/gems/ransack_ui-1.3.4/app/assets/javascripts/ransack/predicates.js.coffee +41 -0
  213. data/vendor/gems/ransack_ui-1.3.4/app/assets/javascripts/ransack_ui_bootstrap/button_group_select.js.coffee +26 -0
  214. data/vendor/gems/ransack_ui-1.3.4/app/assets/javascripts/ransack_ui_bootstrap/index.js.coffee +2 -0
  215. data/vendor/gems/ransack_ui-1.3.4/app/assets/javascripts/ransack_ui_jquery/index.js +2 -0
  216. data/vendor/gems/ransack_ui-1.3.4/app/assets/javascripts/ransack_ui_jquery/search_form.js.coffee.erb +499 -0
  217. data/vendor/gems/ransack_ui-1.3.4/app/assets/stylesheets/ransack_ui_bootstrap/index.css +3 -0
  218. data/vendor/gems/ransack_ui-1.3.4/app/assets/stylesheets/ransack_ui_bootstrap/search.css.scss +41 -0
  219. data/vendor/gems/ransack_ui-1.3.4/app/views/ransack_ui/_condition_fields.html.erb +15 -0
  220. data/vendor/gems/ransack_ui-1.3.4/app/views/ransack_ui/_grouping_fields.html.erb +16 -0
  221. data/vendor/gems/ransack_ui-1.3.4/app/views/ransack_ui/_search.html.erb +29 -0
  222. data/vendor/gems/ransack_ui-1.3.4/app/views/ransack_ui/_sort_fields.html.erb +4 -0
  223. data/vendor/gems/ransack_ui-1.3.4/config/locales/en.yml +24 -0
  224. data/vendor/gems/ransack_ui-1.3.4/lib/ransack_ui.rb +9 -0
  225. data/vendor/gems/ransack_ui-1.3.4/lib/ransack_ui/adapters/active_record.rb +6 -0
  226. data/vendor/gems/ransack_ui-1.3.4/lib/ransack_ui/adapters/active_record/base.rb +46 -0
  227. data/vendor/gems/ransack_ui-1.3.4/lib/ransack_ui/controller_helpers.rb +18 -0
  228. data/vendor/gems/ransack_ui-1.3.4/lib/ransack_ui/rails/engine.rb +21 -0
  229. data/vendor/gems/ransack_ui-1.3.4/lib/ransack_ui/ransack_overrides/adapters/active_record/base.rb +47 -0
  230. data/vendor/gems/ransack_ui-1.3.4/lib/ransack_ui/ransack_overrides/configuration.rb +15 -0
  231. data/vendor/gems/ransack_ui-1.3.4/lib/ransack_ui/ransack_overrides/context.rb +9 -0
  232. data/vendor/gems/ransack_ui-1.3.4/lib/ransack_ui/ransack_overrides/helpers/form_builder.rb +262 -0
  233. data/vendor/gems/ransack_ui-1.3.4/lib/ransack_ui/ransack_overrides/nodes/attribute.rb +13 -0
  234. data/vendor/gems/ransack_ui-1.3.4/lib/ransack_ui/ransack_overrides/nodes/condition.rb +13 -0
  235. data/vendor/gems/ransack_ui-1.3.4/lib/ransack_ui/ransack_overrides/nodes/grouping.rb +20 -0
  236. data/vendor/gems/ransack_ui-1.3.4/lib/ransack_ui/version.rb +3 -0
  237. data/vendor/gems/ransack_ui-1.3.4/lib/ransack_ui/view_helpers.rb +30 -0
  238. data/vendor/gems/ransack_ui-1.3.4/ransack_ui.gemspec +23 -0
  239. metadata +79 -67
  240. data/app/controllers/authentications_controller.rb +0 -53
  241. data/app/models/users/authentication.rb +0 -56
  242. data/app/views/authentications/new.html.haml +0 -19
  243. data/app/views/passwords/edit.html.haml +0 -15
  244. data/app/views/passwords/new.html.haml +0 -10
  245. data/app/views/user_mailer/password_reset_instructions.html.haml +0 -6
  246. data/app/views/users/new.html.haml +0 -19
  247. data/spec/controllers/authentications_controller_spec.rb +0 -150
  248. data/spec/controllers/passwords_controller_spec.rb +0 -32
  249. data/spec/models/users/authentication_spec.rb +0 -19
  250. data/spec/support/auth_macros.rb +0 -49
  251. data/spec/views/authentications/new.haml_spec.rb +0 -31
@@ -0,0 +1,41 @@
1
+ .ransack_search {
2
+ .remove_fields, .ransack_attribute, .ransack_predicate,
3
+ .ransack_query, .ransack_sort_order, .ransack_sort,
4
+ .btn-group-select {
5
+ margin: 0 5px;
6
+ vertical-align: middle;
7
+ margin-bottom: 0;
8
+ }
9
+
10
+ .btn-group-select { display: inline-block; }
11
+
12
+ .remove_fields {
13
+ margin-right: 4px;
14
+ img { margin-bottom: 4px; }
15
+ &:hover { opacity: 0.6; }
16
+ }
17
+ .remove_fields.btn {
18
+ padding: 0px 3px 2px;
19
+ &:hover { opacity: 1; }
20
+ }
21
+
22
+ .add_fields.btn {
23
+ margin-top: 20px;
24
+ }
25
+
26
+ .btn {
27
+ i { line-height: 16px; }
28
+ span { margin-left: 5px; }
29
+ }
30
+ .btn-small { padding-left: 6px; }
31
+
32
+ .combinator { margin-bottom: 20px; }
33
+
34
+ .filters > p {
35
+ margin-top: 20px;
36
+ }
37
+
38
+ .fields.sort {
39
+ margin-bottom: 20px;
40
+ }
41
+ }
@@ -0,0 +1,15 @@
1
+ <div class="fields condition" data-object-name="<%= f.object_name %>">
2
+ <%= link_to_remove_fields t('ransack.remove_condition'), f, options %>
3
+
4
+ <%= f.attribute_fields do |a| %>
5
+ <span class="fields" data-object-name="<%= f.object_name %>">
6
+ <%= a.attribute_select({}, :class => 'ransack_attribute') %>
7
+ <% end %>
8
+
9
+ <%= f.predicate_select({}, :class => 'ransack_predicate') %>
10
+
11
+ <%= f.value_fields do |v| %>
12
+ <span class="fields value" data-object-name="<%= f.object_name %>">
13
+ <%= v.text_field :value, :style => "width: 200px;", :class => "ransack_query" %>
14
+ <% end %>
15
+ </div>
@@ -0,0 +1,16 @@
1
+ <div class="fields well" data-object-name="<%= f.object_name %>">
2
+ <div class="combinator">
3
+ <% key = (f.object_name =~ /[0]/) ? :group_first : :group_rest %>
4
+ <% combinator = f.combinator_select({}, :class => 'ransack_combinator') %>
5
+ <%= t("ransack.#{key}", :combinator => combinator).html_safe %>
6
+ </div>
7
+
8
+ <div class="filters">
9
+ <% f.object.build_condition unless f.object.conditions.any? %>
10
+ <%= f.condition_fields do |c| %>
11
+ <%= render 'ransack_ui/condition_fields', :f => c, :options => options %>
12
+ <% end %>
13
+
14
+ <%= link_to_add_fields t('ransack.add_condition'), f, :condition, options %>
15
+ </div>
16
+ </div>
@@ -0,0 +1,29 @@
1
+ <%= search_form_for @ransack_search, :url => (options[:url] || url_for(:action => :index)),
2
+ :html => {:method => :get, :class => "ransack_search"}, :remote => !!options[:remote] do |f| %>
3
+
4
+ <%= javascript_tag do %>
5
+ if (window.Ransack == null) { window.Ransack = {}; }
6
+ Ransack.alt_predicates_i18n = <%= I18n.translate(:"ransack.predicates.alt", :default => {}).to_json.html_safe %>;
7
+ Ransack.value_field_labels = <%= f.labels_for_value_fields.to_json.html_safe %>
8
+ <% end %>
9
+
10
+ <div class="row">
11
+ <div class="span12 well">
12
+ <%= f.sort_fields do |s| %>
13
+ <%= render 'ransack_ui/sort_fields', :f => s %>
14
+ <% end %>
15
+
16
+ <%= f.grouping_fields do |g| %>
17
+ <%= render 'ransack_ui/grouping_fields', :f => g, :options => options %>
18
+ <% end %>
19
+
20
+ <%= link_to_add_fields t('ransack.add_group'), f, :grouping, options %>
21
+
22
+ <div class="pull-right">
23
+ <%= hidden_field_tag :distinct, '1' %>
24
+ <%= hidden_field_tag :page, '1' %>
25
+ <%= f.submit t('ransack.submit'), :class => 'btn btn-primary btn-large submit-search' %>
26
+ </div>
27
+ </div>
28
+ </div>
29
+ <% end %>
@@ -0,0 +1,4 @@
1
+ <div class="fields sort">
2
+ <span class="sort_by">Sort by:</span>
3
+ <%= f.sort_select %>
4
+ </div>
@@ -0,0 +1,24 @@
1
+ en:
2
+ ransack:
3
+ predicates:
4
+ alt:
5
+ date:
6
+ lt: "is before"
7
+ lt_any: "is before any"
8
+ lt_all: "is before all"
9
+ lteq: "is before or on"
10
+ lteq_any: "is before or on any"
11
+ lteq_all: "is before or on all"
12
+ gt: "is after"
13
+ gt_any: "is after any"
14
+ gt_all: "is after all"
15
+ gteq: "is after or on"
16
+ gteq_any: "is after or on any"
17
+ gteq_all: "is after or on all"
18
+
19
+ submit: Search
20
+ add_group: Add a filter group
21
+ group_first: "Show results where %{combinator} of the following match:"
22
+ group_rest: "...and where %{combinator} of the following match:"
23
+ add_condition: Add a filter
24
+ remove_condition: Remove filter
@@ -0,0 +1,9 @@
1
+ require "ransack_ui/version"
2
+ require "ransack_ui/rails/engine"
3
+ require "ransack_chronic"
4
+
5
+ # Require ransack overrides
6
+ require 'ransack_ui/adapters/active_record'
7
+ Dir.glob(File.expand_path('../ransack_ui/ransack_overrides/**/*.rb', __FILE__)) {|f| require f }
8
+
9
+ require "ransack"
@@ -0,0 +1,6 @@
1
+ # Extend original ransack adapter first
2
+ require 'ransack/constants'
3
+ require 'ransack/adapters/active_record'
4
+
5
+ require 'ransack_ui/adapters/active_record/base'
6
+ ActiveRecord::Base.extend RansackUI::Adapters::ActiveRecord::Base
@@ -0,0 +1,46 @@
1
+ module RansackUI
2
+ module Adapters
3
+ module ActiveRecord
4
+ module Base
5
+
6
+ def self.extended(base)
7
+ base.class_eval do
8
+ class_attribute :_ransackable_associations
9
+ class_attribute :_ransack_can_autocomplete
10
+ self._ransackable_associations ||= []
11
+ self._ransack_can_autocomplete = false
12
+ end
13
+ end
14
+
15
+ def has_ransackable_associations(associations)
16
+ self._ransackable_associations = associations
17
+ end
18
+
19
+ def ransack_can_autocomplete
20
+ self._ransack_can_autocomplete = true
21
+ end
22
+
23
+ # Return array of attributes with [name, type]
24
+ # (Default to :string type for ransackers)
25
+ def ransackable_attributes(auth_object = nil)
26
+ columns.map{|c| [c.name, c.type] } +
27
+ _ransackers.keys.map {|k,v| [k, v.type || :string] }
28
+ end
29
+
30
+ def ransackable_associations(auth_object = nil)
31
+ all_associations = reflect_on_all_associations.map {|a| a.name.to_s}
32
+ if self._ransackable_associations.any?
33
+ # Return intersection of all associations, and associations defined on the model
34
+ all_associations & self._ransackable_associations
35
+ else
36
+ all_associations
37
+ end
38
+ end
39
+
40
+ def ransortable_attributes(auth_object = nil)
41
+ ransackable_attributes(auth_object).map(&:first)
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,18 @@
1
+ module RansackUI
2
+ module ControllerHelpers
3
+ # Builds @ransack_search object from params[:q]
4
+ # Model class can be passed in or inferred from controller name.
5
+ #
6
+ # Should be used as a before_filter, e.g.:
7
+ # before_filter :load_ransack_search, :only => :index
8
+ #
9
+ # Can also be called as a function if needed. Will return the search object.
10
+ #
11
+ def load_ransack_search(klass = nil)
12
+ klass ||= controller_path.classify.constantize
13
+ @ransack_search = klass.ransack(params[:q])
14
+ @ransack_search.build_grouping if @ransack_search.groupings.empty?
15
+ @ransack_search
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,21 @@
1
+ require 'ransack_ui/view_helpers'
2
+ require 'ransack_ui/controller_helpers'
3
+
4
+ module RansackUI
5
+ module Rails
6
+ class Engine < ::Rails::Engine
7
+ initializer "ransack_ui.view_helpers" do
8
+ ActionView::Base.send :include, ViewHelpers
9
+ end
10
+
11
+ initializer "ransack_ui.controller_helpers" do
12
+ ActionController::Base.send :include, ControllerHelpers
13
+ end
14
+
15
+ config.before_configuration do
16
+ # Add images to be precompiled
17
+ ::Rails.application.config.assets.precompile += %w(ransack_ui/delete.png ransack_ui/calendar.png)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,47 @@
1
+ require 'ransack/adapters/active_record/base'
2
+
3
+ module Ransack
4
+ module Adapters
5
+ module ActiveRecord
6
+ module Base
7
+ # Return array of attributes with [name, type]
8
+ # (Default to :string type for ransackers)
9
+ def ransackable_attributes(auth_object = nil)
10
+ columns.map{|c| [c.name, c.type] } +
11
+ _ransackers.map {|k,v| [k, v.type || :string] }
12
+ end
13
+
14
+ def self.extended(base)
15
+ alias :search :ransack unless base.method_defined? :search
16
+ base.class_eval do
17
+ class_attribute :_ransackers
18
+ class_attribute :_ransackable_associations
19
+ class_attribute :_ransack_can_autocomplete
20
+ self._ransackers ||= {}
21
+ self._ransackable_associations ||= []
22
+ self._ransack_can_autocomplete = false
23
+ end
24
+ end
25
+
26
+ def has_ransackable_associations(associations)
27
+ self._ransackable_associations = associations
28
+ end
29
+
30
+ def ransack_can_autocomplete
31
+ self._ransack_can_autocomplete = true
32
+ end
33
+
34
+ def ransackable_associations(auth_object = nil)
35
+ all_associations = reflect_on_all_associations.map {|a| a.name.to_s}
36
+ if self._ransackable_associations.any?
37
+ # Return intersection of all associations, and associations defined on the model
38
+ all_associations & self._ransackable_associations
39
+ else
40
+ all_associations
41
+ end
42
+ end
43
+
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,15 @@
1
+ require 'ransack/configuration'
2
+
3
+ module Ransack
4
+ Configuration.class_eval do
5
+ # Set default predicate options for predicate_select in form builder
6
+ # This is ignored if any options are passed
7
+ def default_predicates=(options)
8
+ self.options[:default_predicates] = options
9
+ end
10
+
11
+ def ajax_options=(options)
12
+ self.options[:ajax_options] = options
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,9 @@
1
+ require 'ransack/context'
2
+
3
+ module Ransack
4
+ Context.class_eval do
5
+ def ransackable_attribute?(str, klass)
6
+ klass.ransackable_attributes(auth_object).map(&:first).include? str
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,262 @@
1
+ require 'ransack/helpers/form_builder'
2
+
3
+ module Ransack
4
+ module Helpers
5
+ FormBuilder.class_eval do
6
+ cattr_accessor :cached_searchable_attributes_for_base
7
+ self.cached_searchable_attributes_for_base = {}
8
+
9
+ def attribute_select(options = {}, html_options = {})
10
+ raise ArgumentError, "attribute_select must be called inside a search FormBuilder!" unless object.respond_to?(:context)
11
+ options[:include_blank] = true unless options.has_key?(:include_blank)
12
+
13
+ # Set default associations set on model with 'has_ransackable_associations'
14
+ if options[:associations].nil?
15
+ options[:associations] = object.context.klass.ransackable_associations
16
+ end
17
+
18
+ bases = [''] + association_array(options[:associations])
19
+ if bases.size > 1
20
+ @template.select(
21
+ @object_name, :name,
22
+ @template.grouped_options_for_select(attribute_collection_for_bases(bases), object.name),
23
+ objectify_options(options), @default_options.merge(html_options)
24
+ )
25
+ else
26
+ @template.select(
27
+ @object_name, :name, attribute_collection_for_base(bases.first),
28
+ objectify_options(options), @default_options.merge(html_options)
29
+ )
30
+ end
31
+ end
32
+
33
+ def sort_select(options = {}, html_options = {})
34
+ raise ArgumentError, "sort_select must be called inside a search FormBuilder!" unless object.respond_to?(:context)
35
+ options[:include_blank] = true unless options.has_key?(:include_blank)
36
+ bases = [''] + association_array(options[:associations])
37
+ if bases.size > 1
38
+ @template.select(
39
+ @object_name, :name,
40
+ @template.grouped_options_for_select(attribute_collection_for_bases(bases), object.name),
41
+ objectify_options(options), @default_options.merge({:class => 'ransack_sort'}).merge(html_options)
42
+ ) + @template.collection_select(
43
+ @object_name, :dir, [['asc', object.translate('asc')], ['desc', object.translate('desc')]], :first, :last,
44
+ objectify_options(options.except(:include_blank)), @default_options.merge({:class => 'ransack_sort_order'}).merge(html_options)
45
+ )
46
+ else
47
+ # searchable_attributes now returns [c, type]
48
+ collection = object.context.searchable_attributes(bases.first).map do |c, type|
49
+ [
50
+ attr_from_base_and_column(bases.first, c),
51
+ Translate.attribute(attr_from_base_and_column(bases.first, c), :context => object.context)
52
+ ]
53
+ end
54
+ @template.collection_select(
55
+ @object_name, :name, collection, :first, :last,
56
+ objectify_options(options), @default_options.merge({:class => 'ransack_sort'}).merge(html_options)
57
+ ) + @template.collection_select(
58
+ @object_name, :dir, [['asc', object.translate('asc')], ['desc', object.translate('desc')]], :first, :last,
59
+ objectify_options(options.except(:include_blank)), @default_options.merge({:class => 'ransack_sort_order'}).merge(html_options)
60
+ )
61
+ end
62
+ end
63
+
64
+ def labels_for_value_fields
65
+ labels = {}
66
+
67
+ object.groupings.each do |grouping|
68
+ grouping.conditions.each do |condition|
69
+ condition.values.each do |value|
70
+ # If value is present, and the attribute is an association,
71
+ # load the selected record and include the record name as a data attribute
72
+ if value.value.present?
73
+ condition_attributes = condition.attributes
74
+ if condition_attributes.any?
75
+ attribute = condition_attributes.first.name
76
+ klass_name = foreign_klass_for_attribute(attribute)
77
+
78
+ if klass_name
79
+ klass = klass_name.constantize
80
+
81
+ value_object = klass.find_by_id(value.value)
82
+ if value_object
83
+ labels[attribute] ||= {}
84
+
85
+ if value_object.respond_to? :full_name
86
+ labels[attribute][value.value] = value_object.full_name
87
+ elsif value_object.respond_to? :name
88
+ labels[attribute][value.value] = value_object.name
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
97
+
98
+ labels
99
+ end
100
+
101
+
102
+ def predicate_keys(options)
103
+ keys = options[:compounds] ? Predicate.names : Predicate.names.reject {|k| k.match(/_(any|all)$/)}
104
+ if only = options[:only]
105
+ if only.respond_to? :call
106
+ keys = keys.select {|k| only.call(k)}
107
+ else
108
+ only = Array.wrap(only).map(&:to_s)
109
+ # Create compounds hash, e.g. {"eq" => ["eq", "eq_any", "eq_all"], "blank" => ["blank"]}
110
+ key_groups = keys.inject(Hash.new([])){ |h,k| h[k.sub(/_(any|all)$/, '')] += [k]; h }
111
+ # Order compounds hash by 'only' keys
112
+ keys = only.map {|k| key_groups[k] }.flatten.compact
113
+ end
114
+ end
115
+ keys
116
+ end
117
+
118
+ def predicate_select(options = {}, html_options = {})
119
+ options = Ransack.options[:default_predicates] || {} if options.blank?
120
+
121
+ options[:compounds] = true if options[:compounds].nil?
122
+ keys = predicate_keys(options)
123
+ # If condition is newly built with build_condition(),
124
+ # then replace the default predicate with the first in the ordered list
125
+ @object.predicate_name = keys.first if @object.default?
126
+ @template.collection_select(
127
+ @object_name, :p, keys.map {|k| [k, Translate.predicate(k)]}, :first, :last,
128
+ objectify_options(options), @default_options.merge(html_options)
129
+ )
130
+ end
131
+
132
+ def attribute_collection_for_bases(bases)
133
+ bases.map do |base|
134
+ if collection = attribute_collection_for_base(base)
135
+ [
136
+ Translate.association(base, :context => object.context),
137
+ collection
138
+ ]
139
+ end
140
+ end.compact
141
+ end
142
+
143
+ def attribute_collection_for_base(base)
144
+ klass = object.context.traverse(base)
145
+ ajax_options = Ransack.options[:ajax_options] || {}
146
+
147
+ # Detect any inclusion validators to build list of options for a column
148
+ column_select_options = klass.validators.each_with_object({}) do |v, hash|
149
+ if v.is_a? ActiveModel::Validations::InclusionValidator
150
+ v.attributes.each do |a|
151
+ # Try to translate options from activerecord.attribute_options.<model>.<attribute>
152
+ inclusions = v.send(:delimiter)
153
+ inclusions = inclusions.call if inclusions.respond_to?(:call) # handle lambda
154
+ hash[a.to_s] = inclusions.each_with_object({}) do |o, options|
155
+ options[o.to_s] = I18n.translate("activerecord.attribute_options.#{klass.to_s.downcase}.#{a}.#{o}", :default => o.to_s.titleize)
156
+ end
157
+ end
158
+ end
159
+ end
160
+
161
+ if klass.respond_to?(:ransack_column_select_options)
162
+ column_select_options.merge!(klass.ransack_column_select_options)
163
+ end
164
+
165
+ searchable_attributes_for_base(base).map do |attribute_data|
166
+ column = attribute_data[:column]
167
+
168
+ html_options = {}
169
+
170
+ # Add column type as data attribute
171
+ html_options[:'data-type'] = attribute_data[:type]
172
+ # Set 'base' attribute if attribute is on base model
173
+ html_options[:'data-root-model'] = true if base.blank?
174
+
175
+ # Set column options if detected from inclusion validator
176
+ if column_select_options[column]
177
+ # Format options as an array of hashes with id and text columns, for Select2
178
+ html_options[:'data-select-options'] = column_select_options[column].map {|id, text|
179
+ {:id => id, :text => text}
180
+ }.to_json
181
+ end
182
+
183
+ foreign_klass = attribute_data[:foreign_klass]
184
+
185
+ if foreign_klass
186
+ # If field is a foreign key, set up 'data-ajax-*' attributes for auto-complete
187
+ controller = ActiveSupport::Inflector::tableize(foreign_klass.to_s)
188
+ html_options[:'data-ajax-entity'] = I18n.translate(controller, :default => controller)
189
+ if ajax_options[:url]
190
+ html_options[:'data-ajax-url'] = ajax_options[:url].sub(':controller', controller)
191
+ else
192
+ html_options[:'data-ajax-url'] = "/#{controller}.json"
193
+ end
194
+ html_options[:'data-ajax-type'] = ajax_options[:type] || 'GET'
195
+ html_options[:'data-ajax-key'] = ajax_options[:key] || 'query'
196
+ end
197
+
198
+ [
199
+ attribute_data[:label],
200
+ attribute_data[:attribute],
201
+ html_options
202
+ ]
203
+ end
204
+ rescue UntraversableAssociationError => e
205
+ nil
206
+ end
207
+
208
+
209
+ private
210
+
211
+ def searchable_attributes_for_base(base)
212
+ cache_prefix = object.context.klass.table_name
213
+ cache_key = base.blank? ? cache_prefix : [cache_prefix, base].join('_')
214
+
215
+ self.class.cached_searchable_attributes_for_base[cache_key] ||= object.context.searchable_attributes(base).map do |column, type|
216
+ klass = object.context.traverse(base)
217
+ foreign_keys = klass.reflect_on_all_associations.select(&:belongs_to?).
218
+ each_with_object({}) {|r, h| h[r.foreign_key.to_sym] = r.class_name }
219
+
220
+ # Don't show 'id' column for base model
221
+ next nil if base.blank? && column == 'id'
222
+
223
+ attribute = attr_from_base_and_column(base, column)
224
+ attribute_label = Translate.attribute(attribute, :context => object.context)
225
+
226
+ # Set model name as label for 'id' column on that model's table.
227
+ if column == 'id'
228
+ foreign_klass = object.context.traverse(base).model_name
229
+ # Check that model can autocomplete. If not, skip this id column.
230
+ next nil unless ActiveSupport::Inflector::constantize(foreign_klass.to_s)._ransack_can_autocomplete
231
+
232
+ attribute_label = I18n.translate(foreign_klass, :default => foreign_klass)
233
+ else
234
+ foreign_klass = foreign_keys[column.to_sym]
235
+ end
236
+
237
+ attribute_data = {
238
+ label: attribute_label,
239
+ type: type,
240
+ column: column,
241
+ attribute: attribute
242
+ }
243
+ attribute_data[:foreign_klass] = foreign_klass if foreign_klass
244
+ attribute_data
245
+ end.compact
246
+ end
247
+
248
+ def foreign_klass_for_attribute(attribute)
249
+ associations = object.context.klass.ransackable_associations
250
+ bases = [''] + association_array(associations)
251
+
252
+ bases.each do |base|
253
+ searchable_attributes_for_base(base).each do |attribute_data|
254
+ if attribute == attribute_data[:attribute]
255
+ return attribute_data[:foreign_klass]
256
+ end
257
+ end
258
+ end
259
+ end
260
+ end
261
+ end
262
+ end