rails_admin 3.3.0 → 4.0.0.beta

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 (214) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +7 -5
  3. data/README.md +11 -11
  4. data/Rakefile +3 -11
  5. data/app/assets/builds/rails_admin.css +10 -0
  6. data/app/assets/builds/rails_admin.js +34 -0
  7. data/app/controllers/rails_admin/main_controller.rb +3 -3
  8. data/app/helpers/rails_admin/application_helper.rb +3 -15
  9. data/app/helpers/rails_admin/form_builder.rb +10 -9
  10. data/app/views/layouts/rails_admin/_head.html.erb +10 -27
  11. data/app/views/layouts/rails_admin/_head_custom.html.erb +3 -0
  12. data/app/views/layouts/rails_admin/_secondary_navigation.html.erb +1 -1
  13. data/app/views/rails_admin/main/_dashboard_history.html.erb +1 -1
  14. data/app/views/rails_admin/main/_delete_notice.html.erb +3 -3
  15. data/app/views/rails_admin/main/_form_boolean.html.erb +9 -13
  16. data/app/views/rails_admin/main/_submit_buttons.html.erb +4 -4
  17. data/app/views/rails_admin/main/bulk_delete.html.erb +2 -2
  18. data/app/views/rails_admin/main/dashboard.html.erb +3 -3
  19. data/app/views/rails_admin/main/delete.html.erb +2 -2
  20. data/app/views/rails_admin/main/export.html.erb +2 -2
  21. data/app/views/rails_admin/main/history.html.erb +2 -2
  22. data/app/views/rails_admin/main/index.html.erb +3 -3
  23. data/config/initializers/active_record_extensions.rb +1 -4
  24. data/lib/generators/rails_admin/install_generator.rb +37 -92
  25. data/lib/generators/rails_admin/templates/rails_admin.scss.erb +1 -1
  26. data/lib/rails_admin/abstract_model.rb +69 -191
  27. data/lib/rails_admin/adapters/active_record/association.rb +1 -1
  28. data/lib/rails_admin/adapters/active_record/property.rb +7 -0
  29. data/lib/rails_admin/adapters/active_record/reflection.rb +87 -0
  30. data/lib/rails_admin/adapters/active_record/repository.rb +216 -0
  31. data/lib/rails_admin/adapters/active_record/statement_builder.rb +148 -0
  32. data/lib/rails_admin/adapters/active_record/where_builder.rb +40 -0
  33. data/lib/rails_admin/adapters/active_record.rb +7 -356
  34. data/lib/rails_admin/adapters/mongoid/association.rb +1 -1
  35. data/lib/rails_admin/adapters/mongoid/extension.rb +16 -6
  36. data/lib/rails_admin/adapters/mongoid/property.rb +7 -0
  37. data/lib/rails_admin/adapters/mongoid/reflection.rb +58 -0
  38. data/lib/rails_admin/adapters/mongoid/repository.rb +237 -0
  39. data/lib/rails_admin/adapters/mongoid/statement_builder.rb +93 -0
  40. data/lib/rails_admin/adapters/mongoid.rb +7 -300
  41. data/lib/rails_admin/adapters/reflection.rb +105 -0
  42. data/lib/rails_admin/adapters/repository.rb +169 -0
  43. data/lib/rails_admin/adapters/statement_builder.rb +109 -0
  44. data/lib/rails_admin/adapters.rb +94 -0
  45. data/lib/rails_admin/config/actions/base.rb +8 -0
  46. data/lib/rails_admin/config/actions/bulk_delete.rb +4 -4
  47. data/lib/rails_admin/config/actions/dashboard.rb +1 -1
  48. data/lib/rails_admin/config/actions/edit.rb +3 -2
  49. data/lib/rails_admin/config/actions/index.rb +1 -1
  50. data/lib/rails_admin/config/actions/new.rb +3 -2
  51. data/lib/rails_admin/config/actions/show.rb +1 -1
  52. data/lib/rails_admin/config/configurable.rb +4 -3
  53. data/lib/rails_admin/config/fields/association.rb +9 -8
  54. data/lib/rails_admin/config/fields/base.rb +55 -37
  55. data/lib/rails_admin/config/fields/collection_association.rb +2 -2
  56. data/lib/rails_admin/config/fields/factories/carrierwave.rb +35 -20
  57. data/lib/rails_admin/config/fields/factories/dragonfly.rb +30 -18
  58. data/lib/rails_admin/config/fields/factories/enum.rb +14 -8
  59. data/lib/rails_admin/config/fields/factories/paperclip.rb +29 -19
  60. data/lib/rails_admin/config/fields/factories/shrine.rb +19 -8
  61. data/lib/rails_admin/config/fields/singular_association.rb +6 -1
  62. data/lib/rails_admin/config/fields/types/active_record_enum.rb +3 -7
  63. data/lib/rails_admin/config/fields/types/belongs_to_association.rb +8 -4
  64. data/lib/rails_admin/config/fields/types/bson_object_id.rb +1 -1
  65. data/lib/rails_admin/config/fields/types/datetime.rb +0 -4
  66. data/lib/rails_admin/config/fields/types/dragonfly.rb +1 -1
  67. data/lib/rails_admin/config/fields/types/enum.rb +11 -2
  68. data/lib/rails_admin/config/fields/types/multiple_file_upload.rb +0 -2
  69. data/lib/rails_admin/config/fields/types/polymorphic_association.rb +4 -4
  70. data/lib/rails_admin/config/fields/types/time.rb +1 -1
  71. data/lib/rails_admin/config/model.rb +27 -5
  72. data/lib/rails_admin/config/sections/list.rb +0 -8
  73. data/lib/rails_admin/config.rb +7 -21
  74. data/lib/rails_admin/criteria/path.rb +66 -0
  75. data/lib/rails_admin/criteria/period.rb +67 -0
  76. data/lib/rails_admin/engine.rb +11 -15
  77. data/lib/rails_admin/extensions/pundit/authorization_adapter.rb +2 -2
  78. data/lib/rails_admin/extensions/url_for_extension.rb +8 -0
  79. data/lib/rails_admin/support/asset_source.rb +42 -0
  80. data/lib/rails_admin/support/composite_keys_serializer.rb +28 -2
  81. data/lib/rails_admin/support/csv_converter.rb +3 -10
  82. data/lib/rails_admin/version.rb +7 -6
  83. data/lib/rails_admin.rb +7 -0
  84. data/lib/tasks/assets.rake +23 -0
  85. data/package.json +12 -7
  86. data/src/rails_admin/base.js +18 -15
  87. data/src/rails_admin/filter-box.js +7 -4
  88. data/src/rails_admin/filtering-multiselect.js +1 -1
  89. data/src/rails_admin/filtering-select.js +8 -8
  90. data/src/rails_admin/flatpickr.js +14 -0
  91. data/src/rails_admin/remote-form.js +74 -48
  92. data/src/rails_admin/styles/base/theming.scss +48 -25
  93. data/src/rails_admin/styles/base.scss +25 -1
  94. data/src/rails_admin/styles/filtering-select.scss +23 -62
  95. data/src/rails_admin/ui.js +1 -1
  96. data/src/rails_admin/vendor/jquery_nested_form.js +4 -4
  97. data/src/rails_admin/widgets.js +44 -11
  98. metadata +32 -128
  99. data/app/assets/javascripts/rails_admin/application.js.erb +0 -30
  100. data/app/assets/javascripts/rails_admin/custom/ui.js +0 -1
  101. data/app/assets/stylesheets/rails_admin/application.scss.erb +0 -35
  102. data/app/assets/stylesheets/rails_admin/custom/mixins.scss +0 -11
  103. data/app/assets/stylesheets/rails_admin/custom/theming.scss +0 -13
  104. data/app/assets/stylesheets/rails_admin/custom/variables.scss +0 -15
  105. data/lib/generators/rails_admin/importmap_formatter.rb +0 -28
  106. data/lib/generators/rails_admin/templates/rails_admin.vite.js +0 -2
  107. data/lib/generators/rails_admin/templates/rails_admin.webpacker.js +0 -2
  108. data/lib/rails_admin/adapters/active_record/object_extension.rb +0 -13
  109. data/lib/rails_admin/adapters/active_record.rb.bak +0 -348
  110. data/lib/rails_admin/adapters/mongoid/object_extension.rb +0 -29
  111. data/lib/rails_admin/support/es_module_processor.rb +0 -23
  112. data/vendor/assets/fonts/rails_admin/fa-solid-900.ttf +0 -0
  113. data/vendor/assets/javascripts/rails_admin/bootstrap.js +0 -5046
  114. data/vendor/assets/javascripts/rails_admin/flatpickr-with-locales.js +0 -11246
  115. data/vendor/assets/javascripts/rails_admin/jquery-ui/data.js +0 -41
  116. data/vendor/assets/javascripts/rails_admin/jquery-ui/effect.js +0 -1637
  117. data/vendor/assets/javascripts/rails_admin/jquery-ui/ie.js +0 -17
  118. data/vendor/assets/javascripts/rails_admin/jquery-ui/keycode.js +0 -47
  119. data/vendor/assets/javascripts/rails_admin/jquery-ui/position.js +0 -500
  120. data/vendor/assets/javascripts/rails_admin/jquery-ui/safe-active-element.js +0 -42
  121. data/vendor/assets/javascripts/rails_admin/jquery-ui/scroll-parent.js +0 -47
  122. data/vendor/assets/javascripts/rails_admin/jquery-ui/unique-id.js +0 -51
  123. data/vendor/assets/javascripts/rails_admin/jquery-ui/version.js +0 -17
  124. data/vendor/assets/javascripts/rails_admin/jquery-ui/widget.js +0 -735
  125. data/vendor/assets/javascripts/rails_admin/jquery-ui/widgets/autocomplete.js +0 -689
  126. data/vendor/assets/javascripts/rails_admin/jquery-ui/widgets/menu.js +0 -680
  127. data/vendor/assets/javascripts/rails_admin/jquery-ui/widgets/mouse.js +0 -230
  128. data/vendor/assets/javascripts/rails_admin/jquery-ui/widgets/sortable.js +0 -1561
  129. data/vendor/assets/javascripts/rails_admin/jquery3.js +0 -10881
  130. data/vendor/assets/javascripts/rails_admin/popper.js +0 -1974
  131. data/vendor/assets/stylesheets/rails_admin/bootstrap/_accordion.scss +0 -118
  132. data/vendor/assets/stylesheets/rails_admin/bootstrap/_alert.scss +0 -57
  133. data/vendor/assets/stylesheets/rails_admin/bootstrap/_badge.scss +0 -29
  134. data/vendor/assets/stylesheets/rails_admin/bootstrap/_breadcrumb.scss +0 -28
  135. data/vendor/assets/stylesheets/rails_admin/bootstrap/_button-group.scss +0 -139
  136. data/vendor/assets/stylesheets/rails_admin/bootstrap/_buttons.scss +0 -111
  137. data/vendor/assets/stylesheets/rails_admin/bootstrap/_card.scss +0 -216
  138. data/vendor/assets/stylesheets/rails_admin/bootstrap/_carousel.scss +0 -229
  139. data/vendor/assets/stylesheets/rails_admin/bootstrap/_close.scss +0 -40
  140. data/vendor/assets/stylesheets/rails_admin/bootstrap/_containers.scss +0 -41
  141. data/vendor/assets/stylesheets/rails_admin/bootstrap/_dropdown.scss +0 -240
  142. data/vendor/assets/stylesheets/rails_admin/bootstrap/_forms.scss +0 -9
  143. data/vendor/assets/stylesheets/rails_admin/bootstrap/_functions.scss +0 -302
  144. data/vendor/assets/stylesheets/rails_admin/bootstrap/_grid.scss +0 -33
  145. data/vendor/assets/stylesheets/rails_admin/bootstrap/_helpers.scss +0 -9
  146. data/vendor/assets/stylesheets/rails_admin/bootstrap/_images.scss +0 -42
  147. data/vendor/assets/stylesheets/rails_admin/bootstrap/_list-group.scss +0 -174
  148. data/vendor/assets/stylesheets/rails_admin/bootstrap/_mixins.scss +0 -43
  149. data/vendor/assets/stylesheets/rails_admin/bootstrap/_modal.scss +0 -209
  150. data/vendor/assets/stylesheets/rails_admin/bootstrap/_nav.scss +0 -139
  151. data/vendor/assets/stylesheets/rails_admin/bootstrap/_navbar.scss +0 -335
  152. data/vendor/assets/stylesheets/rails_admin/bootstrap/_offcanvas.scss +0 -83
  153. data/vendor/assets/stylesheets/rails_admin/bootstrap/_pagination.scss +0 -64
  154. data/vendor/assets/stylesheets/rails_admin/bootstrap/_placeholders.scss +0 -51
  155. data/vendor/assets/stylesheets/rails_admin/bootstrap/_popover.scss +0 -158
  156. data/vendor/assets/stylesheets/rails_admin/bootstrap/_progress.scss +0 -48
  157. data/vendor/assets/stylesheets/rails_admin/bootstrap/_reboot.scss +0 -625
  158. data/vendor/assets/stylesheets/rails_admin/bootstrap/_root.scss +0 -54
  159. data/vendor/assets/stylesheets/rails_admin/bootstrap/_spinners.scss +0 -69
  160. data/vendor/assets/stylesheets/rails_admin/bootstrap/_tables.scss +0 -155
  161. data/vendor/assets/stylesheets/rails_admin/bootstrap/_toasts.scss +0 -51
  162. data/vendor/assets/stylesheets/rails_admin/bootstrap/_tooltip.scss +0 -115
  163. data/vendor/assets/stylesheets/rails_admin/bootstrap/_transitions.scss +0 -27
  164. data/vendor/assets/stylesheets/rails_admin/bootstrap/_type.scss +0 -104
  165. data/vendor/assets/stylesheets/rails_admin/bootstrap/_utilities.scss +0 -630
  166. data/vendor/assets/stylesheets/rails_admin/bootstrap/_variables.scss +0 -1641
  167. data/vendor/assets/stylesheets/rails_admin/bootstrap/bootstrap.scss +0 -53
  168. data/vendor/assets/stylesheets/rails_admin/bootstrap/forms/_floating-labels.scss +0 -63
  169. data/vendor/assets/stylesheets/rails_admin/bootstrap/forms/_form-check.scss +0 -152
  170. data/vendor/assets/stylesheets/rails_admin/bootstrap/forms/_form-control.scss +0 -219
  171. data/vendor/assets/stylesheets/rails_admin/bootstrap/forms/_form-range.scss +0 -91
  172. data/vendor/assets/stylesheets/rails_admin/bootstrap/forms/_form-select.scss +0 -72
  173. data/vendor/assets/stylesheets/rails_admin/bootstrap/forms/_form-text.scss +0 -11
  174. data/vendor/assets/stylesheets/rails_admin/bootstrap/forms/_input-group.scss +0 -121
  175. data/vendor/assets/stylesheets/rails_admin/bootstrap/forms/_labels.scss +0 -36
  176. data/vendor/assets/stylesheets/rails_admin/bootstrap/forms/_validation.scss +0 -12
  177. data/vendor/assets/stylesheets/rails_admin/bootstrap/helpers/_clearfix.scss +0 -3
  178. data/vendor/assets/stylesheets/rails_admin/bootstrap/helpers/_colored-links.scss +0 -12
  179. data/vendor/assets/stylesheets/rails_admin/bootstrap/helpers/_position.scss +0 -30
  180. data/vendor/assets/stylesheets/rails_admin/bootstrap/helpers/_ratio.scss +0 -26
  181. data/vendor/assets/stylesheets/rails_admin/bootstrap/helpers/_stacks.scss +0 -15
  182. data/vendor/assets/stylesheets/rails_admin/bootstrap/helpers/_stretched-link.scss +0 -15
  183. data/vendor/assets/stylesheets/rails_admin/bootstrap/helpers/_text-truncation.scss +0 -7
  184. data/vendor/assets/stylesheets/rails_admin/bootstrap/helpers/_visually-hidden.scss +0 -8
  185. data/vendor/assets/stylesheets/rails_admin/bootstrap/helpers/_vr.scss +0 -8
  186. data/vendor/assets/stylesheets/rails_admin/bootstrap/mixins/_alert.scss +0 -11
  187. data/vendor/assets/stylesheets/rails_admin/bootstrap/mixins/_backdrop.scss +0 -14
  188. data/vendor/assets/stylesheets/rails_admin/bootstrap/mixins/_border-radius.scss +0 -78
  189. data/vendor/assets/stylesheets/rails_admin/bootstrap/mixins/_box-shadow.scss +0 -18
  190. data/vendor/assets/stylesheets/rails_admin/bootstrap/mixins/_breakpoints.scss +0 -127
  191. data/vendor/assets/stylesheets/rails_admin/bootstrap/mixins/_buttons.scss +0 -133
  192. data/vendor/assets/stylesheets/rails_admin/bootstrap/mixins/_caret.scss +0 -64
  193. data/vendor/assets/stylesheets/rails_admin/bootstrap/mixins/_clearfix.scss +0 -9
  194. data/vendor/assets/stylesheets/rails_admin/bootstrap/mixins/_color-scheme.scss +0 -7
  195. data/vendor/assets/stylesheets/rails_admin/bootstrap/mixins/_container.scss +0 -9
  196. data/vendor/assets/stylesheets/rails_admin/bootstrap/mixins/_deprecate.scss +0 -10
  197. data/vendor/assets/stylesheets/rails_admin/bootstrap/mixins/_forms.scss +0 -144
  198. data/vendor/assets/stylesheets/rails_admin/bootstrap/mixins/_gradients.scss +0 -47
  199. data/vendor/assets/stylesheets/rails_admin/bootstrap/mixins/_grid.scss +0 -151
  200. data/vendor/assets/stylesheets/rails_admin/bootstrap/mixins/_image.scss +0 -16
  201. data/vendor/assets/stylesheets/rails_admin/bootstrap/mixins/_list-group.scss +0 -24
  202. data/vendor/assets/stylesheets/rails_admin/bootstrap/mixins/_lists.scss +0 -7
  203. data/vendor/assets/stylesheets/rails_admin/bootstrap/mixins/_pagination.scss +0 -31
  204. data/vendor/assets/stylesheets/rails_admin/bootstrap/mixins/_reset-text.scss +0 -17
  205. data/vendor/assets/stylesheets/rails_admin/bootstrap/mixins/_resize.scss +0 -6
  206. data/vendor/assets/stylesheets/rails_admin/bootstrap/mixins/_table-variants.scss +0 -21
  207. data/vendor/assets/stylesheets/rails_admin/bootstrap/mixins/_text-truncate.scss +0 -8
  208. data/vendor/assets/stylesheets/rails_admin/bootstrap/mixins/_transition.scss +0 -26
  209. data/vendor/assets/stylesheets/rails_admin/bootstrap/mixins/_utilities.scss +0 -89
  210. data/vendor/assets/stylesheets/rails_admin/bootstrap/mixins/_visually-hidden.scss +0 -29
  211. data/vendor/assets/stylesheets/rails_admin/bootstrap/utilities/_api.scss +0 -47
  212. data/vendor/assets/stylesheets/rails_admin/bootstrap/vendor/_rfs.scss +0 -354
  213. data/vendor/assets/stylesheets/rails_admin/flatpickr.css +0 -791
  214. data/vendor/assets/stylesheets/rails_admin/font-awesome.scss +0 -6351
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsAdmin
4
+ module Adapters
5
+ # The half of an adapter that describes a model: what its attributes and
6
+ # associations are, what the store calls them, and what the model layer says
7
+ # about them.
8
+ #
9
+ # One instance per model, held by the AbstractModel that composes it. A
10
+ # reflection answers questions about the model and never reads or writes a
11
+ # record; the store it does reach for is the schema, not the data.
12
+ class Reflection
13
+ attr_reader :abstract_model
14
+
15
+ def initialize(abstract_model)
16
+ @abstract_model = abstract_model
17
+ end
18
+
19
+ delegate :model, to: :abstract_model
20
+
21
+ # The columns the primary key is made of, always as an array of symbols --
22
+ # a single-column key is an array of one.
23
+ #
24
+ # #primary_key answers in the store's own shape, a name or an array of
25
+ # them, and keeps doing so for callers outside the gem. Everything inside
26
+ # asks in the plural instead, so that a composite key is not a special case
27
+ # each caller has to remember to notice.
28
+ def primary_keys
29
+ Array(primary_key).collect(&:to_sym)
30
+ end
31
+
32
+ # Stores that neither qualify nor quote identifiers use the names as they
33
+ # are; ActiveRecord asks its connection instead.
34
+ def quoted_table_name
35
+ table_name
36
+ end
37
+
38
+ def quote_column_name(name)
39
+ name
40
+ end
41
+
42
+ def pretty_name
43
+ model.model_name.human
44
+ end
45
+
46
+ def human_attribute_name(name)
47
+ model.human_attribute_name(name)
48
+ end
49
+
50
+ # Whether the model makes the given attribute mandatory, either through a
51
+ # validation or through the options of a belongs_to association.
52
+ #
53
+ # +context+ is +:create+, +:update+ or +nil+ and is matched against the
54
+ # +:on+ option of a validation. Adapters may override this when their ORM
55
+ # expresses requiredness differently.
56
+ def attribute_required?(name, context)
57
+ !!(required_by_validation?(name, context) || required_by_association?(name))
58
+ end
59
+
60
+ # Options of the length validation defined on the given attribute, if any.
61
+ def attribute_length_options(name)
62
+ model.validators_on(name).detect { |validator| validator.kind == :length }.try(&:options) || {}
63
+ end
64
+
65
+ # The values of an enum defined on the given attribute, or nil.
66
+ #
67
+ # Only stores with a native enum concept answer this; the rest leave enums
68
+ # to the model, which is what Types::Enum asks about instead.
69
+ def attribute_enum_values(_name)
70
+ nil
71
+ end
72
+
73
+ # A throwaway record, for asking what an instance of this model responds to
74
+ # when no real one is at hand.
75
+ #
76
+ # Instantiating one is wasteful and runs the model's initialization, but
77
+ # method_defined? would miss anything served through method_missing, so the
78
+ # original behavior is kept until there is a reason to narrow it.
79
+ def dummy_record
80
+ model.new
81
+ end
82
+
83
+ private
84
+
85
+ def required_by_validation?(name, context)
86
+ model.validators_on(name).detect do |validator|
87
+ !(validator.options[:allow_nil] || validator.options[:allow_blank]) &&
88
+ %i[presence numericality attachment_presence].include?(validator.kind) &&
89
+ (validator.options[:on] == context || validator.options[:on].blank?) &&
90
+ (validator.options[:if].blank? && validator.options[:unless].blank?)
91
+ end
92
+ end
93
+
94
+ def required_by_association?(name)
95
+ model.reflect_on_all_associations(:belongs_to).detect do |association|
96
+ next unless association.name == name
97
+
98
+ required = association.options[:required] if association.options.key?(:required)
99
+ required = !association.options[:optional] if association.options.key?(:optional) && required.nil?
100
+ required.nil? ? belongs_to_required_by_default : required
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,169 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsAdmin
4
+ module Adapters
5
+ # The half of an adapter that reaches the store: reading records, writing
6
+ # them, and compiling what the configuration asked for into a query.
7
+ #
8
+ # One instance per model, held by the AbstractModel that composes it. Where a
9
+ # repository needs to know something about the model rather than do something
10
+ # with it, it asks through that facade, so that the two halves of an adapter
11
+ # never have to reach for each other directly.
12
+ class Repository
13
+ attr_reader :abstract_model
14
+
15
+ def initialize(abstract_model)
16
+ @abstract_model = abstract_model
17
+ end
18
+
19
+ delegate :model, :config, :associations, :primary_key, :primary_keys, :table_name,
20
+ :quoted_table_name, :quote_column_name, to: :abstract_model
21
+
22
+ # Yield the records of a scope one at a time, in the order it is sorted in,
23
+ # without holding all of them at once. What an export walks.
24
+ #
25
+ # Anything that is not a scope, such as an array of records, is taken as it
26
+ # comes. A store whose query already streams in order needs nothing more
27
+ # than #each, which is all this does.
28
+ def each_record(scope, &block)
29
+ return to_enum(:each_record, scope) unless block
30
+
31
+ scope.each(&block)
32
+ end
33
+
34
+ # Read one attribute from a record: the value the store holds, rather than
35
+ # what a reader of the same name would return.
36
+ #
37
+ # It began as a way around a column named +format+, which on Rails 3
38
+ # reached Kernel#format when it was sent, because attribute methods were
39
+ # not defined until method_missing had been hit (955cca09, 2011). Rails
40
+ # defines them earlier now and that particular collision is gone, but the
41
+ # rule it left behind is what a model redefining a reader still meets.
42
+ #
43
+ # Adapters say how their store tells an attribute from a method.
44
+ def read(record, name)
45
+ record.send(name)
46
+ end
47
+
48
+ # Persist a record, and whatever else the store needs doing to call it
49
+ # saved. Answers the way the record's own #save does.
50
+ def save(record)
51
+ record.save
52
+ end
53
+
54
+ def where(conditions)
55
+ model.where(conditions)
56
+ end
57
+
58
+ def each_associated_children(object)
59
+ associations.each do |association|
60
+ case association.type
61
+ when :has_one
62
+ child = object.send(association.name)
63
+ yield(association, [child]) if child
64
+ when :has_many
65
+ children = object.send(association.name)
66
+ yield(association, Array.new(children))
67
+ end
68
+ end
69
+ end
70
+
71
+ # How a record's id travels through a URL, and back.
72
+ #
73
+ # Only composite keys need more than the id itself, so the default is to
74
+ # hand it over unchanged.
75
+ def format_id(id)
76
+ id
77
+ end
78
+
79
+ def parse_id(id)
80
+ id
81
+ end
82
+
83
+ # Convert a Ruby value into the representation the store holds for the
84
+ # given attribute, and back.
85
+ #
86
+ # Both default to passing the value through. Adapters override them when
87
+ # their ORM has a type system able to do better; the Mongoid adapter does
88
+ # not, so values reach it unconverted.
89
+ def serialize_attribute(_name, value)
90
+ value
91
+ end
92
+
93
+ def deserialize_attribute(_name, value)
94
+ value
95
+ end
96
+
97
+ # Render a sort target as an expression the store understands. Adapters
98
+ # that need qualification or quoting override this; anything already
99
+ # expressed in the store's own terms is handed through.
100
+ def sort_expression(order)
101
+ order.to_s
102
+ end
103
+
104
+ # Render a search target as the column reference the store understands.
105
+ # Overridden where reaching an associated attribute takes more than the
106
+ # dotted path itself.
107
+ def search_column(target)
108
+ target.to_s
109
+ end
110
+
111
+ private
112
+
113
+ def parse_field_value(field, value)
114
+ value.is_a?(Array) ? value.map { |v| parse_one_value(field, v) } : parse_one_value(field, value)
115
+ end
116
+
117
+ # What reaches here came from the query string, so a field is regularly
118
+ # handed something it cannot make sense of -- a date out of range, YAML
119
+ # which does not parse. A field which cannot read the term takes no part in
120
+ # the condition, rather than taking the whole page down with it.
121
+ def parse_one_value(field, value)
122
+ field.parse_value(value)
123
+ rescue StandardError
124
+ nil
125
+ end
126
+
127
+ # The search box asks every queryable field about the same term. Yields
128
+ # [field, parsed value, operator] so that each adapter is left with turning
129
+ # a condition into a scope and combining them, rather than reimplementing
130
+ # which fields take part and how their values are parsed.
131
+ def each_query_condition(query, fields)
132
+ return to_enum(:each_query_condition, query, fields) unless block_given?
133
+
134
+ fields.each do |field|
135
+ yield field, parse_field_value(field, query), field.search_operator
136
+ end
137
+ end
138
+
139
+ # Each filter as [field, parsed value, operator].
140
+ #
141
+ # Filters naming a field that does not exist are skipped, and a filter
142
+ # arriving without an operator falls back to the configured default -- the
143
+ # field types that offer no operator in the filter UI submit none.
144
+ #
145
+ # filters looks like {"string_field" => {"0055" => {"o" => "like", "v" => "x"}}}
146
+ # where "0055" is the filter index and has no meaning here.
147
+ #
148
+ # Nothing guarantees that shape: the filters are read straight off the
149
+ # query string, which anyone can write by hand. Anything which is not the
150
+ # nesting the filter UI submits is ignored, so that a hand-written URL
151
+ # leaves the list unfiltered rather than raising.
152
+ def each_filter_condition(filters, fields)
153
+ return to_enum(:each_filter_condition, filters, fields) unless block_given?
154
+ return unless filters.respond_to?(:each_pair)
155
+
156
+ filters.each_pair do |field_name, filters_dump|
157
+ field = fields.detect { |f| f.name.to_s == field_name }
158
+ next unless field && filters_dump.respond_to?(:each_value)
159
+
160
+ filters_dump.each_value do |filter_dump|
161
+ next unless filter_dump.respond_to?(:each_pair)
162
+
163
+ yield field, parse_field_value(field, filter_dump[:v]), (filter_dump[:o] || RailsAdmin::Config.default_search_operator)
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails_admin/criteria/period'
4
+
5
+ module RailsAdmin
6
+ module Adapters
7
+ # Turns one search or filter condition into whatever the store's query
8
+ # language calls a condition.
9
+ #
10
+ # The parts that are the same everywhere -- discarding a condition, unary
11
+ # operators, numeric parsing, resolving a relative date into a range -- live
12
+ # here; each adapter subclasses this and supplies the rest.
13
+ class StatementBuilder
14
+ def initialize(column, type, value, operator)
15
+ @column = column
16
+ @type = type
17
+ @value = value
18
+ @operator = operator
19
+ end
20
+
21
+ def to_statement
22
+ if @value.is_a?(Array) && @value.include?('_discard')
23
+ @value = @value.reject { |v| v == '_discard' }
24
+ return if @value.empty?
25
+ end
26
+ return if [@operator, @value].any? { |v| v == '_discard' }
27
+
28
+ unary_operators[@operator] || unary_operators[@value] ||
29
+ build_statement_for_type_generic
30
+ end
31
+
32
+ protected
33
+
34
+ def filtering_duration
35
+ RailsAdmin::Criteria::Period.new(@operator, @value).bounds
36
+ end
37
+
38
+ def build_statement_for_type_generic
39
+ build_statement_for_type || begin
40
+ case @type
41
+ when :date
42
+ build_statement_for_date
43
+ when :datetime, :timestamp, :time
44
+ build_statement_for_datetime_or_timestamp
45
+ end
46
+ end
47
+ end
48
+
49
+ def build_statement_for_type
50
+ raise 'You must override build_statement_for_type in your StatementBuilder'
51
+ end
52
+
53
+ def build_statement_for_integer_decimal_or_float
54
+ case @value
55
+ when Array
56
+ val, range_begin, range_end = *@value.collect do |v|
57
+ next unless v.to_i.to_s == v || v.to_f.to_s == v
58
+
59
+ @type == :integer ? v.to_i : v.to_f
60
+ end
61
+ case @operator
62
+ when 'between'
63
+ range_filter(range_begin, range_end)
64
+ else
65
+ column_for_value(val) if val
66
+ end
67
+ else
68
+ if @value.to_i.to_s == @value || @value.to_f.to_s == @value
69
+ @type == :integer ? column_for_value(@value.to_i) : column_for_value(@value.to_f)
70
+ end
71
+ end
72
+ end
73
+
74
+ def build_statement_for_date
75
+ start_date, end_date = filtering_duration
76
+ if start_date
77
+ start_date = begin
78
+ start_date.to_date
79
+ rescue StandardError
80
+ nil
81
+ end
82
+ end
83
+ if end_date
84
+ end_date = begin
85
+ end_date.to_date
86
+ rescue StandardError
87
+ nil
88
+ end
89
+ end
90
+ range_filter(start_date, end_date)
91
+ end
92
+
93
+ def build_statement_for_datetime_or_timestamp
94
+ start_date, end_date = filtering_duration
95
+ start_date = start_date.beginning_of_day if start_date.is_a?(Date)
96
+ end_date = end_date.end_of_day if end_date.is_a?(Date)
97
+ range_filter(start_date, end_date)
98
+ end
99
+
100
+ def unary_operators
101
+ raise 'You must override unary_operators in your StatementBuilder'
102
+ end
103
+
104
+ def range_filter(_min, _max)
105
+ raise 'You must override range_filter in your StatementBuilder'
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsAdmin
4
+ # Which adapters exist, and how a model is matched to one of them.
5
+ #
6
+ # An adapter is a name, a way of telling whether a model belongs to it, and
7
+ # the two classes that implement it -- a Reflection and a Repository. Adding
8
+ # one from outside the gem is a call to +register+:
9
+ #
10
+ # RailsAdmin::Adapters.register(
11
+ # :sequel,
12
+ # reflection: RailsAdmin::Adapters::Sequel::Reflection,
13
+ # repository: RailsAdmin::Adapters::Sequel::Repository,
14
+ # ) { |model| model < ::Sequel::Model }
15
+ #
16
+ # The most recently registered adapter is asked first, so a registration can
17
+ # take over from a built-in one.
18
+ module Adapters
19
+ class Registration
20
+ attr_reader :name
21
+
22
+ # +reflection+ and +repository+ may be classes, or the names of classes as
23
+ # strings together with +require_path+. The built-in adapters name theirs,
24
+ # because loading an adapter loads its ORM and an application must not have
25
+ # the ORM it does not use required on its behalf.
26
+ def initialize(name, reflection:, repository:, require_path: nil, &detector)
27
+ @name = name
28
+ @reflection = reflection
29
+ @repository = repository
30
+ @require_path = require_path
31
+ @detector = detector
32
+ end
33
+
34
+ # Whether this adapter handles the given model. Called for every model in
35
+ # the pool, before anything is loaded, so it must not load the ORM either
36
+ # -- which is why the built-in detectors match on ancestor names rather
37
+ # than on the ancestors themselves.
38
+ def matches?(model)
39
+ @detector.call(model)
40
+ end
41
+
42
+ def reflection
43
+ @reflection = resolve(@reflection)
44
+ end
45
+
46
+ def repository
47
+ @repository = resolve(@repository)
48
+ end
49
+
50
+ private
51
+
52
+ def resolve(klass)
53
+ return klass unless klass.is_a?(::String)
54
+
55
+ require @require_path if @require_path
56
+ klass.constantize
57
+ end
58
+ end
59
+
60
+ class << self
61
+ def register(name, **options, &detector)
62
+ registrations.unshift(Registration.new(name, **options, &detector))
63
+ name
64
+ end
65
+
66
+ def registrations
67
+ @registrations ||= []
68
+ end
69
+
70
+ def detect(model)
71
+ registrations.detect { |registration| registration.matches?(model) }
72
+ end
73
+ end
74
+
75
+ register(
76
+ :active_record,
77
+ require_path: 'rails_admin/adapters/active_record',
78
+ reflection: 'RailsAdmin::Adapters::ActiveRecord::Reflection',
79
+ repository: 'RailsAdmin::Adapters::ActiveRecord::Repository',
80
+ ) do |model|
81
+ model.ancestors.collect(&:to_s).include?('ActiveRecord::Base') &&
82
+ !model.abstract_class? && model.table_exists?
83
+ end
84
+
85
+ register(
86
+ :mongoid,
87
+ require_path: 'rails_admin/adapters/mongoid',
88
+ reflection: 'RailsAdmin::Adapters::Mongoid::Reflection',
89
+ repository: 'RailsAdmin::Adapters::Mongoid::Repository',
90
+ ) do |model|
91
+ model.ancestors.collect(&:to_s).include?('Mongoid::Document')
92
+ end
93
+ end
94
+ end
@@ -149,6 +149,14 @@ module RailsAdmin
149
149
 
150
150
  # Off API.
151
151
 
152
+ # Whether the action can be rendered as a plain <a> link, i.e. visiting
153
+ # its URL performs it. Actions that require a non-GET verb need a
154
+ # button/form instead, so callers render them as text or a disabled
155
+ # link. Tolerates verbs written as `:GET` or `"get"`.
156
+ def linkable?
157
+ http_methods.include?(:get) || http_methods.any? { |verb| verb.to_s.casecmp?('get') }
158
+ end
159
+
152
160
  def key
153
161
  self.class.key
154
162
  end
@@ -20,7 +20,7 @@ module RailsAdmin
20
20
  @objects = list_entries(@model_config, :destroy)
21
21
 
22
22
  if @objects.blank?
23
- flash[:error] = t('admin.flash.error', name: pluralize(0, @model_config.label), action: t('admin.actions.delete.done'))
23
+ flash[:error] = t('admin.flash.error', name: pluralize(0, @model_config.label, @model_config.label_plural), action: t('admin.actions.delete.done'))
24
24
  redirect_to index_path
25
25
  else
26
26
  render @action.template_name
@@ -44,10 +44,10 @@ module RailsAdmin
44
44
  end
45
45
 
46
46
  if destroyed.nil?
47
- flash[:error] = t('admin.flash.error', name: pluralize(0, @model_config.label), action: t('admin.actions.delete.done'))
47
+ flash[:error] = t('admin.flash.error', name: pluralize(0, @model_config.label, @model_config.label_plural), action: t('admin.actions.delete.done'))
48
48
  else
49
- flash[:success] = t('admin.flash.successful', name: pluralize(destroyed.count, @model_config.label), action: t('admin.actions.delete.done')) unless destroyed.empty?
50
- flash[:error] = t('admin.flash.error', name: pluralize(not_destroyed.count, @model_config.label), action: t('admin.actions.delete.done')) unless not_destroyed.empty?
49
+ flash[:success] = t('admin.flash.successful', name: pluralize(destroyed.count, @model_config.label, @model_config.label_plural), action: t('admin.actions.delete.done')) unless destroyed.empty?
50
+ flash[:error] = t('admin.flash.error', name: pluralize(not_destroyed.count, @model_config.label, @model_config.label_plural), action: t('admin.actions.delete.done')) unless not_destroyed.empty?
51
51
  end
52
52
  redirect_to back_or_index
53
53
  end
@@ -32,7 +32,7 @@ module RailsAdmin
32
32
  scope = @authorization_adapter&.query(:index, config.abstract_model)
33
33
  current_count = config.abstract_model.count({}, scope)
34
34
  @max = current_count > @max ? current_count : @max
35
- name = config.abstract_model.model.name
35
+ name = config.abstract_model.model_name
36
36
  @count[name] = current_count
37
37
  @most_recent_created[name] = config.last_created_at
38
38
  end
@@ -26,10 +26,11 @@ module RailsAdmin
26
26
  elsif request.put? # UPDATE
27
27
  sanitize_params_for!(request.xhr? ? :modal : :update)
28
28
 
29
- @object.assign_attributes(params[@abstract_model.param_key])
29
+ # A form that posted nothing for the model leaves nothing to assign.
30
+ @object.assign_attributes(params[@abstract_model.param_key] || {})
30
31
  @authorization_adapter&.authorize(:update, @abstract_model, @object)
31
32
  changes = @object.changes
32
- if @object.save
33
+ if @abstract_model.save(@object)
33
34
  @auditing_adapter&.update_object(@object, @abstract_model, _current_user, changes)
34
35
  respond_to do |format|
35
36
  format.html { redirect_to_on_success }
@@ -53,7 +53,7 @@ module RailsAdmin
53
53
  if @association
54
54
  @association.collection(@objects).collect { |(label, id)| {id: id, label: label} }
55
55
  else
56
- @objects.collect { |object| {id: object.id.to_s, label: object.send(@model_config.object_label_method).to_s} }
56
+ @objects.collect { |object| {id: object.id.to_s, label: @model_config.object_label(object).to_s} }
57
57
  end
58
58
  else
59
59
  @objects.to_json(@schema)
@@ -39,10 +39,11 @@ module RailsAdmin
39
39
  @object = @abstract_model.new
40
40
  sanitize_params_for!(request.xhr? ? :modal : :create)
41
41
 
42
- @object.assign_attributes(params[@abstract_model.param_key])
42
+ # A form that posted nothing for the model leaves nothing to assign.
43
+ @object.assign_attributes(params[@abstract_model.param_key] || {})
43
44
  @authorization_adapter&.authorize(:create, @abstract_model, @object)
44
45
 
45
- if @object.save
46
+ if @abstract_model.save(@object)
46
47
  @auditing_adapter&.create_object(@object, @abstract_model, _current_user)
47
48
  respond_to do |format|
48
49
  format.html { redirect_to_on_success }
@@ -21,8 +21,8 @@ module RailsAdmin
21
21
  register_instance_option :controller do
22
22
  proc do
23
23
  respond_to do |format|
24
- format.json { render json: @object }
25
24
  format.html { render @action.template_name }
25
+ format.json { render json: @object }
26
26
  end
27
27
  end
28
28
  end
@@ -9,7 +9,7 @@ module RailsAdmin
9
9
  base.send :extend, ClassMethods
10
10
  end
11
11
 
12
- def has_option?(name) # rubocop:disable Naming/PredicateName
12
+ def has_option?(name) # rubocop:disable Naming/PredicatePrefix
13
13
  options = self.class.instance_variable_get('@config_options')
14
14
  options&.key?(name)
15
15
  end
@@ -57,7 +57,8 @@ module RailsAdmin
57
57
 
58
58
  # If it's a boolean create an alias for it and remove question mark
59
59
  if option_name.end_with?('?')
60
- scope.send(:define_method, "#{option_name.chop!}?") do
60
+ option_name = option_name.chop
61
+ scope.send(:define_method, "#{option_name}?") do
61
62
  send(option_name)
62
63
  end
63
64
  end
@@ -84,7 +85,7 @@ module RailsAdmin
84
85
  def register_deprecated_instance_option(option_name, replacement_option_name = nil, scope = self)
85
86
  scope.send(:define_method, option_name) do |*args, &block|
86
87
  if replacement_option_name
87
- ActiveSupport::Deprecation.warn("The #{option_name} configuration option is deprecated, please use #{replacement_option_name}.")
88
+ RailsAdmin.deprecator.warn("The #{option_name} configuration option is deprecated, please use #{replacement_option_name}.")
88
89
  send(replacement_option_name, *args, &block)
89
90
  elsif block_given?
90
91
  yield
@@ -21,7 +21,7 @@ module RailsAdmin
21
21
  [value].flatten.select(&:present?).collect do |associated|
22
22
  amc = polymorphic? ? RailsAdmin.config(associated) : associated_model_config # perf optimization for non-polymorphic associations
23
23
  am = amc.abstract_model
24
- wording = associated.send(amc.object_label_method)
24
+ wording = amc.object_label(associated)
25
25
  can_see = !am.embedded? && (show_action = v.action(:show, am, associated))
26
26
  can_see ? v.link_to(wording, v.url_for(action: show_action.action_name, model_name: am.to_param, id: associated.id)) : ERB::Util.html_escape(wording)
27
27
  end.to_sentence.html_safe.presence || '-'
@@ -36,7 +36,7 @@ module RailsAdmin
36
36
 
37
37
  # use the association name as a key, not the association key anymore!
38
38
  register_instance_option :label do
39
- (@label ||= {})[::I18n.locale] ||= abstract_model.model.human_attribute_name association.name
39
+ (@label ||= {})[::I18n.locale] ||= abstract_model.human_attribute_name association.name
40
40
  end
41
41
 
42
42
  # scope for possible associable records
@@ -137,7 +137,7 @@ module RailsAdmin
137
137
  # Returns collection of all selectable records
138
138
  def collection(scope = nil)
139
139
  (scope || bindings[:controller].list_entries(associated_model_config, :index, associated_collection_scope, false)).
140
- map { |o| [o.send(associated_object_label_method), format_key(o.send(associated_primary_key)).to_s] }
140
+ map { |o| [associated_model_config.object_label(o), format_key(o.send(associated_primary_key)).to_s] }
141
141
  end
142
142
 
143
143
  # has many?
@@ -155,12 +155,13 @@ module RailsAdmin
155
155
 
156
156
  private
157
157
 
158
+ # A key travels through the DOM and comes back as a URL or form
159
+ # parameter, so it is the associated model's repository that decides how
160
+ # it is written down.
158
161
  def format_key(key)
159
- if key.is_a?(Array)
160
- RailsAdmin.config.composite_keys_serializer.serialize(key)
161
- else
162
- key
163
- end
162
+ return if key.nil? # the association is not set
163
+
164
+ associated_model_config.abstract_model.format_id(key)
164
165
  end
165
166
  end
166
167
  end