Method errors during before_save - ruby-on-rails

I'm sure I am just missing something simple, but have been racking my brain for the past few days over this.
I have a Booking and Review table, where the Booking has many Reviews. I can create the Review, but run through an error when trying to define roles of the user leaving and receiving the review.
Here are my models. The Review
class Review < ActiveRecord::Base
before_save :define_review_role
after_create :call_update_rating
belongs_to :booking
belongs_to :client, class_name: "User", primary_key: "client_id"
belongs_to :talent, class_name: "User", primary_key: "talent_id"
def define_review_role
if review_sender_id === self.booking.client_id
review_receiver_id = self.booking.talent_id
else
review_receiver_id = self.booking.client_id
end
self.update
end
def call_update_rating
user = User.find(self.review_receiver_id)
if review_receiver_id == self.booking.talent_id
user.update_talent_rating(self.rating)
else
user.update_client_rating(self.rating)
end
user.save
end
end
And the Booking model
class Booking < ActiveRecord::Base
# Start Validations
validates :amount, format: {with: /(\d{1,3})(\.\d{1,2})?/, :message => "field is invalid. Please enter a correct amount."}
belongs_to :user
belongs_to :client, class_name: "User", primary_key: "client_id"
belongs_to :talent, class_name: "User", primary_key: "talent_id"
has_many :reviews
has_many :sent_reviews, class_name: "Review", primary_key: "talent_id"
has_many :received_reviews, class_name: "Review", primary_key: "client_id"
def client
User.find(client_id)
end
def talent
User.find(talent_id)
end
end
I have been able to create the review just fine, and upon inspection I am finding that the review.review_receiver_id is being left blank. The define_review_role for some reason is not running, I have tried with before_create, after_save, after_create and no dice.
I know that this is not running because upon inspection, the review_receiver_id is being left blank.
I am also able to access the information through review.booking.talent_id, and review.booking.client_id, so the connections are there. I know I must be missing something but have no idea what.

Your method define_review_role running you only have badly written code. It should probably look something like this
def define_review_role
if review_sender_id === self.booking.client_id
self.review_receiver_id = self.booking.talent_id
else
self.review_receiver_id = self.booking.client_id
end
end
If you try to assign value without self the value is assigned to newly created local method instead of attribute of your Report class.
You also cannot call save or update on the end of this method because you are calling it with before_save callback. Methods save and update trigger it again and method gonna be called again and you create infinity loop.

Related

When creating a record, copy an attribute from a associated record

In my Rails to-do app, I have a Tasks model. Tasks can be blocked_by each other. Each task has a User. When I do taskA.blocked_by.create(name: "Task B"), I would like Task B to get the same User that Task A has.
The problem is, I can't figure out how to refer to the record that's creating the current record. I need to learn how to get taskA.user so I can automatically assign it to taskB. I'd rather not have to do that manually every time I create a blocked_by task.
I've tried setting self.user in a before_validation method.
before_validation :inherit_user, on: :create
private
def inherit_user
if self.user == nil
p "Task #{self.name} missing user"
if self.blocked_by.count > 0
self.user = self.blocked_by.first.user
p "Inheriting user from first blocked_by task #{self.blocked_by.first.name}"
end
end
end
This doesn't work because self.blocked_by is empty because the record isn't saved yet.
Rails' documentation on association class methods leads me to believe I should be able to do something like this:
has_many :blocked_by do |owner|
def create(attributes)
p owner # access properties from association owner here
attributes.push(user: owner.user)
super
end
end
When I try this I get:
NoMethodError (undefined method `owner' for #<ActiveRecord::Associations::CollectionProxy []>)
Edit: Here's my model file:
class Task < ApplicationRecord
validates :name, :presence => true
belongs_to :user
has_many :children, class_name: "Task", foreign_key: "parent_id"
belongs_to :parent, class_name: "Task", optional: true
has_ancestry
# thanks to https://medium.com/#jbmilgrom/active-record-many-to-many-self-join-table-e0992c27c1e
has_many :blocked_blocks, foreign_key: :blocker_id, class_name: "BlockingTask"
has_many :blocked_by, through: :blocked_blocks, source: :blocking, dependent: :destroy
has_many :blocker_blocks, foreign_key: :blocked_id, class_name: "BlockingTask"
has_many :blocking, through: :blocker_blocks, source: :blocker, dependent: :destroy
has_many_attached :attachments
before_validation :inherit_user, on: :create
def completed_descendants
self.descendants.where(completed: true)
end
def attachment_count
self.attachments.count
end
private
def inherit_user
if self.user == nil and self.parent
self.user = self.parent.user
end
end
end
I can inherit_user from a parent task, like so: taskA.children.create(name: "Task B"). I'd like to do the same for a blocked_by relationship.
To refer to the current record which is supposed to be created, try running before_create callback.
before_create :inherit_user
and the self.blocked_by now must have a value.

Prevent from raising ActiveRecord::RecordInvalid or adding twice on has_many association

I want to change has_many association behaviour
considering this basic data model
class Skill < ActiveRecord::Base
has_many :users, through: :skills_users
has_many :skills_users
end
class User < ActiveRecord::Base
has_many :skills, through: :skills_users, validate: true
has_many :skills_users
end
class SkillsUser < ActiveRecord::Base
belongs_to :user
belongs_to :skill
validates :user, :skill, presence: true
end
For adding a new skill we can easily do that :
john = User.create(name: 'John Doe')
tidy = Skill.create(name: 'Tidy')
john.skills << tidy
but if you do this twice we obtain a duplicate skill for this user
An possibility to prevent that is to check before adding
john.skills << tidy unless john.skills.include?(tidy)
But this is quite mean...
We can as well change ActiveRecord::Associations::CollectionProxy#<< behaviour like
module InvalidModelIgnoredSilently
def <<(*records)
super(records.to_a.keep_if { |r| !!include?(r) })
end
end
ActiveRecord::Associations::CollectionProxy.send :prepend, InvalidModelIgnoredSilently
to force CollectionProxy to ignore transparently adding duplicate records.
But I'm not happy with that.
We can add a validation on extra validation on SkillsUser
class SkillsUser < ActiveRecord::Base
belongs_to :user
belongs_to :skill
validates :user, :skill, presence: true
validates :user, uniqueness: { scope: :skill }
end
but in this case adding twice will raise up ActiveRecord::RecordInvalid and again we have to check before adding
or make a uglier hack on CollectionProxy
module InvalidModelIgnoredSilently
def <<(*records)
super(valid_records(records))
end
private
def valid_records(records)
records.with_object([]).each do |record, _valid_records|
begin
proxy_association.dup.concat(record)
_valid_records << record
rescue ActiveRecord::RecordInvalid
end
end
end
end
ActiveRecord::Associations::CollectionProxy.send :prepend, InvalidModelIgnoredSilently
But I'm still not happy with that.
To me the ideal and maybe missing methods on CollectionProxy are :
john.skills.push(tidy)
=> false
and
john.skills.push!(tidy)
=> ActiveRecord::RecordInvalid
Any idea how I can do that nicely?
-- EDIT --
A way I found to avoid throwing Exception is throwing an Exception!
class User < ActiveRecord::Base
has_many :skills, through: :skills_users, before_add: :check_presence
has_many :skills_users
private
def check_presence(skill)
raise ActiveRecord::Rollback if skills.include?(skill)
end
end
Isn't based on validations, neither a generic solution, but can help...
Perhaps i'm not understanding the problem but here is what I'd do:
Add a constraint on the DB level to make sure the data is clean, no matter how things are implemented
Make sure that skill is not added multiple times (on the client)
Can you show me the migration that created your SkillsUser table.
the better if you show me the indexes of SkillsUser table that you have.
i usually use has_and_belongs_to_many instead of has_many - through.
try to add this migration
$ rails g migration add_id_to_skills_users id:primary_key
# change the has_many - through TO has_and_belongs_to_many
no need for validations if you have double index "skills_users".
hope it helps you.

Rails scope with method value comparison

So i'm fairly new to rails and ActiveRecord and I have a need for a scope to filter between Client entities. Basically the scope should return all Client records where the client's current state is equal to a certain state object.
This is calculated by getting a client's last state_change and then pulling that state_change's from_state which is a State object.
I have defined a method to return the current_state however in rails console when I test it with Client.current_state(Client.last) I get this error:
NameError: undefined local variable or method 'state_changes for #<Class:0x0000000685eb88> but when running Client.last.state_changes in console it works fine.
My client.rb
class Client < ActiveRecord::Base
has_and_belongs_to_many :users
belongs_to :industry
belongs_to :account
has_many :contacts
has_many :state_changes
belongs_to :head, class_name: "Client"
has_many :branches, class_name: "Client", foreign_key: "head_id"
has_many :meetings, through: :contacts
has_many :sales, through: :meetings
scope :prospects, -> (client) { where(Client.current_state(client): State.PROSPECT_STATE) }
def self.has_at_least_one_sale? (client)
return client.sales.empty?
end
def self.has_account_number? (client)
return client.account_number.present?
end
def self.current_state (client)
state_changes.last.to_state
end
end
state_change.rb
class StateChange < ActiveRecord::Base
belongs_to :client
belongs_to :from_state, class_name: "State", foreign_key: :to_state_id
belongs_to :to_state, class_name: "State", foreign_key: :from_state_id
end
state.rb
class State < ActiveRecord::Base
has_many :from_states, class_name: "StateChange", foreign_key: :to_state_id
has_many :to_states, class_name: "StateChange", foreign_key: :from_state_id
def self.PROSPECT_STATE
return State.find_by name: 'Prospect'
end
def self.CLIENT_STATE
return State.find_by name: 'Client'
end
def self.SUSPECT_STATE
return State.find_by name: 'Suspect'
end
end
I also get syntax errors regarding the scope I defined in client.rb. I have followed the ActiveRecord guide but they don't explain how to have chained methods in the actualy scope query.
The reason you get the error NameError: undefined local variable or method 'state_changes for #<Class:0x0000000685eb88> is because you define current_state as a class method and pass the client as a parameter. That's why state_changes is called on the class and not the instance. In this case you would need to use the client to get the state_changes.
def self.current_state (client)
client.state_changes.last.to_state
end
Also scopes are meant to just chain query logic. I'm not sure if it is possible to just use queries to get your wanted result. And I hope I understood your logic correctly. Alternatively you could use a class method.
def self.prospects (client)
Client.all.select { |c| c.current_state(c) == State.PROSPECT_STATE }
end
As pointed out by Зелёный in the comment, maybe you also want to just change the methods to instance methods, in which case reading the resource he linked would be very helpful.
Update based on comment:
I think what you actually want is using an instance method for current_state like this:
def current_state
state_changes.last.to_state
end
And then you can get prospects like this:
def self.prospects
Client.all.select { |c| c.current_state == State.PROSPECT_STATE }
end

Rails association with multiple foreign keys

I want to be able to use two columns on one table to define a relationship. So using a task app as an example.
Attempt 1:
class User < ActiveRecord::Base
has_many :tasks
end
class Task < ActiveRecord::Base
belongs_to :owner, class_name: "User", foreign_key: "owner_id"
belongs_to :assignee, class_name: "User", foreign_key: "assignee_id"
end
So then Task.create(owner_id:1, assignee_id: 2)
This allows me to perform Task.first.owner which returns user one and Task.first.assignee which returns user two but User.first.task returns nothing. Which is because task doesn't belong to a user, they belong to owner and assignee. So,
Attempt 2:
class User < ActiveRecord::Base
has_many :tasks, foreign_key: [:owner_id, :assignee_id]
end
class Task < ActiveRecord::Base
belongs_to :user
end
That just fails altogether as two foreign keys don't seem to be supported.
So what I want is to be able to say User.tasks and get both the users owned and assigned tasks.
Basically somehow build a relationship that would equal a query of Task.where(owner_id || assignee_id == 1)
Is that possible?
Update
I'm not looking to use finder_sql, but this issue's unaccepted answer looks to be close to what I want: Rails - Multiple Index Key Association
So this method would look like this,
Attempt 3:
class Task < ActiveRecord::Base
def self.by_person(person)
where("assignee_id => :person_id OR owner_id => :person_id", :person_id => person.id
end
end
class Person < ActiveRecord::Base
def tasks
Task.by_person(self)
end
end
Though I can get it to work in Rails 4, I keep getting the following error:
ActiveRecord::PreparedStatementInvalid: missing value for :owner_id in :donor_id => :person_id OR assignee_id => :person_id
TL;DR
class User < ActiveRecord::Base
def tasks
Task.where("owner_id = ? OR assigneed_id = ?", self.id, self.id)
end
end
Remove has_many :tasks in User class.
Using has_many :tasks doesn't make sense at all as we do not have any column named user_id in table tasks.
What I did to solve the issue in my case is:
class User < ActiveRecord::Base
has_many :owned_tasks, class_name: "Task", foreign_key: "owner_id"
has_many :assigned_tasks, class_name: "Task", foreign_key: "assignee_id"
end
class Task < ActiveRecord::Base
belongs_to :owner, class_name: "User"
belongs_to :assignee, class_name: "User"
# Mentioning `foreign_keys` is not necessary in this class, since
# we've already mentioned `belongs_to :owner`, and Rails will anticipate
# foreign_keys automatically. Thanks to #jeffdill2 for mentioning this thing
# in the comment.
end
This way, you can call User.first.assigned_tasks as well as User.first.owned_tasks.
Now, you can define a method called tasks that returns the combination of assigned_tasks and owned_tasks.
That could be a good solution as far the readability goes, but from performance point of view, it wouldn't be that much good as now, in order to get the tasks, two queries will be issued instead of once, and then, the result of those two queries need to be joined as well.
So in order to get the tasks that belong to a user, we would define a custom tasks method in User class in the following way:
def tasks
Task.where("owner_id = ? OR assigneed_id = ?", self.id, self.id)
end
This way, it will fetch all the results in one single query, and we wouldn't have to merge or combine any results.
Extending upon #dre-hh's answer above, which I found no longer works as expected in Rails 5. It appears Rails 5 now includes a default where clause to the effect of WHERE tasks.user_id = ?, which fails as there is no user_id column in this scenario.
I've found it is still possible to get it working with a has_many association, you just need to unscope this additional where clause added by Rails.
class User < ApplicationRecord
has_many :tasks, ->(user) {
unscope(:where).where(owner: user).or(where(assignee: user)
}
end
Rails 5:
you need to unscope the default where clause
see #Dwight answer if you still want a has_many associaiton.
Though User.joins(:tasks) gives me
ArgumentError: The association scope 'tasks' is instance dependent (the scope block takes an argument). Preloading instance dependent scopes is not supported.
As it is no longer possible you can use #Arslan Ali solution as well.
Rails 4:
class User < ActiveRecord::Base
has_many :tasks, ->(user){ where("tasks.owner_id = :user_id OR tasks.assignee_id = :user_id", user_id: user.id) }
end
Update1:
Regarding #JonathanSimmons comment
Having to pass the user object into the scope on the User model seems like a backwards approach
You don't have to pass the user model to this scope.
The current user instance is passed automatically to this lambda.
Call it like this:
user = User.find(9001)
user.tasks
Update2:
if possible could you expand this answer to explain what's happening? I'd like to understand it better so I can implement something similar. thanks
Calling has_many :tasks on ActiveRecord class will store a lambda function in some class variable and is just a fancy way to generate a tasks method on its object, which will call this lambda. The generated method would look similar to following pseudocode:
class User
def tasks
#define join query
query = self.class.joins('tasks ON ...')
#execute tasks_lambda on the query instance and pass self to the lambda
query.instance_exec(self, self.class.tasks_lambda)
end
end
I worked out a solution for this. I'm open to any pointers on how I can make this better.
class User < ActiveRecord::Base
def tasks
Task.by_person(self.id)
end
end
class Task < ActiveRecord::Base
scope :completed, -> { where(completed: true) }
belongs_to :owner, class_name: "User", foreign_key: "owner_id"
belongs_to :assignee, class_name: "User", foreign_key: "assignee_id"
def self.by_person(user_id)
where("owner_id = :person_id OR assignee_id = :person_id", person_id: user_id)
end
end
This basically overrides the has_many association but still returns the ActiveRecord::Relation object I was looking for.
So now I can do something like this:
User.first.tasks.completed and the result is all completed task owned or assigned to the first user.
Since Rails 5 you can also do that which is the ActiveRecord safer way:
def tasks
Task.where(owner: self).or(Task.where(assignee: self))
end
My answer to Associations and (multiple) foreign keys in rails (3.2) : how to describe them in the model, and write up migrations is just for you!
As for your code,here are my modifications
class User < ActiveRecord::Base
has_many :tasks, ->(user) { unscope(where: :user_id).where("owner_id = ? OR assignee_id = ?", user.id, user.id) }, class_name: 'Task'
end
class Task < ActiveRecord::Base
belongs_to :owner, class_name: "User", foreign_key: "owner_id"
belongs_to :assignee, class_name: "User", foreign_key: "assignee_id"
end
Warning:
If you are using RailsAdmin and need to create new record or edit existing record,please don't do what I've suggested.Because this hack will cause problem when you do something like this:
current_user.tasks.build(params)
The reason is that rails will try to use current_user.id to fill task.user_id,only to find that there is nothing like user_id.
So,consider my hack method as an way outside the box,but don't do that.
Better way is using polymorphic association:
task.rb
class Task < ActiveRecord::Base
belongs_to :taskable, polymorphic: true
end
assigned_task.rb
class AssignedTask < Task
end
owned_task.rb
class OwnedTask < Task
end
user.rb
class User < ActiveRecord::Base
has_many :assigned_tasks, as: :taskable, dependent: :destroy
has_many :owned_tasks, as: :taskable, dependent: :destroy
end
In result, we can use it so:
new_user = User.create(...)
AssignedTask.create(taskable: new_user, ...)
OwnedTask.create(taskable: new_user, ...)
pp user.assigned_tasks
pp user.owned_tasks

Polymorphic association confusion, get resource through association

I might be totally confused and off target here.
I have an Event model, which has_one a Timeline that belongs_to Event. The Timeline has_many TimelineItems that belong_to Timeline.
def Event
has_one :timeline
end
def Timeline
belongs_to :event
has_many :timeline_items
end
def TimelineItem
belongs_to :timeline
end
create_table :admin_timeline_items do |t|
t.references :admin_timeline
t.references :user
t.references :resource, :polymorphic => true
t.string :method
end
So through from a TimelineItem object I would be able to generate for example this output:
eml created the Post "Hi this is from eml" on [DATE]
The title "Hi this is from eml" would come from the Post object's .title. So eg. this TimelineItem has these settings
item.user = User.where(:username => "eml")
item.resource_type = "Post"
item.resource_id = 1
item.created_at = 27/082...13:37
So the problem is fetching said Post object, or rather the correct syntax for this particular association. Seems terribly simple to me, but I am not finding the information I need to properly write it.
Thanks for your help & time.
Sorry... I was just confusing myself because I had set resource_id in my own test item to a faulty value. My first guess was correct. It is quite simple:
class TimelineItem < ActiveRecord::Base
belongs_to :timeline
belongs_to :user
belongs_to :resource, :polymorphic => true
end
Access resource like so:
TimelineItem.first.resource
=> Post(....)

Resources