In Ransack update ranackers dynamically when a model gets updated - ruby-on-rails

I have Company Customer and CompanyCustomerField models. Customers store the hstore values in a column "properties" - the keys are from the CompanyCustomerField#name field. When a new CompanyCustomerField get created i need to add the #name to ransack to make them searchable.
When a new CompanyCustomerField gets added and I go to the search form I get
undefined method `*_cont' for #<Ransack::Search:0x00007ff670100978>
because the new field is not available for searching. If i shutdown my fails server and reboot it works because it gets it into ransack.
I don't know how to dynamically add the functionality into ransack. Any ideas greatly appreciated.
Customer.rb. this puts all the searchable fields into ransack but doesnt update it when new ones get added. because this only gets called once.
class Customer < ApplicationRecord
# ['favorite_color', 'receive_email_marketing' etc etc]
CompanyCustomerField.pluck(:name).each do |name|
ransacker name.to_sym do |parent|
Arel::Nodes::InfixOperation.new('->', parent.table[:properties], Arel::Nodes.build_quoted(name))
end
end
end
here is the search form:
#customers/index.html
<%= search_form_for #search, remote: true do |f| %>
<% current_company.customer_fields.each do |field| %>
<%= render "customers/search_fields/#{field.field_type}", f: f, field: field %>
<% end %>
<% end %>
#customers/search_fields/text_field
<%= f.label (field.name + "_cont").to_sym, field.name.humanize %>
<%= f.text_field (field.name + "_cont").to_sym %>
....
Even if moving reloading to controller, still same result.
CustomersController.rb
def index
Customer.reload_ransacker
#search = current_company.customers.includes(:owner).ransack(params[:q])
#customers = #search.result.page(params[:page])
end
Customer.rb
def self.reload_ransacker
puts "==="
puts "reload ransacker"
puts "==="
CompanyCustomerField.pluck(:name).each do |name|
ransacker name.to_sym do |parent|
Arel::Nodes::InfixOperation.new('->', parent.table[:properties], Arel::Nodes.build_quoted(name))
end
end
end
ActionView::Template::Error (undefined method `foo_cont' for #<Ransack::Search:0x00007fba3c05d5b8>):

SOLUTION:
needed to override:
module Ransack
module Adapters
module ActiveRecord
module Base
def ransacker(name, opts = {}, &block)
#ransackable_attributes = nil
self._ransackers = _ransackers.merge name.to_s => Ransacker
.new(self, name, opts, &block)
end
end
end
end
end
#ransackable_attributes needs to be reset to nil so in def ransackable_attributes(auth_object = nil) the instance var is nil and can reload
should be considered a bug in ransack.

Related

Rails: How to pass params of multiple checkbox to the model

i built this form that generate me some chebox with value like "U6", "U8" eccc
<%= form.label "Seleziona Categorie" %>
<% TeamCategory::NAMES.each do |category| %>
<%= check_box_tag 'categories_selected[]', category -%>
<% end %>
Now i have to pass the value of selected check_box to a method in my model.
Now is:
def create_tournament_team_categories
TeamCategory::NAMES.each do |name|
team_category = TeamCategory.where(name: name).first_or_create
self.tournament_team_categories << TournamentTeamCategory.create(team_category: team_category)
end
end
I would like to replace the TeamCategory::NAMES.each do with "selected check_box each do" and TeamCategory.where(name: name) with the value selected.
Thank you in advance
I am a newbie with Rails. What I see is that you took the part of the form to create the team, right?
For your code straight forward it could be:
<%= form.label "Seleziona Categorie" %>
<% TeamCategory::NAMES.each do |name| %> #you are looping through team category NAMES constant
<%= check_box_tag 'category_names_selected[]', name %>
<% end %>
Your form as is allows more than one category to be selected.
For the method:
def create_tournament_team_categories(category_names_selected)
category_names_selected.each do |name|
team_category = name
self.tournament_team_categories << TournamentTeamCategory.create(team_category: team_category)
end
end
you will probably use this method in your teams_controller.rb. In the controller, you should be able to retrieve from params a freshly created array of selected names with something along the lines with this.
#category_names_selected = params[:category_names_selected]
I do not know how complicated your app is so it might also be nested under ["team"][:category_names_selected] or ["team"]["category_names_selected"] in your params hash.
To see the exact structure of the params hash and adjust the equation above you can add for example require 'pry' at the top of your controller file and then but the binding.pry just after the part where your method is executed. When you restart the server and the app hits this part of the controller you should be able to see the exact structure of your params hash in the terminal.
You can then pass the array to the method that you can call in the controller. Do not forget to add :category_names_selected to the strong params in the controller. I hope this helps.
Controller on line 30
def create
#tournament = Tournament.new(tournament_params)
#tournament.sport_club = current_user.sport_club
#category_names_selected = params[:category_names_selected]
if #tournament.save
redirect_to tournaments_path, notice: 'Torneo creato con successo'
end
end
Method create_tournament_team_categories in the model
after_create :create_tournament_team_categories
def create_tournament_team_categories(category_names_selected)
#category_names_selected.each do |name|
team_category = name
self.tournament_team_categories << TournamentTeamCategory.create(team_category: team_category)
end
end

"First argument in form cannot contain nil or be empty"

I have difficulties with ruby on rails syntax.
I got this error
First argument in form cannot contain nil or be empty
class PersonalsController
def index
end
def create
#personal = Personal.new
end
def new
#personal = Personal.new
end
def show
#personal = Personal.find([:id])
end
end
index.html.erb
<%= form_for #personal do |f| %>
<%= f.label :title %><br>
<%= f.text_field :title %>
<%= f.submit %>
<% end %>
the value of #personal is nil that's why you are getting error.
Change you code like this
def index
#personal= Personal.all
end
form_for is helper method
check with this link form_helper
The error is generated since #personal was not set in the controller. So either you add a #personal = Personal.new to the index method, or set it to a specific database entry, e.g., #personal = Personal.find(1)
However, it seems strange that you have a form displaying a single record in the index view.
More likely, you want to have the form in your new or edit views (in the former case you typically use new, while in the latter case you would use the find method to find a specific record and let the user edit it).
In the index method, you usually use the controller to select a group of records (e.g., #ps = Personal.all to get all the records) and iterate over them in the view (#ps.each do |person| .... end)
P.S. The show method should probably use Personal.find(params[:id]) instead of Personal.find([:id])

Rails 4 HABTM stops working when I add "multiple true" to collection_select

I've googling and trying everything I could think of for the past couple of days to solve a relatively simple (I presume) issue with has_and_belongs_to_many relation.
I managed to successful use the HABTM relation to submit a single relation value. Here's the sample code:
Model:
class Livre < ActiveRecord::Base
has_and_belongs_to_many : auteurs
end
class Auteur < ActiveRecord::Base
has_and_belongs_to_many :livres
end
Controller:
def new
#livre = Livre.new
#auteurs = Auteur.all
end
def create
#livre = Livre.new(livre_params)
if #livre.save
redirect_to [:admin, #livre]
else
render 'new'
end
end
private
def livre_params
params.require(:livre).permit(:name, :auteur_ids)
end
View:
<% f.label :auteur %><br>
<% f.collection_select(:auteur_ids, #auteurs, :id, :name) %>
Posted Params:
{"utf8"=>"✓",
"authenticity_token"=>"mAXUm7MRDgJgCH00VPb9bpgC+y/iOfxBjXSazcthWYs=",
"livre"=>{"name"=>"sdfsdfd",
"auteur_ids"=>"3"},
"commit"=>"Create Livre"}
But when I try to add "multiple true" to the view's collection_select helper, the (now multiple) relation doesn't get saved anymore. Sample code:
(both Model and Controller unchanged)
View:
<% f.label :auteur %><br>
<% f.collection_select(:auteur_ids, #auteurs, :id, :name, {}, {:multiple => true}) %>
Posted Params:
{"utf8"=>"✓",
"authenticity_token"=>"mAXUm7MRDgJgCH00VPb9bpgC+y/iOfxBjXSazcthWYs=",
"livre"=>{"name"=>"sdfsdf",
"auteur_ids"=>["1",
"5",
"3"]},
"commit"=>"Create Livre"}
As you can see, the params for "auteur_ids" is now an array. That's the only difference.
What am I doing wrong?
Just to clarify: both piece of code are able to add a new record to the livres db table, but only the 1st code is able to add the appropriate record to the auteurs_livres db table. The second one simply does not insert anything into auteurs_livres.
(I run on ruby 1.9.3p194 and rails 4.0.1)
Thanks!
Answer
For the fine folks stuck with the same problem, here's the answer:
Edit your controller and change the permitted parameter from :auteur_ids to {:auteur_ids => []}
params.require(:livre).permit(:name, {:auteur_ids => []})
And it now works :)
For the fine folks stuck with the same problem, here's the answer:
Edit your controller and change the permitted parameter from :auteur_ids to {:auteur_ids => []}
params.require(:livre).permit(:name, {:auteur_ids => []})
And it now works :)
You worked out the solution because Rails now expects auteur_ids to be an array, rather than a single item. This means that instead of just passing a single entity to the model, it will package the params as [0][1][2] etc, which is how you can submit your HABTM records now
There is a more Rails way to do this, which is to use accepts_nested_attributes_for. This is going to seem like a lot more work, but it will dry up your system, and also ensure convention over configuration:
Model
class Livre < ActiveRecord::Base
has_and_belongs_to_many : auteurs
accepts_nested_attributes_for :auteurs
end
class Auteur < ActiveRecord::Base
has_and_belongs_to_many :livres
end
Controller
def new
#livre = Livre.new
#livre.auteurs.build
#auteurs = Auteur.all
end
def create
#livre = Livre.new(livre_params)
if #livre.save
redirect_to [:admin, #livre]
else
render 'new'
end
end
private
def livre_params
params.require(:livre).permit(:name, auteur_attributes: [:auteur_id])
end
Form
<%= form_for #livre do |f| %>
<%= f.text_field :your_current_vars %>
<%= f.fields_for :auteurs do |a| %>
<%= a.collection_select(:auteur_id, #auteurs, :id, :name, {}) %>
<% end %>
<% end %>
This will submit the auteur_id for you (and automatically associate the livre_id foreign key in the HABTM model. Currently, this will only submit the number of objects which have been built in the new action -- so in order to add more items, you'll have to build more

Rails - when a row returns nil

I'm getting this error when loading my view that has a Model User that has no records in it. I simply want it to return "Unassigned" in the view if there is no record. Otherwise, display the first and last name of the User. Displaying User first and last name works as expected when a record exists.
I've messed around with many different combinations of this and can't see to get it to work.
Error:
undefined method `full_name' for nil:NilClass
In tickets.index.html.erb:
<% #tickets.each do |ticket| %>
<%= ticket.user.full_name %>
<% end %>
In ticket.rb Model
def full_name
if full_name.blank?
full_name = "Unassigned"
else
ticket.user.first_name + ' ' + ticket.user.last_name
end
end
The problem is that when you call ticket.user that might be nil, so you can't call anything on it. You have the right idea in making a method on your Ticket model to isolate this, but it's calling itself which will get an infinite loop, and also you're still calling the method on your user and not your ticket. Try this:
In your view:
<% #tickets.each do |ticket| %>
<%= ticket.full_name %>
<% end %>
In your ticket model:
def full_name
if user.nil?
return "Unassigned"
else
return user.full_name
end
end
And in your user model:
def full_name
return "#{first_name} #{last_name}"
end
I made the user model have its own method for this so it's further isolated (following OO practices).

Looping Through Multiple Arrays in Ruby

I have multiple arrays of instances of ActiveRecord subclass Item that I need to loop through an print in accordance to earliest event. In this case, I need to print print out payment and maintenance dates as follows:
Item A maintenance required in 5 days
Item B payment required in 6 days
Item A payment required in 7 days
Item B maintenance required in 8 days
I currently have two queries for finding maintenance and payment items (non-exclusive query) and something like the following to output them:
<%- item_p = nil -%>
<%- item_m = nil -%>
<%- loop do -%>
<% item_p ||= #items_p.shift %>
<% item_m ||= #items_m.shift %>
<%- if item_p.nil? and item_m.nil? then break -%>
<%- elsif item_p and (item_m.nil? or item_p.paymt < item_m.maint) then -%>
<%= item_p.name %> payment required in ...
<%- elsif item_m and (item_p.nil? or item_m.maint < item_p.paymt) then -%>
<%= item_m.name %> maintenance required in ...
<%- end -%>
<%- end -%>
Any way to cleanup the readability of the above (ugly) code?
Embrace duck-typing and make sure that your objects are polymorphic. You want your payment items to be comparable with maintenance items, in order to sort them.
So, suppose you have a Payment and a Maintenance class:
module Due
include Comparable
# Compare this object with another. Used for sorting.
def <=>(other)
self.due <=> other.due
end
end
class Payment < ActiveRecord::Base
include Due
alias_method :due, :payment
def action
"#{name} requires payment"
end
end
class Maintenance < ActiveRecord::Base
include Due
alias_method :due, :maintenance
def action
"#{name} requires maintenance"
end
end
See how we create an action, due and <=> method in both classes? We also took care to include the Ruby built-in module Comparable. This allows us to do the following:
# Assuming 'payment' and 'maintenance' are date fields...
a = Payment.new :payment => 3.days.from_now
b = Maintenance.new :maintenance => 2.days.from_now
[a, b].sort
#=> [b, a]
The view then becomes as simple as:
<% (#payment_items + #maintenance_items).sort.each do |item| %>
<%= item.action %> in <%= distance_of_time_in_words_to_now(item.due) %><br/>
<% end %>
I'm sure I haven't got the details of your implementation right, but I hope this gives you an idea of how to approach your problem.
This is quick and dirty (i.e. not optimized):
# In your controller:
#items = #items_p.map{ |item| {:item => item, :days => item.paymt, :description => "payment"} }
#items += #items_m.map{ |item| {:item => item, :days => item.maint, :description => "maintenance"} }
#items = #items.sort_by{ |item| item[:day] }
# In your view:
<% #items.each do |item| %>
<%= item[:item].name %> <%= item[:description] %> required in <%= item[:days] %> days
<% end %>
You're doing way too much in your view. Really you should figure out all of this in the controller and pass through a cleaned up structure that you can iterate over for display purposes.
As an example:
length = [ #items_p.length, #items_m.length ].sort.last
#messages = [ ]
length.times do |i|
item_p = #items_p[i]
item_m = #items_m[i]
if (item_p and (item_m and item_p.paymt < item_m.maint) or !item_m)
#messages << "#{item_p.name} payment required in ..."
elsif (item_m and (item_p and item_m.maint < item_p.paymt) or !item_p)
#messages << "#{item_m.name} maintenance required in ..."
end
end
You would then iterate over #messages as required.
The real problem here is that you haven't structured these objects for this sort of thing strategically speaking. It would be nice if you had a single method for the due date instead of having to differentiate between paymt and maint according to the type. Likewise, it would be better if both of these were paired up into an Array instead of supplied separately.
If you had them in [ p, m ] pairs you could iterate much more simply:
items.each do |pair|
first_due = pair.compact.sort_by(&:due).first
#messages << "#{first_due.name} #{first_due.action} required in ..."
end
The action method would return payment or maintenance as required.

Resources