I have roles for users implemented with an enum in the user model:
enum role: [:staff, :clinician]
I have a University model with the User belongs_to :university and a University model with has_many :users.
The way that my app will work is that "staff" will belong to a university, but "clinicians" are private practice and therefore do not need to belong to a university and need not select one during signup.
I have my signup form set up to hide the university field if the user selects Clinician, but I want to make sure that my validations are set up to require that any user who selects staff on signup must also select a university and that any user who selects clinician on signup fails validation if they select a university.
Here's the role section of the user signup form:
<%= f.label :role %>
<%= f.select(:role, User.roles.keys.map {|role| [role.titleize,role]}, :include_blank => "Please Select", id: 'user_role') %>
<%= content_tag :div, class: 'js-dependent-fields', data: { 'select-id': 'user_role', 'option-value': 'staff'} do %>
<%= f.label :university_id%>
<%= collection_select( :user, :university_id, University.all, :id, :name, prompt: true) %>
It requires a bit more extra setup but I think pays off in flexibility over time:
Try Single Table Inheritance combined with your enum roles. You'll be able to more easily define separate callbacks, validations, scopes, and associations for your different roles, while inheriting the ones you want them to share in common. For example, you could just make it so only Staff belongs_to :university, and Clinician does not.
# Stripped down schema
create_table "universities", force: :cascade do |t|
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "users", force: :cascade do |t|
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.bigint "university_id"
t.integer "role"
t.index ["university_id"], name: "index_users_on_university_id"
end
# Models
class University < ApplicationRecord
has_many :staffs
end
class User < ApplicationRecord
self.inheritance_column = :role
enum role: { Staff: 0, Clinician: 1 }
end
class Clinician < User
end
class Staff < User
belongs_to :university
end
Staff.first.university # => returns instance of University
Clinician.first.university # => raises NoMethodError
University.first.staffs # => returns collection of Staff objects
University.first.clinicians # => raises NoMethodError
Note that there is no type column. It's been overridden by the role:integer column used for the enum by setting self.inheritance_column = :role. You can interact with the enum roles with the string/symbol representation ("Staff", Staff.new, User.first.Staff?, User.first.Staff!, User.new(role: "Staff") and ActiveRecord takes care of converting that string to the right integer for the database queries.
For example, here's the query for User.where(role: "Staff")
SELECT "users".* FROM "users" WHERE "users"."role" = 0
Staff.all returns the same result but the wording of the query is slightly different
SELECT "users".* FROM "users" WHERE "users"."role" IN (0)
See this question for more detail: Same Model with different columns Rails
You can give a condition to the validates call in your User.rb model:
validates :university, presence: true, if: lambda { self.role.to_s == 'staff' }
# watch out for symbol vs. string in your self.role array
And I think (never done it but I guess that would work) you can do this for the :clinician role:
validates :university, presence: false, if: lambda { self.role.to_s == 'clinician' }
Related
I am creating a webshop with products. I sell digital game accounts.
I have invented a dropdown menu, with f.collection_select, for the user to choose which server the account must belong to.
If the user selects a server, lets say "North America", I want the page to show the accounts which I have that belong to the server "North America". I have set up two models, the server model, this model just contains name:string, and the model has_many :accounts.
Next model I have is account.rb. This model stores all information in regards to account, and belongs_to :server.
I use this form for selection of server, in my views/accounts/index.html.erb
I can see that it works, and shows my servers, which I have created in the database.
<%= form_with do |f| %>
<%= f.collection_select(:server_ids, Server.all, :id, :name, remote: true) %>
<% end %>
class CreateAccounts < ActiveRecord::Migration[6.0]
def change
create_table :accounts do |t|
t.string :name
t.string :password
t.string :BE
t.integer :level
t.integer :price
t.references :server, null: false, foreign_key: true
t.timestamps
end
end
end
class CreateServers < ActiveRecord::Migration[6.0]
def change
create_table :servers do |t|
t.string :name
t.timestamps
end
end
end
This is how the site looks atm with the dropdown:
So here I have selected EUW server. When I change it to NA, I want the page to display the accounts I have in the association Server.find(name:"Europe West (EUW)".accounts
Here is a video, that shows what I am going for:
https://www.youtube.com/watch?v=bZUxS5oMMOw
I have models for Users and Sites with a has_many through relaitonship between them. There is one extra piece of data on the sites_users join table is_default which is type boolean with the intention being to allow each user to have one default site from the list of related sites for that user.
user model
class User < ApplicationRecord
has_many :sites_users
has_many :sites, through: :sites_users
accepts_nested_attributes_for :sites_users, allow_destroy: true
...
end
user factory
factory :user do
sequence(:email) { |n| "user_#{n}#example.com" }
role { Role.find_by(title: 'Marketing') }
image { Rack::Test::UploadedFile.new(Rails.root.join('spec', 'support', 'fixtures', 'user.jpg'), 'image/jpeg') }
factory :super_admin do
role { Role.find_by(title: "Super Admin") }
admin true
end
before :create do |u|
u.sites_users.build(site: site, is_default: true)
end
end
alternate user factory Approach
On the User factory I have also tried this method included below, but cannot find a way to include the is_default: true using this syntax. So I ended up abandoning this method in favor of the above before_create call.
factory :user do
...
site { site }
...
end
I would really appreciate any help anyone could provide. Thank you!
schema info
table: users
t.string "email", default: "", null: false
t.boolean "admin", default: false
t.integer "role_id"
t.string "first_name"
t.string "last_name"
table: sites
t.string "domain", default: "", null: false
t.string "name", default: "", null: false
t.string "logo"
t.string "logo_mark"
table: sites_users
t.bigint "site_id", null: false
t.bigint "user_id", null: false
t.boolean "is_default", default: false
Create a factory for :site_user
factory :site_user, class: SiteUser do
site { site } # you could delete this line and add the site in factory :user
is_default { false } # as specified in your DB
end
Instead of creating the site within the :user factory, create its relation using the nice syntax:
factory :user do
...
sites_users { [FactoryBot.build(:site_user, is_default: true)] }
...
end
It should do the trick!
So when I deal with having extra fields on my joins tables I create custom methods to build those join relations rather than trying to rely on rails built in methods. I have tested this method and it works with factory_bot just fine.
User Model
class User < ApplicationRecord
...
def set_site(site, default = false)
SiteUser.find_or_create_by(
user_id: id,
site_id: site.id
).update_attribute(:is_default, default)
end
end
Note: This code is a block that I use so I would be able to both create new site relations and update the default value via the same method. You could simplify it to just create without checking for existence if you desired.
User Factory
factory :user do
...
# to relate to new site with default true
after(:create) do |u|
u.set_site(create(:site), true)
end
# to relate to existing site with default true
after(:create) do |u|
u.set_site(site_name, true)
end
end
Please let me know if this helps! (or if anyone has a more default railsish way that works just as well, I'd love to hear about it!)
You might want to consider addressing this with a schema change. You could add a default_site_id column to the users table and manage the default site as a separate association of the user model.
In a migration:
add_foreign_key :users, :sites, column: :default_site_id
In User class:
class User < ApplicationRecord
...
belongs_to :default_site, class_name: 'Site'
...
# validate that the default site has an association to this user
validate :default_site_id, inclusion: {in: sites.map(&:id)}, if: Proc.new {default_site_id.present?}
end
This will simplify the association and guarantee no user will ever have multiple site_users records where is_default is true. Setting the default site in a factory should be trivial.
first question for me here! Im trying to assign 'key companies' to my users. These are found in a many-to-many rich join table. On this table there are attributes like new, key, active and so forth. I want to assign companies to users in a long list and for that Im using SimpleForm.
Everything is working excepts that I want to filter out and limit the association relation based on the attributes on the rich relation. I have company relations for each user but not all of them are akey-relation or a new-relation for example. I only want the association being key to show up and not touch the other ones. I also want to set the attribute active to true when Im assigning these companies to the users. My code looks like this now:
user.rb
class User < ActiveRecord::Base
has_many :company_user_relationships
has_many :companies, through: :company_user_relationships
company.rb
class Company < ActiveRecord::Base
has_many :company_user_relationships
has_many :users, through: :company_user_relationships
schema.rb
create_table "company_user_relationships", force: true do |t|
t.integer "company_id"
t.integer "user_id"
t.boolean "key"
t.boolean "active"
t.datetime "last_contacted"
t.string "status_comment"
t.datetime "created_at"
t.datetime "updated_at"
t.integer "status"
t.boolean "new"
end
users_controller.rb
def assign_key_companies
User.update(params[:users].keys, params[:users].values)
redirect_to(:back)
end
view
= form_for :user_companies,
url: assign_key_companies_users_path,
html: {:method => :put} do |f|
- users.each do |user|
= simple_fields_for "users[]", user do |u|
tr
td.col-md-4
= "#{user.first_name} #{user.last_name}"
td.col-md-8
= u.association :companies, label: false, collection: #key_company_candidates,
input_html: {data: {placeholder: " Assign key companies"}, class: 'chosen-select'}
= submit_tag "Save key companies", class: "btn btn-success pull-right"
I basically want to only show user.companies.where(key: true) and the SQLCOMMIT to always put the key-field to true when updating the record.
How can i filter out to only affect the associations I want?
I can think of two ways.
First to filter it at the association level
class User < ActiveRecord::Base
has_many :company_user_relationships, -> { where("company_user_relationships.key" => true) }
Or a where
user.companies.where("company_user_relationships.key" => true)
When you call user.companies it actually doing the join table among all three tables, so you could specify the condition like my example.
I have been reading a lot on the difference between STI and polymorphic associations and decided to use STI:
user.rb
Class User < ActiveRecord::Base
has_many :articles
end
article.rb
Class Article < ActiveRecord::Base
belongs_to :users
end
sport.rb
Class Sport < Article
end
politic.rb
Class Politic < Article
end
food.rb
Class Food < Article
end
create_table "articles", force: :cascade do |t|
t.string "title"
t.string "artwork"
t.integer "user_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.boolean "approved", default: false
t.string "type"
However, upon further reading, this becomes even more complicated. All I am really looking to do is to find some way to sort my articles by type. For example, is it possible that I simply have a string column tag and specify that tag must be either politics, sports, or food?
In this case, use an enum:
#app/models/article.rb
class Article < ActiveRecord::Base
enum article_type: [:sport, :politic, :food] #-> "article_type" (int) column required
end
The only drawback to this would be that you can only assign one enum value to your model; from the use case you've outlined, it seems that's what you need.
The enum will allow you to use the following:
#article = Article.find params[:id]
#article.sport? #-> true
#article.politic? #-> false
#article.food? #-> false
#article.profile_type #-> "sport"
You also get a set of class methods to identify the various objects you need from the db:
#sports_articles = Article.sport #-> collection of "sport" articles
To create an #article through a form, you'll need collection_select:
#app/views/articles/new.html.erb
<%= form_for #article do |f| %>
<%= f.collection_select :profile_type, Article.profile_types, :first, :first %>
<%= f.submit %>
<% end %>
Update
Pagination occurs on data received from the db.
Thus, if you wanted to "include" data in the pagination, you'd just have to make sure you're pulling it from the db. To do this, you'd need to include as many article_types as you want:
#app/models/article.rb
class Article < ActiveRecord::Base
scope :by_type, (types) -> { where(article_type: Array.new(types)) }
end
This will allow you to use the following:
#articles = Article.by_type(:sport, :politic).paginate(page: params [:page], per_page: 12)
As per the docs:
Conversation.where(status: [:active, :archived])
In my Rails app, I only require users to enter email and name upon signup, but then give them the option to provide fuller contact details for their profile. Therefore, I have a User.rb model that has an association with Contact.rb, namely,
User.rb
has_one :contact
Contact.rb
belongs_to :user
Contact.rb has the predictable fields you might expect such as address, postal code etc, but it also stores the province_id for a relation with the Province.rb model, so
Contact.rb
attr_accessible :address, :city, :mobile, :postalcode, :province_id, :user_id
belongs_to :user
belongs_to :province
Province.rb
has_many :contacts
I did it that way (rather than storing the name of the province as a "string" on contact.rb) so that I could more easily (so I thought) categorize users by province.
In the show action of one of the artists_controller, I do the following to check whether the user is trying to sort by province and then call an artists_by_province method that does a search
if params[:province_id]
province = params[:province_id]
province = province.to_i #convert string to integer
#artistsbyprovince = User.artists_by_province(province)
else
#artists = User.where(:sculptor => true)
end
This is the method on the User.rb model that it calls if a province id is passed in
scope :artists_by_province, lambda {|province|
joins(:contact).
where( contact: {province_id: province},
users: {sculptor: true})
}
However it gives me this error:
Could not find table 'contact'
If I make contacts plural
scope :artists_by_province, lambda {|province|
joins(:contacts).
where( contacts: {province_id: province},
users: {sculptor: true})
}
This error
Association named 'contacts' was not found; perhaps you misspelled it?
Can anyone tell me what I'm doing wrong when I'm making this query?
Update: I changed some of the details after posting because my copy and paste had some problems with it
P.S. ignore the fact that I'm searching for a 'sculptor.' I changed the names of the user types for the question.
from schema.rb
create_table "contacts", :force => true do |t|
t.string "firm"
t.string "address"
t.string "city"
t.string "postalcode"
t.string "mobile"
t.string "office"
t.integer "user_id"
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
t.integer "province_id"
end
The problem was fixed by using contact (singular) in the join and contacts (plural) in the where clause. I'm guessing 'contact' (singular) reflects the has_one association between User.rb and Contact.rb, whereas 'contacts' is used in the where clause to represent the name of the table, which is always plural.
User.rb
has_one :contact
scope :artists_by_province, lambda {|province|
joins(:contact).
where( contacts: {province_id: province},
users: {sculptor: true})
}
Can you try the following?
scope :artists_by_province, lambda {|province|
joins(contact: {province: { id: province}}).
where(sculptor: true)
}