Rails Devise attr_accessible problem - ruby-on-rails

Im trying to add devise authorization to my rails 3 app.
Its all going well except Im also trying to follow this tutorial to dynamically set attr_accessible for role_ids only for admin users (I dont want regular users changing their role, but an admin should be able to do so)... the problem is, the railscast tutorial approach assumes I have access to change the controller behavior when in fact devise is handling all that under the hood.
Please Help

You can subclass the Devise controllers, you just have to generate the views and move them to the correct place. Check out "Configuring views" and "Configuring controllers" in the Devise readme.
I ended up adding role_ids to attr_accessible, then subclassing the RegistrationsController and adding a before_filter to remove that param for non-admins.
class Users::RegistrationsController < Devise::RegistrationsController
before_filter :remove_admin_params, :only => [:create, :update]
protected
# disable setting the role_ids value unless an admin is doing the edit.
def remove_admin_params
params[:user].delete(:role_ids) unless current_user.try(:admin?)
end
end
Just make sure to add the registration views to /app/views/users/registrations/.

The best way I found to handle this is from RailsCast 237. It is more verbose than Arrel's answer, but it does not force you to add role (or other fields) to attr_accessible.
Add the following method in an initializer:
class ActiveRecord::Base
attr_accessible
attr_accessor :accessible
private
def mass_assignment_authorizer(role = :default)
if accessible == :all
self.class.protected_attributes # hack
else
# super returns a whitelist object
super + (accessible || [])
end
end
end
Then in your controller, you can do:
user.accessible = :role if can? :set_role, resource
This call, unfortunately, has to be made after the user (or whatever) object has been instantiated. That means that you would have to subclass the controller, and call this after the resource instantiation in update and create.
This is for Rails 3.2. In earlier versions I believe the method mass_assignment_authorizer does not take a parameter. The attr_accessible with no values sets a fail-safe application wide denial for mass assignment. This can also be done in the application.rb file with
config.active_record.whitelist_attributes = true

Related

Rails 5: Calling a service class - relation mapping

I am trying to learn how to write a service class in my rails 5 app.
When a user registers with devise, I'm trying to incorporate a service class that makes models associated with the user's account on creation of the user account.
In my devise registrations controller, I have:
class Users::RegistrationsController < Devise::RegistrationsController
before_action :configure_permitted_parameters, if: :devise_controller?
def create
super do |user|
if user.persisted?
User::CompleteRegistration.call(user: user)
end
end
end
protected
def configure_permitted_parameters
devise_parameter_sanitizer.permit(:sign_up, keys: [:first_name, :last_name, :email])
devise_parameter_sanitizer.permit(:account_update, keys: [:first_name, :last_name, :email, ])
end
private
end
In my app / services/ user folder, I have a file called: complete_registration.rb:
class User::CompleteRegistration #< ActiveRecord::Base
def self.call(user: u)
new(user: user).call
end
def initialize(user: u)
self.user = user
end
def call
Profile::SetupService.call(user: user)
Setting::SetupService.call(user: user)
User::OrganisationMapperService.new(user: user).call
end
# def join_an_organisation
# render "organisation_requests/new"
# end
private
attr_accessor :user
end
When I try to save this and then sign up as a user, I can see an error message that says:
NameError - uninitialized constant User::CompleteRegistrations:
I note that it has pluralised the word 'registration'. I don't know if this has something to do with the problem. I tried saving the file as a plural, and renaming the class as a plural, but that didn't work.
The actual error rendered says:
PG::UndefinedTable at /users
ERROR: relation "complete_registration_services" does not exist
LINE 8: WHERE a.attrelid = '"complete_registration_se...
I cant expand the message to get more detail, but there is no relation to be found, because it isnt an active record table that I'm trying to call.
I also tried adding a callback to my user model as an alternative solution:
class User < ApplicationRecord
after_update :after_confirmation_setup
def after_confirmation_setup
return unless !self.confirmed_at.blank?
User::CompleteRegistration.call(user: #user)
end
But that doesnt work either.
Can anyone see how I can setup my app to call a service class on user create (from the registration controller, or any other method)?
By the looks of things, this seems like a naming issue.
From first appearances (I can't say definatively without actually using your code base in console and debugging) but based on a quick scan and looking at the errors you are receiving this appears to be one of Ruby's quirks which I have bumped into a few times.
You have a User class in the global scope (In your models directory), and you have a service class User::CompleteRegistration, which when you call from somewhere is perfectly logical to assume will point to the defined User::CompleteRegistration class. However, Ruby doesn't see it like that.
User::CompleteRegistration can be split into two parts, first User:: Is evaluated, and Ruby searches for a User class, and grabs your model. It then evaluates the CompleteRegistration, and looks for a defined CompleteRegistration that can be used in your User class. This is may be why you are getting the error about a relation. It is searching for a relation within the User model scope.
So essentially, when you write User::CompleteRegistration Ruby doesnt say Right! lets grab a User::CompleteRegistration class! Ruby says Right! lets grab a CompleteRegistration class that can be used within the User scope!
To prevent this, I would perhaps change the naming of your service to something more simple, like: Registration::Complete and avoid any overlapping of class / module names within the global scope.
also on a side note, I too love using services in code, and I don't really want to write a shameless plug on SO but perhaps it your case it can help? I have written a gem to provide easy to use services that has an implementation not too dissimilar to what you are using, perhaps it could be of use, whether you use it as a gem or just scan the code in it, I hope somehow maybe it can help.

Why do functions from my Rails plugin not work without specifically requiring?

I need some help with my plugin. I want to extend ActiveRecord::Base with a method that initializes another method that can be called in the controller.
It will look like this:
class Article < ActiveRecord::Base
robot_catch :title, :text
...
end
My attempt at extending the ActiveRecord::Base class with robot_catch method looks like following. The function will initialize the specified attributes (in this case :title and :text) in a variable and use class_eval to make the robot? function available for the user to call it in the controller:
module Plugin
module Base
extend ActiveSupport::Concern
module ClassMethods
def robot_catch(*attr)
##robot_params = attr
self.class_eval do
def robot?(params_hash)
# Input is the params hash, and this function
# will check if the some hashed attributes in this hash
# correspond to the attribute values as expected,
# and return true or false.
end
end
end
end
end
end
ActiveRecord::Base.send :include, Plugin::Base
So, in the controller, this could be done:
class ArticlesController < ApplicationController
...
def create
#article = Article.new(params[:article])
if #article.robot? params
# Do not save this in database, but render
# the page as if it would have succeeded
...
end
end
end
My question is whether if I am right that robot_catch is class method. This function is to be called inside a model, as shown above. I wonder if I am extending the ActiveRecord::Base the right way. The robot? function is an instance method without any doubt.
I am using Rails 3.2.22 and I installed this plugin as a gem in another project where I want to use this functionality.
Right now, it only works if I specifically require the gem in the model. However, I want it the functionality to be included as a part of ActiveRecord::Base without requiring it, otherwise I'd have to require it in every model I want to use it, not particularly DRY. Shouldn't the gem be automatically loaded into the project on Rails start-up?
EDIT: Maybe callbacks (http://api.rubyonrails.org/classes/ActiveSupport/Callbacks/ClassMethods.html) would be a solution to this problem, but I do not know how to use it. It seems a bit obscure.
First, I would suggest you make sure that none of the many many built in Rails validators meet your needs.
Then if that's the case, what you actually want is a custom validator.
Building a custom validator is not as simple as it might seem, the basic class you'll build will have this structure:
class SpecialValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
# Fill this with your validation logic
# Add to record.errors if validation fails
end
end
Then in your model:
class Article < ActiveRecord::Base
validates :title, :text, special: true
end
I would strongly suggest making sure what you want is not already built, chances are it is. Then use resources like this or ruby guides resources to continue going down the custom validator route.
Answer
I found out the solution myself. Bundler will not autoload dependencies from a gemspec that my project uses, so I had to require all third party gems in an engine.rb file in the lib/ directory of my app in order to load the gems. Now everything is working as it should.
Second: the robot_catch method is a class method.

Ruby - How to access module's methods?

I'm installing a forum using the Forem gem. There's an option that allows avatar personalization, since it's possible to login with Facebook. You just specify your method in the User model and that's it.
# Forem initializer
Forem.avatar_user_method = 'forem_avatar'
# User model
def forem_avatar
unless self.user_pic.empty?
self.user_pic
end
end
But I want a fallback on Gravatar for normal, non-facebook accounts. I've found the method on Forem and in theory, I need to call the avatar_url method:
# User model
def forem_avatar
unless self.user_pic.empty?
self.user_pic
else
Forem::PostsHelper.avatar_url self.email
end
end
However, Forem isn't an instance, but a module and I can't call it nor create a new instance. The easy way is to copy the lines of that method, but that's not the point. Is there a way to do it?
Thanks
Update
Both answers are correct, but when I call the method either way, there's this undefined local variable or method 'request' error, which is the last line of the original avatar_url.
Is there a way to globalize that object like in PHP? Do I have to manually pass it that argument?
perhaps reopen the module like this:
module Forem
module PostsHelper
module_function :avatar_url
end
end
then call Forem::PostsHelper.avatar_url
if avatar_url call other module methods, you'll have to "open" them too via module_function
or just include Forem::PostsHelper in your class and use avatar_url directly, without Forem::PostsHelper namespace
If you want to be able to use those methods in the user class, include them and use
class User < ActiveRecord::Base
include Forem::PostsHelper
def forem_avatar
return user_pic if user_pic.present?
avatar_url email
end
end
Another way would be to set the Forem.avatar_user_method dynamically since the Forem code checks it it exists before using it and defaults to avatar_url if it does not.
class User < ActiveRecord::Base
# This is run after both User.find and User.new
after_initialize :set_avatar_user_method
# Only set avatar_user_method when pic is present
def set_avatar_user_method
unless self.user_pic.empty?
Forem.avatar_user_method = 'forem_avatar'
end
end
def forem_avatar
self.user_pic
end
end
This way you dont pollute your model with unnecessary methods from Forem and don't monkey patch Forem itself.

Is the following naming convention needed in a rails project?

My project name is clog, so I named my models and controllers like this: Clog::User Clog::Userscontroller.
Is this naming convention mandatory?
No, in a conventional Rails project, that's not necessary. Just name your models and controllers the usual way, like eg User or UsersController.
The other thing is that, when your project grows in size, you may need to organize your models into submodules. One approach to do so is extending your models with app concerns, as show eg here or here.
As for organizing controllers, one approach is to create a module in the lib directory, which you then include in your ApplicationController, like so:
In lib/authentication.rb:
module Authentication
def self.included(base)
base.send :before_filter, :login_required
base.send :helper_method, :current_user, :logged_in?
end
def current_user
#current_user ||= User.find_by_remember_token(cookies[:remember_token]) if cookies[:remember_token].present?
end
#...
end
In app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
include Authentication
#...
end
For this to work, you need to add
config.autoload_paths << "#{config.root}/lib"
to your config/application.rb file
However, if you plan to build your Rails project as a Rails Engine, you may want to follow some naming convention. A good example of a Rails Engine is forem.
Yes, following the naming convention helps a great deal because not only does rails use it to generate other names, but other gems as well.
Specific to your question, you may be asking if you need to name the controller as UserController given that your model is called User. That is not necessary at all, and you may call it anything else if it better fits your purpose.
In this case, you will probably want to create a few controllers like so:
My::AccountController # for e.g.. /my/account
Admin::UsersController # for e.g. /admin/users/1
For a user, you refer to your own user record, as 'your account' so this makes more sense. However, the administrator's perspective would be to manage user records. You may also name a controller one thing and serve it under a different route. In your routes file, you may do this:
namespace :admin do
resources :users, :path => "user-accounts"
end
To reiterate, your model name need not match up to the controller name. They are only named similarly by association: UserController is understood to handle User records.

Determine the domain in an ActiveRecord model

I am in the middle of migrating my application from using subdirectories for userspace to subdomains (ie. domain.com/~user to user.domain.com). I've got a method in my user class currently to get the "home" URL for each user:
class User
def home_url
"~#{self.username}"
# How I'd like to do it for subdomains:
#"http://#{self.username}.#{SubdomainFu.host_without_subdomain(request.host)}"
end
end
I'd like to update this for subdomains, but without hardcoding the domain into the method. As you can see, I am using the subdomain-fu plugin, which provides some methods that I could use to do this, except that they need access to request, which is not available to the model.
I know it's considered bad form to make request available in a model, so I'd like to avoid doing that, but I'm not sure if there's a good way to do this. I could pass the domain along every time the model is initialized, I guess, but I don't think this is a good solution, because I'd have to remember to do so every time a class is initialized, which happens often.
The model shouldn't know about the request, you're right. I would do something like this:
# app/models/user.rb
class User
def home_url(domain)
"http://#{username}.#{domain}"
end
end
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
# ...
def domain
SubdomainFu.host_without_subdomain(request.host)
end
# Make domain available to all views too
helper_method :domain
end
# where you need it (controller or view)
user.home_url(domain)
If there is such a thing as a canonical user home URL, I would make a configurable default domain (e.g. YourApp.domain) that you can use if you call User#home_url without arguments. This allows you to construct a home URL in places where, conceptually, the "current domain" does not exist.
While molf's answer is good, it did not solve my specific problem as there were some instances where other models needed to call User#home_url, and so there would be a lot of methods I'd have to update in order to pass along the domain.
Instead, I took inspiration from his last paragraph and added a base_domain variable to my app's config class, which is the set in a before_filter in ApplicationController:
module App
class << self
attr_accessor :base_domain
end
end
class ApplicationController < ActionController::Base
before_filter :set_base_domain
def set_base_domain
App.base_domain = SubdomainFu.host_without_subdomain(request.host)
end
end
And thus, when I need to get the domain in a model, I can just use App.base_domain.

Resources