How to update associated models in gem "devise" account_update action - ruby-on-rails

I'm new in ruby on rails
I use gem "devise" for authentication users and I have trouble with update assosiated models in account update action. At account update, keys 'user_id' in assosiated records set to nil
database queries that occurs:
UPDATE "user_information" SET "user_id" = ? WHERE "user_information"."user_id" = ? [["user_id", nil], ["user_id", 1]]
UPDATE "user_settings" SET "user_id" = ? WHERE "user_settings"."user_id" = ? [["user_id", nil], ["user_id", 1]]
ie request reset key user_id to nil in associated models. What am I doing wrong?
My code:
migrations
create_table(:users) do |t|
t.string :email, null: false, default: ""
t.string :encrypted_password, null: false, default: ""
# etc
end
create_table :user_settings, :id => false do |t|
t.references :user, null: false, index: true, unique: true
# etc
end
create_table :user_information, :id => :user_id do |t|
t.references :user, null: false, index: true, unique: true
# etc
end
user model
class User < ActiveRecord::Base
has_one :setting, :class_name => 'User::Setting', inverse_of: :user
has_one :information, :class_name => 'User::Information', inverse_of: :user
accepts_nested_attributes_for :setting
accepts_nested_attributes_for :information'
# devise
end
registrations controller
class Users::Devise::RegistrationsController < Devise::RegistrationsController
before_filter :configure_permitted_parameters
protected
def configure_permitted_parameters
devise_parameter_sanitizer.for(:account_update) do |u|
u.permit(:name, :email, :password, :password_confirmation, :current_password,
information_attributes: [:name, :contacts], setting_attributes: [:monetization])
# etc
end
end
def build_resource(hash=nil)
super
self.resource.build_setting
self.resource.build_information if hash.empty?
end
end

Related

Rails 5 belongs_to weird behaviour

Client Class
class Client < ApplicationRecord
validates_presence_of :name, :email
validates_email_format_of :email, :message => 'is not looking good'
validates_uniqueness_of :email
has_many :projects
end
Project Class
class Project < ApplicationRecord
belongs_to :client, optional: false
validates_presence_of :name
end
And schema of my tables
create_table "clients", force: :cascade do |t|
t.string "name"
t.string "email"
t.string "phone"
t.string "address"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "projects", force: :cascade do |t|
t.string "name"
t.integer "client_id"
t.string "description"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
Sometime I want to create Project without having client specified. So I added option :option false. And so now I'm able to create project without specifying any client id. However when I try to create project with client_id, it accepts any value for client_id i.e. if enter 8 and if this id is not present in client then also its accepted. I want in such cases it shouldn't save this project.
How can I achieve this behaviour?
I have rails version 5.1.4
First some changes, is optional: true, optional: false is the default behavior and expects to get a associated record every time, with that said, you could do something like this:
class Project < ApplicationRecord
belongs_to :client, optional: true
# Sorry, small bug, client_id? with value zero would return always false and it becomes zero when you input strings or false
# validates_presence_of :client, if: client_id?
validates_presence_of :client, unless: Proc.new{ |d| d.client_id.blank? }
end
This would validate the client record when the client_id is present
p = Project.new # => #<Project id: nil, client_id: nil, ...
p.valid? # => true
p.client = Client.first # => #Client id: 1, ...
p.valid? # => true
p.client = nil # => nil
p.valid? # => true
#Non-existent id 10
p.client_id = 10 # => 10
p.valid? # => false
p.errors.full_messages # => ["Client can't be blank"]
p.client_id = 1 # => 1
p.client # => #Client id: 1, ...
p.valid? # => true
You can even use a custom message
validates_presence_of :client, message: 'invalid client', if: :client_id?
Finally, just a recommendation, use t.references on your migrations to be able to use foreign keys and get this fields indexed
t.references :client, foreign_key: true
I would write your own custom validation to ensure that the id of the client actually exists in your database before persisting the project record:
class Project < ApplicationRecord
belongs_to :client, optional: false
validates_presence_of :name
validate :validate_client_id, if: :client_id?
private
def validate_client_id
errors.add(:client_id, "is invalid") unless Client.exists?(self.client_id)
end
end
The validation will only run if the field is not blank. Which is convenient since client_id is optional in your case.

Failed Signup with Devise nested parameters model have serialized column

During a signup process I have a user model and Tenant model. Recently I added a serialized column to the Tenant model and I can update the this column fine. However when creating a new Tenant I have devise creating tenant through nested parameters and I get the following error:
ActiveRecord::SerializationTypeMismatch (Attribute was supposed to be a Hash, but was a String. -- "{}"): Important to note that I don't touch that column during the sign up process I have tried including the column on the sanitizer but it does the same. On the schema there is a default value which is '{}'. Below some of the code:
create_table "tenants", force: :cascade do |t|
t.string "tenant_name"
t.string "tenant_address"
t.string "tenant_city"
t.string "tenant_zip"
t.string "tenant_phone"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.boolean "authorized"
t.boolean "trial"
t.string "plan_id"
t.string "plan_name"
t.string "braintree_id"
t.string "subscription_id"
t.jsonb "preferences", default: "{}", null: false
t.string "tenant_state"
t.string "tenant_country"
t.index ["preferences"], name: "index_tenants_on_preferences", using: :gin
end
class Tenant < ApplicationRecord
has_many :users, :dependent => :delete_all
has_many :customers, :dependent => :delete_all
has_many :work_orders, :dependent => :delete_all
has_many :vehicles, :dependent => :delete_all
has_many :suppliers, :dependent => :delete_all
end
serialize :preferences, Hash
store_accessor :preferences, :state_tax, :mun_tax, :welcome_sms, :estimate_sms, :completed_sms, :disclaimer
Here is part of my User controller:
class Users::RegistrationsController < Devise::RegistrationsController
before_action :configure_sign_up_params, only: [:create]
# before_action :configure_account_update_params, only: [:update]
# GET /resource/sign_up
def new
build_resource({})
self.resource.tenant = Tenant.new
respond_with self.resource
end
# POST /resource
def create
super
if #user.save
#result = Braintree::Customer.create(
:first_name => #user.name,
:last_name => #user.lastname,
:company => #user.tenant.tenant_name,
:email => #user.email,
:phone => #user.phone
)
if #result.success?
#user.tenant.set_braintree_id(#result.customer.id)
flash[:notice] = 'Thanks you! and Welcome to Autokick.tech enjoy your free 30 days!'
else
flash[:notice] = #result.errors
end
end
end
t.jsonb "preferences", default: "{}", null: false
The default is a string "{}" like the error says.
Change it to default: {} without the quotes.

How to only use tag_cloud for current_user?

In my application_controller I have this:
def tag_cloud
#tags = Tag.top_20.sort{ |x,y| x.id <=> y.id } if current_user
end
which will make the tag count work for all tags from all users, but instead I want the tag_cloud to show only the tags of the current_user. I tried this:
def tag_cloud
#tags = current_user.tags.top_20.sort{ |x,y| x.id <=> y.id } if current_user
end
but that gave me an error:
NoMethodError (undefined method `top_20' for #<ActiveRecord::Associations::CollectionProxy []>):
app/controllers/application_controller.rb:13:in `tag_cloud'
even though top_20 is defined in tag.rb:
class Tag < ActiveRecord::Base
has_many :taggings
scope :top_20, -> {
where("taggings_count != 0").order("taggings_count DESC").limit(15)
}
end
I'm using the acts-as-taggable-on gem. Thank you!
UPDATES
user.rb
class User < ActiveRecord::Base
acts_as_tagger
acts_as_taggable
has_many :notifications
has_many :activities
has_many :activity_likes
has_many :liked_activities, through: :activity_likes, class_name: 'Activity', source: :liked_activity
has_many :liked_comments, through: :comment_likes, class_name: 'Comment', source: :liked_comment
has_many :valuation_likes
has_many :habit_likes
has_many :goal_likes
has_many :quantified_likes
has_many :comment_likes
has_many :authentications
has_many :habits, dependent: :destroy
has_many :levels
has_many :combine_tags
has_many :valuations, dependent: :destroy
has_many :comments
has_many :goals, dependent: :destroy
has_many :quantifieds, dependent: :destroy
has_many :results, through: :quantifieds
has_many :notes
accepts_nested_attributes_for :habits, :reject_if => :all_blank, :allow_destroy => true
accepts_nested_attributes_for :notes, :reject_if => :all_blank, :allow_destroy => true
accepts_nested_attributes_for :quantifieds, :reject_if => :all_blank, :allow_destroy => true
accepts_nested_attributes_for :results, :reject_if => :all_blank, :allow_destroy => true
has_many :active_relationships, class_name: "Relationship",
foreign_key: "follower_id",
dependent: :destroy
has_many :passive_relationships, class_name: "Relationship",
foreign_key: "followed_id",
dependent: :destroy
has_many :following, through: :active_relationships, source: :followed
has_many :followers, through: :passive_relationships, source: :follower
attr_accessor :remember_token, :activation_token, :reset_token
before_save :downcase_email
before_create :create_activation_digest
validates :name, presence: true, length: { maximum: 50 }
VALID_EMAIL_REGEX = /\A[\w+\-.]+#[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, length: { maximum: 255 },
format: { with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: false }, unless: -> { from_omniauth? }
has_secure_password
validates :password, length: { minimum: 6 }
scope :publish, ->{ where(:conceal => false) }
User.tag_counts_on(:tags)
def count_mastered
#res = habits.reduce(0) do |count, habit|
habit.current_level == 6 ? count + 1 : count
end
end
def count_challenged
#challenged_count = habits.count - #res
end
def self.from_omniauth(auth)
where(provider: auth.provider, uid: auth.uid).first_or_initialize.tap do |user|
user.provider = auth.provider
user.image = auth.info.image
user.uid = auth.uid
user.name = auth.info.name
user.oauth_token = auth.credentials.token
user.oauth_expires_at = Time.at(auth.credentials.expires_at)
user.password = (0...8).map { (65 + rand(26)).chr }.join
user.email = (0...8).map { (65 + rand(26)).chr }.join+"#mailinator.com"
user.save!
end
end
def self.koala(auth)
access_token = auth['token']
facebook = Koala::Facebook::API.new(access_token)
facebook.get_object("me?fields=name,picture")
end
# Returns the hash digest of the given string.
def User.digest(string)
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
BCrypt::Engine.cost
BCrypt::Password.create(string, cost: cost)
end
# Returns a random token.
def User.new_token
SecureRandom.urlsafe_base64
end
# Remembers a user in the database for use in persistent sessions.
def remember
self.remember_token = User.new_token
update_attribute(:remember_digest, User.digest(remember_token))
end
# Forgets a user. NOT SURE IF I REMOVE
def forget
update_attribute(:remember_digest, nil)
end
# Returns true if the given token matches the digest.
def authenticated?(attribute, token)
digest = send("#{attribute}_digest")
return false if digest.nil?
BCrypt::Password.new(digest).is_password?(token)
end
# Activates an account.
def activate
update_attribute(:activated, true)
update_attribute(:activated_at, Time.zone.now)
end
# Sends activation email.
def send_activation_email
UserMailer.account_activation(self).deliver_now
end
def create_reset_digest
self.reset_token = User.new_token
update_attribute(:reset_digest, User.digest(reset_token))
update_attribute(:reset_sent_at, Time.zone.now)
end
# Sends password reset email.
def send_password_reset_email
UserMailer.password_reset(self).deliver_now
end
# Returns true if a password reset has expired.
def password_reset_expired?
reset_sent_at < 2.hours.ago
end
def good_results_count
results.good_count
end
# Follows a user.
def follow(other_user)
active_relationships.create(followed_id: other_user.id)
end
# Unfollows a user.
def unfollow(other_user)
active_relationships.find_by(followed_id: other_user.id).destroy
end
# Returns true if the current user is following the other user.
def following?(other_user)
following.include?(other_user)
end
private
def from_omniauth?
provider && uid
end
# Converts email to all lower-case.
def downcase_email
self.email = email.downcase unless from_omniauth?
end
# Creates and assigns the activation token and digest.
def create_activation_digest
self.activation_token = User.new_token
self.activation_digest = User.digest(activation_token)
end
end
tags_controller
class TagsController < ApplicationController
def index
#tags = Tag.all
end
def show
#tag = Tag.find(params[:id])
end
end
routes.rb
get 'tags/:tag', to: 'pages#home', as: :tag
schema.rb
create_table "taggings", force: true do |t|
t.integer "tag_id"
t.integer "taggable_id"
t.string "taggable_type"
t.integer "tagger_id"
t.string "tagger_type"
t.string "context", limit: 128
t.datetime "created_at"
end
add_index "taggings", ["tag_id", "taggable_id", "taggable_type", "context", "tagger_id", "tagger_type"], name: "taggings_idx", unique: true
add_index "taggings", ["taggable_id", "taggable_type", "context"], name: "index_taggings_on_taggable_id_and_taggable_type_and_context"
create_table "tags", force: true do |t|
t.string "name"
t.integer "taggings_count", default: 0
end
add_index "tags", ["name"], name: "index_tags_on_name", unique: true
create_table "users", force: true do |t|
t.string "name"
t.boolean "conceal", default: false
t.string "email"
t.text "missed_days"
t.text "missed_levels"
t.string "provider"
t.string "uid"
t.string "oauth_token"
t.datetime "oauth_expires_at"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "password_digest"
t.string "remember_digest"
t.boolean "admin", default: false
t.string "activation_digest"
t.boolean "activated", default: false
t.datetime "activated_at"
t.string "reset_digest"
t.datetime "reset_sent_at"
t.string "image"
end
goal.rb
class Goal < ActiveRecord::Base
scope :publish, ->{ where(:conceal => false) }
belongs_to :user
has_many :comments
has_many :notifications
has_many :notes
acts_as_taggable
scope :accomplished, -> { where(accomplished: true) }
scope :unaccomplished, -> { where(accomplished: false) }
scope :private_submit, -> { where(private_submit: true) }
scope :public_submit, -> { where(private_submit: false) }
validates :name, presence: true
has_many :goal_likes
has_many :likers, through: :goal_likes, class_name: 'User', source: :liker
scope :top_3, -> do
order("deadline").
limit(3)
end
end
Your issue is that acts-as-taggable-on does not actually use your Tag class. Or to be more specific - it works when you do Tags.top_20 since you are calling it on your tag class. But the User#tags relationship actually uses ActsAsTaggableOn::Tag which explains the NoMethodError.
ActsAsTaggableOn seems to have this functionality built in already though:
current_user.tags.most_used(20)
Addition:
I am combining the tags from several model instances into a single tag
cloud, which I am showing in the sidebar.
There is nothing in this requirement which says that you need to create a Tag or Tagging class. In fact doing so is likely to cause you and other developers a bunch of grief. The fact that the tag cloud on your home page works does not change the fact that you most likely are creating a bunch of future issues by replacing two relatively complex components (Tagging and Tag classes) with tiny stubs willy-nilly.
If you look at the tag_cloud implementation is pretty easy to see that it takes a ActiveRecord::Relation or a collection or just any old enumerable such as an array.
module ActsAsTaggableOn
module TagsHelper
# See the wiki for an example using tag_cloud.
def tag_cloud(tags, classes)
return [] if tags.empty?
max_count = tags.sort_by(&:taggings_count).last.taggings_count.to_f
tags.each do |tag|
index = ((tag.taggings_count / max_count) * (classes.size - 1))
yield tag, classes[index.nan? ? 0 : index.round]
end
end
end
end
Since collections are just fancy pants versions of arrays merging them together is as easy as:
#tags = foo.tags + bar.tags
However joining relations together is a bit more complex since Rails currently does not support the SQL OR clause. (Its coming in Rails 5). You would either have to load and merge the collections as above or create your own where clause with AREL if multiple SQL queries is a performance issue.

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

Resources