Add Record to Existing Collection in a Rails App - ruby-on-rails

I am attempting to develop a model in which a user can add the recipe they are viewing to an existing menu of recipes they have created, similar to adding a song to a custom playlist. I believe I have the models set up correctly (using a many to many through relationship) however I am unsure how to go about the adding of the actual records to a selected collection. Any guidance would be helpful. My code is as below.
Menus Controller
class MenusController < ApplicationController
before_action :set_search
def show
#menu = Menu.find(params[:id])
end
def new
#menu = Menu.new
end
def edit
#menu = Menu.find(params[:id])
end
def create
#menu = current_user.menus.new(menu_params)
if #menu.save
redirect_to #menu
else
render 'new'
end
end
def update
#menu = Menu.find(params[:id])
if #menu.update(menu_params)
redirect_to #menu
else
render 'edit'
end
end
def destroy
#menu = Menu.find(params[:id])
#menu.destroy
redirect_to recipes_path
end
private
def menu_params
params.require(:menu).permit(:title)
end
end
Menu Model
class Menu < ApplicationRecord
belongs_to :user
has_many :menu_recipes
has_many :recipes, through: :menu_recipes
end
menu_recipe Model
class MenuRecipe < ApplicationRecord
belongs_to :menu
belongs_to :recipe
end
Recipe Model
class Recipe < ApplicationRecord
belongs_to :user
has_one_attached :cover
has_many :menu_recipes
has_many :menus, through: :menu_recipes
end
User Model
class User < ApplicationRecord
has_secure_password
has_one_attached :profile_image
has_many :recipes
has_many :menus
end

You can do something like :
def add_recipe_to_menu
menu = current_user.menus.find params[:id]
recipe = current_user.recipes.find params[:recipe_id]
menu.recipes << recipe
end
It will add a viewing recipe to existing menu of recipes.

First make sure you build the new record off the user:
class MenusController < ApplicationController
# make sure you authenticate the user first
before_action :authenticate_user!, except: [:show, :index]
def new
#menu = current_user.menus.new
end
def create
#menu = current_user.menus.new(menu_attributes)
# ...
end
end
Then we can just add a select to the form where the user can select from his recipes:
# use form_with in Rails 5.1+
<%= form_for(#menu) do |f| %>
... other fields
<div class="field">
<%= f.label :recipe_ids %>
<%= f.collection_select :recipe_ids, f.object.user.recipies, :id, :name, multiple: true %>
</div>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
f.object accesses the model instance wrapped by the form builder.
recipe_ids is a special setter/getter created by ActiveRecord for has_many associations. As you may have guesses it returns an array of ids and lets the association be set with an array of ids - automatically inserting/deleting rows in the join table in the process.
You then just need to whitelist the recipe_ids param:
def menu_attributes
params.require(:menu)
.permit(:foo, :bar, recipe_ids: [])
end
recipe_ids: [] whitelists an array of permitted scalar types. Since this is a hash option it must be listed after any positional arguments to be syntactically valid.
rb(main):003:0> params.require(:menu).permit(:foo, recipe_ids: [], :bar)
SyntaxError: (irb):3: syntax error, unexpected ')', expecting =>

Related

Ruby on Rails failed to add a new data

I'm making simple CRUD and the current goal is to add data. However, I found that I can't add any data, and the terminal log also shows "[Webpacker] Everything's up-to-date. Nothing to do", which means there is no error message.
According to my design in the controller, the new data must have failed, so I stopped at new.html.erb. I'm guessing it has something to do with the model's relationship.
This is model User
class User < ApplicationRecord
has_many :reviews
has_many :recipes, through: :reviews
end
This is the model Recipe
class Recipe < ApplicationRecord
has_many :reviews
belongs_to :user
end
This is model Review
class Review < ApplicationRecord
belongs_to :user
belongs_to :recipe
end
This is the RecipeController
class RecipesController < ApplicationController
def index
#recipes = Recipe.all
end
def new
#recipe = Recipe.new
end
def create
#recipe = Recipe.new(recipe_params)
if #recipe.save
redirect_to recipes_path, notice: "Successful!"
else
render :new
end
end
private
def recipe_params
params.require(:recipe).permit(:title, :money)
end
end
this is the web page
<h1>Add New One</h1>
<%= form_for(#recipe) do |r| %>
<%= r.label :title, "Title" %>
<%= r.text_field :title%>
<%= r.label :money, "Budget" %>
<%= r.text_field :money %>
<%= r.submit %>
<% end %>
<%= link_to "Back to list", recipes_path %>
You should first add a callback to ensure that only signed in users can create recipes (unless you actually want to let anomynous users create/update/delete recipies).
For example with Devise you would use its authenticate_user! helper which will bail and redirect to the sign in path if the user is not authenticated:
class RecipesController < ApplicationController
before_action :authenticate_user!, only: [:new, :create]
# ...
end
If you're reinventing the authentication wheel you should create a similiar method which is used to prevent access.
You would then initialize the resource off the current user:
class RecipesController < ApplicationController
before_action :authenticate_user!, except: [:show, :index]
def create
#recipe = current_user.recipes.new(recipe_params)
if #recipe.save
redirect_to recipes_path, notice: "Successful!"
else
render :new
end
end
end
Here I am assuming that you have a current_user method which will retrieve the user based on an id stored the session.
Since you have an indirect assocation this will create a row in the reviews table with the users id and the recipe id as the record in the recipies table.
You also want to display the validation errors in the form so that the user gets feedback.
You are probably right that it is a validation error related to the belongs_to relationship. You should display validation errors for your form as described here https://guides.rubyonrails.org/getting_started.html#validations-and-displaying-error-messages

How a restrict users from creating multiple object on a particular resource

I want to restrict users from having more than 1 job Application for a particular job.
Here are my routes:
resources :jobs do
resources :job_applications
end
job.rb
class Job < ApplicationRecord
has_many :job_applications
validate :limit_user_to_one_job_application
private
def limit_user_to_one_job_application
if user.job_applicaitons.count > 1
self.errors.add(:base, "You can only have one job application")
end
end
end
job_application.rb
class JobApplication < ApplicationRecord
belongs_to :user
belongs_to :job
end
user.rb
class User < ApplicationRecord
has_one :job_application
end
job_application_controller.rb
class JobApplicationsController < ApplicationController
before_action :authenticate_user!
def new
#job = Job.find(params[:job_id])
#job_application = #job.job_applications.create
end
def create
#job = Job.find(params[:job_id])
#job_application =
#job.job_applications.create(job_application_params)
#job_application.user_id = current_user.id
#job_application.save
end
in my new.html.erb file to create new job application
<%= form_with(model: [ #job, #job.job_applications.build ],local: true) do |f| %>
<%= job_application attributes %> <br>
<%= job_application attributes %> <br>
..........
<%= f.submit %>
The above code works, the user is enable to create many job applications for a particular job but only the first job in saved in his database. So ,I want the user to be restricted to create only one job application for a particular job
As per the description mentioned in the post you can manage with uniqueness validation in join model.
class JobApplication < ApplicationRecord
belongs_to :user
belongs_to :job
validates_uniqueness_of :user_id, scope: :job_id
end
This validation will prevent creation of duplicate record in the join model based on the pair for user_id and job_id.
Hope this helps.
You can do this on the controller or the model.
before_action :find_job, except: [:index, :new, :create]
before_action :limit_user_to_one_job_application, only: [:new, :create]
...
private
def find_job
#job = Job.find(params[:job_id])
end
def limit_user_to_one_job_application
if #job.job_applications.where(:user => current_user).count == 1
return redirect_to jobs_path, :notice => "You can only have one job application"
end
end
You'll find that it may be better to make #job.job_applications.where(:user => current_user).count == 1 a method on User, but for simplicity sake I was explicit in its intent.
Or the model (something like):
class JobApplication < ApplicationRecord
validate :limit_user_to_one_job_application
private
def limit_user_to_one_job_application
if user.job_applicaitons.count > 1
self.errors.add(:base, "You can only have one job application")
end
end
end

Update form not working at all in many-to-many relationship

In my app there is a many-to-many relationship between recipe and ingredient, everything is working fine but update.
When I update a recipe, i can update any value associated to recipe table in my database but ingredients are not modified
Here is the recipe model
class Recipe < ActiveRecord::Base
after_create :save_implementos
after_create :save_ingredientes
has_many :HasImplemento, dependent: :destroy
has_many :HasIngrediente, dependent: :destroy
has_many :ingredientes, through: :HasIngrediente
has_many :implementos, through: :HasImplemento
#CUSTOM SETTER#
def ingredientes=(value)
#ingredientes = value
end
def implementos=(value)
#implementos = value
#raise #implementos.to_yaml
end
private
#Guarda los implemenos de una receta
def save_implementos
#raise self.id.to_yaml
#implementos.each do |implemento_id|
HasImplemento.create(implemento_id: implemento_id, recipe_id: self.id)
end
end
def save_ingredientes
#raise #ingredientes.to_yaml
#ingredientes.each do |ingrediente_id|
HasIngrediente.create(ingrediente_id: ingrediente_id, recipe_id: self.id)
end
end
Here is the ingredient model
class Ingrediente < ActiveRecord::Base
has_many :has_ingredientes
has_many :recipes, through: :HasIngrediente
end
and Here is the join table
class HasIngrediente < ActiveRecord::Base
belongs_to :recipe
belongs_to :ingrediente
end
I think you just forgot the accepts_nested_attributes_for in your Recipe model:
accepts_nested_attributes_for :ingredients
Your models have a lot of non-conventional code, which is likely the reason for them not working as required:
Use snake_case to define your associations (not CamelCase)
If you're saving nested/associated data, use accepts_nested_attributes_for
Only use callbacks to deal with the model directly; not to deal with other models
#app/models/recipe.rb
class Recipe < ActiveRecord::Base
has_and_belongs_to_many :ingredientes
has_and_belongs_to_many :implementos
accepts_nested_attributes_for :ingredientes, :implementos #-> if you wanted to create new ones
end
Because you're only creating join table records with your callbacks, you can get away with using << or populating the collection_singular_ids value (if you're using existing records):
#app/controllers/recipes_controller.rb
class RecipesController < ApplicationController
def new
#recipe = Recipe.new recipe_params
#recipe.save
end
private
def recipe_params
params.require(:recipe).permit(:implementos_ids, :ingredientes_ids)
end
end
This will allow you to use:
#app/views/recipes/new.html.erb
<%= form_for #recipe do |f| %>
<%= f.collection_select :implementos_ids, Implementos.all, :id, :name %>
<%= f.collection_select :ingredientes_ids, Ingrediente.all, :id, :name %>
<%= f.submit %>
<% end %>
-
I recommend has_and_belongs_to_many because it does not appear that you're populating your join table with any more data than the two model references.
The difference between has_many :through and has_and_belongs_to_many is has_many :through gives you the ability to store extra data in the join table. If you don't need this, has_and_belongs_to_many is far simpler to maintain.
Thus, to answer your question directly, to update your recipe, you can use the following (with my updated code):
#app/views/recipes/edit.html.erb
<%= form_for #recipe do |f| %>
<%= f.collection_select :implementos_ids, Implementos.all, :id, :name %>
<%= f.collection_select :ingredientes_ids, Ingrediente.all, :id, :name %>
<%= f.submit %>
<% end %>
#app/controllers/recipes_controller.rb
class RecipesController < ApplicationController
def edit
#recipe = Recipe.find params[:id]
end
def update
#recipe = Recipe.find params[:id]
#recipe.update recipe_params
end
private
def recipe_params
params.require(:recipe).permit(:implementos_ids, :ingredientes_ids)
end
end
This will set the implementos_ids & ingredientes_ids values for your recipe, which will update the associations automatically.

Rails 4: First argument in form cannot contain nil or be empty

In our Rails 4 app, there are four models:
class User < ActiveRecord::Base
has_many :administrations, dependent: :destroy
has_many :calendars, through: :administrations
end
class Administration < ActiveRecord::Base
belongs_to :user
belongs_to :calendar
end
class Calendar < ActiveRecord::Base
has_many :administrations, dependent: :destroy
has_many :users, through: :administrations
end
class Post < ActiveRecord::Base
belongs_to :calendar
end
We have routed the corresponding resources with Routing Concerns, as follows:
concern :administratable do
resources :administrations
end
resources :users, concerns: :administratable
resources :calendars, concerns: :administratable
To create a new #calendar, we use the following form:
<h2>Create a new calendar</h2>
<%= form_for(#calendar) do |f| %>
<%= render 'shared/error_messages', object: f.object %>
<div class="field">
<%= f.text_field :name, placeholder: "Your new calendar name" %>
</div>
<%= f.submit "Create", class: "btn btn-primary" %>
<% end %>
When we embed this form into the user's show.html.erb (so that a user can create a new calendar from his profile), everything works fine.
However, we would like to have a special page, call dashboard, where a user could see all his calendars and create a new one.
We believe the logical url should be /users/:id/administrations.
So, we embedded the above form in the Administration's index.html.erb.
And as soon as we do that and visit for instance /users/1/administrations, we get the following error:
ArgumentError in AdministrationsController#index
First argument in form cannot contain nil or be empty
Extracted source (around line #432):
else
object = record.is_a?(Array) ? record.last : record
raise ArgumentError, "First argument in form cannot contain nil or be empty" unless object
object_name = options[:as] || model_name_from_record_or_class(object).param_key
apply_form_for_options!(record, object, options)
end
EDIT: here is also our Administrations#Controller:
class AdministrationsController < ApplicationController
def to_s
role
end
def index
#user = current_user
#administrations = #user.administrations
end
def show
#administration = Administration.find(params[:id])
end
end
Any idea of what we are doing wrong?
First argument in form cannot contain nil or be empty
The reason is #calendar is not initialised in your controller action, so obviously #calendar is nil. So is the error.
To get your form to work in administrations/index.html.erb, you should be having the below code in your index action of your administrations_controller.
def index
#user = current_user
#administrations = #user.administrations
#calendar = Calendar.new # this one.
end

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.

Resources