1

我正在尝试采用 state_machine 使我的用户能够在 Rails 3.1.3 应用程序的注册过程中拥有状态。我正在尝试制作一个非常简单的案例,但我无法通过事件更改其状态。在重新阅读文档几次之后,我还没有发现问题所在。

我的用户 ActiveRecord 模型是:

# == Schema Information
#
# Table name: users
#
#  id                 :integer         not null, primary key
#  name               :string(255)
#  email              :string(255)
#  created_at         :datetime
#  updated_at         :datetime
#  encrypted_password :string(255)
#  salt               :string(255)
#  admin              :boolean         default(FALSE)
#  notify_followers   :boolean         default(TRUE)
#  state              :string(255)
#

# MME per a utilitzar les Hash functions
require 'digest'

class User < ActiveRecord::Base

  attr_accessor :password # MME nomes dona acces a la instance var @password que no es guarda a la BBDD

  # MME si es posa, atributs (columnes) als que es podrà accedir via ActiveRecord
  attr_accessible   :name, :email, :password, :password_confirmation, :admin, :notify_followers
  # MME validacions
  validates :name, :presence => true,
                   :length=> {maximum: 50}

  validates :email, :presence => true,
                    :format => { :with => /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i },
                    :uniqueness => { :case_sensitive => false}

  validates :password, :presence => true,
                       :confirmation => true,   # crea un atribut password_confirmation i a la vegada confirma que sigui igual que password
                       :length => { :within => 6..40 }

    # validates :password_confirmation, :presence => true   # MME aixo exigigeix que al crear es passi un :password_confirmation, doncs amb nomes
                              #   l'anterior validator sol, pot crearse un usuari si no es passa :password_confirmation

  before_save :encrypt_password

  # MME a l'esborrar un User s'esborren tb els seus Micropost
  has_many :microposts, :dependent => :destroy

 # MME Afegim respostes als usuaris
  has_many :replies, :class_name => 'Micropost',
                     :foreign_key => "in_reply_to",
                     :inverse_of => :replied_user,
                     :dependent => :destroy

  # User com a seguidor (follower)

  # te molts :relationships apuntant-lo amb la clau follower_id. Si el User s'elimina tots aquests Relationship tambe seran eliminats.
  has_many :relationships, :foreign_key => "follower_id",
                           :dependent => :destroy

  # te molts seguits via :relationships als que s'apunta via :followed_id  (inferit gracies a :followed, que apunta a la vegada als User)
  has_many :following, :through => :relationships,
                       :source => :followed

  # User com a seguit (followed)

  # te molts :reverse_relationships apuntant-lo amb la clau followed_id. Si el User s'elimina tots aquests Relationship tambe seran eliminats.
  has_many :reverse_relationships, :class_name => "Relationship",
                                   :foreign_key => "followed_id",
                                   :dependent => :destroy

  # te molts seguidors via :reverse_relationships als que s'apunta via :follower_id  (inferit gracies a :follower, que apunta a la vegada als User)
  has_many :followers, :through => :reverse_relationships

  # Torna els microposts dels usuaris seguits per un user, per exemple:
  #    usr=User.find(12)
  #    usr.following_microposts
  # (no el faig anar finalment: Micropost.from_users_followed_by(user) ho he implementat sense aquests metode perque
  # em falten els microposts del propi user) 
  has_many :following_microposts, :through => :following, 
                                  :source => :microposts

  # Si n'hi ha, te un password_reminder
  has_one :password_reminder

  # Torna l'User de l'email si el password es correcte
  def self.authenticate(email, submited_pwd)
    if usr = find_by_email(email)
      usr.has_password?(submited_pwd) ? usr : nil
    else
      nil
    end
  end

  # Torna l'User del id si el salt es correcte (s'utilitza per les sessions)
  def self.authenticate_with_salt(id, salt)
    user = find_by_id(id)
    (user && user.salt == salt) ? user : nil
  end

  # verifica si el password correspon a l'User
  def has_password?(submited_pwd)
    self.encrypted_password == encrypt(submited_pwd)
  end

  def feed
    #Micropost.from_users_followed_by self
    # Microposts from
    #   self
    #   self.following
    #   self.replies
    Micropost.not_messages.from_users_followed_by_or_in_reply_to self
  end

  # Is usr being followed by self?
  def following? usr
    following.include? usr
    # MME segons el tutorial seria
    #relationships.find_by_followed_id(followed)
  end

  def follow! usr
    relationships.create! :followed_id => usr.id
  end

  def unfollow! usr
    relationships.find_by_followed_id(usr.id).destroy if following?(usr)
  end

  def replies_to(usr, content)
    microposts.create :content=>content, :in_reply_to=>usr.id, :private=>false
  end

  def sends_to(usr, content)
    microposts.create :content=>content, :in_reply_to=>usr.id, :private=>true
  end

  def messages_to usr
    microposts.messages.where(:in_reply_to => usr.id)
  end

  def messages_from usr
    usr.microposts.messages.where(:in_reply_to => self.id)
  end

  def messages_to_or_from usr
    Micropost.messages.between usr, self
  end
  alias conversation_with messages_to_or_from

  # MME generates a unique login name for a user
  def pseudo_login_name
    name.downcase.split.join("_")+"_"+ id.to_s
  end

  # MME generates a password reminder if it doesn't yet exist
  def generate_password_reminder
    #PasswordReminder.find_or_create_by_user_id_and_token :user_id=>self.id,
    #                                                     :token=>SecureRandom.hex(32)
    create_password_reminder!(:token=>SecureRandom.hex(32)) unless password_reminder
  end

  # MME removes its password reminder if exists
  def remove_password_reminder
    password_reminder.delete if password_reminder
  end

  # finds a user from a token (password reminder to change password)
  def self.find_by_token(token)
    pr=PasswordReminder.find_by_token(token, :include=>:user)
    pr.user if pr
  end

  # MME finds a user from a pseudo_login_name
  # first tries to get it from an id
  # last tries to get it from a name
  def self.find_by_pseudo_login_name(pln)
    nam=pln.split("_")
    id = nam.last.to_i
    if id>0 # First attempt: if it exists an id as the last part off the pln 
      User.find_by_id(id)
    else # Second attempt: try to generate a name from a pln
      User.find_by_name(nam.map(&:capitalize).join(" "))
    end
  end

  ## MME state_machine per a fer la inscripcio en passos
  state_machine :initial => :pending do
    event :email_confirm do
      transition :pending => :email_confirmed
    end
  end


  # FUNCIONS PRIVADES
  private

    def encrypt_password
      self.salt = make_salt unless has_password?(password)  # self.salt resets everytime user changes its password
      self.encrypted_password = encrypt(password)   # password refers to self.password
    end

    def make_salt
      Digest::SHA2.hexdigest "#{Time.now.utc}--#{password}"
    end

    def encrypt(str)
      Digest::SHA2.hexdigest "#{salt}--#{str}"
    end

end

当然,我已经进行了迁移以使用户能够容纳状态机

$ rails g migration AddStateToUser state:string
$ rake db:migrate

并检查了用户确实响应了来自 rails 控制台的状态属性。

当我尝试像在此控制台会话日志中那样简单地更改机器的状态时,就会出现问题:

1.9.2-p290 :006 > u=User.find 1
  User Load (0.5ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT 1  [["id", 1]]
 => #<User id: 1, name: "Marcel", email: "mme@gmail.com", created_at: "2012-04-29 10:43:42", updated_at: "2012-04-29 10:43:42", encrypted_password: "d08c12c1cfb51fe5732f5e423b94dfdcaca1a1eb67821e3e37a...", salt: "78dfbecdfd4ffdd1fbcac5a878529b91a5200d563ebe3af23cf...", admin: false, notify_followers: true, state: "pendant"> 
1.9.2-p290 :007 > u.state
 => "pendant" 
1.9.2-p290 :008 > u.email_confirm
   (0.5ms)  SELECT 1 FROM "users" WHERE (LOWER("users"."email") = LOWER('mme@gmail.com') AND "users"."id" != 1) LIMIT 1
 => false 
1.9.2-p290 :009 > u.state
 => "pendant" 

您可能会注意到,从最后一个命令开始,我的用户并没有像预期的那样将他的状态更改为 :email_confirmed。我也不明白顺便说一下正在执行的 SQL 查询。在我看来这很可疑。

更多关于那个。如果我像往常一样尝试更新用户模型,则会出现相同的奇怪 SQL 查询并且不会更新模型。此会话日志显示:

1.9.2-p290 :001 > u=User.find 1
  User Load (55.3ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT 1  [["id", 1]]
 => #<User id: 1, name: "Marcel Massana", email: "xaxaupua@gmail.com", created_at: "2012-04-29 19:32:26", updated_at: "2012-04-29 20:44:10", encrypted_password: "2ef5fec3287e2b26600521488060f698abed387e18e395d1331...", salt: "fa4d3ebb44c00237b66c95cc75ed5d1cda3b6e1535082def2a8...", admin: true, notify_followers: true, state: "pending"> 
1.9.2-p290 :002 > u.update_attributes(:name=>"Marcel")
   (0.1ms)  SAVEPOINT active_record_1
   (0.4ms)  SELECT 1 FROM "users" WHERE (LOWER("users"."email") = LOWER('xaxaupua@gmail.com') AND "users"."id" != 1) LIMIT 1
   (0.1ms)  ROLLBACK TO SAVEPOINT active_record_1
 => false 

谁能告诉我怎么了?有什么提示吗?

(当然我可以更改 user.state="email_confirmed" 但是为什么要使用 state_machine?)

4

3 回答 3

1

额外的 SQL 查询是您验证的结果:

validates :email, :uniqueness => { :case_sensitive => false }

它检查数据库以查看是否id != 1已存在具有该(小写)电子邮件的不同用户 ( )。

于 2012-06-05T22:03:31.987 回答
0

好的,似乎我发现了发生了什么:

每次我对 进行更改时state_machine,例如:

$ u.email_confirm

state_machine内部调用(我User#update_attributesu用户实例),state属性是唯一要更新的。这意味着User#save调用该方法,顺便说一句,在此之前还会检查验证。至于其他属性没有更新,一些验证可能会禁止 save u,所以u.state没有明显改变。

为了克服这个问题,我只是将我所有的验证放在一个状态中。总结一下:

class User < ActiveRecord::Base
    ...
    state_machine :initial => :pending do

       event :email_confirm do
         transition :pending => :email_confirmed
       end

      state :pending do

        validates :name, :presence => true,
                         :length=> {maximum: 50}

        validates :email, :presence => true,
                          :format => { :with => /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i },
                          :uniqueness => { :case_sensitive => false}

        validates :password, :presence => true,
                             :confirmation => true,
                             :length => { :within => 6..40 }
      end
    end
    ...
  end

验证总是在保存之前调用,因此,当从:pendingto转换时:email_confirmedu.state已经有一个值:email_confirmed并且不执行验证。

此外,奇怪的(至少对我而言)SQL 查询

SELECT 1 FROM "users" WHERE (LOWER("users"."email") = LOWER('xaxaupua@gmail.com') AND "users"."id" != 1) LIMIT 1

仅在启用验证时执行。如果我禁用验证,则不会执行此查询。不知道 ActiveRecord 执行该查询的原因。尽管这对我来说现在不是问题,但我会感谢任何对这个问题有一点启发的人,或者向我指出任何解释这种行为的链接。

于 2012-05-01T15:16:08.430 回答
0

默认情况下,对模型上的 :create 和 :update 事件执行验证。我对 state_machine 有类似的问题。我的解决方案是简单地删除 :update 事件的验证,因为电子邮件和名称属性在创建记录后是只读的。例如 :

validates(:name, :presence => true,
        :length => { :maximum => 50},
        :uniqueness =>true,
        :on => :create)
validates(:email, :presence => true,
        :format => {:with => email_regex},
        :uniqueness => { :case_sensitive => false},
        :on => :create)
validates(:password, :presence => true,
        :confirmation => true,
        :length => { :within => 6..40},
        :if => :password)

请注意,如果密码属性已更改,则会执行密码验证。这也避免了您遇到的 state_machine 问题。如果您向用户提供更改名称和电子邮件的访问权限,您也可以将相同的逻辑应用于这些验证器。

于 2013-07-01T18:10:19.620 回答