Single Table Inheritance with Devise in Rails 4 - ruby-on-rails

I have read the posts here, here, and here, but I'm still having trouble with implementing Single Table Inheritance.
Ideally I would like to have two registration paths (one for clients and one for providers) with the common fields name, email, password, and confirm_password, and the provider registration having an extra radiobutton field to specify a provider type. I am doing the registration through devise. Upon clicking submit on the registration form a user would then be redirected to a second form which is totally different for clients and providers (I have been doing this using the edit page for a resource).
As it stands, everything works if I am just doing it through User, but as soon as I add single table inheritance the registration forms tell me they are missing the requirements of the second forms.
Here is my config/routes.rb
Rails.application.routes.draw do
devise_for :users, :controllers => {:sessions => "sessions"}, :skip=> :registrations
devise_for :clients, :providers, :skip=> :sessions
resources :clients
resources :providers
root :to=>'pages#home'
match '/home', to: 'pages#home', via: 'get'
end
My models look as follows:
User:
class User < ActiveRecord::Base
before_save {self.email = email.downcase}
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
validates :name, presence: true, length: {maximum: 50}
validates :email, presence: true, :email => {:ban_disposable_email => true, :message => I18n.t('validations.errors.models.user.invalid_email')}, uniqueness: { case_sensitive: false }
validates :password, presence: true, length: {minimum: 6},:if=>:password_validation_required?
LOGO_TYPES = ['image/jpeg', 'image/png', 'image/gif']
has_attached_file :avatar, :styles => {:medium => "300x300>",:square=>"200x200>", :thumb => "100x100>" }, :default_url => '/assets/missing_:style.png'
validates_attachment_content_type :avatar, :content_type => LOGO_TYPES
def password_validation_required?
!#password.blank?
end
end
Client:
class Client < User
validates :industry, presence: true
validates :city, presence: true
validates :state, presence: true
validates :description, presence: true, length: {minimum: 50, maximum: 300}
end
Provider:
class Provider < User
validates :ptype, presence: true
validates :city, presence: true
validates :state, presence: true
validates :education, presence: true
validates :biography, presence:true, length: {minimum: 50, maximum: 300}
validates_format_of :linkedin, :with => URI::regexp(%w(http https))
validates :resume, presence: true
has_many :disciplines
end
and here are my controllers:
class SessionsController < Devise::SessionsController
def create
rtn = super
sign_in(resource.type.underscore, resource.type.constantize.send(:find,resource.id)) unless resource.type.nil?
rtn
end
end
class RegistrationsController < Devise::RegistrationsController
protected
def after_sign_up_path_for(resource)
if resource.is_a?(User)
if current_user.is_a?(Client)
edit_client_path(current_user.id)
elsif current_user.is_a?(Provider)
edit_provider_path(current_user.id)
end
else
super
end
end
end
class ClientsController < ApplicationController
def show
#client = Client.find(params[:id])
end
def edit
#client = Client.find(params[:id])
end
def update
#client = Client.find(params[:id])
if #client.update_attributes(client_params_edit)
flash[:success] = "Profile Updated"
redirect_to #client
else
flash[:failure] = "Profile Information Invalid"
render 'edit'
end
end
def client_params_edit
params.require(:client).permit(:avatar,:industry,:city,:website, :description)
end
end
the provider controller is quite similar.
Finally, here is my schema.rb:
ActiveRecord::Schema.define(version: 20140628213816) do
create_table "disciplines", force: true do |t|
t.integer "years"
t.string "description"
t.integer "user_id"
end
create_table "users", force: true do |t|
t.string "name"
t.string "email"
t.string "avatar_file_name"
t.string "avatar_content_type"
t.integer "avatar_file_size"
t.datetime "avatar_updated_at"
t.string "password_digest"
t.string "industry"
t.string "city"
t.string "state"
t.string "website"
t.string "description"
t.string "encrypted_password", default: "", null: false
t.string "reset_password_token"
t.datetime "reset_password_sent_at"
t.datetime "remember_created_at"
t.integer "sign_in_count", default: 0, null: false
t.datetime "current_sign_in_at"
t.datetime "last_sign_in_at"
t.string "current_sign_in_ip"
t.string "last_sign_in_ip"
t.datetime "created_at"
t.datetime "updated_at"
t.string "type"
t.string "ptype"
t.string "education"
t.string "resume_file_name"
t.string "resume_content_type"
t.integer "resume_file_size"
t.datetime "resume_updated_at"
end
add_index "users", ["email"], name: "index_users_on_email", unique: true
add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
end

You need to specify which model should be instantiated inside your custom registrations controller (that one which inherits from Devise::RegistrationsController).
You have to override the protected method called resource_class to somewhat like this:
def resource_class
# for example you pass type inside params[:user]
klass = params[:user].try(:[], :type) || 'Client'
# we don't want wrong class to be instantiated
raise ArgumentError, 'wrong user class' unless ['Client', 'Provider'].include?(klass)
# transform string to class
klass.constantize
end
Also you might want to override sign_up_params to specify allowed params based on user type too.

Just a thought.
Have you considered allowing registration as a user and holding the type parameter back until later in the workflow.
i.e.
Registration page:
Creates User (with a parameter that decides which Type the user will end up being)
Second page (to which you are automatically redirected upon creating user, or even logging in as user having not gone through part 2):
Adds the appropriate required information and changes type from User to your appropriate STI type upon submit.
Other option would be to swap your first "submit" button for a button which simply reveals the relevant extra fields (and the real submit button) via JS.

Related

How to fix User 1-to-many association when getting NoMethodError "Undefined method 'user_id', did you mean? user"

I'm trying to implement a User model with a 1-to-many association with articles. So I added a User.rb model with
class User < ApplicationRecord
has_many :articles
before_save { self.email = email.downcase }
validates :username, presence: true,
uniqueness: { case_sensitive: false },
length: { minimum: 3, maximum: 25 }
VALID_EMAIL_REGEX = /\A\S+#.+\.\S+\z/i
validates :email, presence: true,
uniqueness: { case_sensitive: false },
length: { maximum: 105 }, format: { with: VALID_EMAIL_REGEX }
end
Also added belongs_to: user in the Article model:
class Article < ApplicationRecord
belongs_to :user
has_many :comments, dependent: :destroy
validates :title, presence: true, length: { minimum: 5 }
validates :text, presence: true, length: { minimum: 10, maximum: 400}
validates :user_id, presence: true
end
I ran this migration
rails generate migration add_user_id_to_articles
and this is my migration file
class AddUserIdToArticles < ActiveRecord::Migration[5.2]
def change
add_column :articles, :user_id, :integer
end
end
Here is the definition of my schema.rb:
ActiveRecord::Schema.define(version: 2019_02_26_124924) do
create_table "articles", force: :cascade do |t|
t.string "title"
t.text "text"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "comments", force: :cascade do |t|
t.string "commenter"
t.text "body"
t.integer "article_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["article_id"], name: "index_comments_on_article_id"
end
create_table "users", force: :cascade do |t|
t.string "username"
t.string "email"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
end
And lastly my change function in the articles_controller.rb file:
def create
#article = Article.new(article_params)
if #article.save
redirect_to #article
else
render 'new'
end
end
I ran rails db:migrate so that shouldn't be a problem. I saw some people on Stackoverflow say I should just add
#article.user_id = current_user.id
but already did that and nothing happened.
I keep getting this error:
NoMethodError (undefined method `user_id' for #)
Did you mean? user
and I don't know what else to try. I'm starting with Ruby on Rails so keep that in mind.
Thanks a lot for reading, hopefully somebody knows a fix.
First, make sure your migration actually runs through; then your schema.rb should include the user_id column too.
Then, try changing the line creating the new article to:
#article = current_user.articles.new(article_params)
Alternatively, you can set
#article.user = current_user
Inside your Articles controller, you'll need to add user_id into your permitted parameters.
def article_params
params.require(:article).permit(:title, :text, :user_id)
end

Rails - Authlogic: undefined method 'valid_password?'

I managed to get user account creation working however I am running into an issue when I try to log in to the newly created account:
undefined method `valid_password?'
and the error highlights
if #user_session.save!
I am Authlogic and BCrypt on Rails 5 and I do not know why or how to fix the error above. I used this guide in order to setup Authlogic and I am able to sign out but when trying to log in, I get the above error. From some searches, some people have been able to solve this by restarting Heroku but I am not using Heroku so I do not believe that would work for me. Another solution was to add some password fields but I believe the "has_secure_password" would supply those fields accordingly. Does anyone know why this error occurs and/or how to fix it? Thanks!
Users Model:
class User < ApplicationRecord
has_many :activities
has_secure_password
validates :first_name, presence: true, length: {minimum: 1}
validates :last_name, presence: true, length: {minimum: 1}
validates :email, presence: true, uniqueness: true, length: {minimum: 5}
validates :password_digest, length: {minimum: 6}
validates :password, :confirmation => true, length: {minimum: 4}
validates :password_confirmation, presence: true
#-----------------------New Stuff ---------------------------------------
acts_as_authentic do |c|
c.crypto_provider = Authlogic::CryptoProviders::BCrypt
end
#------------------------------------------------------------------------
#---------------Unsure if working--------------
#validates_presence_of :password, :on => :create
#validates_presence_of :email
#validates_uniqueness_of :email
#----------------------------------------------
def self.authenticate(email, password)
user = find_by_email(email)
if user && user.password_hash == BCrypt::Engine.hash_secret(password, user.password_salt)
user
else
nil
end
end
def encrypt_password
if password.present?
self.password_salt = BCrypt::Engine.generate_salt
self.password_hash = BCrypt::Engine.hash_secret(password, password_salt)
end
end
end
User Sessions Controller:
class UserSessionsController < ApplicationController
def new
#user_session = UserSession.new
end
def create
#user_session = UserSession.new(user_session_params)
if #user_session.save!
flash[:success] = 'Welcome back'
redirect_to root_path
else
render :new
end
end
def destroy
current_user_session.destroy
flash[:success] = 'Goodbye'
#redirect_to root_path - Should redirect somewhere after logout
end
private
def user_session_params
params.require(:user_session).permit(:email,:password)
end
end
Current User and User Session Tables:
create_table "user_sessions", 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.string "first_name"
t.string "last_name"
t.string "email"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "persistence_token"
t.string "password_digest"
t.index ["email"], name: "index_users_on_email", unique: true, using: :btree
end
EDIT 04-11-17
After making the changes below, it worked for me and I was able to log out and in without the mentioned problems. I still do not know why it gave me the error but I suspect it is because I was using "has_secure_password" which does not have a function to check if the password is valid ("valid_password?").
Alright turns out I managed to fix it by removing "has_secure_password" and just solely relying on "acts_as_authentic". After that, I was able to log in successfully. Here's my updated code:
User Model:
class User < ApplicationRecord
has_many :activities
acts_as_authentic
validates :first_name, presence: true, length: {minimum: 1}
validates :last_name, presence: true, length: {minimum: 1}
validates :email, presence: true, uniqueness: true, length: {minimum: 5}
validates :password, :confirmation => true, length: {minimum: 4}
validates :password_confirmation, presence: true
#-----------------------New Stuff ---------------------------------------
#------------------------------------------------------------------------
#---------------Unsure if working--------------
#validates_presence_of :password, :on => :create
#validates_presence_of :email
#validates_uniqueness_of :email
#----------------------------------------------
def self.authenticate(email, password)
user = find_by_email(email)
if user && user.password_hash == BCrypt::Engine.hash_secret(password, user.password_salt)
user
else
nil
end
end
def encrypt_password
if password.present?
self.password_salt = BCrypt::Engine.generate_salt
self.password_hash = BCrypt::Engine.hash_secret(password, password_salt)
end
end
end
User Table:
create_table "users", force: :cascade do |t|
t.string "first_name"
t.string "last_name"
t.string "email"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "persistence_token"
t.string "password_digest"
t.string "crypted_password", null: false
t.string "password_salt"
t.index ["email"], name: "index_users_on_email", unique: true, using: :btree
end
Not all of the fields above are required but I have them as I'm still new to Rails. The main thing required was "crypted_password" and "password_salt". After making the above edits, I was able to log in and out successfully without the mentioned error.

whats wrong with my collection_select in my sign up view page

I am newbie to rails ,
I am using devise from ryan bates video tutorial , and , i am stuck at one point
I have created user , role relationship
and in the sign up page , i need to provide select option group for existing roles ,
in my sign up view page i am writing
<%= collection_select(:user,:roles,Role.find(:all),:id,:name) %>
i dont precisely understand collection_select method , kindly help what i might be doing wrong
my models 1 : user.rb
class User < ActiveRecord::Base
has_and_belongs_to_many :roles
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
attr_accessible :email, :password, :password_confirmation, :remember_me , :roles
has_and_belongs_to_many :roles
def role?(role)
return !!self.roles.find_by_name(role.to_s.camelize)
end
end
my model 2 : role.rb
class Role < ActiveRecord::Base
attr_accessible :name
has_and_belongs_to_many :users
end
my user migration file
class DeviseCreateUsers < ActiveRecord::Migration
def change
create_table(:users) do |t|
## Database authenticatable
t.string :email, :null => false, :default => ""
t.string :encrypted_password, :null => false, :default => ""
## Recoverable
t.string :reset_password_token
t.datetime :reset_password_sent_at
## Rememberable
t.datetime :remember_created_at
## Trackable
t.integer :sign_in_count, :default => 0
t.datetime :current_sign_in_at
t.datetime :last_sign_in_at
t.string :current_sign_in_ip
t.string :last_sign_in_ip
t.timestamps
end
add_index :users, :email, :unique => true
add_index :users, :reset_password_token, :unique => true
end
end
my role migration file
class CreateRoles < ActiveRecord::Migration
def change
create_table :roles do |t|
t.string :name
t.timestamps
end
end
end
my join table migration file
class UsersHaveAndBelongToManyRoles < ActiveRecord::Migration
def up
create_table :roles_users, :id => false do |t|
t.references :role, :user
end
end
def down
drop_table :roles_users
end
end
the ERROR COMING is
undefined method `each' for "2":String
2 being the id of the role selected
In a has_and_belongs_to_many relationship like you have between User and Role, there is no role_id on the User object.
The second parameter of the collection_select is the attribute you're updating with the selection(s), in your case it's not role_id, it's role_ids and seeing as it's a has_and_belongs_to_many relationship you probably want to allow the user to select multiple options, so try something like this:
<%= collection_select(:user, :role_ids, Role.all, :id, :name, {}, { selected: #user.role_ids, multiple: true }) %>
If you attach it to a form_for on your #user object you can use:
<%= f.collection_select(:role_ids, Role.all, :id, :name, {}, multiple: true) %>

Counting Members Based on Invitations - Query

I am counting many user generated actions and for the most part it's easy, but with regard to one, more complex query, I am having trouble.
I have an invitations model and a user model and I can easily count the number of invitations the user sent, but I want to count the number of new members that signed up based on the invitations the existing member sent out.
In invitations, the invitees email is saved as recipient_email
Then, I know I can check that against new members email some how, but am not clear on the syntax.
Any help will be greatly appreciated. More information below.
Invitation Model:
class Invitation < ActiveRecord::Base
attr_accessible :recipient_email, :sender_id, :sent_at, :token
belongs_to :sender, :class_name => 'User'
has_one :recipient, :class_name => 'User'
validates_presence_of :recipient_email
validates_uniqueness_of :recipient_email, :message => '%{value} has already been invited'
validate :recipient_is_not_registered
validate :sender_has_invitations, :if => :sender
default_scope order: 'invitations.created_at DESC'
before_create :generate_token
before_create :decrement_sender_count, :if => :sender
after_create do |invitation|
InvitationMailer.delay.invitation_email(self)
end
def invitee
User.find_by_email(self.recipient_email)
end
def invitee_registered?
!invitee.blank?
end
def invitee_submitted?
!invitee.try(:submissions).blank?
end
private
def recipient_is_not_registered
errors.add :recipient_email, 'is already registered' if User.find_by_email(recipient_email)
end
def sender_has_invitations
unless sender.invitation_limit > 0
errors.add_to_base "You have reached your limit of invitations to send.
You can contact Lumeo if you'd like to request more."
end
end
def generate_token
self.token = Digest::SHA1.hexdigest([Time.now, rand].join)
end
def decrement_sender_count
sender.decrement! :invitation_limit
end
end
User Model:
class User < ActiveRecord::Base
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable, :mailchimp
attr_accessible :name, :email, :password, :password_confirmation,
:remember_me, :role_id, :role_ids, :image_attributes,
:terms, :profile_attributes, :current, :image, :roles,
:invitation_token, :join_mailing_list, :affiliate_id,
:invitation_affiliate_token, :affiliation, :referrer_id
validates_uniqueness_of :email
VALID_NAME_REGEX = /[\w]+([\s]+[\w]+){1}+/
validates :name, presence: true,
format: {with: VALID_NAME_REGEX}
#invitation
has_many :sent_invitations, :class_name => 'Invitation', :foreign_key => 'sender_id'
belongs_to :invitation
def invitation_token
invitation.token if invitation
end
def invitation_token=(token)
self.invitation = Invitation.find_by_token(token)
end
before_create :set_invitation_limit
has_one :invitation_affiliate, :class_name => "Affiliate", :foreign_key => 'token', :primary_key => 'invitation_affiliate_token'
private
def set_invitation_limit
self.invitation_limit = 100
end
end
Invitation and User Tables:
create_table "users", :force => true do |t|
t.string "email", :default => "", :null => false
t.string "encrypted_password", :default => "", :null => false
t.string "reset_password_token"
t.datetime "reset_password_sent_at"
t.datetime "remember_created_at"
t.integer "sign_in_count", :default => 0
t.datetime "current_sign_in_at"
t.datetime "last_sign_in_at"
t.string "current_sign_in_ip"
t.string "last_sign_in_ip"
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
t.integer "role_id"
t.string "name"
t.integer "invitation_id"
t.integer "invitation_limit"
end
create_table "invitations", :force => true do |t|
t.integer "sender_id"
t.string "recipient_email"
t.string "token"
t.datetime "sent_at"
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
end
I could think of two different ways:
Add accepted field in invitations
You could add a boolean field for invitations named accepted, the default value will be false and you set it to true when the receipent accepts the invitation. Then you create a scope named accepted that returns only accepted invitations
scope :accepted, where(accepted: true)
You get what you want by #user.sent_invitations.accepted.count
2 . Do the following query
User.where(email: #user.sent_invitations.map(&:recipient_email)).count

Why is my user_id nil?

def destroy
#dignity.destroy
end
Sorry, that's not code, that's just how I feel right now. I know there are a ton of beginner questions on Devise, I think I looked at almost every single one.
I have a very simple Devise setup in Rails 3. I did:
rails generate devise User
I'm also running the rails 3 GeoKit plugin,(not sure if that's relevant, just know that I have this other model) so I have another model called Location, and it acts_as_mappable.
Before I post the code, the basic problem is that I cannot seem to get user_id to auto-increment. It was my understanding that a bit of Rails magic should take care of this for me, if I add a column called user_id to Location class. (which I did through a migration.) and then simply set has_many and belongs_to accordingly. (see below)
I can't figure out why user_id is always nil. Does it have something to do with the way the Devise engine works? I am pretty sure I've made this type of association work in the past in the same way when I wasn't using Devise.
user.rb:
class User < ActiveRecord::Base
has_many :locations
# Include default devise modules. Others available are:
# :token_authenticatable, :confirmable, :lockable and :timeoutable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
# Setup accessible (or protected) attributes for your model
attr_accessible :email, :password, :password_confirmation, :remember_me
end
location.rb:
class Location < ActiveRecord::Base
belongs_to :user
attr_accessible :street_adress, :city, :state, :zip, :item, :user_id
acts_as_mappable :auto_geocode => true
def address
return "#{self.street_adress}, #{self.city}, #{self.state}, #{self.zip}, #{self.item}"
end
end
here is the migration that added the column:
class AddUseridToLocation < ActiveRecord::Migration
def self.up
add_column :locations, :user_id, :integer
end
def self.down
remove_column :locations, :user_id
end
end
And finally, here is the schema.rb:
ActiveRecord::Schema.define(:version => 20110213035432) do
create_table "locations", :force => true do |t|
t.string "street_adress"
t.string "city"
t.string "state"
t.string "zip"
t.float "lat"
t.float "lng"
t.datetime "created_at"
t.datetime "updated_at"
t.string "item"
t.integer "user_id"
end
create_table "users", :force => true do |t|
t.string "email", :default => "", :null => false
t.string "encrypted_password", :limit => 128, :default => "", :null => false
t.string "password_salt", :default => "", :null => false
t.string "reset_password_token"
t.string "remember_token"
t.datetime "remember_created_at"
t.integer "sign_in_count", :default => 0
t.datetime "current_sign_in_at"
t.datetime "last_sign_in_at"
t.string "current_sign_in_ip"
t.string "last_sign_in_ip"
t.datetime "created_at"
t.datetime "updated_at"
end
add_index "users", ["email"], :name => "index_users_on_email", :unique => true
add_index "users", ["reset_password_token"], :name => "index_users_on_reset_password_token", :unique => true
end
EDIT: I'm okay with a RTFM response, as long as I can get a little push in the right direction. I have a suspicion that I need to tell rails something in the create action of my locations_controller.rb ? Someone just give me a little hint here!
def destroy
#dignity.destroy
end
Clearly the first thing to be done is:
raise self.esteem
You say you can't get user_id to "autoincrement". I think what you meant is that user_id is not being assigned (i.e. it is always nil). Can you show the part of the code that assigns a location to a user? Either of these should work:
#user.locations.build
#user.locations << Location.new
EDIT
To expand on this a bit, say you have a request that looks like this:
POST /users/37/locations
And the submitted form contains input name=user[location][name] value="Paris". A typical Rails controller create action might look like this:
def create
#user = User.find(params[:user_id])
#user.locations.build(params[:user][:location])
if #user.save
flash[:notice] = "Location created successfully"
redirect_to user_locations_path(#user)
else
render :new
end
end
The 'magic' is basically Rails inferring from the has_many statement that it needs to set the value of the foreign key column ('user_id') in the related row in the locations table. When you call #user.save it adds a new row in locations and sets user_id to the value of #user.id.

Resources