Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Mongoid support #1031

Merged
merged 42 commits into from

8 participants

@mshibuya
Collaborator

(related to #105)

Most of the features seem to be working now!

I'm also planning major refactoring on tests(making ORM switchable, running integration tests on each ORM like the way Devise does), but that may take some time, so I'll make another pull request when I've done.

Thanks again for creating and maintaining this awesome gem!

Marius Seritan and others added some commits
Marius Seritan Initial checking, identify models and lists them.
Editing, associations do not work.
99362a3
Marius Seritan Fix fetching by ID and remove debugging. e604e42
Marius Seritan Add ability to save simple mongoid objects (keys hardcoded to :_id). dbcd35d
Marius Seritan Small fix for destroy, enables delete functionality. 1df7ab6
Marius Seritan Fix search crashes, search not working yet. 7651d17
Marius Seritan Fix bulk get and destroy. 4a922c1
Marius Seritan Avoid crash on embedded array document. fb3b846
Marius Seritan Remove debugging. 1cea49e
Marius Seritan Hack search capabilities in.
1. detect LIKE condition generated by the main_controller
2. extract the search string and rebuild Mongoid query with it
789bcbb
Marius Seritan Fix bug introduced by search. bccb4db
Marius Seritan Port to latest head. 4e6ede2
fjg add missing mongoid types 238291b
@mshibuya mshibuya Preliminary support for Mongoid c2028cf
@mshibuya mshibuya Support for filters a22ff21
@mshibuya mshibuya Removed unused codes c773783
@mshibuya mshibuya Added some tests for Mongoid adapter a7ebbfa
@mshibuya mshibuya Mapped Mongoid String type to RA string/text type 82e65a6
@mshibuya mshibuya Fixed the issue with foo_ids= assignment of references_many association f196cfb
@mshibuya mshibuya Followed the changes of upstream f8d2602
@mshibuya mshibuya Fixed spec failure caused by ordering of result set e78a14c
@mshibuya mshibuya Record deletion was not working 68fed35
@mshibuya mshibuya Invoke SimpleCov with 'rake spec:coverage' 7749ecd
@mshibuya mshibuya Mongoid Hash type should be represented as text 1075d68
@mshibuya mshibuya Minimal support for Mongoid Array/Hash types dfe5b34
@mshibuya mshibuya Added specs for ActiveRecord adapter 82858c7
@mshibuya mshibuya Added specs for Mongoid adapter bacfe92
@mshibuya mshibuya Stop bypassing cancan 3536cdc
@bbenezech bbenezech Merge remote-tracking branch 'mshibuya/mongoid' into mongoid
Conflicts:
	app/controllers/rails_admin/main_controller.rb
	lib/rails_admin/adapters/active_record.rb
	lib/rails_admin/config/fields/factories/belongs_to_association.rb
0b6ec58
@bbenezech bbenezech merge latest master into mongoid branch, fix specs 662cb54
@bbenezech
Collaborator

I did some changes in active_record.rb. To spare you any useless hassle, I merged master into your branch on sferik/mongoid.

Thanks for your hard work.

Collaborator

OK, I'll take a look later on.
Thanks!

@bbenezech
Collaborator

Merged :hammer:

@bbenezech bbenezech merged commit a69281d into sferik:master
@bbenezech
Collaborator

Thanks, that's awesome. I'll update Readme and add some doc when I find the time.

@bbenezech
Collaborator

There are a few spec failures:
http://travis-ci.org/#!/sferik/rails_admin/builds/846265

Nothing big enough to revert. I'm just curious about that one: http://travis-ci.org/#!/sferik/rails_admin/jobs/846268

And disregard the rbx build, it's been failing with Rspec for a while.

@mshibuya
Collaborator

That's because I forgot to add mongoid gem to Gemfile31.. :/

I'll try to resolve those failures including that one. Please wait for a while.
Sorry for inconvenience.

@sferik
Owner

@mshibuya This is great! Thanks so much for your work on this.

@mshibuya mshibuya referenced this pull request
Merged

Fixed CI failures #1032

@bbenezech
Collaborator

@mshibuya It was only spec failures, nothing harmful.
Thanks again for tackling such a big chunk as the Mongoid adapter (and the ORM abstractions that went with it). Your code is really clean, it pleases the eyes!
Unfortunately I haven't found any time to try it, but I'm really eager to.

@sferik
Owner

@mshibuya I would be happy to add as a committer on the project if you would like. Are there any other issues you're planning to work on?

@mshibuya
Collaborator

@bbenezech
Without your work on ActiveRecord abstraction, this couldn't be done.
Thanks so much!

@sferik
Really? I'm willing to be!
Currently I have only that test refactoring one, but I'm ready to do more when I come up with another idea.
Thanks for invitation!

@sferik
Owner

@mshibuya I've added you as a collaborator. Feel free to commit directly to this repo, however, if you need to commit something experimental or untested, please do it in a branch (not master).

Thanks again for your work on RailsAdmin!

@mshibuya
Collaborator

I understand that.
Many thanks!

@khomco

I am having trouble figuring out how to configure rails_admin with giving visibility to my embedded documents. Is this supported? I have installed rails_admin successfully and can view "top-level" models, but receive an error with models that relate to being embedded_in. I appreciate any feedback.

@mshibuya
Collaborator

Yes, Mongoid embedded document is currently not supported, and that's exactly what I'm working on now!
Giving access to embedded document through parent model's nested attribute seems to work.
I think I could push it to master by tomorrow if everything goes fine.

@mshibuya
Collaborator

OK, I just pushed.
Suppose Article embeds_many Note,

class Article
  include Mongoid::Document
  field :title, :type => String
  ...
  embeds_many :notes
  accepts_nested_attributes_for :notes, :allow_destroy => true
end

class Note
  include Mongoid::Document
  embedded_in :article
  field :subject, :type => String
  field :description, :type => String
end

and you'll get access to document of Note in Article's edit view.

@bbenezech
Collaborator

Cool!

Sorting doesn't work. It can be solved easily: we should sort on 'title', not 'article.title' (same for conditions).

Include in Mongoid only solves the n+1 pb, it's not nearly as smart as the AR one, which creates a join if you sort or add conditions on the join-tables. There is no join in Mongodb, in fact. Let's find a way to describe the possibilities of the adapter and notify the user about them. (No sorting/conditions on join tables for Mongoid).

@kidoman

This is awesome guys. I am ready to help you guys test this thoroughly. But I am new to Mongoid and Rails/Ruby though.

@mshibuya
Collaborator

@bbenezech
As for disability of sorting, I noticed that problem at some point, but I've forgotten to fix it...

Not having joins is really an issue.

For conditions, I've already made a workaround for that.
I didn't want to get rid of the feature to filter records by content of associated records, so I implemented a logic to convert associated model's conditions to target model's conditions, executing search query on associated model, retrieving an array of ids which qualify the conditions. But this logic only works with directly-associated model. not working in indirectly(more than 1 step) associated model.
This could be solved by executing query recursively(searching associated model's associated model's...), but this might affect performance negatively(array of ids can be huge, depending on datasets).
What do you think of this? My opinion is that lacking the ability to search on associated model is worse than performing slow.

And sorting by associated model column appears to be impossible without joins. We should have some way(probably in Adapter's #associations) to indicate that this association is not sortable, and set the field's sortable to false.

Now I'll work on both of that.

@kid0m4n
Don't worry. I'm also new to Mongoid :)
Let's make RA better together!

@bbenezech
Collaborator

@mshibuya If I were you, I'd stick to ORM's well tested possibilities. People that use MongoDB are aware that it doesn't support joins and shouldn't expect any support for it in RailsAdmin. Instead we might look at interesting MongoDB 'specificities' that can add awesome added-value for little work and is more in line with MongoDB's philosophy.

At the end, if you write too much magic code, users tend to think as RailsAdmin as a magic box, with all the associated pain. :-)

My 2 cents.

@mshibuya
Collaborator

@bbenezech
What you are saying make sense. Putting everything into ORM adapter layer might be pretty bad thing.

Alternatively, it might better to have configurable search option which enables users to write a ORM-dependent code, by which they make a query on associated model's columns, or any custom query they need.
Currently I have no idea of what that option will be like, but it's promising.

As for now, instead of adding magic feature to Mongoid adapter, I'll prioritize ORM-switchable testing capability.

Thanks for your opinion!

@mshibuya
Collaborator

I've just pushed my test refactoring works into master.

Now we have capability to run almost all integration tests on each of ORMs!

@bbenezech
Collaborator

You, sir, are and achiever :clap:

@enriclluelles

This is great! Thanks for your work

@timkurvers

Fantastic work!

@mshibuya mshibuya referenced this pull request
Closed

Support for MongoId #157

@swistaczek

Great work!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Feb 29, 2012
  1. @mshibuya

    Initial checking, identify models and lists them.

    Marius Seritan authored mshibuya committed
    Editing, associations do not work.
  2. @mshibuya

    Fix fetching by ID and remove debugging.

    Marius Seritan authored mshibuya committed
  3. @mshibuya

    Add ability to save simple mongoid objects (keys hardcoded to :_id).

    Marius Seritan authored mshibuya committed
  4. @mshibuya

    Small fix for destroy, enables delete functionality.

    Marius Seritan authored mshibuya committed
  5. @mshibuya

    Fix search crashes, search not working yet.

    Marius Seritan authored mshibuya committed
  6. @mshibuya

    Fix bulk get and destroy.

    Marius Seritan authored mshibuya committed
  7. @mshibuya

    Avoid crash on embedded array document.

    Marius Seritan authored mshibuya committed
  8. @mshibuya

    Remove debugging.

    Marius Seritan authored mshibuya committed
  9. @mshibuya

    Hack search capabilities in.

    Marius Seritan authored mshibuya committed
    1. detect LIKE condition generated by the main_controller
    2. extract the search string and rebuild Mongoid query with it
  10. @mshibuya

    Fix bug introduced by search.

    Marius Seritan authored mshibuya committed
  11. @mshibuya

    Port to latest head.

    Marius Seritan authored mshibuya committed
  12. @mshibuya

    add missing mongoid types

    fjg authored mshibuya committed
  13. @mshibuya
  14. @mshibuya

    Support for filters

    mshibuya authored
  15. @mshibuya

    Removed unused codes

    mshibuya authored
  16. @mshibuya
  17. @mshibuya
  18. @mshibuya
  19. @mshibuya
  20. @mshibuya
  21. @mshibuya
  22. @mshibuya
  23. @mshibuya
  24. @mshibuya
  25. @mshibuya
Commits on Mar 2, 2012
  1. @mshibuya
Commits on Mar 4, 2012
  1. @mshibuya

    Stop bypassing cancan

    mshibuya authored
Commits on Mar 5, 2012
  1. @bbenezech

    Merge remote-tracking branch 'mshibuya/mongoid' into mongoid

    bbenezech authored
    Conflicts:
    	app/controllers/rails_admin/main_controller.rb
    	lib/rails_admin/adapters/active_record.rb
    	lib/rails_admin/config/fields/factories/belongs_to_association.rb
  2. @bbenezech
Commits on Mar 7, 2012
  1. @bbenezech

    Merge remote-tracking branch 'origin/master' into mongoid

    bbenezech authored
    Conflicts:
    	lib/rails_admin/adapters/active_record.rb
  2. @bbenezech

    fix specs

    bbenezech authored
  3. @mshibuya
  4. @mshibuya

    Fixed spec failure

    mshibuya authored
Commits on Mar 8, 2012
  1. @mshibuya
  2. @mshibuya
Commits on Mar 10, 2012
  1. @mshibuya
  2. @mshibuya

    Merge remote-tracking branch 'upstream/master' into mongoid

    mshibuya authored
    Conflicts:
    	lib/rails_admin/adapters/active_record.rb
  3. @mshibuya
  4. @mshibuya
  5. @mshibuya
  6. @mshibuya
Commits on Mar 12, 2012
  1. @mshibuya
This page is out of date. Refresh to see the latest.
Showing with 1,665 additions and 104 deletions.
  1. +3 −0  Gemfile
  2. +6 −1 Rakefile
  3. +9 −7 app/controllers/rails_admin/main_controller.rb
  4. +4 −0 config/initializers/mongoid_extensions.rb
  5. +55 −1 lib/rails_admin/abstract_model.rb
  6. +20 −38 lib/rails_admin/adapters/active_record.rb
  7. +333 −0 lib/rails_admin/adapters/mongoid.rb
  8. +32 −0 lib/rails_admin/adapters/mongoid/abstract_object.rb
  9. +27 −0 lib/rails_admin/adapters/mongoid/extension.rb
  10. +4 −4 lib/rails_admin/config/fields/base.rb
  11. +5 −2 lib/rails_admin/config/fields/factories/belongs_to_association.rb
  12. +1 −1  lib/rails_admin/config/fields/factories/serialized.rb
  13. +2 −1  lib/rails_admin/config/fields/types.rb
  14. +2 −0  lib/rails_admin/config/fields/types/all.rb
  15. +1 −1  lib/rails_admin/config/fields/types/belongs_to_association.rb
  16. +42 −0 lib/rails_admin/config/fields/types/bson_object_id.rb
  17. +2 −2 lib/rails_admin/config/fields/types/enum.rb
  18. +25 −0 lib/rails_admin/config/fields/types/mongoid_type.rb
  19. +1 −1  lib/rails_admin/config/fields/types/polymorphic_association.rb
  20. +1 −1  lib/rails_admin/config/fields/types/serialized.rb
  21. +1 −0  rails_admin.gemspec
  22. +2 −0  spec/dummy_app/Gemfile
  23. +9 −0 spec/dummy_app/app/models/article.rb
  24. +6 −0 spec/dummy_app/app/models/author.rb
  25. +22 −0 spec/dummy_app/app/models/mongoid_field_test.rb
  26. +7 −0 spec/dummy_app/app/models/tag.rb
  27. +17 −0 spec/dummy_app/config/mongoid.yml
  28. +7 −7 spec/dummy_app/db/seeds.rb
  29. +18 −0 spec/factories.rb
  30. +13 −0 spec/integration/basic/create/rails_admin_basic_create_spec.rb
  31. +28 −0 spec/integration/basic/update/rails_admin_basic_update_spec.rb
  32. +12 −0 spec/spec_helper.rb
  33. +330 −37 spec/unit/adapters/active_record_spec.rb
  34. +30 −0 spec/unit/adapters/mongoid/abstract_object_spec.rb
  35. +579 −0 spec/unit/adapters/mongoid_spec.rb
  36. +9 −0 spec/unit/config/fields/base_spec.rb
View
3  Gemfile
@@ -29,6 +29,8 @@ group :development, :test do
end
end
+ gem 'bson_ext'
+ gem 'mongoid'
gem 'cancan'
end
@@ -40,6 +42,7 @@ group :debug do
platform :mri_19 do
gem 'ruby-debug19'
+ gem 'simplecov', :require => false
end
end
View
7 Rakefile
@@ -11,4 +11,9 @@ RSpec::Core::RakeTask.new(:spec)
task :test => :spec
task :default => :spec
-
+namespace :spec do
+ task :coverage do
+ ENV['INVOKE_SIMPLECOV'] = 'true'
+ Rake::Task[:spec].invoke
+ end
+end
View
16 app/controllers/rails_admin/main_controller.rb
@@ -30,8 +30,10 @@ def bulk_action
end
def list_entries(model_config = @model_config, auth_scope_key = :index, additional_scope = get_association_scope_from_params, pagination = !(params[:associated_collection] || params[:all]))
- scope = @authorization_adapter && @authorization_adapter.query(auth_scope_key, model_config.abstract_model)
- scope = model_config.abstract_model.scoped.merge(scope)
+ scope = model_config.abstract_model.scoped
+ if auth_scope = @authorization_adapter && @authorization_adapter.query(auth_scope_key, model_config.abstract_model)
+ scope = scope.merge(auth_scope)
+ end
scope = scope.instance_eval(&additional_scope) if additional_scope
get_collection(model_config, scope, pagination)
@@ -53,23 +55,23 @@ def get_sort_hash(model_config)
field = model_config.list.fields.find{ |f| f.name.to_s == params[:sort] }
column = if field.nil? || field.sortable == true # use params[:sort] on the base table
- "#{abstract_model.model.table_name}.#{params[:sort]}"
+ "#{abstract_model.table_name}.#{params[:sort]}"
elsif field.sortable == false # use default sort, asked field is not sortable
- "#{abstract_model.model.table_name}.#{model_config.list.sort_by}"
+ "#{abstract_model.table_name}.#{model_config.list.sort_by}"
elsif field.sortable.is_a?(String) && field.sortable.include?('.') # just provide sortable, don't do anything smart
field.sortable
elsif field.sortable.is_a?(Hash) # just join sortable hash, don't do anything smart
"#{field.sortable.keys.first}.#{field.sortable.values.first}"
elsif field.association? # use column on target table
- "#{field.associated_model_config.abstract_model.model.table_name}.#{field.sortable}"
+ "#{field.associated_model_config.abstract_model.table_name}.#{field.sortable}"
else # use described column in the field conf.
- "#{abstract_model.model.table_name}.#{field.sortable}"
+ "#{abstract_model.table_name}.#{field.sortable}"
end
reversed_sort = (field ? field.sort_reverse? : model_config.list.sort_reverse?)
{:sort => column, :sort_reverse => (params[:sort_reverse] == reversed_sort.to_s)}
end
-
+
def redirect_to_on_success
notice = t("admin.flash.successful", :name => @model_config.label, :action => t("admin.actions.#{@action.key}.done"))
if params[:_add_another]
View
4 config/initializers/mongoid_extensions.rb
@@ -0,0 +1,4 @@
+if defined?(::Mongoid::Document)
+ require 'rails_admin/adapters/mongoid/extension'
+ Mongoid::Document.send(:include, RailsAdmin::Adapters::Mongoid::Extension)
+end
View
56 lib/rails_admin/abstract_model.rb
@@ -20,15 +20,38 @@ def new(m)
rescue LoadError, NameError
nil
end
+
+ @@polymorphic_parents = {}
+
+ def polymorphic_parents(adapter, name)
+ @@polymorphic_parents[adapter.to_sym] ||= {}.tap do |hash|
+ all(adapter).each do |am|
+ am.associations.select{|r| r[:as] }.each do |association|
+ (hash[association[:as].to_sym] ||= []) << am.model
+ end
+ end
+ end
+ @@polymorphic_parents[adapter.to_sym][name.to_sym]
+ end
+
+ # For testing
+ def reset_polymorphic_parents
+ @@polymorphic_parents = {}
+ end
end
def initialize(m)
@model_name = m.to_s
- # ActiveRecord
if m.ancestors.map(&:to_s).include?('ActiveRecord::Base') && !m.abstract_class?
+ # ActiveRecord
@adapter = :active_record
require 'rails_admin/adapters/active_record'
extend Adapters::ActiveRecord
+ elsif m.ancestors.map(&:to_s).include?('Mongoid::Document')
+ # Mongoid
+ @adapter = :mongoid
+ require 'rails_admin/adapters/mongoid'
+ extend Adapters::Mongoid
end
end
@@ -52,5 +75,36 @@ def param_key
def pretty_name
model.model_name.human
end
+
+ private
+
+ def get_filtering_duration(operator, value)
+ date_format = I18n.t("admin.misc.filter_date_format", :default => I18n.t("admin.misc.filter_date_format", :locale => :en)).gsub('dd', '%d').gsub('mm', '%m').gsub('yy', '%Y')
+ case operator
+ when 'between'
+ start_date = value[1].present? ? (beginning_of_date(Date.strptime(value[1], date_format)) rescue false) : false
+ end_date = value[2].present? ? (Date.strptime(value[2], date_format).end_of_day rescue false) : false
+ when 'today'
+ start_date = beginning_of_date(Date.today)
+ end_date = Date.today.end_of_day
+ when 'yesterday'
+ start_date = beginning_of_date(Date.yesterday)
+ end_date = Date.yesterday.end_of_day
+ when 'this_week'
+ start_date = beginning_of_date(Date.today.beginning_of_week)
+ end_date = Date.today.end_of_week.end_of_day
+ when 'last_week'
+ start_date = beginning_of_date(1.week.ago.to_date.beginning_of_week)
+ end_date = 1.week.ago.to_date.end_of_week.end_of_day
+ else # default
+ start_date = (beginning_of_date(Date.strptime(Array.wrap(value).first, date_format)) rescue false)
+ end_date = (Date.strptime(Array.wrap(value).first, date_format).end_of_day rescue false)
+ end
+ [start_date, end_date]
+ end
+
+ def beginning_of_date(date)
+ date.beginning_of_day
+ end
end
end
View
58 lib/rails_admin/adapters/active_record.rb
@@ -7,11 +7,6 @@ module ActiveRecord
DISABLED_COLUMN_TYPES = [:tsvector, :blob, :binary, :spatial]
AR_ADAPTER = ::ActiveRecord::Base.configurations[Rails.env]['adapter']
LIKE_OPERATOR = AR_ADAPTER == "postgresql" ? 'ILIKE' : 'LIKE'
- BEGINNING_OF_DAY = if AR_ADAPTER == "postgresql"
- lambda { |date| date.beginning_of_day }
- else
- lambda { |date| date.yesterday.end_of_day }
- end
def new(params = {})
AbstractObject.new(model.new(params))
@@ -90,6 +85,14 @@ def properties
end
end
+ def table_name
+ model.table_name
+ end
+
+ def serialized_attributes
+ model.serialized_attributes.keys
+ end
+
private
def query_conditions(query, fields = config.list.fields.select(&:queryable?))
@@ -168,30 +171,12 @@ def build_statement(column, type, value, operator)
"%#{value}"
when 'is', '='
"#{value}"
+ else
+ return
end
["(#{column} #{LIKE_OPERATOR} ?)", value]
when :datetime, :timestamp, :date
- date_format = I18n.t("admin.misc.filter_date_format", :default => I18n.t("admin.misc.filter_date_format", :locale => :en)).gsub('dd', '%d').gsub('mm', '%m').gsub('yy', '%Y')
- case operator
- when 'between'
- start_date = value[1].present? ? (Date.strptime(value[1], date_format).instance_eval(&BEGINNING_OF_DAY) rescue false) : false
- end_date = value[2].present? ? (Date.strptime(value[2], date_format).end_of_day rescue false) : false
- when 'today'
- start_date = Date.today.instance_eval(&BEGINNING_OF_DAY)
- end_date = Date.today.end_of_day
- when 'yesterday'
- start_date = Date.yesterday.instance_eval(&BEGINNING_OF_DAY)
- end_date = Date.yesterday.end_of_day
- when 'this_week'
- start_date = Date.today.beginning_of_week.instance_eval(&BEGINNING_OF_DAY)
- end_date = Date.today.end_of_week.end_of_day
- when 'last_week'
- start_date = 1.week.ago.to_date.beginning_of_week.instance_eval(&BEGINNING_OF_DAY)
- end_date = 1.week.ago.to_date.end_of_week.end_of_day
- else # default
- start_date = (Date.strptime(Array.wrap(value).first, date_format).instance_eval(&BEGINNING_OF_DAY) rescue false)
- end_date = (Date.strptime(Array.wrap(value).first, date_format).end_of_day rescue false)
- end
+ start_date, end_date = get_filtering_duration(operator, value)
if start_date && end_date
["(#{column} BETWEEN ? AND ?)", start_date, end_date]
@@ -206,22 +191,19 @@ def build_statement(column, type, value, operator)
end
end
- @@polymorphic_parents = nil
-
- def self.polymorphic_parents(name)
- @@polymorphic_parents ||= {}.tap do |hash|
- RailsAdmin::AbstractModel.all(:active_record).each do |am|
- am.model.reflect_on_all_associations.select{|r| r.options[:as] }.each do |reflection|
- (hash[reflection.options[:as].to_sym] ||= []) << am.model
- end
- end
+ if AR_ADAPTER == "postgresql"
+ def beginning_of_date(date)
+ date.beginning_of_day
+ end
+ else
+ def beginning_of_date(date)
+ date.yesterday.end_of_day
end
- @@polymorphic_parents[name.to_sym]
end
def association_model_lookup(association)
if association.options[:polymorphic]
- RailsAdmin::Adapters::ActiveRecord.polymorphic_parents(association.name) || []
+ RailsAdmin::AbstractModel.polymorphic_parents(:active_record, association.name) || []
else
association.klass
end
@@ -242,7 +224,7 @@ def association_as_lookup(association)
end
def association_polymorphic_lookup(association)
- association.options[:polymorphic]
+ !!association.options[:polymorphic]
end
def association_primary_key_lookup(association)
View
333 lib/rails_admin/adapters/mongoid.rb
@@ -0,0 +1,333 @@
+require 'mongoid'
+require 'rails_admin/config/sections/list'
+require 'rails_admin/adapters/mongoid/abstract_object'
+
+module RailsAdmin
+ module Adapters
+ module Mongoid
+ STRING_TYPE_COLUMN_NAMES = [:name, :title, :subject]
+
+ def new(params = {})
+ AbstractObject.new(model.new)
+ end
+
+ def get(id)
+ if object = model.where(:_id=>BSON::ObjectId(id)).first
+ AbstractObject.new object
+ else
+ nil
+ end
+ end
+
+ def scoped
+ model.scoped
+ end
+
+ def first(options = {},scope=nil)
+ all(options, scope).first
+ end
+
+ def all(options = {},scope=nil)
+ scope ||= self.scoped
+ scope = scope.includes(options[:include]) if options[:include]
+ scope = scope.limit(options[:limit]) if options[:limit]
+ scope = scope.any_in(:_id => options[:bulk_ids]) if options[:bulk_ids]
+ scope = scope.where(query_conditions(options[:query])) if options[:query]
+ scope = scope.where(filter_conditions(options[:filters])) if options[:filters]
+ scope = scope.page(options[:page]).per(options[:per]) if options[:page] && options[:per]
+ scope = if options[:sort] && options[:sort_reverse]
+ scope.desc(options[:sort])
+ elsif options[:sort]
+ scope.asc(options[:sort])
+ else
+ scope
+ end
+ end
+
+ def count(options = {},scope=nil)
+ all(options.merge({:limit => false, :page => false}), scope).count
+ end
+
+ def destroy(objects)
+ Array.wrap(objects).each &:destroy
+ end
+
+ def primary_key
+ :_id
+ end
+
+ def associations
+ model.associations.values.map do |association|
+ {
+ :name => association.name.to_sym,
+ :pretty_name => association.name.to_s.tr('_', ' ').capitalize,
+ :type => association_type_lookup(association.macro),
+ :model_proc => Proc.new { association_model_proc_lookup(association) },
+ :primary_key_proc => Proc.new { association_primary_key_lookup(association) },
+ :foreign_key => association_foreign_key_lookup(association),
+ :foreign_type => association_foreign_type_lookup(association),
+ :as => association_as_lookup(association),
+ :polymorphic => association_polymorphic_lookup(association),
+ :inverse_of => association_inverse_of_lookup(association),
+ :read_only => nil,
+ :nested_form => nil
+ }
+ end
+ end
+
+ def properties
+ @properties if @properties
+ @properties = model.fields.map do |name,field|
+ ar_type =
+ if name == '_type'
+ { :type => :mongoid_type, :length => 1024 }
+ elsif field.type.to_s == 'String'
+ if (length = length_validation_lookup(name)) && length < 256
+ { :type => :string, :length => length }
+ elsif STRING_TYPE_COLUMN_NAMES.include?(name.to_sym)
+ { :type => :string, :length => 255 }
+ else
+ { :type => :text, :length => nil }
+ end
+ else
+ {
+ "Array" => { :type => :serialized, :length => nil },
+ "BigDecimal" => { :type => :string, :length => 1024 },
+ "Boolean" => { :type => :boolean, :length => nil },
+ "BSON::ObjectId" => { :type => :bson_object_id, :length => nil },
+ "Date" => { :type => :date, :length => nil },
+ "DateTime" => { :type => :datetime, :length => nil },
+ "Float" => { :type => :float, :length => nil },
+ "Hash" => { :type => :serialized, :length => nil },
+ "Integer" => { :type => :integer, :length => nil },
+ "Time" => { :type => :datetime, :length => nil },
+ "Object" => { :type => :bson_object_id, :length => nil },
+ }[field.type.to_s] or raise "Need to map field #{field.type.to_s} for field name #{name} in #{model.inspect}"
+ end
+
+ {
+ :name => field.name.to_sym,
+ :pretty_name => field.name.to_s.gsub('_', ' ').strip.capitalize,
+ :nullable? => true,
+ :serial? => false,
+ }.merge(ar_type)
+ end
+ end
+
+ def table_name
+ model.collection.name
+ end
+
+ def serialized_attributes
+ # Mongoid Array and Hash type columns are mapped to RA serialized type
+ # through type detection in self#properties.
+ []
+ end
+
+ private
+
+ def query_conditions(query, fields = config.list.fields.select(&:queryable?))
+ statements = []
+
+ fields.each do |field|
+ conditions_per_collection = {}
+ field.searchable_columns.flatten.each do |column_infos|
+ collection_name, column_name = column_infos[:column].split('.')
+ statement = build_statement(column_name, column_infos[:type], query, field.search_operator)
+ if statement
+ conditions_per_collection[collection_name] ||= []
+ conditions_per_collection[collection_name] << statement
+ end
+ end
+ statements.concat make_condition_for_current_collection(field, conditions_per_collection)
+ end
+
+ if statements.any?
+ { '$or' => statements }
+ else
+ {}
+ end
+ end
+
+ # filters example => {"string_field"=>{"0055"=>{"o"=>"like", "v"=>"test_value"}}, ...}
+ # "0055" is the filter index, no use here. o is the operator, v the value
+ def filter_conditions(filters, fields = config.list.fields.select(&:filterable?))
+ statements = []
+
+ filters.each_pair do |field_name, filters_dump|
+ filters_dump.each do |filter_index, filter_dump|
+ conditions_per_collection = {}
+ field = fields.find{|f| f.name.to_s == field_name}
+ next unless field
+ field.searchable_columns.each do |column_infos|
+ collection_name, column_name = column_infos[:column].split('.')
+ statement = build_statement(column_name, column_infos[:type], filter_dump[:v], (filter_dump[:o] || 'default'))
+ if statement
+ conditions_per_collection[collection_name] ||= []
+ conditions_per_collection[collection_name] << statement
+ end
+ end
+ if conditions_per_collection.any?
+ field_statements = make_condition_for_current_collection(field, conditions_per_collection)
+ if field_statements.length > 1
+ statements << { '$or' => field_statements }
+ else
+ statements << field_statements.first
+ end
+ end
+ end
+ end
+
+ if statements.any?
+ { '$and' => statements }
+ else
+ {}
+ end
+ end
+
+ def build_statement(column, type, value, operator)
+ # this operator/value has been discarded (but kept in the dom to override the one stored in the various links of the page)
+ return if operator == '_discard' || value == '_discard'
+
+ # filtering data with unary operator, not type dependent
+ if operator == '_blank' || value == '_blank'
+ return { column => {'$in' => [nil, '']} }
+ elsif operator == '_present' || value == '_present'
+ return { column => {'$nin' => [nil, '']} }
+ elsif operator == '_null' || value == '_null'
+ return { column => nil }
+ elsif operator == '_not_null' || value == '_not_null'
+ return { column => {'$ne' => nil} }
+ elsif operator == '_empty' || value == '_empty'
+ return { column => '' }
+ elsif operator == '_not_empty' || value == '_not_empty'
+ return { column => {'$ne' => ''} }
+ end
+ # now we go type specific
+ case type
+ when :boolean
+ return { column => false } if ['false', 'f', '0'].include?(value)
+ return { column => true } if ['true', 't', '1'].include?(value)
+ when :integer, :belongs_to_association
+ return if value.blank?
+ { column => value.to_i } if value.to_i.to_s == value
+ when :string, :text
+ return if value.blank?
+ value = case operator
+ when 'default', 'like'
+ Regexp.compile(Regexp.escape(value))
+ when 'starts_with'
+ Regexp.compile("^#{Regexp.escape(value)}")
+ when 'ends_with'
+ Regexp.compile("#{Regexp.escape(value)}$")
+ when 'is', '='
+ value.to_s
+ else
+ return
+ end
+ { column => value }
+ when :datetime, :timestamp, :date
+ start_date, end_date = get_filtering_duration(operator, value)
+
+ if start_date && end_date
+ { column => { '$gte' => start_date, '$lte' => end_date } }
+ elsif start_date
+ { column => { '$gte' => start_date } }
+ elsif end_date
+ { column => { '$lte' => end_date } }
+ end
+ when :enum
+ return if value.blank?
+ { column => { "$in" => Array.wrap(value) } }
+ end
+ end
+
+ def association_model_proc_lookup(association)
+ if association.polymorphic? && association.macro == :referenced_in
+ RailsAdmin::AbstractModel.polymorphic_parents(:mongoid, association.name) || []
+ else
+ association.klass
+ end
+ end
+
+ def association_foreign_type_lookup(association)
+ if association.polymorphic? && association.macro == :referenced_in
+ association.inverse_type.try(:to_sym) || :"#{association.name}_type"
+ end
+ end
+
+ def association_as_lookup(association)
+ association.as.try :to_sym
+ end
+
+ def association_polymorphic_lookup(association)
+ !!association.polymorphic? && association.macro == :referenced_in
+ end
+
+ def association_primary_key_lookup(association)
+ :_id # todo
+ end
+
+ def association_inverse_of_lookup(association)
+ association.inverse_of.try :to_sym
+ end
+
+ def association_foreign_key_lookup(association)
+ association.foreign_key.to_sym rescue nil
+ end
+
+ def association_type_lookup(macro)
+ case macro.to_sym
+ when :referenced_in, :embedded_in
+ :belongs_to
+ when :references_one, :embeds_one
+ :has_one
+ when :references_many, :embeds_many
+ :has_many
+ when :references_and_referenced_in_many
+ :has_and_belongs_to_many
+ else
+ raise "Unknown association type: #{macro.inspect}"
+ end
+ end
+
+ def length_validation_lookup(name)
+ shortest = model.validators.select do |validator|
+ validator.attributes.include?(name.to_sym) &&
+ validator.class == ActiveModel::Validations::LengthValidator
+ end.min{|a, b| a.options[:maximum] <=> b.options[:maximum] }
+ if shortest
+ shortest.options[:maximum]
+ else
+ false
+ end
+ end
+
+ def make_condition_for_current_collection(target_field, conditions_per_collection)
+ result =[]
+ conditions_per_collection.each do |collection_name, conditions|
+ if collection_name == table_name
+ # conditions referring current model column are passed directly
+ result.concat conditions
+ else
+ # otherwise, collect ids of documents that satisfy search condition
+ result.concat perform_search_on_associated_collection(target_field.name, conditions)
+ end
+ end
+ result
+ end
+
+ def perform_search_on_associated_collection(field_name, conditions)
+ target_association = associations.find{|a| a[:name] == field_name }
+ return [] unless target_association
+ model = target_association[:model_proc].call
+ case target_association[:type]
+ when :belongs_to, :has_and_belongs_to_many
+ [{ target_association[:foreign_key].to_s => { '$in' => model.where('$or' => conditions).all.map{|r| r.send(target_association[:primary_key_proc].call)} }}]
+ when :has_many
+ [{ target_association[:primary_key_proc].call.to_s => { '$in' => model.where('$or' => conditions).all.map{|r| r.send(target_association[:foreign_key])} }}]
+ end
+ end
+ end
+ end
+end
View
32 lib/rails_admin/adapters/mongoid/abstract_object.rb
@@ -0,0 +1,32 @@
+require 'rails_admin/adapters/active_record/abstract_object'
+module RailsAdmin
+ module Adapters
+ module Mongoid
+ class AbstractObject < RailsAdmin::Adapters::ActiveRecord::AbstractObject
+ def initialize(object)
+ super
+ object.associations.each do |name, association|
+ if association.macro == :references_many
+ instance_eval <<-RUBY, __FILE__, __LINE__ + 1
+ def #{name.to_s.singularize}_ids
+ #{name}.map{|item| item.id }
+ end
+
+ def #{name.to_s.singularize}_ids=(items)
+ self.#{name} = items.
+ map{|item_id| self.#{name}.klass.find(item_id) rescue nil }.
+ compact
+ end
+RUBY
+ end
+ end
+ end
+
+ def destroy
+ object.destroy
+ object
+ end
+ end
+ end
+ end
+end
View
27 lib/rails_admin/adapters/mongoid/extension.rb
@@ -0,0 +1,27 @@
+module RailsAdmin
+ module Adapters
+ module Mongoid
+ module Extension
+ extend ActiveSupport::Concern
+
+ included do
+ def self.rails_admin(&block)
+ RailsAdmin::Config.model(self, &block)
+ end
+ end
+
+ def rails_admin_default_object_label_method
+ self.new_record? ? "new #{self.class.to_s}" : "#{self.class.to_s} ##{self.id}"
+ end
+
+ def safe_send(value)
+ if self.attributes.find{ |k,v| k.to_s == value.to_s }
+ self.read_attribute(value)
+ else
+ self.send(value)
+ end
+ end
+ end
+ end
+ end
+end
View
8 lib/rails_admin/config/fields/base.rb
@@ -73,11 +73,11 @@ def virtual?
register_instance_option :searchable_columns do
@searchable_columns ||= case self.searchable
when true
- [{ :column => "#{self.abstract_model.model.table_name}.#{self.name}", :type => self.type }]
+ [{ :column => "#{self.abstract_model.table_name}.#{self.name}", :type => self.type }]
when false
[]
when :all # valid only for associations
- table_name = self.associated_model_config.abstract_model.model.table_name
+ table_name = self.associated_model_config.abstract_model.table_name
self.associated_model_config.list.fields.map { |f| { :column => "#{table_name}.#{f.name}", :type => f.type } }
else
[self.searchable].flatten.map do |f|
@@ -86,13 +86,13 @@ def virtual?
type = nil
elsif f.is_a?(Hash) # <Model|table_name> => <attribute|column>
am = f.keys.first.is_a?(Class) && AbstractModel.new(f.keys.first)
- table_name = am && am.model.table_name || f.keys.first
+ table_name = am && am.table_name || f.keys.first
column = f.values.first
property = am && am.properties.find{ |p| p[:name] == f.values.first.to_sym }
type = property && property[:type]
else # <attribute|column>
am = (self.association? ? self.associated_model_config.abstract_model : self.abstract_model)
- table_name = am.model.table_name
+ table_name = am.table_name
column = f
property = am.properties.find{ |p| p[:name] == f.to_sym }
type = property && property[:type]
View
7 lib/rails_admin/config/fields/factories/belongs_to_association.rb
@@ -4,7 +4,7 @@
RailsAdmin::Config::Fields.register_factory do |parent, properties, fields|
if association = parent.abstract_model.associations.find {|a| a[:foreign_key] == properties[:name] }
- field = RailsAdmin::Config::Fields::Types.load("#{association[:polymorphic] ? :polymorphic : :belongs_to}_association").new(parent, association[:name], association)
+ field = RailsAdmin::Config::Fields::Types.load("#{association[:polymorphic] ? :polymorphic : association[:type]}_association").new(parent, association[:name], association)
fields << field
child_columns = []
@@ -13,7 +13,10 @@
if association[:polymorphic]
type_colum = parent.abstract_model.properties.find {|p| p[:name].to_s == association[:foreign_type].to_s }
- child_columns << RailsAdmin::Config::Fields.default_factory.call(parent, type_colum, fields)
+ unless type_field = fields.find{|f| f.name.to_s == type_colum[:name].to_s }
+ type_field = RailsAdmin::Config::Fields.default_factory.call(parent, type_colum, fields)
+ end
+ child_columns << type_field
end
child_columns.each do |child_column|
View
2  lib/rails_admin/config/fields/factories/serialized.rb
@@ -8,7 +8,7 @@
# @see RailsAdmin::Config::Fields::Types::Password.column_names
# @see RailsAdmin::Config::Fields.register_factory
RailsAdmin::Config::Fields.register_factory do |parent, properties, fields|
- if parent.abstract_model.model.serialized_attributes.keys.include?(properties[:name].to_s)
+ if parent.abstract_model.serialized_attributes.include?(properties[:name].to_s)
fields << RailsAdmin::Config::Fields::Types::Serialized.new(parent, properties[:name], properties)
true
else
View
3  lib/rails_admin/config/fields/types.rb
@@ -1,5 +1,6 @@
require 'active_support/core_ext/string/inflections'
require 'rails_admin/config/fields'
+require 'rails_admin/config/fields/association'
module RailsAdmin
module Config
@@ -8,7 +9,7 @@ module Types
@@registry = {}
def self.load(type)
- @@registry[type.to_sym] or logger.info "Unsupported field datatype: #{type}"
+ @@registry[type.to_sym] or raise "Unsupported field datatype: #{type}"
end
def self.register(type, klass = nil)
View
2  lib/rails_admin/config/fields/types/all.rb
@@ -1,5 +1,6 @@
require 'rails_admin/config/fields/types/belongs_to_association'
require 'rails_admin/config/fields/types/boolean'
+require 'rails_admin/config/fields/types/bson_object_id'
require 'rails_admin/config/fields/types/date'
require 'rails_admin/config/fields/types/datetime'
require 'rails_admin/config/fields/types/decimal'
@@ -13,6 +14,7 @@
require 'rails_admin/config/fields/types/has_many_association'
require 'rails_admin/config/fields/types/has_one_association'
require 'rails_admin/config/fields/types/integer'
+require 'rails_admin/config/fields/types/mongoid_type'
require 'rails_admin/config/fields/types/password'
require 'rails_admin/config/fields/types/polymorphic_association'
require 'rails_admin/config/fields/types/string'
View
2  lib/rails_admin/config/fields/types/belongs_to_association.rb
@@ -12,7 +12,7 @@ class BelongsToAssociation < RailsAdmin::Config::Fields::Association
end
register_instance_option :sortable do
- @sortable ||= associated_model_config.abstract_model.properties.map{ |p| p[:name] }.include?(associated_model_config.object_label_method) ? associated_model_config.object_label_method : {self.abstract_model.model.table_name => self.method_name}
+ @sortable ||= associated_model_config.abstract_model.properties.map{ |p| p[:name] }.include?(associated_model_config.object_label_method) ? associated_model_config.object_label_method : {self.abstract_model.table_name => self.method_name}
end
register_instance_option :searchable do
View
42 lib/rails_admin/config/fields/types/bson_object_id.rb
@@ -0,0 +1,42 @@
+require 'rails_admin/config/fields/types/string'
+
+module RailsAdmin
+ module Config
+ module Fields
+ module Types
+ class BsonObjectId < RailsAdmin::Config::Fields::Types::String
+ # Register field type for the type loader
+ RailsAdmin::Config::Fields::Types::register(self)
+
+ register_instance_option(:label) do
+ label = ((@label ||= {})[::I18n.locale] ||= abstract_model.model.human_attribute_name name)
+ label = "_id" if label == ''
+ label
+ end
+
+ register_instance_option(:help) do
+ "BSON::ObjectId"
+ end
+
+ register_instance_option(:read_only) do
+ true
+ end
+
+ register_instance_option(:visible?) do
+ @name.to_s != '_id'
+ end
+
+ def parse_input(params)
+ begin
+ params[name] = (params[name].blank? ? nil : BSON::ObjectId(params[name])) if params[name].is_a?(::String)
+ rescue BSON::InvalidObjectId
+ end
+ end
+ end
+ end
+ end
+ end
+end
+
+
+
View
4 lib/rails_admin/config/fields/types/enum.rb
@@ -20,9 +20,9 @@ class Enum < RailsAdmin::Config::Fields::Base
end
register_instance_option(:pretty_value) do
- if enum.is_a?(Hash)
+ if enum.is_a?(::Hash)
enum.reject{|k,v| v.to_s != value.to_s}.keys.first.to_s.presence || value.presence || ' - '
- elsif enum.is_a?(Array) && enum.first.is_a?(Array)
+ elsif enum.is_a?(::Array) && enum.first.is_a?(::Array)
enum.find{|e|e[1].to_s == value.to_s}.try(:first).to_s.presence || value.presence || ' - '
else
value.presence || ' - '
View
25 lib/rails_admin/config/fields/types/mongoid_type.rb
@@ -0,0 +1,25 @@
+require 'rails_admin/config/fields/types/string'
+
+module RailsAdmin
+ module Config
+ module Fields
+ module Types
+ class MongoidType < RailsAdmin::Config::Fields::Types::String
+ # Register field type for the type loader
+ RailsAdmin::Config::Fields::Types::register(self)
+
+ register_instance_option(:label) do
+ "Type"
+ end
+
+ register_instance_option(:visible) do
+ false
+ end
+ end
+ end
+ end
+ end
+end
+
+
+
View
2  lib/rails_admin/config/fields/types/polymorphic_association.rb
@@ -60,7 +60,7 @@ def polymorphic_type_urls
[config.abstract_model.model.name, config.abstract_model.to_param]
end
- Hash[*types.collect { |v|
+ ::Hash[*types.collect { |v|
[v[0], bindings[:view].index_path(v[1])]
}.flatten]
end
View
2  lib/rails_admin/config/fields/types/serialized.rb
@@ -13,7 +13,7 @@ class Serialized < RailsAdmin::Config::Fields::Types::Text
end
def parse_input(params)
- params[name] = (params[name].blank? ? nil : YAML.load(params[name]))
+ params[name] = (params[name].blank? ? nil : YAML.load(params[name])) if params[name].is_a?(::String)
end
end
end
View
1  rails_admin.gemspec
@@ -24,6 +24,7 @@ Gem::Specification.new do |gem|
gem.add_development_dependency 'mini_magick'
gem.add_development_dependency 'paperclip'
gem.add_development_dependency 'rspec-rails'
+ gem.add_development_dependency 'timecop'
gem.authors = ["Erik Michaels-Ober", "Bogdan Gaza", "Petteri Kaapa", "Benoit Benezech"]
gem.description = %q{RailsAdmin is a Rails engine that provides an easy-to-use interface for managing your data.}
gem.email = ['sferik@gmail.com', 'bogdan@cadmio.org', 'petteri.kaapa@gmail.com']
View
2  spec/dummy_app/Gemfile
@@ -2,6 +2,8 @@ source 'https://rubygems.org'
gem 'rails', '~> 3.2'
gem 'devise', '~> 2.0'
+gem 'bson_ext'
+gem 'mongoid'
gem 'rails_admin', :path => '../../'
gem 'mlb', '~> 0.5'
gem 'paperclip', '~> 2.4'
View
9 spec/dummy_app/app/models/article.rb
@@ -0,0 +1,9 @@
+class Article
+ include Mongoid::Document
+
+ field :title, :type => String
+ field :body, :type => String
+
+ referenced_in :author
+ references_and_referenced_in_many :tags
+end
View
6 spec/dummy_app/app/models/author.rb
@@ -0,0 +1,6 @@
+class Author
+ include Mongoid::Document
+
+ field :name, :type => String
+ references_many :articles
+end
View
22 spec/dummy_app/app/models/mongoid_field_test.rb
@@ -0,0 +1,22 @@
+class MongoidFieldTest
+ include Mongoid::Document
+
+ field :name, :type => String
+ field :title, :type => String
+ field :subject, :type => String
+ field :description, :type => String
+ field :short_text, :type => String
+ field :array_field, :type => Array
+ field :big_decimal_field, :type => BigDecimal
+ field :boolean_field, :type => Boolean
+ field :bson_object_id_field, :type => BSON::ObjectId
+ field :date_field, :type => Date
+ field :date_time_field, :type => DateTime
+ field :float_field, :type => Float
+ field :hash_field, :type => Hash
+ field :integer_field, :type => Integer
+ field :time_field, :type => Time
+ field :object_field, :type => Object
+
+ validates :short_text, :length => {:maximum => 255}
+end
View
7 spec/dummy_app/app/models/tag.rb
@@ -0,0 +1,7 @@
+class Tag
+ include Mongoid::Document
+
+ field :name, :type => String
+
+ references_and_referenced_in_many :articles
+end
View
17 spec/dummy_app/config/mongoid.yml
@@ -0,0 +1,17 @@
+defaults: &defaults
+ host: localhost
+ autocreate_indexes: false
+ allow_dynamic_fields: true
+ include_root_in_json: false
+ parameterize_keys: true
+ persist_in_safe_mode: false
+ raise_not_found_error: true
+ reconnect_time: 3
+
+development:
+ <<: *defaults
+ database: dummy_app_development
+
+test:
+ <<: *defaults
+ database: dummy_app_test
View
14 spec/dummy_app/db/seeds.rb
@@ -3,16 +3,16 @@
User.create(:email => 'username@example.com', :password => 'password', :password_confirmation => 'password')
MLB::Team.all.each do |mlb_team|
- unless league = RailsAdmin::AbstractModel.new("League").first(:conditions => ["name = ?", mlb_team.league])
- league = RailsAdmin::AbstractModel.new("League").create(:name => mlb_team.league)
+ unless league = League.first(:conditions => ["name = ?", mlb_team.league])
+ league = League.create(:name => mlb_team.league)
end
- unless division = RailsAdmin::AbstractModel.new("Division").first(:conditions => ["name = ?", mlb_team.division])
- division = RailsAdmin::AbstractModel.new("Division").create(:name => mlb_team.division, :league => league)
+ unless division = Division.first(:conditions => ["name = ?", mlb_team.division])
+ division = Division.create(:name => mlb_team.division, :league => league)
end
- unless team = RailsAdmin::AbstractModel.new("Team").first(:conditions => ["name = ?", mlb_team.name])
- team = RailsAdmin::AbstractModel.new("Team").create(:name => mlb_team.name, :logo_url => mlb_team.logo_url, :manager => mlb_team.manager, :ballpark => mlb_team.ballpark, :mascot => mlb_team.mascot, :founded => mlb_team.founded, :wins => mlb_team.wins, :losses => mlb_team.losses, :win_percentage => ("%.3f" % (mlb_team.wins.to_f / (mlb_team.wins + mlb_team.losses))).to_f, :division => division)
+ unless team = Team.first(:conditions => ["name = ?", mlb_team.name])
+ team = Team.create(:name => mlb_team.name, :logo_url => mlb_team.logo_url, :manager => mlb_team.manager, :ballpark => mlb_team.ballpark, :mascot => mlb_team.mascot, :founded => mlb_team.founded, :wins => mlb_team.wins, :losses => mlb_team.losses, :win_percentage => ("%.3f" % (mlb_team.wins.to_f / (mlb_team.wins + mlb_team.losses))).to_f, :division => division)
end
mlb_team.players.reject{|player| player.number.nil?}.each do |player|
- RailsAdmin::AbstractModel.new("Player").create(:name => player.name, :number => player.number, :position => player.position, :team => team)
+ Player.create(:name => player.name, :number => player.number, :position => player.position, :team => team)
end
end
View
18 spec/factories.rb
@@ -63,4 +63,22 @@
factory :hardball do
color('blue')
end
+
+ factory :article do
+ sequence(:title) { |n| "Article #{n}" }
+ end
+
+ factory :author do
+ sequence(:name) { |n| "Author #{n}" }
+ end
+
+ factory :tag do
+ sequence(:name) { |n| "Tag #{n}" }
+ end
+
+ factory :mongoid_field_test do
+ sequence(:name) { |n| "Mongoid Field Test #{n}" }
+ array_field([1])
+ hash_field({:a => 1})
+ end
end
View
13 spec/integration/basic/create/rails_admin_basic_create_spec.rb
@@ -138,4 +138,17 @@
should have_content("Player failed to be created. Player is cheating")
end
end
+
+ describe "creation of Mongoid habtm model object" do
+ before(:each) do
+ visit new_path(:model_name => "tag")
+ fill_in "tag[name]", :with => "Funny"
+ click_button "Save"
+ @tag = RailsAdmin::AbstractModel.new("Tag").first
+ end
+
+ it "should create an object with correct attributes" do
+ @tag.name.should eql("Funny")
+ end
+ end
end
View
28 spec/integration/basic/update/rails_admin_basic_update_spec.rb
@@ -154,6 +154,34 @@
end
end
+ describe "update with serialized objects of Mongoid" do
+ before(:each) do
+ @field_test = FactoryGirl.create :mongoid_field_test
+
+ visit edit_path(:model_name => "mongoid_field_test", :id => @field_test.id)
+ end
+
+ it "should save the serialized data" do
+ fill_in "mongoid_field_test[array_field]", :with => "[4, 2]"
+ fill_in "mongoid_field_test[hash_field]", :with => "{ a: 6, b: 2 }"
+ click_button "Save"
+
+ @field_test.reload
+ @field_test.array_field.should eql([4, 2])
+ @field_test.hash_field.should eql({ "a" => 6, "b" => 2 })
+ end
+
+ it "should clear data when empty string is passed" do
+ fill_in "mongoid_field_test[array_field]", :with => ""
+ fill_in "mongoid_field_test[hash_field]", :with => ""
+ click_button "Save"
+
+ @field_test.reload
+ @field_test.array_field.should eql(nil)
+ @field_test.hash_field.should eql(nil)
+ end
+ end
+
describe "update with overridden to_param" do
before(:each) do
@ball = FactoryGirl.create :ball
View
12 spec/spec_helper.rb
@@ -1,6 +1,12 @@
# Configure Rails Envinronment
ENV["RAILS_ENV"] = "test"
ENV['SKIP_RAILS_ADMIN_INITIALIZER'] = 'true'
+
+if ENV['INVOKE_SIMPLECOV']
+ require 'simplecov'
+ SimpleCov.start 'rails'
+end
+
require File.expand_path('../dummy_app/config/environment', __FILE__)
require 'rspec/rails'
@@ -61,6 +67,10 @@ def password_digest(password)
Team.delete_all
User.delete_all
FieldTest.delete_all
+ Author.delete_all
+ Article.delete_all
+ MongoidFieldTest.delete_all
+ Tag.delete_all
login_as User.create(
:email => "username@example.com",
:password => "password"
@@ -70,4 +80,6 @@ def password_digest(password)
config.after(:each) do
Warden.test_reset!
end
+
+ config.seed = ENV['SEED'] if ENV['SEED']
end
View
367 spec/unit/adapters/active_record_spec.rb
@@ -1,75 +1,368 @@
require 'spec_helper'
+require 'timecop'
require 'rails_admin/adapters/active_record'
describe RailsAdmin::Adapters::ActiveRecord do
+ before do
+ @like = ::ActiveRecord::Base.configurations[Rails.env]['adapter'] == "postgresql" ? 'ILIKE' : 'LIKE'
+ end
+
+ describe '#associations' do
+ before :all do
+ RailsAdmin::AbstractModel.reset_polymorphic_parents
+
+ class ARBlog < ActiveRecord::Base
+ has_many :a_r_posts
+ has_many :a_r_comments, :as => :commentable
+ end
+
+ class ARPost < ActiveRecord::Base
+ belongs_to :a_r_blog
+ has_and_belongs_to_many :a_r_categories
+ has_many :a_r_comments, :as => :commentable
+ end
- before :all do
+ class ARCategory < ActiveRecord::Base
+ has_and_belongs_to_many :a_r_posts
+ end
- class ARBlog < ActiveRecord::Base
- has_many :a_r_posts
- has_many :a_r_comments, :as => :commentable
+ class ARUser < ActiveRecord::Base
+ has_one :a_r_profile
+ end
+
+ class ARProfile < ActiveRecord::Base
+ belongs_to :a_r_user
+ end
+
+ class ARComment < ActiveRecord::Base
+ belongs_to :commentable, :polymorphic => true
+ end
+
+ @blog = RailsAdmin::AbstractModel.new(ARBlog)
+ @post = RailsAdmin::AbstractModel.new(ARPost)
+ @category = RailsAdmin::AbstractModel.new(ARCategory)
+ @user = RailsAdmin::AbstractModel.new(ARUser)
+ @profile = RailsAdmin::AbstractModel.new(ARProfile)
+ @comment = RailsAdmin::AbstractModel.new(ARComment)
end
- class ARPost < ActiveRecord::Base
- belongs_to :a_r_blog
- has_and_belongs_to_many :a_r_categories
- has_many :a_r_comments, :as => :commentable
+ after :all do
+ RailsAdmin::AbstractModel.reset_polymorphic_parents
end
- class ARCategory < ActiveRecord::Base
- has_and_belongs_to_many :a_r_posts
+ it 'lists associations' do
+ @post.associations.map{|a|a[:name].to_s}.sort.should == ['a_r_blog', 'a_r_categories', 'a_r_comments']
+ end
+
+ it 'list associations types in supported [:belongs_to, :has_and_belongs_to_many, :has_many, :has_one]' do
+ (@post.associations + @blog.associations + @user.associations).map{|a|a[:type]}.uniq.map(&:to_s).sort.should == ['belongs_to', 'has_and_belongs_to_many', 'has_many', 'has_one']
+ end
+
+ it "has correct parameter of belongs_to association" do
+ param = @post.associations.select{|a| a[:name] == :a_r_blog}.first
+ param.reject{|k, v| [:primary_key_proc, :model_proc].include? k }.should == {
+ :name=>:a_r_blog,
+ :pretty_name=>"A r blog",
+ :type=>:belongs_to,
+ :foreign_key=>:a_r_blog_id,
+ :foreign_type=>nil,
+ :as=>nil,
+ :polymorphic=>false,
+ :inverse_of=>nil,
+ :read_only=>nil,
+ :nested_form=>nil
+ }
+ param[:primary_key_proc].call.should == 'id'
+ param[:model_proc].call.should == ARBlog
end
- class ARUser < ActiveRecord::Base
- has_one :a_r_profile
+ it "has correct parameter of has_many association" do
+ param = @blog.associations.select{|a| a[:name] == :a_r_posts}.first
+ param.reject{|k, v| [:primary_key_proc, :model_proc].include? k }.should == {
+ :name=>:a_r_posts,
+ :pretty_name=>"A r posts",
+ :type=>:has_many,
+ :foreign_key=>:ar_blog_id,
+ :foreign_type=>nil,
+ :as=>nil,
+ :polymorphic=>false,
+ :inverse_of=>nil,
+ :read_only=>nil,
+ :nested_form=>nil
+ }
+ param[:primary_key_proc].call.should == 'id'
+ param[:model_proc].call.should == ARPost
end
- class ARProfile < ActiveRecord::Base
- belongs_to :a_r_user
+ it "has correct parameter of has_and_belongs_to_many association" do
+ param = @post.associations.select{|a| a[:name] == :a_r_categories}.first
+ param.reject{|k, v| [:primary_key_proc, :model_proc].include? k }.should == {
+ :name=>:a_r_categories,
+ :pretty_name=>"A r categories",
+ :type=>:has_and_belongs_to_many,
+ :foreign_key=>:ar_post_id,
+ :foreign_type=>nil,
+ :as=>nil,
+ :polymorphic=>false,
+ :inverse_of=>nil,
+ :read_only=>nil,
+ :nested_form=>nil
+ }
+ param[:primary_key_proc].call.should == 'id'
+ param[:model_proc].call.should == ARCategory
end
- class ARComment < ActiveRecord::Base
- belongs_to :commentable, :polymorphic => true
+ it "has correct parameter of polymorphic belongs_to association" do
+ RailsAdmin::Config.stub!(:models_pool).and_return(["ARBlog", "ARPost", "ARCategory", "ARUser", "ARProfile", "ARComment"])
+ param = @comment.associations.select{|a| a[:name] == :commentable}.first
+ param.reject{|k, v| [:primary_key_proc, :model_proc].include? k }.should == {
+ :name=>:commentable,
+ :pretty_name=>"Commentable",
+ :type=>:belongs_to,
+ :foreign_key=>:commentable_id,
+ :foreign_type=>:commentable_type,
+ :as=>nil,
+ :polymorphic=>true,
+ :inverse_of=>nil,
+ :read_only=>nil,
+ :nested_form=>nil
+ }
+ # param[:primary_key_proc].call.should == 'id' Should not be called for polymorphic relations. Todo, Handle this niver
+ param[:model_proc].call.should == [ARBlog, ARPost]
end
- @blog = RailsAdmin::AbstractModel.new(ARBlog)
- @post = RailsAdmin::AbstractModel.new(ARPost)
- @category = RailsAdmin::AbstractModel.new(ARCategory)
- @user = RailsAdmin::AbstractModel.new(ARUser)
- @profile = RailsAdmin::AbstractModel.new(ARProfile)
- @comment = RailsAdmin::AbstractModel.new(ARComment)
+ it "has correct parameter of polymorphic inverse has_many association" do
+ param = @blog.associations.select{|a| a[:name] == :a_r_comments}.first
+ param.reject{|k, v| [:primary_key_proc, :model_proc].include? k }.should == {
+ :name=>:a_r_comments,
+ :pretty_name=>"A r comments",
+ :type=>:has_many,
+ :foreign_key=>:commentable_id,
+ :foreign_type=>nil,
+ :as=>:commentable,
+ :polymorphic=>false,
+ :inverse_of=>nil,
+ :read_only=>nil,
+ :nested_form=>nil
+ }
+ param[:primary_key_proc].call.should == 'id'
+ param[:model_proc].call.should == ARComment
+ end
end
- describe '#associations' do
- it 'lists associations' do
- @post.associations.map{|a|a[:name].to_s}.sort.should == ['a_r_blog', 'a_r_categories', 'a_r_comments']
+ describe "#properties" do
+ before do
+ @abstract_model = RailsAdmin::AbstractModel.new('Player')
end
- it 'list associations types in supported [:belongs_to, :has_and_belongs_to_many, :has_many, :has_one]' do
- (@post.associations + @blog.associations + @user.associations).map{|a|a[:type]}.uniq.map(&:to_s).sort.should == ['belongs_to', 'has_and_belongs_to_many', 'has_many', 'has_one']
+ it "returns parameters of string-type field" do
+ @abstract_model.properties.select{|f| f[:name] == :name}.should ==
+ [{:name => :name, :pretty_name => "Name", :type => :string, :length => 100, :nullable? => false, :serial? => false}]
end
end
-
-
- describe '#all' do
+
+ describe "data access method" do
+ before do
+ @players = FactoryGirl.create_list(:player, 3)
+ @abstract_model = RailsAdmin::AbstractModel.new('Player')
+ end
+
+ it "#new returns instance of AbstractObject" do
+ @abstract_model.new.object.should be_instance_of(Player)
+ end
+
+ it "#get returns instance of AbstractObject" do
+ @abstract_model.get(@players.first.id).object.should == @players.first
+ end
+
+ it "#get returns nil when id does not exist" do
+ @abstract_model.get('abc').should be_nil
+ end
+
+ it "#first returns first item" do
+ @abstract_model.first.should == @players.first
+ end
+
+ it "#count returns count of items" do
+ @abstract_model.count.should == @players.count
+ end
+
+ it "#destroy destroys multiple items" do
+ @abstract_model.destroy(@players[0..1])
+ Player.all.should == @players[2..2]
+ end
+
+ describe "#all" do
+ it "works without options" do
+ @abstract_model.all.sort.should == @players.sort
+ end
+
+ it "supports eager loading" do
+ @abstract_model.all(:include => :team).includes_values.should == [:team]
+ end
+
+ it "supports limiting" do
+ @abstract_model.all(:limit => 2).count.should == 2
+ end
+
+ it "supports retrieval by bulk_ids" do
+ @abstract_model.all(:bulk_ids => @players[0..1].map{|player| player.id }).
+ sort.should == @players[0..1].sort
+ end
+
+ it "supports pagination" do
+ @abstract_model.all(:page => 2, :per => 1).should == @players[1..1]
+ end
+
+ it "supports ordering" do
+ @abstract_model.all(:sort => "id", :sort_reverse => true).should == @players.sort
+ end
+
+ it "supports querying" do
+ @abstract_model.all(:query => @players[1].name).should == @players[1..1]
+ end
+
+ it "supports filtering" do
+ @abstract_model.all(:filters => {"name" => {"0000" => {:o=>"is", :v=>@players[1].name}}}).should == @players[1..1]
+ end
+ end
+ end
+
+ describe "#query_conditions" do
+ before do
+ @abstract_model = RailsAdmin::AbstractModel.new('Ball')
+ end
+
+ it "returns query statement" do
+ @abstract_model.send(:query_conditions, "word").should == ["(balls.color #{@like} ?) OR (balls.type #{@like} ?)", "%word%", "%word%"]
+ end
+ end
+
+ describe "#filter_conditions" do
+ before do
+ @abstract_model = RailsAdmin::AbstractModel.new('Team')
+ end
+
+ it "returns filter statement" do
+ @abstract_model.send(
+ :filter_conditions,
+ {"name" => {"0000" => {:o=>"is", :v=>"Jets"}},
+ "division" => {"0001" => {:o=>"like", :v=>"1"}}}
+ ).should == ["((teams.name #{@like} ?)) AND ((divisions.name #{@like} ?) OR (teams.division_id = ?))", "Jets", "%1%", 1]
+ end
+ end
+
+ describe "#build_statement" do
+ before do
+ @abstract_model = RailsAdmin::AbstractModel.new('FieldTest')
+ end
+
+ it "ignores '_discard' operator or value" do
+ [["_discard", ""], ["", "_discard"]].each do |value, operator|
+ @abstract_model.send(:build_statement, :name, :string, value, operator).should be_nil
+ end
+ end
+
+ it "supports '_blank' operator" do
+ [["_blank", ""], ["", "_blank"]].each do |value, operator|
+ @abstract_model.send(:build_statement, :name, :string, value, operator).should == ["(name IS NULL OR name = '')"]
+ end
+ end
+
+ it "supports '_present' operator" do
+ [["_present", ""], ["", "_present"]].each do |value, operator|
+ @abstract_model.send(:build_statement, :name, :string, value, operator).should == ["(name IS NOT NULL AND name != '')"]
+ end
+ end
+
+ it "supports '_null' operator" do
+ [["_null", ""], ["", "_null"]].each do |value, operator|
+ @abstract_model.send(:build_statement, :name, :string, value, operator).should == ["(name IS NULL)"]
+ end
+ end
+
+ it "supports '_not_null' operator" do
+ [["_not_null", ""], ["", "_not_null"]].each do |value, operator|
+ @abstract_model.send(:build_statement, :name, :string, value, operator).should == ["(name IS NOT NULL)"]
+ end
+ end
+
+ it "supports '_empty' operator" do
+ [["_empty", ""], ["", "_empty"]].each do |value, operator|
+ @abstract_model.send(:build_statement, :name, :string, value, operator).should == ["(name = '')"]
+ end
+ end
+
+ it "supports '_not_empty' operator" do
+ [["_not_empty", ""], ["", "_not_empty"]].each do |value, operator|
+ @abstract_model.send(:build_statement, :name, :string, value, operator).should == ["(name != '')"]
+ end
+ end
+
+ it "supports boolean type query" do
+ ['false', 'f', '0'].each do |value|
+ @abstract_model.send(:build_statement, :field, :boolean, value, nil).should == ["(field IS NULL OR field = ?)", false]
+ end
+ ['true', 't', '1'].each do |value|
+ @abstract_model.send(:build_statement, :field, :boolean, value, nil).should == ["(field = ?)", true]
+ end
+ @abstract_model.send(:build_statement, :field, :boolean, 'word', nil).should be_nil
+ end
+
+ it "supports integer type query" do
+ @abstract_model.send(:build_statement, :field, :integer, "1", nil).should == ["(field = ?)", 1]
+ @abstract_model.send(:build_statement, :field, :integer, 'word', nil).should be_nil
+ end
+
+ it "supports string type query" do
+ @abstract_model.send(:build_statement, :field, :string, "", nil).should be_nil
+ @abstract_model.send(:build_statement, :field, :string, "foo", "was").should be_nil
+ @abstract_model.send(:build_statement, :field, :string, "foo", "default").should == ["(field #{@like} ?)", "%foo%"]
+ @abstract_model.send(:build_statement, :field, :string, "foo", "like").should == ["(field #{@like} ?)", "%foo%"]
+ @abstract_model.send(:build_statement, :field, :string, "foo", "starts_with").should == ["(field #{@like} ?)", "foo%"]
+ @abstract_model.send(:build_statement, :field, :string, "foo", "ends_with").should == ["(field #{@like} ?)", "%foo"]
+ @abstract_model.send(:build_statement, :field, :string, "foo", "is").should == ["(field #{@like} ?)", "foo"]
+ end
+
context 'filters on dates' do
it 'lists elements within outbound limits' do
date_format = I18n.t("admin.misc.filter_date_format", :default => I18n.t("admin.misc.filter_date_format", :locale => :en)).gsub('dd', '%d').gsub('mm', '%m').gsub('yy', '%Y')
-
+
FieldTest.create!(:date_field => Date.strptime("01/01/2012", date_format))
FieldTest.create!(:date_field => Date.strptime("01/02/2012", date_format))
FieldTest.create!(:date_field => Date.strptime("01/03/2012", date_format))
FieldTest.create!(:date_field => Date.strptime("01/04/2012", date_format))
- RailsAdmin.config(FieldTest).abstract_model.all(:filters => { "date_field" => { "1" => { :v => ["", "01/02/2012", "01/03/2012"], :o => 'between' } } } ).count.should == 2
- RailsAdmin.config(FieldTest).abstract_model.all(:filters => { "date_field" => { "1" => { :v => ["", "01/02/2012", "01/02/2012"], :o => 'between' } } } ).count.should == 1
- RailsAdmin.config(FieldTest).abstract_model.all(:filters => { "date_field" => { "1" => { :v => ["", "01/03/2012", ""], :o => 'between' } } } ).count.should == 2
- RailsAdmin.config(FieldTest).abstract_model.all(:filters => { "date_field" => { "1" => { :v => ["", "", "01/02/2012"], :o => 'between' } } } ).count.should == 2
- RailsAdmin.config(FieldTest).abstract_model.all(:filters => { "date_field" => { "1" => { :v => ["01/02/2012"], :o => 'default' } } } ).count.should == 1
-
+ @abstract_model.all(:filters => { "date_field" => { "1" => { :v => ["", "01/02/2012", "01/03/2012"], :o => 'between' } } } ).count.should == 2
+ @abstract_model.all(:filters => { "date_field" => { "1" => { :v => ["", "01/02/2012", "01/02/2012"], :o => 'between' } } } ).count.should == 1
+ @abstract_model.all(:filters => { "date_field" => { "1" => { :v => ["", "01/03/2012", ""], :o => 'between' } } } ).count.should == 2
+ @abstract_model.all(:filters => { "date_field" => { "1" => { :v => ["", "", "01/02/2012"], :o => 'between' } } } ).count.should == 2
+ @abstract_model.all(:filters => { "date_field" => { "1" => { :v => ["01/02/2012"], :o => 'default' } } } ).count.should == 1
+
end
end
+
+ it "supports enum type query" do
+ @abstract_model.send(:build_statement, :field, :enum, "1", nil).should == ["(field IN (?))", ["1"]]
+ end
end
+ describe "model attribute method" do
+ before do
+ @abstract_model = RailsAdmin::AbstractModel.new('Player')
+ end
+
+ it "#scoped returns relation object" do
+ @abstract_model.scoped.should be_instance_of(ActiveRecord::Relation)
+ end
+
+ it "#table_name works" do
+ @abstract_model.table_name.should == 'players'
+ end
+
+ it "#serialized_attributes works" do
+ RailsAdmin::AbstractModel.new('User').serialized_attributes.should == ["roles"]
+ end
+ end
end
View
30 spec/unit/adapters/mongoid/abstract_object_spec.rb
@@ -0,0 +1,30 @@
+require 'spec_helper'
+require 'rails_admin/adapters/mongoid/abstract_object'
+
+describe "Mongoid::AbstractObject" do
+ before(:each) do
+ @articles = FactoryGirl.create_list :article, 3
+ @author = RailsAdmin::Adapters::Mongoid::AbstractObject.new FactoryGirl.create :author
+ end
+
+ describe "references_many association" do
+ it "supports retrieval of ids through foo_ids" do
+ @author.article_ids.should == []
+ article = FactoryGirl.create :article, :author => @author
+ @author.article_ids.should == [article.id]
+ end
+
+ it "supports assignment of items through foo_ids=" do
+ @author.articles.should == []
+ @author.article_ids = @articles.map(&:id)
+ @author.reload
+ @author.articles.sort.should == @articles.sort
+ end
+
+ it "skips invalid id on assignment through foo_ids=" do
+ @author.article_ids = @articles.map{|item| item.id.to_s }.unshift('4f431021dcf2310db7000006')
+ @author.reload
+ @author.articles.sort.should == @articles.sort
+ end
+ end
+end
View
579 spec/unit/adapters/mongoid_spec.rb
@@ -0,0 +1,579 @@
+require 'spec_helper'
+require 'timecop'
+require 'rails_admin/adapters/mongoid'
+
+describe RailsAdmin::Adapters::Mongoid do
+ describe '#associations' do
+ before :all do
+ RailsAdmin::AbstractModel.reset_polymorphic_parents
+
+ class MongoBlog
+ include Mongoid::Document
+ references_many :mongo_posts
+ references_many :mongo_comments, :as => :commentable
+ end
+
+ class MongoPost
+ include Mongoid::Document
+ referenced_in :mongo_blog
+ has_and_belongs_to_many :mongo_categories
+ references_many :mongo_comments, :as => :commentable
+ end
+
+ class MongoCategory
+ include Mongoid::Document
+ has_and_belongs_to_many :mongo_posts
+ end
+
+ class MongoUser
+ include Mongoid::Document
+ references_one :mongo_profile
+ field :name, :type => String
+ field :message, :type => String
+ field :short_text, :type => String
+
+ validates :short_text, :length => {:maximum => 255}
+ end