How to associate Models using one to one relationship - ruby-on-rails

Given below are two models associated with one to many relationship and it works fine
class User < ActiveRecord::Base
has_many :events, dependent: :destroy
end
but when I associate them one to one
class User < ActiveRecord::Base
has_one :event, dependent: :destroy
end
It gives me the following error
undefined method `build' for nil:NilClass
Events Controller
class EventsController < ApplicationController
def new
#event = current_user.event.build
end
def create
#event = current_user.event.build(event_params)
if #event.save
redirect_to #event
else
render 'new'
end
end
private
def event_params
params.require(:event).permit(:date, :time, :venue)
end
end

The reason it is throwing the error is because there is no event for that user. While that is how to build through association for a has_many relationship, it doesn't work for has_one. See the documentation on has_one where they say that calling .build will not work.
Instead use #event = current_user.create_event
Adding a has_one relationship will give you the following methods:
association(force_reload = false)
association=(associate)
build_association(attributes = {})
create_association(attributes = {})
create_association!(attributes = {})

For has_one associations, Rails provides a special set of methods for building the association (documentation)
build_event
create_event
Both of these take a hash of attributes just like the build method of a has_many association does.
In your case, change current_user.event.build to current_user.build_event and current_user.event.build(event_params) to current_user.build_event(event_params)

Related

Mongoid after/before_remove callback is not triggered

I have the following relationship between Classroom and Student models :
Classroom has_many :students
Student belongs_to :classroom
In my Classroom model I have these relation callbacks :
has_many :students,
after_add: :update_student_count,
before_remove: :update_student_count
def update_student_count(student)
self.student__count = students.count
self.save
end
In my Student controller I have :
def destroy
student = Student.find params[:id]
student.destroy!
redirect_to action: :index
end
However student.destroy! never triggers the before_remove callback in my Classroom model.
I have tried writing the action destroy in the following way to be executing the destroy action on the classroom instance but it seems destroy cant be used in this way with mongoid...
def destroy
student = Student.find params[:id]
classroom= student.classroom
student.destroy!
classroom.students.destroy(student)
redirect_to action: :index
end
Why is my before_remove callback never executed ?
Try before_destroy instead.
Here's the Mongoid docs for callbacks
Just to flush out teddybear's solution, I think you could do something like this:
class Student
belongs_to :classroom
after_destroy :update_student_count
after_create :update_student_count
def update_student_count
classroom.update_student_count
end
end
class Classroom
has_many :students
def update_student_count
self.student__count = students.count
save
end
end

Rails 5 dependent: :destroy doesn't work

I have the following classes:
class Product < ApplicationRecord
belongs_to :product_category
def destroy
puts "Product Destroy!"
end
end
class ProductCategory < ApplicationRecord
has_many :products, dependent: :destroy
def destroy
puts "Category Destroy!"
end
end
Here, I am trying to override the destroy method where I eventually want to do this:
update_attribute(:deleted_at, Time.now)
When I run the following statement in Rails console: ProductCategory.destroy_all I get the following output
Category Destroy!
Category Destroy!
Category Destroy!
Note: I have three categories and each category has more than one Products. I can confirm it by ProductCategory.find(1).products, which returns an array of products. I have heard the implementation is changed in Rails 5. Any points on how I can get this to work?
EDIT
What I eventually want is, to soft delete a category and all associated products in one go. Is this possible? Or will ave to iterate on every Product object in a before destroy callback? (Last option for me)
You should call super from your destroy method:
def destroy
super
puts "Category destroy"
end
But I definitely wouldn't suggest that you overide active model methods.
So this is how I did it in the end:
class Product < ApplicationRecord
belongs_to :product_category
def destroy
run_callbacks :destroy do
update_attribute(:deleted_at, Time.now)
# return true to escape exception being raised for rollback
true
end
end
end
class ProductCategory < ApplicationRecord
has_many :products, dependent: :destroy
def destroy
# run all callback around the destory method
run_callbacks :destroy do
update_attribute(:deleted_at, Time.now)
# return true to escape exception being raised for rollback
true
end
end
end
I am returning true from the destroy does make update_attribute a little dangerous but I am catching exceptions at the ApplicationController level as well, so works well for us.

Adding Posts To A Collection with Join Models

I'm trying to add a 'Collections' model to group Posts so that any user can add any Post they like to any Collection they've created. The Posts will have already been created by a different user. We are just letting other users group these posts in their own Collections. Basically like bookmarking.
What is the cleanest, and most Rails-ey-way of doing this?
I've created the model and run through the migration and what not. Also I've already created proper views for Collection.
rails g model Collection title:string user_id:integer
collections_controller.rb
class CollectionsController < ApplicationController
def index
#collections = current_user.collections.all
end
def show
#collection = Collection.all
end
def new
#collection = Collection.new
end
def create
#collection = current_user.collections.build(collection_params)
if #collection.save
redirect_to #collection, notice: 'saved'
else
render action: 'new'
end
end
def update
end
private
def collection_params
params.require(:collection).permit(:title)
end
end
collection.rb
class Collection < ActiveRecord::Base
belongs_to :user
has_many :posts
validates :title, presence: true
end
post.rb
has_many :collections
It seems like has_many or has_and_belongs_to_many associations are not correct? Should I be creating another model to act as an intermediary to then use
has_many :collections :through :collectionList?
If my association is wrong, can you explain what I need to change to make this work?
Also the next part in this is since this is not being created when the Post or Collection is created, I'm not sure the best way to handle this in the view. What is the best way to handle this, keeping my view/controller as clean as possible? I just want to be able to have a button on the Post#Show page that when clicked, allows users to add that post to a Collection of their own.
In such case you should use or has_and_belongs_to_many or has_many :through association. The second one is recommended, because it allows more flexibility. So now you should:
Create new model PostsCollections
rails g model PostsCollections post_id:integer collection_id:integer
and migrate it
Set correct model associations:
Something like:
class Post < ActiveRecord::Base
has_many :posts_collections
has_many :categories, through: :posts_collections
end
class Collection < ActiveRecord::Base
has_many :posts_collections
has_many :posts, through: :posts_collections
end
class PostsCollections < ActiveRecord::Base
belongs_to :post
belongs_to :collection
end
Then you'll be able to use
#collection.first.posts << #post
And it will add #post to #collection's posts
To add a post to a collection from view
Add a new route to your routes.rb, something like:
resources :collections do # you should have this part already
post :add_post, on: :member
end
In your Collections controller add:
def add_post
#post = Post.find(params[:post_id])
#collection = Collection.find(params[:id])
#collection.posts << #post
respond_to do |format|
format.js
end
end
As for views, you'll have to create a form to show a collection select and button to add it. That form should make POST method request to add_post_collection_path(#collection) with :post_id parameter.
You can read more explanations of how rails associations work in Michael Hartl's tutorial, because that subject is very wide, and can't be explained with short answer.

Add record to a model upon create used in many models

I have a survey and I would like to add participants to a Participant model whenever a user answers to a question for the first time. The survey is a bit special because it has many functions to answer questions such as Tag words, Multiple choices and Open Question and each function is actually a model that has its own records. Also I only want the Participant to be saved once.
The Participant model is fairly simple:
class Participant < ActiveRecord::Base
belongs_to :survey
attr_accessible :survey_id, :user_id
end
The Survey model is also straightforward:
class Survey < ActiveRecord::Base
...
has_many :participants, :through => :users
has_many :rating_questions, :dependent => :destroy
has_many :open_questions, :dependent => :destroy
has_many :tag_questions, :dependent => :destroy
belongs_to :account
belongs_to :user
accepts_nested_attributes_for :open_questions
accepts_nested_attributes_for :rating_questions
accepts_nested_attributes_for :tag_questions
...
end
Then you have models such as rating_answers that belong to a rating_question, open_answers that belong to open_questions and so on.
So initially I thought for within my model rating_answers I could add after_create callback to add_participant
like this:
class RatingAnswer < ActiveRecord::Base
belongs_to :rating_question
after_create :add_participant
...
protected
def add_participant
#participant = Participant.where(:user_id => current_user.id, :survey_id => Survey.find(params[:survey_id]))
if #participant.nil?
Participant.create!(:user_id => current_user.id, :survey_id => Survey.find(params[:survey_id]))
end
end
end
In this case, I didn't know how to find the survey_id, so I tried using the params but I don't think that is the right way to do it. regardles it returned this error
NameError (undefined local variable or method `current_user' for #<RatingAnswer:0x0000010325ef00>):
app/models/rating_answer.rb:25:in `add_participant'
app/controllers/rating_answers_controller.rb:12:in `create'
Another idea I had was to create instead a module Participants.rb that I could use in each controllers
module Participants
def add_participant
#participant = Participant.where(:user_id => current_user.id, :survey_id => Survey.find(params[:survey_id]))
if #participant.nil?
Participant.create!(:user_id => current_user.id, :survey_id => Survey.find(params[:survey_id]))
end
end
end
and in the controller
class RatingAnswersController < ApplicationController
include Participants
def create
#rating_question = RatingQuestion.find_by_id(params[:rating_question_id])
#rating_answer = RatingAnswer.new(params[:rating_answer])
#survey = Survey.find(params[:survey_id])
if #rating_answer.save
add_participant
respond_to do |format|
format.js
end
end
end
end
And I got a routing error
ActionController::RoutingError (uninitialized constant RatingAnswersController::Participants):
I can understand this error, because I don't have a controller for participants with a create method and its routes resources
I am not sure what is the proper way to add a record to a model from a nested model and what is the cleaner approach.
Ideas are most welcome!
current_user is a helper that's accessible in views/controller alone. You need to pass it as a parameter into the model. Else, it ain't accessible in the models. May be, this should help.
In the end I ended up using the after_create callback but instead of fetching the data from the params, I used the associations. Also if #participant.nil? didn't work for some reason.
class RatingAnswer < ActiveRecord::Base
belongs_to :rating_question
after_create :add_participant
...
protected
def add_participant
#participant = Participant.where(:user_id => self.user.id, :survey_id => self.rating_question.survey.id)
unless #participant.any?
#new_participant = Participant.create(:user_id => self.user.id, :survey_id => self.survey.rating_question.id)
end
end
end
The cool thing with associations is if you have deeply nested associations for instead
Survey has_many questions
Question has_many answers
Answer has_many responses
in order to fetch the survey id from within the responses model you can do
self.answer.question.survey.id
very nifty!

Rails, using build to create a nested object, but it is not being saved

My app has three models:
Thread (has_many :thread_apps)
ThreadApp (belongs_to :thread, has_many :forms, :as => :appable)
Form (belongs_to :app)
ThreadApp Fields: thread_id, form_id, appable_id, appable_type
What I want to be able to do is when creating a form, ensure that a ThreadApp record is also created to make the association:
Here is what I have:
class FormsController < ApplicationController
def create
#thread = Thread.find(params[:thread_id])
#thread_app = #thread.thread_apps.new
#form = #thread_app.forms.build(params[:form].merge(:user_id => current_user.id))
#form.save
....
This is saving the form nicely, but the thread_app associated is not being made? Any ideas why?
Thank you
callings model.save does not save associations unless you tell it to
you can set autosave
class Form < ActiveRecord::Base
belongs_to :thread_app , :autosave => true
end
or call save on it in the controller
#thread_app.save
or you can take it out of the controller entirely
and do this with a callback
class Form < ActiveRecord::Base
before_create :create_thread_app
def create_thread_app
self.thread_app ||= ThreadApp.create(...)
end
end
or after_create, _before_validation_on_create, or any other call back would work
--UPDATE--
this might make a difference using create inse=tead of new, and appables that you specified as 'as'
class FormsController < ApplicationController
def create
#thread = Thread.find(params[:thread_id])
#thread_app = #thread.thread_apps.create
#form = #thread_app.appables.build(params[:form].merge(:user_id => current_user.id))
#form.save
....
Instead of:
#thread_app = #thread.thread_apps.new
You should have:
#thread_app = #thread.thread_apps.create

Resources