rails arguments to after_save observer - ruby-on-rails

I want users to enter a comma-delimited list of logins on the form, to be notified by email when a new comment/post is created. I don't want to store this list in the database so I would use a form_tag_helper 'text_area_tag' instead of a form helper text_field. I have an 'after_save' observer which should send an email when the comment/post is created. As far as I am aware, the after_save event only takes the model object as the argument, so how do I pass this non model backed list of logins to the observer to be passed on to the Mailer method that uses them in the cc list.
thanks

You want to store the list in a virtual attribute. It will be available in the after_save callback.

I think the better way will be to use a tableless model. Look at Railscatsts screencast for an example. It's pretty simple.

Here are the models you would need (along w/ the form) and the virtual attribute in the user model.
# app/models/user.rb
class User < ActiveRecord::Base
# virutal attribute and validations
attr_accessor :unpersisted_info
validates_presence_of :unpersisted_info
end
# app/models/user_observer.rb
class UserObserver < ActiveRecord::Observer
def after_save(user)
# logic here...
end
end
# form for view...
<%form_for #user do |f|%>
<%= f.text_field :unpersisted_info %>
<%= f.submit "Go" %>
<%end%>

Related

Add fields to 'new' view in Rails Admin

I would like to add additional fields to the Rails Admin 'new' view for a specific model object, 'User'. These fields would not be attributes on the model itself but instead just fields that I would like users to be able to submit information with in order to calculate another field.
Is this possible?
Add virtual field to your model in rails admin using,
config.model Address do
list do
# virtual field
configure :full_address do
# any configuration
end
fields :full_address, :street, :number #, ...
end
end
Reference - https://github.com/sferik/rails_admin/wiki/Fields#virtual-fields
I'm not entirely familiar with Rails Admin, but you should be able to get what you want with Rails' virtual attributes mechanism.
In your user.rb model file, you need to add an attr_accessor line, listing the symbols you want to assign to your non-model fields, like this:
class User < ActiveRecord::Base
attr_accessor :virtual_field_one, :virtual_field_two
# Remainder of your code
end
You can add fields to the corresponding view that populate those values:
<%= f.text_field :virtual_field_one %>
Then you can add those attributes to the strong parameters method of your users_controller.rb, like this:
class ActivitiesController < ApplicationController
# other code
def user_params
params.require(:activity).permit(:mode_field_one, :mode_field_two, :virtual_field_one, :virtual_field_two)
end
# other code
end
Now you should be able to access virtual_field_one and virtual_field_two from the params hash like any other field in your User model:
virtual_field_one = params[:virtual_field_one]

How Do I Pass Additional Parameters in Rails from Controller to Model

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)

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 %>

Can't mass-assign protected attributes for creating a has_many nested model with Devise

I've watched the RailsCast, another nested attributes video, lots of SO posts, and fought with this for a while, but I still can't figure it out. I hope it's something tiny.
I have two models, User (created by Devise), and Locker (aka, a product wishlist), and I'm trying to create a Locker for a User when they sign up. My login form has a field for the name of their new Locker (aptly called :name) that I'm trying to assign to the locker that gets created upon new user registration. All I'm ever greeted with is:
WARNING: Can't mass-assign protected attributes: locker
I've tried every combination of accepts_nested_attributes and attr_accesible in both of my models, yet still nothing works. I can see from the logs that it's being processed by the Devise#create method, and I know Devise isn't smart enough to create my models how I want :)
Here's the relevant bits of my two models:
# user.rb
class User < ActiveRecord::Base
attr_accessible :username, :email, :password, :password_confirmation, :remember_me, :locker_attributes
# Associations
has_many :lockers
has_many :lockups, :through => :lockers
# Model nesting access
accepts_nested_attributes_for :lockers
end
and
# locker.rb
class Locker < ActiveRecord::Base
belongs_to :user
has_many :lockups
has_many :products, :through => :lockups
attr_accessible :name, :description
end
# lockers_controller.rb (create)
#locker = current_user.lockers.build(params[:locker])
#locker.save
I'm assuming I need to override Devise's create method to somehow get this to work, but I'm quite new to rails and am getting used to the black box "magic" nature of it all.
If anyone can help me out, I'd be incredibly thankful. Already spent too much time on this as it is :)
EDIT: I realized I omitted something in my problem. My Locker model has three attributes - name, description (not mandatory), and user_id to link it back to the User. My signup form only requires the name, so I'm not looping through all the attributes in my nested form. Could that have something to do with my issue too?
EDIT 2: I also figured out how to override Devise's RegistrationsController#create method, I just don't know what to put there. Devise's whole resource thing doesn't make sense to me, and browsing their source code for the RegistrationsController didn't help me much either.
And for bonus points: When a user submits the login form with invalid data, the Locker field always comes back blank, while the regular Devise fields, username & email, are filled in. Could this also be fixed easily? If so, how?
first, you have a typo :
attr_accessible :locker_attributes
should be plural :
attr_accessible :lockers_attributes
then, the standard way to use nested_attributes is :
<%= form_for #user do |f| %>
<%# fields_for will iterate over all user.lockers and
build fields for each one of them using the block below,
with html name attributes like user[lockers_attributes][0][name].
it will also generate a hidden field user[lockers_attributes][0][id]
if the locker is already persisted, which allows nested_attributes
to know if the locker already exists of if it must create a new one
%>
<% f.fields_for :lockers do |locker_fields| %>
<%= locker_fields.label :name %>
<%= locker_fields.text_field :name %>
<% end %>
<% end %>
and in you controller :
def new
#user = User.new
#user.lockers.build
end
def create
# no need to use build here : params[:user] contains a
# :lockers_attributes key, which has an array of lockers attributes as value ;
# it gets assigned to the user using user.lockers_attributes=,
# a method created by nested_attributes
#user = User.new( params[:user] )
end
as a side note, you can avoid building a new locker for new users in controller in different ways:
create a factory method on User, or override new, or use an after_initialize callback to ensure every new user instantiated gets a locker builded automatically
pass a specific object to fields_for :
<% f.fields_for :lockers, f.object.lockers.new do |new_locker_fields| %>
Someone helped me figure out the solution in a more "Rails 4'y" way with strong attributes & how to override Devise's sign_up_params (to catch all the data coming from my signup form).
def sign_up_params
params.require(:user).permit(:username, :email, :password, :lockers_attributes)
end
Gemfile addition: gem 'strong_parameters'
Commenting out the attr_accessible statement in my user.rb file, since apparently strong parameters eliminate the need for attr_accessible declarations.
# attr_accessible :username, :email, :password, :password_confirmation, :lockers
And the/a correct way of building a Locker before submitting the form: at the beginning of the nested form:
<%= l.input :name, :required => true, label: "Locker name", :placeholder => "Name your first locker" %>
Thanks again for all your help. I know a question like this is difficult to answer without seeing the whole codebase.

Ruby on Rails Validation Question Inside Controller

Let's say I have a form_tag in a view gathering a view user input . With this input that I get through the params hash, I want to verify some of htis information such as email phone name.
Can I just verify in the controller or is this a bad thing? I don't plan to save the thing I get to a db or anything, it's just getting put into an email and sent off, but I want to verify these things before it's sent.
Thank you.
EDIT:
Found this http://www.viddler.com/explore/rails3/videos/6/
class Test
include ActiveRecord::Validations
validates_presence_of :name, :email
attr_accessor :name, :email
end
You can use the model for whatever you need that is related to the object and don't need to save it. Keeping stuff like this in the model is desirable in order to keep controller tidy. Say you have a user model:
#controller
#user.new params[:user]
#user.check_valid
#user.save # is optional
#user.rb
def check_valid
!email.blank? and !phone.blank?
end

Resources