Ruby on Rails - how to handle belongs_to multiple on create - ruby-on-rails

I'm trying to create a Comment that belongs_to multiple Models but I'm not sure on what the best practice is to assign it to both.
class Comment < ApplicationRecord
validates :message, :presence => true
belongs_to :user
belongs_to :discussion
end
When creating a Comment within a Discussion, I run into the error "Discussion must exist".
I'm using Devise so I am using current_user and building a Comment to align it with them, but I'm not sure on exactly how to point it to both. This seems to work, but seems very hacky.
def create
#comment = current_user.comments.build(comment_params)
#comment.discussion_id = params[:discussion_id]
...
end
What's the best practice?

I would add the :discussion_id to the comment_params method:
def create
#comment = current_user.comments.build(comment_params)
...
end
private
def comment_params
params.require(:comment)
.permit(...)
.merge(discussion_id: params.require(:discussion_id))
end
Or you could use build with a block:
def create
#comment = current_user.comments.build(comment_params) { |comment|
comment.discussion_id = params[:discussion_id]
}
# ...
end

No matter how you go about it, you do need to either set discussion_id or get it to be present in comment_params.
Does this look less hacky to you?
def create
comment_params.merge(discussion_id: params[:discussion_id])
#comment = current_user.comments.build(comment_params)
...
end

You can pass additional params in the block
params = ActionController::Parameters.new({
comment: {
name: 'Francesco',
body: 'Text'
}
})
comment_params = params.require(:comment).permit(:name, :body)
def create
#comment = current_user.comments.build(comment_params) do |comment|
comment.discussion_id = params[:discussion_id]
end
end

There are several things you need to keep in mind when you create an entity which is associated to more than one entity.
With respect to your question regarding to best practices. I would like to emphasise the usage of an interactor here:
# xx_controller.rb
def create
success = CreateComment.call(params)
if success
....
else
....
end
end
# ../../create_comment.rb
# pseudocode
class CreateComment
def call(params)
validate your params # check if nothing is missing, the discussion exists and you have a user
build_and_save_your_comment
return_your_result_object # return a tuple or a result or whatever you need in order to handle errors and success cases...
end
end
With this approach you keep the readability within your controller and you can focus on what matters to the comment creation in a dedicated place.

Related

Dynamic Strong params for require - Rails

I have an update method right now that will not work for all situations. It is hard coded in the strong params like this params.require(:integration_webhook).permit(:filters) that all fine right now but sometimes it may be integration_webhook and other times it needs to be integration_slack. Basically, is there a way that I don't need to hardcode the require in the strong params? I'll show my code for clarity.
Update Method:
def update
#integration = current_account.integrations.find(params[:id])
attrs = params.require(:integration_webhook).permit(:filters)
if #integration.update_attributes(attrs)
flash[:success] = "Filters added"
redirect_to account_integrations_path
else
render :filters
end
end
As you can see it's a standard update method. But I need the integration_webhook params to be dynamic. I'm wondering if there is a model method I could call to strip away the integration_webhook part?
Not totally sure how dynamic this needs to be, but assuming that we are either getting an integratino_webhook or a integration_slack.
def update
#integration = current_account.integrations.find(params[:id])
if #integration.update_attributes(update_params)
# ...
else
# ...
end
end
private
def update_params
params.require(:integration_webhook).permit(:filters) if params.has_key?(:integration_webhook)
params.require(:integration_slack).permit(:filters) if params.has_key?(:integration_slack)
end
Checkout Strong parameters require multiple if this didn't answer your question.
Edit
For more dynamic requiring:
def update_params
[:integration_webhook, :integration_slack].each do |model|
return params.require(model).permit(:filters) if params.has_key?(model)
end
end
Off the top of my head something like this should work. The naming convention isn't the best but it the structure will allow you to just add to the list if you need to.
def update
#integration = current_account.integrations.find(params[:id])
if #integration.update_attributes(webhook_and_slack_params)
flash[:success] = "Filters added"
redirect_to account_integrations_path
else
render :filters
end
end
def webhook_and_slack_params
[:integration_webhook, :integration_slack].each do |the_params|
if(params.has_key?(the_params))
params.require(the_params).permit(:filters)
end
end

Insert a record with "find_or_create_by" inside another controller fails

This is my code:
class ApplicationsController < ApplicationController
def new
#application = Application.new
end
def create
#application = Application.new(application_params)
#layout = Layout.find_or_create_by(application_id: #application.id)
if #application.save
redirect_to #application
else
render 'new'
end
end
layout belongs_to :application
When I check the Layouts table it is empty. Can you help me, please?
Your model contains the following validations:
validates :adv_path, presence: true
validates :start_time, presence: true
validates :end_time, presence: true
Therefore you are not able to create a Layout without this values. You must do something like this (with useful values):
Layout.find_or_create_by(id: #application.id) do |layout|
layout.adv_path = 'A useful default'
layout.start_time = 1.second.ago
layout.end_time = 100.year.from_now
end
Or rethink the need for the validators.
In your layout creation line, #application doesn't have an id yet. Resultantly, you pass 'nil' to the #layout's application_id which makes its validation fail. (You mentioned the layout's application presence validation in a comment).
So create the layout after #application is saved and you should be good to go.
if #application.save
#layout = Layout.create(application_id: #application.id)
When you use new method like
#application = Application.new(application_params)
it does not persist that record to db, other words it does not have id. you should use method create instead
#application = Application.create(application_params)
then #application will be persisted to db, and when you say find_or_create_by it will find with id, and not search for id nil
def create
#application = Application.new(application_params)
#layout = Layout.find_or_create_by(application_id: #application.id) # <== that line
if ...
....
end
end
That line is rather misleading. #application.id is nil. So the first time, you'll create a layout record with a application_id as nil. The next time, it'll find the record with application_id: nil and use that. So it'll just create one single record and forever use it.
If you are creating a layout every time you create an application, consider doing it this way:
def create
#application = Application.new(application_params)
if #application.save
#layout = #application.layouts.create( ... ) # assuming application has_many :layouts
redirect_to #application
else
render 'new'
end
end

ActiveRecord::RecordNotFound - in a descendant class' associated_controller#index

I am attempting to locate a parent object in a nested controller, so that I can associate the descendant resource with the parent like so:
# teams_controller.rb <snippet only>
def index
#university = Univeresity.find(params[:university_id])
#teams = #university.teams
end
When I call find(params[:university_id]) per the snippet above & in line 6 of teams_controller.rb, I receive ActiveRecord::RecordNotFound - Couldn't find University without an ID.
I'm not only interested in fixing this issue, but would also enjoy a better understanding of finding objects without having to enter a University.find(1) value, since I grant Admin the privilege of adding universities.
The Rails Guides say the following about the two kinds of parameters in a website:
3 Parameters
You will probably want to access data sent in by the user or other
parameters in your controller actions. There are two kinds of
parameters possible in a web application. The first are parameters
that are sent as part of the URL, called query string parameters. The
query string is everything after “?” in the URL. The second type of
parameter is usually referred to as POST data. This information
usually comes from an HTML form which has been filled in by the user.
It’s called POST data because it can only be sent as part of an HTTP
POST request. Rails does not make any distinction between query string
parameters and POST parameters, and both are available in the params
hash in your controller:
It continues a little further down, explaining that the params hash is an instance of HashWithIndifferentAccess, which allows usage of both symbols and strings interchangeably for the keys.
From what I read above, my understanding is that Rails recognizes both parameters (URL & POST) and stores them in the same hash (params).
Can I pass the params hash into a find method in any controller action, or just the create/update actions? I'd also be interested in finding a readable/viewable resource to understand the update_attributes method thats called in a controller's 'update' action.
Please overlook the commented out code, as I am actively searching for answers as well.
Thanks in advance.
Here are the associated files and server log.
Webrick
teams_controller.rb
class TeamsController < ApplicationController
# before_filter :get_university
# before_filter :get_team
def index
#university = University.find(params[:univeristy_id])
#teams = #university.teams
end
def new
#university = University.find(params[:university_id])
#team = #university.teams.build
end
def create
#university = University.find(params[:university_id])
#team = #university.teams.build(params[:team])
if #team.save
redirect_to [#university, #team], success: 'Team created!'
else
render :new, error: 'There was an error processing your team'
end
end
def show
#university = University.find(params[:university_id])
#team = #university.teams.find(params[:id])
end
def edit
#university = University.find(params[:university_id])
#team = #university.teams.find(params[:id])
end
def update
#university = University.find(params[:university_id])
#team = #university.teams.find(params[:id])
if #team.update_attributes(params[:team])
redirect_to([#university, #team], success: 'Team successfully updated')
else
render(:edit, error: 'There was an error updating your team')
end
end
def destroy
#university = University.find(params[:university_id])
#team = #university.teams.find(params[:id])
#team.destroy
redirect_to university_teams_path(#university)
end
private
def get_university
#university = University.find(params[:university_id]) # can't find object without id
end
def get_team
#team = #university.teams.find(params[:id])
end
end
team.rb
class Team < ActiveRecord::Base
attr_accessible :name, :sport_type, :university_id
has_many :home_events, foreign_key: :home_team_id, class_name: 'Event'
has_many :away_events, foreign_key: :away_team_id, class_name: 'Event'
has_many :medias, as: :mediable
belongs_to :university
validates_presence_of :name, :sport_type
# scope :by_university, ->(university_id) { where(team_id: team_id).order(name: name) }
# scope :find_team, -> { Team.find_by id: id }
# scope :by_sport_type, ->(sport_type) { Team.where(sport_type: sport_type) }
# scope :with_university, joins: :teams
# def self.by_university(university_id)
# University.where(id: 1)
# University.joins(:teams).where(teams: { name: name })
# end
def self.by_university
University.where(university_id: university_id).first
end
def self.university_join
University.joins(:teams)
end
def self.by_sport_type(sport_type)
where(sport_type: sport_type)
end
def self.baseball
by_sport_type('Baseball/Softball')
end
end
university.rb
class University < ActiveRecord::Base
attr_accessible :address, :city, :name, :state, :url, :zip
has_many :teams, dependent: :destroy
validates :zip, presence: true, format: { with: /\A\d{5}(-\d+)?\z/ },
length: { minimum: 5 }
validates_presence_of :name, :address, :city, :state, :url
scope :universities, -> { University.order(name: 'ASC') }
# scope :by_teams, ->(university_id) { Team.find_by_university_id(university_id) }
# scope :team_by_university, ->(team_id) { where(team_id: team_id).order(name: name)}
def sport_type
team.sport_type
end
end
views/teams/index.html.erb
Placed in gists for formatting reasons
rake routes output: (in a public gist)
enter link description here
rails console
You're not going to want to have both:
resources :universities #lose this one
resources :universities do
resources :teams
end
As for params... you have to give a param. So, when you go to http://localhost:3000/teams there are no params, by default. If you go to http://localhost:3000/teams/3 then params[:id] = 3 and this will pull up your third team.
Keep in mind the nomenclature of an index. The index action of Teams, is going to list all of the teams. All of them. There is no one University there, so what are you actually trying to find? If anything, you'd have, for your University controller:
def show
#university = University.find(params[:id])
#teams = #university.teams
end
so, the address bar will be showing http://localhost:3000/universities/23, right? params[:id] = 23, then you can find the teams associated with that university.

How to remove API logic from view in Rails Way?

I would like to remove this logic:
Suitcase::Hotel.find(id: hotel.id).images.first.url
from view.
https://gist.github.com/2719479
I dont have model Hotel. I get this url via API using Suitcase gem.
Problem is because
hotel is from #hotels = Suitcase::Hotel.find(location: "%#{headed}%") and API recevie me images only if do Suitcase::Hotel.find(id: hotel.id)
If Suitcase::Hotel.find(id: hotel.id).images.first.url works then i would guess hotel.images.first.url will work too if hotel is an hotel instance.
Is adding:
#hotel = Suitcase::Hotel.find(id: hotel.id)
to #show action doesn't work?
EDIT:
In that case make an helper:
def hotel_image_url(hotel)
Suitcase::Hotel.find(id: hotel.id).images.first.url
end
But as I can see here you can simply write in controller:
#hotels_data = Suitcase::Hotel.find(ids: #hotels.map(&:id))
Or to be more elegant add to your model (or create decorator (it's better option)):
def photo
Suitcase::Hotel.find(id: self.id).images.first.url
end
I think this should work, not sure about the second option
class Search < ActiveRecord::Base
attr_accessible :headed, :children, :localization, :arriving_date, :leaving_date, :rooms, :adults
def hotels
#hotels ||= find_hotels
end
private
def find_hotels
return unless headed.present?
#hotels = Suitcase::Hotel.find(location: "%#{headed}%")
#hotels.each do |hotel|
def hotel.image_url
Suitcase::Hotel.find(id: hotel.id).images.first.url
end
end
end
end
# or this, but I'm not sure if this works
#hotels.each do |hotel|
image_url = Suitcase::Hotel.find(id: hotel.id).images.first.url
def hotel.image_url
image_url
end
end

Best practice for creating a model with two belongs_to associations?

This is a consistency problem that I'm running into often.
Let's consider a typical Forum:
User can create Posts
Posts belong to a Topic
Posts also belong to the User that created them
What's the best practice for choosing between these two options:
# Initialize #post on the User
def create
#post = current_user.posts.build(params[:post])
#post.topic_id = #topic.id
if #post.save
...
end
end
Or
# Initialize #post on the Topic
def create
#post = #topic.posts.build(params[:post])
#post.user_id = current_user.id
if #post.save
...
end
end
Or is there a better way, considering that, in the above examples, either #post's user_id or topic_id would have to be added to attr_accesssible (feels hacky)?
The cleanest approach I managed to find is using CanCan: when having a rule can :create, Post, :user_id => user.id and adding load_resource in your controller it will set the attributes.
But it is not always suitable. It would be nice to have some generic solution to initalize nested objects in one shot.
Update. I've come up with another option:
#post = #topic.posts.where(user_id: current_user.id).build(params[:post])
Generally speaking, all of these approaches break the Law of Demeter. It would be better to encapsulate in a method of the model, like this:
class Topic < ActiveRecord::Base
def new_post(params={}, author=nil)
posts.build(params).tap {|p| p.user = author}
end
end
Then in controller:
#post = #topic.new_post(params[:post], current_user)
You never need to monkey with IDs or attr_accessible. If a User has_many Posts and a Topic has_many Posts than you can do
# Initialize #post on the User
def create
#post = current_user.posts.build(params[:post])
#post.topic = #topic #assuming you've gotten the topic from somewhere
if #post.save
...
end
end
There really isn't a big difference in building from the user or from the topic, but going from the user seems more natural to me.
I prefer
#post = #topic.posts.build(params[:post])
#post.user = current_user
Although I dont see any problem with the other approach, building post via topic make more natural to me(as posts are mostly displayed in the context of its topic rather than the user itself).

Resources