Ruby on rails: array at initialize - ruby-on-rails

I'm working with rails 5.1.2 and ruby 2.2.6
I have the following classes:
class Idea < ApplicationRecord
validates :title, presence: true
validates :description, presence: false
has_many :discussions
end
class Discussion < ApplicationRecord
validates :title, presence: true
belongs_to :idea
def initialize(title)
#title = title
end
end
At idea creation, I'd like to add a default discussion in the attribute discussions. As I'm a newbie at ruby and rails, I don't know which is the best approach to do this. Here is what I tried unsuccessfully.
In the idea controller, I tried to create the default discussion at the idea creation, as follows:
class IdeasController < ApplicationController
def create
discussion = Discussion.new "Main thread"
#idea = Idea.new(idea_params)
#idea.discussions << discussion
#idea.save
redirect_to ideas_path
end
private
def idea_params
params.require(:idea).permit(:title)
end
end
This drives me to an error in the controller:
undefined method `<<' for nil:NilClass
on the line
#idea.discussions << discussion
I think this is due to an uninitialized discussions array in my idea. However, the guide states that any class that has the declaration has_many would inherit the method <<, as stated in this guide. But maybe this is only true after the idea has been saved at least one time?
I tried manually initialize the array in my controller.
#idea.discussions = []
This helps removing the error, but I'm surprised this is not done automatically. Furthermore, the discussion is not saved in database. I tried adding the declaration autosave in Idea class, with no effect:
has_many :discussions, autosave: true
I'm a little bit lost. At the end, I'd just like to add a discussion in an idea between its creation and save, and persist it. What is the best approach?
Thanks for any help.

Discussion is already an ActiveRecord object, so you don't need the initialize method. Simply calling Discussion.new should work out of the box.
To build a default Discussion when creating an Idea just do this:
#idea.build_discussion . This is will instantiate a new Discussion association on your Idea model. When you save Idea, it will automatically save the Discussion object as well and automatically associate it to that Idea.
Edit: To simplify the answer, here's the code:
def create
#idea = Idea.new
#idea.build_discussion(title: 'Main Thread')
if #idea.save
redirect_to ideas_path
else
redirect_to :new
end
end
Edit 2: And because you build the Discussion through Idea, you need to add this to your IdeaController strong_params:
def idea_params
params.require(:idea).permit(
...
discussion_attributes: [
:id,
:title,
..
]
)
end
Edit 3: Sorry, I didn't pay attention to your association type. Update to this:
def create
#idea = Idea.new
#idea.discussions.new(title: 'Main Thread')
if #idea.save
redirect_to ideas_path
else
redirect_to :new
end
end

First off, don't override initialize in an ActiveRecord model unless you know what you're doing. Your object already has an initialize method defined, you just can't see it because it's inherited. If you override without accepting the right set of parameters and calling super you will introduce bugs.
ActiveRecord gives you an easy hash syntax for setting attributes at initialize already. You can do Discussion.new(title: 'Title') right out of the box.
If you always want your ideas to be created with a default discussion you can move this down to the model in a before_create callback.
class Idea < ApplicationRecord
validates :title, presence: true
validates :description, presence: false
has_many :discussions
before_create :build_default_discussion
private
def build_default_discussion
discussions.build(title: 'Main Thread')
end
end
Here you're calling the private method build_default_discussion before every new idea is persisted. This will happen automatically when you create a new Idea either with Idea.new.save or Idea.create or any other proxy method that creates a new Idea, anywhere in your application.

Related

Rails nested form - refactor create action | cocoon gem

Everything is working fine but I want to change the code in create action to something like in update action. Right now, in the create action I am looping through all the values and saving them, but want to do it in a single line.
I have a College model.
class College< ActiveRecord::Base
has_many :staffs, dependent: :destroy
accepts_nested_attributes_for :staffs, reject_if: :all_blank, allow_destroy: true
end
And this is my Staff.rb
class Staff < ActiveRecord::Base
belongs_to :college
end
And these are my Staffs controller create and update actions
def create
#college= College.find(params[:college][:id_college_profile]
)
staff_params = params[:college][:staffs_attributes].values
staff_params.each do |staff_param|
#staff = #college.staffs.new
#staff.name = staff_param[:name]
#staff.designation = staff_param[:designation]
#staff.experience = staff_param[:experience]
#staff.specialization = staff_param[:specialization]
#staff.save
end
redirect_to dashboard_path(id: #college.id), notice: "Successfully added Staff."
end
def update
#college= College.find(params[:college][:id_college]
)
#college.update_attributes(staff_parameters)
redirect_to root_path
end
These are strong parameters
def staff_parameters
params.require(:college).permit(:id, staffs_attributes: [:id, :name, :specialization, :experience, :designation, :_destroy])
end
Is there a way to save all of staffs in create action, without looping through all the values, but save all of them with a single line of code as in update action?
I have tried this in the StaffsController create action
def create
#college= College.find(params[:college][:id_college]
)
#staff= #college.staffs.new(staff_parameters)
#staff.save
redirect_to dashboard_path(id: #college.id), notice: "Successfully added Staffs."
end
But it threw this error
unknown attribute 'staffs_attributes' for Staff.
Can someone kindly help me with this issue?
This is a CollegesController so I am assuming the create action also creates the new college?
So in that case your create action should simply be something like:
def create
#college = College.new(staff_parameters)
if #college.save
# succesfully created
else
# there was a validation error
end
end
Note that in general we would use college_parameters because the root element is college and that you not only edit the nested staff, but also possibly attributes from college.
If the college always already exists (because you are doing a find), it is a bit confusing to me what the difference is between create and update and why not always render the edit action in that case?
I have a demo-project show-casing cocoon and nested attributes.
You can do this many ways. The "staff_parameters" method threw an error because you are calling it on class Staff in the create action and on the college class for the update action. Simplest thing to do what you want is to copy the staff parameters method strong parameters and duplicate it. Name this second method create_staff and change the "params.require(:college)" part to "params.require(:staff)" and leave the rest the same. Then in your create action you can do "college.staff(create_staff)". Im on my phone so the formatting isnt good lol i put the code in quotes.

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 }).

Creating 2 objects with has_one association in Rails

I need to get some info about creating new objects in Rails with validation. For example, there is the following code:
def create
#user = User.new(params[:user])
if #user.save
# some actions: redirect, render, etc
else
render 'new'
end
end
But if there is 2 models with has_one association, for example Club and Place. I need to create both this objects from params in the same 'create' action, because I've got the same form for inputing data for it(params[:club] and params[:club][:place]). I don't know how I should save this objects, because for building a place (#club.build_place(params[:club][:place])) I should save #club in database. Please, give me example of the code for my problem. Thanks in advance.
If you're creating multiple objects from a single form you'd probably be best off putting this logic into a "Form Object"... See the article "7 Patterns to Refactor Fat ActiveRecord Models" from the CodeClimate blog found here (look for Section #3 on extracting Form Objects): http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models.
Railscasts also has a good episode on form objects, though it is a "Pro Episode" (i.e. requires subscription). http://railscasts.com/episodes/416-form-objects
In short, you create a custom model including some of the necessary ActiveModel modules then create a custom save method, e.g. (this is directly from the article which has a lot of great advice).
class Signup
include Virtus
extend ActiveModel::Naming
include ActiveModel::Conversion
include ActiveModel::Validations
attr_reader :user
attr_reader :company
attribute :name, String
attribute :company_name, String
attribute :email, String
validates :email, presence: true
# … more validations …
# Forms are never themselves persisted
def persisted?
false
end
def save
if valid?
persist!
true
else
false
end
end
private
def persist!
#company = Company.create!(name: company_name)
#user = #company.users.create!(name: name, email: email)
end
end
This gives you much more control and a much cleaner interface.

What is the proper way to access virtual atributes in Ruby on Rails?

I have a model w/ a virtual attribute:
class Campaign < ActiveRecord::Base
def status
if deactivated
return "paused"
else
return "live"
end
end
end
now, in my view, when I access the attribute with campaign.status, I am getting the proper result. However, when I try to access it like this campaign[:status], I get nothing back.
Why is that?
[:status] uses the [] method in Ruby. 'def status' defines a method which shouldn't be mistaken with an ActiveRecord attribute or an virtual attribute (e.g. attr_reader or attr_accessor).
ActiveRecord adds the [] method to your class and makes all the (database) attributes accessible by object[:attr_name] AND object.attr_name(And even object.attributes[:attr_name]).
This is different from how f.e. Javascript works where obj[:method] is virtually the same as obj.method.
Edit: You should be able to use the attr_accessor if you use them for example in any form:
<%= form.input :status %>
Submitting the form will then set the instance variable #status. If you want to do anything with this before or after saving you can call an before_save or after_save hook:
class Campaign < ActiveRecord::Base
attr_accessible :status
attr_accessor :status
before_save :raise_status
def raise_status
raise #status
end
end
This will throw an error with the value submitted value for status.
Hope this helps.

Resources