thousand_island 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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