im_onix 1.0.2 → 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +4 -0
  3. data/.yardopts +1 -0
  4. data/Gemfile +8 -0
  5. data/LICENSE.md +7 -0
  6. data/README.md +3 -3
  7. data/Rakefile +10 -0
  8. data/bin/html_codelist_to_yml.rb +14 -15
  9. data/bin/onix_bench.rb +1 -0
  10. data/bin/onix_pp.rb +4 -8
  11. data/bin/onix_serialize.rb +27 -0
  12. data/doc-src/handlers.rb +154 -0
  13. data/im_onix.gemspec +32 -0
  14. data/lib/im_onix.rb +0 -1
  15. data/lib/onix/addressee.rb +10 -0
  16. data/lib/onix/code.rb +108 -282
  17. data/lib/onix/collateral_detail.rb +24 -17
  18. data/lib/onix/collection.rb +38 -0
  19. data/lib/onix/collection_sequence.rb +7 -0
  20. data/lib/onix/contributor.rb +40 -39
  21. data/lib/onix/date.rb +73 -109
  22. data/lib/onix/descriptive_detail.rb +90 -417
  23. data/lib/onix/discount_coded.rb +3 -16
  24. data/lib/onix/entity.rb +28 -62
  25. data/lib/onix/epub_usage_constraint.rb +7 -0
  26. data/lib/onix/epub_usage_limit.rb +6 -0
  27. data/lib/onix/extent.rb +39 -0
  28. data/lib/onix/helper.rb +25 -25
  29. data/lib/onix/identifier.rb +13 -54
  30. data/lib/onix/language.rb +8 -0
  31. data/lib/onix/market.rb +5 -0
  32. data/lib/onix/market_publishing_detail.rb +20 -0
  33. data/lib/onix/onix21.rb +76 -139
  34. data/lib/onix/onix_message.rb +87 -100
  35. data/lib/onix/price.rb +19 -39
  36. data/lib/onix/product.rb +141 -637
  37. data/lib/onix/product_form_feature.rb +7 -0
  38. data/lib/onix/product_part.rb +89 -0
  39. data/lib/onix/product_supplies_extractor.rb +275 -0
  40. data/lib/onix/product_supply.rb +17 -58
  41. data/lib/onix/publishing_detail.rb +16 -32
  42. data/lib/onix/related_material.rb +4 -3
  43. data/lib/onix/related_product.rb +9 -29
  44. data/lib/onix/related_work.rb +3 -17
  45. data/lib/onix/sales_outlet.rb +2 -10
  46. data/lib/onix/sales_restriction.rb +8 -21
  47. data/lib/onix/sales_rights.rb +1 -5
  48. data/lib/onix/sender.rb +12 -0
  49. data/lib/onix/serializer.rb +156 -0
  50. data/lib/onix/subject.rb +9 -30
  51. data/lib/onix/subset.rb +88 -78
  52. data/lib/onix/supply_detail.rb +42 -0
  53. data/lib/onix/supporting_resource.rb +29 -86
  54. data/lib/onix/tax.rb +9 -18
  55. data/lib/onix/territory.rb +23 -17
  56. data/lib/onix/title_detail.rb +22 -0
  57. data/lib/onix/title_element.rb +32 -0
  58. data/lib/onix/website.rb +3 -16
  59. metadata +53 -34
@@ -0,0 +1,7 @@
1
+ module ONIX
2
+ class ProductFormFeature < SubsetDSL
3
+ element "ProductFormFeatureType", :subset, :shortcut => :type
4
+ element "ProductFormFeatureValue", :text, :shortcut => :value
5
+ elements "ProductFormFeatureDescription", :text, :shortcut => :descriptions
6
+ end
7
+ end
@@ -0,0 +1,89 @@
1
+ module ONIX
2
+ # product part use full Product to provide file protection and file size
3
+ class ProductPart < SubsetDSL
4
+ include EanMethods
5
+ include ProprietaryIdMethods
6
+
7
+ elements "ProductIdentifier", :subset, :shortcut => :identifiers
8
+ element "ProductForm", :subset, :shortcut => :form
9
+ element "ProductFormDescription", :text, :shortcut => :file_description
10
+ elements "ProductFormDetail", :subset, :shortcut => :form_details
11
+ elements "ProductContentType", :subset, :shortcut => :content_types
12
+ element "NumberOfCopies", :integer
13
+
14
+ def file_formats
15
+ @product_form_details.select { |fd| fd.code =~ /^E1.*/ }
16
+ end
17
+
18
+ # full Product if referenced in ONIXMessage
19
+ attr_accessor :product
20
+
21
+ # this ProductPart is part of Product
22
+ attr_accessor :part_of
23
+
24
+ # @!group High level
25
+
26
+ # digital file format string (Epub,Pdf,AmazonKindle)
27
+ # @return [String]
28
+ def file_format
29
+ file_formats.first.human if file_formats.first
30
+ end
31
+
32
+ # digital file format mimetype
33
+ # @return [String]
34
+ def file_mimetype
35
+ if file_formats.first
36
+ file_formats.first.mimetype
37
+ end
38
+ end
39
+
40
+ # is digital file reflowable ?
41
+ # @return [Boolean]
42
+ def reflowable?
43
+ return true if @product_form_details.select { |fd| fd.code == "E200" }.length > 0
44
+ return false if @product_form_details.select { |fd| fd.code == "E201" }.length > 0
45
+ end
46
+
47
+ # raw part file description string without HTML
48
+ # @return [String]
49
+ def raw_file_description
50
+ if @product_form_description
51
+ Helper.strip_html(@product_form_description).gsub(/\s+/, " ").strip
52
+ end
53
+ end
54
+
55
+ # Protection type string (None, Watermarking, DRM, AdobeDRM)
56
+ # @return [String]
57
+ def protection_type
58
+ if product
59
+ product.protection_type
60
+ else
61
+ if part_of
62
+ part_of.protection_type
63
+ end
64
+ end
65
+ end
66
+
67
+ # List of protections type string (None, Watermarking, DRM, AdobeDRM)
68
+ # @return [Array<String>]
69
+ def protections
70
+ if product
71
+ product.protections
72
+ else
73
+ if part_of
74
+ part_of.protections
75
+ end
76
+ end
77
+ end
78
+
79
+ # digital file filesize in bytes
80
+ # @return [Integer]
81
+ def filesize
82
+ if product
83
+ product.filesize
84
+ end
85
+ end
86
+
87
+ # @!endgroup
88
+ end
89
+ end
@@ -0,0 +1,275 @@
1
+ module ONIX
2
+ # flattened supplies extractor
3
+ module ProductSuppliesExtractor
4
+ # class must define a product_supplies returning an Array of objects responding to :
5
+ # - availability_date (Date)
6
+ # - countries (country code Array)
7
+
8
+ # @!group High level
9
+
10
+ # flattened supplies with prices
11
+ #
12
+ # supplies is a hash symbol array in the form :
13
+ # [{:available=>bool,
14
+ # :availability_date=>date,
15
+ # :including_tax=>bool,
16
+ # :currency=>string,
17
+ # :territory=>string,
18
+ # :suppliers=>[Supplier,...],
19
+ # :prices=>[{:amount=>int,
20
+ # :from_date=>date,
21
+ # :until_date=>date,
22
+ # :tax=>{:amount=>int, :rate_percent=>float}}]}]
23
+ def supplies(keep_all_prices_dates = false)
24
+ supplies = []
25
+
26
+ # add territories if missing
27
+ if self.product_supplies
28
+ self.product_supplies.each do |ps|
29
+ ps.supply_details.each do |sd|
30
+ sd.prices.each do |p|
31
+ supply = {}
32
+ supply[:suppliers] = sd.suppliers
33
+ supply[:available] = sd.available?
34
+ supply[:availability_date] = sd.availability_date
35
+
36
+ unless supply[:availability_date]
37
+ if ps.availability_date
38
+ supply[:availability_date] = ps.market_publishing_detail.availability_date
39
+ end
40
+ end
41
+ supply[:price] = p.amount
42
+ supply[:qualifier] = p.qualifier.human if p.qualifier
43
+ supply[:including_tax] = p.including_tax?
44
+ if !p.territory or p.territory.countries.length == 0
45
+ supply[:territory] = []
46
+ supply[:territory] = ps.countries
47
+
48
+ if supply[:territory].length == 0
49
+ if @publishing_detail
50
+ supply[:territory] = self.countries_rights
51
+ end
52
+ end
53
+ else
54
+ supply[:territory] = p.territory.countries
55
+ end
56
+ supply[:from_date] = p.from_date
57
+ supply[:until_date] = p.until_date
58
+ supply[:currency] = p.currency
59
+ supply[:tax] = p.tax
60
+
61
+ unless supply[:availability_date]
62
+ if @publishing_detail
63
+ supply[:availability_date] = @publishing_detail.publication_date
64
+ end
65
+ end
66
+
67
+ supplies << supply
68
+ end
69
+ end
70
+ end
71
+ end
72
+
73
+ grouped_supplies = {}
74
+ supplies.each do |supply|
75
+ supply[:territory].each do |territory|
76
+ pr_key = "#{supply[:available]}_#{supply[:including_tax]}_#{supply[:currency]}_#{territory}"
77
+ grouped_supplies[pr_key] ||= []
78
+ grouped_supplies[pr_key] << supply
79
+ end
80
+ end
81
+
82
+ nb_suppliers = supplies.map { |s| s[:suppliers][0].name }.uniq.length
83
+ # render prices sequentially with dates
84
+ grouped_supplies.each do |ksup, supply|
85
+ if supply.length > 1
86
+ global_price = supply.select { |p| not p[:from_date] and not p[:until_date] }
87
+ global_price = global_price.first
88
+
89
+ if global_price
90
+ if nb_suppliers > 1
91
+ grouped_supplies[ksup] += self.prices_with_periods(supply, global_price)
92
+ else
93
+ grouped_supplies[ksup] = self.prices_with_periods(supply, global_price)
94
+ end
95
+ grouped_supplies[ksup].uniq!
96
+ else
97
+ # remove explicit from date
98
+ explicit_from = supply.select { |p| p[:from_date] and not supply.select { |sp| sp[:until_date] and sp[:until_date] <= p[:from_date] }.first }.first
99
+ if explicit_from
100
+ explicit_from[:from_date] = nil unless keep_all_prices_dates
101
+ end
102
+ end
103
+ else
104
+ supply.each do |s|
105
+ if s[:from_date] and s[:availability_date] and s[:from_date] >= s[:availability_date]
106
+ s[:availability_date] = s[:from_date]
107
+ end
108
+ s[:from_date] = nil unless keep_all_prices_dates
109
+ end
110
+ end
111
+ end
112
+
113
+ # merge by territories
114
+ grouped_territories_supplies = {}
115
+ grouped_supplies.each do |ksup, supply|
116
+ fsupply = supply.first
117
+ pr_key = "#{fsupply[:available]}_#{fsupply[:including_tax]}_#{fsupply[:currency]}"
118
+ supply.each do |s|
119
+ pr_key += "_#{s[:price]}_#{s[:from_date]}_#{s[:until_date]}"
120
+ end
121
+ grouped_territories_supplies[pr_key] ||= []
122
+ grouped_territories_supplies[pr_key] << supply
123
+ end
124
+
125
+ supplies = []
126
+
127
+ grouped_territories_supplies.each do |ksup, supply|
128
+ fsupply = supply.first.first
129
+ supplies << {:including_tax => fsupply[:including_tax], :currency => fsupply[:currency],
130
+ :territory => supply.map { |fs| fs.map { |s| s[:territory] } }.flatten.uniq,
131
+ :available => fsupply[:available],
132
+ :availability_date => fsupply[:availability_date],
133
+ :suppliers => fsupply[:suppliers],
134
+ :prices => supply.first.map { |s|
135
+
136
+ s[:amount] = s[:price]
137
+ s.delete(:price)
138
+ s.delete(:available)
139
+ s.delete(:currency)
140
+ s.delete(:availability_date)
141
+ s.delete(:including_tax)
142
+ s.delete(:territory)
143
+ s
144
+ }}
145
+ end
146
+
147
+ supplies
148
+ end
149
+
150
+ # add missing periods when they can be guessed
151
+ def prices_with_periods(supplies, global_supply)
152
+ complete_supplies = supplies.select { |supply| supply[:from_date] && supply[:until_date] }.sort_by { |supply| supply[:from_date] }
153
+ missing_start_period_supplies = supplies.select { |supply| supply[:from_date] && !supply[:until_date] }.sort_by { |supply| supply[:from_date] }
154
+ missing_end_period_supplies = supplies.select { |supply| !supply[:from_date] && supply[:until_date] }.sort_by { |supply| supply[:until_date] }
155
+
156
+ return [global_supply] if [complete_supplies, missing_start_period_supplies, missing_end_period_supplies].all? { |supply| supply.empty? }
157
+
158
+ return self.add_missing_periods(complete_supplies, global_supply) unless complete_supplies.empty?
159
+
160
+ without_start = missing_start_period_supplies.length == 1 && complete_supplies.empty? && missing_end_period_supplies.empty?
161
+ without_end = missing_end_period_supplies.length == 1 && complete_supplies.empty? && missing_start_period_supplies.empty?
162
+
163
+ return self.add_starting_period(missing_start_period_supplies.first, global_supply) if without_start
164
+ return self.add_ending_period(missing_end_period_supplies.first, global_supply) if without_end
165
+
166
+ [global_supply]
167
+ end
168
+
169
+ def add_missing_periods(supplies, global_supply)
170
+ new_supplies = []
171
+
172
+ supplies.each.with_index do |supply, index|
173
+ new_supplies << global_supply.dup.tap { |start_sup| start_sup[:until_date] = supply[:from_date] - 1 } if index == 0
174
+
175
+ if index > 0 && index != supplies.length
176
+ new_supplies << global_supply.dup.tap do |missing_supply|
177
+ missing_supply[:from_date] = supplies[index - 1][:until_date] + 1
178
+ missing_supply[:until_date] = supply[:from_date] - 1
179
+ end
180
+ end
181
+
182
+ new_supplies << supply
183
+
184
+ new_supplies << global_supply.dup.tap { |end_sup| end_sup[:from_date] = supply[:until_date] + 1 } if index == supplies.length - 1
185
+ end
186
+
187
+ new_supplies
188
+ end
189
+
190
+ def add_starting_period(supply, global_supply)
191
+ missing_supply = global_supply.dup
192
+ missing_supply[:until_date] = supply[:from_date] - 1
193
+
194
+ [missing_supply, supply]
195
+ end
196
+
197
+ def add_ending_period(supply, global_supply)
198
+ missing_supply = global_supply.dup
199
+ missing_supply[:from_date] = supply[:until_date] + 1
200
+
201
+ [supply, missing_supply]
202
+ end
203
+
204
+ # flattened supplies only including taxes
205
+ def supplies_including_tax
206
+ self.supplies.select { |p| p[:including_tax] }
207
+ end
208
+
209
+ # flattened supplies only excluding taxes
210
+ def supplies_excluding_tax
211
+ self.supplies.select { |p| not p[:including_tax] }
212
+ end
213
+
214
+ # flattened supplies with default tax (excluding tax for US and CA, including otherwise)
215
+ def supplies_with_default_tax
216
+ self.supplies_including_tax + self.supplies_excluding_tax.select { |s| ["CAD", "USD"].include?(s[:currency]) }
217
+ end
218
+
219
+ # flattened supplies for country
220
+ def supplies_for_country(country, currency = nil)
221
+ country_supplies = self.supplies
222
+ if currency
223
+ country_supplies = country_supplies.select { |s| s[:currency] == currency }
224
+ end
225
+ country_supplies.select { |s|
226
+ if s[:territory].include?(country)
227
+ true
228
+ else
229
+ false
230
+ end
231
+ }
232
+ end
233
+
234
+ # price amount for given +currency+ and country at time
235
+ def at_time_price_amount_for(time, currency, country = nil)
236
+ sups = self.supplies_with_default_tax.select { |p| p[:currency] == currency }
237
+ if country
238
+ sups = sups.select { |p| p[:territory].include?(country) }
239
+ end
240
+ if sups.length > 0
241
+ # exclusive
242
+ sup = sups.first[:prices].select { |p|
243
+ (!p[:from_date] or p[:from_date].to_date <= time.to_date) and
244
+ (!p[:until_date] or p[:until_date].to_date > time.to_date)
245
+ }.first
246
+
247
+ if sup
248
+ sup[:amount]
249
+ else
250
+ # or inclusive
251
+ sup = sups.first[:prices].select { |p|
252
+ (!p[:from_date] or p[:from_date].to_date <= time.to_date) and
253
+ (!p[:until_date] or p[:until_date].to_date >= time.to_date)
254
+ }.first
255
+
256
+ if sup
257
+ sup[:amount]
258
+ else
259
+ nil
260
+ end
261
+ end
262
+
263
+ else
264
+ nil
265
+ end
266
+ end
267
+
268
+ # current price amount for given +currency+ and country
269
+ def current_price_amount_for(currency, country = nil)
270
+ at_time_price_amount_for(Time.now, currency, country)
271
+ end
272
+
273
+ # @!endgroup
274
+ end
275
+ end
@@ -1,85 +1,44 @@
1
- require 'onix/price'
2
1
  require 'onix/date'
2
+ require 'onix/market'
3
+ require 'onix/market_publishing_detail'
4
+ require 'onix/supply_detail'
3
5
 
4
6
  module ONIX
5
-
6
- class Market < SubsetDSL
7
- element "Territory", :subset
8
- end
9
-
10
- class MarketPublishingDetail < SubsetDSL
11
- elements "PublisherRepresentative", :subset, {:klass=>"Agent"}
12
- element "MarketPublishingStatus", :subset
13
- elements "MarketDate", :subset
14
-
15
- def availability_date
16
- av=@market_dates.availability.first
17
- if av
18
- av.date
19
- else
20
- nil
21
- end
22
- end
23
- end
24
-
25
- class SupplyDetail < SubsetDSL
26
- elements "Supplier", :subset
27
- element "ProductAvailability", :subset
28
- elements "SupplyDate", :subset
29
- elements "Price", :subset
30
- element "UnpricedItemType", :subset
31
-
32
- def availability
33
- @product_availability
34
- end
35
-
36
- def distributors
37
- @suppliers.select{|s| s.role.human=~/Distributor/}.uniq
38
- end
39
-
40
- def available?
41
- ["Available","NotYetAvailable","InStock","ToOrder","Pod"].include?(@product_availability.human)
42
- end
43
-
44
- def sold_separately?
45
- @product_availability.human!="NotSoldSeparately"
46
- end
47
-
48
- def availability_date
49
- av=@supply_dates.availability.first
50
- if av
51
- av.date
52
- else
53
- nil
54
- end
55
- end
56
- end
57
-
58
7
  class ProductSupply < SubsetDSL
59
8
  elements "Market", :subset
60
9
  element "MarketPublishingDetail", :subset
61
10
  elements "SupplyDetail", :subset
62
11
 
12
+ # availability date from market
13
+ # @return [Date]
63
14
  def availability_date
64
15
  if @market_publishing_detail
65
16
  @market_publishing_detail.availability_date
66
17
  end
67
18
  end
68
19
 
20
+ # countries string array
21
+ # @return [Array<String>]
69
22
  def countries
70
- @markets.map{|m| m.territory.countries}.flatten.uniq
23
+ @markets.map { |market| market.territory.countries }.flatten.uniq
71
24
  end
72
25
 
26
+ # distributors string array
27
+ # @return [Array<String>]
73
28
  def distributors
74
- @supply_details.map{|sd| sd.distributors}.flatten.uniq{|d| d.name}
29
+ @supply_details.map { |supply_detail| supply_detail.distributors }.flatten.uniq { |distributor| distributor.name }
75
30
  end
76
31
 
32
+ # available supply details
33
+ # @return [Array<SupplyDetail>]
77
34
  def available_supply_details
78
- @supply_details.select{|sd| sd.available?}
35
+ @supply_details.select { |supply_detail| supply_detail.available? }
79
36
  end
80
37
 
38
+ # unavailable supply details
39
+ # @return [Array<SupplyDetail>]
81
40
  def unavailable_supply_details
82
- @supply_details.delete_if{|sd| sd.available?}
41
+ @supply_details.delete_if { |supply_detail| supply_detail.available? }
83
42
  end
84
43
 
85
44
  def available?