fustrate-rails 0.4.1 → 0.10.1
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 +5 -5
- data/config/initializers/jbuilder.rb +13 -0
- data/config/initializers/renderers.rb +40 -0
- data/config/initializers/sanitize.rb +112 -0
- data/config/rubocop/default.yml +96 -0
- data/config/rubocop/rails.yml +22 -0
- data/lib/fustrate/rails/concerns/clean_attributes.rb +49 -0
- data/lib/fustrate/rails/concerns/model.rb +48 -0
- data/lib/fustrate/rails/concerns/sanitize_html.rb +34 -0
- data/lib/fustrate/rails/engine.rb +14 -7
- data/lib/fustrate/rails/services/base.rb +46 -0
- data/lib/fustrate/rails/services/generate_csv.rb +35 -0
- data/lib/fustrate/rails/services/generate_excel.rb +32 -0
- data/lib/fustrate/rails/services/log_edit.rb +132 -0
- data/lib/fustrate/rails/spec_helper.rb +62 -0
- data/lib/fustrate/rails/version.rb +5 -1
- data/lib/fustrate-rails.rb +5 -2
- metadata +44 -140
- data/vendor/assets/javascripts/awesomplete.js +0 -402
- data/vendor/assets/javascripts/fustrate/_module.coffee +0 -140
- data/vendor/assets/javascripts/fustrate/components/_module.coffee +0 -3
- data/vendor/assets/javascripts/fustrate/components/alert_box.coffee +0 -10
- data/vendor/assets/javascripts/fustrate/components/autocomplete.coffee +0 -161
- data/vendor/assets/javascripts/fustrate/components/disclosure.coffee +0 -12
- data/vendor/assets/javascripts/fustrate/components/drop_zone.coffee +0 -9
- data/vendor/assets/javascripts/fustrate/components/dropdown.coffee +0 -48
- data/vendor/assets/javascripts/fustrate/components/file_picker.coffee +0 -11
- data/vendor/assets/javascripts/fustrate/components/flash.coffee +0 -31
- data/vendor/assets/javascripts/fustrate/components/modal.coffee +0 -273
- data/vendor/assets/javascripts/fustrate/components/pagination.coffee +0 -84
- data/vendor/assets/javascripts/fustrate/components/tabs.coffee +0 -28
- data/vendor/assets/javascripts/fustrate/components/tooltip.coffee +0 -72
- data/vendor/assets/javascripts/fustrate/generic_form.coffee +0 -30
- data/vendor/assets/javascripts/fustrate/generic_page.coffee +0 -40
- data/vendor/assets/javascripts/fustrate/generic_table.coffee +0 -57
- data/vendor/assets/javascripts/fustrate/listenable.coffee +0 -25
- data/vendor/assets/javascripts/fustrate/object.coffee +0 -21
- data/vendor/assets/javascripts/fustrate/record.coffee +0 -23
- data/vendor/assets/javascripts/fustrate.coffee +0 -6
- data/vendor/assets/stylesheets/_fustrate.sass +0 -7
- data/vendor/assets/stylesheets/awesomplete.sass +0 -76
- data/vendor/assets/stylesheets/fustrate/_colors.sass +0 -12
- data/vendor/assets/stylesheets/fustrate/_settings.sass +0 -20
- data/vendor/assets/stylesheets/fustrate/components/_components.sass +0 -36
- data/vendor/assets/stylesheets/fustrate/components/_functions.sass +0 -41
- data/vendor/assets/stylesheets/fustrate/components/alerts.sass +0 -86
- data/vendor/assets/stylesheets/fustrate/components/buttons.sass +0 -99
- data/vendor/assets/stylesheets/fustrate/components/disclosures.sass +0 -23
- data/vendor/assets/stylesheets/fustrate/components/dropdowns.sass +0 -36
- data/vendor/assets/stylesheets/fustrate/components/flash.sass +0 -38
- data/vendor/assets/stylesheets/fustrate/components/forms.sass +0 -195
- data/vendor/assets/stylesheets/fustrate/components/grid.sass +0 -196
- data/vendor/assets/stylesheets/fustrate/components/labels.sass +0 -64
- data/vendor/assets/stylesheets/fustrate/components/modals.sass +0 -167
- data/vendor/assets/stylesheets/fustrate/components/pagination.sass +0 -69
- data/vendor/assets/stylesheets/fustrate/components/panels.sass +0 -67
- data/vendor/assets/stylesheets/fustrate/components/popovers.sass +0 -19
- data/vendor/assets/stylesheets/fustrate/components/tables.sass +0 -62
- data/vendor/assets/stylesheets/fustrate/components/tabs.sass +0 -44
- data/vendor/assets/stylesheets/fustrate/components/tooltips.sass +0 -28
- data/vendor/assets/stylesheets/fustrate/components/typography.sass +0 -391
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 881e74c991300471aafa028edfad4b01f100244396234b0da2688db01e2073be
|
4
|
+
data.tar.gz: 6cf4952d8518fe41835ea8bb4c13f6764c8aed89a97c43e6814af3d61cc787a9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0da0314b9449d2dd31c7ad36dfb515094e91564b0805c8618d9a18bca6a9ff7930e154575f29070f7eff24a3df666d4c967a3af0ed7366234ed79b82ea7f9226
|
7
|
+
data.tar.gz: 8f0cd3d69cc37559f7c011f49e50396abb4f535b2cd665b8bb3d5926f765c6dd67abd75c93080686d5166f7bce3df4b7898684532a6a20365cc5395d0b3f8f3b
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright (c) Steven Hoffman
|
4
|
+
# All rights reserved.
|
5
|
+
|
6
|
+
class Jbuilder
|
7
|
+
def pagination!(results)
|
8
|
+
_set_value :currentPage, results.current_page
|
9
|
+
_set_value :totalPages, results.total_pages
|
10
|
+
_set_value :totalEntries, results.total_entries
|
11
|
+
_set_value :perPage, results.per_page
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright (c) Steven Hoffman
|
4
|
+
# All rights reserved.
|
5
|
+
|
6
|
+
::ActionController::Renderers.add :excel do |data, options|
|
7
|
+
name = options[:filename] || 'export'
|
8
|
+
|
9
|
+
xlsx_options = { filename: "#{name}.xlsx", disposition: 'attachment', type: :xlsx }
|
10
|
+
|
11
|
+
case data
|
12
|
+
when ::Axlsx::Package
|
13
|
+
send_data(data.to_stream.read, **xlsx_options)
|
14
|
+
when ::Array
|
15
|
+
send_data(::Fustrate::Rails::Services::GenerateExcel.new.call(data, options[:sheet] || name), **xlsx_options)
|
16
|
+
when ::Pathname
|
17
|
+
xlsx_options[:filename] = data.basename.to_s if xlsx_options[:filename] == 'export'
|
18
|
+
|
19
|
+
send_file(data, **xlsx_options)
|
20
|
+
else
|
21
|
+
send_data(data, **xlsx_options)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
::ActionController::Renderers.add :csv do |data, options|
|
26
|
+
name = options[:filename] || 'export'
|
27
|
+
|
28
|
+
csv_options = { filename: "#{name}.csv", disposition: 'attachment', type: :csv }
|
29
|
+
|
30
|
+
case data
|
31
|
+
when ::Array, ::Hash
|
32
|
+
send_data(::Fustrate::Rails::Services::GenerateCsv.new.call(data), **csv_options)
|
33
|
+
else
|
34
|
+
send_data(data, **csv_options)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
::ActionController::Renderers.add :zip do |path, options|
|
39
|
+
send_file path, type: :zip, filename: "#{options[:filename]}.zip"
|
40
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright (c) Steven Hoffman
|
4
|
+
# All rights reserved.
|
5
|
+
|
6
|
+
class Sanitize
|
7
|
+
class CSS
|
8
|
+
def properties(css)
|
9
|
+
tree = ::Crass.parse_properties(
|
10
|
+
css,
|
11
|
+
preserve_comments: @config[:allow_comments],
|
12
|
+
preserve_hacks: @config[:allow_hacks]
|
13
|
+
)
|
14
|
+
|
15
|
+
tree!(tree)
|
16
|
+
|
17
|
+
::CSSTreeStringifier.new.stringify(tree)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
SELF_CLOSING = %w[br td img hr input].freeze
|
22
|
+
|
23
|
+
class << self
|
24
|
+
def clean_nodes(node)
|
25
|
+
# Replace non-root empty nodes with a blank space
|
26
|
+
return node.replace(' ') if empty_element?(node)
|
27
|
+
|
28
|
+
# Replace root empty nodes with a newline
|
29
|
+
if node.text? && !node.parent&.parent && node.text.blank?
|
30
|
+
# Replacing a newline with a newline causes an infinite loop
|
31
|
+
return node.text == "\n" ? node : node.replace("\n")
|
32
|
+
end
|
33
|
+
|
34
|
+
clean_style_attribute(node)
|
35
|
+
clean_attributes(node)
|
36
|
+
clean_whitespace(node)
|
37
|
+
end
|
38
|
+
|
39
|
+
def clean_page_breaks(node)
|
40
|
+
return unless node.element?
|
41
|
+
|
42
|
+
if node.content.match?(/↡/)
|
43
|
+
node.content = '↡'
|
44
|
+
node['class'] = 'pagebreak'
|
45
|
+
node.remove_attribute 'style'
|
46
|
+
elsif node['class']
|
47
|
+
node.remove_attribute 'class'
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
protected
|
52
|
+
|
53
|
+
# Remove useless padding and text-align attributes
|
54
|
+
def clean_style_attribute(node)
|
55
|
+
return unless node['style']
|
56
|
+
|
57
|
+
style = node['style']
|
58
|
+
.gsub(/padding: 0(?:px|rem|in|em|);/, '')
|
59
|
+
.gsub(/text-align: (?:start|justify|left);/, '')
|
60
|
+
.strip
|
61
|
+
|
62
|
+
if style.present?
|
63
|
+
node['style'] = style
|
64
|
+
else
|
65
|
+
node.remove_attribute 'style'
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Remove empty attributes from a node, e.g. alt=""
|
70
|
+
def clean_attributes(node)
|
71
|
+
return unless node.element?
|
72
|
+
|
73
|
+
node.attributes.each do |key, value|
|
74
|
+
node.remove_attribute(key) if value.text.empty?
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def clean_whitespace(node)
|
79
|
+
return clean_element_whitespace(node) unless node.text?
|
80
|
+
|
81
|
+
node.content = node.content.gsub(/[[:space:]]+/, ' ')
|
82
|
+
end
|
83
|
+
|
84
|
+
def clean_element_whitespace(node)
|
85
|
+
return unless node.name.casecmp('p').zero?
|
86
|
+
|
87
|
+
clean_paragraph_tag(node.children.first, true)
|
88
|
+
clean_paragraph_tag(node.children.last, false)
|
89
|
+
end
|
90
|
+
|
91
|
+
def clean_paragraph_tag(node, is_first)
|
92
|
+
return unless node&.text?
|
93
|
+
|
94
|
+
return node.content = '' if node.text.blank?
|
95
|
+
|
96
|
+
# Remove all leading whitespace and replace all space-like chars with a single space
|
97
|
+
node.content = node.content
|
98
|
+
.gsub(is_first ? /\A[[:space:]]+/ : /[[:space:]]+\z/, '')
|
99
|
+
.gsub(/[[:space:]]+/, ' ')
|
100
|
+
end
|
101
|
+
|
102
|
+
def empty_element?(node) = node.element? && !self_closing?(node) && !content?(node)
|
103
|
+
|
104
|
+
def content?(node) = text?(node) || (node.element? && node.children.any? { content_or_self_closing?(_1) })
|
105
|
+
|
106
|
+
def content_or_self_closing?(node) = self_closing?(node) || text?(node) || content?(node)
|
107
|
+
|
108
|
+
def self_closing?(node) = self::SELF_CLOSING.include?(node.name.downcase)
|
109
|
+
|
110
|
+
def text?(node) = node.text? && node.text.present?
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require:
|
2
|
+
- rubocop-performance
|
3
|
+
|
4
|
+
AllCops:
|
5
|
+
# Always enable new cops, disabling manually when they don't fit.
|
6
|
+
EnabledByDefault: true
|
7
|
+
NewCops: enable
|
8
|
+
SuggestExtensions: false
|
9
|
+
|
10
|
+
# ----------------------------------------------------------------------------------------------------------------------
|
11
|
+
# Coding Style
|
12
|
+
# ----------------------------------------------------------------------------------------------------------------------
|
13
|
+
|
14
|
+
Layout/MultilineMethodCallIndentation:
|
15
|
+
EnforcedStyle: indented
|
16
|
+
|
17
|
+
Lint/SuppressedException:
|
18
|
+
AllowComments: true
|
19
|
+
|
20
|
+
Performance/FlatMap:
|
21
|
+
EnabledForFlattenWithoutParams: true
|
22
|
+
|
23
|
+
Style/DateTime:
|
24
|
+
AllowCoercion: true
|
25
|
+
|
26
|
+
# ----------------------------------------------------------------------------------------------------------------------
|
27
|
+
# Cops that should not be run
|
28
|
+
# ----------------------------------------------------------------------------------------------------------------------
|
29
|
+
|
30
|
+
# Recommends each array item be on its own line.
|
31
|
+
Layout/MultilineArrayLineBreaks:
|
32
|
+
Enabled: false
|
33
|
+
|
34
|
+
# Recommends right hand side of multi-line assignment be on a new line
|
35
|
+
Layout/MultilineAssignmentLayout:
|
36
|
+
Enabled: false
|
37
|
+
|
38
|
+
# Wants every argument in a multi-line method call to be on its own line
|
39
|
+
Layout/MultilineMethodArgumentLineBreaks:
|
40
|
+
Enabled: false
|
41
|
+
|
42
|
+
# Recommends squishing a multi-line hash into one line. I don't like that. It's not readable.
|
43
|
+
Layout/RedundantLineBreak:
|
44
|
+
Enabled: false
|
45
|
+
|
46
|
+
# Recommends `::File` instead of `File`
|
47
|
+
Lint/ConstantResolution:
|
48
|
+
Enabled: false
|
49
|
+
|
50
|
+
# Recommends Integer(xxx, 10) instead of xxx.to_i
|
51
|
+
Lint/NumberConversion:
|
52
|
+
Enabled: false
|
53
|
+
|
54
|
+
# Seriously? "Postmaster" is racist now?
|
55
|
+
Naming/InclusiveLanguage:
|
56
|
+
Enabled: false
|
57
|
+
|
58
|
+
# String#drop is not a function, and besides, arr.drop(n) doesn't read better than arr[n..]
|
59
|
+
Performance/ArraySemiInfiniteRangeSlice:
|
60
|
+
Enabled: false
|
61
|
+
|
62
|
+
# I want to chain flatten/compact/uniq
|
63
|
+
Performance/ChainArrayAllocation:
|
64
|
+
Enabled: false
|
65
|
+
|
66
|
+
# Converts if-elsif to case-when.
|
67
|
+
Style/CaseLikeIf:
|
68
|
+
Enabled: false
|
69
|
+
|
70
|
+
# Wants every constant to be listed in #public_constant or #private_constant
|
71
|
+
Style/ConstantVisibility:
|
72
|
+
Enabled: false
|
73
|
+
|
74
|
+
# Converts [1, 2, three: 3] to [1, 2, { three: 3 }]
|
75
|
+
Style/HashAsLastArrayItem:
|
76
|
+
Enabled: false
|
77
|
+
|
78
|
+
# Prefers `if !condition` over `unless condition`
|
79
|
+
Style/InvertibleUnlessCondition:
|
80
|
+
Enabled: false
|
81
|
+
|
82
|
+
# Converts `a_method 1` to `a_method(1)`
|
83
|
+
Style/MethodCallWithArgsParentheses:
|
84
|
+
Enabled: false
|
85
|
+
|
86
|
+
# Not every `if` needs an `else`
|
87
|
+
Style/MissingElse:
|
88
|
+
Enabled: false
|
89
|
+
|
90
|
+
# Hashes that come from a database JSON column use strings for keys
|
91
|
+
Style/StringHashKeys:
|
92
|
+
Enabled: false
|
93
|
+
|
94
|
+
# I consider myself capable of understanding logical operators in an unless
|
95
|
+
Style/UnlessLogicalOperators:
|
96
|
+
Enabled: false
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require:
|
2
|
+
- rubocop-rails
|
3
|
+
|
4
|
+
AllCops:
|
5
|
+
# Exclude auto-generated files and uploads
|
6
|
+
Exclude:
|
7
|
+
- 'bin/**/*'
|
8
|
+
- 'db/schema.rb'
|
9
|
+
- 'node_modules/**/*'
|
10
|
+
- 'public/**/*'
|
11
|
+
- 'storage/**/*'
|
12
|
+
|
13
|
+
Rails:
|
14
|
+
Enabled: true
|
15
|
+
|
16
|
+
# There are a ton of times I don't want to validate!
|
17
|
+
Rails/SkipsModelValidations:
|
18
|
+
Enabled: false
|
19
|
+
|
20
|
+
# This cop "may detect #to_s calls that are not related to Active Support"
|
21
|
+
Rails/ToSWithArgument:
|
22
|
+
Enabled: false
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright (c) Steven Hoffman
|
4
|
+
# All rights reserved.
|
5
|
+
|
6
|
+
module Fustrate
|
7
|
+
module Rails
|
8
|
+
module Concerns
|
9
|
+
module CleanAttributes
|
10
|
+
extend ::ActiveSupport::Concern
|
11
|
+
|
12
|
+
STRING_TYPES = %i[string text citext].freeze
|
13
|
+
|
14
|
+
# Collapse multiple spaces, remove leading/trailing whitespace, and remove carriage returns
|
15
|
+
def self.strip(text)
|
16
|
+
return text.map { strip(_1) } if text.is_a?(::Array)
|
17
|
+
|
18
|
+
return if text.blank?
|
19
|
+
|
20
|
+
text.strip.gsub(/ {2,}/, ' ').gsub(/^[ \t]+|[ \t]+$/, '').gsub(/\r\n?/, "\n").gsub(/\n{3,}/, "\n\n")
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.string_columns(klass)
|
24
|
+
# There's no reason to clean polymorphic type columns
|
25
|
+
polymorphic_type_columns = klass.reflect_on_all_associations
|
26
|
+
.select { _1.options[:polymorphic] }
|
27
|
+
.map { "#{_1.name}_type" }
|
28
|
+
|
29
|
+
klass.columns
|
30
|
+
.select { self::STRING_TYPES.include?(_1.sql_type_metadata.type) }
|
31
|
+
.reject { polymorphic_type_columns.include?(_1.name) }
|
32
|
+
.map(&:name)
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.clean_record(record)
|
36
|
+
string_columns(record.class).each do |attribute|
|
37
|
+
next unless record[attribute]
|
38
|
+
|
39
|
+
record[attribute] = strip record[attribute]
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
included do
|
44
|
+
before_validation { ::Fustrate::Rails::Concerns::CleanAttributes.clean_record(self) }
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright (c) Steven Hoffman
|
4
|
+
# All rights reserved.
|
5
|
+
|
6
|
+
module Fustrate
|
7
|
+
module Rails
|
8
|
+
module Concerns
|
9
|
+
module Model
|
10
|
+
extend ::ActiveSupport::Concern
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
# Allow models to define a more reasonable name, usually used to remove a module/namespace.
|
14
|
+
def human_name(new_name = nil)
|
15
|
+
@human_name = new_name.to_s if new_name
|
16
|
+
|
17
|
+
@human_name ||= to_s.underscore.gsub(%r{\A.*/}, '')
|
18
|
+
end
|
19
|
+
|
20
|
+
def build_from_params(permitted_params, **attributes)
|
21
|
+
key = attributes.delete(:params_key)
|
22
|
+
|
23
|
+
new(**attributes) { _1.assign_params(permitted_params, key:) }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
included do
|
28
|
+
self.abstract_class = true
|
29
|
+
self.inheritance_column = :_disabled
|
30
|
+
|
31
|
+
# Assign strong parameters based on the class name - just a convenience method for services.
|
32
|
+
def assign_params(permitted_params, key: nil)
|
33
|
+
if key == false
|
34
|
+
assign_attributes ::Current.params.permit(permitted_params)
|
35
|
+
else
|
36
|
+
assign_attributes ::Current.params.require(key || default_param_key).permit(permitted_params)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Define a different Editable record to log edits on
|
41
|
+
def log_edits_on = self
|
42
|
+
|
43
|
+
def default_param_key = ::ActiveModel::Naming.param_key(self.class)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright (c) Steven Hoffman
|
4
|
+
# All rights reserved.
|
5
|
+
|
6
|
+
module Fustrate
|
7
|
+
module Rails
|
8
|
+
module Concerns
|
9
|
+
module SanitizeHtml
|
10
|
+
extend ::ActiveSupport::Concern
|
11
|
+
|
12
|
+
def self.sanitize(html, config) = smart_strip ::Sanitize.fragment(normalize(html), config)
|
13
|
+
|
14
|
+
# Remove non-breaking & ideographic spaces before sanitizing, and un-fancy quotes.
|
15
|
+
def self.normalize(html) = html.tr('‘’“”', %q(''"")).gsub(/(?:[\u00A0\u3000]| ) ?/, ' ')
|
16
|
+
|
17
|
+
# There shouldn't be whitespace or newlines at the beginning or end of the text
|
18
|
+
def self.smart_strip(html) = html.gsub(/\A(?:[[:space:]]|<br>)+|(?:[[:space:]]|<br>)+\z/, '')
|
19
|
+
|
20
|
+
module ClassMethods
|
21
|
+
def sanitize_html(*attributes, config)
|
22
|
+
before_validation do
|
23
|
+
attributes.flatten.each do |attribute|
|
24
|
+
next unless self[attribute]
|
25
|
+
|
26
|
+
self[attribute] = ::Fustrate::Rails::Concerns::SanitizeHtml.sanitize(self[attribute], config)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -1,14 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright (c) Steven Hoffman
|
4
|
+
# All rights reserved.
|
5
|
+
|
1
6
|
require 'rails/engine'
|
2
7
|
|
3
|
-
require '
|
4
|
-
require '
|
5
|
-
require '
|
6
|
-
|
7
|
-
require '
|
8
|
+
require 'fustrate/rails/concerns/clean_attributes'
|
9
|
+
require 'fustrate/rails/concerns/model'
|
10
|
+
require 'fustrate/rails/concerns/sanitize_html'
|
11
|
+
|
12
|
+
require 'fustrate/rails/services/base'
|
13
|
+
require 'fustrate/rails/services/generate_csv'
|
14
|
+
require 'fustrate/rails/services/generate_excel'
|
15
|
+
require 'fustrate/rails/services/log_edit'
|
8
16
|
|
9
17
|
module Fustrate
|
10
18
|
module Rails
|
11
|
-
class Engine < ::Rails::Engine
|
12
|
-
end
|
19
|
+
class Engine < ::Rails::Engine; end
|
13
20
|
end
|
14
21
|
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright (c) Steven Hoffman
|
4
|
+
# All rights reserved.
|
5
|
+
|
6
|
+
module Fustrate
|
7
|
+
module Rails
|
8
|
+
module Services
|
9
|
+
class Base
|
10
|
+
# Lets us use `t` and `l` helpers.
|
11
|
+
include ::ActionView::Helpers::TranslationHelper
|
12
|
+
|
13
|
+
protected
|
14
|
+
|
15
|
+
def service(service_class) = service_class.new
|
16
|
+
|
17
|
+
def authorize(action, resource) = ::Authority.enforce(action, resource, ::Current.user)
|
18
|
+
|
19
|
+
def transaction(&) = ::ActiveRecord::Base.transaction(&)
|
20
|
+
|
21
|
+
class LoadPage < self
|
22
|
+
DEFAULT_ORDER = nil
|
23
|
+
DEFAULT_INCLUDES = nil
|
24
|
+
RESULTS_PER_PAGE = 25
|
25
|
+
|
26
|
+
def call(page: nil, includes: nil, scope: nil, order: nil)
|
27
|
+
(scope || default_scope)
|
28
|
+
.reorder(order || default_order)
|
29
|
+
.paginate(page: page || params[:page], per_page: self.class::RESULTS_PER_PAGE)
|
30
|
+
.includes(includes || self.class::DEFAULT_INCLUDES)
|
31
|
+
end
|
32
|
+
|
33
|
+
protected
|
34
|
+
|
35
|
+
def default_scope = (raise ::NotImplementedError, '#default_scope not defined')
|
36
|
+
|
37
|
+
def default_order
|
38
|
+
return self.class::DEFAULT_ORDER.call if self.class::DEFAULT_ORDER.is_a? ::Proc
|
39
|
+
|
40
|
+
self.class::DEFAULT_ORDER
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright (c) Steven Hoffman
|
4
|
+
# All rights reserved.
|
5
|
+
|
6
|
+
module Fustrate
|
7
|
+
module Rails
|
8
|
+
module Services
|
9
|
+
class GenerateCsv
|
10
|
+
def call(data) = data.first.is_a?(::Hash) ? csv_from_hash(data) : csv_from_array(data)
|
11
|
+
|
12
|
+
protected
|
13
|
+
|
14
|
+
def csv_from_hash(data)
|
15
|
+
::CSV.generate do |csv|
|
16
|
+
csv << data.first.keys
|
17
|
+
|
18
|
+
data.each do |row|
|
19
|
+
csv << (row.values.map { _1&.to_s&.tr("\n", "\v") })
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def csv_from_array(data)
|
25
|
+
::CSV.generate do |csv|
|
26
|
+
# It's just an array of arrays; the first row is likely the header
|
27
|
+
data.each do |row|
|
28
|
+
csv << (Array(row).map { _1&.to_s&.tr("\n", "\v") })
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright (c) Steven Hoffman
|
4
|
+
# All rights reserved.
|
5
|
+
|
6
|
+
module Fustrate
|
7
|
+
module Rails
|
8
|
+
module Services
|
9
|
+
class GenerateExcel
|
10
|
+
def call(data, name = 'Sheet 1')
|
11
|
+
::Axlsx::Package.new do |package|
|
12
|
+
package.use_shared_strings = true
|
13
|
+
|
14
|
+
@wrap = package.workbook.styles.add_style(alignment: { wrap_text: true })
|
15
|
+
|
16
|
+
package.workbook.add_worksheet(name:) { add_data_to_sheet(data, _1) }
|
17
|
+
|
18
|
+
return package.to_stream.read
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
protected
|
23
|
+
|
24
|
+
def add_data_to_sheet(data, sheet)
|
25
|
+
sheet.add_row data.first.keys if data.any?
|
26
|
+
|
27
|
+
data.each { sheet.add_row _1.values, style: @wrap }
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|