2012-04-29 59 views
1

我想通過state_machine來讓我的用戶在Rails 3.1.3應用程序的註冊過程中有一個狀態。我試圖做一個非常簡單的案例,但我不能通過事件來改變它的狀態。在重新閱讀文檔sevreal時間後,我沒有發現什麼是錯的。rails state_machine不會改變狀態

我的用戶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+\-.][email protected][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 

當然,我已經做了遷移,以使用戶以acomodate狀態機

$ 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: "[email protected]", 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('[email protected]') 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: "[email protected]", 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('[email protected]') AND "users"."id" != 1) LIMIT 1 
    (0.1ms) ROLLBACK TO SAVEPOINT active_record_1 
=> false 

有人可以告訴我什麼是錯?任何提示?

(?當然我coud改變user.state =「email_confirmed」但當時爲什麼要使用state_machine)

+0

問題出現在你的'email_confirm'中。它返回false,並且該查詢很奇怪。爲什麼它在做'用戶'。「id」!= 1'? –

+0

好吧...... email_confirm在state_machine中定義,只需將它從:pendant更改爲:email_confirmed,但不會超過此值,正如您在帖子開頭處所看到的那樣。 –

+0

你是否在使用一些創意會話,如設計? –

回答

1

OK,看來我發現發生的事情:

每次我做出改變的state_machine,如,例如:

$ u.email_confirm 

state_machine內部調用User#update_attributesu(我的用戶實例)與state屬性作爲國內唯一一家進行更新。這意味着User#save方法被調用,並且在驗證之前也檢查BTW。至於其他屬性沒有更新,一些驗證可能禁止保存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+\-.][email protected][a-z\d\-.]+\.[a-z]+\z/i }, 
          :uniqueness => { :case_sensitive => false} 

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

驗證總是叫剛剛保存過,所以,從:pending過渡到:email_confirmed時,u.state早已值:email_confirmed並且不執行驗證。

此外,奇怪的(至少對我來說)的SQL查詢

SELECT 1 FROM "users" WHERE (LOWER("users"."email") = LOWER('[email protected]') AND "users"."id" != 1) LIMIT 1 

被驗證時才能啓用執行。如果我禁用驗證,則不執行此查詢。不知道爲什麼ActiveRecord執行該查詢。儘管現在對我來說這不是一個問題,但我要感謝所有在這個問題上有所啓發的人,或者指出解釋這種行爲的任何鏈接。

1

額外的SQL查詢您的驗證結果:

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

它檢查數據庫,看看是否有不同的用戶(id != 1)與(套管下)的電子郵件已經存在。

0

默認情況下,針對:創建和更新模型上的事件執行驗證。我和state_machine有類似的問題。我的解決方案是簡單地刪除:update事件的驗證,因爲電子郵件和名稱attribs在記錄創建後是隻讀的。例如:

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) 

請注意,如果密碼attrib已更改,則執行密碼驗證。這也可以避免你遇到的state_machine問題。如果您向用戶提供更改名稱和電子郵件的權限,則您也可以將相同的邏輯應用於這些驗證器。