Create resource from model when User signs up - ruby-on-rails

I'm trying to create a resource Mission when a new user is signed up. Mission has a foreign key Location as well.
class User < ApplicationRecord
after_create :create_mission
private
def create_mission
Mission.create(user: self, location_id: 1, name: 'Mission 1')
end
end
But this code doesn't work unfortunately. How can I solve it?

How about this:
class User < ApplicationRecord
after_create :create_mission
private
def create_mission
missions.create! location_id: 1, name: 'Mission 1'
end
end
Now your error could be visible with create!. Try this.

I do not recommend you to create relations in callbacks. It will hurt you if you will need to create user in another action or in console. Better to create it in controller after creation (in some service object in the future)
def create
if #user.save
#user.missions.create(...)
end
end
And you can use debugger to check errors dynamically (shipped with rails https://github.com/deivid-rodriguez/byebug, just insert 'debugger' in your code), probably you have some validation error.

Related

Safest way to override the update method of a model

I have the following model:
class TwitterEngagement < ApplicationRecord
end
And I would like to override create (and create!), update (and
update!) methods of it so no one can manually entry fake data. I would like the help of someone more experienced with active record and rails so I don't mess anything up. Right now what I have is:
class TwitterEngagement < ApplicationRecord
belongs_to :page
def create
super(metrics)
end
def update
super(metrics)
end
private
def metrics
client.get_engagements(page.url)
def client
TwitterClient.new
end
end
Thank you.
TL;DR:
class FacebookEngagement < ApplicationRecord
def create_or_update(*args, &block)
super(metrics)
end
Probably depends on your Rails version, but I traced the ActiveRecord::Persistence sometime before in Rails 5, and found out that both create and update eventually calls create_or_update.
Suggestion:
If ever possible, I'll just do a validation, because it kinda makes more sense because you are validating the inputs, and then probably set an optional readonly?, to prevent saving of records. This will also prevent "silent failing" code / behaviour as doing TL;DR above would not throw an exception / populate the validation errors, if say an unsuspecting developer does: facebook_engagement.update(someattr: 'somevalue') as the arguments are gonna basically be ignored because it's instead calling super(metrics), and would then break the principle of least surprise.
So, I'll probably do something like below:
class FacebookEngagement < ApplicationRecord
belongs_to :page
validate :attributes_should_not_be_set_manually
before_save :set_attributes_from_facebook_engagement
# optional
def readonly?
# allows `create`, prevents `update`
persisted?
end
private
def attributes_should_not_be_set_manually
changes.keys.except('page_id').each do |attribute|
errors.add(attribute, 'should not be set manually!')
end
end
def set_attributes_from_facebook_engagement
assign_attributes(metrics)
end
def metrics
# simple memoization to prevent wasteful duplicate requests (or remove if not needed)
#metrics ||= graph.get_object("#{page.url}?fields=engagement")
end
def graph
Koala::Facebook::API.new
end
end

Rails - passed parameter not recognized in custom service [NoMethodError]

I've written a custom service for my project:
#app\services\quiz_data_creation.rb:
class QuizDataCreation
def initialize(user)
#quiz_session = user.quiz_session #<-- here is the problem
end
def create
create_answer_data
end
private
def create_answer_data
#quiz_session.quiz.questions[#quiz_session.current_question_index].answers
end
end
I call the create method in my ActionCable channel like this:
class QuizDataChannel < ApplicationCable::Channel
def send_data
logger.debug "[AC] before quiz data creation service - current user: #{current_user.inspect}"
#answers = QuizDataCreation.new(user: current_user).create
#then send answers
end
end
The problem I'm having is that the service does not recognize the user object I pass when calling the create method, even though the logger shows me that the correct object is being found! It gives me the Error Message:
NoMethodError - undefined method quiz_session for #<Hash:0xc86cfa0>
I'm using a Devise generated User. Also, it might be relevant that I'm using 2 models that inherit from the User model (Teacher and Student), but all the attributes are stored in the users table.
A user record looks like this:
[AC] before quiz data creation service - current user: #<Teacher id: 1, email: "bob#teacher.edu", created_at: "2017-05-11 07:24:48", updated_at: "2017-07-26 08:40:27", name: "Bob", quiz_session_id: 7>
I would be very happy if anyone could point out to me how to solve this issue - thank you! :)
You declared a positional parameter
def initialize(user)
but you pass a keyword argument:
QuizDataCreation.new(user: current_user)
Change it to
QuizDataCreation.new(current_user)
or change method signature to accept keyword args.

Delayed Job - job cannot be created for non-persisted record after destroy object

Rails 4 and delayed_job 4.1.2. I attempt to delay recalculation of overall rating after destroying a Review, but obviously because after destroying a review object there is no ID for the Review object. So every time after trying to destroy an object, it tries to create a delayed job but throws this error:
ArgumentError (job cannot be created for non-persisted record:
#<Review id: 44, review: "Bad", rating: 1, reviewable_id: 2,
reviewable_type: "Spot", user_id: 1, created_at: "2016-05-30 17:13:29",
updated_at: "2016-05-30 17:13:29">):
app/controllers/reviews_controller.rb:40:in `destroy'
I have the following code:
# reviews_controller.rb
class ReviewsController < ApplicationController
def destroy
review.destroy
flash[:success] = t("reviews.destroy.success")
end
end
# review.rb
class Review < ActiveRecord::Base
after_destroy :calculate_overall_rating
def calculate_overall_rating
if number_of_reviews > 0
reviewable.update_attribute(:overall_rating, overall_rating)
else
reviewable.update_attribute(:overall_rating, 0)
end
end
handle_asynchronously :calculate_overall_rating
end
It's good to note that the calculate_overall_rating doesn't require a Review object.
If I remove handle_asynchronously :calculate_overall_rating it will work, and recalculate. But I am trying to delay this job.
This error is indeed raised by delayed_job when you try to delay a method on a deleted (or not yet created) record. The direct cause of this error is because delayed_job passes self (i.e. the just-deleted review object) as the target object when calling thehandle_asynchronously method. I don't know why it behaves like that, I just found a statement from one of the gem's authors that says it works the same way as ActiveJob.
Anyway, it seems to me that you might have the recalculation method defined in the wrong place. If I understand it correctly, after destroying a review, an average rating is recalculated based on all reviews of the given reviewable (e.g. a spot). It seems weird to me that such code is defined as a method of a single review instance. A single review (what's more a deleted one) should not know anything about other reviews of the same spot and should not have to deal with them at all.
I guess the method should have been defined as a class method instead, with the reviewable as a parameter. Of course this would mean you'd have to make the other calculation methods overall_rating and number_of_reviews, class methods, too. But I think this is a good thing, again, because the domain of these methods lies outside a single review. Something like the following:
# review.rb
class Review < ActiveRecord::Base
after_destroy :recalculate_overall_rating
def recalculate_overall_rating
self.class.calculate_overall_rating(reviewable)
end
def self.calculate_overall_rating(reviewable)
if number_of_reviews(reviewable) > 0
reviewable.update_attribute(:overall_rating, overall_rating(reviewable))
else
reviewable.update_attribute(:overall_rating, 0)
end
end
handle_asynchronously :calculate_overall_rating
end
Another option (and I like it even a bit more I guess) would be to place the recalculation methods inside the reviewable class, e.g. inside the Post class. If you have more reviewable class types, you could make a module included from all these classes, e.g. Reviewable (I hope that would not clash with the Rails association name though) and place the recalculation methods inside it, this time as instance methods. Why? Because it is an instance of a reviewable which wants to get all its reviews recalculated and which still exists even after a review deletion so it can be easily run asynchronously. Something like the following:
# reviewable.rb
module Reviewable
def calculate_overall_rating
if number_of_reviews > 0
update_attribute(:overall_rating, overall_rating)
else
update_attribute(:overall_rating, 0)
end
end
handle_asynchronously :calculate_overall_rating
# overall_rating and number_of_reviews are also defined in this module
end
# review.rb
class Review < ActiveRecord::Base
after_destroy :recalculate_overall_rating
def recalculate_overall_rating
reviewable.calculate_overall_rating
end
end
# post.rb
class Post < ActiveRecord::Base
include Reviewable
end

Is it possible to pass params from to a before_create in a model?

I have a model, for example :
class Account < ActiveRecord::Base
before_create :build_dependencies
def build_dependencies
# use nifty params to build this related object
build_nifty_object(params)
end
The initial params are sent in through a hidden form tag on the Account#new form.
But there's no reason/need for these params to be saved to the account model. I just need them in the NiftyObject model.
Is there a clever way to pass these params to the before_create method ? Or any other alternatives that might accomplish the same task?
Thanks!
You can use instance variables to workaround this, and do +1 step from the controller:
class Account < ActiveRecord::Base
before_create :build_dependencies
def assign_params_from_controller(params)
#params = params
end
def build_dependencies
# use nifty params to build this related object
build_nifty_object(#params)
end
In the controller:
def Create
account = new Account(params)
account.assign_params_from_controller( ... )
account.save # this will trigger before_create
end
I believe the active-record callback cycle hurts you in most cases, this included. Instead of using before_create, I recommend you use a service object that coordinates the creation of Account and nifty-object.
I assume you want nifty-object to know about the account, so I passed it in to it's create method.
class CreatesAccount
def self.create(params)
account = Account.new(params)
return account unless account.valid?
ActiveRecord::Base.transaction do
account.save!
NifyObject.create(params, account: account)
return account
end
end
end
I appreciate everyone's answers. But I finally am going with an attr_accessor instead. In this way, it doesn't save anything anywhere, but would still be accessible from the model in a before_create method.

Add record to a has_and_belongs_to_many relationship

I have two models, users and promotions. The idea is that a promotion can have many users, and a user can have many promotions.
class User < ActiveRecord::Base
has_and_belongs_to_many :promotions
end
class Promotion < ActiveRecord::Base
has_and_belongs_to_many :users
end
I also have a promotions_users table/model, with no id of its own. It references user_id and promotions_id
class PromotionsUsers < ActiveRecord::Base
end
So, how do I add a user to a promotion? I've tried something like this:
user = User.find(params[:id])
promotion = Promotion.find(params[:promo_id])
promo = user.promotions.new(promo)
This results in the following error:
NoMethodError: undefined method `stringify_keys!' for #<Promotion:0x10514d420>
If I try this line instead:
promo= user.promotions.new(promo.id)
I get this error:
TypeError: can't dup Fixnum
I'm sure that there is a very easy solution to my problem, and I'm just not searching for the solution the right way.
user = User.find(params[:id])
promotion = Promotion.find(params[:promo_id])
user.promotions << promotion
user.promotions is an array of the promotions tied to the user.
See the apidock for all the different functions you have available.
You can do just
User.promotions = promotion #notice that this will delete any existing promotions
or
User.promotions << promotion
You can read about has_and_belongs_to_many relationship here.
This is also useful
User.promotion.build(attr = {})
so, promotion object saves, when you save User object.
And this is
User.promotion.create(attr = {})
create promotion you not need to save it or User model
If you want to add a User to a Promotion using a prototypical PromotionsController CRUD setup and you're not using Rails form helpers, you can format the params as:
params = {id: 1, promotion: { id: 1, user_ids: [2] }}
This allows you to keep the controller slim, e.g., you don't have to add anything special to the update method.
class PromotionsController < ApplicationController
def update
promotion.update(promotion_params)
# simplified error handling
if promotion.errors.none?
render json: {message: 'Success'}, status: :ok
else
render json: post.errors.full_messages, status: :bad_request
end
end
private
def promotions_params
params.require(:promotion).permit!
end
def promotion
#promotion ||= Promotion.find(params[:id])
end
end
The result would be:
irb(main)> Promotion.find(1).users
=> #<ActiveRecord::Associations::CollectionProxy [#<User id: 2 ...>]>
For all those in the current times, Rails does have built-in functions that are like simpler associations.
For building (i.e. Promotion.new), you can use
user.promotions.build(promotion_attributes)
For creating it's the same
user.promotions.create(promotion_attributes)
Just wanted to give a more familiar option. It's outline in the apidoc The other answers work as well.

Resources