RoR 4 belongs_to foreign_key not checked - ruby-on-rails

I'm a newbie in RoR and I'm trying to make a simple experiment. I want to have a User model and a Role model. Each user a single Role, but a single Role may refer to multiple Users.
So, here are my models:
class Role < ActiveRecord::Base
has_many :user
end
class User < ActiveRecord::Base
has_one :role
end
And here are migrations:
class CreateRoles < ActiveRecord::Migration
def change
create_table :roles do |t|
t.string :name, null: false, index: true, unique: true, limit: 16
t.integer :permissions, null: false
end
end
end
class CreateUsers < ActiveRecord::Migration
def change
create_table :users do |t|
t.string :email, null: false, index: true, unique: true, limit: 128
t.string :password_digest, null: false, limit: 40
t.string :password_salt, null: false, limit: 40
t.string :screen_name, default: '', limit: 32
t.belongs_to :role, index: true, foreign_key: true
t.timestamps null: false
end
end
end
What I want is to make it raise an exception when I try to connect user with a role that does not exist:
user = User.create(email: 'user#example.com', password_digest: 'pwd_dig',
password_salt: 'salt', role_id: 10)
Unfortunately this works and the new user is created, no matter that the role with id 10 does not exist.
So, how can I force foreign_key check here?
And another question about that. If I try to do like this:
user = User.create(email: 'user#example.com', password_digest: 'pwd_dig',
password_salt: 'salt', role: role)
it raises an exception because role does not have attribute user_id. There is no way to do like this, does it?

I'm a newbie in RoR
Welcome!!
#app/models/role.rb
class Role < ActiveRecord::Base
has_many :users
end
#app/models/user.rb
class User < ActiveRecord::Base
belongs_to :role
validates :role_id, presence: true, on: :create
validate :check_role, on: :create
private
def check_role
errors.add(:role_id, "Role doesn't exist") unless Role.exists? role_id
end
end
This will allow you the following:
#app/controllers/users_controller.rb
class UsersController < ApplicationController
def new
#user = User.new
#roles = Role.all
end
def create
#user = User.new user_params
#user.save
end
private
def user_params
params.require(:user).permit(:email, :etc, :role)
end
end
#app/views/users/new.html.erb
<%= form_for #user do |f| %>
<%= f.email_field :email %>
<%= f.collection_select :role_id, #roles, :id, :name %>
<%= f.submit %>
<% end %>
Because you're new, I'll give you some info:
1. Association
Your association is slightly incorrect - you'll be best using a belongs_to/has_many relationship:
The belongs_to will assign the foreign_key in your User model, allowing you to both reference and validate against it.
-
2. Custom validator
Secondly, you'll be able to use a validation to check whether the role has been set correctly.
Each time a Rails model is saved, it calls a series of validators you've set -- allowing you to call such functionality as checking whether a role exists.

What you can do is add a custom validation:
class User < ActiveRecord::Base
belongs_to :role
validate :existance_of_role
private
def existance_of_role
if role_id && !Role.exists?(role_id)
errors.add(:role, "must already exist.")
end
end
end
Also you need to use belongs_to :role. has_one would place the foreign_key in the relationship on the Role model instead of on the user.
http://requiremind.com/differences-between-has-one-and-belongs-to-in-ruby-on-rails/
If the user must have a role.
Also if you want to enforce on the database level that the user has a role you would add a NOT NULL constraint to users.role_id.
Run: rails g migration AddNotNullConstraintToUsers
And then edit the migration file created:
class AddNotNullConstraintToUsers < ActiveRecord::Migration
def change
change_column_null(:users, :role_id, false)
end
end
After running the migration you can change the validation so that it will add an error also when the role_id is nil.
private
def existance_of_role
errors.add(:role, "must already exist.") unless Role.exists?(role_id)
end
You could do the same thing with validates_presence_of :role but that will not enforce that the Role is pre-existing.

You can add this validator at your User model
class User < ActiveRecord::Base
has_one :role
validate :role_id_exists
def role_id_exists
return false if Role.find_by_id(self.role_id).nil?
end
end

Related

How to call database attributes in models in Ruby on Rails

I am making a project with ruby on rails, i have recipes table in the db recipes table has user_id etc. Also i have a user table and in this table i have an attribute called n_days. I am using sidekiq to do some background processes (for automatically delete in n days) In models(recipe.rb) i need to reach user_id(in recipe table) and n_days(in user table) but i do not know how to access them.
i tried this code but i get NoMethodError
scope :recent, -> { where('created_at <= :a', a: Time.now - User.find_by(id: :user_id.to_s.to_i).n_days.days) }
I think it would be simplier to set deleted_at datetime field in your sidekiq worker, it will make easier to compare datetimes. However, if you want to use n_days integer column, you can write such code:
class Recipe < ApplicationRecord
belongs_to :user
validates :name, :text, presence: true
# scope :recent,
# -> {
# joins(:user)
# .where.not(users: { deleted_at: nil })
# .where('recipes.created_at <= users.deleted_at')
# }
# For SQLite database
scope :recent,
-> {
joins(:user)
.where("recipes.created_at <= datetime('now', '-' || ifnull(users.n_days, 0) || ' days')")
}
end
class User < ApplicationRecord
has_many :recipes, dependent: :destroy
validates :first_name, :last_name, presence: true
end
class CreateUsers < ActiveRecord::Migration[5.2]
def change
create_table :users do |t|
t.string :first_name
t.string :last_name
t.integer :n_days
t.datetime :deleted_at
t.timestamps
end
end
end
class CreateRecipes < ActiveRecord::Migration[5.2]
def change
create_table :recipes do |t|
t.string :name
t.text :text
t.references :user, foreign_key: true
t.timestamps
end
end
end
# seeds.rb
User.destroy_all
10.times do |t|
r = rand(15)
u = User.create!(
first_name: SecureRandom.uuid,
last_name: SecureRandom.uuid,
n_days: rand(15),
deleted_at: Time.now - r.days
)
5.times do |t|
u.recipes.create!(
name: SecureRandom.uuid,
text: SecureRandom.uuid,
created_at: 60.days.ago
)
end
end
p Recipe.recent.inspect

Rails has_many :through association: Updating all 3 models at the same time

This question follows up on Rails has_many :through association: save instance into join table and I am restating things here for more clarity.
In our Rails app, there are 3 models:
class User < ActiveRecord::Base
has_many :administrations, dependent: :destroy
has_many :calendars, through: :administrations
end
class Administration < ActiveRecord::Base
belongs_to :user
belongs_to :calendar
end
class Calendar < ActiveRecord::Base
has_many :administrations, dependent: :destroy
has_many :users, through: :administrations
end
And here are the corresponding migrations:
class CreateUsers < ActiveRecord::Migration
def change
create_table :users do |t|
t.string :first_name
t.string :last_name
t.string :email
t.integer :total_calendar_count
t.integer :owned_calendar_count
t.timestamps null: false
end
end
end
class CreateAdministrations < ActiveRecord::Migration
def change
create_table :administrations do |t|
t.references :user, index: true, foreign_key: true
t.references :calendar, index: true, foreign_key: true
t.string :role
t.timestamps null: false
end
end
end
class CreateCalendars < ActiveRecord::Migration
def change
create_table :calendars do |t|
t.string :name
t.timestamps null: false
end
end
end
Here is what we are trying to accomplish:
When a logged in user (current_user) creates a calendar, we should:
Create a new #calendar and save it to the Calendar table
Assign the "Creator" role to the user (current_user) for this newly created calendar through the Role column in the Administration table
Increment the total_calendar_count and the owner_calendar_count columns of the User table
In order to do that, we think we need to work on calendars#create.
In the CalendarsController, we already have the following code:
def create
#calendar = current_user.calendars.create(calendar_params)
if #calendar.save
flash[:success] = "Calendar created!"
redirect_to root_url
else
render 'static_pages/home'
end
end
And we collect data from users through the following _calendar_form.html.erb form:
<%= form_for(#calendar) do |f| %>
<%= render 'shared/error_messages', object: f.object %>
<div class="field">
<%= f.text_field :name, placeholder: "Your new calendar name" %>
</div>
<%= f.submit "Create", class: "btn btn-primary" %>
<% end %>
We are considering updating the controller as follows:
def create
#calendar = current_user.calendars.create(calendar_params)
#current_user.total_calendar_count += 1
#current_user.owned_calendar_count += 1
current_user.administrations << #calendar.id
#calendar.administration.role = 'Creator'
if #calendar.save
flash[:success] = "Calendar created!"
redirect_to root_url
else
render 'static_pages/home'
end
end
ActiveRecord::AssociationTypeMismatch in CalendarsController#create
Administration(#70307724710480) expected, got Fixnum(#70307679752800)
unless record.is_a?(reflection.klass) || record.is_a?(reflection.class_name.constantize)
message = "#{reflection.class_name}(##{reflection.klass.object_id}) expected, got #{record.class}(##{record.class.object_id})"
raise ActiveRecord::AssociationTypeMismatch, message
end
end
app/controllers/calendars_controller.rb:7:in `create'
How can we make it work?
This line is actually causing the error: current_user.administrations << #calendar.id.
current.administrations expects an object of type Administration while you are passing a Fixnum into it.
You can accomplish the same functionality in the following way:
current_user.administrations.create(calendar_id: #calendar.id)
Edit:
As OP asked in comments that it is a good practice or not. See, there is rule that says that controllers should be skinny, and models should be fatty. Well, it means you should try to write minimum code, and all the logic and fetching of objects should be there in models. But that isn't the case in your code scenario. You should move your code into model, and then call that into your controller.
Here's how:
class User < ActiveRecord::Base
def add_calendar_and_role(calendar_id, role)
self.administrations.find_by(calendar_id: calendar_id).update(role: role)
end
end
This way, your code reduces to just:
current_user.add_calendar_and_role(#calendar.id, 'Creator')
And on the same way, you can further refactor your controller code.

How to make a many to many association in rails

I am trying to make a forum application with Rails 4. I want users to have many forums and so I know I need a many-to-many relationship. I have a form to save the title and the description of the new forum. I Have 3 tables so far, users, forums, and forums_users. Everything works great when I create a new form and it gets added to the forums database. My question is how do I get the information to go to the forums_users table? Because right now when I submit my form, it does not add the information to the association table.
Here is my migration file for forums.
def up
create_table :forums do |t|
t.string :title
t.text :description
t.string :logo
t.boolean :is_active, default: true
t.timestamps
end
add_index :forums, :title
create_table :forums_users, id: false do |t|
t.belongs_to :forum, index: true
t.belongs_to :user, index: true
end
end
def down
drop_table :forums
drop_table :forums_users
end
These are my models.
class Forum < ActiveRecord::Base
has_and_belongs_to_many :users
end
class User < ActiveRecord::Base
has_and_belongs_to_many :forums
end
Here is my create method in the Forum Controller
def create
#forum = Forum.new(forum_params)
#forum.save
respond_to do |format|
format.html{redirect_to admin_path, notice: 'New forum was successfully created.'}
end
end
private
def forum_params
params.require(:forum).permit(:title, :description, :logo, :is_active)
end
And here is the form you submit.
= simple_form_for(:forum, url: {action: :create, controller: :forums}) do |f|
= f.error_notification
.form-inputs
= f.input :title, autofocus: true
= f.input :description, as: :text
.form-actions
= f.button :submit
Thank you in advance.
If you want to get the data from your join table forum_users then use has_many :through
class Forum < ActiveRecord::Base
has_many :users, through: :forum_users
end
class User < ActiveRecord::Base
has_many :forums, through: :forum_user
end
class ForumUser < ActiveRecord::Base
belongs_to :user
belongs_to :forum
end
Now you can access/fetch the forum_users table data using UserForum Model
Create the forum using a reference to the current user, for example:
#forum = current_user.forums.create(forum_params)

Rails & Rolify: Add default role on creation?

Currently I'm using Rolify & CanCan to manage roles and abilities in my Rails 3 app. My question is: How can I get a user to have a role by default on creation? for example, if I have a "user" role, ¿How can I make all the users that register in my app have a user Role by default? My Ability.rb has this code:
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new # guest user (not logged in)
if user.has_role? :admin
can :manage, :all
elsif user.has_role? :user
can :update, User, :id => user.id
end
end
end
My User Model has this one:
class User < ActiveRecord::Base
rolify
authenticates_with_sorcery!
attr_accessible :username, :email, :password, :password_confirmation
validates_confirmation_of :password
validates_presence_of :password, :on => :create
validates_presence_of :username
validates_uniqueness_of :username
validates_presence_of :email
validates_uniqueness_of :email
end
The Role Model This One:
class Role < ActiveRecord::Base
has_and_belongs_to_many :users, :join_table => :users_roles
belongs_to :resource, :polymorphic => true
end
And From the UsersController we have:
def new
#user = User.new
end
def create
#user = User.new(params[:user])
if #user.save
redirect_to users_path, :notice => "Tu usuario se ha guardado"
else
render "new"
end
end
Finally the Rolify Migration is this one:
class RolifyCreateRoles < ActiveRecord::Migration
def change
create_table(:roles) do |t|
t.string :name
t.references :resource, :polymorphic => true
t.timestamps
end
create_table(:users_roles, :id => false) do |t|
t.references :user
t.references :role
end
add_index(:roles, :name)
add_index(:roles, [ :name, :resource_type, :resource_id ])
add_index(:users_roles, [ :user_id, :role_id ])
end
end
Now, I can assign roles manually from the rails console by using:
1 User.all
2 User.find(id)
3 User.add_role(:role)
But how can I assign automatically a default role when every user it's created?
Thanks!
You can use an active record callback to assign the role after the user is created. Something like
class User < ActiveRecord::Base
after_create :assign_default_role
def assign_default_role
add_role(:role)
end
end
Note that there's also an after_save callback but it's called EVERY time the user is saved. So if you edit the user and save it would try to add the role again. That's why I'm using the after_create callback instead.
You'd better check if a role is assigned before add_role. so I prefer:
class User < ActiveRecord::Base
after_create :assign_default_role
def assign_default_role
add_role(:normal) if self.roles.blank?
end
end
Forget it, Just had to add:
#user.add_role(:user)
in my create action right after the #user = User.new(params[:user]) line.
Figured it out by myself... I'm still learning :)
Thanks!
after_create :default_role
private
def default_role
self.roles << Role.find_by_name("user")
self.save
end

ActiveRecord - automatically merging model data as an aggregate

Lets say I have two tables.
class CreateUsers < ActiveRecord::Migration
def self.up
create_table :users do |t|
t.string :type, :default => 'User'
t.string :user_name, :null => false
t.boolean :is_registered, :default => true
# ... many more fields
end
end
end
class CreateContactInfo < ActiveRecord::Migration
def self.up
create_table :contact_info do |t|
t.integer :resource_id
t.string :resource_type
t.string :first_name
t.string :last_name
t.string :middle_initial
t.string :title
end
end
end
class ContactInfo < ActiveRecord::Base
belongs_to :contactable, :polymorphic => true
end
class User < ActiveRecord::Base
has_one :contact_info, :as => :contactable
# composed_of :contact_info # ... It would be nice if magics happened here
end
I would like to have the User's contact_info automatically merged into my User object as attributes of the user object without having to say #user.contact_info.first_name; instead, I would prefer to be able to write #user.first_name.
The reason I am breaking out attributes to the contact_info table is that these are common attributes to multiple models. That is why I am making setting up the contact_info as a polymorphic association.
Does anyone know of a good way to aggregate/merge the attributes of contact_info directly into my user model?
Use delegate:
class User < ActiveRecord::Base
has_one :contact_info, :as => :contactable
delegate :name, :name=, :email, :email=, :to => :contact_info
end
Not necessarily a good way to do it, but I did something similar by overriding the method_missing method and then calling my aggregated object. So, it would look something like:
class User
def method_missing(method_id)
self.contact_info.send(method_id)
end
end
Edit 1: Better implementation (I think this will work):
class User
alias_method :orig_method_missing, :method_missing
def method_missing(method_id)
if (self.contact_info.respond_to?(method_id))
self.contact_info.send(method_id)
else
orig_method_missing(method_id)
end
end
end
The above has the advantage that all other unknown method calls will get passed correctly.
I finally got it! Thank you both amikazmi and Topher Fangio. I had to implement both the delegate and method_missing techniques to get this to work.
Here is the total madness that finally ended up working for me! If anybody has suggestions on how to further improve this, I'd love to hear your suggestions.
class User < ActiveRecord::Base
attr_accessible *([:user_name, :udid, :password, :password_confirmation, :contact_info] + ContactInfo.accessible_attributes.to_a.map {|a| a.to_sym})
has_one :contact_info, :as => :contactable
def method_missing(method_id, *args)
if (!self.respond_to?(method_id) && self.contact_info.respond_to?(method_id))
self.contact_info.send(method_id, *args)
elsif (!self.class.respond_to?(method_id) && ContactInfo.respond_to?(method_id))
ContactInfo.send(method_id, *args)
else
super(method_id, *args)
end
end
# delegating attributes seems redundant with the method_missing above, but this secret sauce works.
ContactInfo.accessible_attributes.to_a.each do |a|
delegate a.to_sym, "#{a}=".to_sym, :to => :contact_info
end
def initialize(*args)
options = args.extract_options!
contact_attrs = ContactInfo.accessible_attributes.to_a.map{|a| a.to_sym}
#ci = ContactInfo.new(options.reject {|k,v| !contact_attrs.include?(k) })
super(*(args << options.reject { |k,v| contact_attrs.include?(k) }.merge(:contact_info => #ci) ) )
self.contact_info = #ci
end
validates_presence_of :user_name
validates_uniqueness_of :user_name
validates_associated :contact_info
def after_save
# automatically save the contact info record for the user after the user has been saved.
self.contact_info.save!
end
end
class ContactInfo < ActiveRecord::Base
set_table_name "contact_info"
belongs_to :contactable, :polymorphic => true
validates_presence_of :email
validates_uniqueness_of :email
attr_accessible :first_name,
:last_name,
:middle_initial,
:title,
:organization_name,
:email,
:email_2,
:twitter_name,
:website_url,
:address_1,
:address_2,
:city,
:state,
:zip,
:phone_work,
:phone_mobile,
:phone_other,
:phone_other_type
def full_name
[self.first_name, self.last_name].compact.join(' ')
end
end

Resources