Many to many and one to many association between same models - ruby

I am creating a simple Sinatra app, using Sequel for my ORM.
Most of the data revolves around users and events where:
An event can have many users, one of which is the "owner".
Users can have many events, one or many of which they "own".
Here is a simplified version of my schema/model definitions:
class User < Sequel::Model(:users)
many_to_many :events
one_to_one :event
end
class Event < Sequel::Model(:events)
many_to_many :users
many_to_one :user
end
# provides a link between users and events
# e.g. event.users or user.events
# I am unsure how necessary this is :)
db.create_table :events_users do
primay_key :id
foreign_key :event_id, :events
foreign_key :user_id, :users
end
This allows me to get the users attached to an event, or the events that a user is attached to, but I am struggling to express the "ownership" of an event. It seems like the following pseudocode would work:
my_user = User.all.first
owned_events = Event.where(user_id = my_user.user_id)
That leads to two questions:
Does the current way i'm using assocations make sense?
How do I express ownership of an event in terms of Sequel's association model?

Maybe something like this:
class Event
many_to_one :owner, :class=>:User
many_to_many :users
end
class User
one_to_many :owned_events, :class=>:Event, :key=>:owner_id
many_to_many :events
end
You'll need to add owned_id field in events table.
Usage:
user = User.all.first
event = Event.new(:title => 'New Event')
events.add_owner(user)
event.save
another_user = User.create(:name => 'Jack')
event.add_user(another_user)

Related

Papertrail tracks create but not delete through association

I have a table of Industries and am keeping tracking of it's competitors which are also industries. This is through a mapping table industry_competitors which has industry_id and competitor_id. I want papertrail to track associations and dissociations of industry competitors.
class Industry < ApplicationRecord
has_many :industry_competitors, dependent: :destroy
has_many :competitors, through: :industry_competitors
end
class IndustryCompetitor < ApplicationRecord
has_paper_trail
belongs_to :industry
belongs_to :competitor, class_name: "Industry"
end
My controller code is as such.
competitors = ::Industry.where(id: params[:competitor_ids])
#industry.competitors = competitors
#industry.save
Every time the entire competitor list is passed. If I try to disassociate a few competitor (by not passing the ids to the controller) from the industry a 'Delete' query is fired.
DELETE FROM `industry_competitors` WHERE `industry_competitors`.`industry_id` = 4559 AND `industry_competitors`.`competitor_id` = 4564
I suspect because activerecord calls 'delete' and not 'destroy' this papertrail callbacks are not triggered hence the changes are not tracked.
If there a way to call delete explicitly (with minimal code changes). Or is there a way for papertrail to track delete?
Adding this patch can get it to work.
module HasManyThroughAssociationPatch
def delete_records(records, method)
method ||= :destroy
super
end
end
ActiveRecord::Associations::HasManyThroughAssociation.prepend(HasManyThroughAssociationPatch)
Credits: https://github.com/westonganger/paper_trail-association_tracking

Configuring the proper join column in Rails Admin

I have two models, which associate with each other through a has_and_belongs_to_many relationship.
class Band < ActiveRecord::Base
has_and_belongs_to_many :stages, association_foreign_key: :stage_number
end
class Stage < ActiveRecord::Base
has_and_belongs_to_many :bands
end
Assume both tables have an id field, and that stage has a stage_name field.
They're related to each other through a table called bands_stages, with a schema that looks similar to this:
create_table :bands_stages, id: false do |t|
t.integer :band_id
t.integer :stage_number
end
My intention is to use Rails Admin to allow us to modify certain fields on the Stage, but every time that runs, I get an SQL error doing so:
column stages.id does not exist
It seems that Rails Admin is picking the wrong column by default to join on. How would I inform Rails Admin that I want it to join on a column that actually exists in my join table?
Note that I can't actually make use of the ID in the stages table. The intention is that only ten stages exist at any given time, denoted by their stage number, but every band can visit each stage. Since an ID would automatically increment, it seems to be safer and more explicit to its intent to leverage the more concrete :stage_number field instead.
I'm sure that it's not a problem of rails admin but habtm association.
To make habtm use the right column in sql primary key must be specified for stage model and foreign key for association.
And it is the only way to make it works right.
class Stage < ActiveRecord::Base
self.primary_key = "stage_number"
has_and_belongs_to_many :bands, foreign_key: :stage_number
end
But I think the best way is to use joint model and has_many/belongs_to because for has_many/belongs_to it's possible to set any column to be used as primary key via :primary_key option.
class BandStageLink < ActiveRecord::Base
self.table_name = "bands_stages"
belongs_to :band
belongs_to :stage, foreign_key: :stage_number, primary_key: :stage_number
end
class Band < ActiveRecord::Base
has_many :band_stage_links
has_many :stages, through: :band_stage_links, foreign_key: :stage_number
end
class Stage < ActiveRecord::Base
has_many :band_stage_links, primary_key: :stage_number, foreign_key: :stage_number
has_many :bands, through: :band_stage_links
end
Update: Note that in this case there is still no need to specify any primary keys for stage table. For instance my migration is:
class CreateStageBandTables < ActiveRecord::Migration
def change
create_table :bands_stages, id: false do |t|
t.integer :band_id
t.integer :stage_number
end
create_table :bands do |t|
t.string :name
end
create_table :stages, id: false do |t|
t.integer :stage_number
t.string :name
end
end
end
I tested both cases for rails 4.2.5 and everything works just fine.
Edit - I did mis-understand the primary key bit, I think the desire was to tell Rails to use different attribute as PK, which should be less problematic than re-purposing the auto-increment-by-default PK ID. In that case, the Stage model should include self.primary_key = "stage_number", and the rest of the details at the bottom of this answer relating to HABTM alongside that. Of course has-many-through would still be my preferred solution here.
I think there's a bigger problem with the models and approach, than Rails Admin.
If I understand what you're trying to do, then you'd also need to turn off auto-increment for the primary key in stages table, to hold arbitrary numbers (representing stage numbers) as primary key IDs. It could end badly very quickly, so I'd advise against it.
If the data is genuinely static (10 stages ever), you could even keep it as a constant in the Band model and scrap Stage completely (unless there's more there), e.g.
class Band < ActiveRecord::Base
POSSIBLE_STAGES = [1, 2, ...]
validates :stage, inclusion: { in: POSSIBLE_STAGES, message: "%{value} is not a stage we know of!" }
end
For a table-based approach, I would suggest has-many-through, it'll save you a lot of pain in the future (even if you don't need additional attributes on the join table, things like nested forms are a little easier to work with than in HABTM). Something like this:
class Band < ActiveRecord::Base
has_many :events
has_many :stages, through :events
# band details go into this model
end
class Event < ActiveRecord::Base
belongs_to :band
belongs_to :stage
# you could later add attributes here, such as date/time of event, used_capacity, attendee rating, and
# even validations such as no more than X bands on any given stage at the same time etc.
end
class Stage < ActiveRecord::Base
has_many :events
has_many :bands, through :events
# stage number/details go into this model
end
The migration for that could look something like this:
create_table :bands do |t|
t.string :bandname
# whatever else
end
create_table :events do |t|
t.belongs_to :band
t.belongs_to :stage
# you could add attributes here as well, e.g. t.integer :used_capacity
end
create_table :stages do |t|
t.integer :number
t.integer :total_capacity
# whatever else
end
As you can see primary key IDs are not touched here at all, and I would always avoid storing business data in Rails' and databases' plumbing of any sort (which is what I consider IDs to be, they're there to ensure relation/integrity of the data in a relational database, as well as nice and consistent mapping to ActiveRecord - all business data should be beside that, in actual attributes, not plumbing used to connect models).
If you still want HABTM and re-purposing of primary ID, then I think Stage should include a foreign_key statement to "advertise" itself to the bands_stages join table as having a custom key name (in bands_stages only), while keeping the association_foreign_key on the Band end to show what you want to query in the join table to reach the other side. The stages table would still utilise id though as its primary key, you'd just want to turn off auto-increment with something like t.integer :id, :options => 'PRIMARY KEY' (might be dependent on the database flavour - and again, I would advise against this).
Your models would look like this:
class Band < ActiveRecord::Base
has_and_belongs_to_many :stages, association_foreign_key: "stage_number"
end
class Stage < ActiveRecord::Base
has_and_belongs_to_many :bands, foreign_key: "stage_number"
end
The connection between bands and bands_stages would be bands.id = bands_stages.band_id, for which many bands_stages.stage_number would be found, and each would be connected to stage via bands_stages.stage_number = stages.id (where stages.id has been re-purposed to represent business data at a likely future peril).
Change the association_foreign_key value to be a string instead of symbol.
class Band < ActiveRecord::Base
has_and_belongs_to_many :stages, association_foreign_key: 'stage_number'
end
class Stage < ActiveRecord::Base
has_and_belongs_to_many :bands, foreign_key: 'stage_number'
end

ruby remove common element collections

I have three classes: User, Subscription and Plan. I want to load all of the Plans that the User doesn't have. What's the best way to do it in Rails?
I have two collections: current_user.subscriptions and Plan.where(active: true)
And i am using mongoid
def dashboard
#plans = Plan.where(active: true)#.each { #plans.delete_if has_current_user_plan subscription.title }
end
def has_current_user_plan(name)
current_user.subscriptions.where(title: name, active: true).exists?
end
class User
has_many :subscriptions
class Subscription
belongs_to :plan
belongs_to :user
class Plan
has_many :subscriptions
AR:
class User < ActiveRecord::Base
has_many :subscriptions
has_many :plans, through: :subscriptions # !!!
end
Plan.where(active: true).where.not(id: current_user.plans)
I'm not really sure what's the best approach for Mongoid because I've never used it. From what I've gather from the documentation, something like the following might work although I'm not running the code.
Plan.where(active: true).not_in(_id: Subscription.where(user_id: current_user.id).pluck(:plan_id))

Attempting to create a database item using the has_one relationship, no exceptions, but still no item

Models:
A User has_one Ucellar
A Ucellar belongs_to User
I have confirmed from multiple sources that these are set up correctly. For posterity, here is the top portion of those two models.
class User < ActiveRecord::Base
has_many :authorizations
has_one :ucellar
validates :name, :email, :presence => true
This is actually the entire Ucellar model.
class Ucellar < ActiveRecord::Base
belongs_to :user
end
Ucellar has a column called user_id, which I know is necessary. The part of my application that creates a user uses the method create_with_oath. Below is the entire User class. Note the second line of the create method.
class User < ActiveRecord::Base
has_many :authorizations
has_one :ucellar
validates :name, :email, :presence => true
def create
#user = User.new(user_params)
#ucellar = #user.create_ucellar
end
def add_provider(auth_hash)
# Check if the provider already exists, so we don't add it twice unless authorizations.find_by_provider_and_uid(auth_hash["provider"], auth_hash["uid"])
Authorization.create :user => self, :provider => auth_hash["provider"], :uid => auth_hash["uid"]
end
end
def self.create_with_omniauth(auth)
user = User.create({:name => auth["info"]["name"], :email => auth["info"]["email"]})
end
private
def user_params
params.require(:user).permit(:name, :email)
end
end
EDIT:
Forgot to summarize the symptoms. On create, the user is in the db, with no exceptions thrown, and nothing to signify that anything went wrong. However, the related ucellar is never created. Per the documentation Here, the create method should create AND save the related ucellar.
It should create ucellar too.
Try to get the error messages after the creation by calling:
raise #user.errors.full_messages.to_sentence.inspect
I'm not sure why this wasn't working, but I ended up just moving this code out of the create action of the user controller, and putting it directly after an action that was creating a user. It solved my issue though. Thanks everyone for your help!

Ruby on Rails: Associations when a user likes a song

I'm trying to figure out the best way to setup my database and models for the following scenario.
A user can like an infinite number of songs.
A song can be liked once by an infinite number of users.
I have these tables:
songs, users, likes etc... Following RoR conventions.
The table named likes has these foreign keys: user_id, song_id. And also a field named 'time' to save a timestamp when the song was liked.
I'm not sure of how to do this, I would like to be able to use code like this in my controllers:
User.find(1).likes.all
This should not return from the likes table, but join the songs table and output all the songs that the user likes.
What are the best practises to achieve this in Ruby on Rails following their conventions?
Unless you need to act specifically on the likes table data, the model itself is probably not necessary. The relationship is easy:
class User < ActiveRecord::Base
has_and_belongs_to_many :songs
end
class Song < ActiveRecord::Base
has_and_belongs_to_many :users
end
This will join through the currently non-existent song_users table. But since you want it to join through likes you can change each one to this:
has_and_belongs_to_many :songs, :join_table => 'likes'
If you want to be able to call User.find(1).likes and get songs, then change the user's version to this:
class User < ActiveRecord::Base
has_and_belongs_to_many :likes, :join_table => 'likes', :class_name => 'Song'
end
And you could change the songs version to something like this:
class Song < ActiveRecord::Base
has_and_belongs_to_many :liked_by, :join_table => 'likes', :class_name => 'User'
end
This will give you Song.find(1).liked_by.all and give you the users (You could keep it to users if you wanted using the first version)
More details on habtm relationships can be found here: http://apidock.com/rails/ActiveRecord/Associations/ClassMethods/has_and_belongs_to_many
Edit to add
If you want to act on the join table for whatever reason (you find yourself needing methods specifically on the join), you can include the model by doing it this way:
class User < ActiveRecord::Base
has_many :songs, :through => :likes
has_many :likes
end
class Like < ActiveRecord::Base
belongs_to :user
belongs_to :song
end
class Song < ActiveRecord::Base
has_many :users, :through => :likes
has_many :likes
end
This will let you do User.find(1).songs.all, but User.find(1).likes.all will give you the join data

Resources