ActiveAdmin batch action form with entries from multiple classes as input - ruby-on-rails

Following the batch actions guide from activeadmin, one can create a form of different types. What I want is, that the form have entries of different model classes.
I have three classes: Shop, RecordingShop and DistributionChain. A DistributionChain can have one or more Shops or RecordingShops but can also have none of them.
On the Scores index table, I want to show an export batch action and recognize which class the form entry belongs to (either Shop, RecordingShop or DistributionChain)
Is it possible to do something like:
ActiveAdmin.register Score, as: 'Delivery' do
...
batch_action :export, form: {shops: (DistributionChain.all + Shop.not_distribution_chain).collect{ |e| [e.name, e.id, e.class.name]} } do |ids, inputs|
...
end
...
end
so that the class would be a third parameter in the nested array, after the element name and id, and include it in the inputs variable?

Solved. I found a way through declaring variables in the admin resource code itself.
ActiveAdmin.register Score, as: 'Delivery' do
...
EXPORT_INPUTS = DistributionChain.all + Shop.not_distribution_chain
EXPORT_INPUTS_DATA = EXPORT_INPUTS.collect.with_index{ |e, i| [e.name, i] }
...
batch_action :export, form: {shops: EXPORT_INPUTS_DATA } do |ids, inputs|
element = EXPORT_INPUTS[inputs['shops'].to_i]
...
end
...
end
EXPORT_INPUTS contains the actual set of elements from multiple classes.
EXPORT_INPUTS_DATA is the variable (array) to use in the form and has the name and the index of the elements in the array. The chosen index can be retrieved later as inputs['shops'] and EXPORT_INPUTS[inputs['shops'].to_i] corresponds exactly to the element we want.
NB: EXPORT_INPUTS_DATA should be defined outside the action. If you write
batch_action :export, form: {shops: EXPORT_INPUTS.collect.with_index{ |e, i| [e.name, i] } }
directly, inputs['shops'] will only be equal to the id of the element in its respective class, which does not tell us anything.

Related

Rails - More about "N+1"

When I try to show linked data in index action - I can't avoid N+1 problem using standart includes. Example:
Model 1 - Pet (let it will be animal: title:string. Pet can has many PetAttributeElems, they show attributes this Pet have and what values they have)
Model 2 - PetAttribute (this model contains only titles of attributes, like weight, age and 1000+ more attributes. PetAttribute has many PetAttributeElems - one attribute, such as weight, can be described for many Pets)
Model 3 - PetAttributeElem (this model belongs to pet and to petAttribute, also it has value field, that show value of attribute for pet.
When I make show action - I use in HAML:
-#pet.pet_attribute_elems.includes(:pet_attribute).each do |elem|
="#{elem.pet_attribtute.title}: #{elem.value}"
But when I make index action I want to use:
-#pets.each do |pet|
-pet.pet_attribute_elems.includes(:pet_attribute).each do |elem|
="#{elem.pet_attribtute.title}: #{elem.value}"
That includes method will call many SQL queries, for every pet
Now I solve it by manually creating additional object like this:
#pet_elems = {}
PetAtributeElems.includes(:pet_attribute)
.where(pet_id:#pets.map(&:id)).each do |elem|
pet_id = elem.pet_id
if #pet_elems.include?(pet_id)
#pet_elems[pet_id] << elem
else
#pet_elems[pet_id] = [elem]
end
end
Than I can use:
-#pets.each do |pet|
-if #pet_elems.include?(pet.id)
-#pet_elems[pet.id].each do |elem|
="#{elem.pet_attribtute.title}: #{elem.value}"
How could I solve that task more easy?
You're going down a non rails-convention path.
Move code out of the views so it's simply
= render #pet_attribute_elems
Make a partial to handle display
# _pet_attribute_elems.html.haml
="#{pet_attribute_elem.pet_attribtute.title}: #{pet_attribute_elem.value}"
In the controller, do the queries
def show
#pet = Pet.find(...)
#pet_attribute_elems = #pet.pet_attribute_elems.includes(:pet_attribute)
end
def index
#pet_attribute_elems = PetAttributeElem.includes(:pet_attribute)
end

How to pass default filter into Filterrific get

I finally got my filterrific get working and its a great gem, if not a little complex for a noob like me.
My original index page was filtering the active records based on those nearby to the user like this:
def index
location_ids = Location.near([session[:latitude], session[:longitude]], 50, order: '').pluck(:id)
#vendor_locations = VendorLocation.includes(:location).where(location_id: location_ids)
#appointments = Appointment.includes(:vendor).
where(vendor_id: #vendor_locations.select(:vendor_id))
end
So this pulls in all of the Appointments with Vendors in the area, but how do I pass this over to the Filterrific search:
#filterrific = initialize_filterrific(
params[:filterrific],
select_options:{ sorted_by: Appointment.options_for_sorted_by, with_service_id: Service.options_for_select },
) or return
#appointments = #filterrific.find.page(params[:page])
respond_to do |format|
format.html
format.js
end
It seems like the Filterrerrific is loading ALL of the appointments by default, but I want to limit to the ones nearby. What am I missing?
What you appear to be missing is a param default_filter_params to filterrific macro in the model. (Your question didn't mention that you made any adjustments to the VendorLocation model, since that is the object that you want to filter, that's where the macro should be called. Maybe you just omitted it from your question...)
From the model docs:
filterrific(
default_filter_params: { sorted_by: 'created_at_desc' },
available_filters: [
:sorted_by,
:search_query,
:with_country_id,
:with_created_at_gte
]
)
You probably found this already, it was on the first page of the documentation, but there's more important stuff in the example application that you need (I ran into this too, when I was just recently using Filterrific for the first time.)
The information on the start page is not enough to really get you started at all.
You have to read a bit further to see the other ways you may need to change your models, model accesses, and views in order to support Filterrific.
The part that makes the default filter setting effective is this default_filter_params hash (NOT select_options, which provides the options for "select" aka dropdown boxes. That's not what you want at all, unless you're doing a dropdown filter.) This hash holds a list of the scopes that need to be applied by default (the hash keys) and the scope parameter is used as the hash value.
That default_filter_params hash may not be the only thing you are missing... You also must define those ActiveRecord scopes for each filter that you want to use in the model, and name these in available_filters as above to make them available to filterrific:
scope :with_created_at_gte, lambda { |ref_date|
where('created_at >= ?', ref_date)
end
It's important that these scopes all take an argument (the value comes from the value of the filter field on the view page, you must add these to your view even if you want to keep them hidden from the user). It's also important that they always return ActiveRecord associations.
This is more like what you want:
scope :location_near, lambda { |location_string|
l = Location.near(location_string).pluck(:id)
where(location_id: l)
end
The problem with this approach is that in your case, there is no location_string or any single location variable, you have multiple coordinates for your location parameters. But you are not the first person to have this problem at all!
This issue describes almost exactly the problem you set out to solve. The author of Filterrific recommended embedding the location fields into hidden form fields in a nested fields_for, so that the form can still pass a single argument into the scope (as in with_distance_fields):
<%= f.fields_for :with_distance do |with_distance_fields| %>
<%= with_distance_fields.hidden_field :lat, value: current_user.lat %>
<%= with_distance_fields.hidden_field :lng, value: current_user.lng %>
<%= with_distance_fields.select :distance_in_meters,
#filterrific.select_options[:with_distance] %>
<% end %>
... make that change in your view, and add a matching scope that looks something like (copied from the linked GitHub issue):
scope :with_distance, -> (with_distance_attrs) {
['lng' => '-123', 'lat' => '49', 'distance_in_meters' => '2000']
where(%{
ST_DWithin(
ST_GeographyFromText(
'SRID=4326;POINT(' || courses.lng || ' ' || courses.lat || ')'
),
ST_GeographyFromText('SRID=4326;POINT(%f %f)'),
%d
)
} % [with_distance_attrs['lng'], with_distance_attrs['lat'], with_distance_attrs['distance_in_meters']])
}
So, your :with_distance scope should go onto the VendorLocation model and it should probably look like this:
scope :with_distance, -> (with_distance_attrs) {
lat = with_distance_attrs['lat']
lng = with_distance_attrs['lng']
dist = with_distance_attrs['distance']
location_ids = Location.near([lat, lng], dist, order: '').pluck(:id)
where(location_id: location_ids)
end
Last but not least, you probably noticed that I removed your call to includes(:location) — I know you put it there on purpose, and I didn't find it very clear in the documentation, but you can still get eager loading and have ActiveRecord optimize into a single query before passing off the filter work to Filterrific by defining your controller's index method in this way:
def index
#appointments = Appointment.includes(:vendor).
filterrific_find(#filterrific).page(params[:page])
end
Hope this helps!

Search for an object in rails by an array and create objects if not exist

I want to call the ActiveRecord method where with an array for a column. If the each item on the array doesn't exist, create the object. The closest method I found for this is first_or_create but this seems to be called only once, not for each time the record doesn't exist. Below is my example code-
hashtag_list = params[:message][:hashtag_primary]
#hashtags = Hashtag.where({:name => hashtag_list}).first_or_create do |hashtag|
hashtag.creator = current_user.id
end
Rails version- 4.2.1
I don't know a direct method, only a workaround
existing_tags = Hashtag.where({:name => hashtag_list}).pluck(:name)
not_existing_tags = hashtag_list - existing_tags
#hashtags = Hashtag.where({:name => existing_tags}).all
not_existing_tags.each do |tag|
#hashtags << Hashtag.new name: tag
end
#hashtags.each do |hashtag|
hashtag.creator = current_user.id
end
This is expected behavior of where + first_or_create method. Basically where(field: array) produces an SQL to find all records where field matches any item in the array. Than you have first_or_create method which takes the first record from results or creates a new one with escaped array value assigned to a field (so something like field: "[\"foo\", \"bar\"]" when used as where(field: %w(foo bar)).
If you want to create records for each hashtag from your list, you should iterate over it:
if #hashtag = Hashtag.where({:name => hashtag_list}).first
# do something if found the first one
else
hashtag_list.each do |hashtag|
# create an object
end
end
If you want to create missing hashtags even if the record is found, you can extract this to a private helper method with missing tags as the argument and re-write code as:
if #hashtags = Hashtag.where({:name => hashtag_list})
# do something if found
end
create_missing_hashtags(hashtag_list - #hashtags.pluck(:name))

rails "where" statement: How do i ignore blank params

I am pretty new to Rails and I have a feeling I'm approaching this from the wrong angle but here it goes... I have a list page that displays vehicles and i am trying to add filter functionality where the user can filter the results by vehicle_size, manufacturer and/or payment_options.
Using three select form fields the user can set the values of :vehicle_size, :manufacturer and/or :payment_options parameters and submit these values to the controller where i'm using a
#vehicles = Vehicle.order("vehicles.id ASC").where(:visible => true, :vehicle_size => params[:vehicle_size] )
kind of query. this works fine for individual params (the above returns results for the correct vehicle size) but I want to be able to pass in all 3 params without getting no results if one of the parameters is left blank..
Is there a way of doing this without going through the process of writing if statements that define different where statements depending on what params are set? This could become very tedious if I add more filter options.. perhaps some sort of inline if has_key solution to the effect of:
#vehicles = Vehicle.order("vehicles.id ASC").where(:visible => true, if(params.has_key?(:vehicle_size):vehicle_size => params[:vehicle_size], end if(params.has_key?(:manufacturer):manufacturer => params[:manufacturer] end )
You can do:
#vehicles = Vehicle.order('vehicles.id ASC')
if params[:vehicle_size].present?
#vehicles = #vehicles.where(vehicle_size: params[:vehicle_size])
end
Or, you can create scope in your model:
scope :vehicle_size, ->(vehicle_size) { where(vehicle_size: vehicle_size) if vehicle_size.present? }
Or, according to this answer, you can create class method:
def self.vehicle_size(vehicle_size)
if vehicle_size.present?
where(vehicle_size: vehicle_size)
else
scoped # `all` if you use Rails 4
end
end
You call both scope and class method in your controller with, for example:
#vehicles = Vehicle.order('vehicles.id ASC').vehicle_size(params[:vehicle_size])
You can do same thing with remaining parameters respectively.
The has_scope gem applies scope methods to your search queries, and by default it ignores when parameters are empty, it might be worth checking

How to represent dynamically derived data? (model without a table?)

I have tables for salespeople, products, and sales_activities (consider these to be 'transactions', but Rails reserves that name, so I'm calling them sales_activities).
For each salesperson, I need to dynamically derive their sales_total for a given day.
To do this, I run through the list of sales_activities, and create my derived content list (as an array of objects that hold salesperson_id & sales_total). I then want to display it in a view somewhat equivalent to an 'index' view of salespeople, but this view does not correspond to any of the existing index views I already have, due to the extra field (sales_total).
My question is how do I best define the class (or whatever) for each instance of my dynamically derived data (salesperson_id + sales_total)? It seems I could use a model without a table (with columns salesperson_id and the derived sales_total). That way, I could build an array of instances of these types as I generate the dynamic content, and then hand off the resulting array to the corresponding index view. However, from reading around, this doesn't seem 'the Rails way'.
I'd really appreciate advice on how to tackle this. The examples I've seen only show cases where a single overall total is required in the index view, and not dynamic content per row that can't be derived by a simple 'sum' or equivalent.
[This is a simplified explanation of the actual problem I'm trying to solve, so I'd appreciate help with the 'dynamically derived view / model without table' problem, rather than a short-cut answer to the simplified problem outlined above, thanks]
Maybe a plain Ruby class would do the trick?
class SalesStats
def initialize(sales_person, date_range = Date.today)
#sales_person = sales_person
#date_range = date_range
end
def results
# return a array or hash (anything which responds to an 'each' method), e.g:
SalesActivity.find(:all, :conditions => { :sales_person => #sales_person, :created_at => #date_range }).group_by(&:sales_person).collect { |person, sales_activities| { :person => person, :total => sales_activities.sum(&:price) } }
end
end
in the view:
<% #sales_stats.results.each do | stat | %>
<%= stat[:person] %> - <%= stat[:total] %>
<% end %>
However like mischa said in the comments this could equally be achieved using a method on SalePerson:
class SalesPerson < AR::Base
has_many :sales_activities
def total_sales(date_range)
sales_activities.find(:all, :conditions => { :created_at => date_range }).collect { ... }
end
end
Note: date_range can be a single date or a range e.g (Date.today-7.days)..Date.today

Resources