Associating a User to model via id - ruby-on-rails

I'm having some difficulty expressing a linkage of a User to my Listing model.
I set up a Rails form when I associated a designer_id (added to listing_params as a private controller method) that would link a selected user to a Listing model when created:
#migration
add_column :listings, :designer_id, :integer
_form.html.erb
<%= collection_select :listing, :designer_id, #account.users, :id, :name, prompt: "Choose..." %>
Checking in the console, the form returned the correct user id as designer_id. Success!
What I need to do now is access the User name using a Listing method, but I'm getting stuck- the issue is primarily making the translation from the id procured to the User referenced:
#listing.rb
def designer
self.designer_id == User.find_by_id(params[:name])
if self.designer_id = nil
return "N/A"
else
return "#{User.name}"
end
Much appreciated!

in the migration if you are on at least rails 4 you can do
add_reference(:listings, :designer)
you may need to do
add_reference(:listings, :designer, :foreign_key => { to_table: 'users'}
other options I often use
add_reference(:listings, :designer, :foreign_key => { to_table: 'users'} index: true, limit: 8)
Migration aside you can do this in the models.
class Listing
belongs_to :designer, class_name: 'User', inverse_of: :listings
end
and in users
class User
has_many :listings, inverse_of: :designer, dependent: :destroy
end
Getting the users name would then be,
listing.designer.name
if you are doing this in a controller you will want to pre-load the association so you are not introducing an n+1 query to a list of listings.

Related

How to structure a has_many association with a dynamic scope?

I have a users table in my db. A user can be either of type 'admin' or 'manager'.
Given the models and schema below, I would like that for each instance of 'manager' user, an 'admin' user could select one, some or all the locations of the tenant that the manager belongs to in order to select which locations the manager can have control over.
My models
class User < ActiveRecord::Base
belongs_to :tenant
class Tenant < ActiveRecord::Base
has_many :users, dependent: :destroy
has_many :locations, dependent: :destroy
class Location < ActiveRecord::Base
belongs_to :tenant, inverse_of: :locations
I've tried two paths
First, trying to establish a scoped has_many association between the User and the Location models. However, I can't wrap my head around structuring this scope so that an 'admin' user could select which locations the 'manager' users can control.
Second, setting up a controlled_locations attribute in the users table. Then I set up some code so that an 'admin' user can select which locations a 'manager' can control, populating its 'controlled_locations' attribute. However, what gets saved in the database (inside the controlled_locations array) is strings instead of instances of locations.
Here's the code that I tried for the second path:
The migration
def change
add_column :users, :controlled_locations, :string, array: true, default: []
end
In the view
= f.input :controlled_locations, label: 'Select', collection: #tenant_locations, include_blank: "Anything", wrapper_html: { class: 'form-group' }, as: :check_boxes, include_hidden: false, input_html: {multiple: true}
In the users controller (inside the update method)
if params["user"]["controlled_locations"]
params["user"]["controlled_locations"].each do |l|
resource.controlled_locations << Location.find(l.to_i)
end
resource.save!
end
What I expect
First of all, I'm not quite sure the second path that I tried is a good approach (storing arrays in the db). So my best choice would be to set up a scoped association if it's possible.
In case the second path is feasible, what I would like to get is something like this. Let's say that logging in an Admin, I selected that the user with ID 1 (a manager) can control one location (Boston Stadium):
user = User.find(1)
user.controlled_locations = [#<Location id: 55, name: "Boston Stadium", created_at: "2018-10-03 12:45:58", updated_at: "2018-10-03 12:45:58", tenant_id: 5>]
Instead, what I get after trying is this:
user = User.find(1)
user.controlled_locations = ["#<Location:0x007fd2be0717a8>"]
Instead of instances of locations, what gets saved in the array is just plain strings.
First, your code is missing the locations association in the Tenant class.
class Tenant < ActiveRecord::Base
has_many :users, dependent: :destroy
has_many :locations
Let's say the variable manager has a User record. Then the locations it can control are:
manager.tenant.locations
If you want, you can shorten this with a delegate statement.
class User < ActiveRecord::Base
belongs_to :tenant
delegate :locations, to: :tenant
then you can call this with
manager.locations
A common pattern used for authorization is roles:
class User < ApplicationRecord
has_many :user_roles
has_many :roles, through: :user_roles
def add_role(name, location)
self.roles << Role.find_or_create_by(name: name, location: location)
end
def has_role?(name, location)
self.roles.exists?(name: name, location: location)
end
end
# rails g model role name:string
# make sure you add a unique index on name and location
class Role < ApplicationRecord
belongs_to :location
has_many :user_roles
has_many :users, through: :user_roles
validates_uniqueness_of :name, scope: :location_id
end
# rails g model user_role user:references role:references
# make sure you add a unique compound index on role_id and user_id
class UserRole < ApplicationRecord
belongs_to :role
belongs_to :user
validates_uniqueness_of :user_id, scope: :role_id
end
class Location < ApplicationRecord
has_many :roles
has_many :users, through: :roles
end
By making the system a bit more generic than say a controlled_locations association you can re-use it for different cases.
Let's say that logging in an Admin, I selected that the user with ID 1
(a manager) can control one location (Boston Stadium)
User.find(1)
.add_role(:manager, Location.find_by(name: "Boston Stadium"))
In actual MVC terms you can do this by setting up roles as a nested resource that can be CRUD'ed just like any other resource. Editing multiple roles in a single form can be done with accepts_nested_attributes or AJAX.
If you want to scope a query by the presence of a role then join the roles and user roles table:
Location.joins(roles: :user_roles)
.where(roles: { name: :manager })
.where(user_roles: { user_id: 1 })
To authenticate a single resource you would do:
class ApplicationController < ActionController::Base
protected
def deny_access
redirect_to "your/sign_in/path", error: 'You are not authorized.'
end
end
class LocationsController < ApplicationController
# ...
def update
#location = Location.find(params[:location_id])
deny_access and return unless current_user.has_role?(:manger, #location)
# ...
end
end
Instead of rolling your own authorization system though I would consider using rolify and pundit.

ActiveAdmin / Formtastic sortable has_many through relationship

I may be missing something fundamental here, but I can't seem to get ActiveAdmin to work with a sortable has_many through relationship, with the ability to create new records.
So given the following models
class User < ActiveRecord::Base
has_many :user_videos
has_many :videos, through: :user_videos
accepts_nested_attributes_for :user_videos
accepts_nested_attributes_for :videos
...
end
class UserVideo < ActiveRecord::Base
belongs_to :user
belongs_to :video
accepts_nested_attributes_for :video
end
class Video < ActiveRecord::Base
has_many :user_videos
has_many :users, through: :user_videos
...
end
(I admit I'm throwing accepts_nested_attributes_for around somewhat in the hopes that something may work)
And Active Admin setup goes something like this (WIP of course):
f.inputs "User" do
f.has_many :user_videos, heading: 'Videos', sortable: :order, allow_destroy: true, new_record: 'New Record' do |v|
v.inputs for: :video do |video|
video.input :video_url
end
end
f.has_many :videos, heading: 'Videos', new_record: 'New Video' do |v|
v.input :video_url
end
end
f.actions
The first has_many on the :user_videos association does not seem to render any inputs. If there are records there, I can see that video.input :video_url is actually returning an li tag with label and input, however nothing gets rendered to the page. For new records the whole v.inputs bit does not get run (do I need to create the child records somehow there first?).
The second has_many will work in that you'll be able to add records, and update existing records, however it's impossible to sort as the order column is on the UserVideos model. I include this more as illustration than anything.
If anyone has any pointers for this, they would be most appreciated. :)
WHOA! I know I am late to the party, but this is a perfect opportunity to utilize the :delegate method!
Your UserVideo class would look something like this
class UserVideo < ActiveRecord::Base
belongs_to :user
belongs_to :video
validates_with VideoValidator
delegate :video_url, :video_url=, to: :video
end
Best of luck!
Since nobody seemed interested in tackling this, I took another approach - rather than trying to get ActiveAdmin / Formtastic to work with the existing model structure, I added getters and setters for the necessary field on the intersection model.
class UserVideo < ActiveRecord::Base
belongs_to :user
belongs_to :video
validates_with VideoValidator
def video_url
self.video = Video.create if video.nil?
self.video.video_url
end
def video_url=(video_url)
self.video = Video.create if video.nil?
self.video.video_url = video_url
# Video url is set via Active Admin, AA will not call save on the video as it does not realise it's changed
self.video.save! if video.present? and video.valid?
end
end
Doing this meant that Active Admin did not need to know about the Video model, and could just operate on the UserVideo model:
f.has_many :user_videos, heading: 'Videos', sortable: :order, allow_destroy: true, new_record: 'New Record' do |v|
v.input :video_url, :hint => (v.object.video.embed_code unless v.object.nil? or v.object.video.nil?)
end
If anyone has an actual solution rather than a work around, I'd love to hear it, but otherwise this is a possible solution for anyone searching for an answer to the same problem.

Validate presence of nested attributes within a form

I have the following associations:
#models/contact.rb
class Contact < ActiveRecord::Base
has_many :contacts_teams
has_many :teams, through: :contacts
accepts_nested_attributes_for :contacts_teams, allow_destroy: true
end
#models/contacts_team.rb
class ContactsTeam < ActiveRecord::Base
belongs_to :contact
belongs_to :team
end
#models/team.rb
class Team < ActiveRecord::Base
has_many :contacts_team
has_many :contacts, through: :contacts_teams
end
A contact should always have at least one associated team (which is specified in the rich join table of contacts_teams).
If the user tried to create a contact without an associated team: a validation should be thrown. If the user tries to remove all of a contact's associated teams: a validation should be thrown.
How do I do that?
I did look at the nested attributes docs. I also looked at this article and this article which are both a bit dated.
For completion: I am using the nested_form_fields gem to dynamically add new associated teams to a contact. Here is the relevant part on the form (which works, but currently not validating that at least one team was associated to the contact):
<%= f.nested_fields_for :contacts_teams do |ff| %>
<%= ff.remove_nested_fields_link %>
<%= ff.label :team_id %>
<%= ff.collection_select(:team_id, Team.all, :id, :name) %>
<% end %>
<br>
<div><%= f.add_nested_fields_link :contacts_teams, "Add Team"%></div>
So when "Add Team" is not clicked then nothing gets passed through the params related to teams, so no contacts_team record gets created. But when "Add Team" is clicked and a team is selected and form submitted, something like this gets passed through the params:
"contacts_teams_attributes"=>{"0"=>{"team_id"=>"1"}}
This does the validations for both creating and updating a contact: making sure there is at least one associated contacts_team. There is a current edge case which leads to a poor user experience. I posted that question here. For the most part though this does the trick.
#custom validation within models/contact.rb
class Contact < ActiveRecord::Base
...
validate :at_least_one_contacts_team
private
def at_least_one_contacts_team
# when creating a new contact: making sure at least one team exists
return errors.add :base, "Must have at least one Team" unless contacts_teams.length > 0
# when updating an existing contact: Making sure that at least one team would exist
return errors.add :base, "Must have at least one Team" if contacts_teams.reject{|contacts_team| contacts_team._destroy == true}.empty?
end
end
In Rails 5 this can be done using:
validates :contacts_teams, :presence => true
If you have a Profile model nested in a User model, and you want to validate the nested model, you can write something like this: (you also need validates_presence_of because validates_associated doesn't validate the profile if the user doesn't have any associated profile)
class User < ApplicationRecord
has_one :profile
accepts_nested_attributes_for :profile
validates_presence_of :profile
validates_associated :profile
docs recommend using reject_if and passing it a proc:
accepts_nested_attributes_for :posts, reject_if: proc { |attributes| attributes['title'].blank? }
http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html
Model Names:
1: approval
2: approval_sirs
Associations:
1: approval
has_many :approval_sirs, :foreign_key => 'approval_id', :dependent => :destroy
accepts_nested_attributes_for :approval_sirs, :allow_destroy => true
2: approval_sirs
belongs_to :approval , :foreign_key => 'approval_id'
In approvals.rb
## nested form validations
validate :mandatory_field_of_demand_report_sirs
private
def mandatory_field_of_demand_report_sirs
self.approval_sirs.each do |approval_sir|
unless approval_sir.marked_for_destruction?
errors.add(:base, "Demand Report Field are mandatory in SIRs' Detail") unless approval_sir.demand_report.present?
end
end
end

After upgrading to Rails 4.1, new polymorphic associations are invalid when saving them along with their parent

After upgrading from Rails 3.2 to 4.1, the following code which used to work is now failing:
in a controller/spec:
post = user.posts.build
post.contacts << contact # contact is a persisted record
post.save! # now fails
I'm basically trying to save the post along with its associated contact, which is supposed to create a contact_publishment record on-the-fly.
The error is on the new contact_publishment record: "Publishable can't be blank"
the model:
class Contact
...
has_many :contact_publishments
...
end
class ContactPublishment
...
belongs_to :publishable, polymorphic: true
belongs_to :contact
validates_uniqueness_of :publishable_id, :scope => [:contact_id, :publishable_type]
validates_presence_of :contact, :publishable
...
end
class Post
...
has_many :contact_publishments, as: :publishable
has_many :contacts, through: :contact_publishments
...
end
In Rails 3.2 owner model has been saved before perform validation nested association, in 4.1 validation before model saved, and because post not saved, validation
class ContactPublishment
validates_presence_of :publishable
does not allow to pass validation (post not saved in db)
For resolve this, you may disable validation in Post model, (validation on ContactPublishment has been called from Contact model)
class Post < ActiveRecord::Base
has_many :contact_publishments, as: :publishable, validate: false
or replace presence validation like this:
class ContactPublishment < ActiveRecord::Base
validates_associated :publishable
change_column :contact_publishments, :publishable_type, :string, null: false
change_column :contact_publishments, :publishable_id, :integer, null: false
or do it through proxy_association
I think the association is not updating because you do not have inverse_of setup between contact and contact_publishment.
From the docs about setting up a :through
If you are going to modify the association (rather than just read from
it), then it is a good idea to set the :inverse_of option on the
source association on the join model. This allows associated records
to be built which will automatically create the appropriate join model
records when they are saved.

User has several skills

I want my users to have many skills. I do have a users and skills database table.
I used has_many_and_belongs_to association in user.rb
has_many :skills
which I am not sure if its correct. And in skill.rb
has_and_belongs_to_many :users
I also created a migration like that:
def change
create_table :user_skills do |t|
t.belongs_to :users
t.belongs_to :skills
end
Is this correct?
So IF this is correct, how do I add new skills to my user? What is the general approach?
What I thought of,
In my users controller on update action I will be updating user's skill and update the user_skills table.
How is this done?
Also How do I iterate through my user_skills table for a specific user? (in view)
Any guidance, resource, tip will be great help for me as its the first time i do something like this in Rails.
Thanks
In Rails, most would prefer to use has_many :through over habtm associations. Here's a guide on how to use it: ActiveRecord guide.
A has_many through association for users and skills would look like this in your relevant models:
class User < ActiveRecord::Base
has_many :user_skills
has_many :skills, through: :user_skills
end
class UserSkill < ActiveRecord::Base
belongs_to :user
belongs_to :skill
end
class Skill < ActiveRecord::Base
has_many :user_skills
has_many :users, through: :user_skills
end
Your migration would look like:
def change
create_table :user_skills do |t|
t.references :user, index: true
t.references :skill, index: true
end
end
The indexes in the migration are for faster look-ups for using the reference_id. It's advisable to do that for all references.
To add new skills to your user, you can refer to this SO answer.
To update a user's skill, you could do this:
#skill = #user.skills.find(params[:skill_id])
#skill.update(skill_params)
To create a user's skill, you could do this:
#user.skills.create(skill_params)
To add a skill to user, you could do this in your update action:
#user.update(user_params)
#app/views/users/edit.html.erb
<%= f.select :skill_ids, Skill.all.collect {|x| [x.name, x.id]}, {}, :multiple => true %>
When working with has_many through, you won't need to go through the user_skills table to get a specific user. You would, however, might need to get a specific user from a skill. To do this:
#skill.users.find(user_id)
Hope that helps!
If you set user to have_and_belong_to_many :skills also then this will work.
To create a new skill for a user do
user.skills.create!{...}
or to associate an existing skill with a user do
user << skill
"In my users controller on update action I will be updating user's skill and update the user_skills table. How is this done?"
user = User.find params[:id]
skills = user.skills
You can then do what you like to users skills
"Also How do I iterate through my user_skills table for a specific user? (in view)"
user.skills.each do |skill|
...
end
for more on HABTM association see http://guides.rubyonrails.org/association_basics.html#has-and-belongs-to-many-association-reference
Forgive me If I get it wrong, try to fill in the gaps but I think you want something that looks like this.
controller
def index
#to fetch all skills associated to users (add where u.id=? to fetch for a single user)
#users = User.select("u.name, s.name").
from("users u, skills s, users_skills us").
where("u.id = us.user_id").
where("s.id = us.skill_id")
end
def new
#user = User.new
#skills = Skill.all
end
def create
#user = User.new(params[:user])
...............................
end
in the create form
<%= form_for #user do |f| %>
<%= f.collection_select(:skill_ids, #skills,:id,:name)%>
<%= f.submit "Save" %>
<% end %>
In order to use HABTM you need a join table named either users_skills or skills_users (not sure it matters). It should contain two integer columns named user_id and skill_id. You should create indices for them as well. In your User model you want has_and_belongs_to_many :skills and in your Skill model you want has_and_belongs_to_many :users.
You need has_and_belongs_to_many on both sides of the realtionship.
class User
has_and_belongs_to_many :skills
class Skill
has_and_belongs_to_many :users
Alternatively (and better, in my opinion) would be to use has_many :through:
class User
has_many :user_skills
has_many :skills, through: :user_skills
class Skill
has_many :user_skills
has_many :users, through: :user_skills

Resources