shipping_materials 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 09d4424dc7c0604733b0a036401f36ce73e2c18c
4
+ data.tar.gz: 8775e800e7580de9203ad819bb4c8337d6444f7c
5
+ SHA512:
6
+ metadata.gz: 001907c0d0a1ebda644b76221a44de39d5813a3fc542eaac733fc221dbd963d1d707ab6f812eda8f9ee8a2268327309ad4606146307c49d303c5b5ce06a176cd
7
+ data.tar.gz: f03e05bfec70bc67692286dcf4bfe0b681a23fa3090a4d0c3eb4e068c94a5093097777b22343eb2d57ab1cb5f913a1db5e452bdeeca4925df690791d42b87ac8
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in shipping_materials.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Andrew Haust
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,232 @@
1
+ # Shipping Materials
2
+
3
+ Shipping Materials provides a simple DSL for grouping and sorting a collection
4
+ of orders and their items and creating print materials for them. So far this
5
+ includes packing slips and CSVs for label makers.
6
+
7
+ ## Installation
8
+
9
+ $ gem install shipping_materials
10
+
11
+ ## Dependencies
12
+
13
+ `wkhtmltopdf` if used for PDF generation. The call to it is made as a linux
14
+ command therefore this plugin will not work on Windows yet.
15
+
16
+ `gzip` is used for the zip functionality.
17
+
18
+ ## Usage
19
+
20
+ ### Setup
21
+
22
+ There is a little bit of configuration you are going to want to do first and
23
+ that is to add a save_path.
24
+
25
+ ```ruby
26
+ ShippingMaterials.config do |config|
27
+ config.save_path = 'local/save/path'
28
+ end
29
+ ```
30
+
31
+ If you would like to use S3, add the following:
32
+
33
+ ```ruby
34
+ ShippingMaterials.config do |config|
35
+ config.s3_bucket = 'bucket.domain.com'
36
+ config.s3_access_key_id = ENV['AWS_ACCESS_KEY']
37
+ config.s3_secret_access_key = ENV['AWS_SECRET_ACCESS_KEY']
38
+ end
39
+ ```
40
+
41
+ ### The Packager
42
+
43
+ The DSL is provided via the ShippingMaterials::Packager class.
44
+
45
+ ```ruby
46
+ packager = ShippingMaterials::Packager.new
47
+ ```
48
+
49
+ The Packager's `#package` method takes a collection of objects of the same
50
+ type.
51
+
52
+ ```ruby
53
+ orders = Order.where(state: 'placed')
54
+
55
+ packager.package :orders do
56
+ # ...
57
+ end
58
+ ```
59
+
60
+ Because we are creating shipping materials here, at the very least, it is
61
+ assumed you are going to want packing slips. You may specify a global template
62
+ with the `#pdf` method:
63
+
64
+ ```ruby
65
+ packager.package orders do
66
+ pdf 'path/to/template.mustache'
67
+ end
68
+ ```
69
+
70
+ Now, at the simplest level, we can start breaking these objects down into
71
+ groups.
72
+
73
+ ```ruby
74
+ packager.package orders do
75
+ pdf 'path/to/template.mustache'
76
+
77
+ group 'Canadian Standard Post' do
78
+ filter {
79
+ ship_method == 'std' && country == 'CA'
80
+ }
81
+ end
82
+
83
+ group 'United States UPS Expedited' do
84
+ filter {
85
+ ship_method == 'UPSexp' && country == 'US'
86
+ }
87
+ end
88
+
89
+ group 'International World Ship' do
90
+ filter {
91
+ ship_method == 'UPS' && !%w[ US CA ].include?(country)
92
+ }
93
+ end
94
+ end
95
+ ```
96
+
97
+ PDFs (one per group) will now be created. With groups named as above, you can
98
+ expect the file names 'CanadianStandardPost.pdf', 'UnitedStatesUPSExpedite.pdf'
99
+ and 'InternationalWorldShip.pdf'.
100
+
101
+ ### Templating
102
+
103
+ Right now, templating is done with Mustache. I plan on adding more in the
104
+ future (or feel free to send me a pull request). Here is an example template:
105
+
106
+ ```html
107
+ <html>
108
+ {{# objects }}
109
+ <div>
110
+ <p>{{ number }}</p>
111
+ {{# line_items }}
112
+ <p>{{ name }}: ${{ price }} x {{ quantity }} = {{ total }}</p>
113
+ {{/ line_items }}
114
+ </div>
115
+ {{/ objects }}
116
+ </html>
117
+ ```
118
+
119
+
120
+ I chose the 'objects' as the default variable name, though you may change this
121
+ in config:
122
+
123
+ ```ruby
124
+ ShippingMaterials.config do |config|
125
+ config.base_context = 'orders'
126
+ end
127
+ ```
128
+
129
+ Each group will produce one PDF.
130
+
131
+ ### CSV (for shipping labels)
132
+
133
+ Any label printer I know -- as well as things like UPS Worldship -- use CSVs,
134
+ so Shipping Materials provides a little CSV templating DSL. This is provided
135
+ via the `Group#csv` method.
136
+
137
+ The `#csv` method takes a block which exposes the `#row` method. `#row` takes
138
+ a hash or an array and may be called multiple times.
139
+
140
+ Here is an example with hashes:
141
+
142
+ ```ruby
143
+ group 'Canadian Standard Post' do
144
+ csv(headers: true) {
145
+ row 'Code' => 'Q',
146
+ 'Order Number' => :number,
147
+ 'Name' => [ :shipping_address, :name ]
148
+ # ...
149
+ 'Country' => [ :shipping_address, :country, :iso ]
150
+
151
+ row line_items: [ 'H', :id, :name, :quantity, :price ]
152
+ }
153
+ end
154
+ ```
155
+
156
+ In this example, the first call to row is evaluated in the context of each
157
+ order. Symbols are called as methods whereas string values are kept as-is. In
158
+ order to chain method calls, use an array of symbols as a value. For example,
159
+ `[ :shipping_address, :country, :iso ]` will call
160
+ `order.shipping_method.country.iso`.
161
+
162
+ Passing `headers: true` to the csv method will use the keys from the _first_
163
+ call to row (if it is a hash) as the headers for the CSV.
164
+
165
+ As demonstrated in the second call to row, you can evalute your row in the
166
+ context of your line items (or other one-to-many relationship) using its method
167
+ name as a key.
168
+
169
+ ### Sorting
170
+
171
+ While most sorting should probably be done at the query level, Shipping
172
+ Materials provides a sorting DSL for more complex sorts. For example:
173
+
174
+ ```ruby
175
+ packager.package orders do
176
+ pdf 'path/to/template.mustache'
177
+
178
+ sort do
179
+ # put orders containing only Vinyl at the top
180
+ rule {
181
+ return true unless line_items.detect {|li| type != 'Vinyl' }
182
+ }
183
+
184
+ # next come orders that have both Vinyl and CDs and nothing else
185
+ rule {
186
+ line_items.select {|li| %w[ Vinyl CD ].include?(li.type) }.uniq.size == 2
187
+ }
188
+
189
+ # get the picture?
190
+ end
191
+
192
+ group # ...
193
+ end
194
+ ```
195
+
196
+ A merge sort will be performed sorting the orders within each group according
197
+ to each rule in the order they are defined.
198
+
199
+ While it is definitely recommended to sort line items at the query level, you
200
+ can operate in the context of line items by passing the name of the association
201
+ to the sort method (ie: your association doesn't have to be called "line_items"
202
+ specifically):
203
+
204
+ ```ruby
205
+ sort(:line_items) do
206
+ rule { type == 'Vinyl' }
207
+ rule { type == 'CD' }
208
+ rule { type == 'Cassette' }
209
+ rule { type == '8-Track' }
210
+ end
211
+ ```
212
+
213
+ This will sort your line items within each packing slip.
214
+
215
+ ## Documentation
216
+
217
+ Other than this README, there is no documentation yet. There are a few other
218
+ experimental and volatile features not mentioned here. They are certainly
219
+ going to change soon.
220
+
221
+ ## Contributing
222
+
223
+ This is my first foray into the world of library authoring. I welcome any and
224
+ all advice and pull requests with open arms, but for the love of whoever or
225
+ whatever you believe in: please follow [these
226
+ guidelines](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html)
227
+ when writing your commit messages.
228
+
229
+ ## A note about the tests
230
+
231
+ I am still learning to test properly. Tests are passing _but_ some of them are
232
+ writing to the filesystem. These wrongs will be righted in due time.
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,33 @@
1
+ ShippingMaterials.config do |config|
2
+ config.save_path = MyProject::Config[:asset_storage_location]
3
+ end
4
+
5
+ packager = ShippingMaterials::Packager.new
6
+
7
+ orders = Orders.where(state: 'placed')
8
+
9
+ packager.package orders do
10
+ pdf 'path/to/template',
11
+
12
+ sort(:line_items) {
13
+ rule { type == 'Vinyl' }
14
+ rule { type == 'CD' }
15
+ rule { type == 'Cassette' }
16
+ }
17
+
18
+ sort {
19
+ rule { line_items.detect {|li| li.type == 'vinyl' } }
20
+ }
21
+
22
+ group 'canada_standard_shipping' do
23
+ filter { country == 'CA' && shipping_method == 'std' }
24
+
25
+ csv(extenstion: 'txt', headers: true) {
26
+ row code: 'Q',
27
+ order_id: :id,
28
+ hello: :hello
29
+
30
+ row line_items: [ 'H', :id, :name, :price, :quantity ]
31
+ }
32
+ end
33
+ end
@@ -0,0 +1,19 @@
1
+ require 'mustache'
2
+
3
+ require 'shipping_materials/version'
4
+ require 'shipping_materials/mixins/sortable'
5
+
6
+ require 'shipping_materials/config'
7
+ require 'shipping_materials/storage'
8
+ require 'shipping_materials/packager'
9
+ require 'shipping_materials/group'
10
+ require 'shipping_materials/csv_dsl'
11
+ require 'shipping_materials/packing_slips'
12
+ require 'shipping_materials/sorter'
13
+ require 'shipping_materials/s3'
14
+
15
+ module ShippingMaterials
16
+ def self.config
17
+ yield Config
18
+ end
19
+ end
@@ -0,0 +1,35 @@
1
+ module ShippingMaterials
2
+ class Config
3
+ class << self
4
+ attr_accessor :s3_bucket,
5
+ :s3_access_key,
6
+ :s3_secret,
7
+ :gzip_file_name
8
+
9
+ def base_context
10
+ @base_context || :objects
11
+ end
12
+
13
+ def base_context=(bc)
14
+ @base_context = bc.to_sym
15
+ end
16
+
17
+ def save_path=(save_path)
18
+ @save_path = save_path.sub(/(\/)+$/, '')
19
+ end
20
+
21
+ def save_path
22
+ @default ||= Time.now.to_i
23
+ @save_path || "/tmp/shipping_materials#{@default}"
24
+ end
25
+
26
+ def use_s3?
27
+ @s3_bucket
28
+ end
29
+
30
+ def use_gzip?
31
+ @gzip_file_name
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,82 @@
1
+ module ShippingMaterials
2
+ class CSVDSL
3
+ require 'csv'
4
+
5
+ attr_accessor :objects, :row_maps
6
+
7
+ def initialize(objects, options={})
8
+ @objects = objects
9
+ @row_maps = {}
10
+ @options = options
11
+ end
12
+
13
+ # This method is on the complex side. It is a DSL method that
14
+ # performs type-checking and also sets the headers.
15
+ # Be sure to see headers=() defined below
16
+ def row(collection)
17
+ if collection.is_a? Array
18
+ @row_maps[:object] = collection
19
+ elsif collection.is_a? Hash
20
+ f = collection.first
21
+ if f[1].is_a? Array
22
+ @row_maps[f[0]] = f[1]
23
+ elsif f[1].is_a? Hash
24
+ self.headers = collection.first[1]
25
+ @row_maps[f[0]] = f[1].values
26
+ else
27
+ self.headers = collection
28
+ @row_maps[:object] = collection.values
29
+ end
30
+ end
31
+ end
32
+
33
+ def to_csv
34
+ CSV.generate do |csv|
35
+ csv << headers if headers?
36
+ @objects.each do |object|
37
+ @row_maps.each do |context, methods|
38
+ if context == :object
39
+ csv << get_row(object, methods)
40
+ else
41
+ object.send(context).each do |c|
42
+ csv << get_row(c, methods)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+ alias_method :to_s, :to_csv
51
+
52
+ def extension
53
+ @options[:extension] || 'csv'
54
+ end
55
+
56
+ def headers
57
+ @headers
58
+ end
59
+
60
+ def headers=(object)
61
+ @headers ||= object.keys.map {|h| h.to_s } if self.headers?
62
+ end
63
+
64
+ def headers?
65
+ @options[:headers]
66
+ end
67
+
68
+ private
69
+
70
+ def get_row(object, methods)
71
+ methods.map do |meth|
72
+ if meth.is_a? Symbol
73
+ object.send(meth)
74
+ elsif meth.is_a? Array
75
+ meth.reduce(object) {|obj, m| obj.send(m) }
76
+ elsif meth.is_a? String
77
+ meth
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end