PageRenderTime 26ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 1ms

/lib/rails_admin/adapters/mongoid.rb

https://github.com/victorbv/rails_admin
Ruby | 390 lines | 339 code | 41 blank | 10 comment | 80 complexity | 3a220491989f6b044408407c69a47960 MD5 | raw file
  1. require 'mongoid'
  2. require 'rails_admin/config/sections/list'
  3. require 'rails_admin/adapters/mongoid/abstract_object'
  4. module RailsAdmin
  5. module Adapters
  6. module Mongoid
  7. STRING_TYPE_COLUMN_NAMES = [:name, :title, :subject]
  8. DISABLED_COLUMN_TYPES = ['Range']
  9. def new(params = {})
  10. AbstractObject.new(model.new)
  11. end
  12. def get(id)
  13. begin
  14. AbstractObject.new(model.find(id))
  15. rescue BSON::InvalidObjectId, ::Mongoid::Errors::DocumentNotFound
  16. nil
  17. end
  18. end
  19. def scoped
  20. model.scoped
  21. end
  22. def first(options = {},scope=nil)
  23. all(options, scope).first
  24. end
  25. def all(options = {},scope=nil)
  26. scope ||= self.scoped
  27. scope = scope.includes(options[:include]) if options[:include]
  28. scope = scope.limit(options[:limit]) if options[:limit]
  29. scope = scope.any_in(:_id => options[:bulk_ids]) if options[:bulk_ids]
  30. scope = scope.where(query_conditions(options[:query])) if options[:query]
  31. scope = scope.where(filter_conditions(options[:filters])) if options[:filters]
  32. scope = scope.page(options[:page]).per(options[:per]) if options[:page] && options[:per]
  33. scope = sort_by(options, scope) if options[:sort]
  34. scope
  35. end
  36. def count(options = {},scope=nil)
  37. all(options.merge({:limit => false, :page => false}), scope).count
  38. end
  39. def destroy(objects)
  40. Array.wrap(objects).each &:destroy
  41. end
  42. def primary_key
  43. '_id'
  44. end
  45. def associations
  46. model.associations.values.map do |association|
  47. {
  48. :name => association.name.to_sym,
  49. :pretty_name => association.name.to_s.tr('_', ' ').capitalize,
  50. :type => association_type_lookup(association.macro),
  51. :model_proc => Proc.new { association_model_proc_lookup(association) },
  52. :primary_key_proc => Proc.new { association_primary_key_lookup(association) },
  53. :foreign_key => association_foreign_key_lookup(association),
  54. :foreign_type => association_foreign_type_lookup(association),
  55. :foreign_inverse_of => association_foreign_inverse_of_lookup(association),
  56. :as => association_as_lookup(association),
  57. :polymorphic => association_polymorphic_lookup(association),
  58. :inverse_of => association_inverse_of_lookup(association),
  59. :read_only => nil,
  60. :nested_form => association_nested_attributes_options_lookup(association)
  61. }
  62. end
  63. end
  64. def properties
  65. fields = model.fields.reject{|name, field| DISABLED_COLUMN_TYPES.include?(field.type.to_s) }
  66. fields.map do |name,field|
  67. ar_type = {
  68. "Array" => { :type => :serialized },
  69. "BigDecimal" => { :type => :decimal },
  70. "Boolean" => { :type => :boolean },
  71. "BSON::ObjectId" => { :type => :bson_object_id, :serial? => (name == primary_key) },
  72. "Date" => { :type => :date },
  73. "DateTime" => { :type => :datetime },
  74. "Float" => { :type => :float },
  75. "Hash" => { :type => :serialized },
  76. "Integer" => { :type => :integer },
  77. "Object" => (
  78. if associations.find{|a| a[:type] == :belongs_to && a[:foreign_key] == name.to_sym}
  79. { :type => :bson_object_id }
  80. else
  81. { :type => :string, :length => 255 }
  82. end
  83. ),
  84. "String" => (
  85. if (length = length_validation_lookup(name)) && length < 256
  86. { :type => :string, :length => length }
  87. elsif STRING_TYPE_COLUMN_NAMES.include?(name.to_sym)
  88. { :type => :string, :length => 255 }
  89. else
  90. { :type => :text }
  91. end
  92. ),
  93. "Symbol" => { :type => :string, :length => 255 },
  94. "Time" => { :type => :datetime },
  95. }[field.type.to_s] or raise "Need to map field #{field.type.to_s} for field name #{name} in #{model.inspect}"
  96. {
  97. :name => field.name.to_sym,
  98. :length => nil,
  99. :pretty_name => field.name.to_s.gsub('_', ' ').strip.capitalize,
  100. :nullable? => true,
  101. :serial? => false,
  102. }.merge(ar_type)
  103. end
  104. end
  105. def table_name
  106. model.collection.name
  107. end
  108. def serialized_attributes
  109. # Mongoid Array and Hash type columns are mapped to RA serialized type
  110. # through type detection in self#properties.
  111. []
  112. end
  113. def encoding
  114. 'UTF-8'
  115. end
  116. def embedded?
  117. @embedded ||= !!model.associations.values.find{|a| a.macro.to_sym == :embedded_in }
  118. end
  119. private
  120. def query_conditions(query, fields = config.list.fields.select(&:queryable?))
  121. statements = []
  122. fields.each do |field|
  123. conditions_per_collection = {}
  124. field.searchable_columns.flatten.each do |column_infos|
  125. collection_name, column_name = column_infos[:column].split('.')
  126. statement = build_statement(column_name, column_infos[:type], query, field.search_operator)
  127. if statement
  128. conditions_per_collection[collection_name] ||= []
  129. conditions_per_collection[collection_name] << statement
  130. end
  131. end
  132. statements.concat make_condition_for_current_collection(field, conditions_per_collection)
  133. end
  134. if statements.any?
  135. { '$or' => statements }
  136. else
  137. {}
  138. end
  139. end
  140. # filters example => {"string_field"=>{"0055"=>{"o"=>"like", "v"=>"test_value"}}, ...}
  141. # "0055" is the filter index, no use here. o is the operator, v the value
  142. def filter_conditions(filters, fields = config.list.fields.select(&:filterable?))
  143. statements = []
  144. filters.each_pair do |field_name, filters_dump|
  145. filters_dump.each do |filter_index, filter_dump|
  146. conditions_per_collection = {}
  147. field = fields.find{|f| f.name.to_s == field_name}
  148. next unless field
  149. field.searchable_columns.each do |column_infos|
  150. collection_name, column_name = column_infos[:column].split('.')
  151. statement = build_statement(column_name, column_infos[:type], filter_dump[:v], (filter_dump[:o] || 'default'))
  152. if statement
  153. conditions_per_collection[collection_name] ||= []
  154. conditions_per_collection[collection_name] << statement
  155. end
  156. end
  157. if conditions_per_collection.any?
  158. field_statements = make_condition_for_current_collection(field, conditions_per_collection)
  159. if field_statements.length > 1
  160. statements << { '$or' => field_statements }
  161. else
  162. statements << field_statements.first
  163. end
  164. end
  165. end
  166. end
  167. if statements.any?
  168. { '$and' => statements }
  169. else
  170. {}
  171. end
  172. end
  173. def build_statement(column, type, value, operator)
  174. # this operator/value has been discarded (but kept in the dom to override the one stored in the various links of the page)
  175. return if operator == '_discard' || value == '_discard'
  176. # filtering data with unary operator, not type dependent
  177. if operator == '_blank' || value == '_blank'
  178. return { column => {'$in' => [nil, '']} }
  179. elsif operator == '_present' || value == '_present'
  180. return { column => {'$nin' => [nil, '']} }
  181. elsif operator == '_null' || value == '_null'
  182. return { column => nil }
  183. elsif operator == '_not_null' || value == '_not_null'
  184. return { column => {'$ne' => nil} }
  185. elsif operator == '_empty' || value == '_empty'
  186. return { column => '' }
  187. elsif operator == '_not_empty' || value == '_not_empty'
  188. return { column => {'$ne' => ''} }
  189. end
  190. # now we go type specific
  191. case type
  192. when :boolean
  193. return { column => false } if ['false', 'f', '0'].include?(value)
  194. return { column => true } if ['true', 't', '1'].include?(value)
  195. when :integer
  196. return if value.blank?
  197. { column => value.to_i } if value.to_i.to_s == value
  198. when :string, :text
  199. return if value.blank?
  200. value = case operator
  201. when 'default', 'like'
  202. Regexp.compile(Regexp.escape(value))
  203. when 'starts_with'
  204. Regexp.compile("^#{Regexp.escape(value)}")
  205. when 'ends_with'
  206. Regexp.compile("#{Regexp.escape(value)}$")
  207. when 'is', '='
  208. value.to_s
  209. else
  210. return
  211. end
  212. { column => value }
  213. when :date
  214. start_date, end_date = get_filtering_duration(operator, value)
  215. if start_date && end_date
  216. { column => { '$gte' => start_date, '$lte' => end_date } }
  217. elsif start_date
  218. { column => { '$gte' => start_date } }
  219. elsif end_date
  220. { column => { '$lte' => end_date } }
  221. end
  222. when :datetime, :timestamp
  223. start_date, end_date = get_filtering_duration(operator, value)
  224. if start_date && end_date
  225. { column => { '$gte' => start_date.to_time.beginning_of_day, '$lte' => end_date.to_time.end_of_day } }
  226. elsif start_date
  227. { column => { '$gte' => start_date.to_time.beginning_of_day } }
  228. elsif end_date
  229. { column => { '$lte' => end_date.to_time.end_of_day } }
  230. end
  231. when :enum
  232. return if value.blank?
  233. { column => { "$in" => Array.wrap(value) } }
  234. when :belongs_to_association, :bson_object_id
  235. object_id = (BSON::ObjectId(value) rescue nil)
  236. { column => object_id } if object_id
  237. end
  238. end
  239. def association_model_proc_lookup(association)
  240. if association.polymorphic? && [:referenced_in, :belongs_to].include?(association.macro)
  241. RailsAdmin::AbstractModel.polymorphic_parents(:mongoid, association.name) || []
  242. else
  243. association.klass
  244. end
  245. end
  246. def association_foreign_type_lookup(association)
  247. if association.polymorphic? && [:referenced_in, :belongs_to].include?(association.macro)
  248. association.inverse_type.try(:to_sym) || :"#{association.name}_type"
  249. end
  250. end
  251. def association_foreign_inverse_of_lookup(association)
  252. if association.polymorphic? && [:referenced_in, :belongs_to].include?(association.macro) && association.respond_to?(:inverse_of_field)
  253. association.inverse_of_field.try(:to_sym)
  254. end
  255. end
  256. def association_nested_attributes_options_lookup(association)
  257. nested = model.nested_attributes_options.try { |o| o[association.name.to_sym] }
  258. if !nested && [:embeds_one, :embeds_many].include?(association.macro.to_sym)
  259. raise <<-MSG.gsub(/^\s+/, '')
  260. Embbeded association without accepts_nested_attributes_for can't be handled by RailsAdmin,
  261. because embedded model doesn't have top-level access.
  262. Please add `accepts_nested_attributes_for :#{association.name}' line to `#{model.to_s}' model.
  263. MSG
  264. end
  265. nested
  266. end
  267. def association_as_lookup(association)
  268. association.as.try :to_sym
  269. end
  270. def association_polymorphic_lookup(association)
  271. !!association.polymorphic? && [:referenced_in, :belongs_to].include?(association.macro)
  272. end
  273. def association_primary_key_lookup(association)
  274. :_id # todo
  275. end
  276. def association_inverse_of_lookup(association)
  277. association.inverse_of.try :to_sym
  278. end
  279. def association_foreign_key_lookup(association)
  280. unless [:embeds_one, :embeds_many].include?(association.macro.to_sym)
  281. association.foreign_key.to_sym rescue nil
  282. end
  283. end
  284. def association_type_lookup(macro)
  285. case macro.to_sym
  286. when :belongs_to, :referenced_in, :embedded_in
  287. :belongs_to
  288. when :has_one, :references_one, :embeds_one
  289. :has_one
  290. when :has_many, :references_many, :embeds_many
  291. :has_many
  292. when :has_and_belongs_to_many, :references_and_referenced_in_many
  293. :has_and_belongs_to_many
  294. else
  295. raise "Unknown association type: #{macro.inspect}"
  296. end
  297. end
  298. def length_validation_lookup(name)
  299. shortest = model.validators.select do |validator|
  300. validator.attributes.include?(name.to_sym) &&
  301. validator.kind == :length &&
  302. validator.options[:maximum]
  303. end.min{|a, b| a.options[:maximum] <=> b.options[:maximum] }
  304. if shortest
  305. shortest.options[:maximum]
  306. else
  307. false
  308. end
  309. end
  310. def make_condition_for_current_collection(target_field, conditions_per_collection)
  311. result =[]
  312. conditions_per_collection.each do |collection_name, conditions|
  313. if collection_name == table_name
  314. # conditions referring current model column are passed directly
  315. result.concat conditions
  316. else
  317. # otherwise, collect ids of documents that satisfy search condition
  318. result.concat perform_search_on_associated_collection(target_field.name, conditions)
  319. end
  320. end
  321. result
  322. end
  323. def perform_search_on_associated_collection(field_name, conditions)
  324. target_association = associations.find{|a| a[:name] == field_name }
  325. return [] unless target_association
  326. model = target_association[:model_proc].call
  327. case target_association[:type]
  328. when :belongs_to, :has_and_belongs_to_many
  329. [{ target_association[:foreign_key].to_s => { '$in' => model.where('$or' => conditions).all.map{|r| r.send(target_association[:primary_key_proc].call)} }}]
  330. when :has_many
  331. [{ target_association[:primary_key_proc].call.to_s => { '$in' => model.where('$or' => conditions).all.map{|r| r.send(target_association[:foreign_key])} }}]
  332. end
  333. end
  334. def sort_by(options, scope)
  335. return scope unless options[:sort]
  336. field_name, collection_name = options[:sort].to_s.split('.').reverse
  337. if collection_name && collection_name != table_name
  338. # sorting by associated model column is not supported, so just ignore
  339. return scope
  340. end
  341. if options[:sort_reverse]
  342. scope.asc field_name
  343. else
  344. scope.desc field_name
  345. end
  346. end
  347. end
  348. end
  349. end