diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b235eb6d..09132396 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,11 +8,13 @@ jobs: fail-fast: false matrix: # To keep matrix size down, only test highest and lowest rubies. - ruby: ["2.7", "3.3"] - rails: ["6.1", "7.0", "7.1", "7.2"] + ruby: ["3.0", "3.3"] + rails: ["7.0", "7.1", "7.2", "8.0"] exclude: - - ruby: "2.7" + - ruby: "3.0" rails: "7.2" + - ruby: "3.0" + rails: "8.0" name: Ruby ${{ matrix.ruby }}, Rails ${{ matrix.rails }} runs-on: ubuntu-latest env: diff --git a/CHANGELOG.md b/CHANGELOG.md index b9285560..d9e0c68b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 2.0.0 + +Version 2 is a major update implementing a lot of major improvements +at the cost of backward compatibility. + +[Changes and migration guide](./version-2) + ## 1.8.3 * Fix rails hooking for version 7.1. [#327](https://github.com/bogdan/datagrid/issues/327) diff --git a/Gemfile b/Gemfile index 141cabfe..71afccb4 100644 --- a/Gemfile +++ b/Gemfile @@ -12,7 +12,10 @@ group :development do gem "debug" gem "nokogiri" # used to test html output gem "pry-byebug" + gem "rails-dom-testing", "~> 2.2" gem "rspec" + gem "rubocop", "~> 1.68" + gem "rubocop-yard", "~> 0.9.3", require: false gem "sequel" gem "sqlite3", "~> 1.7.0" @@ -21,7 +24,3 @@ group :development do gem "mongoid", "~> 9.0" end end - -gem "rubocop", "~> 1.68" - -gem "rubocop-yard", "~> 0.9.3", require: false diff --git a/README.md b/README.md index 628f698f..19d8fd28 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,13 @@ # Datagrid -[![Build Status](https://github.com/bogdan/datagrid/workflows/CI/badge.svg?branch=master)](https://github.com/bogdan/datagrid/actions) +Datagrid Version 2.0.0 is here. -[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fbogdan%2Fdatagrid.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fbogdan%2Fdatagrid?ref=badge_shield) +[Migration Guide](./version-2). + +[![Build Status](https://github.com/bogdan/datagrid/actions/workflows/ci.yml/badge.svg)](https://github.com/bogdan/datagrid/actions/workflows/ci.yml) A really mighty and flexible ruby library that generates reports -including admin panels, analytics and data representation: +including admin panels, analytics and data browsers: * Filtering * Columns @@ -21,8 +23,6 @@ including admin panels, analytics and data representation: * Sequel * Array (in-memory data of smaller scale) -[Create an issue](https://github.com/bogdan/datagrid/issues/new) if you want more. - ## Datagrid Philosophy 1. Expressive DSL complements OOD instead of replacing it. @@ -31,9 +31,12 @@ including admin panels, analytics and data representation: ## Documentation -* [Readme](/Readme.markdown) - this read-me for basic information. -* [Wiki](https://github.com/bogdan/datagrid/wiki) - general reference on how to use the gem. -* [Rdoc](https://rubydoc.info/gems/datagrid) - API reference. +* [Rdoc](https://rubydoc.info/gems/datagrid) - full API reference +* [Scope](https://rubydoc.info/gems/datagrid/Datagrid/Core) - working with datagrid scope +* [Columns](https://rubydoc.info/gems/datagrid/Datagrid/Columns) - definging datagrid columns +* [Filters](https://rubydoc.info/gems/datagrid/Datagrid/Filters) - defining datagrid filters +* [Frontend](https://rubydoc.info/gems/datagrid/Datagrid/Helper) - building a frontend +* [Configuration](https://rubydoc.info/gems/datagrid/Datagrid/Configuration) - configuring the gem ### Live Demo @@ -47,9 +50,7 @@ including admin panels, analytics and data representation: In order to create a grid: ``` ruby -class UsersGrid - - include Datagrid +class UsersGrid < Datagrid::Base scope do User.includes(:group) @@ -122,7 +123,7 @@ scope do end ``` -[More about scope](https://github.com/bogdan/datagrid/wiki/Scope) +[More about scope](https://rubydoc.info/gems/datagrid/Datagrid/Core) ### Filters @@ -146,7 +147,7 @@ Datagrid supports different type of filters including: * string * dynamic - build dynamic SQL condition -[More about filters](https://github.com/bogdan/datagrid/wiki/Filters) +[More about filters](https://rubydoc.info/gems/datagrid/Datagrid/Filters) ### Columns @@ -161,7 +162,7 @@ end Some formatting options are also available. Each column is sortable. -[More about columns](https://github.com/bogdan/datagrid/wiki/Columns) +[More about columns](https://rubydoc.info/gems/datagrid/Datagrid/Columns) ### Front end @@ -183,19 +184,19 @@ route resources :skills insert app/assets/stylesheet/application.css ``` -#### Customize Built-in partials +#### Customize Built-in views -In order to get a control on datagrid built-in partials run: +In order to get a control on datagrid built-in views run: ``` sh -rake datagrid:copy_partials +rails g datagrid::views ``` #### Advanced frontend All advanced frontend things are described in: -[Frontend section on wiki](https://github.com/bogdan/datagrid/wiki/Frontend) +[Frontend section on wiki](https://rubydoc.info/gems/datagrid/Datagrid/Helper) ## Questions & Issues diff --git a/app/assets/stylesheets/datagrid.css b/app/assets/stylesheets/datagrid.css new file mode 100644 index 00000000..d06f1a41 --- /dev/null +++ b/app/assets/stylesheets/datagrid.css @@ -0,0 +1,145 @@ +/* Table */ + +table.datagrid-table { + background-color: transparent; + border-collapse: collapse; + max-width: 100%; +} + +table.datagrid-table th { + background-color: #eee; + text-align: left; + vertical-align: top; +} + +table.datagrid-table td, +table.datagrid-table th { + border: 1px solid #d6d6d6; + padding: 5px 10px; +} + +.datagrid-order-control-asc, +.datagrid-order-control-desc { + text-decoration: none; + font-weight: normal; +} + +.datagrid-order-active-asc, +.datagrid-order-active-desc { + background-color: #fff7d5; +} + +.datagrid-order-active-asc a.datagrid-order-control-asc, +.datagrid-order-active-desc a.datagrid-order-control-desc { + font-weight: bold; + color: #d00; +} + +.datagrid-no-results { + text-align: center; +} + +/* Form */ + +.datagrid-form { + background-color: #f0f0f0; + border-radius: 5px; + padding: 20px; +} + +.datagrid-filter { + margin: 10px; +} + +.datagrid-filter label { + width: 150px; + display: inline-block; +} + +.datagrid-filter input, .datagrid-filter select { + border: 2px solid #ccc; + border-radius: 4px; + display: inline-block; + padding: 5px 12px; + width: 300px; +} + +input.datagrid-range-from, input.datagrid-range-to { + width: 138px; +} + +.datagrid-filter select[multiple] { + border: 2px solid #ccc; + border-radius: 5px; + height: 100px; +} + +select.datagrid-dynamic-field { + width: 228px +} + +select.datagrid-dynamic-operation { + margin-left: 7px; + width: 60px; +} + +.datagrid-dynamic-value { + margin: 10px 0 0 154px; +} + +.datagrid-range-separator { + display: inline-block; + margin: 6px 4px 0; +} + +.datagrid-enum-checkboxes { + display: inline-block; +} + +.datagrid-enum-checkboxes input { + margin: 7px; + width: auto; +} + +.datagrid-enum-checkboxes label { + display: block; + width: 100%; +} + +.datagrid-actions { + padding-left: calc(150px + 10px); +} + +.datagrid-submit { + background-color: #555; + border: none; + border-radius: 5px; + color: white; + cursor: pointer; + font-size: 14px; + font-weight: bold; + line-height: normal; + padding: 7px 15px; + vertical-align: middle; + display: inline-block; + zoom: 1; + *display: inline; +} + +.datagrid-submit:hover, +.datagrid-submit:focus { + background-color: #333; +} + +.datagrid-submit:active { + background-color: #000; +} + +.datagrid-reset { + font-size: 14px; + padding: 7px 15px; + vertical-align: middle; + display: inline-block; + zoom: 1; + *display: inline; +} diff --git a/app/assets/stylesheets/datagrid.sass b/app/assets/stylesheets/datagrid.sass deleted file mode 100644 index 63fa4d61..00000000 --- a/app/assets/stylesheets/datagrid.sass +++ /dev/null @@ -1,135 +0,0 @@ -$dg-form-label: 150px - -= clearfix - *zoom: 1 - - &:before, - &:after - display: table - content: '' - - &:after - clear: both - -=inline-block - display: inline-block - zoom: 1 - *display: inline - -table.datagrid - background-color: transparent - border-collapse: collapse - max-width: 100% - - th - background-color: #eee - text-align: left - - td, - th - border: 1px solid #d6d6d6 - padding: 5px 10px - - .order - a.asc, a.desc - text-decoration: none - font-weight: normal - - &.ordered - background-color: #fff7d5 - - &.asc - a.asc - font-weight: bold - color: #d00 - - &.desc - a.desc - font-weight: bold - color: #d00 - .noresults - text-align: center - -.datagrid-form - background-color: #f0f0f0 - border-radius: 5px - padding: 20px - -.datagrid-filter - margin: 10px - +clearfix - - label - width: $dg-form-label - float: left - a - float: left - - input[class*='filter'] - border: 2px solid #ccc - border-radius: 4px - float: left - padding: 5px 12px - width: 207px - - &.from, &.to - width: 83px - - select - float: left - width: 235px - - &[multiple] - border: 2px solid #ccc - border-radius: 5px - height: 100px - &.dynamic_filter - &.field - width: 178px - &.operation - margin-left: 7px - width: 50px - input.dynamic_filter.value - margin: 10px 0 0 $dg-form-label - clear: both - - .separator - float: left - margin: 6px 4px 0 - .enum_filter.checkboxes - float: none - display: block - width: 100% - input - margin: 7px - margin-left: 150px - - -.datagrid-actions - padding-left: $dg-form-label + 10 - - input[type='submit'] - background-color: #555 - border: none - border-radius: 5px - color: white - cursor: pointer - font-size: 14px - font-weight: bold - line-height: normal - padding: 7px 15px - vertical-align: middle - +inline-block - - &:hover, - &:focus - background-color: #333 - - &:active - background-color: #000 - - > a - font-size: 14px - padding: 7px 15px - vertical-align: middle - +inline-block diff --git a/app/views/datagrid/_enum_checkboxes.html.erb b/app/views/datagrid/_enum_checkboxes.html.erb index 9f483196..281bb6f6 100644 --- a/app/views/datagrid/_enum_checkboxes.html.erb +++ b/app/views/datagrid/_enum_checkboxes.html.erb @@ -2,10 +2,12 @@ Indent in this file may cause extra space to appear. You can add indent if whitespace doesn't matter for you %> -<%- elements.each do |value, text, checked| -%> +
+<%- choices.each do |value, text| -%> <%- id = [form.object_name, filter.name, value].join('_').underscore -%> -<%= form.label filter.name, options.merge(for: id) do -%> -<%= form.check_box(filter.name, {multiple: true, id: id, checked: checked, include_hidden: false}, value.to_s, nil) -%> +<%= form.datagrid_label(filter.name, for: id, **options) do -%> +<%= form.datagrid_filter_input(filter.name, id: id, value: value) -%> <%= text -%> <%- end -%> <%- end -%> +
diff --git a/app/views/datagrid/_form.html.erb b/app/views/datagrid/_form.html.erb index 7e175c17..fc4f4aed 100644 --- a/app/views/datagrid/_form.html.erb +++ b/app/views/datagrid/_form.html.erb @@ -1,12 +1,12 @@ -<%= form_for grid, options do |f| -%> +<%= form_with model: grid, html: {class: 'datagrid-form'}, scope: grid.param_name, method: :get, **options do |f| %> <% grid.filters.each do |filter| %> -
+
<%= f.datagrid_label filter %> <%= f.datagrid_filter filter %>
<% end %>
- <%= f.submit I18n.t("datagrid.form.search").html_safe, class: "datagrid-submit" %> - <%= link_to I18n.t('datagrid.form.reset').html_safe, url_for(grid.to_param => {}), class: "datagrid-reset" %> + <%= f.submit I18n.t("datagrid.form.search"), class: "datagrid-submit" %> + <%= link_to I18n.t('datagrid.form.reset'), url_for(grid.to_param => {}), class: "datagrid-reset" %>
<% end -%> diff --git a/app/views/datagrid/_head.html.erb b/app/views/datagrid/_head.html.erb index e9391289..2c59aa74 100644 --- a/app/views/datagrid/_head.html.erb +++ b/app/views/datagrid/_head.html.erb @@ -1,8 +1,31 @@ <% grid.html_columns(*options[:columns]).each do |column| %> - + <%= tag.th( + # Consider maintaining consistency with datagrid/rows partial + "data-column": column.name, + **column.tag_options, + class: [ + column.tag_options[:class], + # Adding HTML classes based on condition + "datagrid-order-active-asc": grid.ordered_by?(column, false), + "datagrid-order-active-desc": grid.ordered_by?(column, true), + ] + ) do %> <%= column.header %> - <%= datagrid_order_for(grid, column, options) if column.supports_order? && options[:order]%> - + <% if column.supports_order? && options[:order] -%> +
+ <%= link_to( + I18n.t("datagrid.table.order.asc"), + datagrid_order_path(grid, column, false), + class: "datagrid-order-control-asc" + ) %> + <%= link_to( + I18n.t("datagrid.table.order.desc"), + datagrid_order_path(grid, column, true), + class: "datagrid-order-control-desc" + ) %> +
+ <% end -%> + <% end -%> <% end %> diff --git a/app/views/datagrid/_range_filter.html.erb b/app/views/datagrid/_range_filter.html.erb index 7a8a1237..faa2575c 100644 --- a/app/views/datagrid/_range_filter.html.erb +++ b/app/views/datagrid/_range_filter.html.erb @@ -1,3 +1,5 @@ -<%= form.datagrid_filter_input(filter, **from_options) %> -<%= I18n.t('datagrid.filters.range.separator') %> -<%= form.datagrid_filter_input(filter, **to_options) %> +<%= form.datagrid_filter_input(filter, class: 'datagrid-range-from', **from_options) %> +<%= I18n.t('datagrid.filters.range.separator') %> +<%# Generating id only for "from" input to make sure -%> +<%# there is no duplicate id in DOM and click on label focuses the first input -%> +<%= form.datagrid_filter_input(filter, class: 'datagrid-range-to', **to_options, id: nil) %> diff --git a/app/views/datagrid/_row.html.erb b/app/views/datagrid/_row.html.erb index f54d21cb..a2254b00 100644 --- a/app/views/datagrid/_row.html.erb +++ b/app/views/datagrid/_row.html.erb @@ -1,5 +1,16 @@ <% grid.html_columns(*options[:columns]).each do |column| %> - <%= datagrid_value(grid, column, asset) %> + <%= tag.td( + datagrid_value(grid, column, asset), + # Consider maintaining consistency with datagrid/rows partial + "data-column": column.name, + **column.tag_options, + class: [ + column.tag_options[:class], + # Adding HTML classes based on condition + "datagrid-order-active-asc": grid.ordered_by?(column, false), + "datagrid-order-active-desc": grid.ordered_by?(column, true), + ] + ) %> <% end %> diff --git a/app/views/datagrid/_table.html.erb b/app/views/datagrid/_table.html.erb index 8708c05a..88eeafbe 100644 --- a/app/views/datagrid/_table.html.erb +++ b/app/views/datagrid/_table.html.erb @@ -5,18 +5,18 @@ Local variables: * options - passed options Hash %> <% if grid.html_columns(*options[:columns]).any? %> - <%= content_tag :table, options[:html] do %> + <%= tag.table class: 'datagrid-table', **options.fetch(:html, {}) do %> - <%= datagrid_header(grid, options) %> + <%= datagrid_header(grid, **options) %> <% if assets.any? %> <%= datagrid_rows(grid, assets, **options) %> <% else %> - <%= I18n.t('datagrid.no_results').html_safe %> + <%= I18n.t('datagrid.no_results') %> <% end %> <% end %> <% else -%> - <%= I18n.t("datagrid.table.no_columns").html_safe %> + <%= I18n.t("datagrid.table.no_columns") %> <% end %> diff --git a/datagrid.gemspec b/datagrid.gemspec index 737205f7..419c4d11 100644 --- a/datagrid.gemspec +++ b/datagrid.gemspec @@ -19,19 +19,20 @@ Gem::Specification.new do |s| "CHANGELOG.md", "README.md", "datagrid.gemspec", + ".yardopts", ] s.files += `git ls-files | grep -E '^(app|lib|templates)'`.split("\n") s.homepage = "https://github.com/bogdan/datagrid" s.licenses = ["MIT"] - s.required_ruby_version = Gem::Requirement.new(">= 2.7") + s.required_ruby_version = Gem::Requirement.new(">= 3.0") s.metadata = { "homepage_uri" => s.homepage, "bug_tracker_uri" => "#{s.homepage}/issues", - "documentation_uri" => "#{s.homepage}/wiki", - "changelog_uri" => "#{s.homepage}/blob/master/CHANGELOG.md", + "documentation_uri" => "https://rubydoc.info/gems/datagrid", + "changelog_uri" => "#{s.homepage}/blob/main/CHANGELOG.md", "source_code_uri" => s.homepage, "rubygems_mfa_required" => "true", } - s.add_dependency "railties", ">= 6.1" + s.add_dependency "railties", ">= 7.0" end diff --git a/gemfiles/rails_8.0.gemfile b/gemfiles/rails_8.0.gemfile new file mode 100644 index 00000000..5185cbad --- /dev/null +++ b/gemfiles/rails_8.0.gemfile @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +# This file was generated by Appraisal + +source "https://rubygems.org" + +group :development do + gem "appraisal" + gem "bump" + gem "csv" + gem "nokogiri" + gem "pry-byebug" + gem "rails", "~> 8.0.0" + gem "rspec" + gem "sequel" + gem "sqlite3", "~> 2.1.0" + + group :mongo do + gem "bson" + gem "mongoid", github: "mongodb/mongoid" + end +end + +gemspec path: "../" diff --git a/lib/datagrid.rb b/lib/datagrid.rb index 5028e522..7a3111bb 100644 --- a/lib/datagrid.rb +++ b/lib/datagrid.rb @@ -4,27 +4,10 @@ require "datagrid/configuration" require "datagrid/engine" -# @!visibility public module Datagrid - extend ActiveSupport::Autoload - - autoload :Core - autoload :ActiveModel - autoload :Filters - autoload :Columns - autoload :ColumnNamesAttribute - autoload :Ordering - autoload :Configuration - - autoload :Helper - autoload :FormBuilder - - autoload :Renderer - - autoload :Engine - # @!visibility private def self.included(base) + Utils.warn_once("Including Datagrid is deprecated. Inherit Datagrid::Base instead.") base.class_eval do include ::Datagrid::Core include ::Datagrid::ActiveModel @@ -40,5 +23,7 @@ class ArgumentError < ::ArgumentError; end class ColumnUnavailableError < StandardError; end end -require "datagrid/scaffold" +require "datagrid/base" +require "datagrid/generators/scaffold" +require "datagrid/generators/views" I18n.load_path << File.expand_path("datagrid/locale/en.yml", __dir__) diff --git a/lib/datagrid/active_model.rb b/lib/datagrid/active_model.rb index 22860e0a..bc77426b 100644 --- a/lib/datagrid/active_model.rb +++ b/lib/datagrid/active_model.rb @@ -3,21 +3,11 @@ module Datagrid # Required to be ActiveModel compatible module ActiveModel - # @!visibility private - def self.included(base) - base.extend ClassMethods - base.class_eval do - begin - require "active_model/naming" - extend ::ActiveModel::Naming - rescue LoadError - end - begin - require "active_model/attributes_assignment" - extend ::ActiveModel::AttributesAssignment - rescue LoadError - end - end + extend ActiveSupport::Concern + + included do + require "active_model/naming" + extend ::ActiveModel::Naming end module ClassMethods diff --git a/lib/datagrid/base.rb b/lib/datagrid/base.rb new file mode 100644 index 00000000..f9412418 --- /dev/null +++ b/lib/datagrid/base.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module Datagrid + extend ActiveSupport::Autoload + + autoload :Core + autoload :ActiveModel + autoload :Filters + autoload :Columns + autoload :ColumnNamesAttribute + autoload :Ordering + autoload :Configuration + + autoload :Helper + autoload :FormBuilder + + autoload :Engine + + # Main datagrid class allowing to define columns and filters on your objects + # + # @example + # class UsersGrid < Datagrid::Base + # scope { User } + # + # filter(:id, :integer) + # filter(:name, :string) + # + # column(:id) + # column(:name) + # end + class Base + include ::Datagrid::Core + include ::Datagrid::ActiveModel + include ::Datagrid::Filters + include ::Datagrid::Columns + include ::Datagrid::ColumnNamesAttribute + include ::Datagrid::Ordering + end +end diff --git a/lib/datagrid/columns.rb b/lib/datagrid/columns.rb index c45ab741..fdd23e98 100644 --- a/lib/datagrid/columns.rb +++ b/lib/datagrid/columns.rb @@ -2,13 +2,12 @@ require "datagrid/utils" require "active_support/core_ext/class/attribute" +require "datagrid/columns/column" module Datagrid # Defines a column to be used for displaying data in a Datagrid. # - # class UserGrid - # include Datagrid - # + # class UserGrid < ApplicationGrid # scope do # User.order("users.created_at desc").joins(:group) # end @@ -201,33 +200,30 @@ module Datagrid # presenter.user.created_at # end module Columns - require "datagrid/columns/column" - # @!method default_column_options=(value) - # @param [Hash] value default options passed to #column method call - # @return [Hash] default options passed to #column method call - # @example - # # Disable default order - # self.default_column_options = { order: false } - # # Makes entire report HTML - # self.default_column_options = { html: true } + # @param [Hash] value default options passed to {#column} method call + # @return [Hash] default options passed to {#column} method call + # @example Disable default order + # self.default_column_options = { order: false } + # @example Makes entire report HTML + # self.default_column_options = { html: true } # @!method default_column_options - # @return [Hash] - # @see #default_column_options= + # @return [Hash] default options passed to {#column} method call + # @see #default_column_options= # @!method batch_size=(value) - # @param [Integer] value Specify a default batch size when generating CSV or just data. Default: 1000 - # @return [Integer] Specify a default batch size when generating CSV or just data. - # @example - # self.batch_size = 500 - # # Disable batches - # self.batch_size = nil - # + # Specify a default batch size when generating CSV or just data. + # @param [Integer] value a batch size when generating CSV or just data. Default: 1000 + # @return [void] + # @example Set batch size to 500 + # self.batch_size = 500 + # @example Disable batches + # self.batch_size = nil # @!method batch_size - # @return [Integer] - # @see #batch_size= + # @return [Integer] + # @see #batch_size= # @visibility private # @param [Object] base @@ -255,35 +251,31 @@ def columns(*column_names, data: false, html: false) filter_columns(columns_array, *column_names, data: data, html: html) end - # Defines new datagrid column - # + # Defines a new datagrid column # @param name [Symbol] column name # @param query [String, nil] a string representing the query to select this column (supports only ActiveRecord) - # @param options [Hash{Symbol => Object}] hash of options # @param block [Block] proc to calculate a column value + # @option options [Boolean, String] html Determines if the column should be present + # in the HTML table and how it is formatted. + # @option options [String, Array] order Determines if the column can be sortable and + # specifies the ORM ordering method. + # Example: `"created_at, id"` for ActiveRecord, `[:created_at, :id]` for Mongoid. + # @option options [String] order_desc Specifies a descending order for the column + # (used when `:order` cannot be easily reversed by the ORM). + # @option options [Boolean, Proc] order_by_value Enables Ruby-level ordering for the column. + # Warning: Sorting large datasets in Ruby is not recommended. + # If `true`, Datagrid orders by the column value. + # If a block is provided, Datagrid orders by the block's return value. + # @option options [Boolean] mandatory If `true`, the column will never be hidden by the `#column_names` selection. + # @option options [Symbol] before Places the column before the specified column when determining order. + # @option options [Symbol] after Places the column after the specified column when determining order. + # @option options [Boolean, Proc] if conditions when a column is available. + # @option options [Boolean, Proc] unless conditions when a column is not available. + # @option options [Symbol, Array] preload Specifies associations + # to preload for the column within the scope. + # @option options [Hash] tag_options Specifies HTML attributes for the `` or `` of the column. + # Example: `{ class: "content-align-right", "data-group": "statistics" }`. # @return [Datagrid::Columns::Column] - # - # Available options: - # - # * html - determines if current column should be present in html table and how is it formatted - # * order - determines if this column could be sortable and how. - # The value of order is explicitly passed to ORM ordering method. - # Ex: "created_at, id" for ActiveRecord, [:created_at, :id] for Mongoid - # * order_desc - determines a descending order for given column - # (only in case when :order can not be easily reversed by ORM) - # * order_by_value - used in case it is easier to perform ordering at ruby level not on database level. - # Warning: using ruby to order large datasets is very unrecommended. - # If set to true - datagrid will use column value to order by this column - # If block is given - datagrid will use value returned from block - # * mandatory - if true, column will never be hidden with #column_names selection - # * url - a proc with one argument, pass this option to easily convert the value into an URL - # * before - determines the position of this column, by adding it before the column passed here - # * after - determines the position of this column, by adding it after the column passed here - # * if - the column is shown if the reult of calling this argument is true - # * unless - the column is shown unless the reult of calling this argument is true - # * preload - spefies which associations of the scope should be preloaded for this column - # - # @see https://github.com/bogdan/datagrid/wiki/Columns def column(name, query = nil, **options, &block) define_column(columns_array, name, query, **options, &block) end @@ -312,7 +304,7 @@ def respond_to(&block) # @example # column(:name) do |model| # format(model.name) do |value| - # content_tag(:strong, value) + # tag.strong(value) # end # end def format(value, &block) @@ -333,10 +325,10 @@ def format(value, &block) # Defines a model decorator that will be used to define a column value. # All column blocks will be given a decorated version of the model. # @return [void] - # @example + # @example Wrapping a model with presenter # decorate { |user| UserPresenter.new(user) } - # - # decorate { UserPresenter } # a shortcut + # @example A shortcut for doing the same + # decorate { UserPresenter } def decorate(model = nil, &block) if !model && !block raise ArgumentError, "decorate needs either a block to define decoration or a model to decorate" @@ -403,14 +395,16 @@ def assets ) end - # @param column_names [Array] list of column names if you want to limit data only to specified columns + # @param column_names [Array] list of column names + # if you want to limit data only to specified columns # @return [Array] human readable column names. See also "Localization" section def header(*column_names) data_columns(*column_names).map(&:header) end # @param asset [Object] asset from datagrid scope - # @param column_names [Array] list of column names if you want to limit data only to specified columns + # @param column_names [Array] list of column names + # if you want to limit data only to specified columns # @return [Array] column values for given asset def row_for(asset, *column_names) data_columns(*column_names).map do |column| @@ -419,7 +413,8 @@ def row_for(asset, *column_names) end # @param asset [Object] asset from datagrid scope - # @return [Hash] A mapping where keys are column names and values are column values for the given asset + # @return [Hash] A mapping where keys are column names and + # values are column values for the given asset def hash_for(asset) result = {} data_columns.each do |column| @@ -428,7 +423,8 @@ def hash_for(asset) result end - # @param column_names [Array] list of column names if you want to limit data only to specified columns + # @param column_names [Array] list of column names + # if you want to limit data only to specified columns # @return [Array>] with data for each row in datagrid assets without header def rows(*column_names) map_with_batches do |asset| @@ -436,7 +432,8 @@ def rows(*column_names) end end - # @param column_names [Array] list of column names if you want to limit data only to specified columns + # @param column_names [Array] list of column names + # if you want to limit data only to specified columns. # @return [Array>] data for each row in datagrid assets with header. def data(*column_names) rows(*column_names).unshift(header(*column_names)) @@ -461,7 +458,7 @@ def data_hash end end - # @param column_names [Array] + # @param column_names [Array] # @param options [Hash] CSV generation options # @return [String] a CSV representation of the data in the grid # @@ -499,7 +496,8 @@ def columns(*column_names, data: false, html: false) end end - # @param column_names [Array] list of column names if you want to limit only to specified columns + # @param column_names [Array] list of column names + # if you want to limit data only to specified columns # @param [Boolean] html return only HTML columns # @return [Array] columns that can be represented in plain data(non-html) way def data_columns(*column_names, html: false) @@ -513,7 +511,7 @@ def html_columns(*column_names, data: false) columns(*column_names, data: data, html: true) end - # Finds a column definition by name + # Finds a column by name # @param name [String, Symbol] column name to be found # @return [Datagrid::Columns::Column, nil] def column_by_name(name) @@ -521,22 +519,21 @@ def column_by_name(name) end # Gives ability to have a different formatting for CSV and HTML column value. - # - # @example + # @example Formating using Rails view helpers # column(:name) do |model| # format(model.name) do |value| - # content_tag(:strong, value) + # tag.strong(value) # end # end - # + # @example Formatting using a separated view template # column(:company) do |model| # format(model.company.name) do - # render partial: "company_with_logo", locals: {company: model.company } + # render partial: "companies/company_with_logo", locals: {company: model.company } # end # end # @return [Datagrid::Columns::Column::ResponseFormat] Format object def format(value, &block) - if block_given? + if block self.class.format(value, &block) else # don't override Object#format method @@ -544,26 +541,26 @@ def format(value, &block) end end + # @param [Object] asset one of the assets from grid scope # @return [Datagrid::Columns::DataRow] an object representing a grid row. # @example - # class MyGrid - # scope { User } - # column(:id) - # column(:name) - # column(:number_of_purchases) do |user| - # user.purchases.count - # end - # end + # class MyGrid + # scope { User } + # column(:id) + # column(:name) + # column(:number_of_purchases) do |user| + # user.purchases.count + # end + # end # - # row = MyGrid.new.data_row(User.last) - # row.id # => user.id - # row.number_of_purchases # => user.purchases.count + # row = MyGrid.new.data_row(User.last) + # row.id # => user.id + # row.number_of_purchases # => user.purchases.count def data_row(asset) ::Datagrid::Columns::DataRow.new(self, asset) end # Defines a column at instance level - # # @see Datagrid::Columns::ClassMethods#column def column(name, query = nil, **options, &block) self.class.define_column(columns_array, name, query, **options, &block) @@ -575,8 +572,8 @@ def initialize(*) super end - # @return [Array] all columns that are possible to be displayed for the current grid object - # + # @return [Array] all columns + # that are possible to be displayed for the current grid object # @example # class MyGrid # filter(:search) {|scope, value| scope.full_text_search(value)} @@ -598,6 +595,8 @@ def available_columns end end + # @param [String,Symbol] column_name column name + # @param [Object] asset one of the assets from grid scope # @return [Object] a cell data value for given column name and asset def data_value(column_name, asset) column = column_by_name(column_name) @@ -609,6 +608,9 @@ def data_value(column_name, asset) end end + # @param [String,Symbol] column_name column name + # @param [Object] asset one of the assets from grid scope + # @param [ActionView::Base] context view context object # @return [Object] a cell HTML value for given column name and asset and view context def html_value(column_name, context, asset) column = column_by_name(column_name) @@ -622,6 +624,7 @@ def html_value(column_name, context, asset) end end + # @param [Object] model one of the assets from grid scope # @return [Object] a decorated version of given model if decorator is specified or the model otherwise. def decorate(model) self.class.decorate(model) @@ -679,7 +682,9 @@ def cache_key(asset) end rescue NotImplementedError raise Datagrid::ConfigurationError, - "#{self} is setup to use cache. But there was appropriate cache key found for #{asset.inspect}. Please set cached option to block with asset as argument and cache key as returning value to resolve the issue." + <<~MSG + #{self} is setup to use cache. But there was appropriate cache key found for #{asset.inspect}. + MSG end def map_with_batches(&block) @@ -727,6 +732,10 @@ def initialize(grid, model) def method_missing(meth, *_args) @grid.data_value(meth, @model) end + + def respond_to_missing?(meth, include_private = false) + !!@grid.column_by_name(meth) || super + end end end end diff --git a/lib/datagrid/columns/column.rb b/lib/datagrid/columns/column.rb index 2f153592..f41d4101 100644 --- a/lib/datagrid/columns/column.rb +++ b/lib/datagrid/columns/column.rb @@ -39,6 +39,15 @@ def initialize(grid_class, name, query, options = {}, &block) self.grid_class = grid_class self.name = name.to_sym self.options = options + if options[:class] + Datagrid::Utils.warn_once( + "column[class] option is deprecated. Use {tag_options: {class: ...}} instead.", + ) + self.options[:tag_options] = { + **self.options.fetch(:tag_options, {}), + class: options[:class], + } + end if options[:html] == true self.html_block = block else @@ -108,6 +117,17 @@ def mandatory? !!options[:mandatory] end + def tag_options + options[:tag_options] || {} + end + + def html_class + Datagrid::Utils.warn_once( + "Column#html_class is deprecated. Use Column#tag_options instead.", + ) + options[:class] + end + def mandatory_explicitly_set? options.key?(:mandatory) end diff --git a/lib/datagrid/configuration.rb b/lib/datagrid/configuration.rb index 5c64e3e0..a80e8b22 100644 --- a/lib/datagrid/configuration.rb +++ b/lib/datagrid/configuration.rb @@ -25,12 +25,14 @@ def self.configure(&block) # Datagrid.configure do |config| # # Defines date formats that can be used to parse dates. # # Note: Multiple formats can be specified. The first format is used to format dates as strings, - # # while other formats are used only for parsing dates from strings (e.g., if your app supports multiple formats). + # # while other formats are used only for parsing dates + # # from strings (e.g., if your app supports multiple formats). # config.date_formats = "%m/%d/%Y", "%Y-%m-%d" # # # Defines timestamp formats that can be used to parse timestamps. # # Note: Multiple formats can be specified. The first format is used to format timestamps as strings, - # # while other formats are used only for parsing timestamps from strings (e.g., if your app supports multiple formats). + # # while other formats are used only for parsing timestamps + # # from strings (e.g., if your app supports multiple formats). # config.datetime_formats = ["%m/%d/%Y %h:%M", "%Y-%m-%d %h:%M:%s"] # end # ``` diff --git a/lib/datagrid/core.rb b/lib/datagrid/core.rb index 46d58f2e..e13d6b17 100644 --- a/lib/datagrid/core.rb +++ b/lib/datagrid/core.rb @@ -61,20 +61,18 @@ module ClassMethods def datagrid_attribute(name, &block) return if datagrid_attributes.include?(name) - block ||= lambda do |value| - value - end datagrid_attributes << name define_method name do instance_variable_get("@#{name}") end define_method :"#{name}=" do |value| - instance_variable_set("@#{name}", instance_exec(value, &block)) + instance_variable_set("@#{name}", block ? instance_exec(value, &block) : value) end end - # @return [void] Defines a scope at class level + # Defines a relation scope of database models to be filtered + # @return [void] # @example # scope { User } # scope { Project.where(deleted: false) } @@ -146,13 +144,15 @@ def dynamic(&block) end end - protected - + # @!visibility private def check_scope_defined!(message = nil) message ||= "#{self}.scope is not defined" raise(Datagrid::ConfigurationError, message) unless scope_value end + protected + + # @!visibility private def inherited(child_class) super child_class.datagrid_attributes = datagrid_attributes.clone @@ -174,6 +174,15 @@ def initialize(attributes = nil, &block) end # @return [Hash{Symbol => Object}] grid attributes including filter values and ordering values + # @example + # class UsersGrid < ApplicationGrid + # scope { User } + # filter(:first_name, :string) + # filter(:last_name, :string) + # end + # + # grid = UsersGrid.new(first_name: 'John', last_name: 'Smith') + # grid.attributes # => {first_name: 'John', last_name: 'Smith', order: nil, descending: nil} def attributes result = {} datagrid_attributes.each do |name| @@ -182,14 +191,6 @@ def attributes result end - # Updates datagrid attributes with a passed hash argument - # @param attributes [Hash{Symbol => Object}] - # @example - # grid = MyGrid.new - # grid.attributes = {first_name: 'John', last_name: 'Smith'} - # grid.first_name # => 'John' - # grid.last_name # => 'Smith' - # @param [String, Symbol] attribute attribute name # @return [Object] Any datagrid attribute value def [](attribute) @@ -258,7 +259,7 @@ def scope(&block) # @!visibility private def original_scope - check_scope_defined! + self.class.check_scope_defined! scope_value.call end @@ -277,11 +278,6 @@ def driver self.class.driver end - # @!visibility private - def check_scope_defined!(message = nil) - self.class.send :check_scope_defined!, message - end - # @return [String] a datagrid attributes and their values in inspection form def inspect attrs = attributes.map do |key, value| @@ -304,7 +300,13 @@ def reset protected def sanitize_for_mass_assignment(attributes) - forbidden_attributes_protection ? super : attributes + if forbidden_attributes_protection + super + elsif defined?(ActionController::Parameters) && attributes.is_a?(ActionController::Parameters) + attributes.to_unsafe_h + else + attributes + end end end end diff --git a/lib/datagrid/deprecated_object.rb b/lib/datagrid/deprecated_object.rb new file mode 100644 index 00000000..aa316460 --- /dev/null +++ b/lib/datagrid/deprecated_object.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Datagrid + # @!visibility private + class DeprecatedObject < BasicObject + def initialize(real_object, &block) + @real_object = real_object + @block = block + end + + def method_missing(method_name, ...) + @block.call + @real_object.public_send(method_name, ...) + end + + def respond_to_missing?(method_name, include_private = false) + @real_object.respond_to?(method_name, include_private) + end + end +end diff --git a/lib/datagrid/drivers/abstract_driver.rb b/lib/datagrid/drivers/abstract_driver.rb index 8cf5e6bc..5b92409b 100644 --- a/lib/datagrid/drivers/abstract_driver.rb +++ b/lib/datagrid/drivers/abstract_driver.rb @@ -93,13 +93,6 @@ def default_cache_key(asset) raise NotImplementedError end - def where_by_timestamp_gotcha(scope, name, value) - value = Datagrid::Utils.format_date_as_timestamp(value) - scope = greater_equal(scope, name, value.first) if value.first - scope = less_equal(scope, name, value.last) if value.last - scope - end - def default_preload(scope, value) raise NotImplementedError end diff --git a/lib/datagrid/drivers/active_record.rb b/lib/datagrid/drivers/active_record.rb index fb524bf5..bf10cac5 100644 --- a/lib/datagrid/drivers/active_record.rb +++ b/lib/datagrid/drivers/active_record.rb @@ -32,9 +32,7 @@ def to_scope(scope) def append_column_queries(assets, columns) if columns.present? - if assets.select_values.empty? - assets = assets.select(Arel.respond_to?(:star) ? assets.klass.arel_table[Arel.star] : "#{assets.quoted_table_name}.*") - end + assets = assets.select(assets.klass.arel_table[Arel.star]) if assets.select_values.empty? columns = columns.map { |c| "#{c.query} AS #{c.name}" } assets = assets.select(*columns) end @@ -144,11 +142,3 @@ def contains_predicate end end end - -if defined?(ActiveRecord::Base) - ActiveRecord::Base.class_eval do - def self.datagrid_where_by_timestamp(column, value) - Datagrid::Drivers::ActiveRecord.new.where_by_timestamp_gotcha(self, column, value) - end - end -end diff --git a/lib/datagrid/filters.rb b/lib/datagrid/filters.rb index c7fe692f..19637c8e 100644 --- a/lib/datagrid/filters.rb +++ b/lib/datagrid/filters.rb @@ -6,9 +6,7 @@ module Datagrid # Defines the accessible attribute that is used to filter # the scope by the specified value with specified code. # - # class UserGrid - # include Datagrid - # + # class UserGrid < ApplicationGrid # scope do # User # end @@ -64,13 +62,13 @@ module Datagrid # # `:date` - Converts value to a date. Supports the `:range` option to accept date ranges. # - # filter(:created_at, :date, range: true, default: proc { [1.month.ago.to_date, Date.today] }) + # filter(:created_at, :date, range: true, default: proc { 1.month.ago.to_date..Date.today }) # # ## Datetime # # `:datetime` - Converts value to a timestamp. Supports the `:range` option to accept time ranges. # - # filter(:created_at, :datetime, range: true, default: proc { [1.hour.ago, Time.now] }) + # filter(:created_at, :datetime, range: true, default: proc { 1.hour.ago..Time.now }) # # ## Enum # @@ -93,7 +91,7 @@ module Datagrid # # `:integer` - Converts value to an integer. Supports the `:range` option. # - # filter(:posts_count, :integer, range: true, default: [1, nil]) + # filter(:posts_count, :integer, range: true, default: (1..nil)) # # ## String # @@ -123,7 +121,7 @@ module Datagrid # Example: # # filter(:id, :integer, header: "Identifier") - # filter(:created_at, :date, range: true, default: proc { [1.month.ago.to_date, Date.today] }) + # filter(:created_at, :date, range: true, default: proc { 1.month.ago.to_date..Date.today }) # # # Localization # @@ -140,7 +138,6 @@ module Filters require "datagrid/filters/date_time_filter" require "datagrid/filters/default_filter" require "datagrid/filters/integer_filter" - require "datagrid/filters/composite_filters" require "datagrid/filters/string_filter" require "datagrid/filters/float_filter" require "datagrid/filters/dynamic_filter" @@ -162,16 +159,14 @@ module Filters # @!visibility private DEFAULT_FILTER_BLOCK = Object.new - # @!visibility private - def self.included(base) - base.extend ClassMethods - base.class_eval do - include Datagrid::Core - include Datagrid::Filters::CompositeFilters - class_attribute :filters_array, default: [] - end + extend ActiveSupport::Concern + + included do + include Datagrid::Core + class_attribute :filters_array, default: [] end + # Grid class methods related to filters module ClassMethods # @return [Datagrid::Filters::BaseFilter, nil] filter definition object by name def filter_by_name(attribute) @@ -209,8 +204,8 @@ def filter_by_name(attribute) # Used with the `datagrid_form_for` helper. # @option options [Symbol] after Specifies the position of this filter by placing it after another filter. # Used with the `datagrid_form_for` helper. - # @option options [Boolean] dummy If true, the filter is not applied automatically and is only displayed in the form. - # Useful for manual application. + # @option options [Boolean] dummy If true, the filter is not applied automatically and + # is only displayed in the form. Useful for manual application. # @option options [Proc, Symbol] if Specifies a condition under which the filter is displayed and used. # Accepts a block or the name of an instance method. # @option options [Proc, Symbol] unless Specifies a condition under which the filter is NOT displayed or used. @@ -223,7 +218,7 @@ def filter(name, type = :default, **options, &block) raise ConfigurationError, "filter class #{type.inspect} not found" unless klass position = Datagrid::Utils.extract_position_from_options(filters_array, options) - filter = klass.new(self, name, options, &block) + filter = klass.new(self, name, **options, &block) filters_array.insert(position, filter) datagrid_attribute(name) do |value| @@ -267,7 +262,8 @@ def filters_inspection def initialize(...) self.filters_array = self.class.filters_array.clone filters_array.each do |filter| - self[filter.name] = filter.default(self) + value = filter.default(self) + self[filter.name] = value unless value.nil? end super end @@ -337,8 +333,11 @@ def default_filter def find_select_filter(filter) filter = filter_by_name(filter) unless filter.class.included_modules.include?(::Datagrid::Filters::SelectOptions) - raise ::Datagrid::ArgumentError, - "#{self.class.name}##{filter.name} with type #{FILTER_TYPES.invert[filter.class].inspect} can not have select options" + type = FILTER_TYPES.invert[filter.class].inspect + raise( + ::Datagrid::ArgumentError, + "#{self.class.name}##{filter.name} with type #{type} can not have select options", + ) end filter end diff --git a/lib/datagrid/filters/base_filter.rb b/lib/datagrid/filters/base_filter.rb index eb80c39f..56303f11 100644 --- a/lib/datagrid/filters/base_filter.rb +++ b/lib/datagrid/filters/base_filter.rb @@ -10,20 +10,23 @@ class FilteringError < StandardError module Datagrid module Filters class BaseFilter - class_attribute :input_helper_name, instance_writer: false attr_accessor :grid_class, :options, :block, :name - def initialize(grid_class, name, options = {}, &block) + def initialize(grid_class, name, **options, &block) self.grid_class = grid_class self.name = name.to_sym self.options = options - self.block = block || default_filter_block + self.block = block end def parse(value) raise NotImplementedError, "#parse(value) suppose to be overwritten" end + def default_input_options + { type: "text" } + end + def unapplicable_value?(value) value.nil? ? !allow_nil? : value.blank? && !allow_blank? end @@ -35,10 +38,12 @@ def apply(grid_object, scope, value) return scope unless result - result = default_filter(value, scope, grid_object) if result == Datagrid::Filters::DEFAULT_FILTER_BLOCK + result = default_filter(value, scope) if result == Datagrid::Filters::DEFAULT_FILTER_BLOCK unless grid_object.driver.match?(result) - raise Datagrid::FilteringError, - "Can not apply #{name.inspect} filter: result #{result.inspect} no longer match #{grid_object.driver.class}." + raise( + Datagrid::FilteringError, + "Filter #{name.inspect} unapplicable: result no longer match #{grid_object.driver.class}.", + ) end result @@ -48,12 +53,18 @@ def parse_values(value) if multiple? return nil if value.nil? - normalize_multiple_value(value).map do |v| + return normalize_multiple_value(value).map do |v| parse(v) end - elsif value.is_a?(Array) + end + + case value + when Array raise Datagrid::ArgumentError, "#{grid_class}##{name} filter can not accept Array argument. Use :multiple option." + when Range + raise Datagrid::ArgumentError, + "#{grid_class}##{name} filter can not accept Range argument. Use :range option." else parse(value) end @@ -86,6 +97,10 @@ def multiple? options[:multiple] end + def range? + false + end + def allow_nil? options.key?(:allow_nil) ? options[:allow_nil] : options[:allow_blank] end @@ -110,13 +125,6 @@ def self.form_builder_helper_name :"datagrid_#{to_s.demodulize.underscore}" end - def default_filter_block - filter = self - lambda do |value, scope, grid| - filter.default_filter(value, scope, grid) - end - end - def supports_range? self.class.ancestors.include?(::Datagrid::Filters::RangedFilter) end @@ -140,6 +148,14 @@ def enabled?(grid) ::Datagrid::Utils.process_availability(grid, options[:if], options[:unless]) end + def enum_checkboxes? + false + end + + def default_scope? + !block + end + protected def default_filter_where(scope, value) @@ -147,10 +163,12 @@ def default_filter_where(scope, value) end def execute(value, scope, grid_object) - if block.arity == 1 + if block&.arity == 1 scope.instance_exec(value, &block) - else + elsif block Datagrid::Utils.apply_args(value, scope, grid_object, &block) + else + default_filter(value, scope) end end @@ -175,7 +193,7 @@ def driver grid_class.driver end - def default_filter(value, scope, _grid) + def default_filter(value, scope) return nil if dummy? if !driver.scope_has_column?(scope, name) && scope.respond_to?(name, true) diff --git a/lib/datagrid/filters/boolean_filter.rb b/lib/datagrid/filters/boolean_filter.rb index d7e40b44..753f3da9 100644 --- a/lib/datagrid/filters/boolean_filter.rb +++ b/lib/datagrid/filters/boolean_filter.rb @@ -5,6 +5,16 @@ module Datagrid module Filters class BooleanFilter < Datagrid::Filters::BaseFilter + # @!visibility private + def initialize(grid, name, **opts) + super + options[:default] ||= false + end + + def default_input_options + { **super, type: "checkbox" } + end + def parse(value) Datagrid::Utils.booleanize(value) end diff --git a/lib/datagrid/filters/composite_filters.rb b/lib/datagrid/filters/composite_filters.rb deleted file mode 100644 index afa48d66..00000000 --- a/lib/datagrid/filters/composite_filters.rb +++ /dev/null @@ -1,49 +0,0 @@ -# frozen_string_literal: true - -module Datagrid - module Filters - # @!visibility private - module CompositeFilters - def self.included(base) - base.extend ClassMethods - base.class_eval do - end - end - - # @!visibility private - module ClassMethods - def date_range_filters(field, from_options = {}, to_options = {}) - Utils.warn_once("date_range_filters is deprecated in favor of range option for date filter") - from_options = normalize_composite_filter_options(from_options, field) - to_options = normalize_composite_filter_options(to_options, field) - - filter(from_options[:name] || :"from_#{field.to_s.tr('.', '_')}", :date, -**from_options,) do |date, scope, grid| - grid.driver.greater_equal(scope, field, date) - end - filter(to_options[:name] || :"to_#{field.to_s.tr('.', '_')}", :date, **to_options) do |date, scope, grid| - grid.driver.less_equal(scope, field, date) - end - end - - def integer_range_filters(field, from_options = {}, to_options = {}) - Utils.warn_once("integer_range_filters is deprecated in favor of range option for integer filter") - from_options = normalize_composite_filter_options(from_options, field) - to_options = normalize_composite_filter_options(to_options, field) - filter(from_options[:name] || :"from_#{field.to_s.tr('.', '_')}", :integer, -**from_options,) do |value, scope, grid| - grid.driver.greater_equal(scope, field, value) - end - filter(to_options[:name] || :"to_#{field.to_s.tr('.', '_')}", :integer, **to_options) do |value, scope, grid| - grid.driver.less_equal(scope, field, value) - end - end - - def normalize_composite_filter_options(options, _field) - options = { name: options } if options.is_a?(String) || options.is_a?(Symbol) - options - end - end - end - end -end diff --git a/lib/datagrid/filters/date_filter.rb b/lib/datagrid/filters/date_filter.rb index 12057178..e8462268 100644 --- a/lib/datagrid/filters/date_filter.rb +++ b/lib/datagrid/filters/date_filter.rb @@ -7,8 +7,12 @@ module Filters class DateFilter < Datagrid::Filters::BaseFilter include Datagrid::Filters::RangedFilter + def default_input_options + { **super, type: "date" } + end + def apply(grid_object, scope, value) - value = value.begin&.beginning_of_day..value.end&.end_of_day if value.is_a?(Range) + value = Datagrid::Utils.format_date_as_timestamp(value) if grid_object.driver.timestamp_column?(scope, name) super end @@ -24,11 +28,6 @@ def format(value) end end - def default_filter_where(scope, value) - value = Datagrid::Utils.format_date_as_timestamp(value) if driver.timestamp_column?(scope, name) - super - end - protected def formats diff --git a/lib/datagrid/filters/date_time_filter.rb b/lib/datagrid/filters/date_time_filter.rb index 0014eb1d..55e4e44d 100644 --- a/lib/datagrid/filters/date_time_filter.rb +++ b/lib/datagrid/filters/date_time_filter.rb @@ -7,6 +7,10 @@ module Filters class DateTimeFilter < Datagrid::Filters::BaseFilter include Datagrid::Filters::RangedFilter + def default_input_options + { **super, type: "datetime-local" } + end + def parse(value) Datagrid::Utils.parse_datetime(value) end diff --git a/lib/datagrid/filters/dynamic_filter.rb b/lib/datagrid/filters/dynamic_filter.rb index 7487ab8a..967a37d4 100644 --- a/lib/datagrid/filters/dynamic_filter.rb +++ b/lib/datagrid/filters/dynamic_filter.rb @@ -19,28 +19,29 @@ class DynamicFilter < Datagrid::Filters::BaseFilter ].freeze AVAILABLE_OPERATIONS = %w[= =~ >= <=].freeze - def initialize(*) - super + def initialize(grid, name, **options, &block) options[:select] ||= default_select options[:operations] ||= DEFAULT_OPERATIONS - return if options.key?(:include_blank) + options[:include_blank] = false unless options.key?(:include_blank) + super + end - options[:include_blank] = false + def default_input_options + { **super, type: nil } end def parse_values(filter) - field, operation, value = filter - - [field, operation, type_cast(field, value)] + filter ? FilterValue.new(grid_class, filter) : nil end def unapplicable_value?(filter) - _, _, value = filter - super(value) + super(filter&.value) end def default_filter_where(scope, filter) - field, operation, value = filter + field = filter.field + operation = filter.operation + value = filter.value date_conversion = value.is_a?(Date) && driver.timestamp_column?(scope, field) return scope if field.blank? || operation.blank? @@ -79,7 +80,7 @@ def operations def operations_select operations.map do |operation| - [I18n.t(operation, scope: "datagrid.filters.dynamic.operations").html_safe, operation] + [I18n.t(operation, scope: "datagrid.filters.dynamic.operations"), operation] end end @@ -95,33 +96,76 @@ def default_select } end - def type_cast(field, value) - type = column_type(field) - return nil if value.blank? - - case type - when :string - value.to_s - when :integer - value.is_a?(Numeric) || value =~ %r{^\d} ? value.to_i : nil - when :float - value.is_a?(Numeric) || value =~ %r{^\d} ? value.to_f : nil - when :date - Datagrid::Utils.parse_date(value) - when :timestamp - Datagrid::Utils.parse_date(value) - when :boolean - Datagrid::Utils.booleanize(value) - when nil - value - else - raise NotImplementedError, "unknown column type: #{type.inspect}" - end - end - def column_type(field) grid_class.driver.normalized_column_type(grid_class.scope, field) end + + class FilterValue + attr_accessor :field, :operation, :value + + def initialize(grid_class, object = nil) + super() + + case object + when Hash + object = object.symbolize_keys + self.field = object[:field] + self.operation = object[:operation] + self.value = object[:value] + when Array + self.field = object[0] + self.operation = object[1] + self.value = object[2] + else + raise ArgumentError, object.inspect + end + return unless grid_class + + type = grid_class.driver.normalized_column_type( + grid_class.scope, field, + ) + self.value = type_cast(type, value) + end + + def inspect + { field: field, operation: operation, value: value } + end + + def to_ary + to_a + end + + def to_a + [field, operation, value] + end + + def to_h + { field: field, operation: operation, value: value } + end + + protected + + def type_cast(type, value) + return nil if value.blank? + + case type + when :string + value.to_s + when :integer + value.is_a?(Numeric) || value =~ %r{^\d} ? value.to_i : nil + when :float + value.is_a?(Numeric) || value =~ %r{^\d} ? value.to_f : nil + when :date, :timestamp + Datagrid::Utils.parse_date(value) + when :boolean + Datagrid::Utils.booleanize(value) + when nil + value + else + raise NotImplementedError, "unknown column type: #{type.inspect}" + end + end + end end end end diff --git a/lib/datagrid/filters/enum_filter.rb b/lib/datagrid/filters/enum_filter.rb index d05b215e..d21d6f49 100644 --- a/lib/datagrid/filters/enum_filter.rb +++ b/lib/datagrid/filters/enum_filter.rb @@ -7,9 +7,10 @@ module Filters class EnumFilter < Datagrid::Filters::BaseFilter include Datagrid::Filters::SelectOptions - def initialize(*args) + # @!visibility private + def initialize(grid, name, **options, &block) + options[:multiple] = true if options[:checkboxes] super - options[:multiple] = true if checkboxes? raise Datagrid::ConfigurationError, ":select option not specified" unless options[:select] end @@ -19,11 +20,31 @@ def parse(value) value end + def default_input_options + { + **super, + type: enum_checkboxes? ? "checkbox" : "select", + multiple: multiple?, + include_hidden: enum_checkboxes? ? false : nil, + } + end + + def label_options + if enum_checkboxes? + # Each checkbox has its own label + # The main label has no specific input to focus + # See app/views/datagrid/_enum_checkboxes.html.erb + { for: nil, **super } + else + super + end + end + def strict options[:strict] end - def checkboxes? + def enum_checkboxes? options[:checkboxes] end end diff --git a/lib/datagrid/filters/extended_boolean_filter.rb b/lib/datagrid/filters/extended_boolean_filter.rb index e85181c1..dbd68958 100644 --- a/lib/datagrid/filters/extended_boolean_filter.rb +++ b/lib/datagrid/filters/extended_boolean_filter.rb @@ -8,7 +8,7 @@ class ExtendedBooleanFilter < Datagrid::Filters::EnumFilter TRUTH_VALUES = [true, "true", "y", "yes"].freeze FALSE_VALUES = [false, "false", "n", "no"].freeze - def initialize(report, attribute, options = {}, &block) + def initialize(*args, **options) options[:select] = -> { boolean_select } super end @@ -18,6 +18,10 @@ def execute(value, scope, grid_object) super end + def default_input_options + { **super, type: "select" } + end + def parse(value) value = value.downcase if value.is_a?(String) case value diff --git a/lib/datagrid/filters/float_filter.rb b/lib/datagrid/filters/float_filter.rb index d84f6e12..d6b586c5 100644 --- a/lib/datagrid/filters/float_filter.rb +++ b/lib/datagrid/filters/float_filter.rb @@ -5,6 +5,10 @@ module Filters class FloatFilter < Datagrid::Filters::BaseFilter include Datagrid::Filters::RangedFilter + def default_input_options + { **super, type: "number", step: "any" } + end + def parse(value) return nil if value.blank? diff --git a/lib/datagrid/filters/integer_filter.rb b/lib/datagrid/filters/integer_filter.rb index 6395b0be..26146ffc 100644 --- a/lib/datagrid/filters/integer_filter.rb +++ b/lib/datagrid/filters/integer_filter.rb @@ -7,6 +7,10 @@ module Filters class IntegerFilter < Datagrid::Filters::BaseFilter include Datagrid::Filters::RangedFilter + def default_input_options + { **super, type: "number", step: "1" } + end + def parse(value) return nil if value.blank? if defined?(ActiveRecord) && value.is_a?(ActiveRecord::Base) && @@ -15,6 +19,8 @@ def parse(value) end return value if value.is_a?(Range) + return nil if value.to_i.zero? && value.is_a?(String) && value !~ %r{\A\s*-?0} + value.to_i end end diff --git a/lib/datagrid/filters/ranged_filter.rb b/lib/datagrid/filters/ranged_filter.rb index 9fa95d2d..5b3fac00 100644 --- a/lib/datagrid/filters/ranged_filter.rb +++ b/lib/datagrid/filters/ranged_filter.rb @@ -3,35 +3,29 @@ module Datagrid module Filters module RangedFilter - def initialize(grid, name, options, &block) - super - return unless range? - - options[:multiple] = true - end + SERIALIZED_RANGE = %r{\A(.*)\.{2,3}(.*)\z} def parse_values(value) - result = super - return result if !range? || result.nil? - # Simulate single point range - return [result, result] unless result.is_a?(Array) + return super unless range? - case result.size - when 0 - nil - when 1 - result.first - when 2 - if result.first && result.last && result.first > result.last - # If wrong range is given - reverse it to be always valid - result.reverse - elsif !result.first && !result.last + case value + when String + if ["..", "..."].include?(value) nil + elsif (match = value.match(SERIALIZED_RANGE)) + to_range(match.captures[0], match.captures[1], value.include?("...")) else - result + super end + when Hash + parse_hash(value) + when Array + parse_array(value) + when Range + to_range(value.begin, value.end) else - raise ArgumentError, "Can not create a date range from array of more than two: #{result.inspect}" + result = super + to_range(result, result) end end @@ -40,15 +34,44 @@ def range? end def default_filter_where(scope, value) - if range? && value.is_a?(Array) - left, right = value - scope = driver.greater_equal(scope, name, left) if left - scope = driver.less_equal(scope, name, right) if right + if range? && value.is_a?(Range) + scope = driver.greater_equal(scope, name, value.begin) if value.begin + scope = driver.less_equal(scope, name, value.end) if value.end scope else super end end + + protected + + def parse_hash(result) + to_range(result[:from] || result["from"], result[:to] || result["to"]) + end + + def to_range(from, to, exclusive = false) + from = parse(from) + to = parse(to) + return nil unless to || from + + # If wrong range is given - reverse it to be always valid + from, to = to, from if from && to && from > to + exclusive ? from...to : from..to + end + + def parse_array(result) + first = result.first + last = result.last + + case result.size + when 0 + nil + when 1, 2 + to_range(first, last) + else + raise ArgumentError, "Can not create a range from array of more than two elements" + end + end end end end diff --git a/lib/datagrid/form_builder.rb b/lib/datagrid/form_builder.rb index 8bfdf61d..49106214 100644 --- a/lib/datagrid/form_builder.rb +++ b/lib/datagrid/form_builder.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "action_view" +require "datagrid/deprecated_object" module Datagrid module FormBuilder @@ -10,11 +11,17 @@ module FormBuilder # * select for enum, xboolean filter types # * check_box for boolean filter type # * text_field for other filter types - # @param [String] partials deprecated option - def datagrid_filter(filter_or_attribute, partials: nil, **options, &block) + def datagrid_filter(filter_or_attribute, **options, &block) filter = datagrid_get_filter(filter_or_attribute) - options = add_html_classes({ **filter.input_options, **options }, filter.name, datagrid_filter_html_class(filter)) - send(filter.form_builder_helper_name, filter, **options, &block) + if filter.range? + datagrid_range_filter(filter, options, &block) + elsif filter.enum_checkboxes? + datagrid_enum_checkboxes_filter(filter, options, &block) + elsif filter.type == :dynamic + datagrid_dynamic_filter(filter, options, &block) + else + datagrid_filter_input(filter, **options, &block) + end end # @param filter_or_attribute [Datagrid::Filters::BaseFilter, String, Symbol] filter object or filter name @@ -23,7 +30,8 @@ def datagrid_filter(filter_or_attribute, partials: nil, **options, &block) # @return [String] a form label tag for the corresponding filter name def datagrid_label(filter_or_attribute, text = nil, **options, &block) filter = datagrid_get_filter(filter_or_attribute) - label(filter.name, text || filter.header, **filter.label_options, **options, &block) + options = { **filter.label_options, **options } + label(filter.name, text || filter.header, **options, &block) end # @param [Datagrid::Filters::BaseFilter, String, Symbol] attribute_or_filter filter object or filter name @@ -31,66 +39,34 @@ def datagrid_label(filter_or_attribute, text = nil, **options, &block) # * `type` - special attribute the determines an input tag to be made. # Examples: `text`, `select`, `textarea`, `number`, `date` etc. # @return [String] an input tag for the corresponding filter name - def datagrid_filter_input(attribute_or_filter, **options) + def datagrid_filter_input(attribute_or_filter, **options, &block) filter = datagrid_get_filter(attribute_or_filter) - value = object.filter_value_as_string(filter) - type = options[:type]&.to_sym - if options.key?(:value) && options[:value].nil? && %i[datetime-local date].include?(type) - # https://github.com/rails/rails/pull/53387 - options[:value] = "" + options = add_filter_options(filter, **options) + type = options.delete(:type)&.to_sym + if %i[datetime-local date].include?(type) + if options.key?(:value) && options[:value].nil? + # https://github.com/rails/rails/pull/53387 + options[:value] = "" + end + elsif options[:value] + options[:value] = filter.format(options[:value]) end case type when :"datetime-local" - datetime_local_field filter.name, **options + datetime_local_field filter.name, **options, &block when :date - date_field filter.name, **options + date_field filter.name, **options, &block when :textarea - text_area filter.name, value: value, **options, type: nil - else - text_field filter.name, value: value, **options - end - end - - protected - - def datagrid_extended_boolean_filter(filter, options = {}) - datagrid_enum_filter(filter, options) - end - - def datagrid_boolean_filter(filter, options = {}) - check_box(filter.name, options) - end - - def datagrid_date_filter(filter, options = {}) - datagrid_range_filter(:date, filter, options) - end - - def datagrid_date_time_filter(filter, options = {}) - datagrid_range_filter(:datetime, filter, options) - end - - def datagrid_default_filter(filter, options = {}) - datagrid_filter_input(filter, **options) - end - - def datagrid_enum_filter(filter, options = {}, &block) - if filter.checkboxes? - options = add_html_classes(options, "checkboxes") - elements = object.select_options(filter).map do |element| - text, value = @template.send(:option_text_and_value, element) - checked = enum_checkbox_checked?(filter, value) - [value, text, checked] - end - render_partial( - "enum_checkboxes", - { - elements: elements, - form: self, - filter: filter, - options: options, - }, - ) - else + text_area filter.name, value: object.filter_value_as_string(filter), **options, &block + when :checkbox + value = options.fetch(:value, 1).to_s + options = { checked: true, **options } if filter.enum_checkboxes? && enum_checkbox_checked?(filter, value) + check_box filter.name, options, value + when :hidden + hidden_field filter.name, **options + when :number + number_field filter.name, **options + when :select select( filter.name, object.select_options(filter) || [], @@ -103,11 +79,44 @@ def datagrid_enum_filter(filter, options = {}, &block) **options, &block ) + else + text_field filter.name, value: object.filter_value_as_string(filter), **options, &block end end + protected + + def datagrid_enum_checkboxes_filter(filter, options = {}) + elements = object.select_options(filter).map do |element| + text, value = @template.send(:option_text_and_value, element) + checked = enum_checkbox_checked?(filter, value) + [value, text, checked] + end + choices = elements.map do |value, text, *_| + [value, text] + end + render_partial( + "enum_checkboxes", + { + form: self, + elements: Datagrid::DeprecatedObject.new( + elements, + ) do + Datagrid::Utils.warn_once( + <<~MSG, + Using `elements` variable in enum_checkboxes view is deprecated, use `choices` instead. + MSG + ) + end, + choices: choices, + filter: filter, + options: options, + }, + ) + end + def enum_checkbox_checked?(filter, option_value) - current_value = object.public_send(filter.name) + current_value = object.filter_value(filter) if current_value.respond_to?(:include?) # Typecast everything to string # to remove difference between String and Symbol @@ -117,15 +126,9 @@ def enum_checkbox_checked?(filter, option_value) end end - def datagrid_integer_filter(filter, options = {}) - options[:value] = "" if filter.multiple? && object[filter.name].blank? - datagrid_range_filter(:integer, filter, options) - end - def datagrid_dynamic_filter(filter, options = {}) - input_name = "#{object_name}[#{filter.name}][]" field, operation, value = object.filter_value(filter) - options = options.merge(name: input_name) + options = add_filter_options(filter, **options) field_input = dynamic_filter_select( filter.name, object.select_options(filter) || [], @@ -135,7 +138,8 @@ def datagrid_dynamic_filter(filter, options = {}) include_hidden: false, selected: field, }, - add_html_classes(options, "field"), + **add_html_classes(options, "datagrid-dynamic-field"), + name: @template.field_name(object_name, filter.name, "field"), ) operation_input = dynamic_filter_select( filter.name, filter.operations_select, @@ -145,9 +149,15 @@ def datagrid_dynamic_filter(filter, options = {}) prompt: false, selected: operation, }, - add_html_classes(options, "operation"), + **add_html_classes(options, "datagrid-dynamic-operation"), + name: @template.field_name(object_name, filter.name, "operation"), + ) + value_input = datagrid_filter_input( + filter.name, + **add_html_classes(options, "datagrid-dynamic-value"), + value: value, + name: @template.field_name(object_name, filter.name, "value"), ) - value_input = text_field(filter.name, **add_html_classes(options, "value"), value: value) [field_input, operation_input, value_input].join("\n").html_safe end @@ -163,56 +173,26 @@ def dynamic_filter_select(name, variants, select_options, html_options) end end - def datagrid_range_filter(_type, filter, options = {}) - if filter.range? - options = options.merge(multiple: true) - from_options = datagrid_range_filter_options(object, filter, :from, options) - to_options = datagrid_range_filter_options(object, filter, :to, options) - render_partial "range_filter", { - from_options: from_options, to_options: to_options, filter: filter, form: self, - } - else - datagrid_filter_input(filter, **options) - end + def datagrid_range_filter(filter, options = {}) + from_options = datagrid_range_filter_options(object, filter, :from, **options) + to_options = datagrid_range_filter_options(object, filter, :to, **options) + render_partial "range_filter", { + from_options: from_options, to_options: to_options, filter: filter, form: self, + } end - def datagrid_range_filter_options(object, filter, type, options) - type_method_map = { from: :first, to: :last } - options = add_html_classes(options, type) - options[:value] = filter.format(object[filter.name]&.public_send(type_method_map[type])) - # In case of datagrid ranged filter - # from and to input will have same id - if !options.key?(:id) - # Rails provides it's own default id for all inputs - # In order to prevent that we assign no id by default - options[:id] = nil - elsif options[:id].present? - # If the id was given we prefix it - # with from_ and to_ accordingly - options[:id] = [type, options[:id]].join("_") - end + def datagrid_range_filter_options(object, filter, section, **options) + type_method_map = { from: :begin, to: :end } + options[:value] = object[filter.name]&.public_send(type_method_map[section]) + options[:name] = @template.field_name(object_name, filter.name, section) options end - def datagrid_string_filter(filter, options = {}) - datagrid_range_filter(:string, filter, options) - end - - def datagrid_float_filter(filter, options = {}) - datagrid_range_filter(:float, filter, options) - end - def datagrid_get_filter(attribute_or_filter) - if Utils.string_like?(attribute_or_filter) - object.class.filter_by_name(attribute_or_filter) || - raise(Error, "Datagrid filter #{attribute_or_filter} not found") - else - attribute_or_filter - end - end + return attribute_or_filter unless Utils.string_like?(attribute_or_filter) - def datagrid_filter_html_class(filter) - filter.class.to_s.demodulize.underscore + object.class.filter_by_name(attribute_or_filter) || + raise(ArgumentError, "Datagrid filter #{attribute_or_filter} not found") end def add_html_classes(options, *classes) @@ -232,7 +212,8 @@ def render_partial(name, locals) @template.render partial: partial_path(name), locals: locals end - class Error < StandardError + def add_filter_options(filter, **options) + { **filter.default_input_options, **filter.input_options, **options } end end end diff --git a/lib/datagrid/generators/scaffold.rb b/lib/datagrid/generators/scaffold.rb new file mode 100644 index 00000000..7b387679 --- /dev/null +++ b/lib/datagrid/generators/scaffold.rb @@ -0,0 +1,184 @@ +# frozen_string_literal: true + +require "rails/generators" + +module Datagrid + # @!visibility private + module Generators + # @!visibility private + class Scaffold < Rails::Generators::NamedBase + include Rails::Generators::ResourceHelpers + + check_class_collision suffix: "Grid" + source_root File.expand_path("#{__FILE__}/../../../templates") + + def create_scaffold + template "base.rb.erb", base_grid_file unless file_exists?(base_grid_file) + template "grid.rb.erb", "app/grids/#{grid_class_name.underscore}.rb" + if file_exists?(grid_controller_file) + inject_into_file grid_controller_file, index_action, after: %r{class .*#{grid_controller_class_name}.*\n} + else + create_file grid_controller_file, controller_code + end + create_file view_file, view_code + route(generate_routing_namespace("resources :#{grid_controller_short_name}")) + gem "kaminari" unless kaminari? || will_paginate? || pagy? + in_root do + { + "css" => " *= require datagrid", + "css.sass" => " *= require datagrid", + "css.scss" => " *= require datagrid", + }.each do |extension, string| + file = "app/assets/stylesheets/application.#{extension}" + if file_exists?(file) + inject_into_file file, "#{string}\n", { before: %r{.*require_self} } # before all + end + end + end + end + + def view_file + Rails.root.join("app/views").join(controller_file_path).join("index.html.erb") + end + + def grid_class_name + "#{file_name.camelize.pluralize}Grid" + end + + def grid_base_class + file_exists?("app/grids/base_grid.rb") ? "BaseGrid" : "ApplicationGrid" + end + + def grid_controller_class_name + "#{controller_class_name.camelize}Controller" + end + + def grid_controller_file + Rails.root.join("app/controllers").join("#{grid_controller_class_name.underscore}.rb") + end + + def grid_controller_short_name + controller_file_name + end + + def grid_model_name + file_name.camelize.singularize + end + + def grid_param_name + grid_class_name.underscore + end + + def pagination_helper_code + if will_paginate? + "will_paginate(@grid.assets)" + elsif pagy? + "pagy_nav(@pagy)" + else + # Kaminari is default + "paginate(@grid.assets)" + end + end + + def table_helper_code + if pagy? + "datagrid_table @grid, @records" + else + "datagrid_table @grid" + end + end + + def base_grid_file + "app/grids/application_grid.rb" + end + + def grid_route_name + "#{controller_class_name.underscore.gsub('/', '_')}_path" + end + + def index_code + if pagy? + <<-RUBY + @grid = #{grid_class_name}.new(grid_params) + @pagy, @assets = pagy(@grid.assets) + RUBY + else + <<-RUBY + @grid = #{grid_class_name}.new(grid_params) do |scope| + scope.page(params[:page]) + end + RUBY + end + end + + def controller_code + <<~RUBY + class #{grid_controller_class_name} < ApplicationController + def index + #{index_code.rstrip} + end + + protected + + def grid_params + params.fetch(:#{grid_param_name}, {}).permit! + end + end + RUBY + end + + def view_code + <<~ERB + <%= datagrid_form_with model: @grid, url: #{grid_route_name} %> + + <%= #{pagination_helper_code} %> + <%= #{table_helper_code} %> + <%= #{pagination_helper_code} %> + ERB + end + + protected + + def generate_routing_namespace(code) + depth = regular_class_path.length + # Create 'namespace' ladder + # namespace :foo do + # namespace :bar do + namespace_ladder = regular_class_path.each_with_index.map do |ns, i| + indent("namespace :#{ns} do\n", i * 2) + end.join + + # Create route + # get 'baz/index' + route = indent(code, depth * 2) + + # Create `end` ladder + # end + # end + end_ladder = (1..depth).reverse_each.map do |i| + indent("end\n", i * 2) + end.join + + # Combine the 3 parts to generate complete route entry + "#{namespace_ladder}#{route}\n#{end_ladder}" + end + + def file_exists?(name) + name = Rails.root.join(name) unless name.to_s.first == "/" + File.exist?(name) + end + + def pagy? + defined?(::Pagy) + end + + def will_paginate? + defined?(::WillPaginate) + end + + def kaminari? + defined?(::Kaminari) + end + end + end +end diff --git a/lib/datagrid/generators/views.rb b/lib/datagrid/generators/views.rb new file mode 100644 index 00000000..3f00a351 --- /dev/null +++ b/lib/datagrid/generators/views.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Datagrid + module Generators + class Views < Rails::Generators::Base + source_root File.expand_path("../../../app/views/datagrid", __dir__) + + desc "Copies Datagrid partials to your application." + def copy_views + Dir.glob(File.join(self.class.source_root, "**", "*")).each do |file_path| + relative_path = file_path.sub("#{self.class.source_root}/", "") + + next if relative_path == "_order_for.html.erb" + + copy_file(relative_path, File.join("app/views/datagrid", relative_path)) + end + end + end + end +end diff --git a/lib/datagrid/helper.rb b/lib/datagrid/helper.rb index 24c25eb7..5a1bc374 100644 --- a/lib/datagrid/helper.rb +++ b/lib/datagrid/helper.rb @@ -17,7 +17,8 @@ module Datagrid # [built-in CSS](https://github.com/bogdan/datagrid/blob/master/app/assets/stylesheets/datagrid.sass). # # Datagrid includes helpers and a form builder for easy frontend generation. - # If you need a fully-featured custom GUI, create your templates manually with the help of the {Datagrid::Columns} API. + # If you need a fully-featured custom GUI, create your templates manually + # with the help of the {Datagrid::Columns} API. # # ## Controller and Routing # @@ -57,16 +58,17 @@ module Datagrid # # Use the built-in partial: # - # = datagrid_form_for @grid, url: report_path, other_form_for_option: value + # = datagrid_form_with model: @grid, url: report_path, other_form_for_option: value # - # {#datagrid_form_for} supports the same options as Rails `form_for`. + # {#datagrid_form_with} supports the same options as Rails `form_with`. # # ### Advanced Method # - # You can use Rails built-in tools to create a form. Additionally, Datagrid provides helpers to generate input/select elements for filters: + # You can use Rails built-in tools to create a form. + # Additionally, Datagrid provides helpers to generate input/select elements for filters: # # ``` haml - # - form_for UserGrid.new, method: :get, url: users_path do |f| + # - form_with model: UserGrid.new, method: :get, url: users_path do |f| # %div # = f.datagrid_label :name # = f.datagrid_filter :name # => @@ -152,7 +154,7 @@ module Datagrid # # Modify the form for AJAX: # - # = datagrid_form_for @grid, html: {class: 'js-datagrid-form'} + # = datagrid_form_with model: @grid, html: {class: 'js-datagrid-form'} # .js-datagrid-table # = datagrid_table @grid # .js-pagination @@ -175,9 +177,8 @@ module Datagrid # # app/views/datagrid/ # ├── _enum_checkboxes.html.erb # datagrid_filter for filter(name, :enum, checkboxes: true) - # ├── _form.html.erb # datagrid_form_for + # ├── _form.html.erb # datagrid_form_with # ├── _head.html.erb # datagrid_header - # ├── _order_for.html.erb # datagrid_order_for # ├── _range_filter.html.erb # datagrid_filter for filter(name, type, range: true) # ├── _row.html.erb # datagrid_rows/datagrid_rows # └── _table.html.erb # datagrid_table @@ -194,18 +195,14 @@ module Datagrid # category.orders.sum(:subtotal) / category.orders.count # end # - # The `:description` option is not built into Datagrid, but you can implement it by modifying the column header - # partial `app/views/datagrid/_header.html.erb` like this: + # The `:description` option is not built into Datagrid, but you can implement it + # by adding the following to partial `app/views/datagrid/_header.html.erb`: # - # %tr - # - grid.html_columns(*options[:columns]).each do |column| - # %th{class: datagrid_column_classes(grid, column)} - # = column.header - # - if column.options[:description] - # %a{data: {toggle: 'tooltip', title: column.options[:description]}} - # %i.icon-question-sign - # - if column.order && options[:order] - # = datagrid_order_for(grid, column, options) + # <% if column.options[:description] %> + # + # + # + # <% end %> # # This modification allows the `:description` tooltip to work with your chosen UI and JavaScript library. # The same technique can be applied to filters by calling `filter.options` in corresponding partials. @@ -226,8 +223,7 @@ module Datagrid # # This allows you to define a custom `row_class` method in your grid class, like this: # - # class IssuesGrid - # include Datagrid + # class IssuesGrid < ApplicationGrid # scope { Issue } # # def row_class(issue) @@ -246,7 +242,7 @@ module Datagrid # # https://github.com/bogdan/datagrid/blob/master/lib/datagrid/locale/en.yml module Helper - # @param grid [Datagrid] grid object + # @param grid [Datagrid::Base] grid object # @param column [Datagrid::Columns::Column, String, Symbol] column name # @param model [Object] an object from grid scope # @return [Object] individual cell value from the given grid, column name and model @@ -257,7 +253,9 @@ module Helper # <% end %> # def datagrid_value(grid, column, model) - datagrid_renderer.format_value(grid, column, model) + column = grid.column_by_name(column) if column.is_a?(String) || column.is_a?(Symbol) + + grid.html_value(column, self, model) end # @!visibility private @@ -268,7 +266,7 @@ def datagrid_format_value(grid, column, model) # Renders html table with columns defined in grid class. # In the most common used you need to pass paginated collection # to datagrid table because datagrid do not have pagination compatibilities: - # @param grid [Datagrid] grid object + # @param grid [Datagrid::Base] grid object # @param assets [Array] objects from grid scope # @param [Hash{Symbol => Object}] options HTML attributes to be passed to `` tag # @option options [Hash] html A hash of attributes for the `
` tag. @@ -283,7 +281,14 @@ def datagrid_format_value(grid, column, model) # assets = grid.assets.page(params[:page]) # datagrid_table(grid, assets, options) def datagrid_table(grid, assets = grid.assets, **options) - datagrid_renderer.table(grid, assets, **options) + _render_partial( + "table", options[:partials], + { + grid: grid, + options: options, + assets: assets, + }, + ) end # Renders HTML table header for given grid instance using columns defined in it @@ -295,11 +300,19 @@ def datagrid_table(grid, assets = grid.assets, **options) # Default: all defined columns. # @option options [String] partials The path for partials lookup. # Default: `'datagrid'`. - # @param grid [Datagrid] grid object + # @param grid [Datagrid::Base] grid object + # @param [Object] opts (deprecated) pass keyword arguments instead # @param [Hash] options # @return [String] HTML table header tag markup - def datagrid_header(grid, options = {}) - datagrid_renderer.header(grid, options) + def datagrid_header(grid, opts = :__unspecified__, **options) + unless opts == :__unspecified__ + Datagrid::Utils.warn_once("datagrid_header now requires ** operator when passing options.") + options.reverse_merge!(opts) + end + options[:order] = true unless options.key?(:order) + + _render_partial("head", options[:partials], + { grid: grid, options: options },) end # Renders HTML table rows using given grid definition using columns defined in it. @@ -318,34 +331,78 @@ def datagrid_header(grid, options = {}) # %tr # %td= row.project_name # %td.project-status{class: row.status}= row.status + # @param [Datagrid::Base] grid datagrid object + # @param [Array] assets assets as per defined in grid scope def datagrid_rows(grid, assets = grid.assets, **options, &block) - datagrid_renderer.rows(grid, assets, **options, &block) + safe_join( + assets.map do |asset| + datagrid_row(grid, asset, **options, &block) + end.to_a, + ) end # @return [String] renders ordering controls for the given column name # @option options [String] partials The path for partials lookup. # Default: `'datagrid'`. + # @param [Datagrid::Base] grid datagrid object + # @param [Datagrid::Columns::Column] column + # @deprecated Put necessary code inline inside datagrid/head partial. + # See built-in partial for example. def datagrid_order_for(grid, column, options = {}) - datagrid_renderer.order_for(grid, column, options) + Datagrid::Utils.warn_once(<<~MSG) + datagrid_order_for is deprecated. + Put necessary code inline inside datagrid/head partial. + See built-in partial for example. + MSG + _render_partial("order_for", options[:partials], + { grid: grid, column: column },) + end + + # Renders HTML for grid with all filters inputs and labels defined in it + # @option options [String] partials Path for form partial lookup. + # Default: `'datagrid'`, which uses `app/views/datagrid/` partials. + # Example: `'datagrid_admin'` uses `app/views/datagrid_admin` partials. + # @option options [Datagrid::Base] model a Datagrid object to be rendered. + # @option options [Hash] All options supported by Rails `form_with` helper. + # @param [Hash{Symbol => Object}] options + # @return [String] form HTML tag markup + def datagrid_form_with(**options) + raise ArgumentError, "datagrid_form_with block argument is invalid. Use form_with instead." if block_given? + + grid = options[:model] + raise ArgumentError, "Grid has no available filters" if grid&.filters&.empty? + + _render_partial("form", options[:partials], { grid: options[:model], options: options }) end - # Renders HTML for for grid with all filters inputs and lables defined in it + # Renders HTML for grid with all filters inputs and labels defined in it # # Supported options: # # * :partials - Path for form partial lookup. # Default: 'datagrid'. - # * All options supported by Rails form_for helper - # @param grid [Datagrid] grid object + # * All options supported by Rails form_with helper + # @deprecated Use {#datagrid_form_with} instead. + # @param grid [Datagrid::Base] grid object # @param [Hash] options # @return [String] form HTML tag markup def datagrid_form_for(grid, options = {}) - datagrid_renderer.form_for(grid, options) + Datagrid::Utils.warn_once("datagrid_form_for is deprecated if favor of datagrid_form_with.") + _render_partial( + "form", options[:partials], + grid: grid, + options: { + method: :get, + as: grid.param_name, + local: true, + **options, + }, + ) end # Provides access to datagrid columns data. # Used in case you want to build html table completelly manually - # @param grid [Datagrid] grid object + # @param grid [Datagrid::Base] grid object # @param asset [Object] object from grid scope # @param block [Proc] block with Datagrid::Helper::HtmlRow as an argument returning a HTML markup as a String # @param [Hash{Symbol => Object}] options @@ -364,29 +421,105 @@ def datagrid_form_for(grid, options = {}) # First Name: <%= row.first_name %> # Last Name: <%= row.last_name %> def datagrid_row(grid, asset, **options, &block) - datagrid_renderer.row(grid, asset, **options, &block) + Datagrid::Helper::HtmlRow.new(self, grid, asset, options).tap do |row| + return capture(row, &block) if block_given? + end end # Generates an ascending or descending order url for the given column - # @param grid [Datagrid] grid object + # @param grid [Datagrid::Base] grid object # @param column [Datagrid::Columns::Column, String, Symbol] column name # @param descending [Boolean] order direction, descending if true, otherwise ascending. # @return [String] order layout HTML markup def datagrid_order_path(grid, column, descending) - datagrid_renderer.order_path(grid, column, descending, request) - end - - protected - - def datagrid_renderer - Renderer.for(self) + column = grid.column_by_name(column) + query = request&.query_parameters || {} + ActionDispatch::Http::URL.path_for( + path: request&.path || "/", + params: query.merge(grid.query_params(order: column.name, descending: descending)), + ) end + # @!visibility private def datagrid_column_classes(grid, column) + Datagrid::Utils.warn_once(<<~MSG) + datagrid_column_classes is deprecated. Assign necessary classes manually. + Correspond to default datagrid/rows partial for example.) + MSG + column = grid.column_by_name(column) order_class = if grid.ordered_by?(column) ["ordered", grid.descending ? "desc" : "asc"] end - [column.name, order_class, column.options[:class]].compact.join(" ") + class_names(column.name, order_class, column.options[:class], column.tag_options[:class]) + end + + protected + + def _render_partial(partial_name, partials_path, locals = {}) + render({ + partial: File.join(partials_path || "datagrid", partial_name), + locals: locals, + }) + end + + # Represents a datagrid row that provides access to column values for the given asset + # @example + # row = datagrid_row(grid, user) + # row.class # => Datagrid::Helper::HtmlRow + # row.first_name # => "Bogdan" + # row.grid # => Datagrid::Base object + # row.asset # => User object + # row.each do |value| + # puts value + # end + class HtmlRow + include Enumerable + + attr_reader :grid, :asset, :options + + # @!visibility private + def initialize(renderer, grid, asset, options) + @renderer = renderer + @grid = grid + @asset = asset + @options = options + end + + # @return [Object] a column value for given column name + def get(column) + @renderer.datagrid_value(@grid, column, @asset) + end + + # Iterates over all column values that are available in the row + # param block [Proc] column value iterator + def each(&block) + (@options[:columns] || @grid.html_columns).each do |column| + block.call(get(column)) + end + end + + # @return [String] HTML row format + def to_s + @renderer.send(:_render_partial, "row", options[:partials], { + grid: grid, + options: options, + asset: asset, + },) + end + + protected + + def method_missing(method, *args, &blk) + if (column = @grid.column_by_name(method)) + get(column) + else + super + end + end + + def respond_to_missing?(method, include_private = false) + !!@grid.column_by_name(method) || super + end end end end diff --git a/lib/datagrid/ordering.rb b/lib/datagrid/ordering.rb index 4619dacc..8b8ebc27 100644 --- a/lib/datagrid/ordering.rb +++ b/lib/datagrid/ordering.rb @@ -7,6 +7,7 @@ module Datagrid class OrderUnsupported < StandardError end + # Module adds support for ordering by defined columns for Datagrid. module Ordering # @!visibility private def self.included(base) @@ -51,9 +52,11 @@ def order_column end # @param column [String, Datagrid::Columns::Column] + # @param desc [nil, Boolean] confirm order direction as well if specified # @return [Boolean] true if given grid is ordered by given column. - def ordered_by?(column) - order_column == column_by_name(column) + def ordered_by?(column, desc = nil) + order_column == column_by_name(column) && + (desc.nil? || (desc ? descending? : !descending?)) end private diff --git a/lib/datagrid/renderer.rb b/lib/datagrid/renderer.rb deleted file mode 100644 index 0e7c1db5..00000000 --- a/lib/datagrid/renderer.rb +++ /dev/null @@ -1,154 +0,0 @@ -# frozen_string_literal: true - -require "action_view" - -module Datagrid - # @!visibility private - class Renderer - def self.for(template) - new(template) - end - - def initialize(template) - @template = template - end - - def format_value(grid, column, asset) - column = grid.column_by_name(column) if column.is_a?(String) || column.is_a?(Symbol) - - value = grid.html_value(column, @template, asset) - - url = column.options[:url]&.call(asset) - if url - @template.link_to(value, url) - else - value - end - end - - def form_for(grid, options = {}) - options[:method] ||= :get - options[:html] ||= {} - options[:html][:class] ||= "datagrid-form #{@template.dom_class(grid)}" - options[:as] ||= grid.param_name - _render_partial("form", options[:partials], { grid: grid, options: options }) - end - - def table(grid, assets, **options) - options[:html] ||= {} - options[:html][:class] ||= "datagrid #{@template.dom_class(grid)}" - - _render_partial("table", options[:partials], - { - grid: grid, - options: options, - assets: assets, - },) - end - - def header(grid, options = {}) - options[:order] = true unless options.key?(:order) - - _render_partial("head", options[:partials], - { grid: grid, options: options },) - end - - def rows(grid, assets = grid.assets, **options, &block) - result = assets.map do |asset| - row(grid, asset, **options, &block) - end.to_a.join - - _safe(result) - end - - def row(grid, asset, **options, &block) - Datagrid::Helper::HtmlRow.new(self, grid, asset, options).tap do |row| - return @template.capture(row, &block) if block_given? - end - end - - def order_for(grid, column, options = {}) - _render_partial("order_for", options[:partials], - { grid: grid, column: column },) - end - - def order_path(grid, column, descending, request) - column = grid.column_by_name(column) - query = request ? request.query_parameters : {} - ActionDispatch::Http::URL.path_for( - path: request ? request.path : "/", - params: query.merge(grid.query_params(order: column.name, descending: descending)), - ) - end - - private - - def _safe(string) - string.respond_to?(:html_safe) ? string.html_safe : string - end - - def _render_partial(partial_name, partials_path, locals = {}) - @template.render({ - partial: File.join(partials_path || "datagrid", partial_name), - locals: locals, - }) - end - end - - module Helper - # Represents a datagrid row that provides access to column values for the given asset - # @example - # row = datagrid_row(grid, user) - # row.class # => Datagrid::Helper::HtmlRow - # row.first_name # => "Bogdan" - # row.grid # => Grid object - # row.asset # => User object - # row.each do |value| - # puts value - # end - class HtmlRow - include Enumerable - - attr_reader :grid, :asset, :options - - # @!visibility private - def initialize(renderer, grid, asset, options) - @renderer = renderer - @grid = grid - @asset = asset - @options = options - end - - # @return [Object] a column value for given column name - def get(column) - @renderer.format_value(@grid, column, @asset) - end - - # Iterates over all column values that are available in the row - # param block [Proc] column value iterator - def each(&block) - (@options[:columns] || @grid.html_columns).each do |column| - block.call(get(column)) - end - end - - def to_s - @renderer.send(:_render_partial, "row", options[:partials], { - grid: grid, - options: options, - asset: asset, - },) - end - - protected - - def method_missing(method, *args, &blk) - if (column = @grid.column_by_name(method)) - get(column) - else - super - end - end - end - end -end diff --git a/lib/datagrid/scaffold.rb b/lib/datagrid/scaffold.rb deleted file mode 100644 index b6937cc6..00000000 --- a/lib/datagrid/scaffold.rb +++ /dev/null @@ -1,130 +0,0 @@ -# frozen_string_literal: true - -require "rails/generators" - -module Datagrid - # @!visibility private - class Scaffold < Rails::Generators::NamedBase - include Rails::Generators::ResourceHelpers - - check_class_collision suffix: "Grid" - source_root File.expand_path("/Users/bogdan/makabu/my/datagrid/lib/datagrid/scaffold.rb/../../../templates") - - def create_scaffold - template "base.rb.erb", base_grid_file unless file_exists?(base_grid_file) - template "grid.rb.erb", "app/grids/#{grid_class_name.underscore}.rb" - if file_exists?(grid_controller_file) - inject_into_file grid_controller_file, index_action, after: %r{class .*#{grid_controller_class_name}.*\n} - else - template "controller.rb.erb", grid_controller_file - end - template "index.html.erb", view_file - route(generate_routing_namespace("resources :#{grid_controller_short_name}")) - gem "kaminari" unless defined?(::Kaminari) || defined?(::WillPaginate) - in_root do - { - "css" => " *= require datagrid", - "css.sass" => " *= require datagrid", - "css.scss" => " *= require datagrid", - }.each do |extension, string| - file = "app/assets/stylesheets/application.#{extension}" - if file_exists?(file) - inject_into_file file, "#{string}\n", { before: %r{.*require_self} } # before all - end - end - end - end - - def view_file - Rails.root.join("app/views").join(controller_file_path).join("index.html.erb") - end - - def grid_class_name - "#{file_name.camelize.pluralize}Grid" - end - - def grid_controller_class_name - "#{controller_class_name.camelize}Controller" - end - - def grid_controller_file - Rails.root.join("app/controllers").join("#{grid_controller_class_name.underscore}.rb") - end - - def grid_controller_short_name - controller_file_name - end - - def grid_model_name - file_name.camelize.singularize - end - - def grid_param_name - grid_class_name.underscore - end - - def pagination_helper_code - if defined?(::WillPaginate) - "will_paginate(@grid.assets)" - else - # Kaminari is default - "paginate(@grid.assets)" - end - end - - def base_grid_file - "app/grids/base_grid.rb" - end - - def grid_route_name - "#{controller_class_name.underscore.gsub('/', '_')}_path" - end - - def index_action - indent(<<~RUBY) - def index - @grid = #{grid_class_name}.new(grid_params) do |scope| - scope.page(params[:page]) - end - end - - protected - - def grid_params - params.fetch(:#{grid_param_name}, {}).permit! - end - RUBY - end - - protected - - def generate_routing_namespace(code) - depth = regular_class_path.length - # Create 'namespace' ladder - # namespace :foo do - # namespace :bar do - namespace_ladder = regular_class_path.each_with_index.map do |ns, i| - indent("namespace :#{ns} do\n", i * 2) - end.join - - # Create route - # get 'baz/index' - route = indent(code, depth * 2) - - # Create `end` ladder - # end - # end - end_ladder = (1..depth).reverse_each.map do |i| - indent("end\n", i * 2) - end.join - - # Combine the 3 parts to generate complete route entry - "#{namespace_ladder}#{route}\n#{end_ladder}" - end - - def file_exists?(name) - name = Rails.root.join(name) unless name.to_s.first == "/" - File.exist?(name) - end - end -end diff --git a/lib/datagrid/utils.rb b/lib/datagrid/utils.rb index e1142ae4..c7ea68d2 100644 --- a/lib/datagrid/utils.rb +++ b/lib/datagrid/utils.rb @@ -23,12 +23,20 @@ def translate_from_namespace(namespace, grid_class, key) I18n.t(lookups.shift, default: lookups).presence end + def deprecator + if defined?(Rails) && Rails.version >= "7.1.0" + Rails.deprecator + else + ActiveSupport::Deprecation + end + end + def warn_once(message, delay = 5) @warnings ||= {} timestamp = @warnings[message] return false if timestamp && timestamp >= Time.now - delay - warn message + deprecator.warn(message) @warnings[message] = Time.now true end @@ -93,6 +101,7 @@ def parse_date(value) def parse_datetime(value) return nil if value.blank? return value if value.is_a?(Range) + return value if defined?(ActiveSupport::TimeWithZone) && value.is_a?(ActiveSupport::TimeWithZone) if value.is_a?(String) Array(Datagrid.configuration.datetime_formats).each do |format| @@ -112,10 +121,8 @@ def parse_datetime(value) def format_date_as_timestamp(value) if !value value - elsif value.is_a?(Array) - [value.first&.beginning_of_day, value.last&.end_of_day] elsif value.is_a?(Range) - (value.begin&.beginning_of_day..value.end&.end_of_day) + value.begin&.beginning_of_day..value.end&.end_of_day else value.beginning_of_day..value.end_of_day end diff --git a/lib/datagrid/version.rb b/lib/datagrid/version.rb index 12d0cd3f..403ac57e 100644 --- a/lib/datagrid/version.rb +++ b/lib/datagrid/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Datagrid - VERSION = "1.8.4" + VERSION = "2.0.0-alpha" end diff --git a/lib/tasks/datagrid_tasks.rake b/lib/tasks/datagrid_tasks.rake deleted file mode 100644 index e1ca194a..00000000 --- a/lib/tasks/datagrid_tasks.rake +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -namespace :datagrid do - desc "Copy table partials into rails application" - task :copy_partials do - require "fileutils" - views_path = "app/views/datagrid" - destination_dir = (Rails.root + views_path).to_s - pattern = "#{File.expand_path(File.dirname(__FILE__) + "/../../#{views_path}")}/*" - Dir[pattern].each do |template| - puts "* copy #{template} => #{destination_dir}" - FileUtils.mkdir_p destination_dir - FileUtils.cp template, destination_dir - end - end -end diff --git a/spec/datagrid/columns/column_spec.rb b/spec/datagrid/columns/column_spec.rb index 6594eeb3..12bc2b17 100644 --- a/spec/datagrid/columns/column_spec.rb +++ b/spec/datagrid/columns/column_spec.rb @@ -5,8 +5,7 @@ describe Datagrid::Columns::Column do describe ".inspect" do subject do - class ColumnInspectTest - include Datagrid + class ColumnInspectTest < Datagrid::Base scope { Entry } column(:id, header: "ID") end @@ -14,7 +13,7 @@ class ColumnInspectTest end it "shows inspect information" do - expect(subject.inspect).to eq("#\"ID\"}>") + expect(subject.inspect).to eq('#"ID"}>') end end end diff --git a/spec/datagrid/columns_spec.rb b/spec/datagrid/columns_spec.rb index 97dd01f2..57c6b26c 100644 --- a/spec/datagrid/columns_spec.rb +++ b/spec/datagrid/columns_spec.rb @@ -65,8 +65,7 @@ describe "translations" do module ::Ns45 - class TranslatedReport - include Datagrid + class TranslatedReport < Datagrid::Base scope { Entry } column(:name) end @@ -78,8 +77,7 @@ class TranslatedReport end it "translates column-header without namespace" do - class Report27 - include Datagrid + class Report27 < Datagrid::Base scope { Entry } column(:name) end @@ -90,8 +88,7 @@ class Report27 end it "translates column-header in using defaults namespace" do - class Report27 - include Datagrid + class Report27 < Datagrid::Base scope { Entry } column(:name) end @@ -148,7 +145,9 @@ class Report27 end it "should support csv export" do - expect(subject.to_csv).to eq("Shipping date,Group,Name,Access level,Pet\n#{date},Pop,Star,admin,ROTTWEILER\n") + expect(subject.to_csv).to eq( + "Shipping date,Group,Name,Access level,Pet\n#{date},Pop,Star,admin,ROTTWEILER\n", + ) end it "should support csv export of particular columns" do @@ -156,7 +155,9 @@ class Report27 end it "should support csv export options" do - expect(subject.to_csv(col_sep: ";")).to eq("Shipping date;Group;Name;Access level;Pet\n#{date};Pop;Star;admin;ROTTWEILER\n") + expect(subject.to_csv(col_sep: ";")).to eq( + "Shipping date;Group;Name;Access level;Pet\n#{date};Pop;Star;admin;ROTTWEILER\n", + ) end end @@ -178,8 +179,7 @@ class Report27 end it "should inherit columns correctly" do - parent = Class.new do - include Datagrid + parent = Class.new(Datagrid::Base) do scope { Entry } column(:name) end @@ -209,7 +209,7 @@ class Report27 filter(:name) column(:entries_count, "count(entries.id)") do |model| format("(#{model.entries_count})") do |value| - content_tag(:span, value) + tag.span(value) end end end @@ -579,8 +579,7 @@ def capitalized_name describe "#data_hash" do it "works" do pending - class DataHashGrid - include Datagrid + class DataHashGrid < Datagrid::Base scope { Entry } column(:name, order: true) end diff --git a/spec/datagrid/core_spec.rb b/spec/datagrid/core_spec.rb index 0b0b5628..58cae7e7 100644 --- a/spec/datagrid/core_spec.rb +++ b/spec/datagrid/core_spec.rb @@ -24,8 +24,7 @@ before { 2.times { Entry.create } } let(:report_class) do - class ScopeTestReport - include Datagrid + class ScopeTestReport < Datagrid::Base scope { Entry.order("id desc") } end ScopeTestReport @@ -95,21 +94,21 @@ class TestGrid < ScopeTestReport describe "#inspect" do it "should show all attribute values" do - class InspectTest - include Datagrid + class InspectTest < Datagrid::Base scope { Entry } filter(:created_at, :date, range: true) column(:name) end grid = InspectTest.new(created_at: %w[2014-01-01 2014-08-05], descending: true, order: "name") - expect(grid.inspect).to eq("#") + expect(grid.inspect).to eq( + "#", + ) end end describe "#==" do - class EqualTest - include Datagrid + class EqualTest < Datagrid::Base scope { Entry } filter(:created_at, :date) column(:name) @@ -203,13 +202,14 @@ class EqualTest end it "permites all attributes by default" do - expect do - test_grid(params) do - scope { Entry } - filter(:name) - end - end.to_not raise_error + grid = test_grid(params) do + scope { Entry } + filter(:name) + end + + expect(grid.name).to eq("one") end + it "doesn't permit attributes when forbidden_attributes_protection is set" do expect do test_grid(params) do @@ -219,6 +219,7 @@ class EqualTest end end.to raise_error(ActiveModel::ForbiddenAttributesError) end + it "permits attributes when forbidden_attributes_protection is set and attributes are permitted" do expect do test_grid(params.permit!) do @@ -228,6 +229,13 @@ class EqualTest end end.to_not raise_error end + + it "supports hash attribute assignment" do + grid = test_grid_filter(:group_id, :integer, range: true) + grid.attributes = ActionController::Parameters.new(group_id: { from: 1, to: 2 }) + + expect(grid.group_id).to eq(1..2) + end end describe ".query_param" do diff --git a/spec/datagrid/drivers/active_record_spec.rb b/spec/datagrid/drivers/active_record_spec.rb index 5b84de20..d8a802cd 100644 --- a/spec/datagrid/drivers/active_record_spec.rb +++ b/spec/datagrid/drivers/active_record_spec.rb @@ -18,8 +18,10 @@ end it "should support append_column_queries" do - scope = subject.append_column_queries(Entry.where({}), - [Datagrid::Columns::Column.new(test_grid_class, :sum_group_id, "sum(entries.group_id)")],) + scope = subject.append_column_queries( + Entry.where({}), + [Datagrid::Columns::Column.new(test_grid_class, :sum_group_id, "sum(entries.group_id)")], + ) expect(scope.to_sql.strip).to eq('SELECT "entries".*, sum(entries.group_id) AS sum_group_id FROM "entries"') end @@ -36,13 +38,13 @@ end end - describe "gotcha #datagrid_where_by_timestamp" do + describe "where by timestamp" do subject do test_grid(created_at: 10.days.ago..5.days.ago) do scope { Entry } filter(:created_at, :date, range: true) do |value, scope, _grid| - scope.joins(:group).datagrid_where_by_timestamp("groups.created_at", value) + scope.joins(:group).where(groups: { created_at: value }) end end.assets end diff --git a/spec/datagrid/drivers/array_spec.rb b/spec/datagrid/drivers/array_spec.rb index 3e708b29..50fb0ad2 100644 --- a/spec/datagrid/drivers/array_spec.rb +++ b/spec/datagrid/drivers/array_spec.rb @@ -12,9 +12,8 @@ end describe "api" do - class ArrayGrid + class ArrayGrid < Datagrid::Base User = Struct.new(:name, :age) - include Datagrid scope do [] end @@ -102,8 +101,7 @@ class ArrayGrid end describe "array of hashes" do - class HashGrid - include Datagrid + class HashGrid < Datagrid::Base scope do [{ name: "Bogdan", age: 30 }, { name: "Brad", age: 32 }] end diff --git a/spec/datagrid/drivers/sequel_spec.rb b/spec/datagrid/drivers/sequel_spec.rb index 60db3336..225491f3 100644 --- a/spec/datagrid/drivers/sequel_spec.rb +++ b/spec/datagrid/drivers/sequel_spec.rb @@ -33,8 +33,7 @@ end it "supports pagination" do - class PaginationTest - include Datagrid + class PaginationTest < Datagrid::Base scope { SequelEntry } end grid = PaginationTest.new do |scope| diff --git a/spec/datagrid/filters/base_filter_spec.rb b/spec/datagrid/filters/base_filter_spec.rb index 09c88f38..cb3c84de 100644 --- a/spec/datagrid/filters/base_filter_spec.rb +++ b/spec/datagrid/filters/base_filter_spec.rb @@ -15,4 +15,19 @@ def name_default expect(report.assets).not_to include(Entry.create!(name: "world")) expect(report.assets).not_to include(Entry.create!(name: "")) end + + describe "#default_scope?" do + it "identifies filters without custom block" do + grid = test_grid do + scope { Entry } + filter(:id, :integer) + filter(:group_id, :integer) do |value, _scope| + scope("group_id >= ?", value) + end + end + + expect(grid.filter_by_name(:id)).to be_default_scope + expect(grid.filter_by_name(:group_id)).to_not be_default_scope + end + end end diff --git a/spec/datagrid/filters/composite_filters_spec.rb b/spec/datagrid/filters/composite_filters_spec.rb deleted file mode 100644 index c5a221f4..00000000 --- a/spec/datagrid/filters/composite_filters_spec.rb +++ /dev/null @@ -1,76 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -describe Datagrid::Filters::CompositeFilters do - describe ".date_range_filters" do - it "should generate from date and to date filters" do - e1 = Entry.create!(shipping_date: 6.days.ago) - e2 = Entry.create!(shipping_date: 4.days.ago) - e3 = Entry.create!(shipping_date: 1.days.ago) - assets = test_grid(from_shipping_date: 5.days.ago, to_shipping_date: 2.day.ago) do - scope { Entry } - silence_warnings do - date_range_filters(:shipping_date) - end - end.assets - - expect(assets).to include(e2) - expect(assets).not_to include(e1, e3) - end - - it "should support options" do - report = test_grid do - silence_warnings do - date_range_filters(:shipping_date, { default: 10.days.ago.to_date }, { default: Date.today }) - end - end - expect(report.from_shipping_date).to eq(10.days.ago.to_date) - expect(report.to_shipping_date).to eq(Date.today) - end - it "should support table name in field" do - report = test_grid do - silence_warnings do - date_range_filters("entries.shipping_date", { default: 10.days.ago.to_date }, { default: Date.today }) - end - end - expect(report.from_entries_shipping_date).to eq(10.days.ago.to_date) - expect(report.to_entries_shipping_date).to eq(Date.today) - end - end - - describe ".integer_range_filters" do - it "should generate from integer and to integer filters" do - e1 = Entry.create!(group_id: 1) - e2 = Entry.create!(group_id: 3) - e3 = Entry.create!(group_id: 5) - assets = test_grid(from_group_id: 2, to_group_id: 4) do - scope { Entry } - silence_warnings do - integer_range_filters(:group_id) - end - end.assets - - expect(assets).to include(e2) - expect(assets).not_to include(e1, e3) - end - it "should support options" do - report = test_grid do - silence_warnings do - integer_range_filters(:group_id, { default: 0 }, { default: 100 }) - end - end - expect(report.from_group_id).to eq(0) - expect(report.to_group_id).to eq(100) - end - it "should table name in field name" do - report = test_grid do - silence_warnings do - integer_range_filters("entries.group_id", { default: 0 }, { default: 100 }) - end - end - expect(report.from_entries_group_id).to eq(0) - expect(report.to_entries_group_id).to eq(100) - end - end -end diff --git a/spec/datagrid/filters/date_filter_spec.rb b/spec/datagrid/filters/date_filter_spec.rb index 4a21becd..60744b77 100644 --- a/spec/datagrid/filters/date_filter_spec.rb +++ b/spec/datagrid/filters/date_filter_spec.rb @@ -7,21 +7,33 @@ it "supports date range argument" do e1 = Entry.create!(created_at: 7.days.ago) e2 = Entry.create!(created_at: 4.days.ago) - e3 = Entry.create!(created_at: 1.day.ago) + e3 = Entry.create!(created_at: 3.days.ago) + e4 = Entry.create!(created_at: 1.day.ago) - report = test_grid_filter(:created_at, :date) + report = test_grid_filter(:created_at, :date, range: true) report.created_at = 5.day.ago..3.days.ago + expect(report.created_at).to eq(5.days.ago.to_date..3.days.ago.to_date) expect(report.assets).not_to include(e1) expect(report.assets).to include(e2) - expect(report.assets).not_to include(e3) + expect(report.assets).to include(e3) + expect(report.assets).not_to include(e4) + end + + it "raises when range assigned to non-range filter" do + expect do + test_grid(created_at: 5.day.ago..3.days.ago) do + scope { Entry } + filter(:created_at, :date) + end + end.to raise_error(ArgumentError) end it "endless date range argument" do e1 = Entry.create!(created_at: 7.days.ago) e2 = Entry.create!(created_at: 4.days.ago) - report = test_grid_filter(:created_at, :date) + report = test_grid_filter(:created_at, :date, range: true) report.created_at = (5.days.ago..) expect(report.assets).not_to include(e1) @@ -31,6 +43,32 @@ expect(report.assets).not_to include(e2) end + it "supports hash argument for range filter" do + report = test_grid_filter(:created_at, :date, range: true) + from = 5.days.ago + to = 3.days.ago + report.created_at = { from: from, to: to } + expect(report.created_at).to eq(from.to_date..to.to_date) + + report.created_at = { "from" => from, "to" => to } + expect(report.created_at).to eq(from.to_date..to.to_date) + + report.created_at = {} + expect(report.created_at).to eq(nil) + + report.created_at = { from: nil, to: nil } + expect(report.created_at).to eq(nil) + + report.created_at = { from: Date.today, to: nil } + expect(report.created_at).to eq(Date.today..nil) + + report.created_at = { from: nil, to: Date.today } + expect(report.created_at).to eq(nil..Date.today) + + report.created_at = { from: Time.now, to: Time.now } + expect(report.created_at).to eq(Date.today..Date.today) + end + { active_record: Entry, mongoid: MongoidEntry, sequel: SequelEntry }.each do |orm, klass| describe "with orm #{orm}", orm => true do describe "date to timestamp conversion" do @@ -121,9 +159,11 @@ def entry_dated(date) e1 = Entry.create!(created_at: 7.days.ago) e2 = Entry.create!(created_at: 4.days.ago) e3 = Entry.create!(created_at: 1.day.ago) + report = test_grid_filter(:created_at, :date, range: true) report.created_at = range - expect(report.created_at).to eq([range.last.to_date, range.first.to_date]) + + expect(report.created_at).to eq(range.last.to_date..range.first.to_date) expect(report.assets).to include(e1) expect(report.assets).to include(e2) expect(report.assets).to include(e3) @@ -132,10 +172,12 @@ def entry_dated(date) it "should support block" do date = Date.new(2018, 0o1, 0o7) time = Time.utc(2018, 0o1, 0o7, 2, 2) + report = test_grid_filter(:created_at, :date, range: true) do |value| - where("created_at >= ?", value) + where(created_at: value) end report.created_at = date + expect(report.assets).not_to include(Entry.create!(created_at: time - 1.day)) expect(report.assets).to include(Entry.create!(created_at: time)) end @@ -166,7 +208,7 @@ def entry_dated(date) report = test_grid_filter(:created_at, :date, range: true) report.created_at = %w[2013-01-01 2012-01-01] - expect(report.created_at).to eq([Date.new(2012, 0o1, 0o1), Date.new(2013, 0o1, 0o1)]) + expect(report.created_at).to eq(Date.new(2012, 0o1, 0o1)..Date.new(2013, 0o1, 0o1)) end it "should nullify blank range" do @@ -185,6 +227,14 @@ def entry_dated(date) end end + it "deserializes range" do + report = test_grid_filter(:created_at, :date, range: true) + + value = Date.new(2012, 1, 1)..Date.new(2012, 1, 2) + report.created_at = value.as_json + expect(report.created_at).to eq(value) + end + it "supports search by timestamp column" do report = test_grid_filter(:created_at, :date) report.created_at = Date.today @@ -200,4 +250,18 @@ def entry_dated(date) expect(report.assets).to include(e4) expect(report.assets).to_not include(e5) end + + it "allows filter to be defined before scope" do + class ParentGrid < Datagrid::Base + filter(:created_at, :date, range: true) + end + + class ChildGrid < ParentGrid + scope do + Entry + end + end + + expect(ChildGrid.new.assets).to eq([]) + end end diff --git a/spec/datagrid/filters/date_time_filter_spec.rb b/spec/datagrid/filters/date_time_filter_spec.rb index 3da1c18a..990c1be7 100644 --- a/spec/datagrid/filters/date_time_filter_spec.rb +++ b/spec/datagrid/filters/date_time_filter_spec.rb @@ -121,14 +121,14 @@ def entry_dated(date) report = test_grid_filter(:created_at, :datetime, range: true) report.created_at = range - expect(report.created_at).to eq([range.last, range.first]) + expect(report.created_at).to eq(range.last..range.first) expect(report.assets).to include(e1) expect(report.assets).to include(e2) expect(report.assets).to include(e3) end it "should support block" do - report = test_grid_filter(:created_at, :datetime, range: true) do |value| + report = test_grid_filter(:created_at, :datetime) do |value| where("created_at >= ?", value) end report.created_at = Time.now @@ -162,6 +162,30 @@ def entry_dated(date) report = test_grid_filter(:created_at, :datetime, range: true) report.created_at = ["2013-01-01 01:00", "2012-01-01 01:00"] - expect(report.created_at).to eq([Time.new(2012, 0o1, 0o1, 1, 0), Time.new(2013, 0o1, 0o1, 1, 0)]) + expect(report.created_at).to eq(Time.new(2012, 0o1, 0o1, 1, 0)..Time.new(2013, 0o1, 0o1, 1, 0)) + end + + it "supports serialized range value" do + from = Time.parse("2013-01-01 01:00") + to = Time.parse("2013-01-02 02:00") + report = test_grid_filter(:created_at, :datetime, range: true) + + report.created_at = (from..to).as_json + expect(report.created_at).to eq(from..to) + + report.created_at = (from..).as_json + expect(report.created_at).to eq(from..) + + report.created_at = (..to).as_json + expect(report.created_at).to eq(..to) + + report.created_at = (from...to).as_json + expect(report.created_at).to eq(from...to) + + report.created_at = (nil..nil).as_json + expect(report.created_at).to eq(nil) + + report.created_at = (nil...nil).as_json + expect(report.created_at).to eq(nil) end end diff --git a/spec/datagrid/filters/dynamic_filter_spec.rb b/spec/datagrid/filters/dynamic_filter_spec.rb index 3ad793c8..42ffcff3 100644 --- a/spec/datagrid/filters/dynamic_filter_spec.rb +++ b/spec/datagrid/filters/dynamic_filter_spec.rb @@ -59,22 +59,26 @@ it "should nullify incorrect value for integer" do report.condition = [:group_id, "<=", "aa"] - expect(report.condition).to eq([:group_id, "<=", nil]) + expect(report.condition.to_h).to eq( + { field: :group_id, operation: "<=", value: nil }, + ) end it "should nullify incorrect value for date" do report.condition = [:shipping_date, "<=", "aa"] - expect(report.condition).to eq([:shipping_date, "<=", nil]) + expect(report.condition.to_h).to eq({ + field: :shipping_date, operation: "<=", value: nil, + }) end it "should nullify incorrect value for datetime" do report.condition = [:created_at, "<=", "aa"] - expect(report.condition).to eq([:created_at, "<=", nil]) + expect(report.condition.to_h).to eq({ field: :created_at, operation: "<=", value: nil }) end it "should support date comparation operation by timestamp column" do report.condition = [:created_at, "<=", "1986-08-05"] - expect(report.condition).to eq([:created_at, "<=", Date.parse("1986-08-05")]) + expect(report.condition.to_h).to eq({ field: :created_at, operation: "<=", value: Date.parse("1986-08-05") }) expect(report.assets).to include(Entry.create!(created_at: Time.parse("1986-08-04 01:01:01"))) expect(report.assets).to include(Entry.create!(created_at: Time.parse("1986-08-05 23:59:59"))) expect(report.assets).to include(Entry.create!(created_at: Time.parse("1986-08-05 00:00:00"))) @@ -84,7 +88,7 @@ it "should support date = operation by timestamp column" do report.condition = [:created_at, "=", "1986-08-05"] - expect(report.condition).to eq([:created_at, "=", Date.parse("1986-08-05")]) + expect(report.condition.to_h).to eq({ field: :created_at, operation: "=", value: Date.parse("1986-08-05") }) expect(report.assets).not_to include(Entry.create!(created_at: Time.parse("1986-08-04 23:59:59"))) expect(report.assets).to include(Entry.create!(created_at: Time.parse("1986-08-05 23:59:59"))) expect(report.assets).to include(Entry.create!(created_at: Time.parse("1986-08-05 00:00:01"))) @@ -95,7 +99,7 @@ it "should support date =~ operation by timestamp column" do report.condition = [:created_at, "=~", "1986-08-05"] - expect(report.condition).to eq([:created_at, "=~", Date.parse("1986-08-05")]) + expect(report.condition.to_h).to eq({ field: :created_at, operation: "=~", value: Date.parse("1986-08-05") }) expect(report.assets).not_to include(Entry.create!(created_at: Time.parse("1986-08-04 23:59:59"))) expect(report.assets).to include(Entry.create!(created_at: Time.parse("1986-08-05 23:59:59"))) expect(report.assets).to include(Entry.create!(created_at: Time.parse("1986-08-05 00:00:01"))) @@ -144,9 +148,9 @@ scope { Entry } filter( :condition, :dynamic, operations: ["=", "!="], - ) do |(field, operation, value), scope| - if operation == "!=" - scope.where("#{field} != ?", value) + ) do |filter, scope| + if filter.operation == "!=" + scope.where("#{filter.field} != ?", filter.value) else default_filter end @@ -169,4 +173,16 @@ report.assets end.to raise_error(Datagrid::FilteringError) end + + it "supports assignment of string keys hash" do + report.condition = { + field: "shipping_date", + operation: "<>", + value: "1996-08-05", + }.stringify_keys + + expect(report.condition.to_h).to eq({ + field: "shipping_date", operation: "<>", value: Date.parse("1996-08-05"), + }) + end end diff --git a/spec/datagrid/filters/enum_filter_spec.rb b/spec/datagrid/filters/enum_filter_spec.rb index ac398e18..c8d44d07 100644 --- a/spec/datagrid/filters/enum_filter_spec.rb +++ b/spec/datagrid/filters/enum_filter_spec.rb @@ -24,8 +24,7 @@ end it "should initialize select option only on instanciation" do - class ReportWithLazySelect - include Datagrid + class ReportWithLazySelect < Datagrid::Base scope { Entry } filter(:group_id, :enum, select: proc { raise "hello" }) end diff --git a/spec/datagrid/filters/integer_filter_spec.rb b/spec/datagrid/filters/integer_filter_spec.rb index 1757860e..c5b76fb9 100644 --- a/spec/datagrid/filters/integer_filter_spec.rb +++ b/spec/datagrid/filters/integer_filter_spec.rb @@ -11,7 +11,7 @@ let(:entry7) { Entry.create!(group_id: 7) } it "should support integer range argument" do - report = test_grid_filter(:group_id, :integer) + report = test_grid_filter(:group_id, :integer, range: true) report.group_id = 3..5 expect(report.assets).not_to include(entry1) @@ -30,47 +30,44 @@ it "should support integer range given as array argument" do report = test_grid_filter(:group_id, :integer, range: true) report.group_id = [3.to_s, 5.to_s] - - expect(report.assets).not_to include(entry7) - expect(report.assets).to include(entry4) - expect(report.assets).not_to include(entry1) + expect(report.group_id).to eq(3..5) end it "should support minimum integer argument" do report = test_grid_filter(:group_id, :integer, range: true) report.group_id = [5.to_s, nil] - expect(report.assets).not_to include(entry1) - expect(report.assets).not_to include(entry4) - expect(report.assets).to include(entry7) + expect(report.group_id).to eq(5..) end it "should support maximum integer argument" do report = test_grid_filter(:group_id, :integer, range: true) report.group_id = [nil, 5.to_s] - expect(report.assets).to include(entry1) - expect(report.assets).to include(entry4) - expect(report.assets).not_to include(entry7) + expect(report.group_id).to eq(..5) end it "should find something in one integer interval" do report = test_grid_filter(:group_id, :integer, range: true) - report.group_id = (4..4) + report.group_id = 4 expect(report.assets).not_to include(entry7) expect(report.assets).to include(entry4) expect(report.assets).not_to include(entry1) end - it "should support invalid range" do + it "supports range inversion" do report = test_grid_filter(:group_id, :integer, range: true) report.group_id = 7..1 - expect(report.group_id).to eq([1, 7]) - expect(report.assets).to include(entry7) - expect(report.assets).to include(entry4) - expect(report.assets).to include(entry1) + expect(report.group_id).to eq(1..7) + end + + it "converts infinite range to nil" do + report = test_grid_filter(:group_id, :integer, range: true) + report.group_id = nil..nil + + expect(report.group_id).to eq(nil) end it "should support block" do @@ -88,7 +85,7 @@ scope { Entry.joins(:group) } filter(:rating, :integer, range: true) end - expect(report.rating).to eq([4, nil]) + expect(report.rating).to eq(4..nil) expect(report.assets).not_to include(Entry.create!(group: Group.create!(rating: 3))) expect(report.assets).to include(Entry.create!(group: Group.create!(rating: 5))) end @@ -133,4 +130,57 @@ expect(report.group_id).to eq(group.id) end + + it "supports serialized range value" do + report = test_grid_filter(:group_id, :integer, range: true) + + report.group_id = (1..5).as_json + expect(report.group_id).to eq(1..5) + + report.group_id = (1..).as_json + expect(report.group_id).to eq(1..) + + report.group_id = (..5).as_json + expect(report.group_id).to eq(..5) + + report.group_id = (1...5).as_json + expect(report.group_id).to eq(1...5) + + report.group_id = (nil..nil).as_json + expect(report.group_id).to eq(nil) + + report.group_id = (nil...nil).as_json + expect(report.group_id).to eq(nil) + end + + it "type casts value" do + report = test_grid_filter(:group_id, :integer) + + report.group_id = "1" + expect(report.group_id).to eq(1) + + report.group_id = " 1 " + expect(report.group_id).to eq(1) + + report.group_id = 1.1 + expect(report.group_id).to eq(1) + + report.group_id = "1.1" + expect(report.group_id).to eq(1) + + report.group_id = "-1" + expect(report.group_id).to eq(-1) + + report.group_id = "-1.1" + expect(report.group_id).to eq(-1) + + report.group_id = "1a" + expect(report.group_id).to eq(1) + + report.group_id = "aa" + expect(report.group_id).to eq(nil) + + report.group_id = "a1" + expect(report.group_id).to eq(nil) + end end diff --git a/spec/datagrid/filters_spec.rb b/spec/datagrid/filters_spec.rb index b5431616..3534503f 100644 --- a/spec/datagrid/filters_spec.rb +++ b/spec/datagrid/filters_spec.rb @@ -40,9 +40,7 @@ it "should initialize when report Scope table not exists" do class ModelWithoutTable < ActiveRecord::Base; end expect(ModelWithoutTable).not_to be_table_exists - class TheReport - include Datagrid - + class TheReport < Datagrid::Base scope { ModelWithoutTable } filter(:name) @@ -52,8 +50,7 @@ class TheReport end it "should support inheritence" do - parent = Class.new do - include Datagrid + parent = Class.new(Datagrid::Base) do scope { Entry } filter(:name) end @@ -175,8 +172,7 @@ def check_performed(value, result, **options) describe "tranlations" do module ::Ns46 - class TranslatedReport - include Datagrid + class TranslatedReport < Datagrid::Base scope { Entry } filter(:name) end @@ -239,8 +235,7 @@ class InheritedReport < TranslatedReport describe "#inspect" do it "should list all fitlers with types" do module ::NsInspect - class TestGrid - include Datagrid + class TestGrid < Datagrid::Base scope { Entry } filter(:id, :integer) filter(:name, :string) @@ -254,8 +249,7 @@ class TestGrid end it "dislays no filters" do - class TestGrid8728 - include Datagrid + class TestGrid8728 < Datagrid::Base scope { Entry } end diff --git a/spec/datagrid/form_builder_spec.rb b/spec/datagrid/form_builder_spec.rb index dc5abd01..cdccec7c 100644 --- a/spec/datagrid/form_builder_spec.rb +++ b/spec/datagrid/form_builder_spec.rb @@ -16,16 +16,17 @@ class MyTemplate action_view_template end - let(:view) { ActionView::Helpers::FormBuilder.new(:report, _grid, template, view_options) } + let(:view) do + ActionView::Helpers::FormBuilder.new( + :report, _grid, template, + skip_default_ids: false, + **view_options, + ) + end + let(:view_options) { {} } describe ".datagrid_filter" do - it "should work for every filter type" do - Datagrid::Filters::FILTER_TYPES.each_value do |klass| - expect(Datagrid::FormBuilder.instance_methods.map(&:to_sym)).to include(klass.form_builder_helper_name) - end - end - subject do view.datagrid_filter(_filter, **_filter_options, &_filter_block) end @@ -41,7 +42,7 @@ class MyTemplate let(:_filter) { :name } it { should equal_to_dom(<<~HTML) - + HTML } end @@ -54,7 +55,7 @@ class MyTemplate it { should equal_to_dom(<<~HTML) - + HTML } @@ -63,7 +64,7 @@ class MyTemplate it { should equal_to_dom(<<~HTML) - + HTML } end @@ -77,7 +78,7 @@ class MyTemplate it { should equal_to_dom(<<~HTML) - + HTML } @@ -91,51 +92,54 @@ class MyTemplate it { should equal_to_dom(<<~HTML) - + HTML } end end context "with input_options" do - context "type is date" do + context "date filter type is text" do let(:_filter) { :created_at } let(:_grid) do - test_grid_filter(:created_at, :date, input_options: { type: :date }) + test_grid_filter(:created_at, :date, input_options: { type: "text" }) end it { should equal_to_dom(<<~HTML) - + HTML } end - context "type is textarea" do + context "string filter type is textarea" do let(:_filter) { :name } let(:_grid) do test_grid_filter(:name, :string, input_options: { type: :textarea }) end it { - should equal_to_dom(<<~HTML) - - HTML + should equal_to_dom( + '