What is the best way to test activeadmin classes? - ruby-on-rails

I have simple activeadmin class that looks like this:
ActiveAdmin.register Post do
actions :index
index do
index_columns
end
csv do
index_columns
end
def index_columns
column "Id" do |sp|
sp.id
end
end
end
How will be the best to test this code? Write some integrations specs with capybara or maybe there is some other way?

General idea behind testing gem's functionality - you don't test it.
Gems are (usually) already tested.

I agree with Andrey, but needed to do this for work. Here's how I tested the csv portion.
#csv_doc = StringIO.new
allow_any_instance_of(ActiveAdmin::ResourceController).to receive(:stream_resource) do |aa_controller|
receiver = []
# it's ok to mock this because it's literally their code: https://github.com/activeadmin/activeadmin/blob/master/lib/active_admin/resource_controller/streaming.rb#L38
aa_controller.class.active_admin_config.csv_builder.build(aa_controller, receiver)
receiver.each do |fees_as_csv|
#csv_doc << fees_as_csv
end
end
#csv_doc.rewind
csv_string = #csv_doc.readlines.join
csv = CSV.parse(csv_string, headers: true).map(&:to_hash)
expect(csv[0]["FIGURING THIS OUT"]).to eq "SUCKED"

Related

RSPEC - Test to_csv class method

I have this class method in my model:
def self.to_csv
attributes = %w(title)
CSV.generate(headers: true) do |csv|
csv << attributes
all.each do |campaign|
csv << campaign.attributes.values_at(*attributes)
end
end
end
I'm looking for good ways to test this method with Rspec. Does anyone have good techniques for this method?
I have a few comments:
I would not use all unless you are in a background job or you know the collection would not be that big
if you really have to use all, then dont use .each use .find_each which would do queries in batches
use factory bot if you can
For the spec itself, I would do:
it "creates expected csv" do
allow(described_class).to receive(:all).and_return([
described_class.new(title: "title1"),
described_class.new(title: "title2")
])
expect(described_class.to_csv).to eq "title\ntitle1\ntitle2\n"
end

Limiting download_links for ActiveAdmin based on AdminUser model

I am trying to limit who can access the csv/json/... exports in ActiveAdmin based on the field 'limited'. I'd like to a) hide the links and b) return nothing at all if the path were to get hit anyway
I tried the following:
index downloads_links: !current_admin_user.limited? do
# ...
end
as well as
csv do
return if current_admin_user.limited?
# ...
end
I also briefly tried using procs and lambda's but that's probably not the solution here either?
Neither appear to work and are giving me nomethoderrors on ActiveAdmin::DSLResource and ActiveAdmin::CSVBuilder respectively
Any tips are welcome, thank you
i was able to achieve this with a simple monkey patch, but i was using cancan. cancan helper method 'can?' worked fine, but i wasn't testing the 'current_admin_user'. please, try it
module ActiveAdmin
module Views
class PaginatedCollection
def build_download_format_links(formats = self.class.formats)
params = request.query_parameters.except :format, :commit
links = formats.map { |format| link_to format.to_s.upcase, params: params, format: format }
unless current_admin_user.limited?
div :class => "download_links" do
text_node [I18n.t('active_admin.download'), links].flatten.join(" ").html_safe
end
end
end
end
end
end
upd:
i've tried with current_admin_user, and it worked.
also if you need to limit the formats, you can redefine formats method it this module, using your 'limited' method:
module ActiveAdmin
module Views
class PaginatedCollection
def formats
if current_admin_user.limited?
#formats ||= [:csv] # anything you need for limited users
else
#formats ||= [:csv, :xml, :json]
end
#formats.clone
end
end
end
end

How to generate a file with the names of the methods in class?

I have this Unit Test file:
class WebsiteCheckPageElements < Test::Unit::TestCase
def setup
...
end
def test_achievements_iknow_report_page
...
end
def test_achievements_daily_report_page
...
end
def test_client_side_report_page
...
end
end
I need to write a ruby script that will write the names of the tests to .txt file. Something like
setup
test_achievements_iknow_report_page
test_achievements_daily_report_page
test_client_side_report_page
Thank you in advance for the suggestions.
A more robust way than the other answer:
File.open("test_methods.txt", "w") do |f|
WebsiteCheckPageElements.instance_methods.each do |method|
f << "#{method}\n"
end
end
This will actually use the Ruby runtime and will not fall subject to accidentally matching def in places where it is not defining a method (such as inside a String), it will not match other classes in the file and it will correctly read the inheritance hierarchy.
You could use a simple script that uses regular expressions to print the lines that match def:
match_test_methods.rb
File.open('your_unit_test_file.rb').each_line do |line|
match = line.match(/def (.+)/)
puts match[1] if match
end
Then put the result of the script in a .txt file
ruby match_test_methods.rb > test_methods.txt

Is there a way to get a collection of all the Models in your Rails app?

Is there a way that you can get a collection of all of the Models in your Rails app?
Basically, can I do the likes of: -
Models.each do |model|
puts model.class.name
end
The whole answer for Rails 3, 4 and 5 is:
If cache_classes is off (by default it's off in development, but on in production):
Rails.application.eager_load!
Then:
ActiveRecord::Base.descendants
This makes sure all models in your application, regardless of where they are, are loaded, and any gems you are using which provide models are also loaded.
This should also work on classes that inherit from ActiveRecord::Base, like ApplicationRecord in Rails 5, and return only that subtree of descendants:
ApplicationRecord.descendants
If you'd like to know more about how this is done, check out ActiveSupport::DescendantsTracker.
Just in case anyone stumbles on this one, I've got another solution, not relying on dir reading or extending the Class class...
ActiveRecord::Base.send :subclasses
This will return an array of classes. So you can then do
ActiveRecord::Base.send(:subclasses).map(&:name)
EDIT: Look at the comments and other answers. There are smarter answers than this one! Or try to improve this one as community wiki.
Models do not register themselves to a master object, so no, Rails does not have the list of models.
But you could still look in the content of the models directory of your application...
Dir.foreach("#{RAILS_ROOT}/app/models") do |model_path|
# ...
end
EDIT: Another (wild) idea would be to use Ruby reflection to search for every classes that extends ActiveRecord::Base. Don't know how you can list all the classes though...
EDIT: Just for fun, I found a way to list all classes
Module.constants.select { |c| (eval c).is_a? Class }
EDIT: Finally succeeded in listing all models without looking at directories
Module.constants.select do |constant_name|
constant = eval constant_name
if not constant.nil? and constant.is_a? Class and constant.superclass == ActiveRecord::Base
constant
end
end
If you want to handle derived class too, then you will need to test the whole superclass chain. I did it by adding a method to the Class class:
class Class
def extend?(klass)
not superclass.nil? and ( superclass == klass or superclass.extend? klass )
end
end
def models
Module.constants.select do |constant_name|
constant = eval constant_name
if not constant.nil? and constant.is_a? Class and constant.extend? ActiveRecord::Base
constant
end
end
end
ActiveRecord::Base.connection.tables.map do |model|
model.capitalize.singularize.camelize
end
will return
["Article", "MenuItem", "Post", "ZebraStripePerson"]
Additional information If you want to call a method on the object name without model:string unknown method or variable errors use this
model.classify.constantize.attribute_names
For Rails5 models are now subclasses of ApplicationRecord so to get list of all models in your app you do:
ApplicationRecord.descendants.collect { |type| type.name }
Or shorter:
ApplicationRecord.descendants.collect(&:name)
If you are in dev mode, you will need to eager load models before:
Rails.application.eager_load!
I looked for ways to do this and ended up choosing this way:
in the controller:
#data_tables = ActiveRecord::Base.connection.tables
in the view:
<% #data_tables.each do |dt| %>
<br>
<%= dt %>
<% end %>
<br>
source: http://portfo.li/rails/348561-how-can-one-list-all-database-tables-from-one-project
I think #hnovick's solution is a cool one if you dont have table-less models. This solution would work in development mode as well
My approach is subtly different though -
ActiveRecord::Base.connection.tables.map{|x|x.classify.safe_constantize}.compact
classify is well supposed to give you the name of the class from a string properly. safe_constantize ensures that you can turn it into a class safely without throwing an exception. This is needed in case you have database tables which are not models. compact so that any nils in the enumeration are removed.
If you want just the Class names:
ActiveRecord::Base.descendants.map {|f| puts f}
Just run it in Rails console, nothing more. Good luck!
EDIT: #sj26 is right, you need to run this first before you can call descendants:
Rails.application.eager_load!
With Rails 6, Zetiwerk became the default code loader.
For eager loading, try:
Zeitwerk::Loader.eager_load_all
Then
ApplicationRecord.descendants
This seems to work for me:
Dir.glob(RAILS_ROOT + '/app/models/*.rb').each { |file| require file }
#models = Object.subclasses_of(ActiveRecord::Base)
Rails only loads models when they are used, so the Dir.glob line "requires" all the files in the models directory.
Once you have the models in an array, you can do what you were thinking (e.g. in view code):
<% #models.each do |v| %>
<li><%= h v.to_s %></li>
<% end %>
On one line: Dir['app/models/\*.rb'].map {|f| File.basename(f, '.*').camelize.constantize }
ActiveRecord::Base.connection.tables
Yes there are many ways you can find all model names but what I did in my gem model_info is , it will give you all the models even included in the gems.
array=[], #model_array=[]
Rails.application.eager_load!
array=ActiveRecord::Base.descendants.collect{|x| x.to_s if x.table_exists?}.compact
array.each do |x|
if x.split('::').last.split('_').first != "HABTM"
#model_array.push(x)
end
#model_array.delete('ActiveRecord::SchemaMigration')
end
then simply print this
#model_array
In just one line:
ActiveRecord::Base.subclasses.map(&:name)
I can't comment yet, but I think sj26 answer should be the top answer. Just a hint:
Rails.application.eager_load! unless Rails.configuration.cache_classes
ActiveRecord::Base.descendants
To avoid pre-load all Rails, you can do this:
Dir.glob("#{Rails.root}/app/models/**/*.rb").each {|f| require_dependency(f) }
require_dependency(f) is the same that Rails.application.eager_load! uses. This should avoid already required file errors.
Then you can use all kind of solutions to list AR models, like ActiveRecord::Base.descendants
This works for Rails 3.2.18
Rails.application.eager_load!
def all_models
models = Dir["#{Rails.root}/app/models/**/*.rb"].map do |m|
m.chomp('.rb').camelize.split("::").last
end
end
Dir.foreach("#{Rails.root.to_s}/app/models") do |model_path|
next unless model_path.match(/.rb$/)
model_class = model_path.gsub(/.rb$/, '').classify.constantize
puts model_class
end
This will give to you all the model classes you have on your project.
Module.constants.select { |c| (eval c).is_a?(Class) && (eval c) < ActiveRecord::Base }
Here's a solution that has been vetted with a complex Rails app (the one powering Square)
def all_models
# must eager load all the classes...
Dir.glob("#{RAILS_ROOT}/app/models/**/*.rb") do |model_path|
begin
require model_path
rescue
# ignore
end
end
# simply return them
ActiveRecord::Base.send(:subclasses)
end
It takes the best parts of the answers in this thread and combines them in the simplest and most thorough solution. This handle cases where your models are in subdirectories, use set_table_name etc.
Just came across this one, as I need to print all models with their attributes(built on #Aditya Sanghi's comment):
ActiveRecord::Base.connection.tables.map{|x|x.classify.safe_constantize}.compact.each{ |model| print "\n\n"+model.name; model.new.attributes.each{|a,b| print "\n#{a}"}}
This worked for me. Special thanks to all the posts above. This should return a collection of all your models.
models = []
Dir.glob("#{Rails.root}/app/models/**/*.rb") do |model_path|
temp = model_path.split(/\/models\//)
models.push temp.last.gsub(/\.rb$/, '').camelize.constantize rescue nil
end
I've tried so many of these answers unsuccessfully in Rails 4 (wow they changed a thing or two for god sakes) I decided to add my own. The ones that called ActiveRecord::Base.connection and pulled the table names worked but didn't get the result I wanted because I've hidden some models (in a folder inside of app/models/) that I didn't want to delete:
def list_models
Dir.glob("#{Rails.root}/app/models/*.rb").map{|x| x.split("/").last.split(".").first.camelize}
end
I put that in an initializer and can call it from anywhere. Prevents unnecessary mouse-usage.
The Rails implements the method descendants, but models not necessarily ever inherits from ActiveRecord::Base, for example, the class that includes the module ActiveModel::Model will have the same behavior as a model, just doesn't will be linked to a table.
So complementing what says the colleagues above, the slightest effort would do this:
Monkey Patch of class Class of the Ruby:
class Class
def extends? constant
ancestors.include?(constant) if constant != self
end
end
and the method models, including ancestors, as this:
The method Module.constants returns (superficially) a collection of symbols, instead of constants, so, the method Array#select can be substituted like this monkey patch of the Module:
class Module
def demodulize
splitted_trail = self.to_s.split("::")
constant = splitted_trail.last
const_get(constant) if defines?(constant)
end
private :demodulize
def defines? constant, verbose=false
splitted_trail = constant.split("::")
trail_name = splitted_trail.first
begin
trail = const_get(trail_name) if Object.send(:const_defined?, trail_name)
splitted_trail.slice(1, splitted_trail.length - 1).each do |constant_name|
trail = trail.send(:const_defined?, constant_name) ? trail.const_get(constant_name) : nil
end
true if trail
rescue Exception => e
$stderr.puts "Exception recovered when trying to check if the constant \"#{constant}\" is defined: #{e}" if verbose
end unless constant.empty?
end
def has_constants?
true if constants.any?
end
def nestings counted=[], &block
trail = self.to_s
collected = []
recursivityQueue = []
constants.each do |const_name|
const_name = const_name.to_s
const_for_try = "#{trail}::#{const_name}"
constant = const_for_try.constantize
begin
constant_sym = constant.to_s.to_sym
if constant && !counted.include?(constant_sym)
counted << constant_sym
if (constant.is_a?(Module) || constant.is_a?(Class))
value = block_given? ? block.call(constant) : constant
collected << value if value
recursivityQueue.push({
constant: constant,
counted: counted,
block: block
}) if constant.has_constants?
end
end
rescue Exception
end
end
recursivityQueue.each do |data|
collected.concat data[:constant].nestings(data[:counted], &data[:block])
end
collected
end
end
Monkey patch of String.
class String
def constantize
if Module.defines?(self)
Module.const_get self
else
demodulized = self.split("::").last
Module.const_get(demodulized) if Module.defines?(demodulized)
end
end
end
And, finally, the models method
def models
# preload only models
application.config.eager_load_paths = model_eager_load_paths
application.eager_load!
models = Module.nestings do |const|
const if const.is_a?(Class) && const != ActiveRecord::SchemaMigration && (const.extends?(ActiveRecord::Base) || const.include?(ActiveModel::Model))
end
end
private
def application
::Rails.application
end
def model_eager_load_paths
eager_load_paths = application.config.eager_load_paths.collect do |eager_load_path|
model_paths = application.config.paths["app/models"].collect do |model_path|
eager_load_path if Regexp.new("(#{model_path})$").match(eager_load_path)
end
end.flatten.compact
end
Make sure to eager load your app before calling descendants so that all the classes are loaded:
Rails.application.eager_load! unless Rails.application.config.eager_load
ApplicationRecord.descendants.each do |clazz|
# do something with clazz, e.g. User, Event, Attendance, etc.
end
def load_models_in_development
if Rails.env == "development"
load_models_for(Rails.root)
Rails.application.railties.engines.each do |r|
load_models_for(r.root)
end
end
end
def load_models_for(root)
Dir.glob("#{root}/app/models/**/*.rb") do |model_path|
begin
require model_path
rescue
# ignore
end
end
end
can check this
#models = ActiveRecord::Base.connection.tables.collect{|t| t.underscore.singularize.camelize}
Assuming all models are in app/models and you have grep & awk on your server (majority of the cases),
# extract lines that match specific string, and print 2nd word of each line
results = `grep -r "< ActiveRecord::Base" app/models/ | awk '{print $2}'`
model_names = results.split("\n")
It it faster than Rails.application.eager_load! or looping through each file with Dir.
EDIT:
The disadvantage of this method is that it misses models that indirectly inherit from ActiveRecord (e.g. FictionalBook < Book). The surest way is Rails.application.eager_load!; ActiveRecord::Base.descendants.map(&:name), even though it's kinda slow.
I'm just throwing this example here if anyone finds it useful. Solution is based on this answer https://stackoverflow.com/a/10712838/473040.
Let say you have a column public_uid that is used as a primary ID to outside world (you can findjreasons why you would want to do that here)
Now let say you've introduced this field on bunch of existing Models and now you want to regenerate all the records that are not yet set. You can do that like this
# lib/tasks/data_integirity.rake
namespace :di do
namespace :public_uids do
desc "Data Integrity: genereate public_uid for any model record that doesn't have value of public_uid"
task generate: :environment do
Rails.application.eager_load!
ActiveRecord::Base
.descendants
.select {|f| f.attribute_names.include?("public_uid") }
.each do |m|
m.where(public_uid: nil).each { |mi| puts "Generating public_uid for #{m}#id #{mi.id}"; mi.generate_public_uid; mi.save }
end
end
end
end
you can now run rake di:public_uids:generate

How do you make a method apply to a collection of ActiveRecord objects

Currently, if I want to apply a method to a group of ActiveRecord objects, I have to structure the call like so:
messages = Message.find(:all)
csv = Message.to_csv(messages)
How can I define the method so it's structured like so?
messages = Message.find(:all)
csv = messages.to_csv
This is the current model code:
require 'fastercsv'
class Message < ActiveRecord::Base
def Message.to_csv(messages)
FasterCSV.generate do |csv|
csv << ["from","to", "received"]
for m in messages
csv << [m.from,m.to,m.created_at]
end
end
end
end
The following will call to_csv on all instances included in the messages array.
messages = Message.find(:all)
csv = messages.map { |message| message.to_csv }
In Rails, in Ruby 1.9 or with Symbol#to_proc available through other means, you can also shorten it to:
csv = messages.map(&:to_csv)
The longer form is useful when you want to make a more complex operation:
csv = messages.map { |message|
if message.length < 1000
message.to_csv
else
"Too long"
end
}
Put this in a file in lib/. I would recommend calling it something like base_ext.rb
require 'fastercsv'
class ActiveRecord::Base
def self.to_csv(objects, skip_attributes=[])
FasterCSV.generate do |csv|
csv << attribute_names - skip_attributes
objects.each do |object|
csv << (attribute_names - skip_attributes).map { |a| "'#{object.attributes[a]}'" }.join(", ")
end
end
end
end
After that, go to config/environment.rb and put require 'base_ext' at the bottom of the file to include the new extension. Upon restarting your server you should have access to a to_csv method on all model classes and when you pass it an array of objects should generate a nice CSV format for you.
FasterCSV patches the Array class and adds a 'to_csv' method to it already, but it doesn't do what you want. You could overwrite it yourself by doing something like:
class Array
def to_csv(options = Hash.new)
collect { |item| item.to_csv }.join "\n"
end
end
Or something along those lines, but that's kind of crappy.
Honestly, it makes more sense the way you've done it as a class method on your model.
You could create a method on your Message class to do something along the lines of...
In your controller....
#csv_file = Message.send_all_to_csv
In your model...
require 'fastercsv'
class Message < ActiveRecord::Base
def send_all_to_csv
#messages = Find.all
FasterCSV.generate do |csv|
csv << ["from","to", "received"]
for message in #messages
csv << [message.from,message.to,message.created_at]
end
end
# do something with your csv object (return it to the controller
# or pass it on to another class method
end
end
You could define the method directly on the messages object itself, but if you do that, the method would only be available to that specific instance:
def messages.to_csv()
FasterCSV.generate do |csv|
csv << ["from", "to", "received"]
self.each { |m| csv << [m.from, m.to, m.created_at] }
end
end
Then you could call it like so:
messages.to_csv
I'm a Ruby newbie, so I'm not sure if this is idiomatic Ruby or not: that is, I'm not sure how common or accepted it is to define new methods directly on object instances, I only know it's possible ;-)
If it is isolated to one AR model I would add a to_custom_csv_array instance method
def to_custom_csv_array
[self.from,self.to,self.created_at]
end
then override find on the class
def self.find(*args)
collection = super
collection.extend(CustomToCSV) if collection.is_a?(Array)
end
and in CustomToCSV define the to_custom_csv to generate the csv
module CustomToCSV
def to_custom_csv
FasterCSV.generate do |csv|
csv << ["from","to", "received"]
csv << self.map {|obj| obj.to_custom_csv_array}
end
end
end
This is from memory but should work.
I know that it is a very old question but just thought to provide a feedback anyway.
check blog http://blog.zahiduzzaman.com/2013/07/adding-tocsv-method-to-active-record.html
just another way of achieving this

Resources