I'm trying to create a dynamic interface. Where my model classes exist and my controllers are dynamically created when launching the application.
Everything happens in my routes file where the resources are created!
ActionController::Routing::Routes.draw do |map|
map.namespace :admin do |admin|
TestAdmin.models.each do |m|
admin.resources m.to_s.tableize.to_sym
end
end
end
And then there is my BeAdmin class, this does the following:
module TestAdmin
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def beadmin(options = {})
namespace_name = "Admin"
class_name = "#{self.to_s.pluralize.capitalize}Controller"
klass = namespace_name.constantize.const_set(class_name, Class.new(ApplicationController))
klass.module_eval do
def index
render :text => "test"
end
end
end
end
def self.models
all_models = []
Dir.chdir(File.join(Rails.root, "app/models")) do
Dir["**/*.rb"].each do |m|
class_name = m.sub(/\.rb$/,"").camelize
klass = class_name.split("::").inject(Object){ |klass,part| klass.const_get(part) }
all_models << "#{class_name}" if klass < ActiveRecord::Base && !klass.abstract_class?
end
end
all_models
end
end
And when you now browse to the /admin/users (from the User model) then you get to see "test". so it works great!
But then I just do a simple refresh of the browser and The controller that is called becomes UsersController#index instead of Admin::UsersController#index... He loses it's namespace for some reason...
Maybe another important aspect here is that I added all this as a plugin and user Rails Engines so I can make a plug-able interface...
But so far no luck because my routes seem to get lost somewhere!
Thanks in advance for your help!
Jelle
Related
I have and active admin resource. How i can dynamic extend resource. I try do it like this:
ActiveAdmin.register Order do
include UpdatePriceBlock
price_blocks_names names: [:last, :actual]
end
module UpdatePriceBlock
extend ActiveSupport::Concern
def price_blocks_names(options = {})
#price_blocks_names ||= options[:names]
end
def self.included(base)
#price_blocks_names.each do |name|
base.send :member_action, name, method: :get do
end
end
end
end
Now I has an error:
undefined method `price_blocks_names' for #<ActiveAdmin::ResourceDSL
This is a possible way, I don't know yet how you could keep the names inside the active admin register block. Add the price_blocks_names to your model:
class Order < ApplicationRecord
def self.price_bloks_names
%i(last actual)
end
end
And then place this in config/initializers/active_admin_update_price_block.rb
module ActiveAdminUpdatePriceBlock
def self.extended(base)
base.instance_eval do
self.controller.resource_class.price_bloks_names.each do |name|
member_action name, method: :get do
raise resource.inspect
end
end
end
end
end
Now you can extend, but the configuration needs to reside in the model as a class method this way. Haven't found a cleaner way so far.
ActiveAdmin.register Order do
extend UpdatePriceBlock
end
I think I found it:
ActiveAdmin.register Order do
controller do
include UpdatePriceBlock
end
end
What's going on:
Within the register Order do block, self is a special Active Admin thing:
ActiveAdmin.register Order do
puts "What's self here? #{self}"
end
=>
What's self here? #<ActiveAdmin::ResourceDSL:0x000000012b948230>
Within the controller do block, it's the controller class (so, pretty much the same as the body of a class definition):
ActiveAdmin.register Order do
controller do
puts "What's self here? #{self}"
include UpdatePriceBlock
end
end
=> What's self here? Admin::OrdersController
Within a member_action block, it's an instance of the controller, just like in a regular Rails controller action:
ActiveAdmin.register Order do
member_action :action do
puts "What's self here? #{self}"
end
end
=> What's self here? #<Admin::OrdersController:0x00000001259e7e80>
I am building a Ruby on Rails app where researchers can run studies.
I would like a new sample study to be created for a researcher (user) when they first sign up. This is a bit different to a db seed since it will need to dynamically create the study for this particular user, rather than once for the entire database.
I'm using Devise for user accounts.
What's a good approach for this? Thank you!
You can do it by overriding the Devise::RegistrationsController Devise::InvitationsController.
# feel free to call this class whatever you want.
class Users::InvitationsController < Devise::InvitationsController
def create
# the block is yielded after the resource has been saved
# but before anything has been rendered.
super do |user|
if user.valid?
user.studies.create(title: 'New study')
end
end
end
end
And then we need to tell Devise to route to our custom controller:
# config/routes.rb
devise_for :users, controllers: {
registrations: 'users/registrations',
invitations: 'users/invitations'
}
If you want to extract the creation process from the controller you can use a factory method or a service object:
class Study < ActiveRecord::Base
# ...
def self.create_default(**kwargs)
# the default options are merged with the keyword arguments
attrs = {
title: 'Foo',
bar: 'Baz'
}.merge(kwargs)
study = scoped.build(attrs)
# lets us pass a block just like .new and .create
yield study if block_given?
study
end
end
# app/controllers/user/registrations_controller.rb
# ...
def create
# the block is yielded after the resource has been saved
# but before anything has been rendered.
super do |user|
if user.valid?
study = user.studies.create_default(baz: 'Something else')
study.save
end
end
end
config/routes.rb
devise_for :users, :controllers => { :sessions => "sessions" }
app/controllers/sessions_controller.rb
class SessionsController < Devise::SessionsController
before_action :before_login, only: :create
after_action :after_login, only: :create
def before_login
end
def after_login
if current_user.sign_in_count == 1
User.studies.create(title: 'New study')
end
end
end
I want to create a method that, when called from a controller, will add a nested resource route with a given name that routes to a specific controller. For instance, this...
class Api::V1::FooController < ApplicationController
has_users_route
end
...should be equivalent to...
namespace :api do
namespace :v1 do
resources :foo do
resources :users, controller: 'api_security'
end
end
end
...which would allow them to browse to /api/v1/foo/:foo_id/users and would send requests to the ApiSecurityController. Or would it go to Api::V1::ApiSecurityController? It frankly doesn't matter since they're all in the same namespace. I want to do it this way because I want to avoid having dozens of lines of this:
resources :foo do
resources :users, controller: 'api_security'
end
resources :bar do
resources :users, controller: 'api_security'
end
Using a method is easier to setup and maintain.
I'm fine as far as knowing what to do once the request gets to the controller, but it's the automatic creation of routes that I'm a little unsure of. What's the best way of handling this? The closest I've been able to find is a lot of discussion about engines but that doesn't feel appropriate because this isn't separate functionality that I want to add to my app, it's just dynamic routes that add on to existing resources.
Advice is appreciated!
I ended up building on the blog post suggested by #juanpastas, http://codeconnoisseur.org/ramblings/creating-dynamic-routes-at-runtime-in-rails-4, and tailoring it to my needs. Calling a method from the controllers ended up being a bad way to handle it. I wrote about the whole thing in my blog at http://blog.subvertallmedia.com/2014/10/08/dynamically-adding-nested-resource-routes-in-rails/ but the TL;DR:
# First draft, "just-make-it-work" code
# app/controllers/concerns/user_authorization.rb
module UserAuthorization
extend ActiveSupport::Concern
module ClassMethods
def register_new_resource(controller_name)
AppName::Application.routes.draw do
puts "Adding #{controller_name}"
namespace :api do
namespace :v1 do
resources controller_name.to_sym do
resources :users, controller: 'user_security', param: :given_id
end
end
end
end
end
end
end
# application_controller.rb
include UserAuthorization
# in routes.rb
['resource1', 'resource2', 'resource3'].each { |resource| ApplicationController.register_new_resource(resource) }
# app/controllers/api/v1/user_security_controller.rb
class Api::V1::UserSecurityController < ApplicationController
before_action :authenticate_user!
before_action :target_id
def index
end
def show
end
private
attr_reader :root_resource
def target_id
# to get around `params[:mystery_resource_id_name]`
#target_id ||= get_target_id
end
def get_target_id
#root_resource = request.fullpath.split('/')[3].singularize
params["#{root_resource}_id".to_sym]
end
def target_model
#target_model ||= root_resource.capitalize.constantize
end
def given_id
params[:given_id]
end
end
What's a cool way to protect attributes by role using declarative_authorization? For example, a user can edit his contact information but not his role.
My first inclination was to create multiple controller actions for different scenarios. I quickly realized how unwieldy this could become as the number of protected attributes grows. Doing this for user role is one thing, but I can imagine multiple protected attributes. Adding a lot controller actions and routes doesn't feel right.
My second inclination was to create permissions around specific sensitive attributes and then wrap the form elements with View hepers provided by declarative_authorizations. However, the model and controller aspect of this is a bit foggy in my mind. Suggestions would be awesome.
Please advise on the best way to protect attributes by role using declarative_authorizations.
EDIT 2011-05-22
Something similar is now in Rails as of 3.1RC https://github.com/rails/rails/blob/master/activerecord/test/cases/mass_assignment_security_test.rb so I would suggest going that route now.
ORIGINAL ANSWER
I just had to port what I had been using previously to Rails 3. I've never used declarative authorization specifically, but this is pretty simple and straightforward enough that you should be able to adapt to it.
Rails 3 added mass_assignment_authorizer, which makes this all really simple. I used that linked tutorial as a basis and just made it fit my domain model better, with class inheritance and grouping the attributes into roles.
In model
acts_as_accessible :admin => :all, :moderator => [:is_spam, :is_featured]
attr_accessible :title, :body # :admin, :moderator, and anyone else can set these
In controller
post.accessed_by(current_user.roles.collect(&:code)) # or however yours works
post.attributes = params[:post]
lib/active_record/acts_as_accessible.rb
# A way to have different attr_accessible attributes based on a Role
# #see ActsAsAccessible::ActMethods#acts_as_accessible
module ActiveRecord
module ActsAsAccessible
module ActMethods
# In model
# acts_as_accessible :admin => :all, :moderator => [:is_spam]
# attr_accessible :title, :body
#
# In controller
# post.accessed_by(current_user.roles.collect(&:code))
# post.attributes = params[:post]
#
# Warning: This frequently wouldn't be the concern of the model where this is declared in,
# but it is so much more useful to have it in there with the attr_accessible declaration.
# OHWELL.
#
# #param [Hash] roles Hash of { :role => [:attr, :attr] }
# #see acts_as_accessible_attributes
def acts_as_accessible(*roles)
roles_attributes_hash = Hash.new {|h,k| h[k] ||= [] }
roles_attributes_hash = roles_attributes_hash.merge(roles.extract_options!).symbolize_keys
if !self.respond_to? :acts_as_accessible_attributes
attr_accessible
write_inheritable_attribute :acts_as_accessible_attributes, roles_attributes_hash.symbolize_keys
class_inheritable_reader :acts_as_accessible_attributes
# extend ClassMethods unless (class << self; included_modules; end).include?(ClassMethods)
include InstanceMethods unless included_modules.include?(InstanceMethods)
else # subclass
new_acts_as_accessible_attributes = self.acts_as_accessible_attributes.dup
roles_attributes_hash.each do |role,attrs|
new_acts_as_accessible_attributes[role] += attrs
end
write_inheritable_attribute :acts_as_accessible_attributes, new_acts_as_accessible_attributes.symbolize_keys
end
end
end
module InstanceMethods
# #param [Array, NilClass] roles Array of Roles or nil to reset
# #return [Array, NilClass]
def accessed_by(*roles)
if roles.any?
case roles.first
when NilClass
#accessed_by = nil
when Array
#accessed_by = roles.first.flatten.collect(&:to_sym)
else
#accessed_by = roles.flatten.flatten.collect(&:to_sym)
end
end
#accessed_by
end
private
# This is what really does the work in attr_accessible/attr_protected.
# This override adds the acts_as_accessible_attributes for the current accessed_by roles.
# #see http://asciicasts.com/episodes/237-dynamic-attr-accessible
def mass_assignment_authorizer
attrs = []
if self.accessed_by
self.accessed_by.each do |role|
if self.acts_as_accessible_attributes.include? role
if self.acts_as_accessible_attributes[role] == :all
return self.class.protected_attributes
else
attrs += self.acts_as_accessible_attributes[role]
end
end
end
end
super + attrs
end
end
end
end
ActiveRecord::Base.send(:extend, ActiveRecord::ActsAsAccessible::ActMethods)
spec/lib/active_record/acts_as_accessible.rb
require 'spec_helper'
class TestActsAsAccessible
include ActiveModel::MassAssignmentSecurity
extend ActiveRecord::ActsAsAccessible::ActMethods
attr_accessor :foo, :bar, :baz, :qux
acts_as_accessible :dude => [:bar], :bra => [:baz, :qux], :admin => :all
attr_accessible :foo
def attributes=(values)
sanitize_for_mass_assignment(values).each do |k, v|
send("#{k}=", v)
end
end
end
describe TestActsAsAccessible do
it "should still allow mass assignment to accessible attributes by default" do
subject.attributes = {:foo => 'fooo'}
subject.foo.should == 'fooo'
end
it "should not allow mass assignment to non-accessible attributes by default" do
subject.attributes = {:bar => 'baaar'}
subject.bar.should be_nil
end
it "should allow mass assignment to acts_as_accessible attributes when passed appropriate accessed_by" do
subject.accessed_by :dude
subject.attributes = {:bar => 'baaar'}
subject.bar.should == 'baaar'
end
it "should allow mass assignment to multiple acts_as_accessible attributes when passed appropriate accessed_by" do
subject.accessed_by :bra
subject.attributes = {:baz => 'baaaz', :qux => 'quuux'}
subject.baz.should == 'baaaz'
subject.qux.should == 'quuux'
end
it "should allow multiple accessed_by to be specified" do
subject.accessed_by :dude, :bra
subject.attributes = {:bar => 'baaar', :baz => 'baaaz', :qux => 'quuux'}
subject.bar.should == 'baaar'
subject.baz.should == 'baaaz'
subject.qux.should == 'quuux'
end
it "should allow :all access" do
subject.accessed_by :admin
subject.attributes = {:bar => 'baaar', :baz => 'baaaz', :qux => 'quuux'}
subject.bar.should == 'baaar'
subject.baz.should == 'baaaz'
subject.qux.should == 'quuux'
end
end
To me this filtering problem is something that should be applied at the controller level.
You'll want to have something somewhere that defines how to decide which attributes are writeable for a given user.
# On the user model
class User < ActiveRecord::Base
# ...
# Return a list of symbols representing the accessible attributes
def self.allowed_params(user)
if user.admin?
[:name, :email, :role]
else
[:name, email]
end
end
end
Then, in the application controller you can define a method to filter parameters.
class ApplicationController < ActionController::Base
# ...
protected
def restrict_params(param, model, user)
params[param].reject! do |k,v|
!model.allowed_params(user).include?(k)
end
end
# ...
end
And finally in your controller action you can use this filter:
class UserController < ActionController::Base
# ...
def update
restrict_params(:user, User, #current_user)
# and continue as normal
end
# ...
end
The idea is that you could then define allowed_params on each of your models, and have the controllers for each of these use the same filter method. You could save some boilerplate by having a method in application controller that spits out a before filter, like this:
def self.param_restrictions(param, model)
before_filter do
restrict_params(param, model, #current_user) if params[param]
end
end
# in UserController
param_restrictions :user, User
These examples are intended to be illustrative rather than definitive, I hope they help with the implementation of this.
I'd use scoped_attr_accessible, which looks like just what you're looking for. Only you need to set the scope at the start of a request for all models.
To do that, use a before_filter in your application_controller.rb:
before_filter do |controller|
ScopedAttrAccessible.current_sanitizer_scope = controller.current_user.role
end
I would avoid every solution based on user access in model because it seems potentially dangerous. I would try this approach:
class User < ActiveRecord::Base
def update_attributes_as_user(values, user)
values.each do |attribute, value|
# Update the attribute if the user is allowed to
#user.send("#{attribute}=", value) if user.modifiable_attributes.include?(attribute)
end
save
end
def modifiable_attributes
admin? ? [:name, :email, :role] : [:name, :email]
end
end
Then in your controller change your update action from:
#user.update_attributes(params[:user])
to
#user.update_attributes_as_user(params[:user], current_user)
Rails 3.1+ comes with a +assign_attributes+ method for this purpose - http://apidock.com/rails/ActiveRecord/AttributeAssignment/assign_attributes.
In Rails, is it possible to namespace models in modules and still get correct behavior from url_for?
For instance, here, url_for works as expected:
# app/models/user.rb
class User < ActiveRecord::Base
end
# config/routes.rb
resources :users
# app/views/users/index.html.haml
= url_for(#user) # /users/1
Whereas after putting the User model into a module, url_for complains about an undefined method m_user_path:
# app/models/m/user.rb
module M
class User < ActiveRecord::Base
end
end
# config/routes.rb
resources :users
# app/views/users/index.html.haml
= url_for(#user) # undefined method 'm_users_path'
Is it possible to have url_for ignore the module in M::User and return user_path for url_for(#user) instead of m_user_path?
UPDATE
So, after almost 5 years, here's the solution, thanks to esad. This has been tested in Rails 4.2.
# app/models/m/user.rb
module M
class User < ActiveRecord::Base
end
end
# app/models/m.rb
module M
def self.use_relative_model_naming?
true
end
def self.table_name_prefix
'm_'
end
end
# config/routes.rb
resources :users
# app/views/users/index.html.haml
= url_for(#user) # /users/1
Note: when generating model, view and controller with bin/rails g scaffold m/user, the views and the controller will be namespaced, too. You need to move app/views/m/users to app/views/users and app/controllers/m/users_controller.rb to app/controllers/users_controller.rb; you also need to remove references to the module M everywhere except in the model M::User.
Finally, the goal here was to namespace models but not views and controllers. With esads solution, the module M (containing User) is explicitly told to not appear in routes. Thus, effectifely, the M is stripped of and only User remains.
The user model can now reside in app/views/models/m/user.rb, the users controller lives in app/views/controllers/users_controller.rb and the views can be found in app/views/users.
Just define use_relative_model_naming? in the containing module to avoid prefixing the generated route names:
module M
def self.use_relative_model_naming?
true
end
end
Use
namespace "blah" do
resources :thing
end
Then routes will be named appropiately.
rake routes
To view all routes
Specify the module on the route
resources :users, :module => "m"
or use scope to do it
scope :module => "m" do
resources :users
end
In my case I overridden the url_for method on my application_helper.rb file to add the :network param on all routes from my namespace :mkp.
module ApplicationHelper
def url_for(options = {})
if options.is_a?(Hash) && options.has_key?(:controller)
if options[:network].nil? && options[:controller].match(/^mkp\//).present?
options[:network] = #network.downcase
end
end
super(options)
end
end