How Do I Pass Additional Parameters in Rails from Controller to Model - ruby-on-rails

I would like to pass an additional field, say item_id into these line of codes (both controller and model):
# transactions_controller.rb (controller)
#transaction = Transaction.new(app_token: params[:token])
# transaction.rb (model)
def app_token=(token)
write_attribute(:app_token, token)
# I want to add a few more lines of code here so that I can manipulate item_id
end
That means, I would like my item_id to be passed from the controller to the model so that I can manipulate it do some customization within the model.
What would be the best way in order to do as such (based on the code above)?
===Updated as of 1-Sep-2014 for further details===
I have an association of cart and transaction in which cart has_many transactions and transaction belongs_to cart; below is the controller;
# transactions_controller.rb (controller)
def new
#transaction = Transaction.new(app_token: params[:token])
end
While the method below is in the model:
# transaction.rb (model)
def app_token=(token)
write_attribute(:app_token, token)
# I want to add a few more lines of code here so that I can manipulate cart.id
end
What I would like to achieve here is to pass in the cart.id into the method of app_token which is located in transaction.rb. Please note that this cart.id is not meant to be saved into the database which I can easily do it via the create method through build, but rather this cart.id is used to be passed into the method to invoke other methods which is located within app_token method which sits in the transaction.rb model. The reason why I am doing this is because, the service which I am communicating with returns a token and I would like to hold the token and perform another method which requires the cart.id to be in.
Thus, I just would like to understand, based on the given format of the controller and model above, what is the most recommended manner to pass in this cart.id into the app_token method which sits in the transaction.rb model which I would want to use for other functions within the method?
Thank you!

I have an association of cart and transaction in which cart has_many transactions and transaction belongs_to cart
Since that's the case and you already have a cart object, in your controller just instantiate the transaction from the cart:
transaction = cart.transactions.build app_token: params[:token]
transaction.save
cart_id will then be available to all the instance methods in the model, so there is no need to extend app_token= with additional logic unrelated to the app_token. Instead, take advantage of ActiveRecord callbacks. For example, you could use a before_save callback to implement your logic:
class Transaction < ActiveRecord::Base
belongs_to :cart
before_save :do_something_with_cart
def do_something_with_cart
# I want to add a few more lines of code here so that I can manipulate cart_id
end
end
If for some reason a callback does not fit your use casae, call the custom method directly in the controller:
transaction = cart.transactions.build app_token: params[:token]
transaction.do_something_with_cart

You don't need to override app_token=
# transactions_controller.rb (controller)
#transaction = Transaction.new(app_token: params[:token], item_id: params[:item_id])
#transaction.save

Attribute
It will mainly depend on whether you have item_id set up as an attribute, either virtual or in the database.
If you have an associative foreign_key set up already, you'll be able to discount what I'm going to write, but in case you haven't, you should consider the following:
#app/models/transaction.rb
class Transaction < ActiveRecord::Base
belongs_to :item # -> expects item_id by default
end
If you don't have an association set up (and hence no attributes), you'll want to use attr_accessor to create a virtual attribute:
#app/models/transaction.rb
class Transaction < ActiveRecord::Base
attr_accessor :item_id
end
Params
Passing attributes in Rails 4 is actually the least of your concerns - you can pass as many attributes through your routes as you wish. The problems occur when you try and match the items with your db objects (hence my recommendation above)
If you want to pass the item_id attribute, you'll just have to ensure it's set in your view. This is either done with your routes, or by passing it in your form:
#config/routes.rb
resources :items
resources :transactions #-> domain.com/items/:item_id/transactions/new
end
This would allow you to pass the item_id you wish (which will load in your controllers as params[:item_id]. You can also use the following:
#app/views/transactions/new.html.erb
<%= form_for #transaction do |f| %>
<%= f.text_field :item_id %>
<%= f.text_field :token %>
<%= f.submit %>
<% end %>
This will give you the ability to send the two different attributes to your controller, which can then populate as follows:
#app/controllers/transactions_controller.rb
class TransactionsController < ApplicationController
def new
#transaction = Transaction.new
end
def create
#transaction = Transaction.new transaction_params
#transaction.save
end
private
def transaction_params
params.require(:transaction).permit(:item_id, :token)
end
end
It must be noted the form method will only be viable if you have the attribute defined in your model - either in the database, or virtual (with attr_accessor)

Related

Issue with nested forms and posting simultaneously to two associated tables

So I have tried now to do this for multiple hours pulling from different sources. I'm sure I can probably hack this together in a very ugly way with multiple creates, but im sure there is a simple way to do this.
im trying to create a simple time and task tracker. I have a time tracker table that records the start and end times of a task and the task id.
There is an associated table that contains the title and details of the task.
When I try to .save! it
it abandons the save with ActiveRecord::RecordInvalid (Validation failed: Task must exist):
the logic seems to be that it should first try to create the task details (title and details) which creates the ID, and then it should create the timerecord(timetracker) record and include the task id in the task_id column of the time record. But based on the error, guess this is not happening.
I appreciate any help getting this working or point me in the right direction of what im doing incorrectly.
##########
Edit After posting, I realized that a task can have multiple time records, since a user can stop and restart on the same task that creates a new record with the same task ID. I've changed the task model from has_one to has_many.
###########
Here are the two models
class Timerecord < ApplicationRecord
belongs_to :task
end
class Task < ApplicationRecord
#has_one :timerecord #this has been changed to
has_many :timerecord
accepts_nested_attributes_for :timerecord
end
in my Timetracker controller #new & #create & private
def new
#timetracker = Timerecord.new
#timetracker.build_task
end
def create
#timetracker = Timerecord.new(timetracker_params)
if #timetracker.save
flash[:notice] = "Time Tracker Started"
redirect_to timetracker_index_path
end
end
private #at the tail of permit you will see including the task attribute title
def timetracker_params
params.require(:timerecord).permit(:user_id, :agency_id, :client_id, :task_id, :is_billable, :time_start, :time_end, :manual_input_hours, :timerecordgroup_id, :is_paused, :service_type_id, task_attributes: [:title])
end
and the form
<%= form_with(model: #timetracker, url: timetracker_index_path, local: true) do |f| %>
... a bunch of fields for #timetracker
<%#= fields_for :task, #timetracker.task do |t| %>
<%#= t.text_field :title, class: "form-control shadow-sm", placeholder: "Title" %>
<%# end %>
... more fields for timetracker
<% end %>
First of all you have to know which model is nested within the other and which one you're going to call the save method on.
Since your models are:
class Timerecord < ApplicationRecord
belongs_to :task
end
class Task < ApplicationRecord
#has_one :timerecord #this has been changed to
has_many :timerecord
accepts_nested_attributes_for :timerecord
end
You should call save on the Task model (which will also save timerecord) and not on the TimeRecord model because your Task model is the one accepting the nested attributes for TimeRecord model and not the other way around.
Your controller should look something like this:
def create
#timetracker = Task.new(task_params)
if #timetracker.save
flash[:notice] = "Time Tracker Started"
redirect_to timetracker_index_path
end
end
private
def task_params
params.require(:task).permit(#your permitted params for task, timetracker_attributes: [:title])
end
if you notice, your Task model accepts nested attributes for timetracker, hence you should permit timetracker_attributes and not task_attributes.
It's easy to know which nested params you should permit, just look at your models and see which model the accept_nested_attributes_for is refering to and then just add _attributes at the end.
Check out the accept_nested_attributes_for documentation, it should help
https://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html
Not an answer as such, just a few suggestions to point you in the right direction as there are quite a few things wrong with your implementation.
you don't need accepts_nested_attributes_for :timerecord
in task, as you aren't creating timerecords at the time you are creating/editing the task.
you don't need to require the nested attributes for task in the TimerecordController
the form should have a hidden_field for the :task_id - this is primarily why you are getting the error.
but you don't have an associated task for the timerecord so in addition you need to assign it at the point you generate a new timerecord. Your TimerecordController new action should be something like:
def new
#timetracker = Timerecord.new
#timetracker.task_id = params[:task_id]
end
which means you also need to ensure you are sending task_id to the new action, for example new_timerecord_path(task_id: my_task_instance.id)

Validate Associated Object Presence Before Create

I've been following the Getting Started rails tutorial and am now trying some custom functionality.
I have 2 models, Person and Hangout. A Person can have many Hangouts. When creating a Hangout, a Person has to be selected and associated with the new Hangout. I'm running into issues however when I call my create action. This fires before the validate_presence_of for person.
Am I going about this the wrong way? Seems like I shouldn't have to create a custom before_create validation to make sure that a Hangout was created with a Person.
#hangout_controller
def create
#person = Person.find(params[:hangout][:person_id])
#hangout = #person.hangouts.create(hangout_params)
#hangout.save
redirect_to hangouts_path(#hangout)
end
#hangout.rb
class Hangout < ActiveRecord::Base
belongs_to :person
validates_presence_of :person
end
#person.rb
class Person < ActiveRecord::Base
has_many :hangouts
validates :first_name, presence: true
validates :met_location, presence: true
validates :last_contacted, presence: true
def full_name
"#{first_name} #{last_name}"
end
end
Create action fires before the validate_presence_of for person
I think you are confused about rails MVC. Your form contains a url and when you submit your form your form params are send to your controller action according to the routes you have defined in routes.rb Your controller action, in this case create action, interacts with model this is very it checks for your validations and if all the validations are passed your object is saved in databse so even though in your app the control is first passed to your controller but your object is saved only once if all the validations are passed.
Now lets comeback to your code. There are couple of things you are doing wrong
a. You don't need to associate your person separately:
In your create action you have this line:
#person = Person.find(params[:hangout][:person_id])
You don't need to do this because your person_id is already coming from your form and it'll automatically associate your hangout with person.
b. You are calling create method instead of build:
When you call .association.create method it does two things for you it first initialize your object, in your case your hangout and if all the validations are passed it saves it. If all the validations are not passed it simply rollback your query.
If you'll use .association.build it'll only initialize your object with the params coming from your form
c. Validation errors won't show:
As explained above, since you are calling create method instead of build your validation error won't show up.
Fix
Your create method should look like this:
def create
#hangout = Hangout.new(hangout_params) # since your person_id is coming from form it'll automatically associate your new hangout with person
if #hangout.save
redirect_to hangouts_path(#hangout)
else
render "new" # this will show up validation errors in your form if your hangout is not saved in database
end
end
private
def hangout_params
params.require(:hangout).permit(:person_id, :other_attributes)
end
You are confused with the controller and model responsibilities.
Let me try to explain what I think is confusing you:
First try this in your rails console:
Hangout.create
It shouldn't let you because you are not passing a Person object to the create method. So, we confirm that the validation is working fine. That validation means that before creating a Hangout, make sure that there is a person attribute. All this is at the model level, nothing about controllers yet!
Let's go to the controllers part. When the create action of the controller 'is fired', that controller doesn't know what you are trying to do at all. It doesn't run any validations. It is just an action, that if you want, can call the Hangout model to create one of those.
I believe that when you say 'it fires' you are saying that the create action of the HangoutController is called first than the create method on the Hangout model. And that is completely fine. The validations run at the model level.
Nested Attributes
I think you'll be better using accepts_nested_attributes_for - we've achieved functionality you're seeking before by using validation on the nested model (although you'll be able to get away with using reject_if: :all_blank):
#app/models/person.rb
Class Person < ActiveRecord::Base
has_many :hangouts
accepts_nested_attributes_for :hangouts, reject_if: :all_blank
end
#app/models/hangout.rb
Class Hangout < ActiveRecord::Base
belongs_to :person
end
This will give you the ability to call the reject_if: :all_blank method -
Passing :all_blank instead of a Proc will create a proc that will
reject a record where all the attributes are blank excluding any value
for _destroy.
--
This means you'll be able to create the following:
#config/routes.rb
resources :people do
resources :hangouts # -> domain.com/people/:people_id/hangouts/new
end
#app/controllers/hangouts_controller.rb
Class HangoutsController < ApplicationController
def new
#person = Person.find params[:people_id]
#hangout = #person.hangouts.build
end
def create
#person = Person.find params[:people_id]
#person.update(hangout_attributes)
end
private
def hangout_attributes
params.require(:person).permit(hangouts_attributes: [:hangout, :attributes])
end
end
Although I've not tested the above, I believe this is the way you should handle it. This will basically save the Hangout associated object for a particular Person - allowing you to reject if the Hangout associated object is blank
The views would be as follows:
#app/views/hangouts/new.html.erb
<%= form_for [#person, #hangout] do |f| %>
<%= f.fields_for :hangouts do |h| %>
<%= h.text_field :name %>
<% end %>
<%= f.submit %>
<% end %>

Rails: passing params hash to model

I have a user-to-user messaging system. I'm trying to pass an array of user ids to a ConversationUser (join table) model which would then create multiple conversation_users from each individual user.id. The two fields in ConversationUser are conversation_id and user_id. I'm able to initialize a single conversation user because the new conversation_id is being passed along to the model, but for some reason, the hash of user ids is not getting to my model. I'm getting a Validation failed: User can't be blank
My conversation/new view for capturing the user_ids:
<%= check_box_tag "conversation_user[recipient][]", user.id %> <%= user.name %><br />
I know this is working because part of my params that I'm receiving back are:
"conversation_user"=>{"recipient"=>["9", "10"]}
The essentials of my Rails 4 controller & strong params:
class ConversationsController < ApplicationController
def new
#user = User.find(params[:user_id])
#conversation = #user.conversation_users.build
#conversation.build_conversation.messages.build
end
def create
#conv = Conversation.create!
#conversation = #conv.conversation_users.create!(conversation_user_params)
end
def conversation_user_params
params.require(:conversation_user).permit(recipient: [])
end
The essentials of my ConversationUser model:
class ConversationUser < ActiveRecord::Base
attr_accessor :recipient
before_create :acquire_conversation
validates :user_id, :conversation_id, presence: true
def acquire_conversation
unless recipient.blank?
recipient.each do |u|
ConversationUser.create(user_id: u, conversation: conversation)
end
end
end
end
I think the problem is somewhere in my controller's conversation_user_params. But it also might be in the model's before_create method. I've been trying to fix this problem for a day now, with lots of debugging with no success. If anyone can be of assistance, I thank you in advance.
The problem is in the model. before_create callback is called before creating a ConversationUser. Let's name this created ConversationUser as CURRENT. So, before creating the CURRENT ConversationUser you loop through recipient ids and create a ConversationUser for each of them. The ConversationUsers that you are creating here are not CURRENT ConversationUser. CURRENT ConversationUser is saved after the callback is executed (after you create other ConversationUsers). But in this case CURRENT ConversationUser doesn't know wich User it belongs to, because you pass user_id parameter to ConversationUsers that you create in before_create callback, but you do not pass it to CURRENT ConversationUser when it is created (when original create! method is executed).
To solve this problem you can override original create! method or not use it at all for creating ConversationUsers by recipient ids. Add a new method to your Conversation model (for example create_conversation_users):
Solution
In the controller:
def create
#conv = Conversation.create!
#conversation = #conv.create_conversation_users!(conversation_user_params[:recipient])
end
In the model:
class Conversation
def create_conversation_users!(recipient_ids)
return if recipient_ids.blank?
recipient_ids.each do |recipient_id|
conversation_users.create!(user_id: recipient_id, conversation: self)
end
end
end
You should also update ConversationUser model:
class ConversationUser < ActiveRecord::Base
validates :user_id, :conversation_id, presence: true
end
The error is in the ConversationUser. before_create callbacks are ran before a record is created in the database BUT after validations are ran. To solve your issue, there's a few things you can do. One of them was answered by Chumakoff. Here's another option you can use.
Remove all the code inside ConversationUser and change conversation_user_params to
def conversation_user_params
params[:conversation_user][recipient].map do |recipient|
{ user_id: recipient }
end
end
What happens is you're passing an array of { user_id: 1 } to create! which is the same as calling multiple create!({ user_id: 1 }).

How do I conditionally validate a 1-1 model relationship?

I have a model, Person, that requires a schedule if it's type is "scheduled". In it's controller (which inherits from InheritedResources::Base):
def create
super do
#person.schedule = Schedule.create params[:schedule] if #person.scheduled?
end
end
The thing is, I want to validate that all People of type "scheduled" have a schedule. Something like this:
validates :schedule, :presence => true, :if => :scheduled?
in the Person model. But because a schedule belongs_to a Person, it needs the person to be created prior to being created itself (so the person will have an ID). So with this in my controller, the person validation fails, since the schedule needs to be created later.
Is there something in Rails that I don't know about, which will enable me to perform these validations? If I used accepts_nested_attributes_for, will that allow these validations to pass?
Maybe just don't create it beforehand?
#person.schedule = Schedule.new params[:schedule] if #person.scheduled?
So #person and assigned Schedule shall be saved at the same time (transaction).
I think this is the only correct way.
UPDATED (due to super :create conception):
super action
def create(&block)
...
yield #person if block_given?
#person.save # line where #person get saved
end
inherited action
def create
super do |person|
person.schedule = Schedule.new params[:schedule] if person.scheduled?
end
end

Rails 3: How to enforce model's validation to fail when the validation of the associated model fails?

I have the following two models:
class Product < ActiveRecord::Base
belongs_to :shop
validates_numericality_of :price, :greater_than_or_equal_to => 0
end
class Shop < ActiveRecord::Base
has_many :products
validates_presence_of :name
end
Here is the create method of my ProductsController:
def create
if params[:product][:shop_id] == "new_shop"
#shop = Shop.find_by_name(params[:new_shop]) || Shop.create(:name => params[:new_shop]) # Is there a simpler method to do this ?
params[:product][:shop_id] = #shop.id
end
#product = Product.new(params[:product])
if #product.save
redirect_to(:action => 'index')
else
render('new')
end
end
When user adds a new product he has a select box to choose the shop. The last option in this select box lets user to add a new shop (an additional input text field appears). The value of this last option is new_shop.
If the validation of the new entered shop fails, I would like the validation of the product to fail and display an appropriate error (currently an error displayed only if the validation of the product itself fails).
What would be the most "Rails 3 method" to achieve this ?
I think it would be simpler if you use accepts_nested_attributes_for. So to your Product model add:
accepts_nested_attributes_for :shop
And then in view depending on your select list value you can modify form (in js), so there will be either shop_id field or a whole set of fileds for a shop:
<% f.fields_for :shop do |sf| %>
...
<% end %>
Then if user selects existing shop, it will only pass shop_id, but if users selects new shop, then form will pass also new associated object.
If you want shop name to be unique, then just add validates_uniqueness_of to Shop model.
If validation of a shop fails, then product won't be saved. Basicaly, your controller stays as simple as it could be (just creating new product object from params - you don't care about shop there).
I agree with #klew, you should probably be using accepts_nested_attributes_for.
But, the simple and direct answer to your question is to use validates_associated.
Also, the nicer way of doing:
#shop = Shop.find_by_name(params[:new_shop]) || Shop.create(:name => params[:new_shop])
would be:
#shop = Shop.find_or_create_by_name(params[:new_shop])

Resources