ActiveAdmin - customising the records that are shown - ruby-on-rails

So I've got an app where users (Devise) have the ability to see either all, or a subset of main model (in this case Schools), depending on whether the user is at branch, region or national level.
Branch belongs_to Region
School belongs_to Branch
What I'd like to do is to be able to wire up the permissions (maybe with a scope) in such a way as to be transparent to ActiveAdmin. The user logs in to ActiveAdmin and is presented with a list of only the schools they're allowed to see.
So I guess this could either be an ActiveAdmin solution or something at a lower level.
Any ideas would be very welcome :)

You could set it up so a user has a polymorphic association to either a school, a branch or a region. If this association is nil it would mean that the user has access to everything (the national level you mentioned).
class User < ActiveRecord::Base
belongs_to :administrates, :polymorphic => true
end
class School < ActiveRecord::Base
belongs_to :branch
has_many :users, :as => :administrates
end
class Branch < ActiveRecord::Base
belongs_to :region
has_many :schools
has_many :users, :as => :administrates
end
class Region < ActiveRecord::Base
has_many :branches
has_many :users, :as => :administrates
end
You can't make it completely transparent to Active Admin as you have to tell Active Admin to use the particular scope. For this you should be able to get by with scope_to inside your ActiveAdmin.register blocks. You have to do a little magic to make scope_to work with a polymorphic association, but it's doable:
ActiveAdmin.register School do
scope_to do
Class.new do
def self.schools
case current_user.administrates
when School
School.where(:id => current_user.administrates_id)
when Branch
School.where(:branch_id => current_user.administrates_id)
when Region
School.where(:branch_id => current_user.administrates.branches.map(&:id))
when NilClass
School.scoped
end
end
end
end
end
This basically means that each time Active Admin will load a school (or a list of schools on the index page), it will scope it through the anonymous class we created inside the scope_to block.
You should be able to implement something similar on the Branch and Region models depending on your requirements.
You should be aware though, that there currently is an open issue when using scope_to with regards to filters and forms showing resources outside the current users scope.
You also need authorization to limit users on a certain level to only see that level and below (e.g. users on a branch level should not have access to regions). For this you should use CanCan.
For info on how to integrate CanCan in Active Admin, see this or this.

Related

What's the best way to share user accounts between the application and an engine?

I have a website that has User and Group models and all is well. The User and Group models are two types of accounts that we have and they are currently used for contact information, authentication and authorization.
Now I'm building out the subscription part of the site so we can start billing users (and groups/organizations) who subscribe to our premium services. I've opted to put this new code in a Rails Engine because I hope to deploy the engine only to an environment on a host that is reachable via our VPN, like so:
mount Billing::Engine, :at => '/billing' if Rails.env.admin?
I've got three models that I'm working with to manage subscriptions:
module Billing
class PricingPlan < ActiveRecord::Base
has_many :subscriptions
end
end
module Billing
class Subscription < ActiveRecord::Base
belongs_to :pricing_plan
belongs_to :subscriber, :polymorphic => true
# Used for eager loading
belongs_to :users, :foreign_key => 'subscriber_id', :class_name => '::User'
belongs_to :groups, :foreign_key => 'subscriber_id', :class_name => '::Group'
has_many :payments
end
end
module Billing
class Payments < ActiveRecord::Base
belongs_to :subscription
end
end
The Billing::Subscription.subscriber part is what is currently vexing me. As you can see, I'm currently reaching across the engine boundary to get ahold of the ::User and ::Group models that live in my application, but that feels icky.
I thought about creating Billing::User and Billing::Group AR models so that the engine and application can be completely isolated from one another, but it seems a bit weird to duplicate information between two models that are, for now, in the same database (e.g. first_name, last_name, email, etc.)...plus I'd have to duplicate information between them, which is a recipe for disaster, I'm sure.
I also thought about using some kind of wrapping model to abstract away the actual implementation, something like this:
module Billing
class User < ::User
end
end
But if I recall correctly, I ran into problems with the polymorphic behavior I'm after and/or problems with rspec mocking and stubbing so I abandoned that approach.
I'd appreciate any guidance. I've made numerous trips to Google searching for answers but nothing I've seen so far seems directly applicable.
UPDATE
Using Carl Zulauf's suggestion, I came up with the following:
# File: app/models/concerns/billing/subscribable.rb
require 'active_support/concern'
module Billing
module Subscribable
extend ActiveSupport::Concern
included do
has_one :subscription, {
:class_name => '::Billing::Subscription',
:foreign_key => 'subscriber_id',
:as => :subscriber
}
base = self
Billing::Subscription.class_eval do
belongs_to base.name.tableize.to_sym, {
:foreign_key => 'subscriber_id',
:class_name => base.to_s
}
end
end
end
end
Which I then invoke thusly:
class User < ActiveRecord::Base
include Billing::Subscribable
can_subscribe
end
This works...so long as I load User before I call Billing::Subscription.eager_load :users...which seems really dicey. Got any suggestions for me?
UPDATE #2
I wound up creating an initializer to handle this. This works, but if there are any better options, I'm all ears.
# File: config/initializers/setup_billing.rb
User.class_eval do
include Billing::Subscribable
end
Group.class_eval do
include Billing::Subscribable
end
One approach would be to have a module in your engine that would add a class macro to User and Group.
class User < ActiveRecord::Base
include Billing::ModelHelper
has_subscription # new macro
end
has_subscription could then:
Look up the class name (User)
Add the has_many/has_one/belongs_to association to User
Add the special association to Billing::Subscription (belongs_to :user, ...)

Active Admin nested associations

So I have the following models in my program:
class User < ActiveRecord::Base
has_many :group_members
has_many :groups, through: :group_members
class GroupMember < ActiveRecord::Base
belongs_to :user
belongs_to :group
end
class Group < ActiveRecord::Base
has_many :group_members
has_many :users, through: :group_members
end
And I'm using ActiveAdmin to administer these resources. My groups.rb file in the admin folder looks like this:
ActiveAdmin.register Group do
.
.
belongs_to :user, :optional => true
.
.
end
The problem is, Group is a resource by itself - an admin can manage groups by going to the /admin/groups route - but it also is a resource of users, and I'm able to view the user groups by accessing the /admin/users/:user_id/groups route. However, when I try to create a new group to this user by accessing the /admin/users/:user_id/groups/new page and filling in the form, I get an error:
ActiveRecord::RecordNotFound in Admin::GroupsController#show
Couldn't find Group with id=13 [WHERE `group_members`.`user_id` = 2]
The user id corresponds to my currently logged in user and it is correct, while the group id = 13 corresponds to the id of the newly created group (it is saved in the database correctly), but the association in group_members table is not created. Actually, that's not even exactly what I'm trying to achieve here: the ideal scenario would be to retrieve the list of groups and display it in the new page, so as not to create new groups in this page - only group_members.
How do I proceed in this case? Thanks in advance!
Have you considered removing the
belongs_to
declaration in the Group register block? The interface nesting that this aa dsl statement gives you, looks nice and gives you a clickpath, but, in the end it is merely a decoration on the data(model). Using filters and scopes in the user model could do as well. If you really need aa's belongs_to you will have to create custom edit screens, and custom redirects. Good luck.

Rails Active Record - How to model a user/profile scenario

I have a rails application that has three different types of users and I need them all to share the same common profile information. However, each different user also has unique attributes themselves. I'm not sure how to separate out the different fields.
Admin (site wide admin)
Owner (of a store/etc)
Member (such as a member of a co-op)
I'm using devise for authentication and cancan for authorization. Therefore I have a User model with a set of roles that can be applied to the user. This class looks this:
class User < ActiveRecord::Base
# ... devise stuff omitted for brevity ...
# Roles association
has_many :assignments
has_many :roles, :through => :assignments
# For cancan: https://github.com/ryanb/cancan/wiki/Separate-Role-Model
def has_role?(role_sym)
roles.any? { |r| r.name.underscore.to_sym == role_sym }
end
end
Each user has a profile that includes:
First & Last Name
Address Info (city/st/zip/etc)
Phone
I do not want to pollute the User model with this info so I'm throwing it into a Profile model. This part is fairly simple. This turns the User model into something like this:
class User < ActiveRecord::Base
# ... devise stuff omitted for brevity ...
# ... cancan stuff omitted for brevity ...
has_one :profile
end
The additional fields is where I have some uneasy feelings about how to model are ...
If a user is an admin, they'll have unique fields such as:
admin_field_a:string
admin_field_b:string
etc
If a user is a Owner they'll have unique fields ...
stripe_api_key:string
stripe_test_api_key:string
stripe_account_number:string
has_one :store # AR Refence to another model that Admin and Member do not have.
If a user is a member they'll have a few additional fields as such:
stripe_account_number:string
belongs_to :store # the store that they are a member of
has_many :note
...
and a Store model will contain a has_many on the members so we work the the members of the store.
The issue is around the additional fields. Do I set these up as different classes? Put them into a different
I've currently tried a few different ways to set this up:
One way is to set up the User Model as aggregate root
class User < ActiveRecord::Base
# ...
# Roles association
has_many :assignments
has_many :roles, :through => :assignments
# Profile and other object types
has_one :profile
has_one :admin
has_one :owner
has_one :member
# ...
end
The benefit of this approach is the User model is the root and can access everything. The downfall is that if the user is a "owner" then the "admin" and "member" references will be nil (and the cartesian of the other possibilities - admin but not owner or member, etc).
The other option I was thinking of was to have each type of user inherit from the User model as such:
class User < ActiveRecord::Base
# ... other code removed for brevity
has_one :profile
end
class Admin < User
# admin fields
end
class Owner < User
# owner fields
end
class Member < User
# member fields
end
Problem with this is that I'm polluting the User object with all kinds of nil's in the table where one type doesn't need the values from another type/etc. Just seems kind of messy, but I'm not sure.
The other option was to create each account type as the root, but have the user as a child object as shown below.
class Admin
has_one :user
# admin fields go here.
end
class Owner
has_one :user
# owner fields go here.
end
class Member
has_one :user
# member fields go here.
end
The problem with the above is I'm not sure how to load up the proper class once the user logs in. I'll have their user_id and I'll be able to tell which role they are (because of the role association on the user model), but I'm not sure how to go from user UP to a root object. Methods? other?
Conclusion
I have a few different ways to do this, but I'm not sure what the correct "rails" approach is. What is the correct way to model this in rails AR? (MySQL backend). If there is not a "right" approach, whats the best of the above (I'm also open to other ideas).
Thanks!
My answer assumes that a given user can only be one type of user - e.g. ONLY an Admin or ONLY a Member. If so, this seems like a perfect job for ActiveRecord's Polymorphic association.
class User < ActiveRecord::Base
# ...
belongs_to :privilege, :polymorphic => true
end
This association gives User an accessor called 'privilege' (for lack of a better term and to avoid naming confusion which will become apparent later). Because it is polymorphic, it can return a variety of classes. The polymorphic relationship requires two columns on the corresponding table - one (accessor)_type and (accessor)_id. In my example, the User table would gain two fields: privilege_type and privilege_id which ActiveRecord combines to find the associated entry during lookups.
Your Admin, Owner and Member classes look like this:
class Admin
has_one :user, :as => :privilege
# admin fields go here.
end
class Owner
has_one :user, :as => :privilege
# owner fields go here.
end
class Member
has_one :user, :as => :privilege
# member fields go here.
end
Now you can do things like this:
u = User.new(:attribute1 => user_val1, ...)
u.privilege = Admin.new(:admin_att1 => :admin_val1, ...)
u.save!
# Saves a new user (#3, for example) and a new
# admin entry (#2 in my pretend world).
u.privilege_type # Returns 'Admin'
u.privilege_id # Returns 2
u.privilege # returns the Admin#2 instance.
# ActiveRecord's SQL behind the scenes:
# SELECT * FROM admin WHERE id=2
u.privilege.is_a? Admin # returns true
u.privilege.is_a? Member # returns false
Admin.find(2).user # returns User#3
# ActiveRecord's SQL behind the scenes:
# SELECT * FROM user WHERE privilege_type='Admin'
# AND privilege_id=2
I would recommend you make the (accessor)_type field on the database an ENUM if you expect it to be a known set of values. An ENUM, IMHO, is a better choice than a VARCHAR255 which Rails would normally default to, is easier/faster/smaller to index but makes changes down the road more difficult/timeconsuming when you've got millions of users. Also, index the association properly:
add_column :privilege_type, "ENUM('Admin','Owner','Member')", :null => false
add_column :privilege_id, :integer, :null => false
add_index :user, [:privilege_type, :privilege_id], :unique => true
add_index :user, :privilege_type
The first index allows ActiveRecord to rapidly find the reverse association (e.g. find the user that has a privilege of Admin#2) and the second index allows you to find all Admins or all Members.
This RailsCast is a bit dated but a good tutorial on polymorphic relationships nonetheless.
One last note - in your question, you indicated Admin, Owner or Member was the user's type which is appropriate enough, but as you probably see, I'd have to explain that your user table would then have a user_type_type field.
I'm probably not going to give you a rails approved suggestion, but ..
Separating your profile is a good call. Consider using the Decorator pattern for the a role. You can have an AdminUserDecorator, OwnerUserDecorator, or MemberOwnerDecorator. You could also dynamically add the additional fields directly on the instance (it is Ruby after all), but I think that would get ugly and complicated. (If you really want to do bad things, use a visitor object to give you an instance of your decorator from a method on the user class.)
Also, why put the stripe or payment config on the Owner instead of being part of the store information? Unless perhaps an owner can have multiple stores and use the same payment info for each store?
UPDATE: I should also suggest using TDD to flush out what works.
You've already accepted an answer, but for what it's worth, I have a similar situation and chose to go with your "User Model as aggregate root" approach. My User model contains all the "profile" info, and the User, to use a fictitious example, has_one :buyer and has_one :seller. I use a simple tinyint field as bitflags for which roles the user holds, since users could be both buyers and sellers (or TBD other roles I need in the future). If a bit is set, you can assume the corresponding association is not nil (which hasn't been an issue for me since I always check the bitflags before using the association reference). I don't actually have too many unique fields in my real subordinate models, but it's very useful to keep things decluttered when each subordinate model has additional associations, like if seller has_one :merchant_account" and a buyer has_one :purchase_history, etc. I haven't gone live yet, but when I do, I'll follow this post up with any issues I encounter.

Rails association with almost all other models

I'm looking for some suggestions on how to deal with "Regions" in my system.
Almost all other models in the system (news, events, projects, and others) need to have a region that they can be sorted on.
So far, I've considered a Region model with has_many :through on a RegionLink table. I've never had a model joined to so many others and wonder if this route has any negatives.
I've also considered using the acts_as_taggable_on gem and just tag regions to models. This seems ok but I'll have to write more cleanup type code to handle the customer renaming or removing a region.
Whatever I choose I need to handle renaming and, more importantly, deleting regions. If a region gets deleted I will probably just give the user a choice on another region to replace the association.
Any advice on handling this is greatly appreciated.
If each News, Event, etc. will belong to only 1 Region, tags don't seem the most natural fit IMO. This leaves you with 2 options:
Add a region_id field to each model
This is simplest, but has the drawback that you will not be able to look at all the "regioned" items at once - you'll have to query the news, events, etc. tables separately (or use a UNION, which ActiveRecord doesn't support).
Use RegionLink model with polymorphic associations
This is only slightly more complicated, and is in fact similar to how acts_as_taggable_on works. Look at the Rails docs on *belongs_to* for a fuller description of polymorphic relationships if you are unfamiliar
class Region < ActiveRecord::Base
has_many :region_links
has_many :things, :through => :region_links
end
# This table with have region_id, thing_id and thing_type
class RegionLink < ActiveRecord::Base
belongs_to :region
belongs_to :thing, :polymorphic => true
end
class Event < ActiveRecord::Base
has_one :region_link, :as => :thing
has_one :region, :through => :region_link
end
# Get all "things" (Events, Projects, etc.) from Region #1
things = Region.find(1).things
Renaming is quite simple - just rename the Region. Deleting/reassigning regions is also simple - just delete the RegionLink record, or replace it's region_id.
If you find yourself duplicating a lot of region-related code in your Event, etc. models, you may want to put it into a module in lib or app/models:
module Regioned
def self.inluded(base)
base.class_eval do
has_one :region_link, :as => :thing
has_one :region, :through => :region_link
...
end
end
end
class Event < ActiveRecord::Base
include Regioned
end
class Project < ActiveRecord::Base
include Regioned
end
Checkout the cast about polymorphic associations. They did change a bit in rails 3 though: http://railscasts.com/episodes/154-polymorphic-association?view=asciicast

Ruby on rails with different user types

I'm trying to build a application that has different kinds of users, I'm using authlogic for user authentication.
So I have one user model that has the required field for authlogic to do its magic. I now want to add a couple of different models that would describe the extra fields for the different kinds of users.
Lets say that a user registers, he would then select his user type, when he is done registering he would be able to add information that is specific for his user model.
What would be the best way to do this? I am currently looking into polymorphic models but I'm not sure that's the best route to take. Any help would be greatly appreciated, thanks.
You can create different profile tables and just tie the profile to the user. So for each user type you can create a table and store the specific info there and have a user_id column to point back to users.
class User < ActiveRecord::Base
has_one :type_1
has_one :type_2
end
class Type1 < ActiveRecord::Base
belongs_to :user
end
class Type2 < ActiveRecord::Base
belongs_to :user
end
Now this isn't very DRY and could lead to problems if you are constantly adding user types. So you could look into polymorphism.
For polymorphism, the users table would define what type the user is (profileable_id and profileable_type). So something like this:
class User < ActiveRecord::Base
belongs_to :profileable, :polymorphic => true
end
class Type1 < ActiveRecord::Base
has_one :user, :as => :profileable
end
class Type2 < ActiveRecord::Base
has_one :user, :as => :profileable
end
Then there is a third option of STI (single table inheritance) for the user types. But that doesn't scale well if the user type fields differ dramatically.
The best approach I saw it here
http://astockwell.com/blog/2014/03/polymorphic-associations-in-rails-4-devise/

Resources