I'm trying to create an event platform using MongoDB as the db. I want a many-to-many relationship between Events and Users. The thing is, I want there to be properties in the relationship (e.g., Users can either be confirmed or unconfirmed for a specific Event). I realize this would be ideally suited for an RDBMS, but I'm using MongoDB for reasons that I'm taking advantage elsewhere and I would prefer to continue using it.
What I would like is for each Event to embed many Guests, which belong to Users. That way, I can see which users are attending an event quickly and with only one query. However, I would also like to see which Events a User is attending quickly, so I would like each User to have an array of Event ids.
Here is a code summary.
# user of the application
class User
has_many :events
end
# event that users can choose to attend
class Event
embeds_many :guests
has_many :users, :through => :guests # Won't work
end
# guests for an event
class Guest
field :confirmed?, type: Boolean
embedded_in :event
belongs_to :user
end
# Ideal use pattern
u = User.create
e = Event.create
e.guests.create(:user => u, :confirmed => true)
With the ideal use pattern, e has a Guest with a reference to u and u has a reference to e.
I know the has_many :through line won't work. Any suggestions as to how to get similar functionality? I was thinking of using an after_create callback in Guest to add a reference to the Event in User, but that seems pretty hacky.
Maybe I've gone down the wrong path. Suggestions? Thanks.
You can just store the event ids in a array on the user.
You have to manage the array when the event changes or the user is removed from the event for some reason. But that is the trade off.
User.events can then be found with a single db call.
Look at observers to manage the association.
I ended up using callbacks in the models to accomplish I wanted. Here's what it looks like.
Edit: I just saw nodrog's answer. Yeah, using observers would probably have been neater, I didn't know about them. Thanks!
# user of the application
class User
has_and_belongs_to_many :events, inverse_of: nil, dependent: :nullify
end
# event that users can choose to attend
class Event
embeds_many :guests
index 'guests.user_id', unique: true
before_destroy :cleanup_guest_references
def cleanup_guest_references
self.guests.each do |guest|
guest.destroy
end
end
end
# guests for an event
class Guest
field :confirmed?, type: Boolean
embedded_in :event, :inverse_of => :guests
belongs_to :user
after_create :add_event_for_user
before_destroy :remove_event_for_user
private
def add_event_for_user
self.user.events.push(self.event)
end
def remove_event_for_user
self.user.events.delete self.event
self.user.save
end
end
Related
In a small app I am building, I have a controller that creates an exchange. When a user creates an exchange they are simultaneously the organizer of the exchange and a participant in the exchange. Participants are tracked by a join table that joins a user_id and an exchange_id. Organizers are tracked by a foreign user_id key in the exchange table.
I am trying to figure out where to put the code that will automatically create a new membership record for the organizer of the exchange. Should I put this in the exchange_controller's create action itself, or in an after_filter triggered by the create action? Or maybe somewhere else? Part of the problem is that I could not find any good examples of proper after_filter use (guides.rubyonrails.org only had sparse mention of it), so any links pointing in the correct direction would be appreciated as well.
Here is relevant model code:
app/models/user.rb:
# Returns array of exchanges user is participating in
has_many :participations,
:through => :memberships,
:source => :exchange
# Returns array of exchanges user has organized
has_many :organized_exchanges,
:foreign_key => :organizer_id,
:class_name => "Exchange"
app/models/membership.rb:
class Membership < ActiveRecord::Base
attr_accessible :exchange_id, :user_id, :role
belongs_to :exchange
belongs_to :user
end
app/modles/exchange.rb:
belongs_to :organizer,
:foreign_key => :organizer_id,
:class_name => "User"
has_many :memberships, :dependent => :destroy
has_many :participants,
:through => :memberships,
:source => :user
And here is the relevant controller code:
app/controllers/exchanges_controller.rb:
def create
#exchange = Exchange.new(params[:exchange])
#exchange.organizer_id = current_user.id
if #exchange.save
redirect_to exchange_path(#exchange.id)
else
render 'new'
end
end
after_filter is a completely different thing in this context. It is called when your view is completely processed and so you want to call some action to do something.
You can use after_create callback that is triggered when a record is created in the database.
In your case, a user is creating an exchange and so after the exchange is created, the after_create callback is triggered and you can apply your functionality over there to make the current user who created the exchange to be a participant.
The way to write in a model is like this:
after_create :do_something
def do_something
something.do!
end
Note: It is not good to use after_save here because it is triggered every time you save a record or even if you update a record.
There is a nice SO post that clearly tells you the difference between the after_create and after_save.
See this SO post for the difference between the two.
More on the callbacks is here.
What is the easiest way to implement soft-deletes on has_many through association?
What I want is something like this:
class Company > ActiveRecord::Base
has_many :staffings
has_many :users, through: :staffings, conditions: {staffings: {active: true}}
end
I want to use Company#users the following way:
the Company#users should be a normal association so that it works with forms and doesn't break existing contract.
when adding a user to the company, a new Staffing with active: true is created.
when removing a user from a company, the existing Staffing is updated active: false (currently it just gets deleted).
when adding a previously removed user to the company (so that Staffing#active == false) the Staffing is updated to active: true.
I thought about overriding the Company#users= method, but it really isn't good enough since there are other ways of updating the associations.
So the question is: how to achieve the explained behaviour on the Company#users association?
Thanks.
has_many :through associations are really just syntactic sugar. When you need to do heavy lifting, I would suggest splitting up the logic and providing the appropriate methods and scopes. Understanding how to override callbacks is useful for this sort of thing also.
This will get you started with soft deletes on User and creating Staffings after a User
class Company < ActiveRecord::Base
has_many :staffings
has_many :users, through: :staffings, conditions: ['staffings.active = ?', true]
end
class Staffing < ActiveRecord::Base
belongs_to :company
has_one :user
end
class User < ActiveRecord::Base
belongs_to :staffing
# after callback fires, create a staffing
after_create {|user| user.create_staffing(active: true)}
# override the destroy method since you
# don't actually want to destroy the User
def destroy
run_callbacks :delete do
self.staffing.active = false if self.staffing
end
end
end
I have looked through the Ruby on Rails guides and I can't seem to figure out how to prevent someone from deleting a Parent record if it has Children. For example. If my database has CUSTOMERS and each customer can have multiple ORDERS, I want to prevent someone from deleting a customer if it has any orders in the database. They should only be able to delete a customer if it has no orders.
Is there a way when defining the association between models to enforce this behavior?
class Customer < ActiveRecord::Base
has_many :orders, :dependent => :restrict # raises ActiveRecord::DeleteRestrictionError
Edit: as of Rails 4.1, :restrict is not a valid option, and instead you should use either :restrict_with_error or :restrict_with_exception
Eg.:
class Customer < ActiveRecord::Base
has_many :orders, :dependent => :restrict_with_error
You could do this in a callback:
class Customer < ActiveRecord::Base
has_many :orders
before_destroy :check_for_orders
private
def check_for_orders
if orders.count > 0
errors.add_to_base("cannot delete customer while orders exist")
return false
end
end
end
EDIT
see this answer for a better way to do this.
Try using filters to hook in custom code during request processing.
One possibility would be to avoid providing your users a link to deletion in this scenario.
link_to_unless !#customer.orders.empty?
Another way would be to handle this in your controller:
if !#customer.orders.empty?
flash[:notice] = "Cannot delete a customer with orders"
render :action => :some_action
end
Or, as Joe suggests, before_filters could work well here and would probably be a much more DRY way of doing this, especially if you want this type of behavior for more models than just Customer.
I'm working on this record insert/delete I'm not sure what the syntax is to perform the query.
I have a user model and an event model. I have created a joining table called Personal that stores the user_id, and event_id of any events that the users like.
I created an "Add" method in my events controller so whenever someone clicks it run to that and perform the create logic I'm trying to develop now.The action is tied to a extra column I added to the grid displaying all the events.
The user model =>
has_many :personals
The event model =>
has_many :personals
The personal model =>
belongs_to :user
belongs_to :events
I thought it would be something like =>
#user = User.find(session[:user_id])
#event = Event.find(params[:id])
# Personal.new = User.Event?
can anyone help?
If you're using a has_and_belongs_to_many association, which would be unfortunate, removing the associated links can be tricky as there's no identifier for each link.
Using a has_many :through relationship is much easier to maintain and will allow you to do simple things like:
class User < ActiveRecord::Base
has_many :user_events
has_many :events,
:through => :user_events
end
#user.events.delete(#event)
This doesn't remove the Event itself, that'd require an Event#destroy call, but the join record that links the two.
Using ActiveRecord, I have an object, Client, that zero or more Users (i.e. via a has_many association). Client also has a 'primary_contact' attribute that can be manually set, but always has to point to one of the associated users. I.e. primary_contact can only be blank if there are no associated users.
What's the best way to implement Client such that:
a) The first time a user is added to a client, primary_contact is set to point to that user?
b) The primary_contact is always guaranteed to be in the users association, unless all of the users are deleted? (This has two parts: when setting a new primary_contact or removing a user from the association)
In other words, how can I designate and reassign the title of "primary contact" to one of a given client's users? I've tinkered around with numerous filters and validations, but I just can't get it right. Any help would be appreciated.
UPDATE: Though I'm sure there are a myriad of solutions, I ended up having User inform Client when it is being deleted and then using a before_save call in Client to validate (and set, if necessary) its primary_contact. This call is triggered by User just before it is deleted. This doesn't catch all of the edge cases when updating associations, but it's good enough for what I need.
My solution is to do everything in the join model. I think this works correctly on the client transitions to or from zero associations, always guaranteeing a primary contact is designated if there is any existing association. I'd be interested to hear anyone's feedback.
I'm new here, so cannot comment on François below. I can only edit my own entry. His solution presumes user to client is one to many, whereas my solution presumes many to many. I was thinking the user model represented an "agent" or "rep" perhaps, and would surely manage multiple clients. The question is ambiguous in this regard.
class User < ActiveRecord::Base
has_many :user_clients, :dependent => true
has_many :clients, :through => :user_client
end
class UserClient < ActiveRecord::Base
belongs_to :user
belongs_to :client
# user_client join table contains :primary column
after_create :init_primary
before_destroy :preserve_primary
def init_primary
# first association for a client is always primary
if self.client.user_clients.length == 1
self.primary = true
self.save
end
end
def preserve_primary
if self.primary
#unless this is the last association, make soemone else primary
unless self.client.user_clients.length == 1
# there's gotta be a more concise way...
if self.client.user_clients[0].equal? self
self.client.user_clients[1].primary = true
else
self.client.user_clients[0].primary = true
end
end
end
end
end
class Client < ActiveRecord::Base
has_many :user_clients, :dependent => true
has_many :users, :through => :user_client
end
Though I'm sure there are a myriad of solutions, I ended up having User inform Client when it is being deleted and then using a before_save call in Client to validate (and set, if necessary) its primary_contact. This call is triggered by User just before it is deleted. This doesn't catch all of the edge cases when updating associations, but it's good enough for what I need.
I would do this using a boolean attribute on users. #has_one can be used to find the first model that has this boolean set to true.
class Client < AR::B
has_many :users, :dependent => :destroy
has_one :primary_contact, :class_name => "User",
:conditions => {:primary_contact => true},
:dependent => :destroy
end
class User < AR::B
belongs_to :client
after_save :ensure_only_primary
before_create :ensure_at_least_one_primary
after_destroy :select_another_primary
private
# We always want one primary contact, so find another one when I'm being
# deleted
def select_another_primary
return unless primary_contact?
u = self.client.users.first
u.update_attribute(:primary_contact, true) if u
end
def ensure_at_least_one_primary
return if self.client.users.count(:primary_contact).nonzero?
self.primary_contact = true
end
# We want only 1 primary contact, so if I am the primary contact, all other
# ones have to be secondary
def ensure_only_primary
return unless primary_contact?
self.client.users.update_all(["primary_contact = ?", false], ["id <> ?", self.id])
end
end