How to update my Users/Products association - ruby-on-rails

I'm trying to create an app to share or give products. So I have two models : User and Product.
A user can have many products, as an owner or as a borrower. A product has only one owner and only one borrower.
First I did something like that :
> rails generate model User name:string
class User
has_many :owned_products, class_name: "Product", foreign_key: "owner_id"
has_many :borrowed_products, class_name: "Product", foreign_key: "borrower_id"
end
> rails generate model Product name:string owner_id:integer borrower_id:integer
class Product
belongs_to :owner, class_name: "User", foreign_key: "owner_id"
belongs_to :borrower, class_name: "User", foreign_key: "borrower_id"
end
I added in my Product controller a security filter that enable the update method only for product's owner. But when I want to change the product's borrower, I have some kind of a problem, because the borrower is never the owner, so the product can not be updated.
So now I'm wondering if I should not take the foreign_key out of my products model, in order to dissociate the update action of a user on his own product, and the update action of a user to borrow a product that don't belongs to him...
> rails generate model User name:string
class User
has_many :properties
has_many :loans
has_many :owned_products, through: :properties
has_many :borrowed_products, through: :loans
end
> rails generate model Property owner_id:integer owned_product_id:integer
class Property
belongs_to :owner, class_name: "User", foreign_key: "user_id"
belongs_to :owned_product, class_name: "Product", foreign_key: "product_id"
end
> rails generate model Loan borrower_id:integer borrowed_product_id:integer
class Loan
belongs_to :borrower, class_name: "User", foreign_key: "user_id"
belongs_to :borrowed_product, class_name: "Product", foreign_key: "product_id"
end
> rails generate model Product name:string
class Product
has_one :property
has_one :loan
has_one :owner, through: :property
has_one :borrower, through: :loan
end
What do you think about it ?

Since borrowed products and owned products are the same type of object with the same list of attributes, but differ only in behavior, I would use single table inheritance for Product.
Migrations:
class CreateUsers < ActiveRecord::Migration
def change
create_table :users do |t|
# ...
t.timestamps
end
end
end
class CreateProducts < ActiveRecord::Migration
def change
create_table :products do |t|
t.integer :ownerable_id
t.string :ownerable_type
# ...
t.timestamps
end
end
end
Models:
class User < ActiveRecord::Base
has_many :products, :as => :ownerable
end
class Product < ActiveRecord::Base
belongs_to :user, :polymorphic => true
end
class OwnedProduct < Product
end
class BorrowedProduct < Product
end
The benefit of this approach is that you can just define the appropriate behavior in each model without asking it if it's "owned" or "borrowed." Just tell your models what to do and leave the decisions up to each object to do the right thing.

Related

Ruby on Rails 6 ActiveRecord associations, one model with multiple references to another model

I am building a Ruby on Rails 6 application where I have a model Archive, and a another model ArchiveAgencies. In the Archive model I must have a sender agency and a receiver agency, which should represent ArchiveAgencies.
After going through the Rails docs, and some StackOverflow QA:
How do I add migration with multiple references to the same model in one table? Ruby/Rails
Defining two references to the same column in another table
Ruby on Rails: two references with different name to the same model
I came up with this approach:
Models
class Archive < ActiveRecord::Base
belongs_to :sender_agency, class_name: ArchiveAgencies, foreign_key: "sender_agency_id"
belongs_to :receiver_agency, class_name: ArchiveAgencies, foreign_key: "receiver_agency_id"
end
class ArchiveAgency < ActiveRecord::Base
has_many :archives, inverse_of: 'sender_agency'
has_many :archives, inverse_of: 'receiver_agency'
end
Migration
class CreateArchiveAgencies < ActiveRecord::Migration[6.0]
def change
create_table :archives do |t|
t.string :name
t.timestamps
end
end
end
class CreateArchives < ActiveRecord::Migration[6.0]
def change
create_table :archives do |t|
t.references :sender_agency, foreign_key: { to_table: :archive_agencies }
t.references :receiver_agency, foreign_key: { to_table: :archive_agencies }
t.timestamps
end
end
end
Is this the best approach for this case?
Would, having two inverse_of statements in the model ArchiveAgency work?
If you declare multiple associations with the same name the last one will overwrite the others.
Instead you need to use unique names for each association and configure them properly:
class ArchiveAgency < ActiveRecord::Base
has_many :archives_as_sender,
inverse_of: :sender_agency,
foreign_key: :sender_agency_id,
class_name: 'Archive'
has_many :archives_as_receiver,
inverse_of: :receiver_agency,
foreign_key: :receiver_agency_id,
class_name: 'Archive'
end
class Archive < ActiveRecord::Base
belongs_to :sender_agency, # foreign_key can be derived from the name
class_name: 'ArchiveAgency', # needs to be a string - not the class itself
inverse_of: :archives_as_sender
belongs_to :receiver_agency,
class_name: 'ArchiveAgency'
inverse_of: :archives_as_receiver
end

Dependent destroy to tables with more than one references

I have a table Atribuition with 2 references from User table.
class Attribuition < ApplicationRecord
belongs_to :user, class_name: 'User', foreign_key: 'user_id'
belongs_to :not_rated, class_name: 'User', foreign_key: 'not_rated_id'
end
The User model:
class User < ApplicationRecord
has_many :attribuitions, dependent: :destroy
end
When i destroy an user marked in not_rated i want it to be destroyed, but it just happens when i destroied an user marked as user_id, then the attribute row is deleted. I wanna make dependent:: destroy to work for many references of same model. That is possible?
My migration is:
class CreateAttribuitions < ActiveRecord::Migration[5.2]
def change
create_table :attribuitions do |t|
t.references :user
t.references :not_rated, index: { unique: true }
t.timestamps
end
end
end
Edit:
First you do following change as rails use convention over configuration
class Attribuition < ApplicationRecord
- belongs_to :user, class_name: 'User', foreign_key: 'user_id'
+ belongs_to :user
end
Changes needed
When you mention has_many :attribuitions, dependent: :destroy by side of User model class_name will be Attribuition and foreign_key will be user_id stored in attributions table.
So if you need to destroy attribuitions related by foreign_key not_rated_id & user_id then you need following changes.
class User < ApplicationRecord
has_many :attribuitions, dependent: :destroy # default foreign_key is user_id
has_many :not_rated_attribuitions, foreign_key: 'not_rated_id', dependent: :destroy
end

Rails Association: 2 occurrences of same model

I have a Consultation model that has a post_consultant and a consultant. Both post_consultant and consultant are references to the Employee model. So you could say:
Model
Class Consultation < ActiveRecord::Base
has_one :employee # for consultant
has_one :employee # for post_consultant
end
Migration
create_table "consultations", force: :cascade do |t|
t.boolean "showed_up"
t.boolean "signed_up"
t.integer "client_id"
t.integer "consultant_id"
t.integer "post_consultant_id"
end
How am I supposed to write that?
Correct Model:
class Consultation < ActiveRecord::Base
belongs_to :consultant, class_name: "Employee", foreign_key: "consultant_id"
belongs_to :post_consultant, class_name: "Employee", foreign_key: "post_consultant_id"
end
Class Consultation < ActiveRecord::Base
belongs_to :consultant, :class_name => "Employee", :foreign_key=> "consultant_id", dependent: :destroy
belongs_to :post_consultant, :class_name=>"Employee", :foreign_key=> "post_consultant_id", dependent: :destroy
end
You can define multiple relation referring to same model.
Class Consultation < ActiveRecord::Base
has_one :consultant, class_name: 'Employee', foreign_key: :consultant_id
has_one :post_consultant, class_name: 'Employee', foreign_key: :post_consultant_id
end
Note: mention whichever foreign key you are using for each association using syntax above.

How to set up an admin user in rails

I have a simple relationship
class School < ActiveRecord::Base
has_and_belongs_to_many :users
end
class User < ActiveRecord::Base
has_and_belongs_to_many :schools
end
A user can be part of many schools but at the same time a user might be the admin of a number of schools. I set up a many-to-many relationship to represent this however I'm not sure how I would distinguish between admins and simple users.
I initially thought of setting a table which has a school_id and a user_id and every entry will represent the school id and the user id of any admins that the school has however I'm not sure how I would represent this in rails or if it's the best way to solve this problem? And if it is, how do I access the table without a model associated to it?
What I mean by what I said above:
school_id user_id
1 3
1 4
Which means that the school with id 1 has 2 admins (3 and 4)
What you are looking for is a more complex many_to_many relationship between school and user called has_many :through. This relationship allows you to have many to many relationship with access to the table that represents the relationship. If you use that relationship, your models should look something like this:
class User < ActiveRecord::Base
has_many :school_roles
has_many :schools, through: :school_roles
end
class SchoolRole < ActiveRecord::Base
belongs_to :school
belongs_to :user
end
class School < ActiveRecord::Base
has_many :school_roles
has_many :users, through: :school_roles
end
And the migrations of those tables would look something like this:
class CreateSchoolRoles < ActiveRecord::Migration
def change
create_table :schools do |t|
t.string :name
t.timestamps null: false
end
create_table :users do |t|
t.string :name
t.timestamps null: false
end
create_table :school_roles do |t|
t.belongs_to :school, index: true
t.belongs_to :user, index: true
t.string :role
t.timestamps null: false
end
end
end
I would suggest to make the "role" field in the "school_roles" migration an integer and then use an enum in the model like so:
class SchoolRole < ActiveRecord::Base
belongs_to :school
belongs_to :user
enum role: [ :admin, :user ]
end
which allows you to add more roles in the future, but it's your call
combining polymorphic association with has_many :through in my opinion is best option.
Let's say you create supporting model SchoolRole, which
belongs_to :user
belongs_to :school
belongs_to :rolable, polymorphic:true
This way:
class School ...
has_many :administrators, :as => :schoolroles
has_many :users, :through => :administators
#school.administrators= [..., ...]
It is quite agile.
#user=#school.administrators.build()
class User
has_many :roles, :as => :rolable
def admin?
admin=false
self.roles.each do |r|
if r.role_type == "administator"
admin=true
break
end
end
admin
end
....

Rails model associations, has_many :through and has_one with the same model

I have two models: User and State. The state model has records for each of the 50 states in the United States.
I would like each User to have two attributes: one "home" state, and many states to "visit".
I know I have to set up some sort of model associations to achieve this, but not sure what the best approach is.
Here's what I have so far, but I know there must be something wrong with have has_many and has_one association to the same model.
#user.rb
class User < ActiveRecord::Base
has_many :visits
has_many :states, :through => :visits
has_one :state
end
#visit.rb
class Visit < ActiveRecord::Base
belongs_to :user
belongs_to :state
end
#state.rb
class State < ActiveRecord::Base
has many :visits
has many :users, :through => :visits
belongs_to :user
end
Any suggestions?
In my opinion what you already have is almost right, except you would store the home state foreign key on the user like thus:
# user.rb
class User < ActiveRecord::Base
belongs_to :state
has_many :visits
has_many :states, through: visits
end
# visit.rb
class Visit < ActiveRecord::Base
belongs_to :user
belongs_to :state
end
# state.rb
class State < ActiveRecord::Base
has_many :visits
has_many :users, through: :visits
end
You would then access the home state like thus:
u = User.first
u.state
And the visited states, like thus:
u = User.first
u.states
For programming clarity, you can rename your relations:
# user.rb
class User < ActiveRecord::Base
belongs_to :home_state, class_name: "State"
has_many :visits
has_many :visited_states, class_name: "State", through: visits
end
# state.rb
class State < ActiveRecord::Base
has_many :residents, class_name: "User"
has_many :visits
has_many :visitors, class_name: "User", through: :visits
end
Your domain model would make more sense:
u = User.first
u.home_state
u.visited_states
s = State.first
s.residents
s.visitors
I expect you'll probably want to store additional information about the visit, so keeping the HMT join table for the Visit model will allow you to do this, rather than going with a HABTM relation. You could then add attributes to the visit:
# xxxxxxxxxxxxxxxx_create_visits.rb
class CreateVisits < ActiveRecord::Migration
def change
create_table :visits do |t|
t.text :agenda
t.datetime commenced_at
t.datetime concluded_at
t.references :state
t.references :user
end
end
end
You can't have a has_many and has_one relationship on a single model, in this case state. One solution is to:
create a static model of states, they do not need to be a database model, they could be a static variable on the state model: US_STATES = {'1' => 'AK', '2' => 'AL', etc} or you could use fixtures to load a table of states into the database (more complicated because you need to use a rake task or the db:seed task to load the fixtures into the db, but nice because you can use active record to manage the model).
then you can provide a home_state_id on the user model that defines the home_state and the visits are simply a join between user_id and the state_id.
I would like each User to have two attributes: one "home" state, and many states to "visit".
In your models, a state may only be home to one user (belongs_to).
The correct semantics would be
class User < AR::Base
belongs_to :home_state, :class_name => "State", :foreign_key => "home_state_id", :inverse_of => :users_living
has_and_belongs_to_many :visited_states, :through => :visits
# ...
end
class State < AR::Base
has_many :users_living, :class_name => "User", :inverse_of => :home_state
# ...
end

Resources