How to sort enum with Ransack? - ruby-on-rails

I've got a table users with enum role set like this:
enum role: {
superadmin: 0,
admin: 1,
user: 2
}
And I want to sort it with Ransack like the rest of the attributes. How can I do this?
Of course, I could sort them alphabetically, but what if I want to add another role in the future?

I made a function based off the answer to https://stackoverflow.com/a/37599787/2055858
def alphabetical_enum_sql(enum, field_name)
ordered_enum = enum.keys.sort
order_by = ["case"]
ordered_enum.each_with_index do |key, idx|
order_by << "WHEN #{field_name}=#{enum[key]} THEN #{idx}"
end
order_by << "end"
order_by.join(" ")
end
and then used it like
scope :order_by_role, -> {
order(alphabetical_enum_sql(YourModel.roles, "role"))
}

Related

How to create an instance variable with FactoryBot on Rspec

I want to test a class function on RSpec on my Product class. To ease the reading I will keep it as this:
class Product < ApplicationRecord
private
def self.order_list(sort_by)
if sort_by == 'featured' || sort_by.blank?
self.order(sales: :desc)
elsif sort_by == 'name A-Z'
self.order(name: :asc)
elsif sort_by == 'name Z-A'
self.order(name: :desc)
elsif sort_by == 'rating 1-5'
self.order(:rating)
elsif sort_by == 'rating 5-1'
self.order(rating: :desc)
elsif sort_by == 'price low-high'
self.order(:price)
elsif sort_by == 'price high-low'
self.order(price: :desc)
elsif sort_by == 'date old-new'
self.order(:updated_at)
elsif sort_by == 'date new-old'
self.order(updated_at: :desc)
end
end
end
Once the function is called with a parameter, depending on the parameter that has been used, the list of products is ordered in a different way for the user to see.
I built a FactoryBot for the product model as well:
FactoryBot.define do
factory :product do
sequence(:name) { |n| "product_test#{n}" }
description { "Lorem ipsum dolor sit amet" }
price { rand(2000...10000) }
association :user, factory: :non_admin_user
rating { rand(1..5) }
trait :correct_availability do
availability { 1 }
end
trait :incorrect_availability do
availability { 3 }
end
#Adds a test image for product after being created
after(:create) do |product|
product.photos.attach(
io: File.open(Rails.root.join('test', 'fixtures', 'files', 'test.jpg')),
filename: 'test.jpg',
content_type: 'image/jpg'
)
end
factory :correct_product, traits: [:correct_availability]
factory :incorrect_product, traits: [:incorrect_availability]
end
end
Basically, we want to call the :correct_product to create a product thats accepted by the validations of the model.
For the specs:
describe ".order_list" do
let!(:first_product) { FactoryBot.create(:correct_product, name: "First Product" , rating: 1) }
let!(:second_product) { FactoryBot.create(:correct_product, name: "Second Product" , rating: 2) }
let!(:third_product) { FactoryBot.create(:correct_product, name: "Third Product" , rating: 3) }
it "orders products according to param" do
ordered_list = Product.order_list('rating 1-5')
expect(ordered_list.all).to eq[third_product, second_product, first_product]
end
end
So basically, my question is, how can I create an instance variable for each of the 3 mock products so I can name them here in the order I expect them to appear:
expect(ordered_list.all).to eq[third_product, second_product, first_product]
Or, even better, is there a way to create the instance variables with a loop and actually have the instance variable name to use them in the expect? This would free me from having to create 3 different variables on FactoryBot as I did.
I searched around the web and instance_variable_set is used in some cases:
Testing Rails with request specs, instance variables and custom primary keys
Simple instance_variable_set in RSpec does not work, but why not?
But it doesn´t seem to work for me.
Any idea on how could I make it work? Thanks!

How to merge different keys?

I have two Model classes
class Book
# attributes
# title
# status
# author_id
belongs_to :author
enum status: %w[pending published progress]
end
class Author
# attributes
has_many :books
end
I have an activerecord that return a list of books
The list
[<#Book><#Book><#Book>]
I use this function to group them
def group_by_gender_and_status
books.group_by { |book| book.author.gender }
.transform_values { |books| books.group_by(&:status).transform_values(&:count) }
end
and the outcome is
{
"female"=>{"progress"=>2, "pending"=>1, "published"=>2},
"male"=>{"published"=>3, "pending"=>4, "progress"=>4}
}
How do I merge progress and pending and name the key pending? so it would look like this
{
"female"=>{"pending"=>3, "published"=>2 },
"male"=>{"pending"=>8, "published"=>3, }
}
I prefer to use the group_by method vs the group by SQL for a reason. Thanks
def group_by_gender_and_status
books.group_by { |book| book.author.gender }.
transform_values do |books|
books.group_by { |book| book.status == 'progress' ? 'pending' : book.status }.
transform_values(&:count)
end
end

Looking for advise: Cleanest way to write scopes for filtering purposes [closed]

Closed. This question is opinion-based. It is not currently accepting answers.
Want to improve this question? Update the question so it can be answered with facts and citations by editing this post.
Closed 2 years ago.
Improve this question
Purpose of the post
I'm writing some code here to get an advise from people and see how they're writing clean ruby / rails code.
We're going to assume we have two models, User and Project. I wish to know how you'd create filters / scopes / methods for the best possible clean code.
class User < ApplicationRecord
# first_name, last_name, email
has_many :projects
end
class Project < ApplicationRecord
# title, description, published
belongs_to :user
end
Method 1
class User < ApplicationRecord
# first_name, last_name, email
has_many :projects
scope :with_first_name, -> (value) { where(first_name: value) }
scope :with_last_name, -> (value) { where(last_name: value) }
scope :with_email, -> (value) { where(email: value) }
scope :with_project_name, -> (value) { joins(:projects).where(projects: { name: value }) }
end
class UserController < ApplicationController
def index
#users = User.all
if params[:first_name].present?
#users = #users.with_first_name(params[:first_name])
end
if params[:last_name].present?
#users = #users.with_last_name(params[:last_name])
end
if params[:email].present?
#users = #users.with_email(params[:email])
end
if params[:project_name].present?
#users = #users.with_project_name(params[:project_name])
end
end
end
This can be useful, but we're gonna have a very fat controller. When we add more filters, we're going to have more and more conditions to fill.
It can also be refactored to:
class UserController < ApplicationController
def index
#users = User.all
{
first_name: :with_first_name,
last_name: :with_last_name,
email: :with_email,
project_name: :with_project_name,
}.each do |param, scope|
value = params[param]
if value.present?
#users = #users.public_send(scope, value)
end
end
end
end
but it will eliminate the possibility of having multiple params for a scope.
Method 2
Same as above, but in the model instead of controller:
class User < ApplicationRecord
# first_name, last_name, email
has_many :projects
scope :with_first_name, -> (value) { value ? where(first_name: value) : all }
scope :with_last_name, -> (value) { value.present ? where(last_name: value) : all }
scope :with_email, -> (value) { value.present ? where(email: value) : all }
scope :with_project_name, -> (value) { value.present? joins(:projects).where(projects: { name: value }) : all }
def self.filter(filters)
users = User.all
{
first_name: :with_first_name,
last_name: :with_last_name,
email: :with_email,
project_name: :with_project_name,
}.each do |param, scope|
value = filters[param]
if value.present?
users = users.public_send(scope, value)
end
end
users
end
end
class UserController < ApplicationController
def index
#users = User.filter(
params.permit(:first_name, :last_name, :email, :project_name)
)
end
end
Method 2
class User < ApplicationRecord
# first_name, last_name, email
has_many :projects
scope :with_first_name, -> (value) { value ? where(first_name: value) : all }
scope :with_last_name, -> (value) { value.present ? where(last_name: value) : all }
scope :with_email, -> (value) { value.present ? where(email: value) : all }
scope :with_project_name, -> (value) { value.present? joins(:projects).where(projects: { name: value }) : all }
end
class UserController < ApplicationController
def index
#users = User.all
#users = #users.with_first_name(params[:first_name])
#users = #users.with_last_name(params[:last_name])
#users = #users.with_email(params[:email])
#users = #users.with_project_name(params[:project_name])
end
end
This way, we add the value validation on the scope level, and we remove the param checking in the controller.
However, the repetition here is tremendous and would always return values even if the scope doesn't apply. ( ex: empty string ).
Final note
This post might not seem SO related, but would appreciate the input that anyone is going to give.
None of the above.
I would say that the cleanest way is to neither burdon your controller or User model further. Instead create a separate object which can be tested in isolation.
# Virtual model which represents a search query.
class UserQuery
include ActiveModel::Model
include ActiveModel::Attributes
attribute :first_name
attribute :last_name
attribute :email
attribute :project_name
# Loops through the attributes of the object and contructs a query
# will call 'filter_by_attribute_name' if present.
# #param [ActiveRecord::Relation] base_scope - is not mutated
# #return [ActiveRecord::Relation]
def resolve(base_scope = User.all)
valid_attributes.inject(base_scope) do |scope, key|
if self.respond_to?("filter_by_#{key}")
scope.merge(self.send("filter_by_#{key}"))
else
scope.where(key => self.send(key))
end
end
end
private
def filter_by_project_name
User.joins(:projects)
.where(projects: { name: project_name })
end
# Using compact_blank is admittedly a pretty naive solution for testing
# if attributes should be used in the query - but you get the idea.
def valid_attributes
attributes.compact_blank.keys
end
end
This is especially relevant when you're talking about a User class which usually is the grand-daddy of all god classes in a Rails application.
The key to the elegance here is using Enumerable#inject which lets you iterate accross the attributes and add more and more filters successively and ActiveRecord::SpawnMethods#merge which lets you mosh scopes together. You can think of this kind of like calling .where(first_name: first_name).where(last_name: last_name)... except in a loop.
Usage:
#users = UserQuery.new(
params.permit(:first_name, :last_name, :email, :project_name)
).resolve
Having a model means that you can use it for form bindings:
<%= form_with(model: #user_query, url: '/users/search') do |f| %>
# ...
<% end %>
And add validations and other features without making a mess.
scope :filter_users, -> (params) { where(conditions).with_project_name }
scope :with_project_name, -> (value) { value.present? joins(:projects).where(projects: { name: value }) : all }
def process_condition(attr, hash)
value = params[attr]
return hash if value.blank?
hash[attr] = value
hash
end
#This will return the conditions hash to be supplied to where. Since the param may have some other attribute which we may not need to apply filter, we construct conditions hash here.
def conditions
hash = {}
%i[last_name first_name email].each do |attr|
hash = process_condition(attr, hash)
end
hash
end
Finally, I would recommend you to check out ransack gem and the demo for the app is ransack demo. You can just use the search result of this gem by which you can support more filter options.

Filter by attribute in Rails by using Filterific gem

I have User model which has role (String) attribute. I want to filter my Users by this role attribute.
For filtering I'm using filterific gem.
Here is my index action:
def index
#filterrific = initialize_filterrific(
User,
params[:filterrific],
select_options: {
with_role: User.options_for_select
},
) or return
#users = #filterrific.find.page(params[:page])
end
where options_for_select method is defined:
# user.rb
def self.options_for_select
order('LOWER(role)').map { |e| [e.role, e.id] }
end
and in my view:
<%= f.select(
:with_role,
#filterrific.select_options[:with_role],
{ include_blank: '- Any -' }
) %>
and I have a scope in my user.rb:
scope :with_role, lambda { |roles|
where(role: [*roles])
}
I have only five roles, but in my select each role appears many times, so I don't know how to return unique roles in options_for_select method.
Secondly, options_for_select returns id's of user as a value in each option tag, but I want it to return role itself.
You define options_for_select on the User model. So it pulls in every user record in your database and its associated role entry, along with the user id.
Try the following instead of User.options_for_select in the controller:
select_options: {
with_role: ['Role1', 'Role2', 'Role3']
},

passing a block into create method in Ruby

I want to write a method which will work like this one
def create_new_car(salon)
Car.create name: salon.offer.name, description: salon.offer.description, photo: salon.offer.photo, dealer_id: salon.dealer_id
end
but i want to keep it DRY. is there a way to pass those attributes by, i dont know, iterating through array of attributes by passing a block?
You can pass a block to create:
def create_new_car(salon)
Car.create do |car|
car.name = salon.offer.name
car.description = salon.offer.description
car.photo = salon.offer.photo
car.dealer_id = salon.dealer_id
end
end
You could also set some attributes as the first parameter and then pass a block:
Car.create(name: salon.offer.name) do |car|
car.description = salon.offer.description
#...
end
You can implement any logic you want inside that block to assign the Car properties, like this:
attributes = ["name", "description", "photo", "dealer_id"]
Car.create do |car|
attributes.each { |a| car.send( "#{a}=", salon.offer.send(a) )
end
Please try this
array_of_attrbiutes = [{name: salon.offer.name...}, {name: }, {}...]
def create_new_car(array_of_attributes)
Car.create array_of_attributes
end
end
Please see https://github.com/rails/rails/blob/b15ce4a006756a0b6cacfb9593d88c9a7dfd8eb0/activerecord/lib/active_record/associations/collection_proxy.rb#L259

Resources