thousand_island 0.0.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.
@@ -0,0 +1,280 @@
1
+ module ThousandIsland
2
+ # The Template class is where you can define elements that may be common to
3
+ # all (or some) documents within your application. It is likely that a common
4
+ # style will be required, so defining it in a Template and then using that
5
+ # Template subclass in any custom Builders DRYs up your pdf generation, as
6
+ # well as allowing for easy restyling across the whole application.
7
+ #
8
+ # Typically, the Template subclass would define the settings for the PrawnDocument,
9
+ # as well as the settings for the header and footer. See the Docs below for the
10
+ # <code>settings</code> method for the defaults. Add your own or override any
11
+ # existing settings in the <code>settings</code> method. Any options passed into
12
+ # the constructor as a Hash will be merged with these settings, and the defaults.
13
+ #
14
+ # Content for the header and footer will be defined in the methods
15
+ # <code>header_content</code> and <code>footer_content</code>. These methods are
16
+ # passed as a block when the pdf is rendered. Any standard Prawn methods may be
17
+ # used (including bounding boxes or any other layout tools). In addition, any
18
+ # of the styles from the <code>StyleSheet</code> can be applied as helper methods.
19
+ # For instance, the default style sheet has a <code>h1_style</code> method that
20
+ # returns a ThousandIsland::StyleHash, so in your code you can use:
21
+ # h1 "My Document Header"
22
+ # and Prawn will render the text in the style set in the <code>h1_style</code>
23
+ # ThousandIsland::StyleHash.
24
+ #
25
+ # In addition to the supplied style methods, you can create a custom method:
26
+ # def magic_style
27
+ # ThousandIsland::StyleHash.new({
28
+ # size: 15
29
+ # style: bold
30
+ # })
31
+ # end
32
+ # As long as the method ends in the word "_style" and returns a Hash, you magically
33
+ # get to do this:
34
+ # magic "My magic text is bold and size 15!!"
35
+ # The method may return a standard Hash, but it is safer to return a
36
+ # ThousandIsland::StyleHash, as this dynamically duplicates a few keys to accommodate
37
+ # using the style in normal Prawn text methods as well as formatted text boxes, which
38
+ # use a slightly different convention. You don't have to worry about that if you use
39
+ # the ThousandIsland::StyleHash.
40
+ #
41
+ # Alternatively, your method could do this:
42
+ # def magic_style
43
+ # h1_style.merge({
44
+ # size: 15
45
+ # style: bold
46
+ # })
47
+ # end
48
+ #
49
+ # The following is an example of a custom template that subclasses
50
+ # ThousandIsland::Template -
51
+ #
52
+ # class MyTemplate < ThousandIsland::Template
53
+ # include MyCustomStyleSheet # optional
54
+ #
55
+ # # settings here are merged with and override the defaults
56
+ # def settings
57
+ # {
58
+ # header: {
59
+ # height: 55,
60
+ # render:true,
61
+ # repeated: true
62
+ # },
63
+ # footer: {
64
+ # render:true,
65
+ # height: 9,
66
+ # numbering_string: 'Page <page> of <total>',
67
+ # repeated: true
68
+ # }
69
+ # }
70
+ # end
71
+ #
72
+ # def header_content
73
+ # pdf.image "#{pdf_images_path}/company_logo.png", height: 30 # Standard Prawn syntax
74
+ # end
75
+ #
76
+ # def footer_content
77
+ # footer "www.mycompanyurl.com" # Using the magic method we get from the footer_style
78
+ # end
79
+ #
80
+ # def pdf_images_path
81
+ # "#{Rails.root}/app/assets/pdf_images" # This is entirely up to you
82
+ # end
83
+ # end
84
+ #
85
+ # Nb.
86
+ # The Footer is a three column layout, with the numbering on the right column
87
+ # and the content defined here in the middle. More flexibility will be added
88
+ # in a later version.
89
+ #
90
+ # Optional:
91
+ #
92
+ # Add a <code>body_content</code> method to add content before whatever the
93
+ # Builder defines in it's method of the same name.
94
+ #
95
+ class Template
96
+ include ThousandIsland::StyleSheet
97
+
98
+ attr_reader :pdf, :pdf_options
99
+
100
+ def initialize(options={})
101
+ setup_document_options(options)
102
+ setup_prawn_document
103
+ calculate_bounds
104
+ end
105
+
106
+ # Override in inheriting class to override defaults. The default settings
107
+ # are:
108
+ # page_size: 'A4',
109
+ # page_layout: :portrait,
110
+ # left_margin: 54,
111
+ # right_margin: 54,
112
+ # header: {
113
+ # render: true,
114
+ # height: 33,
115
+ # bottom_padding: 20,
116
+ # repeated: true
117
+ # },
118
+ # footer: {
119
+ # render: true,
120
+ # height: 33,
121
+ # top_padding: 20,
122
+ # repeated: true,
123
+ # number_pages: true,
124
+ # numbering_string: '<page>',
125
+ # numbering_options: {
126
+ # align: :right,
127
+ # start_count_at: 1,
128
+ # }
129
+ # The settings in the hash will be merged with the default settings. Any Prawn
130
+ # setting <i>should</i> be valid at the top level of the hash.
131
+ # The styles used in the Header and Footer are determined by the default styles in the
132
+ # StyleSheet, but can be overridden in your Template class or by building your own StyleSheet
133
+ def settings
134
+ {}
135
+ end
136
+
137
+ def draw_body(&block)
138
+ body_obj.draw do
139
+ body_content if respond_to? :body_content
140
+ yield if block_given?
141
+ end
142
+ end
143
+
144
+ def draw_header
145
+ header_obj.draw do
146
+ header_content if respond_to? :header_content
147
+ yield if block_given?
148
+ end if render_header?
149
+ end
150
+
151
+ def draw_footer(&block)
152
+ footer_obj.draw do
153
+ yield if block_given?
154
+ footer_content &block if respond_to? :footer_content
155
+ end if render_footer?
156
+ end
157
+
158
+ private
159
+
160
+ def render_header?
161
+ pdf_options[:header][:render]
162
+ end
163
+
164
+ def render_footer?
165
+ pdf_options[:footer][:render]
166
+ end
167
+
168
+
169
+
170
+ def calculate_bounds
171
+ pdf_options[:body][:top] = body_start
172
+ pdf_options[:body][:height] = body_height
173
+ end
174
+
175
+ def body_start
176
+ @body_start ||= pdf.bounds.height - header_space
177
+ end
178
+
179
+ def body_height
180
+ @body_height ||= body_start - footer_space
181
+ end
182
+
183
+ def header_space
184
+ return (pdf_options[:header][:height] + pdf_options[:header][:bottom_padding]) if pdf_options[:header][:render]
185
+ 0
186
+ end
187
+
188
+ def footer_space
189
+ return (pdf_options[:footer][:height] + pdf_options[:footer][:top_padding]) if pdf_options[:footer][:render]
190
+ 0
191
+ end
192
+
193
+
194
+ def setup_prawn_document
195
+ @pdf = Prawn::Document.new(pdf_options)
196
+ end
197
+
198
+ def setup_document_options(options={})
199
+ @pdf_options = deep_merger.merge_options(options, settings, defaults, component_defaults)
200
+ end
201
+
202
+
203
+ def component_defaults
204
+ components = {
205
+ footer: footer_klass.defaults,
206
+ header: header_klass.defaults,
207
+ body: body_klass.defaults,
208
+ }
209
+ components[:footer][:style] = footer_style
210
+ components
211
+ end
212
+
213
+ def header_klass
214
+ Components::Header
215
+ end
216
+
217
+ def header_obj
218
+ @header ||= header_klass.new(pdf, pdf_options[:header])
219
+ end
220
+
221
+ def footer_klass
222
+ Components::Footer
223
+ end
224
+
225
+ def footer_obj
226
+ @footer ||= footer_klass.new(pdf, pdf_options[:footer])
227
+ end
228
+
229
+ def body_klass
230
+ Components::Body
231
+ end
232
+
233
+ def body_obj
234
+ @body ||= body_klass.new(pdf, pdf_options[:body])
235
+ end
236
+
237
+
238
+
239
+ def deep_merger
240
+ @deep_merger ||= Utilities::DeepMerge
241
+ end
242
+
243
+ # Called by method missing when a style is supplied with text, ie: h1 'Header'
244
+ def render_with_style(style, output)
245
+ style_values = send("#{style}_style")
246
+ pdf.text output, style_values
247
+ end
248
+
249
+ def defaults
250
+ {
251
+ page_size: 'A4',
252
+ page_layout: :portrait,
253
+ left_margin: 54,
254
+ right_margin: 54,
255
+ header: {
256
+ render: true,
257
+ },
258
+ footer: {
259
+ render: true,
260
+ },
261
+ body: {},
262
+ }
263
+ end
264
+
265
+ #Respond to methods that relate to the style_sheet known styles
266
+ def method_missing(method_name, *arguments, &block)
267
+ style_method = "#{method_name}_style"
268
+ if respond_to?(style_method)
269
+ render_with_style(method_name, arguments[0])
270
+ else
271
+ super
272
+ end
273
+ end
274
+
275
+ def respond_to_missing?(method_name, *)
276
+ available_styles.include?(method_name) || super
277
+ end
278
+
279
+ end
280
+ end
@@ -0,0 +1,20 @@
1
+ module ThousandIsland
2
+ # A subclass of Hash, automatically adds keys that mirror other keys to allow
3
+ # for a couple of small differences in the Prawn options hashes:
4
+ # :font_style = :style
5
+ # :styles = :style and puts it into an Array
6
+ class StyleHash < Hash
7
+ def initialize(style={})
8
+ super()
9
+ self.merge!(style)
10
+ end
11
+
12
+ def [](key)
13
+ return self[:size] if key == :font_size
14
+ return [self[:style]] if key == :styles
15
+ super
16
+ end
17
+
18
+ end
19
+
20
+ end
@@ -0,0 +1,58 @@
1
+ module ThousandIsland
2
+ module Utilities
3
+
4
+ module DeepMerge
5
+
6
+ #Take a number of hashes and merge them into one, respecting
7
+ # the structure and nesting according to the pdf options hash.
8
+ # Hashes work in order of precedence, the first in the array
9
+ # overrides, the second, etc.
10
+ #
11
+ # @param hashes [*Hash] A number of hashes to merge, in the order of precedence
12
+ #
13
+ # @return [Hash] the merged values
14
+ def self.merge_options(*hashes)
15
+ hashes.reverse!
16
+ merged = {}
17
+ footer = merge_footer(*hashes)
18
+ header = merge_header(*hashes)
19
+ body = merge_body(*hashes)
20
+ hashes.each do |h|
21
+ merged.merge!(h)
22
+ end
23
+ merged[:footer] = footer
24
+ merged[:header] = header
25
+ merged[:body] = body
26
+ merged
27
+ end
28
+
29
+ def self.merge_footer(*hashes)
30
+ keys = [:numbering_options, :style]
31
+ merge_for_key_and_nested_keys(:footer, keys, *hashes)
32
+ end
33
+
34
+ def self.merge_header(*hashes)
35
+ merge_for_key_and_nested_keys(:header, [], *hashes)
36
+ end
37
+
38
+ def self.merge_body(*hashes)
39
+ merge_for_key_and_nested_keys(:body, [], *hashes)
40
+ end
41
+
42
+ def self.merge_for_key_and_nested_keys(key, keys, *hashes)
43
+ temp = {}
44
+ merged = {}
45
+ hashes.each do |h|
46
+ keys.each do |k|
47
+ temp[k] = {} unless temp.has_key? k
48
+ temp[k].merge!(h[key][k]) if h[key] && h[key][k]
49
+ end
50
+ merged.merge!(h[key]) if h[key]
51
+ end
52
+ merged.merge(temp)
53
+ end
54
+
55
+ end
56
+
57
+ end
58
+ end
@@ -0,0 +1,3 @@
1
+ module ThousandIsland
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,15 @@
1
+ require "codeclimate-test-reporter"
2
+ CodeClimate::TestReporter.start
3
+
4
+ require 'thousand_island'
5
+
6
+ RSpec.configure do |config|
7
+ if config.files_to_run.one?
8
+ config.default_formatter = 'doc'
9
+ end
10
+ config.order = :random
11
+
12
+ config.mock_with :rspec do |mocks|
13
+ mocks.verify_doubled_constant_names = true
14
+ end
15
+ end
@@ -0,0 +1,106 @@
1
+ module ThousandIsland
2
+ describe Builder do
3
+
4
+ context 'template' do
5
+
6
+ it 'raises with no template' do
7
+ klass = described_class.dup
8
+ builder = klass.new
9
+ expect{ builder.build }.to raise_error(TemplateRequiredError)
10
+ end
11
+
12
+ it 'method instantiates a new instance of the template_class' do
13
+ klass = described_class.dup
14
+ template = double(:template)
15
+ allow(template).to receive(:available_styles) {[]}
16
+ expect(template).to receive(:new).with({}) { template }
17
+ expect(template).to receive(:pdf) {}
18
+ klass.uses_template(template)
19
+ klass.new.send(:template)
20
+ end
21
+ end
22
+
23
+ context 'with instance' do
24
+
25
+ describe 'content method is called when it' do
26
+ let(:template) { ThousandIsland::Template.new }
27
+ let(:klass) { described_class.dup }
28
+ let(:builder) { klass.new }
29
+
30
+ before :each do
31
+ allow(builder).to receive(:template) { template }
32
+ # klass.send(:define_method, :header_content, ->{})
33
+ # klass.send(:define_method, :body_content, ->{})
34
+ # klass.send(:define_method, :footer_content, ->{})
35
+ end
36
+ context 'exists' do
37
+
38
+ it 'header_content' do
39
+ # allow(builder).to receive(:header_content)
40
+ expect(builder).to receive(:header_content)
41
+ builder.send(:draw_header)
42
+ end
43
+ it 'body_content' do
44
+ expect(builder).to receive(:body_content)
45
+ builder.send(:draw_body)
46
+ end
47
+ it 'footer_content' do
48
+ expect(builder).to receive(:footer_content)
49
+ builder.send(:draw_footer)
50
+ end
51
+ end
52
+ context 'does not exist' do
53
+ it 'header_content' do
54
+ expect(builder).to_not respond_to(:header_content)
55
+ end
56
+ it 'body_content' do
57
+ expect(builder).to_not respond_to(:body_content)
58
+ end
59
+ it 'footer_content' do
60
+ expect(builder).to_not respond_to(:footer_content)
61
+ end
62
+ end
63
+ end
64
+
65
+ context 'calls template method' do
66
+ let(:template) { ThousandIsland::Template.new }
67
+ let(:pdf) { instance_double('Prawn::Document') }
68
+ let(:klass) { described_class.dup }
69
+ let(:builder) { klass.new }
70
+
71
+ before :each do
72
+ allow(template).to receive(:draw_body) { template }
73
+ allow(template).to receive(:draw_header) { template }
74
+ allow(template).to receive(:draw_footer) { template }
75
+ allow(template).to receive(:available_styles) { [:h1] }
76
+ allow(pdf).to receive(:render)
77
+ allow(builder).to receive(:template) { template }
78
+ allow(builder).to receive(:pdf) { pdf }
79
+ end
80
+
81
+ it '#draw_body' do
82
+ expect(template).to receive(:draw_body)
83
+ builder.build
84
+ end
85
+
86
+ it '#header' do
87
+ expect(template).to receive(:draw_header)
88
+ builder.build
89
+ end
90
+
91
+ it '#draw_footer' do
92
+ expect(template).to receive(:draw_footer)
93
+ builder.build
94
+ end
95
+
96
+ describe 'style methods' do
97
+ it 'h1' do
98
+ allow(template).to receive(:h1)
99
+ expect(template).to receive(:available_styles)
100
+ builder.h1 'text'
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end