How do I use has_many :through and in_place_edit? - ruby-on-rails

I have two Models: Campaign and Contact.
A Campaign has_many Contacts.
A Contact has_many Campaigns.
Currently, each Contact has a contact.date_entered attribute. A Campaign uses that date as the ate to count down to the different Events that belong_to the Campaign.
However, there are situations where a Campaign for a specific Contact may need to be delayed by X number of days. In this instance, the campaigncontact.delaydays = 10.
In some cases, the Campaign must be stopped altogether for the specific Contact, so for now I set campaigncontact.delaydays = 1. (Are there major problems with that?)
By default, I am assuming that no campaigncontact exists (but not sure how that works?)
So here's what I've tried to do:
class Contact < ActiveRecord::Base
has_many :campaigncontacts
has_many :campaigns, :through => :campaigncontacts
end
class Campaign < ActiveRecord::Base
has_many :campaigncontacts
has_many :contacts, :through => :campaigncontacts
end
script/generate model campaigncontact campaign_id:integer contact_id:integer delaydays:integer
class Campaigncontact < ActiveRecord::Base
belongs_to :campaign
belongs_to :contact
end
So, here's the question: Is the above correct? If so, how do I allow a user to edit the delay of a campaign for a specific Contact.
For now, I want to do so from the Contact View.
This is what I tried:
In the Contact controller (?)
in_place_edit_for :campaigncontact, column.delaydays
And in the View
<%= in_place_editor_field :campaigncontact, :delaydays %>
How can I get it right?

I would add an integer field to your Campaigncontacts resource called days_to_delay_communication_by, since this information relates to the association of a campaign and a contact rather than a contact itself.
in your migration:
def self.up
add_column(:campaigncontacts, :days_to_delay_communication_by, :integer)
end
def self.down
remove_column(:campaigncontacts, :days_to_delay_communication_by)
end
Now you can set that value by:
campaigncontact = Campaigncontacts.find(:first, :conditions => { :campaign_id => campaign_id, :contact_id => contact_id })
campaigncontact.days_to_delay_communication_by = 10
Then in the admin side of your application you can have a controller and a view for campaign communications that lets you set the days_to_delay_communication_by field for campaigncontacts. I can expand on this further for you if you're interested, but I think you get the idea.
Then you'll need to run a background process of some sort (probably a cron job, or use the delayed_job plugin), to find communications that haven't happened yet, and make them happen when the date has passed. You could do this in a rake task like so:
namespace :communications do
namespace :monitor do
desc 'Monitor and send communications for campaigns'
task :erma => :environment do
Rails.logger.info "-----BEGIN COMMUNICATION MONITORING-----"
unsent_communications = Communication.all(:conditions => { :date_sent => nil})
unsent_communications.each do |communication|
Rails.logger.info "**sending communication**"
communication.send if communication.time_to_send < Time.now
Rails.logger.info "**communication sent**"
end
Rails.logger.info "-----END COMMUNICATION MONITORING-----"
end #end erma task
end #end sync namespace
end #end db namespace
Then your cron job would do something like:
cd /path/to/application && rake communications:monitor RAILS_ENV=production
Also, I'd consider changing the name of your join model to something more descriptive of it's purpose, for instance memberships, a campaign has many memberships and a contact has many memberships. Then a membership has a days_to_delay_communication field.

A good way to do this is use a "fake" attribute on your Contact model like so:
class Contact < ActiveRecord::Base
has_many :campaigncontacts
has_many :campaigns, :through => :campaigncontacts
attr_accessor :delay
def delay #edit
self.campaigncontacts.last.delaydays
end
def delay=(val)
self.campaigncontacts.each do |c|
c.delaydays = val
end
end
end
Then just set the in_place_editor for this fake field:
in_place_edit_for :contact, :delay
and
<%= in_place_editor_field :contact, :delay %>
I'm not sure I understood exactly what you wanted to accomplish, but I hope this at least points you into the right direction.

Related

How to detect changes in has_many through association?

I have the following models.
class Company < ApplicationRecord
has_many :company_users
has_many :users, :through => :company_users
after_update :do_something
private
def do_something
# check if users of the company have been updated here
end
end
class User < ApplicationRecord
has_many :company_users
has_many :companies, :through => :company_users
end
class CompanyUser < ApplicationRecord
belongs_to :company
belongs_to :user
end
Then I have these for the seeds:
Company.create :name => 'Company 1'
User.create [{:name => 'User1'}, {:name => 'User2'}, {:name => 'User3'}, {:name => 'User4'}]
Let's say I want to update Company 1 users, I will do the following:
Company.first.update :users => [User.first, User.second]
This will run as expected and will create 2 new records on CompanyUser model.
But what if I want to update again? Like running the following:
Company.first.update :users => [User.third, User.fourth]
This will destroy the first 2 records and will create another 2 records on CompanyUser model.
The thing is I have technically "updated" the Company model so how can I detect these changes using after_update method on Company model?
However, updating an attribute works just fine:
Company.first.update :name => 'New Company Name'
How can I make it work on associations too?
So far I have tried the following but no avail:
https://coderwall.com/p/xvpafa/rails-check-if-has_many-changed
Rails: if has_many relationship changed
Detecting changes in a rails has_many :through relationship
How to determine if association changed in ActiveRecord?
Rails 3 has_many changed?
There is a collection callbacks before_add, after_add on has_many relation.
class Project
has_many :developers, after_add: :evaluate_velocity
def evaluate_velocity(developer)
#non persisted developer
...
end
end
For more details: https://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#label-Association+callbacks
You can use attr_accessor for this and check if it changed.
class Company < ApplicationRecord
attr_accessor :user_ids_attribute
has_many :company_users
has_many :users, through: :company_users
after_initialize :assign_attribute
after_update :check_users
private
def assign_attribute
self.user_ids_attribute = user_ids
end
def check_users
old_value = user_ids_attribute
assign_attribute
puts 'Association was changed' unless old_value == user_ids_attribute
end
end
Now after association changed you will see message in console.
You can change puts to any other method.
I have the feelings you are asking the wrong question, because you can't update your association without destroy current associations. As you said:
This will destroy the first 2 records and will create another 2 records on CompanyUser model.
Knowing that I will advice you to try the following code:
Company.first.users << User.third
In this way you will not override current associations.
If you want to add multiple records once try wrap them by [ ] Or ( ) not really sure which one to use.
You could find documentation here : https://guides.rubyonrails.org/association_basics.html#has-many-association-reference
Hope it will be helpful.
Edit:
Ok I thought it wasn't your real issue.
Maybe 2 solutions:
#1 Observer:
what I do it's an observer on your join table that have the responsability to "ping" your Company model each time a CompanyUser is changed.
gem rails-observers
Inside this observer call a service or whatever you like that will do what you want to do with the values
class CompanyUserObserver < ActiveRecord::Observer
def after_save(company_user)
user = company_user.user
company = company_user.company
...do what you want
end
def before_destroy(company_user)
...do what you want
end
end
You can user multiple callback in according your needs.
#2 Keep records:
It turn out what you need it keep records. Maybe you should considerate use a gem like PaperTrail or Audited to keep track of your changes.
Sorry for the confusion.

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

How do I optimize this chain of associations?

I have an Event model with a many-to-many association with a Service model
A user can create an event and choose what services are tagged to that event.
A user can subscribe to a service, and when an event gets created the user should be notified if the user has subscribed to a service that was tagged in that event.
In addition, the User model has a has_many association to an Email model.
I'd like to be able to get an array of all the email addresses so I can send a notification to the subscribers.
Here's what I have:
class Event < ActiveRecord::Base
has_many :event_services, :dependent => :destroy
has_many :services, :through => :event_services
def recipients
recipients = services.each_with_object(arr = []) do |service|
service.users.each do |user|
user.emails.each do |email|
arr << email.address
end
end
end
end
recipients.uniq
end
This works, but its super ugly and not very efficient. How would I go about optimizing this?
Here's my Email model:
class Email < ActiveRecord::Base
attr_accessible :address, :user_id
belongs_to :user
end
It would be more efficient with a single SQL request The following request, using multiple joins, should work:
def recipients
Email.joins(:user => {:services => :event_services}).where(:event_services => {:event_id => self.id}).pluck(:address).uniq
end

Active Relation: Retrieving records through an association?

I have the following models:
class User < ActiveRecord::Base
has_many :survey_takings
end
class SurveyTaking < ActiveRecord::Base
belongs_to :survey
def self.surveys_taken # must return surveys, not survey_takings
where(:state => 'completed').map(&:survey)
end
def self.last_survey_taken
surveys_taken.maximum(:position) # that's Survey#position
end
end
The goal is to be able to call #user.survey_takings.last_survey_taken from a controller. (That's contrived, but go with it; the general goal is to be able to call class methods on #user.survey_takings that can use relations on the associated surveys.)
In its current form, this code won't work; surveys_taken collapses the ActiveRelation into an array when I call .map(&:survey). Is there some way to instead return a relation for all the joined surveys? I can't just do this:
def self.surveys_taken
Survey.join(:survey_takings).where("survey_takings.state = 'completed'")
end
because #user.survey_takings.surveys_taken would join all the completed survey_takings, not just the completed survey_takings for #user.
I guess what I want is the equivalent of
class User < ActiveRecord::Base
has_many :survey_takings
has_many :surveys_taken, :through => :survey_takings, :source => :surveys
end
but I can't access that surveys_taken association from SurveyTaking.last_survey_taken.
If I'm understanding correctly you want to find completed surveys by a certain user? If so you can do:
Survey.join(:survey_takings).where("survey_takings.state = 'completed'", :user => #user)
Also it looks like instead of:
def self.surveys_taken
where(:state => 'completed').map(&:survey)
end
You may want to use scopes:
scope :surveys_taken, where(:state => 'completed')
I think what I'm looking for is this:
class SurveyTaking < ActiveRecord::Base
def self.surveys_taken
Survey.joins(:survey_takings).where("survey_takings.state = 'completed'").merge(self.scoped)
end
end
This way, SurveyTaking.surveys_taken returns surveys taken by anyone, but #user.survey_takings.surveys_taken returns surveys taken by #user. The key is merge(self.scoped).
Waiting for further comments before I accept..

Alternatives to use polymorphism in Ruby on Rails

I'm currently writing some intranet web application where people could submit to admins requests for adding different resources. The example requests would be:
installing programs, in this case user will select which program he wants installed
increasing quota, in this case user will just enter the amount of disk space he needs or maybe he will select the predefined quantities - 1GB, 10GB etc...
create new email alias, in this case user will just type the alias.
...
I was thinking about having just one model UserRequests with the reference to the sender and
two optional attributes one would be reference_id that would refefrence to other tables (for
example the Program that he wants installed) and another would be used for free type fields
like email alias or quota.
So my problem is that based on the type of the request the model should contain either:
reference to other table
integer data
string data
Based on the type of the request the given action should be taken - probably email alias
could be added from rails but the application on users computer will be installed by hand.
Does anyone had similar problem? Do you think using polymorphism for this kind of stuff is a good idea? Do you have any suggestions on how to organize data in the tables?
Single Table Inheritance! This way you can have each type of request have custom validations, while still having every request live in the same table.
class CreateUserRequests < ActiveRecord::Migration
def self.up
create_table :user_requests do |t|
t.string :string_data, :type
t.integer :user_id, :integer_data
t.timestamps
end
end
def self.down
drop_table :user_requests
end
end
class UserRequest < ActiveRecord::Base
belongs_to :user
end
class EmailAliasRequest < UserRequest
validates_presence_of :string_data
validates_format_of :string_data, :with => EMAIL_REGEX
end
class ProgramInstallRequest < UserRequest
belongs_to :program, :class_name => "Program", :foreign_key => "integer_data"
validates_presence_of :integer_data
end
class QuotaIncreaseRequest < UserRequest
validates_presence_of :string_data
validates_inclusion_of :string_data, :in => %w( 1GB 5GB 10GB 15GB )
end
And of course, alias your string_data and integer_data to email or whatnot to make your other code have a little more meaning. Let the model be the little black box that hides it all away.
I would use polymorphic associations, which let a model belong to more than one other model using a single association. Something like this:
class AdminRequest < ActiveRecord::Base
belongs_to :user
belongs_to :requestable, :polymorphic => true
end
class EmailAlias < ActiveRecord::Base
has_many :admin_requests, :as => :requestable
end
class ProgramInstall < ActiveRecord::Base
has_many :admin_requests, :as => :requestable
end
class QuotaIncrease < ActiveRecord::Base
has_many :admin_requests, :as => :requestable
end
As ever, Ryan Bates has an excellent Railscast on the subject.

Resources