ruby-reports 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 607ca468bb6cc003dd6d1982538fe62f2d58ddaf
4
+ data.tar.gz: 7e58b3ca2062cf8d351a9ec201014b7e119b0857
5
+ SHA512:
6
+ metadata.gz: 996cc830263eda8dc5a8a584d72b26e0bf60bf89d3322962682e70d057ecbbb0501e1824a37e7d26a6b1682f5b5e00118acc9625fed6e4e912f6bbb57c5b144e
7
+ data.tar.gz: c25b1718b161c12a77207dbb8dacbc07f2e6b6ffe27c3bc62df4063faefc19fa8e53e981d2fd7c968fa0649cc872c7a0587b826bf59a994a4bf6a082dccf5d6f
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,7 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.2.1
4
+ before_install: gem install bundler -v 1.10.6
5
+ addons:
6
+ code_climate:
7
+ repo_token: 363fef79ea0d646c43a20ea27d2cc4e3b97c4500023c8dc2ba25b027018a1ec9
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in ruby-reports.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Sergey D.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,99 @@
1
+ # Ruby::Reports [![Build Status](https://travis-ci.org/sclinede/ruby-reports.svg)](https://travis-ci.org/sclinede/ruby-reports) [![Code Climate](https://codeclimate.com/github/sclinede/ruby-reports/badges/gpa.svg)](https://codeclimate.com/github/sclinede/ruby-reports) [![Test Coverage](https://codeclimate.com/github/sclinede/ruby-reports/badges/coverage.svg)](https://codeclimate.com/github/sclinede/ruby-reports/coverage)
2
+
3
+ This gem was written for report automation provided by DSL. See [Usage](#usage) for details
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'ruby-reports'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install ruby-reports
20
+
21
+ ## Usage <a id="usage"></a>
22
+
23
+ Main concept is following:
24
+ - You have report class where you define attributes, column names and mapped to them
25
+ keys from source data
26
+ - Report has ```query``` method. It defines what object will serve data querying from any source
27
+ - (Optional) Report has ```formatter``` method. It defines what object will serve cell value formatting
28
+
29
+ Example:
30
+ ```ruby
31
+ class MyNiceReport < Ruby::Reports::CsvReport
32
+ config(
33
+ source: :fetch_data,
34
+ expire_in: 15 * 60,
35
+ encoding: Ruby::Reports::CP1251,
36
+ directory: File.join('/home', 'my_home', 'nice_reports')
37
+ )
38
+
39
+ table do
40
+ column 'ID', :id
41
+ column 'User Name', :username
42
+ column 'Email', :email, formatter: :mail_to_link
43
+ column 'Last Seen', :last_seen_date, formatter: :date
44
+ end
45
+
46
+ def formatter
47
+ @formatter ||= Formatter.new
48
+ end
49
+
50
+ def query
51
+ @query ||= Query.new(self)
52
+ end
53
+
54
+ class Formatter
55
+ def mail_to_link(email)
56
+ "mailto:#{email}"
57
+ end
58
+
59
+ def date(date)
60
+ Date.parse(date).strftime('%d.%m.%Y')
61
+ end
62
+ end
63
+
64
+ class Query < Ruby::Reports::Services::QueryBuilder
65
+ def fetch_data
66
+ [
67
+ {id: 1, username: 'user#1', email: 'user1@reports.org', last_seen_date: '2015/06/06'},
68
+ {id: 2, username: 'user#2', email: 'user2@reports.org', last_seen_date: '2015/02/07'},
69
+ {id: 3, username: 'user#3', email: 'user3@reports.org', last_seen_date: '2015/08/13'}
70
+ ]
71
+ end
72
+ end
73
+ end
74
+
75
+ > report = MyNiceReport.build
76
+ # => #<MyNiceReport:0x00000002171a68 ...>
77
+ > report.ready?
78
+ # => true
79
+ > report.filename
80
+ # => '/home/my_home/nice_reports/asdvdsadvasdv.csv'
81
+ > IO.read(report.filename)
82
+ # => "ID;User Name;Email;Last Seen\r\n1;user#1;mailto:user1@reports.org;06.06.2015\r\n
83
+ # 2;user#2;mailto:user2@reports.org;07.02.2015\r\n3;user#3;mailto:user3@reports.org;13.08.2015\r\n"
84
+ ```
85
+
86
+ ## Development
87
+
88
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
89
+
90
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
91
+
92
+ ## Contributing
93
+
94
+ Bug reports and pull requests are welcome on GitHub at https://github.com/sclinede/ruby-reports.
95
+
96
+ ## License
97
+
98
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
99
+
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "ruby/reports"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ require "pry"
11
+ Pry.start
data/bin/setup ADDED
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,179 @@
1
+ # coding: utf-8
2
+ # Resque namespace
3
+ module Ruby
4
+ # Resque::Reports namespace
5
+ module Reports
6
+ # Class describes base report class for inheritance.
7
+ # BaseReport successor must implement "write(io, force)" method
8
+ # and may specify file extension with "extension" method call
9
+ # example:
10
+ #
11
+ # class CustomTypeReport < Resque::Reports::BaseReport
12
+ # extension :type # specify that report file must ends
13
+ # # with '.type', e.g. 'abc.type'
14
+ #
15
+ # # Method specifies how to output report data
16
+ # def write(io, force)
17
+ # io << 'Hello World!'
18
+ # end
19
+ # end
20
+ #
21
+ # BaseReport provides following DSL, example:
22
+ #
23
+ # class CustomReport < CustomTypeReport
24
+ # # include Resque::Reports::Common::BatchedReport
25
+ # # overrides data retrieving to achieve batching
26
+ # # if included 'source :select_data' becomes needless
27
+ #
28
+ # queue :custom_reports # Resque queue name
29
+ # source :select_data # method called to retrieve report data
30
+ # encoding UTF8 # file encoding
31
+ # expire_in 86_400 # cache time of the file, default: 86_400
32
+ #
33
+ # # Specify in which directory to keep this type files
34
+ # directory File.join(Dir.tmpdir, 'resque-reports')
35
+ #
36
+ # # Describe table using 'column' method
37
+ # table do |element|
38
+ # column 'Column 1 Header', :decorate_one
39
+ # column 'Column 2 Header', decorate_two(element[1])
40
+ # column 'Column 3 Header', 'Column 3 Cell'
41
+ # column 'Column 4 Header', :formatted_four, formatter: :just_cute
42
+ # end
43
+ #
44
+ # # Class initialize if needed
45
+ # # NOTE: must be used instead of define 'initialize' method
46
+ # # Default behaviour is to receive in *args Hash with report attributes
47
+ # # like: CustomReport.new(main_param: 'value') => calls send(:main_param=, 'value')
48
+ # create do |param|
49
+ # @main_param = param
50
+ # end
51
+ #
52
+ # def self.just_cute_formatter(column_value)
53
+ # "I'm so cute #{column_value}"
54
+ # end
55
+ #
56
+ # # decorate method, called by symbol-name
57
+ # def decorate_one(element)
58
+ # "decorate_one: #{element[0]}"
59
+ # end
60
+ #
61
+ # # decorate method, called directly when filling cell
62
+ # def decorate_two(text)
63
+ # "decorate_two: #{text}"
64
+ # end
65
+ #
66
+ # # method returns report data Enumerable
67
+ # def select_data
68
+ # [[0, 'text0'], [1, 'text1']]
69
+ # end
70
+ # end
71
+ class BaseReport
72
+ extend Forwardable
73
+
74
+ class << self
75
+ attr_reader :config_hash, :table_block, :progress_handle_block, :error_handle_block
76
+
77
+ def config(hash)
78
+ @config_hash = hash
79
+ end
80
+
81
+ def table(&block)
82
+ @table_block = block
83
+ end
84
+
85
+ def build(options = {})
86
+ force = options.delete(:force)
87
+
88
+ report = new(options)
89
+ report.build(force)
90
+
91
+ report
92
+ end
93
+ end
94
+
95
+ attr_reader :args, :job_id, :events_handler
96
+ def_delegators :cache_file, :filename, :exists?, :ready?
97
+
98
+ #--
99
+ # Public instance methods
100
+ #++
101
+
102
+ def initialize(*args)
103
+ @args = args
104
+ assign_attributes
105
+ end
106
+
107
+ # Builds report synchronously
108
+ def build(force = false)
109
+ @table = nil if force
110
+ @events_handler = Services::EventsHandler.new(@progress_handle_block, @error_handle_block)
111
+
112
+ cache_file.open(force) { |file| write(file, force) }
113
+ end
114
+
115
+ def progress_handler(&block)
116
+ @progress_handle_block = block
117
+ end
118
+
119
+ def error_handler(&block)
120
+ @error_handle_block = block
121
+ end
122
+
123
+ private
124
+
125
+ def formatter
126
+ nil
127
+ end
128
+
129
+ def config
130
+ @config ||= Config.new(self.class.config_hash)
131
+ end
132
+
133
+ def assign_attributes
134
+ if args && (attrs_hash = args.first) && attrs_hash.is_a?(Hash)
135
+ attrs_hash.each { |name, value| instance_variable_set("@#{name}", value) }
136
+ end
137
+ end
138
+
139
+ def query
140
+ # descendant of QueryBuilder or SqlQuery with #take_batch(limit, offset) method defined
141
+ # @query ||= Query.new(self)
142
+ fail NotImplementedError
143
+ end
144
+
145
+ def iterator
146
+ @iterator ||= Services::DataIterator.new(query, config)
147
+ end
148
+
149
+ def table
150
+ @table ||= Services::TableBuilder.new(self, self.class.table_block, config, formatter)
151
+ end
152
+
153
+ def cache_file
154
+ @cache_file ||= CacheFile.new(config.directory,
155
+ Services::FilenameGenerator.generate(args, config.extension),
156
+ expire_in: config.expire_in, coding: config.encoding)
157
+ end
158
+
159
+ # Method specifies how to output report data
160
+ # @param [IO] io stream for output
161
+ # @param [true, false] force write to output or skip due its existance
162
+ def write(io, force)
163
+ # You must use ancestor methods to work with report data:
164
+ # 1) iterator.data_size => returns source data size (calls #count on data
165
+ # retrieved from 'source')
166
+ # 2) iterator.data_each => yields given block for each source data element
167
+ # 3) table.build_header => returns Array of report column names
168
+ # 4) table.build_row(object) => returns Array of report cell
169
+ # values (same order as header)
170
+ # 5) events_handler.progress(progress, total) => call to iterate job progress
171
+ # 6) events_handler.error(error) => call to handle error in job
172
+ #
173
+ # HINT: You may override data_size and data_each, to retrieve them
174
+ # effectively
175
+ fail NotImplementedError
176
+ end
177
+ end # class BaseReport
178
+ end # module Report
179
+ end # module Resque
@@ -0,0 +1,84 @@
1
+ require 'tempfile'
2
+ # coding: utf-8
3
+ module Ruby
4
+ module Reports
5
+ # Class describes how to storage and access cache file
6
+ # NOTE: Every time any cache file is opening,
7
+ # cache is cleared from old files.
8
+ class CacheFile
9
+ DEFAULT_EXPIRE_TIME = 86_400
10
+ DEFAULT_CODING = 'utf-8'.freeze
11
+
12
+ attr_reader :dir, :ext, :coding, :expiration_time
13
+ def initialize(dir, filename, options = {})
14
+ @dir = dir
15
+ @filename = File.join(dir, filename)
16
+ @ext = File.extname(@filename)
17
+
18
+ # options
19
+ @coding = options[:coding] || DEFAULT_CODING
20
+ @expiration_time = options[:expire_in] || DEFAULT_EXPIRE_TIME
21
+ end
22
+
23
+ def exists?
24
+ !expired?(@filename)
25
+ end
26
+ alias_method :ready?, :exists?
27
+
28
+ def filename
29
+ fail 'File doesn\'t exist, check exists? before' unless exists?
30
+ @filename
31
+ end
32
+
33
+ def open(force = false)
34
+ prepare_cache_dir
35
+
36
+ (force ? clear : return) if File.exists?(@filename)
37
+
38
+ with_tempfile do |tempfile|
39
+ yield tempfile
40
+
41
+ tempfile.close
42
+ FileUtils.cp(tempfile.path, @filename)
43
+ FileUtils.chmod(0644, @filename)
44
+ end
45
+ end
46
+
47
+ def clear
48
+ FileUtils.rm_f(@filename)
49
+ end
50
+
51
+ protected
52
+
53
+ def with_tempfile
54
+ yield(tempfile = Tempfile.new(Digest::MD5.hexdigest(@filename), :encoding => coding))
55
+ ensure
56
+ return unless tempfile
57
+ tempfile.close unless tempfile.closed?
58
+ tempfile.unlink
59
+ end
60
+
61
+ def prepare_cache_dir
62
+ FileUtils.mkdir_p dir # create folder if not exists
63
+ clear_expired_files
64
+ end
65
+
66
+ def clear_expired_files
67
+ # TODO: avoid races when worker building
68
+ # his report longer than @expiration_time
69
+ FileUtils.rm_f cache_files_array.select { |fname| expired?(fname) }
70
+ end
71
+
72
+ def expired?(fname)
73
+ return true unless File.file?(fname)
74
+ File.mtime(fname) + expiration_time < Time.now
75
+ end
76
+
77
+ def cache_files_array
78
+ Dir.new(dir)
79
+ .map { |fname| File.join(dir, fname) if File.extname(fname) == ext }
80
+ .compact
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,34 @@
1
+ module Ruby
2
+ module Reports
3
+ class Config
4
+ BATCH_SIZE = 10_000
5
+ DEFAULT_EXPIRE_TIME = 86_400
6
+ DEFAULT_CODING = 'utf-8'.freeze
7
+ DEFAULT_CSV_OPTIONS = {col_sep: ';', row_sep: "\r\n"}
8
+
9
+ DEFAULT_CONFIG_ATTRIBUTES = [
10
+ :directory,
11
+ :source,
12
+ :extension,
13
+ :batch_size,
14
+ :encoding,
15
+ :expire_in,
16
+ :csv_options,
17
+ :storage
18
+ ]
19
+
20
+ def self.config_attributes
21
+ DEFAULT_CONFIG_ATTRIBUTES
22
+ end
23
+
24
+ attr_accessor(*config_attributes)
25
+ attr_initialize config_attributes do
26
+ @batch_size ||= BATCH_SIZE
27
+ @encoding ||= DEFAULT_CODING
28
+ @expire_in ||= DEFAULT_EXPIRE_TIME
29
+ @csv_options ||= DEFAULT_CSV_OPTIONS
30
+ @storage ||= Storages::OBJECT
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,77 @@
1
+ # coding: utf-8
2
+ require 'csv'
3
+
4
+ module Ruby
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
11
+ class CsvReport < BaseReport
12
+ attr_reader :csv_options
13
+
14
+ def initialize(*args)
15
+ config.extension = :csv
16
+ super
17
+ @csv_options = config.csv_options
18
+ end
19
+
20
+ def write(io, force = false)
21
+ # You must use ancestor methods to work with report data:
22
+ # 1) data_size => returns source data size
23
+ # 2) data_each => yields given block for each source data element
24
+ # 3) build_table_header => returns Array of report column names
25
+ # 4) build_table_row(object) => returns Array of report cell values
26
+ # (same order as header)
27
+ progress = 0
28
+
29
+ CSV(io, csv_options) do |csv|
30
+ write_line csv, table.build_header
31
+
32
+ iterator.data_each(force) do |data_element|
33
+ begin
34
+ write_line csv, table.build_row(data_element)
35
+ rescue
36
+ events_handler.error
37
+ end
38
+
39
+ events_handler.progress(progress += 1, iterator.data_size)
40
+ end
41
+
42
+ events_handler.progress(progress, iterator.data_size, true)
43
+ end
44
+ end
45
+
46
+ def write_line(csv, row_cells)
47
+ csv << row_cells
48
+ end
49
+
50
+ #--
51
+ # Event handling #
52
+ #++
53
+ #
54
+
55
+ def progress_message(*args)
56
+ 'Выгрузка отчета в CSV'
57
+ end
58
+
59
+ def error_message(error)
60
+ error_message = []
61
+ error_message << 'Выгрузка отчета невозможна. '
62
+ error_message << case error
63
+ when Encoding::UndefinedConversionError
64
+ <<-ERR_MSG.gsub(/^ {29}/, '')
65
+ Символ #{error.error_char} не поддерживается
66
+ заданной кодировкой
67
+ ERR_MSG
68
+ when EncodingError
69
+ 'Ошибка преобразования в заданную кодировку'
70
+ else
71
+ fail error
72
+ end
73
+ error_message * ' '
74
+ end
75
+ end # class CsvReport
76
+ end # module Report
77
+ end # module Ruby
@@ -0,0 +1,42 @@
1
+ module Ruby
2
+ module Reports
3
+ module Services
4
+ class DataIterator
5
+ attr_reader :custom_source
6
+ pattr_initialize :query, :config do
7
+ @custom_source = query.send(config.source) if config.source
8
+ end
9
+
10
+ def iterate_custom_source
11
+ custom_source.each do |row|
12
+ yield row
13
+ end
14
+ end
15
+ # Internal: Выполняет запрос строк отчета пачками
16
+ #
17
+ # Returns Nothing
18
+ def data_each(force = false, &block)
19
+ return iterate_custom_source(&block) if custom_source
20
+
21
+ batch_offset = 0
22
+
23
+ while (rows = query.request_batch(batch_offset)).count > 0 do
24
+ rows.each { |row| yield row }
25
+ batch_offset += config.batch_size
26
+ end
27
+ end
28
+
29
+ # Internal: Возвращает общее кол-во строк в отчете
30
+ #
31
+ # Returns Fixnum
32
+ def data_size
33
+ @data_size ||= if custom_source
34
+ custom_source.count
35
+ else
36
+ query.request_count
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,23 @@
1
+ module Ruby
2
+ module Reports
3
+ module Services
4
+ class EventsHandler
5
+ PROGRESS_STEP = 10
6
+
7
+ pattr_initialize :progress_callback, :error_callback do
8
+ @error_callback ||= ->(e) { fail e }
9
+ end
10
+
11
+ def progress(progress, total, force = false)
12
+ if progress_callback && (force || progress % PROGRESS_STEP == 0)
13
+ progress_callback.call progress, total
14
+ end
15
+ end
16
+
17
+ def error
18
+ error_callback ? error_callback.call($ERROR_INFO) : fail
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,51 @@
1
+ require 'digest'
2
+
3
+ # coding: utf-8
4
+ module Ruby
5
+ module Reports
6
+ module Services
7
+ # Module that generates file name
8
+ # Usage:
9
+ # class SomeClass
10
+ # include Resque::Reports::Extensions::FilenameGen
11
+ #
12
+ # # ...call somewhere...
13
+ # fname = generate_filename(%w(a b c), 'pdf')
14
+ # # 'fname' value is something like this:
15
+ # # "a60428ee50f1795819b8486c817c27829186fa40.pdf"
16
+ # end
17
+ class FilenameGenerator
18
+ DEFAULT_EXTENSION = 'txt'
19
+
20
+ def self.generate(args, fextension = nil)
21
+ "#{hash(self.class.to_s, *args)}.#{fextension || DEFAULT_EXTENSION}"
22
+ end
23
+
24
+ private
25
+
26
+ def self.hash(*args)
27
+ Digest::SHA1.hexdigest(obj_to_string(args))
28
+ end
29
+
30
+ def self.obj_to_string(obj)
31
+ case obj
32
+ when Hash
33
+ s = []
34
+ obj.keys.sort.each do |k|
35
+ s << obj_to_string(k)
36
+ s << obj_to_string(obj[k])
37
+ end
38
+ s.to_s
39
+ when Array
40
+ s = []
41
+ obj.each { |a| s << obj_to_string(a) }
42
+ s.to_s
43
+ else
44
+ obj.to_s
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+
@@ -0,0 +1,113 @@
1
+ module Ruby
2
+ module Reports
3
+ module Services
4
+ class QueryBuilder
5
+ extend Forwardable
6
+ pattr_initialize :report, :config
7
+
8
+ def request_count
9
+ execute(count)[0]['count'].to_i
10
+ end
11
+
12
+ def request_batch(offset)
13
+ execute take_batch(config.batch_size, offset)
14
+ end
15
+
16
+ private
17
+
18
+ def_delegator :connection, :execute
19
+
20
+ def connection
21
+ ActiveRecord::Base.connection
22
+ end
23
+
24
+ # Internal: Возвращает отфильтрованный запрос отчета
25
+ #
26
+ # Returns Arel::SelectManager
27
+ def query
28
+ filter base_query
29
+ end
30
+
31
+ # Internal: Полезный метод для хранения Arel::Table объектов для запроса отчета
32
+ #
33
+ # Returns Hash, {:table_name => #<Arel::Table @name="table_name">, ...}
34
+ def tables
35
+ return @tables if defined? @tables
36
+
37
+ tables = models.map(&:arel_table)
38
+
39
+ @tables = tables.reduce({}) { |a, e| a.store(e.name, e) && a }.with_indifferent_access
40
+ end
41
+
42
+ # Internal: Полезный метод для join'а необходимых таблиц через Arel
43
+ #
44
+ # Returns Arel
45
+ def join_tables(source_table, *joins)
46
+ joins.inject(source_table) { |query, joined| query.join(joined[:table]).on(joined[:on]) }
47
+ end
48
+
49
+ # Internal: Размер пачки отчета
50
+ #
51
+ # Returns Fixnum
52
+ def batch_size
53
+ BATCH_SIZE
54
+ end
55
+
56
+ # Internal: Модели используемые в отчете
57
+ #
58
+ # Returns Array of Arel::Table
59
+ def models
60
+ fail NotImplementedError
61
+ end
62
+
63
+ # Internal: Основной запрос отчета (Arel)
64
+ #
65
+ # Returns Arel::SelectManager
66
+ def base_query
67
+ fail NotImplementedError
68
+ end
69
+
70
+ # Internal: Поля запрашиваемые отчетом
71
+ #
72
+ # Returns String (SQL)
73
+ def select
74
+ fail NotImplementedError
75
+ end
76
+
77
+ # Internal: Порядок строк отчета
78
+ #
79
+ # Returns String (SQL)
80
+ def order_by
81
+ nil
82
+ end
83
+
84
+ # Internal: Фильтры отчета
85
+ #
86
+ # Returns Arel::SelectManager
87
+ def filter(query)
88
+ query
89
+ end
90
+
91
+ # Internal: Запрос количества строк в отчете
92
+ #
93
+ # Returns String (SQL)
94
+ def count
95
+ query.project(Arel.sql('COUNT(*) as count')).to_sql
96
+ end
97
+
98
+ # Internal: Запрос пачки строк отчета
99
+ #
100
+ # offset - Numeric, число строк на которое сдвигается запрос
101
+ #
102
+ # Returns String (SQL)
103
+ def take_batch(limit, offset)
104
+ query.project(Arel.sql(select))
105
+ .take(limit)
106
+ .skip(offset)
107
+ .order(order_by)
108
+ .to_sql
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,103 @@
1
+ require 'facets/hash/rekey'
2
+ require 'iron/dsl'
3
+
4
+ # coding: utf-8
5
+ # Ruby namespace
6
+ module Ruby
7
+ # Resque::Reports namespace
8
+ module Reports
9
+ # Resque::Reports::Extensions namespace
10
+ module Services
11
+ # Defines report table building logic
12
+ class TableBuilder
13
+ attr_reader :building_header
14
+ pattr_initialize :report, :table_block, :config, :formatter do
15
+ init_table
16
+ end
17
+
18
+ def column(name, value = nil, options = {})
19
+ if (skip_if = options.delete(:skip_if))
20
+ if skip_if.is_a?(Symbol)
21
+ return if report.send(skip_if)
22
+ elsif skip_if.respond_to?(:call)
23
+ return if skip_if.call
24
+ end
25
+ end
26
+
27
+ building_header ? add_header_cell(name) : add_row_cell(value, options)
28
+ end
29
+
30
+ def build_row(row)
31
+ @row = row.is_a?(Hash) ? row.rekey! : row
32
+ row = DslProxy.exec(self, @row, &table_block)
33
+ cleanup_row
34
+ row
35
+ end
36
+
37
+ def build_header
38
+ @building_header = true
39
+ header = DslProxy.exec(self, Dummy.new, &table_block)
40
+ @building_header = false
41
+
42
+ cleanup_header
43
+ header
44
+ end
45
+
46
+ private
47
+
48
+ def encoded_string(obj)
49
+ obj.to_s.encode(config.encoding, invalid: :replace, undef: :replace)
50
+ end
51
+
52
+ def init_table
53
+ cleanup_header
54
+ cleanup_row
55
+ end
56
+
57
+ def cleanup_row
58
+ @table_row = []
59
+ end
60
+
61
+ def cleanup_header
62
+ @table_header = []
63
+ end
64
+
65
+ def add_header_cell(column_name)
66
+ @table_header << encoded_string(column_name)
67
+ end
68
+
69
+ def add_row_cell(column_value, options = {})
70
+ column_value = read_from_storage(column_value) if column_value.is_a? Symbol
71
+
72
+ if (formatter_name = options[:formatter])
73
+ column_value = formatter.send(formatter_name, column_value)
74
+ end
75
+
76
+ @table_row << encoded_string(column_value)
77
+ end
78
+
79
+ def read_from_storage(column)
80
+ case config.storage
81
+ when :object
82
+ @row.public_send(column)
83
+ when :hash
84
+ @row[column]
85
+ else
86
+ fail 'Unknown Storage set in report config'
87
+ end
88
+ end
89
+
90
+ class Dummy
91
+ def method_missing(method, *arguments, &block)
92
+ nil
93
+ end
94
+
95
+ def respond_to?(method, include_private = false)
96
+ true
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
103
+
@@ -0,0 +1,5 @@
1
+ require 'ruby/reports/services/data_iterator'
2
+ require 'ruby/reports/services/events_handler'
3
+ require 'ruby/reports/services/filename_generator'
4
+ require 'ruby/reports/services/query_builder'
5
+ require 'ruby/reports/services/table_builder'
@@ -0,0 +1,9 @@
1
+ module Ruby
2
+ module Reports
3
+ module Storages
4
+ HASH = :hash
5
+ OBJECT = :object
6
+ end
7
+ end
8
+ end
9
+
@@ -0,0 +1,7 @@
1
+ # coding: utf-8
2
+ module Ruby
3
+ module Reports
4
+ VERSION = '0.0.3'
5
+ end
6
+ end
7
+
@@ -0,0 +1,20 @@
1
+ require 'forwardable'
2
+ require 'attr_extras'
3
+
4
+ require 'ruby/reports/services'
5
+
6
+ require 'ruby/reports/cache_file'
7
+ require 'ruby/reports/base_report'
8
+ require 'ruby/reports/csv_report'
9
+ require 'ruby/reports/cache_file'
10
+ require 'ruby/reports/config'
11
+ require 'ruby/reports/storages'
12
+
13
+ require 'ruby/reports/version'
14
+
15
+ module Ruby
16
+ module Reports
17
+ CP1251 = 'cp1251'.freeze
18
+ UTF8 = 'utf-8'.freeze
19
+ end
20
+ end
@@ -0,0 +1,32 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'ruby/reports/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'ruby-reports'
8
+ spec.version = Ruby::Reports::VERSION
9
+ spec.authors = ['Sergey D.']
10
+ spec.email = ['sclinede@gmail.com']
11
+
12
+ spec.summary = 'Make your custom reports from any source to CSV by provided DSL'
13
+ spec.description = 'Make your custom reports from any source to CSV by provided DSL'
14
+ spec.homepage = 'https://github.com/sclinede/ruby-reports'
15
+ spec.license = 'MIT'
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
19
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
20
+ spec.require_paths = ['lib']
21
+
22
+ spec.add_runtime_dependency 'iron-dsl'
23
+ spec.add_runtime_dependency 'attr_extras'
24
+ spec.add_runtime_dependency 'facets'
25
+
26
+ spec.add_development_dependency 'rake'
27
+ spec.add_development_dependency 'pry'
28
+ spec.add_development_dependency 'rspec', '>= 2.14.0'
29
+ spec.add_development_dependency 'simplecov'
30
+ spec.add_development_dependency 'timecop', '~> 0.7.1'
31
+ spec.add_development_dependency 'codeclimate-test-reporter'
32
+ end
metadata ADDED
@@ -0,0 +1,195 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ruby-reports
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.3
5
+ platform: ruby
6
+ authors:
7
+ - Sergey D.
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-09-22 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ prerelease: false
15
+ name: iron-dsl
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - '>='
19
+ - !ruby/object:Gem::Version
20
+ version: '0'
21
+ type: :runtime
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ prerelease: false
29
+ name: attr_extras
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - '>='
33
+ - !ruby/object:Gem::Version
34
+ version: '0'
35
+ type: :runtime
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ prerelease: false
43
+ name: facets
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - '>='
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ type: :runtime
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ prerelease: false
57
+ name: rake
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - '>='
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ type: :development
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ prerelease: false
71
+ name: pry
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - '>='
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ type: :development
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ prerelease: false
85
+ name: rspec
86
+ requirement: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - '>='
89
+ - !ruby/object:Gem::Version
90
+ version: 2.14.0
91
+ type: :development
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - '>='
95
+ - !ruby/object:Gem::Version
96
+ version: 2.14.0
97
+ - !ruby/object:Gem::Dependency
98
+ prerelease: false
99
+ name: simplecov
100
+ requirement: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - '>='
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ type: :development
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - '>='
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ prerelease: false
113
+ name: timecop
114
+ requirement: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - ~>
117
+ - !ruby/object:Gem::Version
118
+ version: 0.7.1
119
+ type: :development
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ~>
123
+ - !ruby/object:Gem::Version
124
+ version: 0.7.1
125
+ - !ruby/object:Gem::Dependency
126
+ prerelease: false
127
+ name: codeclimate-test-reporter
128
+ requirement: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - '>='
131
+ - !ruby/object:Gem::Version
132
+ version: '0'
133
+ type: :development
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - '>='
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ description: Make your custom reports from any source to CSV by provided DSL
140
+ email:
141
+ - sclinede@gmail.com
142
+ executables:
143
+ - console
144
+ - setup
145
+ extensions: []
146
+ extra_rdoc_files: []
147
+ files:
148
+ - .gitignore
149
+ - .rspec
150
+ - .travis.yml
151
+ - Gemfile
152
+ - LICENSE.txt
153
+ - README.md
154
+ - Rakefile
155
+ - bin/console
156
+ - bin/setup
157
+ - lib/ruby/reports.rb
158
+ - lib/ruby/reports/base_report.rb
159
+ - lib/ruby/reports/cache_file.rb
160
+ - lib/ruby/reports/config.rb
161
+ - lib/ruby/reports/csv_report.rb
162
+ - lib/ruby/reports/services.rb
163
+ - lib/ruby/reports/services/data_iterator.rb
164
+ - lib/ruby/reports/services/events_handler.rb
165
+ - lib/ruby/reports/services/filename_generator.rb
166
+ - lib/ruby/reports/services/query_builder.rb
167
+ - lib/ruby/reports/services/table_builder.rb
168
+ - lib/ruby/reports/storages.rb
169
+ - lib/ruby/reports/version.rb
170
+ - ruby-reports.gemspec
171
+ homepage: https://github.com/sclinede/ruby-reports
172
+ licenses:
173
+ - MIT
174
+ metadata: {}
175
+ post_install_message:
176
+ rdoc_options: []
177
+ require_paths:
178
+ - lib
179
+ required_ruby_version: !ruby/object:Gem::Requirement
180
+ requirements:
181
+ - - '>='
182
+ - !ruby/object:Gem::Version
183
+ version: '0'
184
+ required_rubygems_version: !ruby/object:Gem::Requirement
185
+ requirements:
186
+ - - '>='
187
+ - !ruby/object:Gem::Version
188
+ version: '0'
189
+ requirements: []
190
+ rubyforge_project:
191
+ rubygems_version: 2.4.8
192
+ signing_key:
193
+ specification_version: 4
194
+ summary: Make your custom reports from any source to CSV by provided DSL
195
+ test_files: []