I'm learning SOLID and trying to introduce SRP into my rails app. I have the following user model with basic authentication:
class User < ActiveRecord::Base
attr_accessible :password, :password_confirmation
attr_accessor :password
before_save :encrypt_password
validates_confirmation_of :password
validates_presence_of :password, :on => :create
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
def self.generate_random_password
return ActiveSupport::SecureRandom.hex(12)
end
end
I want to move all the authentication logic to a module like so:
module Authentication
attr_accessible :password, :password_confirmation
attr_accessor :password
before_save :encrypt_password
validates_confirmation_of :password
validates_presence_of :password, :on => :create
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
def self.generate_random_password
return ActiveSupport::SecureRandom.hex(12)
end
end
And my user model would be like this:
class User < ActiveRecord::Base
include Authentication #SRP in action! :P
end
And now the errors begin:
undefined method `attr_accessible' for Authentication:Module
How would I fix this error? I am convinced this is the best start to introduce SRP to my Rails app.
Thanks
The attr_accessible method is called in the wrong scope. Take a look at Concerns to fix this:
http://api.rubyonrails.org/classes/ActiveSupport/Concern.html
This would result in:
module Authentication
extend ActiveSupport::Concern
included do
attr_accessible :password, :password_confirmation
end
...
end
This will also take care of you class and instance method definitions.
NOTE: To be specific, this does not quite achieve SRP, since multiple responsibilities are still shared within the same class, even though they are separated into modules. Class composition through referencing or decorating would be a more strict solution , but I prefer the pragmatic approach of modules.
Related
I've got a standard User model, it has in it an admin boolean. All's well and good for users that have that set to true, but for normal users I get this error:
undefined local variable or method `current_user'
app/models/doc.rb:18:in `mine'
app/controllers/docs_controller.rb:9:in `index'
The Doc model on line 18 reads like this:
def self.mine
where(:user_id => current_user.name, :retired => "active").order('created_at DESC')
end
My User model looks like this:
class User < ActiveRecord::Base
devise :database_authenticatable, :registerable, :recoverable, :rememberable, :trackable, :validatable
attr_accessor :current_password
attr_accessible :name, :password, :password_confirmation, :current_password, :email, :remember_me, :admin
end
class Ability
include CanCan::Ability
def initialize(user)
can :manage, :all if user.admin
end
end
And in my application controller I have the following:
class ApplicationController < ActionController::Base
protect_from_forgery
after_filter :user_activity
rescue_from CanCan::AccessDenied do |exception|
redirect_to root_path
end
def admin?
self.admin == true
end
def authenticate_admin
redirect_to :new_user_session_path unless current_user && current_user.admin?
end
private
def user_activity
current_user.try :touch
end
end
I think that's everything relevant. I can't for the life of me figure this out.
the current_user helper is a controller method that is not accessible from a model. You should pass current user in as a parameter from the controller to the model.
def self.mine(current_user)
where(:user_id => current_user.name, :retired => "active").order('created_at DESC')
end
EDIT: Side note
It looks like user_id is a string in your logic. If this is what you are doing, you should reconsider. Setting up a belongs_to and a has_many with rails using identifiers in the database is far more maintainable. Using string ids is unconventional, and its a rabbit hole that ends in very bad places.
This is EXTREMELY bizarre. I'm upgrading a Rails 2.3.12 app and running into this same problem over and over again. I'm stumped and nothing else out there seems to touch on it.
I have two models:
class User < ActiveRecord::Base
has_many :logs, :class_name => 'UserLog'
end
and
class UserLog < ActiveRecord::Base
attr_accessor :site_id, :controller, :action, :url, :session
belongs_to :user
validates_presence_of :user
end
then in another controller I'm doing this:
def log_user_activity
#current_user.logs.create(:site_id => #site.id, :controller => params[:controller],
:action => params[:action], :url => request.path,
:session => request.session_options[:id]) if #current_user
end
as you can see, it's pretty straightforward but when I call log_user_activity I'm getting this:
Can't mass-assign protected attributes: site_id, controller, action, url, session
HOWEVER, if I change all my creates or builds to this:
def log_user_activity
log = #current_user.logs.new
log.site_id = #site.id
log.controller = params[:controller]
log.action = params[:action]
log.url = request.path
log.session = request.session_options[:id]
log.save
end
then it works fine!?
Has anyone seen this? Any clues?
In class UserLog, add the following:
attr_accessible :site_id, :controller, :action, :url, :session
The reason you have to use attr_accessible is most likely because you are utilizing a plugin that is relying on this being present for a model. It has happened to all of us and is a royal pita)
Once attr_accessible is designated for a class, then any attribute that is not specified as 'accessible' will not be allowed to be updated.
I'm importing heaps of student data from an spreadsheet document. Each row of student data will represent a new user, however, the possibility of importing an already existing student exists and I want to bypass some of my user validations such as username uniqueness accordingly so that I can build associations for both new and existing records, but only if they're being imported to the same school.
Thus far I have the following validation setup in my User model:
user.rb
validates_uniqueness_of :username, :unless => :not_unique_to_school?
def not_unique_to_school?
user = find_by_username(self.username)
user.present? && user.school_id == 6
end
Now how would I go about replacing that 6 with a value I have access to in the controller? Instructors will be the ones handling the importing and they'll be importing students to their school so I would typically run current_user.school_id to retrieve the school id that I want them to be imported to, but I don't have access to the current_user helper in my model.
I'm not concerned about duplicating usernames as I'll be handling that on a different step, this is just the preliminary validation.
Edit
Simplified school & user model:
user.rb
class User < ActiveRecord::Base
# Setup accessible (or protected) attributes for your model
attr_accessible :email, :password, :password_confirmation, :remember_me, :username,
:first_name, :last_name, :school_id, :roles_mask
belongs_to :school
validates_presence_of :username, :on => :create, :message => "can't be blank"
validates_uniqueness_of :username, :unless => :unique_to_school?
def unique_to_school?
user = find_by_username(self.username)
user.present? && user.school_id == 6
end
def find_by_username(username)
User.where(:username => username).first
end
end
school.rb
class School < ActiveRecord::Base
attr_accessible :country_id, :name, :state_id
has_many :users
end
I'd add a method to your School model:
def student_named?(name)
self.users.where(:username => name).any?
end
then in your validation:
def not_unique_to_school?
self.school.student_named?(self.username)
end
Here's what ended up working for me:
validate :user_cant_be_duplicate_in_other_schools
def user_cant_be_duplicate_in_other_schools
errors.add(:username, :taken) if User.count(:conditions => ["school_id != ? AND username = ?", self.school_id, self.username]) > 0
end
As opposed to testing if a User belongs to a particular school we're testing for the lack of belonging to a particular school. I didn't come up with this answer, another user posted this as an answer but deleted it shortly after for reasons unknown.
I'm not able to access the values, passed as option in 'validates_with'
My model:
class Person < ActiveRecord::Base
include ActiveModel::Validations
attr_accessible :name, :uid
validates :name, :presence => "true"
validates :uid, :presence => "true"
validates_with IdValidator, :attr => :uid
My Custom Validator:
Class IdValidator < ActiveModel::Validator
def validate(record)
puts options[:attr]
...
...
end
end
For testing purpose, I'm printing "options[:attr]" and all I see is ":uid" in the terminal and not the value in it. Please help!
When you pass in :attr => :uid, you're just passing in a symbol. There's no magic happening hereāit just takes the hash of options you've attached and delivers it as the options hash. So when you write it, you see the symbol you've passed.
What you probably want is
Class IdValidator < ActiveModel::Validator
def validate(record)
puts record.uid
...
...
end
end
Because validates_with is a class method, you can't get the values of an individual record in the options hash. If you are interested in a more DRY version, you could try something like:
class IdValidator < ActiveModel::Validator
def validate(record)
puts record[options[:field]]
end
end
class Person < ActiveRecord::Base
include ActiveModel::Validations
attr_accessible :name, :uid
validates :name, :presence => "true"
validates :uid, :presence => "true"
validates_with IdValidator, :field => :uid
end
Where you pass in the name of the field you want evaluated.
I was trying to follow the railscasts tutorial that explains how to handle mass-assignment errors and attr_accessible for admins, but since that was a little outdated, I'm trying to follow what's in the rails API dock for 3.2.6 here.
All I want to do is allow the admin user the ability to access the "winning" attribute for the Proposal Model on the Update action.
Here's my Proposal Model showing the current attr_accessible.
class Proposal < ActiveRecord::Base
attr_accessible :email, :email_confirmation, :link, :name, :references, :short_description
belongs_to :idea
Here's my code for the Proposal Controller's Update action.
class ProposalsController < ApplicationController
include ActiveModel::MassAssignmentSecurity
attr_accessible :email, :email_confirmation, :link, :name, :references, :short_description
attr_accessible :email, :email_confirmation, :link, :name, :references, :short_description, :winning, :as => :admin
def update
#idea = Idea.find(params[:idea_id])
#proposal = #idea.proposals.find(params[:id])
if #proposal.update_attributes(proposal_params)
redirect_to idea_proposals_url(#idea)
else
render 'edit'
end
end
protected
def proposal_params
role = current_user.admin ? :admin : :default
sanitize_for_mass_assignment(params[:proposal], role)
end
Check out this Railscast. I had a similar issue with an Admin field Boolean and didn't want any user to circumvent the security by sending a curl post. If the user is an admin then it gave them the ability to access the field, otherwise Mass Assignment would protect the field from being modified.
http://railscasts.com/episodes/237-dynamic-attr-accessible?view=asciicast