RoR 5.1.4. My application is backed by MongoDB via Mongoid, and many of the models have similar handling. I'm trying to make it as data-driven as possible, which means querying Mongoid
for the models and Rails for the routes, neither of which can be done comfortably in initialisers because of eager loading and routes not yet having been set up. The schema information (controller/model relationships, model fields, etc., are stored in a singleton location, and making that happen and be accessible from class and instance environments has been a PITA. However, that's background.
Most of the controllers support two upload actions, one via form file specification and one via request content body. I'm using webupload
and upload
actions for these respectively, and since they're always handled the same (differences are determined by examining the aforementioned schema info), I want to abstract those two action methods, and their related before/after/.. relationship definitions, into a separate module.
A controller concern won't do it OOTB because the callbacks (e.g., :before_action
and friends) aren't declared in the inheritance, and I can't figure out what module I'd need to include
in the concern in order to get them.
Helpers are out because they've been deprecated from controller inclusion (you need to take extra steps to get them), and they're primarily for views anyway.
So what is the pattern for writing/placing/including modules specifically for controllers, models, and tests? That have the proper inheritance of methods for those functions? (E.g., :before_action
for controllers, :validates
for models, and so on.)
RoR is a wealthy hoard of features and hooks, but I'm finding it bloody hard to apply abstraction and DRY patterns to it simply because it is so rich.
Thanks for any help or pointers!
[EDIT] Someone suggested that I include some code. So here in an abbreviated excerpt of my attempt to do this with a controller concern.
module UploadAssistant
extend ActiveSupport::Concern
#
# Code to execute when something does a `include ControllerAssistant`.
#
included do
#
# Make the application's local storage module more easily
# accessible, too.
#
unless (self.const_defined?('LocalStore'))
self.const_set('LocalStore', ::PerceptSys::LocalStore)
end
def set_title_uploading
title.base = 'Uploading records'
title.add(model_info.friendly)
end # def set_base_title
#+
# Supply these routes to the controllers so they needn't define
# them.
#
#
# GET /<model>/upload(.:format)
#
def webupload
end # def webupload
#
# POST /<model>/upload(.:format)
#
def upload
title.add('Results')
@uploads = {
:success => {},
:failure => {},
}
errors = 0
@upload_records.each do |record|
#
# Stuff happens here.
#
end
successes = @uploads[:success].count
failures = @uploads[:failure].count
respond_to do |format|
format.html {
render(:upload,
:status => :ok,
:template => 'application/upload.html')
}
format.json {
render(:json => @uploads)
}
end
end
def upload_file_params
if (params[:file])
params.require(:file).require(:upload)
colname = model_info.collection
file_id = params[:file][:upload]
#
# Get the file contents.
#
end
@upload_records = params.delete(model_info.collection.to_sym)
end # def upload_file_params
def upload_params
@upload_records = params.require(model_info.collection.to_sym)
end # def upload_params
def set_file_upload
file_id = params.require(:file).require(:upload)
#
# Read/decompress the file.
#
data = JSON.parse(data)
params[model_info.collection] = data[model_info.collection]
end # def set_file_upload
end # included do
#+
# Insert here any class methods we want added to our including class
# or module.
#
class_methods do
#
# Stuff relating specifically to bulk uploading.
#
before_action(:set_title_uploading,
:only => [
:upload,
:webupload,
])
before_action(:set_file_upload,
:only => [
:upload,
])
before_action(:upload_params,
:only => [
:upload,
])
end # class_methods do
end # module ControllerAssistant
# Local Variables:
# mode: ruby
# eval: (fci-mode t)
# End:
You have completly misunderstood pretty much everything about ActiveSupport::Concern
and how module mixins work.
Lets start by using composition to seperate concerns. For example:
module LocalStorage
extend ActiveSupport::Concern
class_methods do
# use a memoized helper instead of a constant
# as its easier to stub for testing
def local_storage
@local_storage ||= ::PerceptSys::LocalStore
end
end
end
This makes sense to extract to a separate concern since its a reusable behaviour.
We can then compose an Uploadable
concern:
module Uploadable
# we compose modules by extending
extend ActiveSupport::Concern
extend LocalStorage
# put instance methods in the module body
# GET /<model>/upload(.:format)
def webupload
# ...
end
#
# POST /<model>/upload(.:format)
#
def upload
# ...
end
# don't abuse use a callback for this - just use a straight
# method that returns a value and preferably does not have side effects
def upload_params
# ...
end
# ...
# use "included" to hook in the class definition
# self here is the singleton class instance
# so this is where you put callbacks, attr_accessor etc
# which would normally go in the class defintion
included do
before_action(:set_title_uploading,
:only => [
:upload,
:webupload,
])
before_action(:set_file_upload,
:only => [
:upload,
])
end
# just use class_methods for actual class_methods!
class_methods do
# for example to derive the name of a model from the controller name
def resource_class_name
controller_name.singularize
end
def resource_class
@resource_class ||= resource_class_name.classify.constantize
end
end
end