Toqua
Collection of controller utilities for rails applications. Created with the intention of bringing back most of the nice things about inherited resources, but in a more simple and explicit way.
Installation
Add this line to your application's Gemfile:
gem 'toqua'And then execute:
$ bundle
Or install it yourself as:
$ gem install toqua
Usage
This library contain different tools that can be used independently, described below.
Transform params
Use this to change the value of a params key, for example:
class MyController < ApplicationController
include Toqua::TransformParams
transform_params(:q) { |v| JSON.parse(v) }
endThis would transform the value of params[:q] from a raw string into its json representation. The new value is whatever returns your block. Also works on nested keys:
transform_params(user: :q) {|v| DataManipulation.change(v) }
Or more levels of nesting:
transform_params(user: {data: :q}) {|v| DataManipulation.change(v) }
Scoping
This allows you to further refine an ActiveRecord::Relation, optionally depending on conditionals. For example:
class MyController < ApplicationController
include Toqua::Scoping
scope {|s| s.where(parent_id: params[:parent_id])}
def index
@elements = apply_scopes(Element)
end
endThe scope definitions are lambdas that receive an ActiveRecord::Relation as parameter and must return another
AR::Relation. They are chained in the order they're defined. To use them, you explicitly call apply_scopes with the initial
argument.
You can use :if or :unless to conditionally choose if execute the scope or not. Examples:
scope(if: :show_all?) { |s| s.includes(:a, :b) }
This will call the method show_all? defined in the controller and their return value (truthy or falsey) will indicate if the scope
applies or not. You can also use anything that responds to :call directly, ex:
scope(if: -> { false }) { |s| s.all }
Finally, you can also condition the scope execution based on the action on the controller:
scope(only: :index) { |s| s.includes(:a, :b) }
This is the foundation used to build searching, sorting and pagination over an AR::Relation.
Used as an independent tool, it provides a way to define scopes used by multiple actions in the same place (authorization, eager loading, etc.).
Pagination
This tool can be used to paginate collections using Kaminari, providing some additional useful things. Example of basic usage:
class MyController < ApplicationController
include Toqua::Pagination
paginate
def index
@elements = apply_scopes(Element)
end
endAs the paginate method uses scoping, you can pass the options of :if, :unless and :action that will get forwarded to the scope method, allowing
you to conditionally decide when to paginate. Ex:
paginate(only: :index)
Or, to paginate only on html but not xlsx format:
paginate(unless: :xlsx?)
The names of the url parameters used to identify the current page and the number of results per page are page and per_page by default, but can be changed using:
paginate(page_key: "page", per_page_key: "per_page")
The number of results in each page can be controlled with the :per option:
paginate(per: 100)
The last option available is :headers. If used, 3 additional headers will be attached into the response allowing you to know info about the pagination of the collection. This is useful for API clients. Ex:
paginate(headers: true)
The response will include the following headers:
- 'X-Pagination-Total': Total number of elements in the collection without pagination
- 'X-Pagination-Per-Page': Number of elements per page
- 'X-Pagination-Page': Number of the current page
Finally, the method paginated? available in both the controller and the views will tell you if the collection has been paginated or not.
Search
Small utility to help in the implementation of searching, using Doure as a way to filter an AR model. Given that you have a model with filters defined, ex:
class Post < ApplicationRecord
extend Doure::Filterable
filter_class PostFilter
end
class PostFilter
cont_filter(:title)
cont_filter(:slug)
present_filter(:scheduled_at)
eq_filter(:id)
filter(:category_id_eq) { |s, value| s.joins(:post_categories).where(post_categories: {category_id: value}) }
endYou can setup searching in the controller using:
class PostsController < ApplicationController
include Toqua::Search
searchable
def index
@elements = apply_scopes(Post)
end
endThe parameter used to represent the search criteria is :q.
The method search_params will give you a hash representing the contents of :q, which is the current search criteria, for example:
{title_cont: "Air", category_id_eq: "12", slug_cont: "", scheduled_at_present: "", id_eq: ""}
The method active_search_params will give you only the search parameters containing some value:
{title_cont: "Air", category_id_eq: "12"}
The method search_object, available in the view, gives an ActiveModel like object stuffed with the current search_params, so you can use that as the object
of the search form to automatically pre-fill all the search inputs with their current value. Ex:
= form_for search_object do |f|
= f.input :title_cont
= f.input :category_id_eq, collection: ...The method searching? will tell you if there's a current search or not.
Finally, you can define a default_search_params method in the controller to setup default search criteria:
def default_search_params
{ visible_by_role: "editor" }
endAs a final note, remember to take care of properly sanitize the input of your search criteria to avoid unintended usage, using TransformParams as seen before or by any other means. Toqua doesn't apply any sanitization by default, since the values that may come from the view can vary between use cases (strings, arrays, hashes, etc.).
Sorting
The sorting utility allows you to sort the collection, using the parameter s in the url with a format like title+asc or title+desc. Usage example:
class PostsController < ApplicationController
include Toqua::Sorting
sorting
def index
@elements = apply_scopes(Post)
end
endA helper to create sorting links easily is not directly provided by the gem, but can be something like this:
def sort_link(name, label = nil, opts = {})
label ||= name.to_s.humanize
current_attr_name, current_direction = params[:s].present? && params[:s].split("+").map(&:strip)
next_direction = opts.fetch(:default_order, current_direction == "asc" ? "desc" : "asc")
parameters = request.query_parameters
parameters.merge!(opts[:link_params]) if opts[:link_params]
dest_url = url_for(parameters.merge(s: "#{name}+#{next_direction}"))
direction_icon = current_direction == "asc" ? "↑" : "↓"
anchor = current_attr_name == name.to_s ? "#{label} #{direction_icon}" : label
link_opts = opts.fetch(:link_opts, {})
link_to(anchor, dest_url, link_opts)
endThen used as:
= sort_link :title, "Title"
Keyset pagination
The keyset pagination is similar to the pagination utility, but working with OrderQuery to provide pagination that works with no offsets. Example usage:
class PostsController < ApplicationController
include Toqua::KeysetPagination
keyset_paginate :score_order
def index
@elements = apply_scopes(Post)
end
endIt takes care of applying the correct scoping based on the id of the current element, as identified by the :idx parameter as default. With the optional :headers parameter some headers are also added into the response:
keyset_paginate :score_order, headers: true
Will generate those headers:
-
'X-KeysetPagination-Index': Theidof the current element index. -
'X-KeysetPagination-Next-Index': Theidof the element to use as the next page. -
'X-KeysetPagination-Prev-Index': Theidof the element to use as the previous page.
The next and prev indexes are also available via the instance vars @keyset_pagination_prev_index and @keyset_pagination_next_index.
If the value of @keyset_pagination_prev_index (or via header) is -1 it means the previous page is the initial one. If it's nil, there's no previous page.
To generate pagination links, you can use something like this:
def keyset_pagination_next_link(index_key = :idx)
if @keyset_pagination_next_index
url_for(request.GET.merge(index_key => @keyset_pagination_next_index))
end
end
def keyset_pagination_prev_link(index_key = :idx)
if @keyset_pagination_prev_index
if @keyset_pagination_prev_index == -1
url_for(request.GET.merge(index_key => nil))
else
url_for(request.GET.merge(index_key => @keyset_pagination_prev_index))
end
end
endFinal notes
If you use multiple scope declarations either mixed with the other utilities shown here or not, be aware of the order. For example, pagination must always go last.
Development
After checking out the repo, run bin/setup to install dependencies. Then, run rake test to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and tags, and push the .gem file to rubygems.org.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/toqua.