├── lib ├── admino │ ├── version.rb │ ├── table.rb │ ├── query │ │ ├── scope_presenter.rb │ │ ├── builder.rb │ │ ├── base_presenter.rb │ │ ├── dsl.rb │ │ ├── search_field.rb │ │ ├── filter_group.rb │ │ ├── sorting_presenter.rb │ │ ├── sorting.rb │ │ ├── filter_group_presenter.rb │ │ ├── configuration.rb │ │ └── base.rb │ ├── query.rb │ ├── table │ │ ├── row.rb │ │ ├── head_row.rb │ │ ├── presenter.rb │ │ └── resource_row.rb │ └── action_view_extension.rb └── admino.rb ├── logo.jpg ├── Gemfile ├── TODO.md ├── Rakefile ├── Appraisals ├── gemfiles ├── rails_40.gemfile ├── rails_42.gemfile ├── rails_40.gemfile.lock └── rails_42.gemfile.lock ├── .gitignore ├── .travis.yml ├── LICENSE.txt ├── CHANGELOG.md ├── admino.gemspec ├── spec ├── admino │ ├── query │ │ ├── dsl_spec.rb │ │ ├── search_field_spec.rb │ │ ├── base_presenter_spec.rb │ │ ├── filter_group_spec.rb │ │ ├── sorting_spec.rb │ │ ├── filter_group_presenter_spec.rb │ │ ├── base_spec.rb │ │ └── sorting_presenter_spec.rb │ ├── table │ │ ├── row_spec.rb │ │ ├── head_row_spec.rb │ │ ├── presenter_spec.rb │ │ └── resource_row_spec.rb │ └── integration_spec.rb └── spec_helper.rb ├── CONTRIBUTING.md └── README.md /lib/admino/version.rb: -------------------------------------------------------------------------------- 1 | module Admino 2 | VERSION = "0.0.22" 3 | end 4 | -------------------------------------------------------------------------------- /logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cantierecreativo/admino/HEAD/logo.jpg -------------------------------------------------------------------------------- /lib/admino/table.rb: -------------------------------------------------------------------------------- 1 | require 'admino/table/presenter' 2 | 3 | module Admino 4 | module Table 5 | end 6 | end 7 | 8 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | 5 | gem 'appraisal' 6 | gem 'coveralls', require: false 7 | 8 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | * Allow validations of input in Query::Base objects 2 | * Support Rails date_select and datetime_select params 3 | * Prepare a demo 4 | 5 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require 'rspec/core/rake_task' 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | task :default => :spec 6 | 7 | -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | appraise "rails-40" do 2 | gem "rails", "4.0.0" 3 | gem "simple_form", "3.0.3" 4 | end 5 | 6 | appraise "rails-42" do 7 | gem "rails", "4.2.1" 8 | gem "simple_form", "3.1.0" 9 | end 10 | -------------------------------------------------------------------------------- /gemfiles/rails_40.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "appraisal" 6 | gem "coveralls", :require => false 7 | gem "rails", "4.0.0" 8 | gem "simple_form", "3.0.3" 9 | 10 | gemspec :path => "../" 11 | -------------------------------------------------------------------------------- /gemfiles/rails_42.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "appraisal" 6 | gem "coveralls", :require => false 7 | gem "rails", "4.2.1" 8 | gem "simple_form", "3.1.0" 9 | 10 | gemspec :path => "../" 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | binstubs/ 13 | pkg 14 | rdoc 15 | spec/reports 16 | test/tmp 17 | test/version_tmp 18 | tmp 19 | bin/* 20 | 21 | -------------------------------------------------------------------------------- /lib/admino.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/core_ext/module/delegation' 2 | 3 | require "admino/version" 4 | require "admino/query" 5 | require "admino/table" 6 | require "admino/action_view_extension" 7 | 8 | module Admino 9 | end 10 | 11 | ActiveSupport.on_load(:action_view) do 12 | include Admino::ActionViewExtension 13 | end 14 | 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | 3 | rvm: 4 | - "2.0.0" 5 | - "2.1.1" 6 | - "2.2.0" 7 | - "2.3.0" 8 | 9 | gemfile: 10 | - "gemfiles/rails_40.gemfile" 11 | - "gemfiles/rails_42.gemfile" 12 | 13 | before_install: 14 | - gem install bundler 15 | 16 | install: 17 | - bundle install --jobs=3 --retry=3 && bundle update 18 | 19 | script: "bundle exec rake spec" 20 | -------------------------------------------------------------------------------- /lib/admino/query/scope_presenter.rb: -------------------------------------------------------------------------------- 1 | require 'showcase' 2 | 3 | module Admino 4 | module Query 5 | class ScopePresenter < Showcase::Presenter 6 | def initialize(object, parent, view_context) 7 | super(object, view_context) 8 | @parent = parent 9 | end 10 | 11 | def link(*args) 12 | @parent.scope_link(object, *args) 13 | end 14 | end 15 | end 16 | end 17 | 18 | -------------------------------------------------------------------------------- /lib/admino/query.rb: -------------------------------------------------------------------------------- 1 | require 'admino/query/base' 2 | require 'admino/query/base_presenter' 3 | require 'admino/query/configuration' 4 | require 'admino/query/search_field' 5 | require 'admino/query/filter_group' 6 | require 'admino/query/filter_group_presenter' 7 | require 'admino/query/sorting' 8 | require 'admino/query/sorting_presenter' 9 | require 'admino/query/scope_presenter' 10 | 11 | module Admino 12 | module Query 13 | end 14 | end 15 | 16 | -------------------------------------------------------------------------------- /lib/admino/query/builder.rb: -------------------------------------------------------------------------------- 1 | module Admino 2 | module Query 3 | class Builder 4 | attr_accessor :scope 5 | attr_reader :context 6 | 7 | def initialize(context, scope) 8 | @context = context 9 | @scope = scope 10 | end 11 | 12 | private 13 | 14 | def method_missing(method, *args) 15 | context_method = "#{method}_scope" 16 | if context.respond_to?(context_method) 17 | Builder.new(context, context.send(context_method, scope, *args)) 18 | else 19 | Builder.new(context, scope.send(method, *args)) 20 | end 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/admino/query/base_presenter.rb: -------------------------------------------------------------------------------- 1 | require 'showcase' 2 | 3 | module Admino 4 | module Query 5 | class BasePresenter < Showcase::Presenter 6 | presents_collection :filter_groups 7 | presents :sorting 8 | 9 | def form(options = {}, &block) 10 | h.form_for( 11 | self, 12 | options.reverse_merge(default_form_options), 13 | &block 14 | ) 15 | end 16 | 17 | def simple_form(options = {}, &block) 18 | h.simple_form_for( 19 | self, 20 | options.reverse_merge(default_form_options), 21 | &block 22 | ) 23 | end 24 | 25 | def default_form_options 26 | { 27 | as: :query, 28 | method: :get, 29 | url: h.request.fullpath 30 | } 31 | end 32 | end 33 | end 34 | end 35 | 36 | -------------------------------------------------------------------------------- /lib/admino/query/dsl.rb: -------------------------------------------------------------------------------- 1 | module Admino 2 | module Query 3 | module Dsl 4 | def config 5 | @config ||= Admino::Query::Configuration.new 6 | end 7 | 8 | def search_field(name, options = {}) 9 | config.add_search_field(name, options) 10 | 11 | define_method name do 12 | search_field_by_name(name).value 13 | end 14 | end 15 | 16 | def filter_by(name, scopes, options = {}) 17 | config.add_filter_group(name, scopes, options) 18 | 19 | define_method name do 20 | filter_group_by_name(name).value.to_s 21 | end 22 | end 23 | 24 | def sorting(*args) 25 | options = args.extract_options! 26 | config.add_sorting_scopes(args, options) 27 | end 28 | 29 | def starting_scope(&block) 30 | config.starting_scope_callable = block 31 | end 32 | 33 | def ending_scope(&block) 34 | config.ending_scope_callable = block 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Stefano Verna 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v0.0.10 2 | 3 | * Fix bug in action generation of html attributes 4 | 5 | # v0.0.9 6 | 7 | * Allow `include_empty_scope` option in filter groups 8 | 9 | ``` 10 | filter_by :status, [:foo, :bar], include_empty_scope: true 11 | ``` 12 | 13 | # v0.0.8 14 | 15 | * Support a symbol column label. It will use human attribute name: 16 | 17 | ``` 18 | = row.column :truncated_title, :title 19 | ``` 20 | 21 | # v0.0.7 22 | 23 | * `#scope_params` does not change request params 24 | 25 | # v0.0.6 26 | 27 | * Moved the filter group params inside of the `:query` hash 28 | 29 | # v0.0.5 30 | 31 | * Rename Field into SearchField 32 | * Admino::Table::Presenter no longer presents collection by default 33 | 34 | # v0.0.4 35 | 36 | * Rename Group into FilterGroup 37 | * Rename `FilterGroup#available_scopes` into `#scopes` 38 | * Rename `Sorting#available_scopes` into `#scopes` 39 | * Removed nil scope in `FilterGroup` 40 | * Clicking on an active filter scope link will deactivate it 41 | 42 | # v0.0.3 43 | 44 | * Fixed bug in `SortingPresenter` with default scope 45 | 46 | # v0.0.2 47 | 48 | * Support to sortings 49 | 50 | # v0.0.1 51 | 52 | * First release 53 | 54 | -------------------------------------------------------------------------------- /lib/admino/query/search_field.rb: -------------------------------------------------------------------------------- 1 | require 'coercible' 2 | require 'active_support/hash_with_indifferent_access' 3 | require 'active_support/core_ext/hash' 4 | 5 | module Admino 6 | module Query 7 | class SearchField 8 | attr_reader :params 9 | attr_reader :config 10 | 11 | def initialize(config, params) 12 | @config = config 13 | @params = ActiveSupport::HashWithIndifferentAccess.new(params) 14 | end 15 | 16 | def augment_scope(scope) 17 | if present? 18 | scope.send(scope_name, value) 19 | else 20 | scope 21 | end 22 | end 23 | 24 | def value 25 | value = params.fetch(:query, {}).fetch(param_name, nil) 26 | 27 | if config.coerce_to 28 | value = begin 29 | coercer = Coercible::Coercer.new 30 | coercer[value.class].send(config.coerce_to, value) 31 | rescue Coercible::UnsupportedCoercion 32 | nil 33 | end 34 | end 35 | 36 | value || config.default_value 37 | end 38 | 39 | def present? 40 | value.present? 41 | end 42 | 43 | def param_name 44 | config.name 45 | end 46 | 47 | def scope_name 48 | config.name 49 | end 50 | end 51 | end 52 | end 53 | 54 | -------------------------------------------------------------------------------- /lib/admino/table/row.rb: -------------------------------------------------------------------------------- 1 | module Admino 2 | module Table 3 | class Row 4 | attr_reader :view_context 5 | 6 | alias_method :h, :view_context 7 | 8 | def initialize(view_context) 9 | @view_context = view_context 10 | end 11 | 12 | def parse_column_args(args) 13 | html_options = args.extract_options! 14 | 15 | attribute_name = if args.first.is_a?(Symbol) 16 | args.shift 17 | else 18 | nil 19 | end 20 | 21 | label = if args.first.is_a?(String) || args.first.is_a?(Symbol) 22 | args.shift 23 | else 24 | nil 25 | end 26 | 27 | [attribute_name, label, html_options] 28 | end 29 | 30 | def parse_action_args(args) 31 | html_options = args.extract_options! 32 | 33 | action_name = if args.first.is_a?(Symbol) 34 | args.shift 35 | else 36 | nil 37 | end 38 | 39 | url = if args.first.is_a?(String) 40 | args.shift 41 | else 42 | nil 43 | end 44 | 45 | label = args.shift 46 | 47 | [action_name, url, label, html_options] 48 | end 49 | 50 | def to_html 51 | nil 52 | end 53 | end 54 | end 55 | end 56 | 57 | -------------------------------------------------------------------------------- /admino.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'admino/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = 'admino' 8 | spec.version = Admino::VERSION 9 | spec.authors = ['Stefano Verna'] 10 | spec.email = ['s.verna@cantierecreativo.net'] 11 | spec.description = %q{Make administrative views creation less repetitive} 12 | spec.summary = %q{Make administrative views creation less repetitive} 13 | spec.homepage = 'https://github.com/cantierecreativo/admino' 14 | spec.license = 'MIT' 15 | 16 | spec.files = `git ls-files`.split($/) 17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 19 | spec.require_paths = ['lib'] 20 | 21 | spec.add_dependency 'showcase', '~> 0.2.5' 22 | spec.add_dependency 'activesupport' 23 | spec.add_dependency 'activemodel' 24 | spec.add_dependency 'coercible' 25 | 26 | spec.add_development_dependency 'bundler', '~> 1.3' 27 | spec.add_development_dependency 'rake' 28 | spec.add_development_dependency 'rspec' 29 | spec.add_development_dependency 'pry' 30 | spec.add_development_dependency 'i18n' 31 | spec.add_development_dependency 'rspec-html-matchers' 32 | spec.add_development_dependency 'actionpack' 33 | spec.add_development_dependency 'simple_form' 34 | end 35 | 36 | -------------------------------------------------------------------------------- /spec/admino/query/dsl_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Admino 4 | module Query 5 | describe Dsl do 6 | let(:config) { TestQuery.config } 7 | let(:instance) { TestQuery.new } 8 | 9 | it 'allows #search_field declaration' do 10 | search_field = config.search_fields.last 11 | expect(search_field.name).to eq :starting_from 12 | expect(search_field.coerce_to).to eq :to_date 13 | end 14 | 15 | it 'allows #filter_by declaration' do 16 | filter_group = config.filter_groups.first 17 | expect(filter_group.name).to eq :bar 18 | expect(filter_group.scopes).to eq [:one, :two] 19 | expect(filter_group.include_empty_scope?).to be_truthy 20 | end 21 | 22 | it 'allows #sortings declaration' do 23 | sorting = config.sorting 24 | expect(sorting.scopes).to eq [:by_title, :by_date] 25 | expect(sorting.default_scope).to eq :by_title 26 | expect(sorting.default_direction).to eq :desc 27 | end 28 | 29 | it 'allows #starting_scope block declaration' do 30 | expect(config.starting_scope_callable.call).to eq 'start' 31 | end 32 | 33 | it 'allows #ending_scope block declaration' do 34 | expect(config.ending_scope_callable.call).to eq 'end' 35 | end 36 | 37 | context 'with a search_field' do 38 | let(:search_field) { double('SearchField', value: 'value') } 39 | 40 | before do 41 | allow(instance).to receive(:search_field_by_name). 42 | with(:foo). 43 | and_return(search_field) 44 | end 45 | 46 | it 'it generates a getter' do 47 | expect(instance.foo).to eq 'value' 48 | end 49 | end 50 | end 51 | end 52 | end 53 | 54 | -------------------------------------------------------------------------------- /lib/admino/query/filter_group.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/hash_with_indifferent_access' 2 | require 'active_support/core_ext/hash' 3 | 4 | module Admino 5 | module Query 6 | class FilterGroup 7 | attr_reader :params 8 | attr_reader :config 9 | attr_reader :query_i18n_key 10 | 11 | def initialize(config, params, query_i18n_key = nil) 12 | @config = config 13 | @params = ActiveSupport::HashWithIndifferentAccess.new(params) 14 | @query_i18n_key = query_i18n_key 15 | end 16 | 17 | def augment_scope(scope) 18 | if active_scope && active_scope != :empty 19 | scope.send(active_scope) 20 | else 21 | scope 22 | end 23 | end 24 | 25 | def active_scope 26 | if value && scopes.include?(value) 27 | value 28 | else 29 | nil 30 | end 31 | end 32 | 33 | def is_scope_active?(scope) 34 | active_scope == scope.to_sym 35 | end 36 | 37 | def value 38 | value = params.fetch(:query, {}).fetch(param_name, default_value) 39 | if value 40 | value.to_sym 41 | else 42 | nil 43 | end 44 | end 45 | 46 | def default_value 47 | if config.default_scope 48 | config.default_scope 49 | elsif config.include_empty_scope? 50 | :empty 51 | else 52 | nil 53 | end 54 | end 55 | 56 | def param_name 57 | config.name 58 | end 59 | 60 | def scopes 61 | @scopes ||= config.scopes.dup.tap do |scopes| 62 | if config.include_empty_scope? 63 | scopes.unshift :empty 64 | end 65 | end 66 | end 67 | 68 | def i18n_key 69 | config.name 70 | end 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /lib/admino/query/sorting_presenter.rb: -------------------------------------------------------------------------------- 1 | require 'showcase' 2 | require 'active_support/core_ext/object/deep_dup' 3 | 4 | module Admino 5 | module Query 6 | class SortingPresenter < Showcase::Presenter 7 | def scope_link(scope, *args) 8 | options = args.extract_options! 9 | 10 | label = args.first || scope_name(scope) 11 | 12 | desc_class = options.delete(:desc_class) { 'is-desc' } 13 | asc_class = options.delete(:asc_class) { 'is-asc' } 14 | 15 | options = Showcase::Helpers::HtmlOptions.new(options) 16 | 17 | if is_scope_active?(scope) 18 | options.add_class!(ascending? ? asc_class : desc_class) 19 | end 20 | 21 | h.link_to label, scope_path(scope), options.to_h 22 | end 23 | 24 | def scope_path(scope) 25 | h.request.path + "?" + scope_params(scope).to_query 26 | end 27 | 28 | def scopes 29 | @scopes ||= super.map do |scope| 30 | ScopePresenter.new(scope, self, view_context) 31 | end 32 | end 33 | 34 | def scope_params(scope) 35 | params = ActiveSupport::HashWithIndifferentAccess.new( 36 | h.request.query_parameters.deep_dup 37 | ) 38 | 39 | if is_scope_active?(scope) 40 | params.merge!(sorting: scope.to_s, sort_order: ascending? ? 'desc' : 'asc') 41 | elsif default_scope == scope 42 | params.merge!(sorting: scope.to_s, sort_order: default_direction.to_s) 43 | else 44 | params.merge!(sorting: scope.to_s, sort_order: 'asc') 45 | end 46 | 47 | params 48 | end 49 | 50 | def scope_name(scope) 51 | I18n.t( 52 | :"#{query_i18n_key}.#{scope}", 53 | scope: 'query.sorting_scopes', 54 | default: scope.to_s.titleize.capitalize 55 | ) 56 | end 57 | end 58 | end 59 | end 60 | 61 | -------------------------------------------------------------------------------- /lib/admino/query/sorting.rb: -------------------------------------------------------------------------------- 1 | require 'coercible' 2 | require 'active_support/hash_with_indifferent_access' 3 | require 'active_support/core_ext/hash' 4 | 5 | module Admino 6 | module Query 7 | class Sorting 8 | attr_reader :params 9 | attr_reader :config 10 | attr_reader :query_i18n_key 11 | 12 | def initialize(config, params, query_i18n_key = nil) 13 | @config = config 14 | @params = ActiveSupport::HashWithIndifferentAccess.new(params) 15 | end 16 | 17 | def augment_scope(scope) 18 | if active_scope 19 | scope.send(active_scope, ascending? ? :asc : :desc) 20 | else 21 | scope 22 | end 23 | end 24 | 25 | def is_scope_active?(scope) 26 | active_scope == scope.to_sym 27 | end 28 | 29 | def ascending? 30 | if params[:sort_order] == 'desc' 31 | false 32 | elsif params[:sort_order].blank? && active_scope == default_scope 33 | default_direction_is_ascending? 34 | else 35 | true 36 | end 37 | end 38 | 39 | def active_scope 40 | if param_value && scopes.include?(param_value.to_sym) 41 | param_value.to_sym 42 | elsif default_scope 43 | default_scope 44 | else 45 | nil 46 | end 47 | end 48 | 49 | def default_scope 50 | config.default_scope 51 | end 52 | 53 | def default_direction 54 | config.default_direction 55 | end 56 | 57 | def default_direction_is_ascending? 58 | default_direction != :desc 59 | end 60 | 61 | def param_value 62 | params.fetch(param_name, nil) 63 | end 64 | 65 | def param_name 66 | :sorting 67 | end 68 | 69 | def scopes 70 | config.scopes 71 | end 72 | end 73 | end 74 | end 75 | 76 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We love pull requests. Here's a quick guide: 4 | 5 | 1. Fork the repo. 6 | 2. Run the tests. We only take pull requests with passing tests, and it's great 7 | to know that you have a clean slate: `bundle && rake` 8 | 3. Add a test for your change. Only refactoring and documentation changes 9 | require no new tests. If you are adding functionality or fixing a bug, we need 10 | a test! 11 | 4. Make the test pass. 12 | 5. Make sure your changes adhere to the [thoughtbot Style 13 | Guide](https://github.com/thoughtbot/guides/tree/master/style) 14 | 6. Push to your fork and submit a pull request. 15 | 7. At this point you're waiting on us. We like to at least comment on, if not 16 | accept, pull requests within three business days (and, typically, one business 17 | day). [We may suggest some changes or improvements or 18 | alternatives](https://github.com/thoughtbot/guides/tree/master/code-review). 19 | 20 | ## Increase your chances of getting merged 21 | 22 | Some things that will increase the chance that your pull request is accepted, 23 | taken straight from the Ruby on Rails guide: 24 | 25 | * Use Rails idioms and helpers 26 | * Include tests that fail without your code, and pass with it 27 | * Update the documentation: that surrounding your change, examples elsewhere, 28 | guides, and whatever else is affected by your contribution 29 | * Syntax: 30 | * Two spaces, no tabs. 31 | * No trailing whitespace. Blank lines should not have any space. 32 | * Make sure your [source files end with newline 33 | characters](http://stackoverflow.com/questions/729692/why-should-files-end-with-a-newline#answer-729725). 34 | * Prefer `&&`/`||` over `and`/`or`. 35 | * `MyClass.my_method(my_arg)` not `my_method( my_arg )` or 36 | `my_method my_arg`. 37 | * `a = b` and not `a=b`. 38 | * Follow the conventions you see used in the source already. 39 | * And in case we didn't emphasize it enough: *We love tests!* 40 | 41 | -------------------------------------------------------------------------------- /lib/admino/action_view_extension.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/core_ext/hash' 2 | require 'admino/table/presenter' 3 | require 'admino/query/base_presenter' 4 | 5 | module Admino 6 | module ActionViewExtension 7 | module Internals 8 | def self.present_query(query, context, options, key = :presenter) 9 | presenter_klass = options.fetch(key, Admino::Query::BasePresenter) 10 | presenter_klass.new(query, context) 11 | end 12 | end 13 | 14 | def table_for(collection, options = {}, &block) 15 | options.symbolize_keys! 16 | options.assert_valid_keys(:presenter, :class, :query, :html) 17 | presenter_klass = options.fetch(:presenter, Admino::Table::Presenter) 18 | query = if options[:query] 19 | Internals.present_query(options[:query], self, options, :query_presenter) 20 | else 21 | nil 22 | end 23 | presenter = presenter_klass.new(collection, options[:class], query, self) 24 | html_options = options.fetch(:html, {}) 25 | presenter.to_html(html_options, &block) 26 | end 27 | 28 | def filters_for(query, options = {}, &block) 29 | options.symbolize_keys! 30 | options.assert_valid_keys(:presenter) 31 | Internals.present_query(query, self, options).filter_groups.each(&block) 32 | end 33 | 34 | def sortings_for(query, options = {}, &block) 35 | options.symbolize_keys! 36 | options.assert_valid_keys(:presenter) 37 | Internals.present_query(query, self, options).sorting.scopes.each(&block) 38 | end 39 | 40 | def search_form_for(query, options = {}, &block) 41 | options.symbolize_keys! 42 | Internals.present_query(query, self, options.slice(:presenter)). 43 | form(options, &block) 44 | end 45 | 46 | def simple_search_form_for(query, options = {}, &block) 47 | options.symbolize_keys! 48 | Internals.present_query(query, self, options.slice(:presenter)). 49 | simple_form(options, &block) 50 | end 51 | end 52 | end 53 | 54 | -------------------------------------------------------------------------------- /spec/admino/table/row_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Admino 4 | module Table 5 | describe Row do 6 | subject(:row) { Row.new(view) } 7 | let(:view) { double('View Context') } 8 | 9 | it 'takes view context' do 10 | expect(row.view_context).to eq view 11 | end 12 | 13 | it 'aliases view_context to h' do 14 | expect(row.h).to eq view 15 | end 16 | 17 | describe '#parse_column_args' do 18 | subject do 19 | row.parse_column_args(arguments) 20 | end 21 | 22 | context 'with a symbol param' do 23 | let(:arguments) { [:title] } 24 | it { is_expected.to eq [:title, nil, {}] } 25 | end 26 | 27 | context 'with a string param' do 28 | let(:arguments) { ['Title'] } 29 | it { is_expected.to eq [nil, 'Title', {}] } 30 | end 31 | 32 | context 'with a symbol and string param' do 33 | let(:arguments) { [:title, 'Title'] } 34 | it { is_expected.to eq [:title, 'Title', {}] } 35 | end 36 | 37 | context 'with two symbol params' do 38 | let(:arguments) { [:title, :foo] } 39 | it { is_expected.to eq [:title, :foo, {}] } 40 | end 41 | 42 | context 'with options' do 43 | let(:arguments) { [{ foo: 'bar' }] } 44 | it { is_expected.to eq [nil, nil, { foo: 'bar' }] } 45 | end 46 | end 47 | 48 | describe '#parse_action_args' do 49 | subject do 50 | row.parse_action_args(arguments) 51 | end 52 | 53 | context 'with a symbol param' do 54 | let(:arguments) { [:show] } 55 | it { is_expected.to eq [:show, nil, nil, {}] } 56 | end 57 | 58 | context 'with a one string param' do 59 | let(:arguments) { ['/'] } 60 | it { is_expected.to eq [nil, '/', nil, {}] } 61 | end 62 | 63 | context 'with a two string params' do 64 | let(:arguments) { ['/', 'Details'] } 65 | it { is_expected.to eq [nil, '/', 'Details', {}] } 66 | end 67 | 68 | context 'with options' do 69 | let(:arguments) { [{ foo: 'bar' }] } 70 | it { is_expected.to eq [nil, nil, nil, { foo: 'bar' }] } 71 | end 72 | end 73 | end 74 | end 75 | end 76 | 77 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'simplecov' 2 | require 'coveralls' 3 | 4 | SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[ 5 | SimpleCov::Formatter::HTMLFormatter, 6 | Coveralls::SimpleCov::Formatter 7 | ] 8 | 9 | SimpleCov.start do 10 | add_filter 'spec' 11 | end 12 | 13 | require 'admino' 14 | require 'pry' 15 | require 'rspec-html-matchers' 16 | 17 | I18n.enforce_available_locales = true 18 | I18n.available_locales = [:en] 19 | I18n.locale = :en 20 | 21 | class ScopeMock 22 | attr_reader :chain, :name 23 | 24 | def initialize(name = nil, chain = []) 25 | @name = name 26 | @chain = chain 27 | end 28 | 29 | def method_missing(method_name, *args, &block) 30 | ::ScopeMock.new(name, chain + [method_name, args]) 31 | end 32 | end 33 | 34 | class TestQuery < Admino::Query::Base 35 | search_field :foo, default: 'bar' 36 | search_field :starting_from, coerce: :to_date 37 | 38 | filter_by :bar, [:one, :two], 39 | include_empty_scope: true, 40 | default: :two 41 | 42 | sorting :by_title, :by_date, 43 | default_scope: :by_title, 44 | default_direction: :desc 45 | 46 | starting_scope { 'start' } 47 | ending_scope { 'end' } 48 | end 49 | 50 | class Post < Struct.new(:key, :dom_id) 51 | extend ActiveModel::Naming 52 | extend ActiveModel::Translation 53 | 54 | def title 55 | "Post #{key}" 56 | end 57 | 58 | def author_name 59 | "steffoz" 60 | end 61 | 62 | def to_param 63 | key 64 | end 65 | 66 | def to_key 67 | [key] 68 | end 69 | end 70 | 71 | module Rails 72 | def self.env 73 | OpenStruct.new(development?: false) 74 | end 75 | end 76 | 77 | require 'action_view' 78 | require 'simple_form' 79 | 80 | class RailsViewContext < ActionView::Base 81 | include ActionView::Helpers::TagHelper 82 | include SimpleForm::ActionViewExtensions::FormHelper 83 | include Admino::ActionViewExtension 84 | 85 | def request 86 | OpenStruct.new(path: '/', fullpath: '/?p=1') 87 | end 88 | 89 | def polymorphic_path(*args) 90 | '/' 91 | end 92 | end 93 | 94 | RSpec.configure do |c| 95 | c.include RSpecHtmlMatchers 96 | 97 | c.before(:each) do 98 | I18n.backend = I18n::Backend::Simple.new 99 | end 100 | end 101 | 102 | -------------------------------------------------------------------------------- /lib/admino/query/filter_group_presenter.rb: -------------------------------------------------------------------------------- 1 | require 'i18n' 2 | require 'showcase' 3 | require 'showcase/helpers/html_options' 4 | require 'active_support/core_ext/object/deep_dup' 5 | require 'active_support/core_ext/array/extract_options' 6 | require 'active_support/hash_with_indifferent_access' 7 | require 'active_support/core_ext/hash' 8 | 9 | module Admino 10 | module Query 11 | class FilterGroupPresenter < Showcase::Presenter 12 | def scope_link(scope, *args) 13 | options = args.extract_options! 14 | 15 | label = args.first || scope_name(scope) 16 | 17 | active_class = options.delete(:active_class) { 'is-active' } 18 | options = Showcase::Helpers::HtmlOptions.new(options) 19 | 20 | if is_scope_active?(scope) 21 | options.add_class!(active_class) 22 | end 23 | 24 | h.link_to label, scope_path(scope), options.to_h 25 | end 26 | 27 | def scopes 28 | @scopes ||= super.map do |scope| 29 | ScopePresenter.new(scope, self, view_context) 30 | end 31 | end 32 | 33 | def each_scope(&block) 34 | scopes.each(&block) 35 | end 36 | 37 | def scope_path(scope) 38 | h.request.path + "?" + scope_params(scope).to_query 39 | end 40 | 41 | def scope_params(scope) 42 | params = ActiveSupport::HashWithIndifferentAccess.new( 43 | h.request.query_parameters.deep_dup 44 | ) 45 | 46 | params[:query] ||= {} 47 | 48 | if is_scope_active?(scope) 49 | params[:query].delete(param_name) 50 | else 51 | params[:query].merge!(param_name => scope.to_s) 52 | end 53 | 54 | if params[:query].empty? 55 | params.delete(:query) 56 | end 57 | 58 | params 59 | end 60 | 61 | def scope_name(scope) 62 | I18n.t( 63 | :"#{query_i18n_key}.#{i18n_key}.scopes.#{scope}", 64 | scope: 'query.filter_groups', 65 | default: [ 66 | :"#{i18n_key}.scopes.#{scope}", 67 | scope.to_s.titleize 68 | ] 69 | ) 70 | end 71 | 72 | def name 73 | I18n.t( 74 | :"#{query_i18n_key}.#{i18n_key}.name", 75 | scope: 'query.filter_groups', 76 | default: [ 77 | :"#{i18n_key}.name", 78 | i18n_key.to_s.titleize.capitalize 79 | ] 80 | ) 81 | end 82 | end 83 | end 84 | end 85 | 86 | -------------------------------------------------------------------------------- /spec/admino/query/search_field_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Admino 4 | module Query 5 | describe SearchField do 6 | subject(:search_field) { SearchField.new(config, params) } 7 | let(:config) { Configuration::SearchField.new(:foo) } 8 | let(:params) { {} } 9 | 10 | describe '#value' do 11 | context 'with a value' do 12 | let(:params) { { 'query' => { 'foo' => 'bar' } } } 13 | 14 | it 'returns the param value for the search_field' do 15 | expect(search_field.value).to eq 'bar' 16 | end 17 | end 18 | 19 | context 'else' do 20 | it 'returns nil' do 21 | expect(search_field.value).to be_nil 22 | end 23 | 24 | context 'with a default value' do 25 | let(:config) { 26 | Configuration::SearchField.new(:foo, default: 'foo') 27 | } 28 | 29 | it 'returns it' do 30 | expect(search_field.value).to eq 'foo' 31 | end 32 | end 33 | end 34 | 35 | context 'with coertion' do 36 | let(:config) { 37 | Configuration::SearchField.new(:foo, coerce: :to_date) 38 | } 39 | 40 | context 'with a possible coertion' do 41 | let(:params) { { 'query' => { 'foo' => '2014-10-05' } } } 42 | 43 | it 'returns the coerced param value for the search_field' do 44 | expect(search_field.value).to be_a Date 45 | end 46 | end 47 | 48 | context 'with a possible coertion' do 49 | let(:params) { { 'query' => { 'foo' => '' } } } 50 | 51 | it 'returns nil' do 52 | expect(search_field.value).to be_nil 53 | end 54 | end 55 | end 56 | end 57 | 58 | describe '#augment_scope' do 59 | let(:result) { search_field.augment_scope(scope) } 60 | let(:scope) { ScopeMock.new('original') } 61 | 62 | context 'if the search_field has a value' do 63 | let(:params) { { 'query' => { 'foo' => 'bar' } } } 64 | 65 | it 'returns the original scope chained with the search_field scope' do 66 | expect(result.chain).to eq [:foo, ['bar']] 67 | end 68 | end 69 | 70 | context 'else' do 71 | it 'returns the original scope' do 72 | expect(result).to eq scope 73 | end 74 | end 75 | end 76 | end 77 | end 78 | end 79 | 80 | -------------------------------------------------------------------------------- /lib/admino/table/head_row.rb: -------------------------------------------------------------------------------- 1 | require 'admino/table/row' 2 | require 'showcase/helpers/html_options' 3 | 4 | module Admino 5 | module Table 6 | class HeadRow < Row 7 | attr_reader :resource_klass 8 | attr_reader :query 9 | 10 | def initialize(resource_klass, query, view_context) 11 | @resource_klass = resource_klass 12 | @query = query 13 | @columns = "" 14 | 15 | super(view_context) 16 | end 17 | 18 | def actions(*args, &block) 19 | default_options = column_html_options(:actions) 20 | label = I18n.t( 21 | :"#{resource_klass.model_name.i18n_key}.title", 22 | scope: 'table.actions', 23 | default: [ 24 | :title, 25 | 'Actions' 26 | ] 27 | ) 28 | 29 | @columns << h.content_tag(:th, label.to_s, default_options) 30 | end 31 | 32 | def column(*args, &block) 33 | attribute_name, label, html_options = parse_column_args(args) 34 | 35 | if label.nil? 36 | label = column_label(attribute_name) 37 | elsif label.is_a? Symbol 38 | label = column_label(label) 39 | end 40 | 41 | html_options = complete_column_html_options( 42 | attribute_name, 43 | html_options 44 | ) 45 | 46 | sorting_scope = html_options.delete(:sorting) 47 | sorting_html_options = html_options.delete(:sorting_html_options) { {} } 48 | 49 | if sorting_scope 50 | raise ArgumentError, 'query object is required' unless query 51 | label = query.sorting.scope_link(sorting_scope, label, sorting_html_options) 52 | end 53 | 54 | @columns << h.content_tag(:th, label.to_s, html_options) 55 | end 56 | 57 | def to_html 58 | @columns.html_safe 59 | end 60 | 61 | private 62 | 63 | def column_label(attribute_name) 64 | if attribute_name 65 | resource_klass.human_attribute_name(attribute_name.to_s) 66 | end 67 | end 68 | 69 | def complete_column_html_options(attribute_name, final_html_options) 70 | if attribute_name.nil? 71 | return final_html_options 72 | end 73 | 74 | default_options = column_html_options(attribute_name) 75 | html_options = Showcase::Helpers::HtmlOptions.new(default_options) 76 | html_options.merge_attrs!(final_html_options) 77 | html_options = html_options.to_h 78 | end 79 | 80 | def column_html_options(attribute_name) 81 | { role: attribute_name.to_s.gsub(/_/, '-') } 82 | end 83 | end 84 | end 85 | end 86 | 87 | -------------------------------------------------------------------------------- /spec/admino/query/base_presenter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Admino 4 | module Query 5 | describe BasePresenter do 6 | subject(:presenter) { BasePresenter.new(query, view) } 7 | let(:view) { RailsViewContext.new } 8 | let(:query) do 9 | TestQuery.new(query: { foo: 'value' }) 10 | end 11 | let(:request_object) do 12 | double( 13 | 'ActionDispatch::Request', 14 | fullpath: '/?foo=bar' 15 | ) 16 | end 17 | 18 | before do 19 | allow(view).to receive(:request).and_return(request_object) 20 | end 21 | 22 | describe '#form' do 23 | let(:result) do 24 | presenter.form do |form| 25 | form.label(:foo) << 26 | form.text_field(:foo) 27 | end 28 | end 29 | 30 | before do 31 | I18n.backend.store_translations( 32 | :en, 33 | query: { attributes: { test_query: { foo: 'NAME' } } } 34 | ) 35 | end 36 | 37 | it 'renders a form pointing to the current URL' do 38 | expect(result).to have_tag(:form, with: { action: '/?foo=bar' }) 39 | end 40 | 41 | it 'renders a form with method GET' do 42 | expect(result).to have_tag(:form, with: { method: 'get' }) 43 | end 44 | 45 | it 'renders inputs with a query[] name prefix' do 46 | expect(result).to have_tag(:input, with: { type: 'text', name: 'query[foo]' }) 47 | end 48 | 49 | it 'prefills inputs with query value' do 50 | expect(result).to have_tag(:input, with: { type: 'text', value: 'value' }) 51 | end 52 | 53 | it 'it generates labels in the :query I18n space' do 54 | expect(result).to have_tag(:label, text: 'NAME') 55 | end 56 | end 57 | 58 | describe '#simple_form' do 59 | let(:result) do 60 | presenter.simple_form do |form| 61 | form.input(:foo) 62 | end 63 | end 64 | 65 | before do 66 | I18n.backend.store_translations( 67 | :en, 68 | query: { attributes: { test_query: { foo: 'NAME' } } } 69 | ) 70 | end 71 | 72 | it 'renders a form pointing to the current URL' do 73 | expect(result).to have_tag(:form, with: { action: '/?foo=bar' }) 74 | end 75 | 76 | it 'renders a form with method GET' do 77 | expect(result).to have_tag(:form, with: { method: 'get' }) 78 | end 79 | 80 | it 'renders inputs with a query[] name prefix' do 81 | expect(result).to have_tag(:input, with: { type: 'text', name: 'query[foo]' }) 82 | end 83 | 84 | it 'prefills inputs with query value' do 85 | expect(result).to have_tag(:input, with: { type: 'text', value: 'value' }) 86 | end 87 | 88 | it 'it generates labels in the :query I18n space' do 89 | expect(result).to have_tag(:label, content: 'NAME') 90 | end 91 | end 92 | end 93 | end 94 | end 95 | 96 | -------------------------------------------------------------------------------- /lib/admino/query/configuration.rb: -------------------------------------------------------------------------------- 1 | module Admino 2 | module Query 3 | class Configuration 4 | class SearchField 5 | attr_reader :name 6 | attr_reader :options 7 | 8 | def initialize(name, options = {}) 9 | options.symbolize_keys! 10 | options.assert_valid_keys( 11 | :coerce, 12 | :default 13 | ) 14 | 15 | @name = name.to_sym 16 | @options = options 17 | end 18 | 19 | def default_value 20 | options[:default] 21 | end 22 | 23 | def coerce_to 24 | if options[:coerce] 25 | options[:coerce].to_sym 26 | end 27 | end 28 | end 29 | 30 | class FilterGroup 31 | attr_reader :name 32 | attr_reader :scopes 33 | attr_reader :options 34 | 35 | def initialize(name, scopes, options = {}) 36 | options.symbolize_keys! 37 | options.assert_valid_keys( 38 | :include_empty_scope, 39 | :default 40 | ) 41 | 42 | @name = name.to_sym 43 | @scopes = scopes.map(&:to_sym) 44 | @options = options 45 | end 46 | 47 | def include_empty_scope? 48 | @options.fetch(:include_empty_scope) { false } 49 | end 50 | 51 | def default_scope 52 | if options[:default] 53 | options[:default].to_sym 54 | end 55 | end 56 | end 57 | 58 | class Sorting 59 | attr_reader :scopes 60 | attr_reader :default_scope 61 | attr_reader :default_direction 62 | 63 | def initialize(scopes, options = {}) 64 | options.symbolize_keys! 65 | options.assert_valid_keys(:default_scope, :default_direction) 66 | 67 | @scopes = scopes.map(&:to_sym) 68 | @default_scope = if options[:default_scope] 69 | options[:default_scope].to_sym 70 | end 71 | 72 | @default_direction = if options[:default_direction] 73 | options[:default_direction].to_sym 74 | end 75 | end 76 | end 77 | 78 | attr_reader :search_fields 79 | attr_reader :filter_groups 80 | attr_reader :sorting 81 | attr_accessor :starting_scope_callable 82 | attr_accessor :ending_scope_callable 83 | 84 | def initialize 85 | @search_fields = [] 86 | @filter_groups = [] 87 | end 88 | 89 | def add_search_field(name, options = {}) 90 | SearchField.new(name, options).tap do |search_field| 91 | self.search_fields << search_field 92 | end 93 | end 94 | 95 | def add_filter_group(name, scopes, options = {}) 96 | FilterGroup.new(name, scopes, options).tap do |filter_group| 97 | self.filter_groups << filter_group 98 | end 99 | end 100 | 101 | def add_sorting_scopes(scopes, options = {}) 102 | @sorting = Sorting.new(scopes, options) 103 | end 104 | end 105 | end 106 | end 107 | 108 | -------------------------------------------------------------------------------- /lib/admino/table/presenter.rb: -------------------------------------------------------------------------------- 1 | require 'showcase' 2 | require 'admino/table/head_row' 3 | require 'admino/table/resource_row' 4 | 5 | module Admino 6 | module Table 7 | class Presenter < Showcase::Presenter 8 | attr_reader :collection_klass 9 | attr_reader :query 10 | 11 | def self.tag_helper(name, tag, options = {}) 12 | options_method = :"#{name}_html_options" 13 | 14 | define_method :"#{name}_tag" do |*args, &block| 15 | options = args.extract_options! 16 | if respond_to?(options_method, true) 17 | default_options = send(options_method, *args) 18 | html_options = Showcase::Helpers::HtmlOptions.new(default_options) 19 | html_options.merge_attrs!(options) 20 | options = html_options.to_h 21 | end 22 | h.content_tag(tag, options, &block) 23 | end 24 | end 25 | 26 | tag_helper :table, :table 27 | tag_helper :thead, :thead 28 | tag_helper :thead_tr, :tr 29 | tag_helper :tbody, :tbody 30 | tag_helper :tbody_tr, :tr, params: %w(resource index) 31 | 32 | def initialize(*args) 33 | context = args.pop 34 | collection = args.shift 35 | 36 | @collection_klass = args.shift 37 | 38 | if !@collection_klass && !collection.empty? 39 | @collection_klass = collection.first.class 40 | end 41 | 42 | @query = args.shift 43 | 44 | super(collection, context) 45 | end 46 | 47 | def to_html(options = {}, &block) 48 | 49 | if @collection_klass.nil? 50 | raise ArgumentError, 'collection is empty and no explicit class is specified' 51 | end 52 | 53 | table_tag(options) do 54 | thead_tag do 55 | thead_tr_tag do 56 | th_tags(&block) 57 | end 58 | end << 59 | tbody_tag do 60 | tbody_tr_tags(&block) 61 | end 62 | end 63 | end 64 | 65 | private 66 | 67 | def tbody_tr_tags(&block) 68 | collection.each_with_index.map do |resource, index| 69 | html_options = base_tbody_tr_html_options(resource, index) 70 | tbody_tr_tag(resource, index, html_options) do 71 | td_tags(resource, &block) 72 | end 73 | end.join.html_safe 74 | end 75 | 76 | def th_tags(&block) 77 | row = head_row(collection_klass, query, view_context) 78 | if block_given? 79 | h.capture(row, nil, &block) 80 | end 81 | row.to_html 82 | end 83 | 84 | def td_tags(resource, &block) 85 | row = resource_row(resource, view_context) 86 | if block_given? 87 | h.capture(row, resource, &block) 88 | end 89 | row.to_html 90 | end 91 | 92 | def collection 93 | object 94 | end 95 | 96 | def head_row(collection_klass, query, view_context) 97 | HeadRow.new(collection_klass, query, view_context) 98 | end 99 | 100 | def resource_row(resource, view_context) 101 | ResourceRow.new(resource, view_context) 102 | end 103 | 104 | def base_tbody_tr_html_options(resource, index) 105 | options = { 106 | class: zebra_css_classes[index % zebra_css_classes.size] 107 | } 108 | 109 | if resource.respond_to?(:dom_id) 110 | options[:id] = resource.dom_id 111 | end 112 | 113 | options 114 | end 115 | 116 | def zebra_css_classes 117 | %w(is-even is-odd) 118 | end 119 | end 120 | end 121 | end 122 | 123 | -------------------------------------------------------------------------------- /lib/admino/query/base.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/concern' 2 | require 'active_model/naming' 3 | require 'active_model/translation' 4 | require 'active_model/conversion' 5 | require 'active_support/hash_with_indifferent_access' 6 | require 'active_support/core_ext/hash' 7 | 8 | require 'admino/query/dsl' 9 | require 'admino/query/builder' 10 | 11 | module Admino 12 | module Query 13 | class Base 14 | extend ActiveModel::Naming 15 | extend ActiveModel::Translation 16 | include ActiveModel::Conversion 17 | extend Dsl 18 | 19 | attr_reader :params 20 | attr_reader :context 21 | attr_reader :filter_groups 22 | attr_reader :search_fields 23 | attr_reader :sorting 24 | 25 | def self.i18n_scope 26 | :query 27 | end 28 | 29 | def initialize(params = nil, context = {}, config = nil) 30 | @params = 31 | if params.respond_to?(:to_unsafe_h) 32 | ActiveSupport::HashWithIndifferentAccess.new(params.to_unsafe_h) 33 | else 34 | ActiveSupport::HashWithIndifferentAccess.new(params) 35 | end 36 | @config = config 37 | @context = context 38 | 39 | init_filter_groups 40 | init_search_fields 41 | init_sorting 42 | end 43 | 44 | def scope(starting_scope = nil) 45 | starting_scope ||= if config.starting_scope_callable 46 | config.starting_scope_callable.call(self) 47 | else 48 | raise ArgumentError, 'no starting scope provided' 49 | end 50 | 51 | scope = augment_scope(Builder.new(self, starting_scope)).scope 52 | 53 | if config.ending_scope_callable 54 | scope.instance_exec(self, &config.ending_scope_callable) 55 | else 56 | scope 57 | end 58 | end 59 | 60 | def persisted? 61 | false 62 | end 63 | 64 | def config 65 | @config || self.class.config 66 | end 67 | 68 | def filter_groups 69 | @filter_groups.values 70 | end 71 | 72 | def filter_group_by_name(name) 73 | @filter_groups[name] 74 | end 75 | 76 | def search_fields 77 | @search_fields.values 78 | end 79 | 80 | def search_field_by_name(name) 81 | @search_fields[name] 82 | end 83 | 84 | private 85 | 86 | def augment_scope(query_builder) 87 | scope_augmenters.each do |augmenter| 88 | query_builder = augmenter.augment_scope(query_builder) 89 | end 90 | 91 | query_builder 92 | end 93 | 94 | def scope_augmenters 95 | scope_augmenters = search_fields + filter_groups 96 | scope_augmenters << sorting if sorting 97 | scope_augmenters 98 | end 99 | 100 | def init_filter_groups 101 | @filter_groups = {} 102 | i18n_key = self.class.model_name.i18n_key 103 | config.filter_groups.each do |config| 104 | @filter_groups[config.name] = FilterGroup.new(config, params, i18n_key) 105 | end 106 | end 107 | 108 | def init_search_fields 109 | @search_fields = {} 110 | config.search_fields.each do |config| 111 | @search_fields[config.name] = SearchField.new(config, params) 112 | end 113 | end 114 | 115 | def init_sorting 116 | if config.sorting 117 | i18n_key = self.class.model_name.i18n_key 118 | @sorting = Sorting.new(config.sorting, params, i18n_key) 119 | end 120 | end 121 | end 122 | end 123 | end 124 | -------------------------------------------------------------------------------- /spec/admino/integration_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Action View integration' do 4 | let(:context) { RailsViewContext.new } 5 | 6 | describe '#simple_table_for' do 7 | let(:collection) { [ first_post, second_post ] } 8 | let(:first_post) { Post.new('1') } 9 | let(:second_post) { Post.new('2') } 10 | let(:params) { 11 | { 12 | sorting: 'by_title', 13 | sort_order: 'desc' 14 | } 15 | } 16 | let(:query) { TestQuery.new(params) } 17 | 18 | it 'produces HTML' do 19 | result = context.table_for(collection, query: query) do |table, record| 20 | table.column :title, sorting: :by_title 21 | table.actions do 22 | table.action :show, '/foo', 'Show' 23 | end 24 | end 25 | 26 | expect(result).to have_tag(:table) do 27 | with_tag 'thead th:first-child', with: { role: 'title' } 28 | with_tag 'thead th:last-child', with: { role: 'actions' } 29 | 30 | with_tag 'th:first-child a', with: { class: 'is-desc', href: '/?sort_order=asc&sorting=by_title' }, text: 'Title' 31 | with_tag 'th:last-child', text: 'Actions' 32 | 33 | with_tag 'tbody tr:first-child', with: { class: 'is-even' } 34 | with_tag 'tbody tr:last-child', with: { class: 'is-odd' } 35 | 36 | with_tag 'tbody tr:first-child td:first-child', with: { role: 'title' }, text: 'Post 1' 37 | with_tag 'tbody tr:first-child td:last-child', with: { role: 'actions' } 38 | 39 | with_tag 'tbody tr:first-child td:last-child a', with: { role: 'show', href: '/foo' } 40 | end 41 | end 42 | end 43 | 44 | describe '#filters_for' do 45 | let(:params) { 46 | { 47 | query: { bar: 'one' } 48 | } 49 | } 50 | let(:query) { TestQuery.new(params) } 51 | 52 | it 'produces HTML' do 53 | result = "" 54 | context.filters_for(query) do |group| 55 | result << "#{group.name}: " 56 | group.each_scope do |scope| 57 | result << scope.link 58 | end 59 | end 60 | 61 | expect(result).to have_tag(:a, with: { href: '/?query%5Bbar%5D=empty' }) do 62 | with_text 'Empty' 63 | end 64 | 65 | expect(result).to have_tag(:a, with: { class: 'is-active', href: '/?' }) do 66 | with_text 'One' 67 | end 68 | 69 | expect(result).to have_tag(:a, with: { href: '/?query%5Bbar%5D=two' }) do 70 | with_text 'Two' 71 | end 72 | end 73 | end 74 | 75 | describe '#sortings_for' do 76 | let(:params) { 77 | { 78 | sorting: 'by_title', 79 | sort_order: 'desc' 80 | } 81 | } 82 | let(:query) { TestQuery.new(params) } 83 | 84 | it 'produces HTML' do 85 | result = "" 86 | context.sortings_for(query) do |scope| 87 | result << scope.link 88 | end 89 | 90 | expect(result).to have_tag(:a, with: { class: 'is-desc', href: '/?sort_order=asc&sorting=by_title' }) do 91 | with_text 'By title' 92 | end 93 | 94 | expect(result).to have_tag(:a, with: { href: '/?sort_order=asc&sorting=by_date' }) do 95 | with_text 'By date' 96 | end 97 | end 98 | end 99 | 100 | describe '#search_form_for' do 101 | let(:params) { 102 | { 103 | query: { foo: 'test' } 104 | } 105 | } 106 | let(:query) { TestQuery.new(params) } 107 | 108 | it 'produces HTML' do 109 | result = context.search_form_for(query) do |f| 110 | f.text_field :foo 111 | end 112 | 113 | expect(result).to have_tag(:form, with: { action: '/?p=1', method: 'get' }) do 114 | with_tag 'input', with: { type: 'text', value: 'test', name: 'query[foo]' } 115 | end 116 | end 117 | end 118 | 119 | describe '#simple_search_form_for' do 120 | let(:params) { 121 | { 122 | query: { foo: 'test' } 123 | } 124 | } 125 | let(:query) { TestQuery.new(params) } 126 | 127 | it 'produces HTML' do 128 | result = context.simple_search_form_for(query) do |f| 129 | f.input :foo 130 | end 131 | 132 | expect(result).to have_tag(:form, with: { action: '/?p=1', method: 'get' }) do 133 | with_tag 'input', with: { type: 'text', value: 'test', name: 'query[foo]' } 134 | end 135 | end 136 | end 137 | end 138 | 139 | -------------------------------------------------------------------------------- /spec/admino/query/filter_group_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Admino 4 | module Query 5 | describe FilterGroup do 6 | subject(:filter_group) { FilterGroup.new(config, params) } 7 | let(:config) do 8 | Configuration::FilterGroup.new(:foo, [:bar, :other], options) 9 | end 10 | let(:options) { {} } 11 | let(:params) { {} } 12 | 13 | describe '#active_scope' do 14 | context 'with no param' do 15 | let(:params) { {} } 16 | 17 | it 'returns nil' do 18 | expect(filter_group.active_scope).to be_nil 19 | end 20 | 21 | context 'if include_empty_scope is true' do 22 | let(:options) { { include_empty_scope: true } } 23 | 24 | it 'returns the :empty scope' do 25 | expect(filter_group.active_scope).to eq :empty 26 | end 27 | 28 | context 'if default scope is set' do 29 | let(:config) do 30 | Configuration::FilterGroup.new( 31 | :foo, 32 | [:bar], 33 | include_empty_scope: true, 34 | default: :bar 35 | ) 36 | end 37 | 38 | it 'returns it' do 39 | expect(filter_group.active_scope).to eq :bar 40 | end 41 | end 42 | end 43 | end 44 | 45 | context 'with "empty" param' do 46 | let(:params) { { 'query' => { 'foo' => 'empty' } } } 47 | 48 | context 'if include_empty_scope is true' do 49 | let(:options) { { include_empty_scope: true, default: :bar } } 50 | 51 | it 'returns the :empty scope' do 52 | expect(filter_group.active_scope).to eq :empty 53 | end 54 | end 55 | end 56 | 57 | context 'with an invalid value' do 58 | let(:params) { { 'query' => { 'foo' => 'qux' } } } 59 | 60 | it 'returns nil' do 61 | expect(filter_group.active_scope).to be_nil 62 | end 63 | 64 | context 'if include_empty_scope is true' do 65 | let(:options) { { include_empty_scope: true, default: :bar } } 66 | 67 | it 'returns nil' do 68 | expect(filter_group.active_scope).to be_nil 69 | end 70 | end 71 | end 72 | 73 | context 'with a valid value' do 74 | let(:params) { { 'query' => { 'foo' => 'bar' } } } 75 | 76 | it 'returns the scope name' do 77 | expect(filter_group.active_scope).to eq :bar 78 | end 79 | end 80 | end 81 | 82 | describe '#augment_scope' do 83 | let(:result) { filter_group.augment_scope(scope) } 84 | let(:scope) { ScopeMock.new('original') } 85 | 86 | context 'if the search_field has a value' do 87 | let(:params) { { 'query' => { 'foo' => 'bar' } } } 88 | 89 | it 'returns the original scope chained with the filter_group scope' do 90 | expect(result.chain).to eq [:bar, []] 91 | end 92 | end 93 | 94 | context 'else' do 95 | it 'returns the original scope' do 96 | expect(result).to eq scope 97 | end 98 | 99 | context 'if include_empty_scope is true' do 100 | let(:options) { { include_empty_scope: true } } 101 | 102 | it 'returns the original scope' do 103 | expect(result).to eq scope 104 | end 105 | end 106 | end 107 | end 108 | 109 | describe '#is_scope_active?' do 110 | let(:params) { { 'query' => { 'foo' => 'bar' } } } 111 | 112 | it 'returns true if the provided scope is the one currently active' do 113 | expect(filter_group.is_scope_active?('bar')).to be_truthy 114 | end 115 | end 116 | 117 | describe '#scopes' do 118 | subject { filter_group.scopes } 119 | 120 | context 'if include_empty_scope is true' do 121 | let(:options) { { include_empty_scope: true } } 122 | 123 | it { is_expected.to eq [:empty, :bar, :other] } 124 | end 125 | 126 | context 'else' do 127 | it { is_expected.to eq [:bar, :other] } 128 | end 129 | end 130 | end 131 | end 132 | end 133 | 134 | -------------------------------------------------------------------------------- /lib/admino/table/resource_row.rb: -------------------------------------------------------------------------------- 1 | require 'admino/table/row' 2 | require 'showcase/helpers/html_options' 3 | 4 | module Admino 5 | module Table 6 | class ResourceRow < Row 7 | attr_reader :resource 8 | 9 | def initialize(resource, view_context) 10 | @resource = resource 11 | @columns = "" 12 | @actions = [] 13 | 14 | super(view_context) 15 | end 16 | 17 | def column(*args, &block) 18 | attribute_name, label, html_options = parse_column_args(args) 19 | 20 | html_options = complete_column_html_options( 21 | attribute_name, 22 | html_options 23 | ) 24 | 25 | if block_given? 26 | content = h.capture(resource, &block) 27 | elsif attribute_name.present? 28 | content = resource.send(attribute_name) 29 | else 30 | raise ArgumentError, 'attribute name or block required' 31 | end 32 | 33 | @columns << h.content_tag(:td, content, html_options) 34 | end 35 | 36 | def actions(*actions, &block) 37 | if block_given? 38 | h.capture(&block) 39 | else 40 | actions.each do |action| 41 | action(action) 42 | end 43 | end 44 | end 45 | 46 | def action(*args, &block) 47 | if block_given? 48 | @actions << h.capture(resource, &block) 49 | else 50 | action_name, url, label, html_options = parse_action_args(args) 51 | 52 | label ||= action_label(action_name) 53 | url ||= action_url(action_name) 54 | html_options = complete_action_html_options( 55 | action_name, 56 | html_options 57 | ) 58 | 59 | @actions << h.link_to(label, url, html_options) 60 | end 61 | end 62 | 63 | def to_html 64 | buffer = @columns 65 | 66 | if @actions.any? 67 | html_options = column_html_options(:actions) 68 | buffer << h.content_tag(:td, html_options) do 69 | @actions.join(" ").html_safe 70 | end 71 | end 72 | 73 | buffer.html_safe 74 | end 75 | 76 | private 77 | 78 | def action_url(action_name) 79 | if action_name.nil? 80 | raise ArgumentError, 81 | 'no URL provided, action name required' 82 | end 83 | 84 | action_url_method = "#{action_name}_action_url" 85 | 86 | if !respond_to?(action_url_method, true) 87 | raise ArgumentError, 88 | "no URL provided, ##{action_url_method} method required" 89 | end 90 | 91 | url = send(action_url_method) 92 | end 93 | 94 | def complete_action_html_options(action_name, final_html_options) 95 | if action_name 96 | default_options = action_html_options(action_name) 97 | html_options = Showcase::Helpers::HtmlOptions.new(default_options) 98 | 99 | action_html_options_method = "#{action_name}_action_html_options" 100 | if respond_to?(action_html_options_method, true) 101 | html_options.merge_attrs!(send(action_html_options_method)) 102 | end 103 | 104 | html_options.merge_attrs!(final_html_options) 105 | html_options.to_h 106 | else 107 | final_html_options 108 | end 109 | end 110 | 111 | def complete_column_html_options(attribute_name, final_html_options) 112 | if attribute_name 113 | default_options = column_html_options(attribute_name) 114 | html_options = Showcase::Helpers::HtmlOptions.new(default_options) 115 | html_options.merge_attrs!(final_html_options) 116 | html_options.to_h 117 | else 118 | final_html_options 119 | end 120 | end 121 | 122 | def action_label(action_name) 123 | return nil unless action_name 124 | 125 | I18n.t( 126 | :"#{resource.class.model_name.i18n_key}.#{action_name}", 127 | scope: 'table.actions', 128 | default: [ 129 | :"#{action_name}", 130 | action_name.to_s.titleize 131 | ] 132 | ) 133 | end 134 | 135 | def action_html_options(action_name) 136 | { role: action_name.to_s.gsub(/_/, '-') } 137 | end 138 | 139 | def column_html_options(attribute_name) 140 | { role: attribute_name.to_s.gsub(/_/, '-') } 141 | end 142 | end 143 | end 144 | end 145 | 146 | -------------------------------------------------------------------------------- /spec/admino/query/sorting_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Admino 4 | module Query 5 | describe Sorting do 6 | subject(:sorting) { Sorting.new(config, params) } 7 | let(:config) do 8 | Configuration::Sorting.new(sorting_scopes, options) 9 | end 10 | let(:sorting_scopes) { [:by_title, :by_date] } 11 | let(:options) { {} } 12 | let(:params) { {} } 13 | 14 | describe '#active_scope' do 15 | context 'with no param' do 16 | let(:params) { {} } 17 | 18 | it 'returns false' do 19 | expect(sorting.active_scope).to be_nil 20 | end 21 | 22 | context 'if a default scope is configured' do 23 | let(:options) { { default_scope: :by_date } } 24 | 25 | it 'returns it' do 26 | expect(sorting.active_scope).to eq :by_date 27 | end 28 | end 29 | end 30 | 31 | context 'with an invalid value' do 32 | let(:params) { { 'sorting' => 'foo' } } 33 | 34 | it 'returns false' do 35 | expect(sorting.active_scope).to be_nil 36 | end 37 | end 38 | 39 | context 'with a valid value' do 40 | let(:params) { { 'sorting' => 'by_title' } } 41 | 42 | it 'returns the scope name' do 43 | expect(sorting.active_scope).to eq :by_title 44 | end 45 | end 46 | end 47 | 48 | describe '#ascending?' do 49 | context 'with no param' do 50 | let(:params) { {} } 51 | 52 | it 'returns true' do 53 | expect(sorting).to be_ascending 54 | end 55 | end 56 | 57 | context 'with invalid value' do 58 | let(:params) { { 'sort_order' => 'foo' } } 59 | 60 | it 'returns true' do 61 | expect(sorting).to be_ascending 62 | end 63 | end 64 | 65 | context 'with "asc" value' do 66 | let(:params) { { 'sort_order' => 'asc' } } 67 | 68 | it 'returns nil' do 69 | expect(sorting).to be_ascending 70 | end 71 | end 72 | 73 | context 'with "desc" value' do 74 | let(:params) { { 'sort_order' => 'desc' } } 75 | 76 | it 'returns the param value for the search_field' do 77 | expect(sorting).not_to be_ascending 78 | end 79 | end 80 | 81 | context 'if a default scope and direction are set and default scope is current' do 82 | let(:options) { { default_scope: :by_date, default_direction: :desc } } 83 | let(:params) { { 'sorting' => 'by_date', 'sort_order' => 'desc' } } 84 | 85 | it 'returns it' do 86 | expect(sorting).not_to be_ascending 87 | end 88 | end 89 | end 90 | 91 | describe '#augment_scope' do 92 | let(:result) { sorting.augment_scope(scope) } 93 | let(:scope) { ScopeMock.new('original') } 94 | 95 | context 'if the search_field has a value' do 96 | let(:params) { { 'sorting' => 'by_title', 'sort_order' => 'desc' } } 97 | 98 | it 'returns the original scope chained with the current scope' do 99 | expect(result.chain).to eq [:by_title, [:desc]] 100 | end 101 | end 102 | 103 | context 'else' do 104 | it 'returns the original scope' do 105 | expect(result).to eq scope 106 | end 107 | end 108 | 109 | context 'if a default scope is configured' do 110 | let(:options) { { default_scope: :by_date } } 111 | let(:params) { {} } 112 | 113 | it 'returns the original scope chained with the default scope' do 114 | expect(result.chain).to eq [:by_date, [:asc]] 115 | end 116 | 117 | context 'if a default direction is configured' do 118 | let(:options) { { default_scope: :by_date, default_direction: :desc } } 119 | 120 | it 'returns the original scope chained with the default scope and default direction' do 121 | expect(result.chain).to eq [:by_date, [:desc]] 122 | end 123 | end 124 | end 125 | end 126 | 127 | describe '#is_scope_active?' do 128 | let(:params) { { 'sorting' => 'by_date' } } 129 | 130 | it 'returns true if the provided scope is the one currently active' do 131 | expect(sorting.is_scope_active?(:by_date)).to be_truthy 132 | end 133 | end 134 | end 135 | end 136 | end 137 | 138 | -------------------------------------------------------------------------------- /gemfiles/rails_40.gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: ../ 3 | specs: 4 | admino (0.0.16) 5 | activemodel 6 | activesupport 7 | coercible 8 | showcase (~> 0.2.5) 9 | 10 | GEM 11 | remote: https://rubygems.org/ 12 | specs: 13 | actionmailer (4.0.0) 14 | actionpack (= 4.0.0) 15 | mail (~> 2.5.3) 16 | actionpack (4.0.0) 17 | activesupport (= 4.0.0) 18 | builder (~> 3.1.0) 19 | erubis (~> 2.7.0) 20 | rack (~> 1.5.2) 21 | rack-test (~> 0.6.2) 22 | activemodel (4.0.0) 23 | activesupport (= 4.0.0) 24 | builder (~> 3.1.0) 25 | activerecord (4.0.0) 26 | activemodel (= 4.0.0) 27 | activerecord-deprecated_finders (~> 1.0.2) 28 | activesupport (= 4.0.0) 29 | arel (~> 4.0.0) 30 | activerecord-deprecated_finders (1.0.4) 31 | activesupport (4.0.0) 32 | i18n (~> 0.6, >= 0.6.4) 33 | minitest (~> 4.2) 34 | multi_json (~> 1.3) 35 | thread_safe (~> 0.1) 36 | tzinfo (~> 0.3.37) 37 | appraisal (2.0.1) 38 | activesupport (>= 3.2.21) 39 | bundler 40 | rake 41 | thor (>= 0.14.0) 42 | arel (4.0.2) 43 | builder (3.1.4) 44 | coderay (1.1.0) 45 | coercible (1.0.0) 46 | descendants_tracker (~> 0.0.1) 47 | coveralls (0.8.1) 48 | json (~> 1.8) 49 | rest-client (>= 1.6.8, < 2) 50 | simplecov (~> 0.10.0) 51 | term-ansicolor (~> 1.3) 52 | thor (~> 0.19.1) 53 | descendants_tracker (0.0.4) 54 | thread_safe (~> 0.3, >= 0.3.1) 55 | diff-lcs (1.2.5) 56 | docile (1.1.5) 57 | domain_name (0.5.24) 58 | unf (>= 0.0.5, < 1.0.0) 59 | erubis (2.7.0) 60 | hike (1.2.3) 61 | http-cookie (1.0.2) 62 | domain_name (~> 0.5) 63 | i18n (0.7.0) 64 | json (1.8.2) 65 | mail (2.5.4) 66 | mime-types (~> 1.16) 67 | treetop (~> 1.4.8) 68 | method_source (0.8.2) 69 | mime-types (1.25.1) 70 | mini_portile (0.6.2) 71 | minitest (4.7.5) 72 | multi_json (1.11.0) 73 | netrc (0.10.3) 74 | nokogiri (1.6.6.2) 75 | mini_portile (~> 0.6.0) 76 | polyglot (0.3.5) 77 | pry (0.10.1) 78 | coderay (~> 1.1.0) 79 | method_source (~> 0.8.1) 80 | slop (~> 3.4) 81 | rack (1.5.2) 82 | rack-test (0.6.3) 83 | rack (>= 1.0) 84 | rails (4.0.0) 85 | actionmailer (= 4.0.0) 86 | actionpack (= 4.0.0) 87 | activerecord (= 4.0.0) 88 | activesupport (= 4.0.0) 89 | bundler (>= 1.3.0, < 2.0) 90 | railties (= 4.0.0) 91 | sprockets-rails (~> 2.0.0) 92 | railties (4.0.0) 93 | actionpack (= 4.0.0) 94 | activesupport (= 4.0.0) 95 | rake (>= 0.8.7) 96 | thor (>= 0.18.1, < 2.0) 97 | rake (10.4.2) 98 | rest-client (1.8.0) 99 | http-cookie (>= 1.0.2, < 2.0) 100 | mime-types (>= 1.16, < 3.0) 101 | netrc (~> 0.7) 102 | rspec (3.2.0) 103 | rspec-core (~> 3.2.0) 104 | rspec-expectations (~> 3.2.0) 105 | rspec-mocks (~> 3.2.0) 106 | rspec-core (3.2.3) 107 | rspec-support (~> 3.2.0) 108 | rspec-expectations (3.2.1) 109 | diff-lcs (>= 1.2.0, < 2.0) 110 | rspec-support (~> 3.2.0) 111 | rspec-html-matchers (0.7.0) 112 | nokogiri (~> 1) 113 | rspec (~> 3) 114 | rspec-mocks (3.2.1) 115 | diff-lcs (>= 1.2.0, < 2.0) 116 | rspec-support (~> 3.2.0) 117 | rspec-support (3.2.2) 118 | showcase (0.2.5) 119 | activesupport 120 | simple_form (3.0.3) 121 | actionpack (~> 4.0) 122 | activemodel (~> 4.0) 123 | simplecov (0.10.0) 124 | docile (~> 1.1.0) 125 | json (~> 1.8) 126 | simplecov-html (~> 0.10.0) 127 | simplecov-html (0.10.0) 128 | slop (3.6.0) 129 | sprockets (2.12.3) 130 | hike (~> 1.2) 131 | multi_json (~> 1.0) 132 | rack (~> 1.0) 133 | tilt (~> 1.1, != 1.3.0) 134 | sprockets-rails (2.0.1) 135 | actionpack (>= 3.0) 136 | activesupport (>= 3.0) 137 | sprockets (~> 2.8) 138 | term-ansicolor (1.3.0) 139 | tins (~> 1.0) 140 | thor (0.19.1) 141 | thread_safe (0.3.5) 142 | tilt (1.4.1) 143 | tins (1.5.1) 144 | treetop (1.4.15) 145 | polyglot 146 | polyglot (>= 0.3.1) 147 | tzinfo (0.3.44) 148 | unf (0.1.4) 149 | unf_ext 150 | unf_ext (0.0.7.1) 151 | 152 | PLATFORMS 153 | ruby 154 | 155 | DEPENDENCIES 156 | actionpack 157 | admino! 158 | appraisal 159 | bundler (~> 1.3) 160 | coveralls 161 | i18n 162 | pry 163 | rails (= 4.0.0) 164 | rake 165 | rspec 166 | rspec-html-matchers 167 | simple_form (= 3.0.3) 168 | 169 | BUNDLED WITH 170 | 1.10.6 171 | -------------------------------------------------------------------------------- /spec/admino/table/head_row_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Admino 4 | module Table 5 | describe HeadRow do 6 | subject(:row) { HeadRow.new(klass, query, view) } 7 | let(:klass) { Post } 8 | let(:query) { nil } 9 | let(:view) { RailsViewContext.new } 10 | 11 | it 'takes a class and a view context' do 12 | expect(row.resource_klass).to eq klass 13 | expect(row.view_context).to eq view 14 | end 15 | 16 | describe '#column' do 17 | subject { row.to_html } 18 | 19 | context 'text' do 20 | context 'with string label' do 21 | before do 22 | row.column(:title, 'This is a title') 23 | end 24 | 25 | it 'generates a label with it' do 26 | is_expected.to have_tag(:th, text: 'This is a title') 27 | end 28 | end 29 | 30 | context 'with symbol label and I18n set up' do 31 | before do 32 | I18n.backend.store_translations( 33 | :en, 34 | activemodel: { attributes: { post: { foo: 'This is foo' } } } 35 | ) 36 | end 37 | 38 | before do 39 | row.column(:title, :foo) 40 | end 41 | 42 | it 'generates a label with the human attribute name' do 43 | is_expected.to have_tag(:th, text: 'This is foo') 44 | end 45 | end 46 | 47 | context 'with no label' do 48 | before { row.column(:title) } 49 | 50 | it 'generates a label with the titleized attribute name' do 51 | is_expected.to have_tag(:th, text: 'Title') 52 | end 53 | 54 | context 'with I18n set up' do 55 | before do 56 | I18n.backend.store_translations( 57 | :en, 58 | activemodel: { attributes: { post: { title: 'Post title' } } } 59 | ) 60 | end 61 | 62 | before { row.column(:title) } 63 | 64 | it 'generates a label with the human attribute name' do 65 | is_expected.to have_tag(:th, text: 'Post title') 66 | end 67 | end 68 | end 69 | end 70 | 71 | context 'sorting' do 72 | context 'if no query object is present' do 73 | it 'raises an ArgumentError' do 74 | expect { row.column(:title, sorting: :by_title) }.to raise_error ArgumentError 75 | end 76 | end 77 | 78 | context 'else' do 79 | let(:query) { double('Query', sorting: sorting) } 80 | let(:sorting) { double('Sorting') } 81 | 82 | before do 83 | allow(sorting).to receive(:scope_link).with(:by_title, 'Title', {}). 84 | and_return('Link') 85 | end 86 | 87 | before { row.column(:title, sorting: :by_title) } 88 | 89 | it 'generates a sorting scope link' do 90 | is_expected.to have_tag(:th, text: 'Link') 91 | end 92 | end 93 | end 94 | 95 | context 'role' do 96 | before { row.column(:author_name) } 97 | 98 | it 'generates a role attribute with the snake-cased name of the attribute' do 99 | is_expected.to have_tag(:th, with: { role: 'author-name' }) 100 | end 101 | end 102 | 103 | context 'with html options param' do 104 | before { row.column(:title, class: 'foo') } 105 | 106 | it 'uses it to build attributes' do 107 | is_expected.to have_tag(:th, with: { class: 'foo' }) 108 | end 109 | end 110 | end 111 | 112 | describe '#actions' do 113 | subject { row.to_html } 114 | 115 | context do 116 | before { row.actions } 117 | 118 | it 'renders a th cell with role "actions"' do 119 | is_expected.to have_tag(:th, with: { role: 'actions' }) 120 | end 121 | 122 | it 'renders a th cell with text "Actions"' do 123 | is_expected.to have_tag(:th, text: 'Actions') 124 | end 125 | end 126 | 127 | context 'with generic I18n set up' do 128 | before do 129 | I18n.backend.store_translations( 130 | :en, 131 | table: { actions: { title: 'Available actions' } } 132 | ) 133 | end 134 | 135 | it 'renders a th cell with I18n text' do 136 | row.actions 137 | is_expected.to have_tag(:th, text: 'Available actions') 138 | end 139 | 140 | context 'and specific I18n set up' do 141 | before do 142 | I18n.backend.store_translations( 143 | :en, 144 | table: { actions: { post: { title: 'Post actions' } } } 145 | ) 146 | end 147 | 148 | it 'uses the specific I18n text' do 149 | row.actions 150 | is_expected.to have_tag(:th, text: 'Post actions') 151 | end 152 | end 153 | end 154 | end 155 | end 156 | end 157 | end 158 | 159 | -------------------------------------------------------------------------------- /gemfiles/rails_42.gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: ../ 3 | specs: 4 | admino (0.0.16) 5 | activemodel 6 | activesupport 7 | coercible 8 | showcase (~> 0.2.5) 9 | 10 | GEM 11 | remote: https://rubygems.org/ 12 | specs: 13 | actionmailer (4.2.1) 14 | actionpack (= 4.2.1) 15 | actionview (= 4.2.1) 16 | activejob (= 4.2.1) 17 | mail (~> 2.5, >= 2.5.4) 18 | rails-dom-testing (~> 1.0, >= 1.0.5) 19 | actionpack (4.2.1) 20 | actionview (= 4.2.1) 21 | activesupport (= 4.2.1) 22 | rack (~> 1.6) 23 | rack-test (~> 0.6.2) 24 | rails-dom-testing (~> 1.0, >= 1.0.5) 25 | rails-html-sanitizer (~> 1.0, >= 1.0.1) 26 | actionview (4.2.1) 27 | activesupport (= 4.2.1) 28 | builder (~> 3.1) 29 | erubis (~> 2.7.0) 30 | rails-dom-testing (~> 1.0, >= 1.0.5) 31 | rails-html-sanitizer (~> 1.0, >= 1.0.1) 32 | activejob (4.2.1) 33 | activesupport (= 4.2.1) 34 | globalid (>= 0.3.0) 35 | activemodel (4.2.1) 36 | activesupport (= 4.2.1) 37 | builder (~> 3.1) 38 | activerecord (4.2.1) 39 | activemodel (= 4.2.1) 40 | activesupport (= 4.2.1) 41 | arel (~> 6.0) 42 | activesupport (4.2.1) 43 | i18n (~> 0.7) 44 | json (~> 1.7, >= 1.7.7) 45 | minitest (~> 5.1) 46 | thread_safe (~> 0.3, >= 0.3.4) 47 | tzinfo (~> 1.1) 48 | appraisal (2.0.1) 49 | activesupport (>= 3.2.21) 50 | bundler 51 | rake 52 | thor (>= 0.14.0) 53 | arel (6.0.0) 54 | builder (3.2.2) 55 | coderay (1.1.0) 56 | coercible (1.0.0) 57 | descendants_tracker (~> 0.0.1) 58 | coveralls (0.8.1) 59 | json (~> 1.8) 60 | rest-client (>= 1.6.8, < 2) 61 | simplecov (~> 0.10.0) 62 | term-ansicolor (~> 1.3) 63 | thor (~> 0.19.1) 64 | descendants_tracker (0.0.4) 65 | thread_safe (~> 0.3, >= 0.3.1) 66 | diff-lcs (1.2.5) 67 | docile (1.1.5) 68 | domain_name (0.5.24) 69 | unf (>= 0.0.5, < 1.0.0) 70 | erubis (2.7.0) 71 | globalid (0.3.5) 72 | activesupport (>= 4.1.0) 73 | http-cookie (1.0.2) 74 | domain_name (~> 0.5) 75 | i18n (0.7.0) 76 | json (1.8.2) 77 | loofah (2.0.1) 78 | nokogiri (>= 1.5.9) 79 | mail (2.6.3) 80 | mime-types (>= 1.16, < 3) 81 | method_source (0.8.2) 82 | mime-types (2.5) 83 | mini_portile (0.6.2) 84 | minitest (5.6.1) 85 | netrc (0.10.3) 86 | nokogiri (1.6.6.2) 87 | mini_portile (~> 0.6.0) 88 | pry (0.10.1) 89 | coderay (~> 1.1.0) 90 | method_source (~> 0.8.1) 91 | slop (~> 3.4) 92 | rack (1.6.0) 93 | rack-test (0.6.3) 94 | rack (>= 1.0) 95 | rails (4.2.1) 96 | actionmailer (= 4.2.1) 97 | actionpack (= 4.2.1) 98 | actionview (= 4.2.1) 99 | activejob (= 4.2.1) 100 | activemodel (= 4.2.1) 101 | activerecord (= 4.2.1) 102 | activesupport (= 4.2.1) 103 | bundler (>= 1.3.0, < 2.0) 104 | railties (= 4.2.1) 105 | sprockets-rails 106 | rails-deprecated_sanitizer (1.0.3) 107 | activesupport (>= 4.2.0.alpha) 108 | rails-dom-testing (1.0.6) 109 | activesupport (>= 4.2.0.beta, < 5.0) 110 | nokogiri (~> 1.6.0) 111 | rails-deprecated_sanitizer (>= 1.0.1) 112 | rails-html-sanitizer (1.0.2) 113 | loofah (~> 2.0) 114 | railties (4.2.1) 115 | actionpack (= 4.2.1) 116 | activesupport (= 4.2.1) 117 | rake (>= 0.8.7) 118 | thor (>= 0.18.1, < 2.0) 119 | rake (10.4.2) 120 | rest-client (1.8.0) 121 | http-cookie (>= 1.0.2, < 2.0) 122 | mime-types (>= 1.16, < 3.0) 123 | netrc (~> 0.7) 124 | rspec (3.2.0) 125 | rspec-core (~> 3.2.0) 126 | rspec-expectations (~> 3.2.0) 127 | rspec-mocks (~> 3.2.0) 128 | rspec-core (3.2.3) 129 | rspec-support (~> 3.2.0) 130 | rspec-expectations (3.2.1) 131 | diff-lcs (>= 1.2.0, < 2.0) 132 | rspec-support (~> 3.2.0) 133 | rspec-html-matchers (0.7.0) 134 | nokogiri (~> 1) 135 | rspec (~> 3) 136 | rspec-mocks (3.2.1) 137 | diff-lcs (>= 1.2.0, < 2.0) 138 | rspec-support (~> 3.2.0) 139 | rspec-support (3.2.2) 140 | showcase (0.2.5) 141 | activesupport 142 | simple_form (3.1.0) 143 | actionpack (~> 4.0) 144 | activemodel (~> 4.0) 145 | simplecov (0.10.0) 146 | docile (~> 1.1.0) 147 | json (~> 1.8) 148 | simplecov-html (~> 0.10.0) 149 | simplecov-html (0.10.0) 150 | slop (3.6.0) 151 | sprockets (3.0.3) 152 | rack (~> 1.0) 153 | sprockets-rails (2.2.4) 154 | actionpack (>= 3.0) 155 | activesupport (>= 3.0) 156 | sprockets (>= 2.8, < 4.0) 157 | term-ansicolor (1.3.0) 158 | tins (~> 1.0) 159 | thor (0.19.1) 160 | thread_safe (0.3.5) 161 | tins (1.5.1) 162 | tzinfo (1.2.2) 163 | thread_safe (~> 0.1) 164 | unf (0.1.4) 165 | unf_ext 166 | unf_ext (0.0.7.1) 167 | 168 | PLATFORMS 169 | ruby 170 | 171 | DEPENDENCIES 172 | actionpack 173 | admino! 174 | appraisal 175 | bundler (~> 1.3) 176 | coveralls 177 | i18n 178 | pry 179 | rails (= 4.2.1) 180 | rake 181 | rspec 182 | rspec-html-matchers 183 | simple_form (= 3.1.0) 184 | 185 | BUNDLED WITH 186 | 1.10.6 187 | -------------------------------------------------------------------------------- /spec/admino/query/filter_group_presenter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'active_support/hash_with_indifferent_access' 3 | 4 | module Admino 5 | module Query 6 | describe FilterGroupPresenter do 7 | subject(:presenter) { FilterGroupPresenter.new(filter_group, view) } 8 | let(:view) { RailsViewContext.new } 9 | let(:filter_group) do 10 | double( 11 | 'FilterGroup', 12 | query_i18n_key: :query_name, 13 | i18n_key: :filter_group, 14 | param_name: :filter_group 15 | ) 16 | end 17 | let(:request_object) do 18 | double( 19 | 'ActionDispatch::Request', 20 | query_parameters: ActiveSupport::HashWithIndifferentAccess.new( 21 | params 22 | ), 23 | path: '/' 24 | ) 25 | end 26 | let(:params) do 27 | { 'query' => { 'field' => 'value', 'filter_group' => 'bar' } } 28 | end 29 | 30 | before do 31 | allow(view).to receive(:request).and_return(request_object) 32 | end 33 | 34 | describe '#scope_link' do 35 | subject { presenter.scope_link(:foo) } 36 | let(:scope_active) { false } 37 | 38 | before do 39 | allow(filter_group).to receive(:is_scope_active?). 40 | with(:foo).and_return(scope_active) 41 | end 42 | 43 | context 'active CSS class' do 44 | let(:scope_active) { true } 45 | 46 | it 'adds an is-active class' do 47 | is_expected.to have_tag(:a, with: { class: 'is-active' }) 48 | end 49 | 50 | context 'if an :active_class option is specified' do 51 | subject { presenter.scope_link(:foo, active_class: 'active') } 52 | 53 | it 'adds it' do 54 | is_expected.to have_tag(:a, with: { class: 'active' }) 55 | end 56 | end 57 | end 58 | 59 | context 'else' do 60 | it 'does not add it' do 61 | is_expected.not_to have_tag(:a, with: { class: 'is-active' }) 62 | end 63 | end 64 | 65 | context 'label' do 66 | before do 67 | allow(presenter).to receive(:scope_name).with(:foo).and_return('scope_name') 68 | end 69 | 70 | it 'uses #scope_name method' do 71 | is_expected.to have_tag(:a, text: 'scope_name') 72 | end 73 | 74 | context 'if a second parameter is supplied' do 75 | subject { presenter.scope_link(:foo, 'test', active_class: 'active') } 76 | 77 | it 'uses it' do 78 | is_expected.to have_tag(:a, text: 'test') 79 | end 80 | end 81 | end 82 | 83 | context 'URL' do 84 | before do 85 | allow(presenter).to receive(:scope_path).with(:foo).and_return('URL') 86 | end 87 | 88 | it 'uses #scope_path method' do 89 | is_expected.to have_tag(:a, href: 'URL') 90 | end 91 | end 92 | end 93 | 94 | describe '#scope_params' do 95 | let(:scope_active) { false } 96 | subject { presenter.scope_params(:foo) } 97 | 98 | before do 99 | allow(filter_group).to receive(:is_scope_active?).with(:foo).and_return(scope_active) 100 | end 101 | 102 | context 'if scope is active' do 103 | let(:scope_active) { true } 104 | 105 | it 'deletes the filter_group param' do 106 | expect(subject[:query]).not_to have_key 'filter_group' 107 | end 108 | 109 | it 'keeps the request parameters intact' do 110 | presenter.scope_params(:foo) 111 | expect(request_object.query_parameters[:query][:filter_group]).to be_present 112 | end 113 | 114 | context 'the resulting query hash becomes empty' do 115 | let(:params) do 116 | { 'query' => { 'filter_group' => 'bar' } } 117 | end 118 | 119 | it 'removes the param altoghether' do 120 | expect(subject).not_to have_key 'query' 121 | end 122 | end 123 | end 124 | 125 | context 'else' do 126 | let(:scope_active) { false } 127 | 128 | it 'is set as filter group value' do 129 | expect(subject[:query][:filter_group]).to eq 'foo' 130 | end 131 | end 132 | 133 | it 'preserves the other params' do 134 | expect(subject[:query][:field]).to eq 'value' 135 | end 136 | end 137 | 138 | describe '#name' do 139 | context do 140 | before do 141 | I18n.backend.store_translations( 142 | :en, 143 | query: { filter_groups: { query_name: { filter_group: { name: 'NAME' } } } } 144 | ) 145 | end 146 | 147 | it 'returns a I18n translatable name for the filter_group' do 148 | expect(presenter.name).to eq 'NAME' 149 | end 150 | end 151 | 152 | context 'if no translation is available' do 153 | it 'falls back to a titleized version of the filter_group name' do 154 | expect(presenter.name).to eq 'Filter group' 155 | end 156 | end 157 | end 158 | 159 | describe '#scope_name' do 160 | context do 161 | before do 162 | I18n.backend.store_translations( 163 | :en, 164 | query: { filter_groups: { query_name: { filter_group: { scopes: { bar: 'NAME' } } } } } 165 | ) 166 | end 167 | 168 | it 'returns a I18n translatable name for the scope' do 169 | expect(presenter.scope_name(:bar)).to eq 'NAME' 170 | end 171 | end 172 | 173 | context 'if no translation is available' do 174 | it 'falls back to a titleized version of the scope name' do 175 | expect(presenter.scope_name(:bar)).to eq 'Bar' 176 | end 177 | end 178 | end 179 | end 180 | end 181 | end 182 | 183 | -------------------------------------------------------------------------------- /spec/admino/query/base_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Admino 4 | module Query 5 | describe Base do 6 | subject(:query) { Base.new(params, context, config) } 7 | let(:params) { {} } 8 | let(:context) { {} } 9 | let(:config) { nil } 10 | 11 | it 'takes a request params' do 12 | expect(query.params).to eq params 13 | end 14 | 15 | context 'with an explicit config' do 16 | let(:config) { Configuration.new } 17 | 18 | it 'uses it' do 19 | expect(query.config).to eq config 20 | end 21 | end 22 | 23 | context 'with a declared search_field' do 24 | let(:config) { Configuration.new } 25 | let(:search_field_config) { config.add_search_field(:search_field) } 26 | 27 | before do 28 | search_field_config 29 | end 30 | 31 | it 'returns a configured SearchField' do 32 | search_field = query.search_field_by_name(:search_field) 33 | expect(search_field.config).to eq search_field_config 34 | expect(search_field.params).to eq params 35 | end 36 | end 37 | 38 | context 'with a declared filter_group' do 39 | let(:config) { Configuration.new } 40 | let(:filter_group_config) { config.add_filter_group(:filter_group, [:one, :two]) } 41 | 42 | before do 43 | filter_group_config 44 | end 45 | 46 | it 'returns a configured FilterGroup' do 47 | filter_group = query.filter_group_by_name(:filter_group) 48 | expect(filter_group.config).to eq filter_group_config 49 | expect(filter_group.params).to eq params 50 | expect(filter_group.i18n_key).to eq :filter_group 51 | end 52 | end 53 | 54 | context 'with a declared sorting' do 55 | let(:config) { Configuration.new } 56 | let(:sorting_config) do 57 | config.add_sorting_scopes([:by_title, :by_date]) 58 | end 59 | 60 | before do 61 | sorting_config 62 | end 63 | 64 | it 'returns a configured Sorting' do 65 | sorting = query.sorting 66 | expect(sorting.config).to eq sorting_config 67 | expect(sorting.params).to eq params 68 | end 69 | end 70 | 71 | describe '#scope' do 72 | let(:config) { Configuration.new } 73 | let(:result) { query.scope(starting_scope) } 74 | let(:starting_scope) { ScopeMock.new('explicit') } 75 | 76 | describe 'starting scope' do 77 | context 'with an explicit scope' do 78 | it 'uses it' do 79 | expect(result.name).to eq 'explicit' 80 | end 81 | end 82 | 83 | context 'with no explicit scope, but a default one configured' do 84 | let(:result) { query.scope } 85 | 86 | before do 87 | config.starting_scope_callable = Proc.new { |query| 88 | ScopeMock.new('configured').foo(query) 89 | } 90 | end 91 | 92 | before do 93 | result 94 | end 95 | 96 | it 'calls it with self and uses it' do 97 | expect(result.name).to eq 'configured' 98 | expect(result.chain).to eq [:foo, [query]] 99 | end 100 | end 101 | 102 | context 'with no scope' do 103 | let(:result) { query.scope } 104 | 105 | it 'raises a ArgumentError' do 106 | expect { result }.to raise_error(ArgumentError) 107 | end 108 | end 109 | end 110 | 111 | context 'with a set of search_fields, filter_groups and sortings' do 112 | let(:search_field_config) { config.add_search_field(:search_field) } 113 | let(:filter_group_config) { config.add_filter_group(:filter_group, [:one, :two]) } 114 | let(:sorting_config) { config.add_sorting_scopes([:title, :year]) } 115 | 116 | let(:params) do 117 | { 118 | query: { 119 | search_field: "foo", 120 | filter_group: "one", 121 | }, 122 | sorting: "title", 123 | sort_order: "desc" 124 | } 125 | end 126 | 127 | before do 128 | search_field_config 129 | filter_group_config 130 | sorting_config 131 | query 132 | end 133 | 134 | context 'if query object does not respond to scopes' do 135 | it 'chains from starting scope' do 136 | expect(result.chain).to eq [ 137 | :search_field, ["foo"], 138 | :one, [], 139 | :title, [:desc] 140 | ] 141 | end 142 | end 143 | 144 | context 'else' do 145 | let(:query_klass) do 146 | Class.new(Base) do 147 | def self.model_name 148 | ActiveModel::Name.new(self, nil, "temp") 149 | end 150 | 151 | def search_field_scope(scope, foo) 152 | scope.my_search_field(foo) 153 | end 154 | 155 | def one_scope(scope) 156 | scope.my_one 157 | end 158 | 159 | def title_scope(scope, order) 160 | scope.my_title(order) 161 | end 162 | end 163 | end 164 | 165 | subject(:query) do 166 | query_klass.new(params, context, config) 167 | end 168 | 169 | it 'chains from starting scope calling query object methods' do 170 | expect(result.chain).to eq [ 171 | :my_search_field, ["foo"], 172 | :my_one, [], 173 | :my_title, [:desc] 174 | ] 175 | end 176 | end 177 | end 178 | 179 | context 'with a configured ending scope' do 180 | before do 181 | config.ending_scope_callable = Proc.new { |query| bar(query) } 182 | end 183 | 184 | it 'calls it with self at the end of the chain' do 185 | expect(result.chain).to eq [:bar, [query]] 186 | end 187 | end 188 | end 189 | end 190 | end 191 | end 192 | 193 | -------------------------------------------------------------------------------- /spec/admino/table/presenter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'ostruct' 3 | 4 | module Admino 5 | module Table 6 | describe Presenter do 7 | subject(:presenter) { presenter_klass.new(collection, Post, query, view) } 8 | let(:presenter_klass) { Presenter } 9 | let(:query) { double('Query') } 10 | let(:view) { RailsViewContext.new } 11 | 12 | let(:collection) { [ first_post, second_post ] } 13 | let(:first_post) { Post.new('1') } 14 | let(:second_post) { Post.new('2') } 15 | 16 | let(:head_row) { double('HeadRow', to_html: '
258 | <%= q.label :title_matches %> 259 | <%= q.text_field :title_matches %> 260 |
261 |262 | <%= q.submit %> 263 |
264 | 265 | <%# generate inputs from filter_by %> 266 |267 | <%= q.label :status %> 268 | <%= q.select :status, Task.statuses.keys %> 269 |
270 | 271 | <%# if filter_by has only one scope you can use a checkbox %> 272 |273 | <%= q.check_box :deleted, {}, checked_value: "with_deleted" %> 274 | <%= q.label :deleted %> 275 |
276 | <% end %> 277 | 278 | <%# generate the filtering links %> 279 | <% filters_for(query) do |filter_group| %> 280 || Title | 426 |Completed | 427 |Due date | 428 |
|---|---|---|
| Call mum ASAP | 433 |✓ | 434 |2013-02-04 | 435 |
| Actions | 464 |
|---|
| 470 | Show 471 | Edit 472 | Destroy 473 | | 474 |
| 507 | Title 508 | | 509 |510 | Due date 511 | | 512 |
|---|
| Title | 670 | 671 | 672 |
|---|
| Call mum ASAP | 674 |
| Buy some milk | 677 |