uchi 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +222 -0
- data/app/assets/config/uchi_manifest.js +2 -0
- data/app/assets/javascripts/uchi/application.js +6095 -0
- data/app/assets/javascripts/uchi.js +4 -0
- data/app/assets/stylesheets/uchi/application.css +3971 -0
- data/app/assets/stylesheets/uchi/uchi.css +17 -0
- data/app/assets/tailwind/uchi.css +21 -0
- data/app/components/flowbite/breadcrumb.rb +33 -0
- data/app/components/flowbite/breadcrumb_home.rb +26 -0
- data/app/components/flowbite/breadcrumb_item/current.rb +33 -0
- data/app/components/flowbite/breadcrumb_item/first.rb +35 -0
- data/app/components/flowbite/breadcrumb_item.rb +48 -0
- data/app/components/flowbite/breadcrumb_separator.rb +30 -0
- data/app/components/flowbite/button/outline.rb +22 -0
- data/app/components/flowbite/button/pill.rb +40 -0
- data/app/components/flowbite/button.rb +92 -0
- data/app/components/flowbite/card.rb +45 -0
- data/app/components/flowbite/input/checkbox.rb +73 -0
- data/app/components/flowbite/input/date.rb +11 -0
- data/app/components/flowbite/input/date_time.rb +11 -0
- data/app/components/flowbite/input/email.rb +12 -0
- data/app/components/flowbite/input/field.rb +117 -0
- data/app/components/flowbite/input/file.rb +30 -0
- data/app/components/flowbite/input/hint.rb +57 -0
- data/app/components/flowbite/input/label.rb +82 -0
- data/app/components/flowbite/input/number.rb +11 -0
- data/app/components/flowbite/input/password.rb +11 -0
- data/app/components/flowbite/input/phone.rb +11 -0
- data/app/components/flowbite/input/radio_button.rb +50 -0
- data/app/components/flowbite/input/select.rb +49 -0
- data/app/components/flowbite/input/textarea.rb +42 -0
- data/app/components/flowbite/input/url.rb +12 -0
- data/app/components/flowbite/input/validation_error.rb +11 -0
- data/app/components/flowbite/input_field/checkbox.html.erb +14 -0
- data/app/components/flowbite/input_field/checkbox.rb +54 -0
- data/app/components/flowbite/input_field/date.rb +13 -0
- data/app/components/flowbite/input_field/date_time.rb +13 -0
- data/app/components/flowbite/input_field/email.rb +13 -0
- data/app/components/flowbite/input_field/file.rb +13 -0
- data/app/components/flowbite/input_field/input_field.html.erb +8 -0
- data/app/components/flowbite/input_field/number.rb +13 -0
- data/app/components/flowbite/input_field/password.rb +13 -0
- data/app/components/flowbite/input_field/phone.rb +13 -0
- data/app/components/flowbite/input_field/radio_button.html.erb +14 -0
- data/app/components/flowbite/input_field/radio_button.rb +86 -0
- data/app/components/flowbite/input_field/select.rb +31 -0
- data/app/components/flowbite/input_field/text.rb +8 -0
- data/app/components/flowbite/input_field/textarea.rb +13 -0
- data/app/components/flowbite/input_field/url.rb +13 -0
- data/app/components/flowbite/input_field.rb +192 -0
- data/app/components/flowbite/link.rb +21 -0
- data/app/components/flowbite/style.rb +13 -0
- data/app/components/flowbite/toast/icon.html.erb +5 -0
- data/app/components/flowbite/toast/icon.rb +57 -0
- data/app/components/flowbite/toast/toast.html.erb +11 -0
- data/app/components/flowbite/toast.rb +34 -0
- data/app/components/uchi/field/base.rb +57 -0
- data/app/components/uchi/field/belongs_to/edit.html.erb +1 -0
- data/app/components/uchi/field/belongs_to/index.html.erb +1 -0
- data/app/components/uchi/field/belongs_to/show.html.erb +3 -0
- data/app/components/uchi/field/belongs_to.rb +105 -0
- data/app/components/uchi/field/blank/edit.html.erb +1 -0
- data/app/components/uchi/field/blank/index.html.erb +1 -0
- data/app/components/uchi/field/blank/show.html.erb +1 -0
- data/app/components/uchi/field/blank.rb +16 -0
- data/app/components/uchi/field/boolean/edit.html.erb +1 -0
- data/app/components/uchi/field/boolean/index.html.erb +9 -0
- data/app/components/uchi/field/boolean/show.html.erb +9 -0
- data/app/components/uchi/field/boolean.rb +27 -0
- data/app/components/uchi/field/date/edit.html.erb +1 -0
- data/app/components/uchi/field/date/index.html.erb +1 -0
- data/app/components/uchi/field/date/show.html.erb +1 -0
- data/app/components/uchi/field/date.rb +27 -0
- data/app/components/uchi/field/date_time/edit.html.erb +1 -0
- data/app/components/uchi/field/date_time/index.html.erb +1 -0
- data/app/components/uchi/field/date_time/show.html.erb +1 -0
- data/app/components/uchi/field/date_time.rb +27 -0
- data/app/components/uchi/field/file/edit.html.erb +1 -0
- data/app/components/uchi/field/file/index.html.erb +6 -0
- data/app/components/uchi/field/file/show.html.erb +8 -0
- data/app/components/uchi/field/file.rb +37 -0
- data/app/components/uchi/field/has_and_belongs_to_many/edit.html.erb +9 -0
- data/app/components/uchi/field/has_and_belongs_to_many/index.html.erb +1 -0
- data/app/components/uchi/field/has_and_belongs_to_many/show.html.erb +28 -0
- data/app/components/uchi/field/has_and_belongs_to_many.rb +131 -0
- data/app/components/uchi/field/has_many/edit.html.erb +1 -0
- data/app/components/uchi/field/has_many/index.html.erb +1 -0
- data/app/components/uchi/field/has_many/show.html.erb +28 -0
- data/app/components/uchi/field/has_many.rb +107 -0
- data/app/components/uchi/field/id/index.html.erb +4 -0
- data/app/components/uchi/field/id/show.html.erb +4 -0
- data/app/components/uchi/field/id.rb +26 -0
- data/app/components/uchi/field/image/edit.html.erb +1 -0
- data/app/components/uchi/field/image/index.html.erb +6 -0
- data/app/components/uchi/field/image/show.html.erb +6 -0
- data/app/components/uchi/field/image.rb +38 -0
- data/app/components/uchi/field/number/edit.html.erb +1 -0
- data/app/components/uchi/field/number/index.html.erb +1 -0
- data/app/components/uchi/field/number/show.html.erb +1 -0
- data/app/components/uchi/field/number.rb +32 -0
- data/app/components/uchi/field/string/edit.html.erb +1 -0
- data/app/components/uchi/field/string/index.html.erb +1 -0
- data/app/components/uchi/field/string/show.html.erb +1 -0
- data/app/components/uchi/field/string.rb +33 -0
- data/app/components/uchi/field/text/edit.html.erb +1 -0
- data/app/components/uchi/field/text/index.html.erb +1 -0
- data/app/components/uchi/field/text/show.html.erb +1 -0
- data/app/components/uchi/field/text.rb +38 -0
- data/app/components/uchi/ui/breadcrumb/breadcrumb.html.erb +13 -0
- data/app/components/uchi/ui/breadcrumb.rb +14 -0
- data/app/components/uchi/ui/form/footer/footer.html.erb +5 -0
- data/app/components/uchi/ui/form/footer.rb +15 -0
- data/app/components/uchi/ui/form/input/collection_checkboxes.html.erb +32 -0
- data/app/components/uchi/ui/form/input/collection_checkboxes.rb +125 -0
- data/app/components/uchi/ui/frame/frame.html.erb +3 -0
- data/app/components/uchi/ui/frame.rb +10 -0
- data/app/components/uchi/ui/index/records_table/records_table.html.erb +67 -0
- data/app/components/uchi/ui/index/records_table/search_form/search_form.html.erb +21 -0
- data/app/components/uchi/ui/index/records_table/search_form.rb +49 -0
- data/app/components/uchi/ui/index/records_table.rb +29 -0
- data/app/components/uchi/ui/index/turbo_frame.rb +50 -0
- data/app/components/uchi/ui/page_header/page_header.html.erb +24 -0
- data/app/components/uchi/ui/page_header.rb +18 -0
- data/app/components/uchi/ui/pagination/current_link.html.erb +3 -0
- data/app/components/uchi/ui/pagination/current_link.rb +10 -0
- data/app/components/uchi/ui/pagination/gap.html.erb +3 -0
- data/app/components/uchi/ui/pagination/gap.rb +10 -0
- data/app/components/uchi/ui/pagination/item.rb +24 -0
- data/app/components/uchi/ui/pagination/link.html.erb +3 -0
- data/app/components/uchi/ui/pagination/link.rb +10 -0
- data/app/components/uchi/ui/pagination/next_link.html.erb +8 -0
- data/app/components/uchi/ui/pagination/next_link.rb +22 -0
- data/app/components/uchi/ui/pagination/pagination.html.erb +15 -0
- data/app/components/uchi/ui/pagination/previous_link.html.erb +8 -0
- data/app/components/uchi/ui/pagination/previous_link.rb +22 -0
- data/app/components/uchi/ui/pagination.rb +48 -0
- data/app/components/uchi/ui/show/attribute_fields/attribute_fields.html.erb +14 -0
- data/app/components/uchi/ui/show/attribute_fields.rb +18 -0
- data/app/components/uchi/ui/spinner/spinner.html.erb +7 -0
- data/app/components/uchi/ui/spinner.rb +15 -0
- data/app/controllers/uchi/application_controller.rb +4 -0
- data/app/controllers/uchi/controller.rb +13 -0
- data/app/controllers/uchi/repository_controller.rb +166 -0
- data/app/helpers/uchi/application_helper.rb +17 -0
- data/app/jobs/uchi/application_job.rb +4 -0
- data/app/mailers/uchi/application_mailer.rb +6 -0
- data/app/views/layouts/uchi/_flash_messages.html.erb +10 -0
- data/app/views/layouts/uchi/application.html.erb +33 -0
- data/app/views/uchi/repository/edit.html.erb +40 -0
- data/app/views/uchi/repository/index.html.erb +49 -0
- data/app/views/uchi/repository/new.html.erb +37 -0
- data/app/views/uchi/repository/show.html.erb +41 -0
- data/lib/generators/uchi/controller/controller_generator.rb +16 -0
- data/lib/generators/uchi/controller/templates/controller.rb.tt +11 -0
- data/lib/generators/uchi/install/install_generator.rb +13 -0
- data/lib/generators/uchi/repository/repository_generator.rb +16 -0
- data/lib/generators/uchi/repository/templates/repository.rb.tt +11 -0
- data/lib/tasks/uchi_tasks.rake +4 -0
- data/lib/uchi/application_record.rb +5 -0
- data/lib/uchi/engine.rb +24 -0
- data/lib/uchi/field/configuration.rb +142 -0
- data/lib/uchi/field.rb +80 -0
- data/lib/uchi/i18n.rb +13 -0
- data/lib/uchi/pagination/controller.rb +26 -0
- data/lib/uchi/pagination/page.rb +20 -0
- data/lib/uchi/pagy/LICENSE.txt +21 -0
- data/lib/uchi/pagy/classes/exceptions.rb +35 -0
- data/lib/uchi/pagy/classes/offset/offset.rb +56 -0
- data/lib/uchi/pagy/classes/request.rb +38 -0
- data/lib/uchi/pagy/modules/abilities/configurable.rb +38 -0
- data/lib/uchi/pagy/modules/abilities/linkable.rb +62 -0
- data/lib/uchi/pagy/modules/abilities/rangeable.rb +17 -0
- data/lib/uchi/pagy/modules/abilities/shiftable.rb +14 -0
- data/lib/uchi/pagy/modules/console.rb +40 -0
- data/lib/uchi/pagy/toolbox/helpers/loader.rb +19 -0
- data/lib/uchi/pagy/toolbox/helpers/page_url.rb +25 -0
- data/lib/uchi/pagy/toolbox/paginators/method.rb +21 -0
- data/lib/uchi/pagy/toolbox/paginators/offset.rb +35 -0
- data/lib/uchi/pagy.rb +60 -0
- data/lib/uchi/repository/routes.rb +62 -0
- data/lib/uchi/repository/translate.rb +284 -0
- data/lib/uchi/repository.rb +156 -0
- data/lib/uchi/sort_order.rb +35 -0
- data/lib/uchi/version.rb +5 -0
- data/lib/uchi.rb +18 -0
- data/uchi.gemspec +35 -0
- metadata +197 -107
- data/.github/dependabot.yml +0 -17
- data/.github/workflows/build.yml +0 -23
- data/.github/workflows/lint.yml +0 -30
- data/package.json +0 -31
- data/sig/uchi.rbs +0 -4
- data/test/components/uchi/field/belongs_to_test.rb +0 -134
- data/test/components/uchi/field/blank_test.rb +0 -119
- data/test/components/uchi/field/boolean_test.rb +0 -163
- data/test/components/uchi/field/date_test.rb +0 -163
- data/test/components/uchi/field/date_time_test.rb +0 -152
- data/test/components/uchi/field/has_many_test.rb +0 -138
- data/test/components/uchi/field/id_test.rb +0 -113
- data/test/components/uchi/field/number_test.rb +0 -163
- data/test/components/uchi/field/string_test.rb +0 -159
- data/test/controllers/uchi/authors_controller_test.rb +0 -119
- data/test/controllers/uchi/repository_controller_test.rb +0 -93
- data/test/controllers/uchi/scoped_repository_controller_test.rb +0 -73
- data/test/dummy/Rakefile +0 -6
- data/test/dummy/app/assets/images/.keep +0 -0
- data/test/dummy/app/assets/stylesheets/application.css +0 -15
- data/test/dummy/app/controllers/application_controller.rb +0 -4
- data/test/dummy/app/controllers/concerns/.keep +0 -0
- data/test/dummy/app/controllers/uchi/authors_controller.rb +0 -7
- data/test/dummy/app/controllers/uchi/books_controller.rb +0 -7
- data/test/dummy/app/controllers/uchi/titles_controller.rb +0 -7
- data/test/dummy/app/helpers/application_helper.rb +0 -2
- data/test/dummy/app/jobs/application_job.rb +0 -7
- data/test/dummy/app/mailers/application_mailer.rb +0 -4
- data/test/dummy/app/models/application_record.rb +0 -3
- data/test/dummy/app/models/author.rb +0 -3
- data/test/dummy/app/models/book.rb +0 -3
- data/test/dummy/app/models/concerns/.keep +0 -0
- data/test/dummy/app/models/title.rb +0 -3
- data/test/dummy/app/uchi/repositories/author.rb +0 -20
- data/test/dummy/app/uchi/repositories/book.rb +0 -16
- data/test/dummy/app/uchi/repositories/title.rb +0 -17
- data/test/dummy/app/views/layouts/application.html.erb +0 -27
- data/test/dummy/app/views/layouts/mailer.html.erb +0 -13
- data/test/dummy/app/views/layouts/mailer.text.erb +0 -1
- data/test/dummy/app/views/pwa/manifest.json.erb +0 -22
- data/test/dummy/app/views/pwa/service-worker.js +0 -26
- data/test/dummy/bin/dev +0 -2
- data/test/dummy/bin/rails +0 -4
- data/test/dummy/bin/rake +0 -4
- data/test/dummy/bin/setup +0 -34
- data/test/dummy/config/application.rb +0 -29
- data/test/dummy/config/boot.rb +0 -5
- data/test/dummy/config/cable.yml +0 -10
- data/test/dummy/config/database.yml +0 -32
- data/test/dummy/config/environment.rb +0 -5
- data/test/dummy/config/environments/development.rb +0 -69
- data/test/dummy/config/environments/production.rb +0 -89
- data/test/dummy/config/environments/test.rb +0 -53
- data/test/dummy/config/initializers/content_security_policy.rb +0 -25
- data/test/dummy/config/initializers/filter_parameter_logging.rb +0 -8
- data/test/dummy/config/initializers/inflections.rb +0 -16
- data/test/dummy/config/locales/da.yml +0 -51
- data/test/dummy/config/locales/en.yml +0 -31
- data/test/dummy/config/puma.rb +0 -38
- data/test/dummy/config/routes.rb +0 -9
- data/test/dummy/config/storage.yml +0 -34
- data/test/dummy/config.ru +0 -6
- data/test/dummy/db/migrate/20251002183635_create_authors.rb +0 -11
- data/test/dummy/db/migrate/20251005131726_create_books.rb +0 -9
- data/test/dummy/db/migrate/20251005131811_create_titles.rb +0 -11
- data/test/dummy/db/schema.rb +0 -38
- data/test/dummy/log/.keep +0 -0
- data/test/dummy/public/400.html +0 -114
- data/test/dummy/public/404.html +0 -114
- data/test/dummy/public/406-unsupported-browser.html +0 -114
- data/test/dummy/public/422.html +0 -114
- data/test/dummy/public/500.html +0 -114
- data/test/dummy/public/icon.png +0 -0
- data/test/dummy/public/icon.svg +0 -3
- data/test/dummy/storage/.keep +0 -0
- data/test/dummy/test/fixtures/authors.yml +0 -11
- data/test/dummy/test/models/author_test.rb +0 -7
- data/test/dummy/tmp/.keep +0 -0
- data/test/dummy/tmp/pids/.keep +0 -0
- data/test/dummy/tmp/storage/.keep +0 -0
- data/test/test_helper.rb +0 -15
- data/test/uchi/field_test.rb +0 -63
- data/test/uchi/i18n_test.rb +0 -18
- data/test/uchi/repository/routes_test.rb +0 -49
- data/test/uchi/repository/translate_test.rb +0 -263
- data/test/uchi/repository_test.rb +0 -137
- data/test/uchi/sort_order_test.rb +0 -47
- data/test/uchi_test.rb +0 -7
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Uchi
|
|
4
|
+
class Pagy
|
|
5
|
+
module Loader
|
|
6
|
+
paths = { public: { page_url: 'page_url' } }.freeze
|
|
7
|
+
|
|
8
|
+
paths.each do |visibility, methods|
|
|
9
|
+
send(visibility)
|
|
10
|
+
# Load the method, overriding its own alias. Next requests will call the method directly.
|
|
11
|
+
define_method(:"load_#{visibility}") do |*args, **kwargs|
|
|
12
|
+
require_relative methods[__callee__]
|
|
13
|
+
send(__callee__, *args, **kwargs)
|
|
14
|
+
end
|
|
15
|
+
methods.each_key { |method| alias_method method, :"load_#{visibility}" }
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Uchi
|
|
4
|
+
class Pagy
|
|
5
|
+
# Return the page url for any page
|
|
6
|
+
# :nocov:
|
|
7
|
+
def page_url(page, **)
|
|
8
|
+
case page
|
|
9
|
+
when :first
|
|
10
|
+
compose_page_url(nil, **)
|
|
11
|
+
when :current
|
|
12
|
+
compose_page_url(@page, **) if @page
|
|
13
|
+
when :previous
|
|
14
|
+
compose_page_url(@previous, **) if @previous
|
|
15
|
+
when :next
|
|
16
|
+
compose_page_url(@next, **) if @next
|
|
17
|
+
when :last
|
|
18
|
+
compose_page_url(@last, **) if @last
|
|
19
|
+
when Integer, String
|
|
20
|
+
compose_page_url(page, **)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
# :nocov:
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../classes/request'
|
|
4
|
+
|
|
5
|
+
module Uchi
|
|
6
|
+
class Pagy
|
|
7
|
+
paginators = { offset: :OffsetPaginator }.freeze
|
|
8
|
+
|
|
9
|
+
path = Pathname.new(__dir__)
|
|
10
|
+
paginators.each { |symbol, name| autoload name, path.join(symbol.to_s) }
|
|
11
|
+
|
|
12
|
+
# Defines the pagy method. Include in the app controller/view.
|
|
13
|
+
Method = Module.new do
|
|
14
|
+
protected
|
|
15
|
+
|
|
16
|
+
define_method :pagy do |paginator = :offset, collection, **options|
|
|
17
|
+
Pagy.const_get(paginators[paginator]).paginate(self, collection, **Pagy.options, **options)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Uchi
|
|
4
|
+
class Pagy
|
|
5
|
+
module OffsetPaginator
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
# Return instance and page of results
|
|
9
|
+
def paginate(context, collection, **options)
|
|
10
|
+
context.instance_eval do
|
|
11
|
+
request = Request.new(options[:request] || self.request, options)
|
|
12
|
+
options[:page] ||= request.resolve_page(options)
|
|
13
|
+
options[:limit] = request.resolve_limit(options)
|
|
14
|
+
options[:count] ||= collection.instance_of?(Array) ? collection.size : OffsetPaginator.get_count(collection, options)
|
|
15
|
+
pagy = Offset.new(**options, request:)
|
|
16
|
+
[pagy, collection.instance_of?(Array) ? collection[pagy.offset, pagy.limit] : pagy.records(collection)]
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Get the collection count
|
|
21
|
+
def get_count(collection, options)
|
|
22
|
+
return collection.count unless defined?(::ActiveRecord) && collection.is_a?(::ActiveRecord::Relation)
|
|
23
|
+
|
|
24
|
+
count = if options[:count_over] && !collection.group_values.empty?
|
|
25
|
+
# COUNT(*) OVER ()
|
|
26
|
+
sql = Arel.star.count.over(Arel::Nodes::Grouping.new([]))
|
|
27
|
+
collection.unscope(:order).pick(sql).to_i
|
|
28
|
+
else
|
|
29
|
+
collection.count(:all)
|
|
30
|
+
end
|
|
31
|
+
count.is_a?(Hash) ? count.size : count
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
data/lib/uchi/pagy.rb
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'pathname'
|
|
4
|
+
require_relative 'pagy/classes/exceptions'
|
|
5
|
+
require_relative 'pagy/modules/abilities/linkable'
|
|
6
|
+
require_relative 'pagy/modules/abilities/configurable'
|
|
7
|
+
require_relative 'pagy/toolbox/helpers/loader'
|
|
8
|
+
|
|
9
|
+
module Uchi
|
|
10
|
+
# Top superclass: it defines only what's common to all the subclasses
|
|
11
|
+
class Pagy
|
|
12
|
+
VERSION = '43.0.0.rc4'
|
|
13
|
+
ROOT = Pathname.new(__dir__).parent.freeze
|
|
14
|
+
DEFAULT = { limit: 20, limit_key: 'limit', page_key: 'page' }.freeze
|
|
15
|
+
PAGE_TOKEN = 'P '
|
|
16
|
+
LIMIT_TOKEN = 'L '
|
|
17
|
+
LABEL_TOKEN = 'L'
|
|
18
|
+
A_TAG = '<a style="display: none;">#</a>'
|
|
19
|
+
|
|
20
|
+
path = Pathname.new(__FILE__).sub_ext('')
|
|
21
|
+
autoload :Method, path.join('toolbox/paginators/method')
|
|
22
|
+
autoload :I18n, path.join('modules/i18n/i18n')
|
|
23
|
+
autoload :Console, path.join('modules/console')
|
|
24
|
+
autoload :Offset, path.join('classes/offset/offset')
|
|
25
|
+
|
|
26
|
+
def self.options = @options ||= {}
|
|
27
|
+
|
|
28
|
+
extend Configurable
|
|
29
|
+
include Linkable
|
|
30
|
+
include Loader
|
|
31
|
+
|
|
32
|
+
attr_reader :page, :next, :in, :limit, :options
|
|
33
|
+
|
|
34
|
+
protected
|
|
35
|
+
|
|
36
|
+
# Define the hierarchical identity methods, overridden by the respective classes
|
|
37
|
+
def offset? = false
|
|
38
|
+
def countless? = false
|
|
39
|
+
|
|
40
|
+
# Validates and assign the passed options: var must be present and value.to_i must be >= to min
|
|
41
|
+
def assign_and_check(name_min)
|
|
42
|
+
name_min.each do |name, min|
|
|
43
|
+
raise OptionError.new(self, name, ">= #{min}", @options[name]) \
|
|
44
|
+
unless @options[name].respond_to?(:to_i) && instance_variable_set(:"@#{name}", @options[name].to_i) >= min
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Merge all the DEFAULT constants of the class hierarchy with the options
|
|
49
|
+
def assign_options(**options)
|
|
50
|
+
@request = options.delete(:request) # internal object
|
|
51
|
+
default = {}
|
|
52
|
+
current = self.class
|
|
53
|
+
begin
|
|
54
|
+
default = current::DEFAULT.merge(default)
|
|
55
|
+
current = current.superclass
|
|
56
|
+
end until current == Object # rubocop:disable Lint/Loop -- see https://github.com/rubocop/rubocop-performance/issues/362
|
|
57
|
+
@options = default.merge!(options.delete_if { |k, v| default.key?(k) && (v.nil? || v == '') }).freeze
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Uchi
|
|
4
|
+
class Repository
|
|
5
|
+
class Routes
|
|
6
|
+
attr_reader :plural, :repository, :singular
|
|
7
|
+
|
|
8
|
+
def initialize(repository)
|
|
9
|
+
@repository = repository
|
|
10
|
+
@singular = repository.controller_name.singularize
|
|
11
|
+
@plural = repository.controller_name.pluralize
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def path_for(action, **options)
|
|
15
|
+
action = action.to_sym
|
|
16
|
+
case action
|
|
17
|
+
when :destroy, :edit, :new, :show, :update
|
|
18
|
+
singular_path_for(action, **options)
|
|
19
|
+
else
|
|
20
|
+
plural_path_for(action, **options)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def root_path
|
|
25
|
+
"/#{uchi_namespace}/"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def call_url_helper_in_main_app(parts, **options)
|
|
31
|
+
url_helper_name = parts.join("_")
|
|
32
|
+
Rails.application.routes.url_helpers.send(url_helper_name, **options)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def plural_path_for(_action, **options)
|
|
36
|
+
parts = [
|
|
37
|
+
uchi_namespace,
|
|
38
|
+
plural,
|
|
39
|
+
"path"
|
|
40
|
+
].compact
|
|
41
|
+
call_url_helper_in_main_app(parts, **options)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def singular_path_for(action, **options)
|
|
45
|
+
action = nil if action == :show
|
|
46
|
+
action = nil if action == :update
|
|
47
|
+
action = nil if action == :destroy
|
|
48
|
+
parts = [
|
|
49
|
+
action,
|
|
50
|
+
uchi_namespace,
|
|
51
|
+
singular,
|
|
52
|
+
"path"
|
|
53
|
+
].compact
|
|
54
|
+
call_url_helper_in_main_app(parts, **options)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def uchi_namespace
|
|
58
|
+
:uchi
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Uchi
|
|
4
|
+
class Repository
|
|
5
|
+
class Translate
|
|
6
|
+
attr_reader :repository
|
|
7
|
+
|
|
8
|
+
# Returns the breadcrumb label for the given page.
|
|
9
|
+
#
|
|
10
|
+
# Example translation key:
|
|
11
|
+
# uchi.repository.author.breadcrumb.edit.label
|
|
12
|
+
def breadcrumb_label(page, record: nil)
|
|
13
|
+
return breadcrumb_label_for_index if page.to_sym == :index
|
|
14
|
+
|
|
15
|
+
default = {
|
|
16
|
+
show: title_for_record(record)
|
|
17
|
+
}[page.to_sym].presence
|
|
18
|
+
default ||= translate("common.#{page}", default: page.to_s.capitalize)
|
|
19
|
+
|
|
20
|
+
translate(
|
|
21
|
+
"label",
|
|
22
|
+
default: default,
|
|
23
|
+
model: singular_name,
|
|
24
|
+
record: title_for_record(record),
|
|
25
|
+
scope: i18n_scope("breadcrumb.#{page}")
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Returns the breadcrumb label for the index page.
|
|
30
|
+
#
|
|
31
|
+
# Returns the first of the following that is present:
|
|
32
|
+
# 1. Translation from "uchi.repository.author.breadcrumb.index.label"
|
|
33
|
+
# 2. Translation from "uchi.repository.author.index.title"
|
|
34
|
+
# 3. plural name of the model
|
|
35
|
+
# 4. Translation from "common.index"
|
|
36
|
+
# 5. Capitalized page name ("Index")
|
|
37
|
+
def breadcrumb_label_for_index
|
|
38
|
+
first_present_value(
|
|
39
|
+
translate(i18n_scope("breadcrumb.index.label"), default: nil),
|
|
40
|
+
translate(i18n_scope("index.title"), default: nil),
|
|
41
|
+
plural_name,
|
|
42
|
+
translate("common.index"),
|
|
43
|
+
"Index"
|
|
44
|
+
)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Returns a description for the given page, or nil if none is found.
|
|
48
|
+
# This description is intended to provide additional context for the page
|
|
49
|
+
# being shown.
|
|
50
|
+
def description(page, record: nil)
|
|
51
|
+
translate(
|
|
52
|
+
"description",
|
|
53
|
+
default: nil,
|
|
54
|
+
model: singular_name,
|
|
55
|
+
record: record,
|
|
56
|
+
scope: i18n_scope(page)
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Returns a title for a dialog with the given name, e.g. :destroy.
|
|
61
|
+
# The title may include interpolation keys such as %{record}.
|
|
62
|
+
#
|
|
63
|
+
# Example translation key:
|
|
64
|
+
# uchi.repository.author.dialog.destroy.title
|
|
65
|
+
#
|
|
66
|
+
# Example default value:
|
|
67
|
+
# Are you sure you want to delete %{record}?
|
|
68
|
+
#
|
|
69
|
+
# Note that the default value itself is also looked up in the "common"
|
|
70
|
+
# scope, so that it can be shared across repositories.
|
|
71
|
+
#
|
|
72
|
+
# Example fallback translation key:
|
|
73
|
+
# common.dialog.destroy.title
|
|
74
|
+
#
|
|
75
|
+
# Example fallback default value:
|
|
76
|
+
# Are you sure you want to delete this record?
|
|
77
|
+
def destroy_dialog_title(record = nil)
|
|
78
|
+
translate(
|
|
79
|
+
"dialog.destroy.title",
|
|
80
|
+
default: translate(
|
|
81
|
+
"common.dialog.destroy.title",
|
|
82
|
+
default: "Are you sure?",
|
|
83
|
+
record: repository.title(record)
|
|
84
|
+
),
|
|
85
|
+
record: repository.title(record),
|
|
86
|
+
scope: "uchi.repository.#{i18n_key}"
|
|
87
|
+
)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def failed_destroy
|
|
91
|
+
translate(
|
|
92
|
+
"destroy.failure",
|
|
93
|
+
default: translate(
|
|
94
|
+
"destroy.failure",
|
|
95
|
+
default: "The record could not be deleted",
|
|
96
|
+
scope: "common"
|
|
97
|
+
),
|
|
98
|
+
scope: "uchi.repository.#{i18n_key}"
|
|
99
|
+
)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def initialize(repository:)
|
|
103
|
+
@repository = repository
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Returns the label for the given field.
|
|
107
|
+
def field_label(field)
|
|
108
|
+
translate(
|
|
109
|
+
"label",
|
|
110
|
+
default: model.human_attribute_name(field.name),
|
|
111
|
+
scope: i18n_scope("field.#{field.name}")
|
|
112
|
+
)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Returns the hint for the given field, or nil if none is found.
|
|
116
|
+
def field_hint(field)
|
|
117
|
+
translate(
|
|
118
|
+
"hint",
|
|
119
|
+
default: nil,
|
|
120
|
+
scope: i18n_scope("field.#{field.name}")
|
|
121
|
+
)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def link_to_cancel
|
|
125
|
+
translate("common.cancel", default: "Cancel")
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def link_to_destroy(record)
|
|
129
|
+
translate(
|
|
130
|
+
"link_to_destroy",
|
|
131
|
+
default: "Delete",
|
|
132
|
+
model: singular_name,
|
|
133
|
+
record: repository.title(record),
|
|
134
|
+
scope: i18n_scope("button")
|
|
135
|
+
)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def link_to_edit(record)
|
|
139
|
+
translate(
|
|
140
|
+
"link_to_edit",
|
|
141
|
+
default: "Edit",
|
|
142
|
+
model: singular_name,
|
|
143
|
+
record: repository.title(record),
|
|
144
|
+
scope: i18n_scope("button")
|
|
145
|
+
)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Returns the text for the "new" action link.
|
|
149
|
+
def link_to_new
|
|
150
|
+
translate(
|
|
151
|
+
"link_to_new",
|
|
152
|
+
default: "New %{model}", # rubocop:disable Style/FormatStringToken
|
|
153
|
+
model: singular_name,
|
|
154
|
+
scope: i18n_scope("button")
|
|
155
|
+
)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def loading_message
|
|
159
|
+
translate("loading", default: "Loading...", scope: "uchi.repository.common")
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Returns the localized, human-readable plural name of the model this
|
|
163
|
+
# repository manages.
|
|
164
|
+
def plural_name
|
|
165
|
+
::I18n.translate(
|
|
166
|
+
"uchi.repository.#{i18n_key}.model",
|
|
167
|
+
count: 2,
|
|
168
|
+
default: model.model_name.plural.humanize
|
|
169
|
+
)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def no_records_found
|
|
173
|
+
translate("no_records_found", default: "No records found", scope: "uchi.common")
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def search_label
|
|
177
|
+
translate("common.search", default: "Search")
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def search_button
|
|
181
|
+
translate("common.search", default: "Search")
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Returns the label for a generic submit button
|
|
185
|
+
def submit_button
|
|
186
|
+
translate("common.save", default: "Save")
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def successful_create
|
|
190
|
+
translate(
|
|
191
|
+
"create.success",
|
|
192
|
+
default: translate(
|
|
193
|
+
"create.success",
|
|
194
|
+
default: "Your changes have been saved",
|
|
195
|
+
scope: "common"
|
|
196
|
+
),
|
|
197
|
+
scope: "uchi.repository.#{i18n_key}"
|
|
198
|
+
)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def successful_destroy
|
|
202
|
+
translate(
|
|
203
|
+
"destroy.success",
|
|
204
|
+
default: translate(
|
|
205
|
+
"destroy.success",
|
|
206
|
+
default: "The record has been deleted",
|
|
207
|
+
scope: "common"
|
|
208
|
+
),
|
|
209
|
+
scope: "uchi.repository.#{i18n_key}"
|
|
210
|
+
)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def successful_update
|
|
214
|
+
translate(
|
|
215
|
+
"update.success",
|
|
216
|
+
default: translate(
|
|
217
|
+
"update.success",
|
|
218
|
+
default: "Your changes have been saved",
|
|
219
|
+
scope: "common"
|
|
220
|
+
),
|
|
221
|
+
scope: "uchi.repository.#{i18n_key}"
|
|
222
|
+
)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Returns the title for the given page.
|
|
226
|
+
def title(page, record: nil)
|
|
227
|
+
return repository.title(record) if record && page == :show
|
|
228
|
+
|
|
229
|
+
translate(
|
|
230
|
+
"title",
|
|
231
|
+
default: plural_name,
|
|
232
|
+
model: singular_name,
|
|
233
|
+
record: record,
|
|
234
|
+
scope: i18n_scope(page)
|
|
235
|
+
)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
private
|
|
239
|
+
|
|
240
|
+
# Returns the first translation that yields a present value by looking
|
|
241
|
+
# through each key in keys in order.
|
|
242
|
+
def first_present_value(*values)
|
|
243
|
+
values.find(&:presence)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Returns the segment of the i18n key specific to this repository.
|
|
247
|
+
def i18n_key
|
|
248
|
+
@repository.model.model_name.i18n_key
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def i18n_scope(section = nil)
|
|
252
|
+
[
|
|
253
|
+
"uchi.repository",
|
|
254
|
+
i18n_key,
|
|
255
|
+
section
|
|
256
|
+
].compact.join(".")
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def model
|
|
260
|
+
@model ||= repository.model
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# Returns the localized, human-readable singular name of the model this
|
|
264
|
+
# repository manages.
|
|
265
|
+
def singular_name
|
|
266
|
+
::I18n.translate(
|
|
267
|
+
"uchi.repository.#{i18n_key}.model",
|
|
268
|
+
count: 1,
|
|
269
|
+
default: model.model_name.human(count: 1)
|
|
270
|
+
)
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def title_for_record(record)
|
|
274
|
+
return nil unless record
|
|
275
|
+
|
|
276
|
+
repository.title(record)
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def translate(key, **options)
|
|
280
|
+
Uchi::I18n.translate(key, **options)
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
end
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "repository/routes"
|
|
4
|
+
|
|
5
|
+
module Uchi
|
|
6
|
+
class Repository
|
|
7
|
+
class << self
|
|
8
|
+
# Returns all defined Uchi::Repository classes
|
|
9
|
+
def all
|
|
10
|
+
Uchi::Repositories.constants.map { |const_name|
|
|
11
|
+
Uchi::Repositories.const_get(const_name)
|
|
12
|
+
}
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Returns the repository for the given model, or nil if none is found.
|
|
16
|
+
def for_model(model)
|
|
17
|
+
all.find { |repository| repository.model.to_s == model.to_s }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Returns the model class this repository manages.
|
|
21
|
+
def model
|
|
22
|
+
@model ||= name.demodulize.constantize
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Returns a new, unsaved instance of the model this repository manages.
|
|
27
|
+
def build(attributes = {})
|
|
28
|
+
model.new(attributes)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Returns the "name" of the controller that handles requests for this
|
|
32
|
+
# repository. Note that this is different from the controllers class name
|
|
33
|
+
# and is intended for generating URLs.
|
|
34
|
+
def controller_name
|
|
35
|
+
model_param_key.pluralize
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def default_sort_order
|
|
39
|
+
SortOrder.new(:id, :asc)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Returns an array of fields to show on the edit page.
|
|
43
|
+
def fields_for_edit
|
|
44
|
+
fields.select { |field| field.on.include?(:edit) }.each { |field| field.repository = self }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Returns an array of fields to show on the index page.
|
|
48
|
+
def fields_for_index
|
|
49
|
+
fields.select { |field| field.on.include?(:index) }.each { |field| field.repository = self }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Returns an array of fields to show on the show page.
|
|
53
|
+
def fields_for_show
|
|
54
|
+
fields.select { |field| field.on.include?(:show) }.each { |field| field.repository = self }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def find_all(search: nil, scope: model.all, sort_order: default_sort_order)
|
|
58
|
+
scope ||= model.all
|
|
59
|
+
query = scope.includes(includes)
|
|
60
|
+
query = apply_search(query, search)
|
|
61
|
+
apply_sort_order(query, sort_order)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def find(id)
|
|
65
|
+
model.find(id)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Returns the list of associations to include when querying for records.
|
|
69
|
+
#
|
|
70
|
+
# See
|
|
71
|
+
# https://api.rubyonrails.org/classes/ActiveRecord/QueryMethods.html#method-i-includes
|
|
72
|
+
# for further details.
|
|
73
|
+
def includes
|
|
74
|
+
[]
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def model
|
|
78
|
+
self.class.model
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def model_param_key
|
|
82
|
+
model.model_name.param_key
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Returns an instance of Uchi::Repository::Routes for this repository,
|
|
86
|
+
# which can be used to generate paths and URLs.
|
|
87
|
+
#
|
|
88
|
+
# @return [Uchi::Repository::Routes]
|
|
89
|
+
def routes
|
|
90
|
+
@routes ||= Routes.new(self)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Returns true if this repository has at least one searchable field.
|
|
94
|
+
#
|
|
95
|
+
# @return [Boolean]
|
|
96
|
+
def searchable?
|
|
97
|
+
searchable_fields.any?
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Returns the title to show for a given record. By default, this method
|
|
101
|
+
# returns the value of the first of the following methods that exist:
|
|
102
|
+
#
|
|
103
|
+
# 1. `name`
|
|
104
|
+
# 2. `title`
|
|
105
|
+
# 3. `to_s`
|
|
106
|
+
#
|
|
107
|
+
# You can override this method in your repository subclass to provide
|
|
108
|
+
# custom logic.
|
|
109
|
+
def title(record)
|
|
110
|
+
return nil unless record
|
|
111
|
+
|
|
112
|
+
[:name, :title, :to_s].each do |method|
|
|
113
|
+
if record.respond_to?(method)
|
|
114
|
+
return record.public_send(method)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Provides access to translation helpers specific to this repository.
|
|
120
|
+
def translate
|
|
121
|
+
@translate ||= Translate.new(repository: self)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
private
|
|
125
|
+
|
|
126
|
+
def apply_search(query, search)
|
|
127
|
+
return query unless search.present?
|
|
128
|
+
return query if searchable_fields.empty?
|
|
129
|
+
|
|
130
|
+
search = search.strip
|
|
131
|
+
conditions = searchable_fields.map { |field|
|
|
132
|
+
arel_field = model.arel_table[field.name]
|
|
133
|
+
Arel::Nodes::NamedFunction.new(
|
|
134
|
+
"CAST",
|
|
135
|
+
[arel_field.as(Arel::Nodes::SqlLiteral.new("VARCHAR"))]
|
|
136
|
+
).matches("%#{search}%")
|
|
137
|
+
}
|
|
138
|
+
query.where(conditions.inject(:or))
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def apply_sort_order(query, sort_order)
|
|
142
|
+
field_to_sort_by = fields.find { |field| field.name == sort_order.name }
|
|
143
|
+
return query unless field_to_sort_by
|
|
144
|
+
|
|
145
|
+
if field_to_sort_by.sortable.respond_to?(:call)
|
|
146
|
+
field_to_sort_by.sortable.call(query, sort_order.direction)
|
|
147
|
+
else
|
|
148
|
+
sort_order.apply(query)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def searchable_fields
|
|
153
|
+
@searchable_fields ||= fields.select { |field| field.searchable? }
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module Uchi
|
|
2
|
+
class SortOrder
|
|
3
|
+
attr_reader :name, :direction
|
|
4
|
+
|
|
5
|
+
class << self
|
|
6
|
+
def from_params(params)
|
|
7
|
+
sort_params = params[:sort] || {}
|
|
8
|
+
return nil if sort_params.empty?
|
|
9
|
+
|
|
10
|
+
by = sort_params[:by]
|
|
11
|
+
return by unless by
|
|
12
|
+
|
|
13
|
+
direction = sort_params[:direction] || :asc
|
|
14
|
+
new(by, direction)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def ascending?
|
|
19
|
+
direction == :asc
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def apply(query)
|
|
23
|
+
query.order(name => direction)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def descending?
|
|
27
|
+
direction == :desc
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def initialize(name, direction)
|
|
31
|
+
@name = name.to_sym
|
|
32
|
+
@direction = direction.to_sym
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|