module BulletTrain::LoadsAndAuthorizesResource
def account_load_and_authorize_resource(model, options, old_options = {})
to help you understand the code below, usually `through` is `team`
namespacing our `Oauth::` models and controllers.)
for namespaced models and controllers. (we introduced this complexity in support of
there are also some complications that were introduced into this method by our support
controllers in the account namespace, including our shallow nested routes.
implements a lot of the options required to make that method work very well for our
cancancan's `load_and_authorize_resource` method, which is awesome, but it also
tied together. we've taken the liberty of doing this because it's heavily based on
for you in your controllers beyond that is provided by the underlying gems that we've
this is one of the few pieces of 'magical' functionality that bullet train implements
def account_load_and_authorize_resource(model, options, old_options = {}) # options are now required, because you have to have at least a 'through' setting. # we used to support calling this method with a signature like this: # # `account_load_and_authorize_resource [:oauth, :twitter_account], :team` # # however this abstraction was too short-sighted so we've updated this method to accept the exact same method # signature as cancancan's original `load_and_authorize_resource` method. if model.is_a?(Array) raise "Bullet Train has depreciated this method of calling `account_load_and_authorize_resource`. Read the comments on this line of source for more details." end # this is providing backward compatibility for folks who are calling this method like this: # account_load_and_authorize_resource :thing, through: :team, through_association: :scaffolding_things # i'm going to deprecate this at some point. if options.is_a?(Hash) through = options[:through] options.delete(:through) else through = options options = old_options end # fetch the namespace of the controller. this should generally match the namespace of the model, except for the # `account` part. namespace = model_namespace_from_controller_namespace tried = [] begin # check whether the parent exists in the model namespace. model_class_name = (namespace + [model.to_s.classify]).join("::") model_class_name.constantize rescue NameError tried << model_class_name if namespace.any? namespace.pop retry else raise "Oh no, it looks like your call to 'account_load_and_authorize_resource' is broken. We tried #{tried.join(" and ")}, but didn't find a valid class name." end end # treat through as an array even if the user only specified one parent type. through_as_symbols = through.is_a?(Array) ? through : [through] through = [] through_class_names = [] through_as_symbols.each do |through_as_symbol| # reflect on the belongs_to association of the child model to figure out the class names of the parents. unless ( association = model_class_name.constantize.reflect_on_association( through_as_symbol ) ) raise "Oh no, it looks like your call to 'account_load_and_authorize_resource' is broken. Tried to reflect on the `#{through_as_symbol}` association of #{model_class_name}, but didn't find one." end through_class_name = association.klass.name begin through << through_class_name.constantize through_class_names << through_class_name rescue NameError raise "Oh no, it looks like your call to 'account_load_and_authorize_resource' is broken. We tried to load `#{through_class_name}}` (the class name defined for the `#{through_as_symbol}` association), but couldn't find it." end end if through_as_symbols.count > 1 && !options[:polymorphic] raise "When a resource can be loaded through multiple parents, please specify the 'polymorphic' option to tell us what that controller calls the parent, e.g. `polymorphic: :imageable`." end # this provides the support we need for shallow nested resources, which # helps keep our routes tidy even after many levels of nesting. most people # i talk to don't actually know about this feature in rails, but it's # actually the recommended approach in the rails routing documentation. # # also, similar to `load_and_authorize_resource`, people can pass in additional # actions for which the resource should be loaded, but because we're making # separate calls to `load_and_authorize_resource` for member and collection # actions, we ask controllers to specify these actions separately, e.g.: # `account_load_and_authorize_resource :invitation, :team, member_actions: [:accept, :promote]` collection_actions = options[:collection_actions] || [] member_actions = options[:member_actions] || [] # this option is native to cancancan and allows you to skip account_load_and_authorize_resource # for a specific action that would otherwise run it (e.g. see invitations#show.) except_actions = options[:except] || [] collection_actions = (%i[index new create reorder] + collection_actions) - except_actions member_actions = (%i[show edit update destroy] + member_actions) - except_actions options.delete(:collection_actions) options.delete(:member_actions) # NOTE: because we're using prepend for all of these, these are written in backwards order # of how they'll be executed during a request! # 4. finally, load the team and parent resource if we can. prepend_before_action :load_team # x. this and the thing below it are only here to make a sortable concern possible. prepend_before_action only: member_actions do instance_variable_name = options[:polymorphic] || through_as_symbols[0] eval "@child_object = @#{model}" eval "@parent_object = @#{instance_variable_name}" end prepend_before_action only: collection_actions do instance_variable_name = options[:polymorphic] || through_as_symbols[0] eval "@parent_object = @#{instance_variable_name}" if options[:through_association].present? eval "@child_collection = :#{options[:through_association]}" else eval "@child_collection = :#{model.to_s.pluralize}" end end prepend_before_action only: member_actions do instance_variable_name = options[:polymorphic] || through_as_symbols[0] possible_sources_of_parent = through_as_symbols.map { |tas| "@#{model}.#{tas}" }.join(" || ") eval_string = "@#{instance_variable_name} ||= " + possible_sources_of_parent eval eval_string end if options[:polymorphic] prepend_before_action only: collection_actions do possible_sources_of_parent = through_as_symbols.map { |tas| "@#{tas}" }.join(" || ") eval "@#{options[:polymorphic]} ||= #{possible_sources_of_parent}" end end # 3. on action resource, we have a specific id for the child resource, so load it directly. load_and_authorize_resource model, options.merge( class: model_class_name, only: member_actions, prepend: true, shallow: true ) # 2. only load the child resource through the parent resource for collection actions. load_and_authorize_resource model, options.merge( class: model_class_name, through: through_as_symbols, only: collection_actions, prepend: true, shallow: true ) # 1. load the parent resource for collection actions only. (we're using shallow routes.) # since a controller can have multiple potential parents, we have to run this as a loop on every possible # parent. (the vast majority of controllers only have one parent.) through_class_names.each_with_index do |through_class_name, index| load_and_authorize_resource through_as_symbols[index], options.merge( class: through_class_name, only: collection_actions, prepend: true, shallow: true ) end end
def load_team
def load_team @team ||= @child_object&.try(:team) || @parent_object&.try(:team) return unless @team if defined?(Current) && Current.respond_to?(:team=) Current.team = @team end # If the currently loaded team is saved to the database, make that the user's new current team. if @team.try(:persisted?) if can? :show, @team current_user.update_column(:current_team_id, @team.id) end end end
def model_namespace_from_controller_namespace
def model_namespace_from_controller_namespace name .gsub(regex_to_remove_controller_namespace || //, "") .split("::") .tap(&:pop) # drops actual class name end
def regex_to_remove_controller_namespace
def regex_to_remove_controller_namespace return super if defined?(super) raise "This is a template method that needs to be implemented by controllers including LoadsAndAuthorizesResource." end