resque-reports 0.0.2 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.rspec +3 -0
- data/.rubocop.yml +18 -0
- data/app/jobs/resque/reports/report_job.rb +48 -8
- data/lib/resque/reports/base_report.rb +151 -98
- data/lib/resque/reports/cache_file.rb +22 -10
- data/lib/resque/reports/csv_report.rb +54 -30
- data/lib/resque/reports/extensions/const.rb +11 -0
- data/lib/resque/reports/extensions/encodings.rb +11 -0
- data/lib/resque/reports/extensions/event_callbacks.rb +64 -0
- data/lib/resque/reports/extensions/event_templates.rb +22 -0
- data/lib/resque/reports/extensions/filename_gen.rb +29 -0
- data/lib/resque/reports/extensions/table_building.rb +117 -0
- data/lib/resque/reports/extensions.rb +37 -0
- data/lib/resque/reports/version.rb +2 -1
- data/lib/resque/reports.rb +6 -5
- data/lib/resque-reports.rb +1 -0
- data/resque-reports.gemspec +7 -5
- data/spec/resque/reports/base_report_spec.rb +196 -0
- data/spec/resque/reports/csv_report_spec.rb +89 -0
- data/spec/resque/reports/report_job_spec.rb +140 -0
- data/spec/spec_helper.rb +8 -73
- metadata +63 -13
- data/init.rb +0 -4
- data/lib/resque/reports/callbacks.rb +0 -50
- data/lib/resque/reports/encodings.rb +0 -13
@@ -1,60 +1,84 @@
|
|
1
1
|
# coding: utf-8
|
2
|
+
require 'csv'
|
3
|
+
|
2
4
|
module Resque
|
3
5
|
module Reports
|
6
|
+
# Class to inherit from for custom CSV reports
|
7
|
+
# To make your custom report you must define at least:
|
8
|
+
# 1. directory, is where to write reports to
|
9
|
+
# 2. source, is symbol of method that retrieves report data
|
10
|
+
# 3. table, report table configuration using DSL
|
4
11
|
class CsvReport < BaseReport
|
5
12
|
extend Forwardable
|
6
|
-
include Callbacks # include on_progress, on_error callbacks, and handle_progress, handle_errors handlers
|
7
13
|
|
8
14
|
class << self
|
9
|
-
attr_accessor :
|
10
|
-
|
15
|
+
attr_accessor :options
|
16
|
+
|
17
|
+
alias_method :csv_options, :options=
|
18
|
+
end
|
11
19
|
|
12
|
-
DEFAULT_CSV_OPTIONS = {
|
20
|
+
DEFAULT_CSV_OPTIONS = {col_sep: ';', row_sep: "\r\n"}
|
13
21
|
|
14
22
|
extension :csv
|
15
23
|
|
16
|
-
|
24
|
+
def_delegators TO_EIGENCLASS, :options, :csv_options
|
17
25
|
|
18
26
|
def initialize(*args)
|
19
|
-
csv_options
|
27
|
+
csv_options DEFAULT_CSV_OPTIONS.merge(options || Hash.new)
|
28
|
+
|
20
29
|
super(*args)
|
21
30
|
end
|
22
31
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
# 3) build_table_row(object) => returns Array of cell values (same order as header)
|
31
|
-
def write(io)
|
32
|
+
def write(io, force = false)
|
33
|
+
# You must use ancestor methods to work with report data:
|
34
|
+
# 1) data_size => returns source data size
|
35
|
+
# 2) data_each => yields given block for each source data element
|
36
|
+
# 3) build_table_header => returns Array of report column names
|
37
|
+
# 4) build_table_row(object) => returns Array of report cell values
|
38
|
+
# (same order as header)
|
32
39
|
progress = 0
|
33
40
|
|
34
|
-
CSV(io,
|
35
|
-
|
36
|
-
|
37
|
-
if data_collection.size > 0
|
38
|
-
write_line csv, build_table_header
|
41
|
+
CSV(io, options) do |csv|
|
42
|
+
write_line csv, build_table_header
|
39
43
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
end
|
46
|
-
|
47
|
-
handle_progress(progress += 1)
|
44
|
+
data_each(force) do |data_element|
|
45
|
+
begin
|
46
|
+
write_line csv, build_table_row(data_element)
|
47
|
+
rescue
|
48
|
+
handle_error
|
48
49
|
end
|
49
50
|
|
50
|
-
handle_progress(progress,
|
51
|
+
handle_progress(progress += 1, data_size)
|
51
52
|
end
|
52
|
-
|
53
|
+
|
54
|
+
handle_progress(progress, data_size, true)
|
55
|
+
end
|
53
56
|
end
|
54
57
|
|
55
58
|
def write_line(csv, row_cells)
|
56
59
|
csv << row_cells
|
57
60
|
end
|
61
|
+
|
62
|
+
#--
|
63
|
+
# Event handling #
|
64
|
+
#++
|
65
|
+
|
66
|
+
def error_message(error)
|
67
|
+
error_message = []
|
68
|
+
error_message << 'Выгрузка отчета невозможна. '
|
69
|
+
error_message << case error
|
70
|
+
when Encoding::UndefinedConversionError
|
71
|
+
<<-ERR_MSG.gsub(/^ {29}/, '')
|
72
|
+
Символ #{error.error_char} не поддерживается
|
73
|
+
заданной кодировкой
|
74
|
+
ERR_MSG
|
75
|
+
when EncodingError
|
76
|
+
'Ошибка преобразования в заданную кодировку'
|
77
|
+
else
|
78
|
+
fail error
|
79
|
+
end
|
80
|
+
error_message * ' '
|
81
|
+
end
|
58
82
|
end # class CsvReport
|
59
83
|
end # module Report
|
60
84
|
end # module Resque
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
module Resque
|
3
|
+
module Reports
|
4
|
+
module Extensions
|
5
|
+
# Defines event callbacks and handlers for Resque::Reports::ReportJob
|
6
|
+
module EventCallbacks
|
7
|
+
# TODO: сделать гибкой логику колбеков и хендлеров
|
8
|
+
# Defines callbacks
|
9
|
+
module ClassMethods
|
10
|
+
|
11
|
+
attr_reader :progress_callback, :error_callback
|
12
|
+
|
13
|
+
#--
|
14
|
+
# Callbacks
|
15
|
+
#++
|
16
|
+
|
17
|
+
# Set callback for watching progress of export
|
18
|
+
# @yield [progress] block to be executed on progress
|
19
|
+
# @yieldparam progress [Integer] current progress
|
20
|
+
# @yieldparam total [Integer] data length
|
21
|
+
def on_progress(&block)
|
22
|
+
@progress_callback = block
|
23
|
+
end
|
24
|
+
|
25
|
+
# Set callback on error
|
26
|
+
# @yield [error] block to be executed when error occurred
|
27
|
+
# @yieldparam [Exception] error
|
28
|
+
def on_error(&block)
|
29
|
+
@error_callback = block
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Defines handlers
|
34
|
+
module InstanceMethods
|
35
|
+
extend Forwardable
|
36
|
+
|
37
|
+
PROGRESS_STEP = 10
|
38
|
+
|
39
|
+
def_delegators Extensions::Const::TO_EIGENCLASS,
|
40
|
+
:error_callback,
|
41
|
+
:progress_callback
|
42
|
+
#--
|
43
|
+
# Handlers
|
44
|
+
#++
|
45
|
+
|
46
|
+
def handle_progress(progress, total, force = false)
|
47
|
+
if progress_callback && (force || progress % PROGRESS_STEP == 0)
|
48
|
+
progress_callback.call progress, total
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def handle_error
|
53
|
+
error_callback ? error_callback.call($ERROR_INFO) : fail
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.included(base)
|
58
|
+
base.extend ClassMethods
|
59
|
+
base.send :include, InstanceMethods
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
module Resque
|
3
|
+
module Reports
|
4
|
+
module Extensions
|
5
|
+
# Defines base event handling methods for Resque::Reports::EventCallbacks
|
6
|
+
module EventTemplates
|
7
|
+
|
8
|
+
# Specifies progress message generation
|
9
|
+
# Can be overridden in successors
|
10
|
+
def progress_message(p, t)
|
11
|
+
nil
|
12
|
+
end
|
13
|
+
|
14
|
+
# Specifies error message generation
|
15
|
+
# Can be overridden in successors
|
16
|
+
def error_message(e)
|
17
|
+
fail e
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
module Resque
|
3
|
+
module Reports
|
4
|
+
module Extensions
|
5
|
+
# Module that generates file name
|
6
|
+
# Usage:
|
7
|
+
# class SomeClass
|
8
|
+
# include Resque::Reports::Extensions::FilenameGen
|
9
|
+
#
|
10
|
+
# # ...call somewhere...
|
11
|
+
# fname = generate_filename(%w(a b c), 'pdf')
|
12
|
+
# # 'fname' value is something like this:
|
13
|
+
# # "a60428ee50f1795819b8486c817c27829186fa40.pdf"
|
14
|
+
# end
|
15
|
+
module FilenameGen
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def generate_filename(args, fextension)
|
20
|
+
"#{ hash(self.class.to_s, *args) }.#{ fextension }"
|
21
|
+
end
|
22
|
+
|
23
|
+
def hash(*args)
|
24
|
+
Digest::SHA1.hexdigest(args.to_json)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
# Resque namespace
|
3
|
+
module Resque
|
4
|
+
# Resque::Reports namespace
|
5
|
+
module Reports
|
6
|
+
# Resque::Reports::Extensions namespace
|
7
|
+
module Extensions
|
8
|
+
# Defines report table building logic
|
9
|
+
module TableBuilding
|
10
|
+
# Defines table building methods
|
11
|
+
# External(DSL):
|
12
|
+
# - source
|
13
|
+
# - table
|
14
|
+
# - column
|
15
|
+
# Internal:
|
16
|
+
# - init_table
|
17
|
+
# - build_table_header
|
18
|
+
# - build_table_row(row_object)
|
19
|
+
# - data_each
|
20
|
+
# - data_size
|
21
|
+
module ClassMethods
|
22
|
+
|
23
|
+
attr_accessor :source_method
|
24
|
+
alias_method :source, :source_method=
|
25
|
+
|
26
|
+
def table(&block)
|
27
|
+
@table_block = block
|
28
|
+
end
|
29
|
+
|
30
|
+
def column(name, value)
|
31
|
+
add_column_header(name) || add_column_cell(value)
|
32
|
+
end
|
33
|
+
|
34
|
+
def init_table
|
35
|
+
@table_header = []
|
36
|
+
@table_row = []
|
37
|
+
end
|
38
|
+
|
39
|
+
def add_column_header(column_name)
|
40
|
+
@table_header << encoded_string(column_name) if @header_collecting
|
41
|
+
end
|
42
|
+
|
43
|
+
def add_column_cell(column_value)
|
44
|
+
return if @header_collecting
|
45
|
+
|
46
|
+
if column_value.is_a? Symbol
|
47
|
+
# Smells bad... changes input variable
|
48
|
+
column_value = @instance.send(column_value, @row_object)
|
49
|
+
end
|
50
|
+
|
51
|
+
@table_row << encoded_string(column_value)
|
52
|
+
end
|
53
|
+
|
54
|
+
def build_table_row(row_object)
|
55
|
+
@header_collecting = false
|
56
|
+
|
57
|
+
@row_object = row_object # for instance decorate methods calls
|
58
|
+
row = @table_block.call(@row_object)
|
59
|
+
|
60
|
+
finish_row
|
61
|
+
|
62
|
+
row
|
63
|
+
end
|
64
|
+
|
65
|
+
def build_table_header
|
66
|
+
@header_collecting = true
|
67
|
+
@table_block.call(Extensions::Dummy.new)
|
68
|
+
end
|
69
|
+
|
70
|
+
# you may override default string endcoding
|
71
|
+
def encoded_string(obj)
|
72
|
+
obj.to_s.encode('utf-8', invalid: :replace, undef: :replace)
|
73
|
+
end
|
74
|
+
|
75
|
+
def finish_row
|
76
|
+
@table_row = []
|
77
|
+
end
|
78
|
+
|
79
|
+
def data(force = false)
|
80
|
+
if force || @data.nil?
|
81
|
+
@data = @instance.send(@source_method)
|
82
|
+
else
|
83
|
+
@data
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def data_each(force = false)
|
88
|
+
data(force).each do |element|
|
89
|
+
yield element
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def data_size
|
94
|
+
@data_size ||= data.count
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Delegates class methods to instance
|
99
|
+
module InstanceMethods
|
100
|
+
extend Forwardable
|
101
|
+
|
102
|
+
def_delegators Extensions::Const::TO_EIGENCLASS,
|
103
|
+
:data_each,
|
104
|
+
:data_size,
|
105
|
+
:build_table_header,
|
106
|
+
:build_table_row,
|
107
|
+
:init_table
|
108
|
+
end
|
109
|
+
|
110
|
+
def self.included(base)
|
111
|
+
base.extend ClassMethods
|
112
|
+
base.send :include, InstanceMethods
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
require 'resque/reports/extensions/const'
|
3
|
+
require 'resque/reports/extensions/event_callbacks'
|
4
|
+
require 'resque/reports/extensions/event_templates'
|
5
|
+
require 'resque/reports/extensions/filename_gen'
|
6
|
+
require 'resque/reports/extensions/table_building'
|
7
|
+
require 'resque/reports/extensions/encodings'
|
8
|
+
|
9
|
+
# Resque namespace
|
10
|
+
module Resque
|
11
|
+
# Resque::Reports namespace
|
12
|
+
module Reports
|
13
|
+
# Resque::Reports::Extensions namespace
|
14
|
+
module Extensions
|
15
|
+
# Class for dummy object that respond to any method
|
16
|
+
# and returns 'nil' on any method call
|
17
|
+
class Dummy
|
18
|
+
def method_missing(method, *arguments, &block)
|
19
|
+
nil
|
20
|
+
end
|
21
|
+
|
22
|
+
def respond_to?(method, include_private = false)
|
23
|
+
true
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.included(base)
|
28
|
+
base.send :include, Const # share gem constants
|
29
|
+
base.send :include, TableBuilding # init and build table
|
30
|
+
base.send :include, FilenameGen # generate_filename method
|
31
|
+
base.send :include, EventCallbacks # event callbacks and handlers
|
32
|
+
base.send :include, EventTemplates # template events handling methods
|
33
|
+
base.send :include, Encodings # encoding constants
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/lib/resque/reports.rb
CHANGED
@@ -1,17 +1,18 @@
|
|
1
1
|
# coding: utf-8
|
2
2
|
require 'forwardable'
|
3
|
-
require 'facets/kernel/constant'
|
4
|
-
|
5
3
|
require 'resque-integration'
|
6
4
|
|
7
|
-
require 'resque/reports/
|
5
|
+
require 'resque/reports/extensions'
|
6
|
+
|
8
7
|
require 'resque/reports/cache_file'
|
9
|
-
require 'resque/reports/callbacks'
|
10
|
-
require 'resque/reports/encodings'
|
11
8
|
require 'resque/reports/base_report'
|
12
9
|
require 'resque/reports/csv_report'
|
13
10
|
|
11
|
+
require 'resque/reports/version'
|
12
|
+
|
13
|
+
# Resque namespace
|
14
14
|
module Resque
|
15
|
+
# Resque::Reports namespace
|
15
16
|
module Reports
|
16
17
|
|
17
18
|
end
|