Pass associated model id to another form - ruby-on-rails

I have the following association setup
class Donation < ActiveRecord::Base
belongs_to :campaign
end
class Campaign < ActiveRecord::Base
has_many :donations
end
in my donation controller i have campaign_id as a strong parameter. Now when creating a donation i would like to have the campaign_id available to save in that form, but better off in the controller somewhere im guessing as less open to being edited. To get from the campaign to the donation form i click a button
<%= link_to 'Donate', new_donation_path, class: 'big-button green' %>
as we are already using
def show
#campaign = Campaign.find(params[:id])
end
How can i get that id into my donations form?
Thanks

It sounds like you're looking for Nested Resources.
Basically, your routes.rb might look something like:
resources :campaigns do
resources :donations
end
Your controller might look something like:
class DonationsController < ApplicationController
before_action :find_campaign
def new
#donation = #campaign.donations.build
end
def create
#donation = #campaign.donations.create(params[:donation])
# ...
end
protected
def find_campaign
#campaign ||= Campaign.find(params[:campaign_id])
end
end
And your example link, something like...
<%= link_to 'Donate', new_campaign_donation_path(#campaign), class: 'big-button green' %>

Related

Rails: How to redirect to a specific controller (index) depending on condition

I have a Ruby on Rails application that can generate "roles" for actors in movies; the idea is that if a user looks at a movie detail page, they can click "add role", and the same if they look at an actor detail page.
Once the role is generated, I would like to redirect back to where they came from - the movie detail page or the actor detail page... so in the controller's "create" and "update" method, the redirect_to should either be movie_path(id) or actor_path(id). How do I keep the "origin" persistent, i. e. how do I remember whether the user came from movie detail or from actor detail (and the id, respectively)?
I would setup separate nested routes and just use inheritance, mixins and partials to avoid duplication:
resources :movies do
resources :roles, module: :movies, only: :create
end
resources :actors do
resources :roles, module: :actors, only: :create
end
class RolesController < ApplicationController
before_action :set_parent
def create
#role = #parent.roles.create(role_params)
if #role.save
redirect_to #parent
else
render :new
end
end
private
# guesses the name based on the module nesting
# assumes that you are using Rails 6+
# see https://stackoverflow.com/questions/133357/how-do-you-find-the-namespace-module-name-programmatically-in-ruby-on-rails
def parent_class
module_parent.name.singularize.constantize
end
def set_parent
parent_class.find(param_key)
end
def param_key
parent_class.model_name.param_key + "_id"
end
def role_params
params.require(:role)
.permit(:foo, :bar, :baz)
end
end
module Movies
class RolesController < ::RolesController
end
end
module Actors
class RolesController < ::RolesController
end
end
# roles/_form.html.erb
<%= form_with(model: [parent, role]) do |form| %>
# ...
<% end %>

Find by linked fields Rails 5

I have a model that works like this:
class User < ApplicationRecord
has_many :deals
...
class Deal < ApplicationRecord
belongs_to :user
has_many :clients
...
class Clients < ApplicationRecord
belongs_to :deal
If I want to find all the clients listed on a certain deal I can enter Client.find_by(deal_id: x) in the console where x is the deal ID I want, but when I try to list them all on a page I'm doing something wrong.
Here's the relevant resource route
Rails.application.routes.draw do
resources :deals, only: [:show]
end
I think the problem is I'm not doing the find correctly in the controller
class DealsController < ApplicationController
def show
#deal = Deal.find_by_id(params[:id])
#client = Client.find_by(deal_id: params[:id])
end
end
On the page I'm trying to list clients like this:
<div class="client">
<% #client.each do |c| %>
<%= c.name %>
<% end %>
</div>
But the error is undefined method each' for #<Client:0x007f8e4cdecfb0>
I thought it would be pretty simple because the :id that's returned from /deal/:id is shared by both, but I'm stumped.
find_by will return only one object, each is a method defined for arrays.
So if you want clients for a particular deal, you can do
#deal = Deal.find_by_id(params[:id])
#clients = #deal.clients
or
#deal = Deal.find_by_id(params[:id])
#clients = Client.where(deal_id: params[:id])
and in view
<% #clients.each do |c| %>
<%= c.name %>
<% end %>

Only allow users to create one review per movie

I have my app setup where users can write reviews for a movie. What I'd like to do is limit the user to create only one review per movie. I've managed to accomplish this in my reviews controller as so:
class ReviewsController < ApplicationController
before_action :has_reviewed, only [:new]
....
def has_reviewed?
if Review.where(user_id: current_user.id, movie_id: #movie.id).any?
redirect_to movie_reviews_path
flash[:notice] = "You've already written a review for this movie."
end
end
end
Where I'm now having trouble is translating this same logic into my index view template with the helper methods of Devise and CanCanCan at my disposal.
<% if user_signed_in? && ... %> # current_user has already created a review for this movie
<%= link_to "Edit Review", edit_movie_review_path(#movie, review) %>
<% else %>
<%= link_to "Write a Review", new_movie_review_path %>
<% end %>
Also: Is there any way to improve the lookup in my has_reviewed? method? I feel like there's a better way to write it but can't determine the most appropriate fix.
Why not use a validation:
#app/models/review.rb
class Review < ActiveRecord::Base
validates :movie_id, uniqueness: { scope: :user_id, message: "You've reviewed this movie!" }
end
This is considering your review model belongs_to :movie
You could also use an ActiveRecord callback:
#app/models/review.rb
class Review < ActiveRecord::Base
before_create :has_review?
belongs_to :user, inverse_of: :reviews
belongs_to :movie
def has_review?
return if Review.exists?(user: user, movie_id: movie_id)
end
end
#app/models/user.rb
class User < ActiveRecord::Base
has_many :reviews, inverse_of: :user
end
Is there any way to improve the lookup in my has_reviewed? method?
def has_reviewed?
redirect_to album_reviews_path, notice: "You've already written a review for this album." if current_user.reviews.exists?(movie: #movie)
end
Why not make a has_reviewed? method on your User class?
e.g.
def has_reviewed?(reviewable)
# query in here
end
Then you should be able use that just fine in your controller and your views.
You will want to do this for both new and create. Otherwise a savvy user would be able to run a post that would get past your new action.
I would put the link_to in either a helper or a presenter object. It would generally look like this.
def create_or_edit_review_path(movie, current_user)
return '' if current_user.blank?
if current_user.review.present?
#Generate review edit link
else
#generate new link
end
end
After that in all of your views it would just be
<%= create_or_edit_review_path(#movie, current_user) %>
Then in your controller for both new and create you could do either a before action or just redirect on each.
before_action :enforce_single_review, only: [:create, :new]
def enforce_single_review
if current_user.review.present?
redirect_to review_path(current_user.review)
end
end
Here's what I came up with:
I created an instance method to retrieve a user's movie review using the find_by method on the Review model:
class User < ActiveRecord::Base
....
def movie_review(album)
Review.find_by(user_id: self, album_id: album)
end
end
This method also comes in handy when setting up my callback:
class ReviewsController < ApplicationController
before_action :limit_review, only: [:new, :create]
....
private
def limit_review
user_review = current_user.movie_review(#movie)
if user_review.present?
redirect_to edit_movie_review_path(#movie, user_review)
end
end
end
Created a helper method for showing the appropriate link to edit or create a review. Big thanks to Austio and his suggestion:
module ReviewsHelper
def create_or_edit_review_path(movie)
user_review = current_user.movie_review(movie) if user_signed_in?
if user_signed_in? && user_review.present?
link_to "Edit review", edit_movie_review_path(movie, user_review)
else
link_to "Write a review", new_movie_review_path
end
end
end
And at last this is how I call the helper in my view template(s):
....
<%= create_or_edit_review_path(#album) %>

Handling different models in one form submission rails

I have been trying to build an app offering discounted vacation trips such that:
(1) a user(travel agent) can compose a trip by combining hotels (hotel chains) and cities
(2) a user(regular user) can review hotels and cities, s/he has already visited.
(3) another user can evaluate how good the deal is with respect to the country and hotel the travel agent will have him/her stay.
The models look like this
class User < ActiveRecord::Base
has_many :trips
has_many :reviews
end
class Trip < ActiveRecord::Base
belongs_to :user
belongs_to :hotel
belongs_to :city
end
class Hotel < ActiveRecord::Base
belongs_to :city
has_many :reviews, as: :reviewable
end
class City < ActiveRecord::Base
has_many :hotels
has_many :reviews, as: :reviewabel
end
class Review < ActiveRecord::Base
belongs_to :user
belongs_to :reviewable, polymorphic: true
end
The problem is I can figure out how to create the controllers for Hotel and City because they are only created in the context of a makeshift trip. I checked the rails casts on nested forms and the use of accepts_nested_attributes_for but I can't seem to get it right.
Note: the reason why I separated the hotels and the cities is to be able to retrieve the reviews independently. Such that Say I enjoyed my stay at the Four Seasons in Toronto but not the one in NY. - because of the cities/hotels (=> accommodating the case where I didn’t enjoy it because the hotel was crap and the one where I didn’t because the city was)
Note 2: I understand it doesn’t make much sense to seperate hotels and cities in this example - I made a mistake in self-appointing the tutorial. But the problem has been haunting me, what if it was an delivery order instead with entree/meal/dinner instead of hotels and cities, or restaurant chains and neighborhoods.
Any help is appreciated. Thank you
Edit
Edited after Settheline’s comment.
I mean the create actions for cities and hotels only exist in the context of a Trip create action.
Trip has 2 attributes: title & description: It’s only then that I “log” the itinerary. Here’s what my controllers look like to give you a better idea
class TripsController < ApplicationController
before_action :signed_in_user
def show
#trip = Trip.find(params[:id])
end
def index
#trips = current_user.Trip.all
end
def new
#trip = Trip.new
end
def create
# #trip = Trip.new(trip_params)
#trip = current_user.trips.build(trip_params)
if #trip.save
flash[:success] = "Your trip was successfully published!"
redirect_to #trip
else
render 'new'
end
end
def edit
end
def update
if #trip.update_attributes(trip_params)
flash[:success] = "Trip was updated"
redirect_to #trip
else
render 'edit'
end
end
def destroy
Trip.find(params[:id]).destroy
flash[:success] = "trip was deleted. Thank you"
redirect_to #user #root_url
end
private
def trip_params
params.require(:trip).permit(:title, :description)
end
end
class CitiesController < ApplicationController
before_action :signed_in_user
def create
#city = City.new(city_params)
if #city.save
# flash[:success] = ""
else
render 'new'
end
end
# def destroy
# City.find(params[:id]).destroy
# flash[:success] = “City was deleted."
# redirect_to root_url
# end
private
def city_params
params.require(:city).permit(:name, :province, :country)
end
end
class HotelsController < ApplicationController
before_action :signed_in_user
def create
#similar to city
end
def destroy
#similar to city
end
private
def hotel_params
params.require(:hotel).permit(:name, :address,
:management_contact,
:city_id)
end
end
And here’s the problem:
I want to have/add create forms within the trip one in
sample_app/app/views/trips/new.html.erb
<% provide(:title, 'New Trip') %>
<h1>New Trip</h1>
<div class="row">
<div class="span6 offset3">
<%= form_for(#trip) do |f| %>
<%= render 'shared/error_messages', object: f.object %>
<%= f.text_field :title, placeholder: "Type in a title" %>
<%= f.text_field :description, placeholder: "Any additional info." %>
<%= f.submit "Publish", class: "btn btn-large btn-primary" %>
<% end %>
</div>
</div>
accepts_nested_attributes_for allows you to save attributes on associations. Although you do have associations in your models it doesn't necessarily mean that you need to use accepts_nested_attributes_for. It depends on how the code flows through your controllers.
Simple Example
For example, you would probably want to allow your users to view their trips and reviews. First you'll need a method to get the current user:
users_controller.rb
class ApplicationController < ActionController::Base
def current_user
#current_user ||= User.find(session[:user_id])
end
end
This method will be inherited by all of your controllers and allow them to get the current user. (There are many solutions out there for getting the current user, this is definitely not a good solution but it is OK for demonstration purposes).
Trips & Reviews
Now you can create some controllers for the current user to view their trips and reviews:
trips_controller.rb
class TripsController < ApplicationController
def index
#trips = current_user.trips.all
end
end
reviews_controller.rb
class ReviewsController < ApplicationController
def index
#reviews = current_user.reviews.all
end
end
Now you have some controller actions displaying the trips/reviews for the current user. I think this example demonstrates how you can create your controllers and that accepts_nested_attributes_for is not necessarily required.

Submit two forms at once with rails

Basically my idea is very simple - I want to create a new cart for each new user. The form itself is generated with scaffold and we're talking rails 4.0.1 here.
Is there a way to do that and if so - how? Maybe you can link me some live examples?
You do not need multiple forms to create multiple objects in Rails controller. Assuming that you have relationships like this:
class User < ActiveRecord::Base
has_many :carts #or has_one :cart
end
class Cart < ActiveRecord::Base
belongs_to :user
end
Then it's perfectly acceptable to do this:
class UsersController < ApplicationController
def new
#user = User.new
end
def create
#user = User.new user_params
if #user.save
#user.carts.create # or #user.create_cart
redirect_to user_path
else
render action: :new
end
end
private
def user_params
params.require(:user).permit(...)
end
end
If the new user form happens to include some cart-specific details, then use fields_for to make them available in the form:
= form_for :user do |f|
... f.blah for user fields ...
= fields_for :cart do |cart_fld|
... cart_fld.blah for cart fields ...
and add cart_params to your controller.

Resources