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>
Related
In my Rails6/Grape API app my controller starting to look a little bit heavy - there is a too much logic going so I wrap webhook distribution data into two services Activities::WebhookData and Journeys::WebhookData like below:
endpoint
post do
name = CmsClient.fetch_model_name(model_id)
#some other logic
name == 'Journey' ? ::Journeys::WebhookData.new(params).call : ::Activities::WebhookData.new(params).call
end
Journeys::WebhookData
module Journeys
class WebhookData
def initialize(webhook)
#webhook = webhook
end
attr_accessor :webhook
def call
case webhook[:event_type]
when 'publish'
JourneyWorker.perform_async(webhook)
when 'delete'
HideJourneyWorker.perform_async(webhook)
end
end
end
end
Activities::WebhookData
module Activities
class WebhookData
def initialize(webhook)
#webhook = webhook
end
attr_accessor :webhook
def call
case webhook[:event_type]
when 'publish'
ActivityWorker.perform_async(webhook)
when 'delete'
DeleteActivityWorker.perform_async(webhook)
end
end
end
end
As you see both are pretty much the same, is there a better way to merge these two services into one instead?
I don't advice you to move that to a single class, because both classes have a different way to interpret the webhook data they receive. If you do so you're going to replace that with some if/else conditions.
You can create a class where you define the call method to fetch the values from a hash given the event_type in the attr_reader, then create two other classes inheriting from the first one and just define a hash of its own with the according classes where you're going to invoke perform_async on:
class Webhook
def call
event_types[webhook[:event_type]].public_send(:perform_async, webhook)
end
protected
def initialize(webhook)
#webhook = webhook
end
private
attr_reader :webhook
def event_types
raise NotImplementedError
end
end
module Journeys
class WebhookData < Webhook
def event_types
{ 'publish' => JourneyWorker, 'delete' => HideJourneyWorker }
end
end
end
module Activities
class WebhookData < Webhook
def event_types
{ 'publish' => ActivityWorker, 'delete' => DeleteActivityWorker }
end
end
end
I realize this is against MVC principals and all, but how can I access the controller name/method in a module that is included in a class?
module DocumentHTMLBoxes
extend ActiveSupport::Concern
included do
def icon_or_text(variable)
if controller.action_name == 'send'
"<b>#{variable.name.capitalize}</b> #{variable.text}\n"
else
variable.format!
end
end
end
end
How can I access controller and/or controller.action_name in the module?
In Rails the view_context object contains all the ivars from the controller and includes all the helpers. It also provides access to the session, cookies and request. It is the implicit self when you are rendering templates.
Models do not have access to the view context - this is a conscious design as it gives a good seperation of concerns.
If you want to break the encapsulation you need to pass the context to the model.
module DocumentHTMLBoxes
extend ActiveSupport::Concern
included do
def icon_or_text(variable, context)
if context.action_name == 'send'
"<b>#{variable.name.capitalize}</b> #{variable.text}\n"
else
variable.format!
end
end
end
end
class Thing < ApplicationModel
include DocumentHTMLBoxes
end
Congratulations, you just created some really smelly code.
But, this is a really bad idea since it just adds one more responsibility to your models which are already near god-class status in Rails. Don't add generating HTML (a view/helper responsiblity!) to that list.
Instead you should just create a simple helper method:
module BoxesHelper
def icon_or_text(obj)
if context.action_name == 'send'
"<b>#{obj.name.capitalize}</b> #{obj.text}\n"
else
obj.format!
end
end
end
Or a decorator:
# #see https://ruby-doc.org/stdlib-2.1.0/libdoc/delegate/rdoc/Delegator.html
class Decorator < Delegator
attr_accessor :object
attr_accessor :context
def intialize(obj, cxt)
#object = obj
#context = cxt
super(obj) # pass obj to Delegator constructor, required
end
# Required by Delegator
def __getobj__
#object
end
def self.decorate(collection, context)
return collection.map { |record| self.new(record, context) }
end
end
module DocumentHTMLBoxes
extend ActiveSupport::Concern
included do
def icon_or_text(variable)
if context.action_name == 'send'
"<b>#{object.name.capitalize}</b> #{object.text}\n"
else
object.format!
end
end
end
end
class ThingDecorator < Decorator
include DocumentHTMLBoxes
end
To decorate a bunch of records in the controller you would do:
#things = ThingDecorator.decorate( Thing.all, self.view_context )
And now you can call icon_or_text on the decorated model:
<% #things.each do |t| %>
<% t.icon_or_text %>
<% end %>
I`d recommend refactor the code to make it cleaner:
Controller level call would be like
model_item.icon_or_text(variable, action_name)
The module
module DocumentHTMLBoxes
extend ActiveSupport::Concern
included do
def icon_or_text(variable, action_name)
if action_name == 'send'
"<b>#{variable.name.capitalize}</b> #{variable.text}\n"
else
variable.format!
end
end
end
end
Assuming you are including this concern into the controller then inside the icon_or_text method self is the controller. You will just be able to call action_name without prefixing it with controller.
module DocumentHTMLBoxes
extend ActiveSupport::Concern
included do
def icon_or_text(variable)
if action_name == 'send'
"<b>#{variable.name.capitalize}</b> #{variable.text}\n"
else
variable.format!
end
end
end
end
I have two models, restaurant and cuisine with a many to many association. And I have this in my app/admin/restaurant.rb
ActiveAdmin.register Restaurant do
scope("All"){|scope| scope.order("created_at desc")}
Cuisine.all.each do |c|
scope(c.name) { |scope| scope.joins(:cuisines).where("cuisines.id=?",c.id)}
end
end
The problem is whenever I delete or add a new cuisine the scopes do not change until I make a change to my admin/restaurant.rb file. How can I fix this issue?
I was able to fix this by adding in my admin/restaurant.rb
controller do
before_filter :update_scopes, :only => :index
def update_scopes
resource = active_admin_config
Cuisine.order("created_at ASC").each do |m|
next if resource.scopes.any? { |scope| scope.name == m.name}
resource.scopes << (ActiveAdmin::Scope.new m.name do |restaurants|
restaurants.joins(:cuisines).where("cuisines.id=?", m.id)
end)
end
resource.scopes.delete_if do |scope|
!(Cuisine.all.any? { |m| scope.name == m.name })
end
resource.scopes.unshift(ActiveAdmin::Scope.new "All" do |restaurants| restaurants end)
end
found the solution here
I'm not sure of a way to defines scopes dynamically, at least using the scope method.
The alternative to the scope method is defining a class method, which accomplishes the same thing so far as I know.
In other words,
scope("All"){|scope| scope.order("created_at desc")}
is the same as
# in a Class
class << self
def All
order("created_at desc")
end
end
You can dynamically create class methods using this method (taken from ruby-defining-class-methods:
class Object
def meta_def name, &blk
(class << self; self; end).instance_eval { define_method name.to_s, &blk }
end
end
I'll use the following to remove the generated class methods:
class Object
def meta_undef name
(class << self; self; end).class_eval { remove_method name.to_sym }
end
end
These methods can be called from the save and destroy hooks on your models, i.e.:
# in a Model
def save(*args)
self.class.meta_def(name) do
joins(:cuisines).where("cuisines.id=?",c.id)
end
super(*args)
end
def destroy(*args)
self.class.meta_undef(name)
super(*args)
end
Then whenever a record is created or removed, the scopes will be updated. There are pros and cons of this approach. Clearly it's nice to define methods on the fly, but this is vulnerable to remote code execution.
Personally I'd probably hold off from dynamically defining class methods (i.e. scopes) and just make one that accepts an argument. Example:
# This is with standard ActiveRecord, not sure about ActiveAdmin
class Restaurant < ActiveRecord::Base
def self.All
order("created_at desc")
end
end
class Cuisine < ActiveRecord::Base
def self.by_name(name)
Restaurant.all.joins(:cuisines).where("cuisines.name=?", name)
end
end
Cuisine.by_name("something")
Restaurant.all.All
Restaurant.All
edit in response to your comment:
load(file) will re-load the source. So you could try the following:
# in a model
def save(*args)
load(Rails.root.join("app", "models", "THIS_MODEL_FILE.rb")
super
end
def destroy(*args)
load(Rails.root.join("app", "models", "THIS_MODEL_FILE.rb")
super
end
Under the hood, save is called for both create and update. So overriding it and destroy covers all the CRUD operations.
The reason I didn't initially recommend this approach is that I haven't personally used it. I'd be curious to know how it works.
My code:
I have used AuthenticationRelated only in the ApplicationHelper
also signed_in? in is_admin? is from Devise Gem.
module AuthenticationRelated
def is_admin?
athu = false
if signed_in?
current_user.roles.each do |role|
if role.name == 'admin'
athu = true
end
end
end
athu
end
end
Now I have a class SalesReportsGrid which I need to be able to access the is_admin?
So this is what I have done:
class SalesReportsGrid
include Datagrid
include AuthenticationRelated
scope do
if is_admin?
Sales.joins(product: [ category: [:access_lists] ] )
else
....
end
end
....
end
Now when I run this I get the following error:
undefined method `is_admin?` for SalesReportsGrid:Class
Edit
When I add extend AuthenticationRelated
This is what I get:
undefined method `signed_in?` for SalesReportsGrid:Class
I am really confused, can someone please have a look and suggest something?
Thanks
If you include module, you add its methods to instances of the class where you used include. To make module methods 'class methods' (i.e. singleton methods of class), you should use extend:
extend AuthenticationRelated
You need to be explicit in using current_user to define grid scope.
Here is example:
def index
#grid = SalesReportGrid.new(params[:sales_report_grid]) do |scope|
unless current_user.admin?
scope.where_accessible_by(current_user)
end
end
end
It allows you to be more explicit and don't move access control to the grid and leave it in controller like it use to happen in other places.
I'm trying to create a flexible dsl. I already have the DSL module, say module DSL. The DSL user can create spin-offs of this as a class. The main point of the DSL is to allow the user to create Feature object with a custom render method. There was a lot of ugly and non-DRY code backing the Feature, hence the abstraction, but the user needs a lot of control on how that feature renders, and my meta-programming is not up to the task. Let me show you how it's set up.
The DSL looks something like this:
module DSL
module ClassMethods
attr_accessor :features
def column(name, *args)
arguments = args.pop || {}
self.features = [] if self.features.nil?
self.features << Feature.new(name, arguments)
end
end
def self.included(base)
base.extend ClassMethods
end
end
end
An implementation of it would look something like this:
class DSLSpinOff
include DSL
feature :one
feature :two, render_with: :predefined_render
feature :three, render_with: :user_defined_render
feature :four, render_with: lambda {
puts "Go nuts, user!"
puts "Do as you please!"
}
def user_defined_render
#...
end
end
And finally, the feature class itself lies within the DSL, like so:
module DSL
#...
private
class Feature
attr_accessor :name, :render_with
def initialize(name, *args)
self.name = name
attributes = args.pop || {}
# somehow delegate attributes[:render_with] to the render function, handling defaults, lamdbas, function string names, etc
self.render_with = attributes.fetch(:render_with, :default_render)
end
private
def default_render
#...
end
def predefined_render
#...
end
end
end
The magic I was looking for: define_singleton_method.
module DSL
#...
private
class Feature
attr_accessor :name
def initialize(name, args)
#...
define_singleton_method :render do
if self.render_with.kind_of? Symbol
content = self.send(self.render_with)
else
content = self.render_with.call
end
content.present? ? content.to_s.html_safe : '–'
end
end
end
end
Now within the DSL I can iterate over all features and render them. It'll send itself :default_render, or some other :predefined_render, or use the block provided instead. However, this does not let users define methods on within the DSLSpinOff and pass them in, since those methods would get delegated to the DSLSpinOff class instead of the DSLSpinOff::Column class.
I suspect they would have to do something like:
feature :three, render_with: self.method(:user_defined_render)
def user_defined_render
#...
end
Edit:
I found a clean way to allow use of a default method, user-defined lambdas, pre-defined methods, and user-defined methods:
module DSL
#...
class Feature
attr_accessor :name, render_with
def initialize(name, *args)
self.name = name
self.render_with = args.has_key?(:render_with) ? args[:render_with] : :default_render
define_singleton_method :render do |object|
render_method = self.render_with.is_a?(Proc) ? renderer : method(renderer)
render_method.call
end
end
private
def default_render
#...
end
def predefined_render
#...
end
end
end
This will grab lambdas or procs if the user passes those in, otherwise, it'll use the method method to return a reference to the method defined on theFeature. The call method works on all three.
To support user-defined render methods, just have the open the class in an initializer to make it findable by the method method:
#initializers/custom_dsl_renders.rb
class DSL::Feature
def user_defined_render
#...
end
end