I have a parent form (lead) and child form (QuoteMetal) (which is rendered multiple times on the same submit). All the information from the forms gets written to their respective data tables, but I need the information from the child forms to perform a query and return those values. I have the forms created and a controller which writes the information to the data tables.
I need help with making the query results for each of the child forms then accessing them in the views. Here is what I currently have.
class LeadsController < ApplicationController
def index
#lead = Lead.new
#quote_metal = #lead.quote_metals.build
end
def create
#raise params.inspect
#lead = Lead.create!(lead_params) #write to data tables (which works)
#lead.quote_metals.each do |f|
#metal = Metal.calculate_metal(f).first #here is where my problem is! the #calculate_metal is the query located in my model
end
end
def show
end
private
def lead_params
params.require(:lead).permit([:name, .....
quote_metals_attributes: [:id...],
quote_melees_attributes: [:shape...],
quote_diamonds_attributes: [:shape...]
])
end
end
and the view:
<div class='container'>
<div class="row">
<div class='col-sm-3 col-sm-offset-2'>
<h3 class="text-center">Your Search: </h3>
</div>
<div class='col-sm-5'>
<h4 class="text-center">Returning estimates for a <%= #metal.metal %> setting
weighing <%= #metal.weight %> <%= #metal.unit %>. I am paying
<%= number_to_currency(#metal.price, precision: 2) %> per <%= #metal.unit %>.</h4>
</div>
</div>
Actions on QuoteMetal instances should be handled in the QuoteMetal class. So, I would replace:
#lead.quote_metals.each do |f|
#metal = Metal.calculate_metal(f).first #here is where my problem is! the #calculate_metal is the query located in my model
end
with:
#lead.quote_metals.each do |f|
f.create
end
And then in your QuoteMetal class, you can use a before_save callback and perform the calculation there. Keep in mind this will invoke calculate_metal every time a QuoteMetal is saved or updated.
Even better would be to use accepts_nested_attributes_for in the Lead model so that quote_metals can be automatically created when leads are created. See Rails documentation here. With this approach, you could eliminate the above three lines in the controller, but would still need the callback in the QuoteMetal class to perform the custom calculation.
Separately, be aware that your call to create! will raise an exception if validation fails. Not sure you intend that.
Related
I am consuming an external 3rd party API in my rails application and I'm wondering how I should best handle the response so I can add functionality to each object in the view.
I have a search box that triggers a call to an external museum API. I have a service object handling the call and parsing the response, so I can ensure a consistent structure.
I am then presenting the items returned in a listing so that individual items can be imported into the app.
I want to add a thumbnail image for each item in the listing where an image id exists in the API response or add a placeholder where it doesn't. I'm handling this with some logic in the view which I would like to extract out.
<% #mus_objects.each do |ob| %>
<div class="row mt-5">
<div class="col-lg-6">
<% if ob['_primaryImageId'] %>
<%= image_tag("https://iiif_image_server_example.com/#{ob['_primaryImageId']}/full/!140,140/0/default.jpg") %>
<% else %>
<%= image_tag("https://placehold.it/140x140?text=No+Image") %>
<% end %>
<%= ob["_primaryTitle"] %>
<%= link_to 'Import', new_admin_museum_object_path(ob) %>
</div>
</div>
<% end %>
This is my controller code:
class Admin::MuseumApiController < ApplicationController
def search
#search_term = params[:search_term]
if params[:searchtype] == 'search_term'
response = MuseumApiManager::FetchBySearchTerm.call(#search_term)
elsif params[:searchtype] == 'systemNumber'
response = MuseumApiManager::FetchBySystemNumber.call(#search_term)
end
if response && response.success?
#mus_objects = response["records"]
end
end
end
The response from my service object call looks like this:
#<OpenStruct success?=true, records=[{"systemNumber"=>"O123", "objectType"=>"Bottle", "_primaryTitle"=>"Bottle", "_primaryImageId"=>'1234'}]>
So my #mus_objects used by the view is an array of hashes.
I'm not sure what the best approach is for handling this. I'm thinking that something like a Decorator would be appropriate so that I could instead call ob.thumbnail_url within the view but a decorator requires a model.
**** Experimenting with Answer Proposed using ActiveModel ****
I've created an PORO as suggested and brought the search methods in here, although just while I'm testing it our I've left the logic in the ServiceObject.
class MuseumApiObject
include ActiveModel::Model
def self.fetch_by_search_term(search_term)
response = MuseumApiManager::FetchBySearchTerm.call(search_term)
response["records"]
end
def thumbnail_url
if _primaryImageId
"https://iiif_example.com/#{_primaryImageId}/full/!140,140/0/default.jpg"
else
'https://placehold.it/140x140?text=No+Image'
end
end
end
now in the controller I am doing this:
class Admin::MuseumApiController < ApplicationController
def search
#search_term = params[:search_term]
#mus_objects = MuseumApiObject.fetch_by_search_term(#search_term)
end
end
Now this still just creates an array of hashes. How can I instantiate this array of hashes into a load of MuseumApiObjects? I tried using create with the array but it does not exist.
I prefer to create model objects (not ActiveRecord) for encapsulating domain logic in my code (instead of services). This can be done by including ActiveModel::Model in POROs.
This strategy allows me to keep all of my business logic in my models, and now not all models need to have an associated table. As a plus, I still get all of the goodness like validations and callbacks, and I can also write my test cases like I would for any other model.
In your specific use case, I will create a MuseumObject model class (which may also include the logic for API interaction), and also the thumbnail_url method.
class MuseumObject
include ActiveModel::Model
attr_accessor :system_number, :object_type, primary_title, primary_image_id
def initialize(attributes)
self.system_number = attributes['systemNumber']
...
end
def self.search
...
end
def thumbnail_url
...
end
end
Edit:
You will need to initialize the object using the hash. You can define attr_accessors for all of your fields, and then you can simply initialize the objects with the hashes.
Say for example if your array of hashes is stored in museum_hashes variable, you can convert it to an array of your model objects as,
museum_hashes.map { |hash| MuseumObject.new(hash) }
If I have the #mis_clases array variable in the controller:
bloques_controller
def new
#bloque = Bloque.new
#mis_clases = Array.new
end
And I try to pass it to view, to fill it with clases:
form.html.erb
<div class="card-body">
<%= form.fields_for :clases do |clase| %>
<%= render partial: 'clase_fields' %>
<% #mis_clases << clase %>
<% end %>
</div>
And access it back in the controller:
bloques_controller
def create
#bloque = Bloque.new(bloque_params)
#mis_clases.each do |clase|
# Do something
end
...
end
I want to access the variable again when I save the form, but it throws this error:
The naming instance variable might be little misleading, but you need to remember that every new request to controller goes to new instance of Controller Class, so even if you create #mis_clases in #new, when reaching #create there is new instance without any variables set, and having nothing in common with recent one but class name.
You should pass variables as field values in form.
I'm creating a marketplace app where sellers can list items to sell. I am in the process of creating seller pages - a page with seller profile and their specific listings.
I've gotten as far as creating the page with seller listings but am having trouble pulling in the name and profile info which is in another model.
For the context here, I have 2 models - a listing model and a user model. The listing model has a user_id which joins with the user table. The user table has name, profile_image, profile_description.
My routes.tb:
get '/listings/s/:id' => 'listings#vendor', as: 'vendor'
My listings_controller.rb:
def vendor
#listings = Listing.where(user: User.find(params[:id]))
end
My view: Note that in the first line below I have ???. I want to pull in user.name in there, which is the sellers name. How do I pull that in? Once I know that, I can use the same process to pull in other fields from the user model.
<h4>Listings for ??? </h4>
<div class="center">
<div class="row">
<% #listings.each do |listing| %>
<div class="col-md-3 col-sm-3 col-xs-6">
<div class="thumbnail" >
<%= link_to image_tag(listing.image.url(:medium), class: "img-responsive"), listing, data: { no_turbolink: true } %>
</div>
<div class="caption">
<h3><%= link_to listing.name.downcase.titleize, listing, data: { no_turbolink: true } %></h3>
<p><%= number_to_currency(listing.price) %></p>
</div>
</div>
<% end %>
</div>
</div>
You can set another instance variable for the user. For example:
def vendor
#user = User.find(params[:id])
#listings = Listing.where(user: #user)
end
and then in the view:
<h4>Listings for <%= #user.name %> </h4>
Associations
You'll be best looking up about ActiveRecord Associations
ActiveRecord is an ORM (Object Relationship Mapper), which provides a level of abstraction for your application's object associations. The importance of this is that if you use it correctly, it will only make Rails run much faster, but also ensure your code is succinct:
#app/models/user.rb
class User < ActiveRecord::Base
has_many :listings
end
#app/models/listing.rb
class Listing < ActiveRecord::Base
belongs_to :user
end
This means that if you call a #user object, you'll be able to call the associative data too, like so:
def vendor
#user = User.find params[:id]
#listings = #user.listings
end
The value of this is that the association is then take care of with ActiveRecord (not in your controller). If you therefore change your association (change models etc), you'll have a single place to make the required changes, rather than having to pick through all your controllers again
Objects
To give you a better understanding about this, you need to consider the role of objects in a Rails application.
Since Rails is built on top of Ruby (it's a gem), it's an object orientated framework. Object orientation means more than just being a buzzword - it's an entire practice of programming; putting the objects for your software at the center of the flow (as opposed to a logic-driven flow):
In reality, objects are just variables with a lot of JSON-notation data inside, including things like data-type etc. Rails populates objects in your models, allowing you to then use the objects in a variety of Rails helper methods such as form_for etc.
The reason this is important is because of how object orientation works. You can associate objects, manipulate them & destroy them. This means that anything you do in Rails should be based around objects, which is why the association I mentioned above is so important
So I've got two records from Model, a and b. I want to do this:
def do_codependant_stuff(a,b)
a.attribute += b.attribute2
b.attribute = b.stuff+a.stuff
end
I keep hearing fat model, skinny controller, and no business logic in views, so I've put this in my Model model. Based on what a user clicks in one of my views, I want to call do_codependant_stuff(a, b) or do_codependant_stuff(b,a).
I've been just using the basic crud controller actions up to this point, and I'm not quite sure how to make this happen. Do I add this logic to my ModelController update action? Because it's technically updating them, just in a more specific way. Or make another action in the controller? How do I call it/set it up? Most of the default new, update etc are somehow called behind the scenes based on their respective views.
And is updating two things at a time bad practice? Should I split the do_codependant_stuff method into two instance methods and call them on each record with the other as a parameter?
Thanks for reading.
Edit:
Alright, real world code. I'm displaying on the home page of my app two pictures. The user selects the one they like most. The ratings of these pictures change based on a chess ranking algorithm. This is the relevant section of my picture class.
class Picture < ActiveRecord::Base
...
...
def expected_first_beats_second(picture first, picture second)
1/(1+10**((second.rating-first.rating)/400))
end
def first_beat_second(picA,picB)
Ea = expected_first_beats_second(picA,picB)
picA.rating += 50*(1-Ea)
picB.rating += 50*(-(1-Ea))
end
end
The partial I'm using for the view just displays two pictures at random so far with
<% picA = Picture.offset(rand(Picture.count)).first %>
<% picB = Picture.offset(rand(Picture.count)).first %>
<div class = "row">
<div class = "col-md-5">
<%= image_tag picA.url, :class => "thumbnail" %>
</div>
<div class = "col-md-5">
<%= image_tag picB.url, :class => "thumbnail" %>
</div>
</div>
I need to somehow link the onclick of those images to the method in the model.
Here's some code to get you started. Note the comments, they're ruby and rails best practices
Controller:
class PC < AC
...
def compare
# count query once, save the number
count = Picture.count
#pic_a = Picture.offset(rand(count)).first
#pic_b = Picture.offset(rand(count)).first
end
def compare_submit
# Note variables are snake cased, not camel cased
pic_a = Picture.find(params[:winner_id])
pic_b = Picture.find(params[:loser_id])
pic_a.beats(pic_b)
redirect to compare_pictures_path # Or wherever
end
...
end
Any sort of querying should be done in the controller or model. You essentially should never directly access a model in your view. At this point your view has access to the instance variables set in the controller: #pic_a and #pic_b
View:
<%= link_to compare_submit_pictures_path(winner_id: #pic_a.id, loser_id: #pic_b.id) do %>
<%= image_tag #pic_a.url, :class => "thumbnail" %>
<% end %>
<%= link_to compare_submit_pictures_path(winner_id: #pic_b.id, loser_id: #pic_a.id) do %>
<%= image_tag #pic_b.url, :class => "thumbnail" %>
<% end %>
So we just linked to a new path (see routes below) that will pass two parameters: winner_id and loser_id so whichever picture the user clicks on you'll know which one they chose and which one they didn't choose.
Model:
class Picture < AR::Base
# Method arguments don't need a type declaration
def beats(loser)
# Always lowercase variables
ea = 1/(1+10**((loser.rating-self.rating)/400))
self.rating += 50*(1-ea)
loser.rating += 50*(-(1-ea))
self.save
loser.save
end
end
This explicitly uses self for clarity, which isn't necessary. Calling save or rating = ... implicitly calls it on self since we're in the context of an instance method on the picture Model.
Routes:
resource :pictures do
collection do
get :compare
get :compare_submit
end
end
I have a form where I'd like to create a parent record and a child record at the same time. For a simple example let's say its a Company with the first Employee.
in my controller I do something like:
def new
#company = Company.new
#company.employees.new
end
and in my view this:
<%= form_for(#company) do |form| %>
<div>
<%= form.label :name %>
<%= form.text_field :name %>
</div>
<%= form.fields_for :employees do |employee_form| %>
<div>
<%= employee_form.label :name %>
<%= employee_form.text_field :name %>
</div>
<% end %>
<% end %>
and back in my controller again:
def create
#company = Company.new(params[:company])
#company.employees << Employee.new(params[:company][:employees_attributes]["0"])
# save stuff
end
Question 1:
I couldn't get the employee collection on the company to be populated with the single employee created in the form. When I looked at the params I found the [:employees_attributes]["0"] stuff.
What I have works, but is there a cleaner way to do this?
Question 2:
If the validation doesn't pass for the employee I get a generic "Employees is invalid" instead of the Name required validator message. I get I am calling save on the collection and rails is doing its best to bubble a validation error up, but is there a cleaner way to do this so I can get the errors specific to the employee?
In Short
How can I clean this up so the related models are created automatically from the params, and so that I get the validation messages for a single employee.
Thanks for looking.
1) fields_for arranges for the child objects attributes to be nested inside the parent objects attributes in the params hash that gets sent back to the controller action. To get Rails to automatically update the child objects tell the parent model to accept nested attributes using the accepts_nested_attributes_for declaration.
2) There is an errors object for every ActiveRecord object. Loop through the errors list and display the messages.
Best way to achieve this is to create a partial and a view helper method that will take render the errors for you. then replace the generated errors messages in the forms with a call to your render_error_messages method. You have all the code to do this already in the generated forms. You just need to refactor that code into a partial, create the helper - which should accept an array of model names as a parameter then do what you want with the info. Wither render a partial for each model or render a partial that will deal with child objects as well as the parent object. Totally your call.
3) Change your new action to build rather that create a new child object so instead of
def new
#company = Company.new
#company.employees.new
end
do this
def new
#company = Company.new
#company.employees.build
end
4) Watch those Railscasts to see how accepts_nested_attributes works
http://railscasts.com/episodes/196-nested-model-form-part-1
and
http://railscasts.com/episodes/197-nested-model-form-part-2
Update
So how does the above information leave you in relation to your questions.
1) What I have works, but is there a cleaner way to do this?
You've fixed the new action as per point 3 above right? Now your create action can look like this
def create
#company = Company.new(params[:company])
# save stuff
end
Which is much cleaner as it has reverted to the original generated create action.
You may not think that's much of an update and therefore not that much cleaner. Well in isolation you'd be right. But consider that you could add as many relationships as you like ad add as many fields_for declarations as you like nd you could turn the user -> employee relationship into a has_many (I know that you wouldn't). You could do all that and your create and update actions stay EXACTLY the same and that's why it's cleaner.
2) is there a cleaner way to do this so I can get the errors specific to the employee?
Given my response in point 2 above you know that there is an errors object on the employee object as well as on the user object right? You also know now that you can loop through that errors object to get the messages right?
So you could do this
<% if #user.employee.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(#user.employee.errors.count, "error") %> prohibited this user from being saved:</h2>
<ul>
<% #user.employee.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
At the risk of repeating myself I'll just say that you should refactor your error messages view code into a partial that will take any object as a parameter then you can call it from any view thus enabling you to change the styling and the functionality for all your forms.
Hope that's clearer