Limiting download_links for ActiveAdmin based on AdminUser model - ruby-on-rails

I am trying to limit who can access the csv/json/... exports in ActiveAdmin based on the field 'limited'. I'd like to a) hide the links and b) return nothing at all if the path were to get hit anyway
I tried the following:
index downloads_links: !current_admin_user.limited? do
# ...
end
as well as
csv do
return if current_admin_user.limited?
# ...
end
I also briefly tried using procs and lambda's but that's probably not the solution here either?
Neither appear to work and are giving me nomethoderrors on ActiveAdmin::DSLResource and ActiveAdmin::CSVBuilder respectively
Any tips are welcome, thank you

i was able to achieve this with a simple monkey patch, but i was using cancan. cancan helper method 'can?' worked fine, but i wasn't testing the 'current_admin_user'. please, try it
module ActiveAdmin
module Views
class PaginatedCollection
def build_download_format_links(formats = self.class.formats)
params = request.query_parameters.except :format, :commit
links = formats.map { |format| link_to format.to_s.upcase, params: params, format: format }
unless current_admin_user.limited?
div :class => "download_links" do
text_node [I18n.t('active_admin.download'), links].flatten.join(" ").html_safe
end
end
end
end
end
end
upd:
i've tried with current_admin_user, and it worked.
also if you need to limit the formats, you can redefine formats method it this module, using your 'limited' method:
module ActiveAdmin
module Views
class PaginatedCollection
def formats
if current_admin_user.limited?
#formats ||= [:csv] # anything you need for limited users
else
#formats ||= [:csv, :xml, :json]
end
#formats.clone
end
end
end
end

Related

Rails N+1 query : monkeypatching ActiveRecord::Relation#as_json

Situation
I have a model User:
def User
has_many :cars
def cars_count
cars.count
end
def as_json options = {}
super options.merge(methods: [:cars_count])
end
end
Problem
When I need to render to json a collection of users, I end up being exposed to the N+1 query problem. It is my understanding that including cars doesn't solve the problem for me.
Attempted Fix
What I would like to do is add a method to User:
def User
...
def self.as_json options = {}
cars_counts = Car.group(:user_id).count
self.map do |user|
user.define_singleton_method(:cars_count) do
cars_counts[user.id]
end
user.as_json options
end
end
end
That way all cars counts would be queried in a single query.
Remaining Issue
ActiveRecord::Relation already has a as_json method and therefore doesn't pick the class defined one. How can I make ActiveRecord::Relation use the as_json method from the class when it is defined? Is there a better way to do this?
Edits
1. Caching
I can cache my cars_count method:
def cars_count
Rails.cache.fetch("#{cache_key}/cars_count") do
cars.count
end
end
This is nice once the cache is warm, but if a lot of users are updated at the same time, it can cause request timeouts because a lot of queries have to be updated in a single request.
2. Dedicated method
Instead of calling my method as_json, I can call it my_dedicated_as_json_method and each time I need to render a collection of users, instead of
render json: users
write
render json: users.my_dedicated_as_json_method
However, I don't like this way of doing. I may forget to call this method somewhere, someone else might forget to call it, and I'm losing clarity of the code. Monkey patching seems a better route for these reasons.
Have you considered using a counter_cache for cars_count? It's a good fit for what you're wanting to do.
This blog article also offers up some other alternatives, e.g. if you want to manually build a hash.
If you really wanted to continue down the monkey patching route, then ensure that you are patching ActiveRecord::Relation rather than User, and override the instance method rather than creating a class method. Note that this will then affect every ActiveRecord::Relation, but you can use #klass to add a condition that only runs your logic for User
# Just an illustrative example - don't actually monkey patch this way
# use `ActiveSupport::Concern` instead and include the extension
class ActiveRecord::Relation
def as_json(options = nil)
puts #klass
end
end
Option 1
In your user model:
def get_cars_count
self.cars.count
end
And in your controller:
User.all.as_json(method: :get_cars_count)
Option 2
You can create a method which will get all the users and their car count. And then you can call the as_json method on that.
It would roughly look like:
#In Users Model:
def self.users_with_cars
User.left_outer_joins(:cars).group(users: {:id, :name}).select('users.id, users.name, COUNT(cars.id) as cars_count')
# OR may be something like this
User.all(:joins => :cars, :select => "users.*, count(cars.id) as cars_count", :group => "users.id")
end
And in your controller you can call as_json:
User.users_with_cars.as_json
Here is my solution in case someone else is interested.
# config/application.rb
config.autoload_paths += %W(#{config.root}/lib)
# config/initializers/core_extensions.rb
require 'core_extensions/active_record/relation/serialization'
ActiveRecord::Relation.include CoreExtensions::ActiveRecord::Relation::Serialization
# lib/core_extensions/active_record/relation/serialization.rb
require 'active_support/concern'
module CoreExtensions
module ActiveRecord
module Relation
module Serialization
extend ActiveSupport::Concern
included do
old_as_json = instance_method(:as_json)
define_method(:as_json) do |options = {}|
if #klass.respond_to? :collection_as_json
scoping do
#klass.collection_as_json options
end
else
old_as_json.bind(self).(options)
end
end
end
end
end
end
end
# app/models/user.rb
def User
...
def self.collection_as_json options = {}
cars_counts = Car.group(:user_id).count
self.map do |user|
user.define_singleton_method(:cars_count) do
cars_counts[user.id]
end
user.as_json options
end
end
end
Thanks #gwcodes for pointing me at ActiveSupport::Concern.

What is the best way to test activeadmin classes?

I have simple activeadmin class that looks like this:
ActiveAdmin.register Post do
actions :index
index do
index_columns
end
csv do
index_columns
end
def index_columns
column "Id" do |sp|
sp.id
end
end
end
How will be the best to test this code? Write some integrations specs with capybara or maybe there is some other way?
General idea behind testing gem's functionality - you don't test it.
Gems are (usually) already tested.
I agree with Andrey, but needed to do this for work. Here's how I tested the csv portion.
#csv_doc = StringIO.new
allow_any_instance_of(ActiveAdmin::ResourceController).to receive(:stream_resource) do |aa_controller|
receiver = []
# it's ok to mock this because it's literally their code: https://github.com/activeadmin/activeadmin/blob/master/lib/active_admin/resource_controller/streaming.rb#L38
aa_controller.class.active_admin_config.csv_builder.build(aa_controller, receiver)
receiver.each do |fees_as_csv|
#csv_doc << fees_as_csv
end
end
#csv_doc.rewind
csv_string = #csv_doc.readlines.join
csv = CSV.parse(csv_string, headers: true).map(&:to_hash)
expect(csv[0]["FIGURING THIS OUT"]).to eq "SUCKED"

Add Facebook metadata to Rails pages with sub-pages

I've tried Facebook's Open Graph protocol in adding meta data on Rails pages. What I want to do now is to make my code not duplicated or D.R.Y.---instead of putting one meta-data header for each controller page I have, I'd like to create a base class called "MyMetaBuilder" which will be inherited by the sub-pages, but don't know where and how to start coding it...
Someone suggested that meta data property values must be dynamically generated depending on the context. For example, PlayMetaBuilder, CookMetaBuilder and so on...
Also, when unit testing the controller action, how do I verify for its existence?
Thanks a lot.
One thing is defining the tags, another is rendering them. I would do the following:
write a controller mixin (something like acts_as_metatagable) where I would define specific fields for each controller (and populate the remaining with defaults). These would be assigned to a class (or instance) variable and in this way be made accessible in the rendering step).
write an helper function which would take all my tags and turn them into html. This helper function would then be called in the layout and be rendered in the head of the document.
so, it would look a bit like this:
# homepage_controller.rb
class HomepageController < ActionController::Base
# option 1.2: include it directly here with the line below
# include ActsAsMetatagable
acts_as_metatagable :title => "Title", :url => homepage_url
end
# lib/acts_as_metatagable.rb
module ActsAsMetatagable
module MetatagableMethods
#option 2.2: insert og_tags method here and declare it as helper method
def og_metatags
#og_tags.map do |k, v|
# render meta tags here according to its spec
end
end
def self.included(base)
base.helper_method :og_tags
end
end
def acts_as_metagabable(*args)
include MetatagableMethods
# insert dirty work here
end
end
# option 1.1: include it in an initializer
# initializers/acts_as_metatagable.rb
ActiveController::Base.send :include, ActsAsMetatagable
# option 2.1: insert og_metatags helper method in an helper
module ApplicationHelper
def og_metatags
#og_tags.map do |k, v|
# render meta tags here according to its spec
end
end
end
What I did for Scoutzie, was put all metadata into a head partial, with if/else cases as such:
%meta{:type => 'Author', :content => "Kirill Zubovsky"}
%meta{'property' => "og:site_name", :content=>"Scoutzie"}
-if #designer
...
-elsif #design
...
-else
...
This way, depending on the variables that load, I know which page it is, and thereby know which metadata to include. This might not be an elegant solution, but it works and it's really simple.

How to set defaults for parameters of url helper methods?

I use language code as a prefix, e.g. www.mydomain.com/en/posts/1.
This is what I did in routes.rb:
scope ":lang" do
resources :posts
end
Now I can easily use url helpers such as: post_path(post.id, :lang => :en). The problem is that I would like to use a value in a cookie as a default language. So I could write just post_path(post.id).
Is there any way how to set default values for parameters in url helpers? I can't find the source code of url helpers - can someone point me in the right direction?
Another way: I have already tried to set it in routes.rb but it's evaluated in startup time only, this does not work for me:
scope ":lang", :defaults => { :lang => lambda { "en" } } do
resources :posts
end
Ryan Bates covered this in todays railscast: http://railscasts.com/episodes/138-i18n-revised
You find the source for url_for here: http://api.rubyonrails.org/classes/ActionDispatch/Routing/UrlFor.html
You will see it merges the given options with url_options, which in turn calls default_url_options.
Add the following as private methods to your application_controller.rb and you should be set.
def locale_from_cookie
# retrieve the locale
end
def default_url_options(options = {})
{:lang => locale_from_cookie}
end
doesterr below has almost got it. That version of default_url_options won't play nice with others. You want to augment instead of clobber options passed in:
def locale_from_cookie
# retrieve the locale
end
def default_url_options(options = {})
options.merge(:lang => locale_from_cookie)
end
This is coding from my head, so no guarantee, but give this a try in an initializer:
module MyRoutingStuff
alias :original_url_for :url_for
def url_for(options = {})
options[:lang] = :en unless options[:lang] # whatever code you want to set your default
original_url_for
end
end
ActionDispatch::Routing::UrlFor.send(:include, MyRoutingStuff)
or straight monkey-patch...
module ActionDispatch
module Routing
module UrlFor
alias :original_url_for :url_for
def url_for(options = {})
options[:lang] = :en unless options[:lang] # whatever code you want to set your default
original_url_for
end
end
end
end
The code for url_for is in actionpack/lib/routing/url_for.rb in Rails 3.0.7

How to customize to_json response in Rails 3

I am using respond_with and everything is hooked up right to get data correctly. I want to customize the returned json, xml and foobar formats in a DRY way, but I cannot figure out how to do so using the limited :only and :include. These are great when the data is simple, but with complex finds, they fall short of what I want.
Lets say I have a post which has_many images
def show
#post = Post.find params[:id]
respond_with(#post)
end
I want to include the images with the response so I could do this:
def show
#post = Post.find params[:id]
respond_with(#post, :include => :images)
end
but I dont really want to send the entire image object along, just the url. In addition to this, I really want to be able to do something like this as well (pseudocode):
def show
#post = Post.find params[:id]
respond_with(#post, :include => { :foo => #posts.each.really_cool_method } )
end
def index
#post = Post.find params[:id]
respond_with(#post, :include => { :foo => #post.really_cool_method } )
end
… but all in a DRY way. In older rails projects, I have used XML builders to customize the output, but replicating it across json, xml, html whatever doesnt seem right. I have to imagine that the rails gurus put something in Rails 3 that I am not realizing for this type of behavior. Ideas?
You can override as_json in your model.
Something like:
class Post < ActiveRecord::Base
def as_json(options = {})
{
attribute: self.attribute, # and so on for all you want to include
images: self.images, # then do the same `as_json` method for Image
foo: self.really_cool_method
}
end
end
And Rails takes care of the rest when using respond_with. not entirely sure what options gets set to, but probably the options you give to respond_with (:include, :only and so on)
Probably too late, but I found a more DRY solution digging through the rails docs. This works in my brief tests, but may need some tweaking:
# This method overrides the default by forcing an :only option to be limited to entries in our
# PUBLIC_FIELDS list
def serializable_hash(options = nil)
options ||= {}
options[:only] ||= []
options[:only] += PUBLIC_FIELDS
options[:only].uniq!
super(options)
end
This basically allows you to have a list of fields that are allowed for your public API, and you cannot accidentally expose the whole object. You can still expose specific fields manually, but by default your object is secure for .to_json, .to_xml, etc.
It is not the rails 3 built-in way, but I found a great gem that is actively maintained on Rails 3: acts_as_api

Resources