Change default sort order in ActiveAdmin scope - ruby-on-rails

For most models, the default sort order (id desc) is fine. But for a couple of scopes on one of my models, it would make more sense to reverse the order, or order by the updated_at field.
I seem unable to achieve this without breaking other functionality, so I'm hoping someone else might teach me how to do this!
I've tried adding a .order() on the objects I'm returning:
scope :example do |models|
models.order('id asc')
end
This does not seem to have any effect.
I've also tried unscoping, which kind of works. It does sort my objects the way I want to, but it completely breaks all filtering/search functionality.
scope :example do |models|
models.unscoped.order('id asc')
end
What to do?

use config.sort_order like:
config.sort_order = "updated_at_desc"

Why not make 2 scopes in your model, one for a particular sort order, the other, reversed, then in ActiveAdmin set one as the default?
scope :example_asc, :default => true
scope :example_desc
If that doesn't work for you, perhaps create a controller block in ActiveAdmin that defines what you're trying to do:
controller do
def asc
Model.order('id ASC')
end
def desc
Model.order('id DESC')
end
end
scope :example do |models|
asc
end
scope :example do |models|
desc
end
And finally, I think this answer is probably fairly accurate: https://stackoverflow.com/a/17612718/175825
But you might want to investigate more about how you can implement sort_order:
https://github.com/gregbell/active_admin/blob/master/lib/active_admin/dsl.rb#L25
https://github.com/gregbell/active_admin/blob/master/lib/active_admin/dsl.rb#L97
For what it's worth, I'm not a fan of ActiveAdmin's skinny docs. Good luck.

This works and also ensures that the default sort order to correctly reflected in the UI.
This is overwriting the apply_sorting method of ActiveAdmin, so all the usual caveats about monkey patching third party gems apply.
module ActiveAdmin
class ResourceController < BaseController
module CallableSortOrder
def apply_sorting(chain)
params[:order] ||= if active_admin_config.sort_order&.respond_to?(:call)
active_admin_config.sort_order.call(self)
else
active_admin_config.sort_order
end
super
end
end
prepend CallableSortOrder
end
end
Use it like this:
config.sort_order = ->(controller) {
controller.params[:scope] == 'something' ? 'created_at_desc' : 'name_asc'
}

You need to use reorder to override the default order, but also be sensitive as to whether ActiveAdmin sorts are being applied
scope :example do |models|
if params[:order].blank? or params[:order] == "id_desc" #default ordering
models.example.reorder('your order SQL here')
else
models.example
end
end

#yxf's answer is correct, but I found it unclear ("where does config come from?!").
Docs: https://activeadmin.info/3-index-pages.html (just search "order")
Code:
ActiveAdmin.register Post do
config.sort_order = 'name_asc'
end

Related

Rails Administrate gem: How to sort records by 'Published At'?

I'm very much new to the Administrate gem for Ruby on Rails.
I can't make it sort/order posts by "Published At". Records seem to be ordered by their id's and not the published_at date, how can I make it work?
See the example app over at Heroku:
https://administrate-prototype.herokuapp.com/admin/blog/posts
Thanks in advance
Your looking for Model.order function, here are the docs:
https://guides.rubyonrails.org/active_record_querying.html#ordering
If you have a model named Post, when defining it in the controller function show or any other that you are using, you can get the record like below so that it comes sorted by published_at
Post.order(published_at: :desc) # ActiveRecord ordering
# OR
Post.sort_by(&:published_at) # pure Ruby sorting implementation
If you always want it come in sorted by published_at you can use default_scope of rails in Post like below -
default_scope { order(published_at: :desc) }
Its easy, you need to modify the request.query_parameters not params to get your job done.
Here is an example
# app/controllers/admin/users_controller.rb
# frozen_string_literal: true
module Admin
class UsersController < Admin::ApplicationController
before_action :default_params
def default_params
request.query_parameters[:user] ||= {} # change :user --> your resource
request.query_parameters[:user][:order] ||= :id # your field
request.query_parameters[:user][:direction] ||= :desc # your direction
end

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.

Implementing simpler find_by_[attribute] code in Rails

I have a table of statuses, each of which have a name attribute. Currently I can do:
FooStatus.find_by_name("bar")
And that's fine. But I'm wondering if I could do:
FooStatus.bar
So I have this approach:
class FooStatus < ActiveRecord::Base
def self.method_missing(meth, *args, &block)
if self.allowed_statuses.include?(meth.to_s.titleize)
self.where("name = ?", meth.to_s.titleize).first
else
super(meth, *args, &block)
end
end
def self.allowed_statuses
self.pluck(:name)
end
end
The above code works, but it leads to the following weird behavior:
FooStatus.respond_to?(:bar) => false
FooStatus.bar => #<FooStatus name: 'bar'>
That's not great, but if I try to implement respond_to?, I get a recursion problem
class FooStatus < ActiveRecord::Base
def self.method_missing(meth, *args, &block)
if self.allowed_statuses.include?(meth.to_s.titleize)
self.where("name = ?", meth.to_s.titleize).first
else
super(meth, *args, &block)
end
end
def self.allowed_statuses
self.pluck(:name)
end
def self.respond_to?(meth, include_private = false)
if self.allowed_statuses.include?(meth.to_s.titleize)
true
else
super(meth)
end
end
end
And that gets me:
FooStatus.bar => ThreadError: deadlock; recursive locking
Any ideas on getting method_missing and respond_to to work together?
I agree with Philip Hallstrom's suggestion. If you know allowed_statuses when the class is built, then just loop through the list and define the methods explicitly:
%w(foo bar baz).each do |status|
define_singleton_method(status) do
where("name = ?", status.titleize).first
end
end
…or if you need that list of statuses elsewhere in the code:
ALLOWED_STATUSES = %w(foo bar baz).freeze
ALLOWED_STATUSES.each do |status|
define_singleton_method(status) do
where("name = ?", status.titleize).first
end
end
Clearer, shorter, and far less prone to future breakage and weird rabbit hole conflicts with ActiveRecord like the one you're in.
You can do really cool things with method_missing and friends, but it's not the first approach to go to when doing metaprogramming. Explicit is usually better when possible.
I also agree with Philip's concernt about creating conflicts with built-in methods. Having a hard-coded list of statuses prevents that from going too far, but you might consider a convention like FooStatus.named_bar instead of FooStatus.bar if that list is likely to grow or change.
I don't know if I'd recommend your approach... seems too magical to me and I worry about what happens when you have a status with a name of 'destroy' or some other method you might legitimately want to call (or that Rails' calls internally that you aren't aware of).
But... instead of mucking with method missing, I think you'd be better off extending the class and automatically defining the methods by looping through allowed_statuses and creating them. This will make respond_to? work. And you could also check to make sure it's not already defined somewhere else...
Use a scope.
class FooStatus < ActiveRecord::Base
scope :bar, where(:name => "bar")
# etc
end
Now, you can do FooStatus.bar which will return an ActiveRelation object. If you expect this to return a single instance, you could do FooStatus.bar.first or if many FooStatus.bar.all, or you could put the .first or .all on the end of the scope in which case it'll return the same thing as the finder.
You can also define a scope with a lambda if the input isn't constant (not always "bar"). Section 13.1 of this guide has an example

How do I modify Rails ActiveRecord query results before returning?

I have a table with data that needs to be updated at run-time by additional data from an external service. What I'd like to do is something like this:
MyModel.some_custom_scope.some_other_scope.enhance_with_external_data.each do |object|
puts object.some_attribute_from_external_data_source
end
Even if I can't use this exact syntax, I would like the end result to respect any scopes I may use. I've tried this:
def self.enhance_with_external_data
external_data = get_external_data
Enumerator.new do |yielder|
# mimick some stuff I saw in ActiveRecord and don't quite understand:
relation.to_a.each do |obj|
update_obj_with_external_data(obj)
yielder.yield(obj)
end
end
end
This mostly works, except it doesn't respect any previous scopes that were applied, so if I do this:
MyModel.some_custom_scope.some_other_scope.enhance_with_external_data
It gives back ALL MyModels, not just the ones scoped by some_custom_scope and some_other_scope.
Hopefully what I'm trying to do makes sense. Anyone know how to do it, or am I trying to put a square peg in a round hole?
I figured out a way to do this. Kind of ugly, but seems to work:
def self.merge_with_extra_info
the_scope = scoped
class << the_scope
alias :base_to_a :to_a
def to_a
MyModel.enhance(base_to_a)
end
end
the_scope
end
def self.enhance(items)
items.each do |item|
item = add_extra_item_info(item)
end
items
end
What this does is add a class method to my model - which for reasons unknown to me seems to also make it available to ActiveRecord::Relation instances. It overrides, just for the current scope object, the to_a method that gets called to get the records. That lets me add extra info to each record before returning. So now I get all the chainability and everything like:
MyModel.where(:some_attribute => some_value).merge_with_extra_info.limit(10).all
I'd have liked to be able to get at it as it enumerates versus after it's put into an array like here, but couldn't figure out how to get that deep into AR/Arel.
I achieved something similar to this by extending the relation:
class EnhancedModel < DelegateClass(Model)
def initialize(model, extra_data)
super(model)
#extra_data = extra_data
end
def use_extra_data
#extra_data.whatever
end
end
module EnhanceResults
def to_a
extra_data = get_data_from_external_source(...)
super.to_a.map do |model_obj|
EnhancedModel.new(model_obj, extra_data)
end
end
end
models = Model.where('condition')
models.extend(EnhanceResults)
models.each do |enhanced_model|
enhanced_model.use_extra_data
end

searchlogic with globalize2?

Given there is a model:
class MenuItem < ActiveRecord::Base
translates :title
end
and searchlogic is plugged in, I'd expect the following to work:
>> MenuItem.search(:title_like => 'tea')
Sadly, it doesn't:
Searchlogic::Search::UnknownConditionError: The title_like is not a valid condition. You may only use conditions that map to a named scope
Is there a way to make work?
P.S.
The closest I managed to get workging, was:
>> MenuItem.search(:globalize_translations_title_like => 'tea')
Which doesn't look nice.
I developed searchlogic. By default, it leverages existing named scopes and the database columns. It can't really go beyond that because ultimately it has to create the resulting SQL using valid column names. That said, there really is no way for searchlogic to cleanly understand what your :title attribute means. Even if it did, it would be specific to the logic defined in your translation library. Which is a red flag that this shouldn't be in the library itself, but instead a plugin or code that gets initialized within your app.
Why not override the method_missing method and do the mapping yourself? Searchlogic provides and easy way to alias scoped by doing alias_scope:
alias_scope :title_like, lambda { |value| globalize_translations_title_like(value) }
Here's a quick stab (this is untested):
module TranslationsMapping
def self.included(klass)
klass.class_eval do
extend ClassMethods
end
end
module ClassMethods
protected
def method_missing(name, *args, &block)
translation_attributes = ["title"].join("|")
conditions = (Searchlogic::NamedScopes::Conditions::PRIMARY_CONDITIONS +
Searchlogic::NamedScopes::Conditions::ALIAS_CONDITIONS).join("|"))
if name.to_s =~ /^(#{translation_attributes})_(#{conditions})$/
attribute_name = $1
condition_name = $2
alias_scope "#{attribute_name}_#{condition_name}", lambda { |value| send("globalize_translations_#{attribute_name}_#{condition_name}", value) }
send(name, *args, &block)
else
super
end
end
end
end
ActiveRecord::Base.send(:include, TranslationsMapping)
Hope that helps. Again, I haven't tested the code, but you should get the general idea. But I agree, the implementation of the translations should be behind the scenes, you really should never be typing "globalize_translations" anywhere in your app, that should be take care of transparently on the model level.

Resources