bipm-data-importer 0.2.2 → 0.4.0

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.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/README.adoc +68 -9
  3. data/TODO.refactor/01-remove-debug-code.md +18 -0
  4. data/TODO.refactor/02-fix-gemspec.md +21 -0
  5. data/TODO.refactor/03-update-readme.md +28 -0
  6. data/TODO.refactor/04-replace-require-relative-with-autoload.md +34 -0
  7. data/TODO.refactor/05-fix-outcomes-bugs.md +22 -0
  8. data/TODO.refactor/06-unify-clause-taxonomy.md +31 -0
  9. data/TODO.refactor/07-extract-french-parsing.md +21 -0
  10. data/TODO.refactor/08-extract-data-quirks.md +25 -0
  11. data/TODO.refactor/09-introduce-body-registry.md +23 -0
  12. data/TODO.refactor/10-introduce-strategy-pattern.md +34 -0
  13. data/TODO.refactor/11-extract-cli-class.md +20 -0
  14. data/TODO.refactor/12-specs-common.md +23 -0
  15. data/TODO.refactor/13-specs-cgpm.md +22 -0
  16. data/TODO.refactor/14-specs-asciimath.md +23 -0
  17. data/TODO.refactor/15-specs-outcomes.md +24 -0
  18. data/TODO.refactor/16-quarantine-failing-specs.md +23 -0
  19. data/TODO.refactor/17-typed-domain-models.md +32 -0
  20. data/TODO.refactor/README.md +62 -0
  21. data/bipm-data-importer.gemspec +11 -7
  22. data/exe/bipm-fetch +2 -440
  23. data/lib/bipm/data/importer/bodies.rb +45 -0
  24. data/lib/bipm/data/importer/body.rb +43 -0
  25. data/lib/bipm/data/importer/cgpm.rb +21 -0
  26. data/lib/bipm/data/importer/clauses.rb +239 -0
  27. data/lib/bipm/data/importer/cli.rb +83 -0
  28. data/lib/bipm/data/importer/common.rb +31 -120
  29. data/lib/bipm/data/importer/fetcher.rb +62 -0
  30. data/lib/bipm/data/importer/language.rb +66 -0
  31. data/lib/bipm/data/importer/quirks.rb +73 -0
  32. data/lib/bipm/data/importer/strategies/base.rb +40 -0
  33. data/lib/bipm/data/importer/strategies/spa_meetings.rb +340 -0
  34. data/lib/bipm/data/importer/strategies/static_index.rb +324 -0
  35. data/lib/bipm/data/importer/strategies.rb +13 -0
  36. data/lib/bipm/data/importer/text/french.rb +49 -0
  37. data/lib/bipm/data/importer/text.rb +11 -0
  38. data/lib/bipm/data/importer/version.rb +1 -1
  39. data/lib/bipm/data/importer.rb +31 -4
  40. data/lib/bipm/data/outcomes/action.rb +1 -1
  41. data/lib/bipm/data/outcomes/approval.rb +1 -1
  42. data/lib/bipm/data/outcomes/body.rb +1 -1
  43. data/lib/bipm/data/outcomes/consideration.rb +1 -1
  44. data/lib/bipm/data/outcomes/localized_body.rb +1 -1
  45. data/lib/bipm/data/outcomes/meeting.rb +2 -2
  46. data/lib/bipm/data/outcomes/resolution.rb +1 -1
  47. data/lib/bipm/data/outcomes.rb +9 -13
  48. data/lib/bipm-data-importer.rb +1 -1
  49. metadata +46 -19
  50. data/.hound.yml +0 -5
  51. data/.rspec +0 -3
  52. data/.rubocop.yml +0 -10
  53. data/exe/bipm-fetch-cgpm +0 -3
@@ -0,0 +1,324 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mechanize"
4
+ require "nokogiri"
5
+ require "yaml"
6
+ require "date"
7
+ require "fileutils"
8
+
9
+ module Bipm
10
+ module Data
11
+ module Importer
12
+ module Strategies
13
+ # Scrapes a body whose data is reachable through a static HTML
14
+ # listing page (one URL per language) plus per-resolution pages
15
+ # that the listing links to. The BIPM SPA rewrite preserved this
16
+ # shape for CGPM; other bodies moved to SPA-only rendering and
17
+ # are handled by SpaMeetings instead.
18
+ class StaticIndex < Base
19
+ def self.recording_mode
20
+ :live
21
+ end
22
+
23
+ def call(agent:)
24
+ listings = fetch_listings(agent)
25
+ urls_by_lang = build_urls_by_language(listings)
26
+ log_url_summary(urls_by_lang)
27
+
28
+ meetings = {}
29
+ scrape_resolutions(agent, urls_by_lang, meetings)
30
+ enrich_meetings(agent, meetings)
31
+ write_files(meetings)
32
+ end
33
+
34
+ # ---------- URL handling ----------
35
+
36
+ def normalize_res_url(url)
37
+ url.to_s.sub(%r{/(en|fr)/committees/}, "/committees/")
38
+ end
39
+
40
+ def extract_resolution_urls(listing_html, language)
41
+ doc = Nokogiri::HTML(listing_html)
42
+ doc.css("a").map do |link|
43
+ href = link["href"].to_s
44
+ next nil unless href =~ %r{/committees/cg/cgpm/\d+-\d+/resolution-\d+\z}
45
+ path = href.sub(%r{^https?://[^/]+}, "").sub(%r{^/(?:en|fr)?/?(?=committees)}, "")
46
+ "#{Body::BIPM_ORIGIN}/#{language}/#{path}"
47
+ end.compact.uniq
48
+ end
49
+
50
+ # ---------- Date parsing ----------
51
+
52
+ def parse_date_text(text)
53
+ Text::French.parse_date(text)&.iso8601
54
+ end
55
+
56
+ # ---------- Clause handling ----------
57
+
58
+ def flush_clause(verb, message_parts, list_items, considerations, actions, date_str)
59
+ full_message = message_parts.concat(list_items).reject(&:empty?).join("\n").gsub(/\s+/, " ").strip
60
+ return if full_message.empty?
61
+
62
+ type, category = Clauses.lookup_verb(verb)
63
+ clause = { "type" => type, "date_effective" => date_str, "message" => full_message }
64
+
65
+ if category == Clauses::CATEGORY_CONSIDERATION
66
+ considerations << clause
67
+ else
68
+ actions << clause
69
+ end
70
+ end
71
+
72
+ # ---------- Page parsing ----------
73
+
74
+ def parse_resolution(page, _language)
75
+ return nil unless page
76
+ doc = Nokogiri::HTML(page.body)
77
+ article = doc.at_css("div.journal-content-article") ||
78
+ doc.at_css("div.asset-entry") ||
79
+ doc.css("div").find { |d| d.inner_html.include?("Resolution") && d.inner_html.size > 1000 }
80
+ return nil unless article
81
+
82
+ h1 = article.at_css("h1")
83
+ h1_text = h1&.text&.strip&.gsub(/\s+/, " ")
84
+ h2s = article.css("h2").map { |h| h.text.strip }
85
+ code_h2 = h2s.find { |t| t =~ /^Resolution-CGPM-\d+-\d+\z/i }
86
+ subject_h2 = h2s.find { |t| t != code_h2 && t !~ /^(CGPM logo|Menu Display)$/i }
87
+
88
+ res_id = nil
89
+ meeting_id = nil
90
+ year = nil
91
+ if h1_text
92
+ en_m = h1_text.match(/(?:Resolution|Declaration|Recommendation)\s+(\d+)\s+of\s+the\s+(\d+)(?:st|nd|rd|th)?\s+CGPM(?:\s*\((\d{4})\))?/i)
93
+ fr_m = h1_text.match(/(?:R[ée]solution|D[ée]claration|Recommandation)\s+(\d+)\s+de la\s+(\d+)[eè]?(?:\s+CGPM)(?:\s*\((\d{4})\))?/i)
94
+ if (m = en_m || fr_m)
95
+ res_id = m[1].to_i
96
+ meeting_id = m[2].to_i
97
+ year = m[3]
98
+ end
99
+ end
100
+
101
+ date_str = nil
102
+ if (date_node = doc.at_css("p.session__date"))
103
+ date_str = parse_date_text(date_node.text)
104
+ end
105
+ date_str ||= year ? "#{year}-01-01" : nil
106
+
107
+ reference_url = nil
108
+ reference_name = nil
109
+ reference_page = nil
110
+ article.css("a").each do |link|
111
+ text = link.text.strip
112
+ href = link["href"].to_s
113
+ next unless href.include?("/documents/")
114
+ next unless text =~ /Proceedings of the\s+(\d+)(?:st|nd|rd|th)?\s+CGPM/i
115
+ reference_name = text
116
+ reference_url = href
117
+ reference_page = Regexp.last_match(1).to_i if text =~ /p(\d+)\s*\z/
118
+ break
119
+ end
120
+
121
+ considerations = []
122
+ actions = []
123
+ approvals = []
124
+
125
+ preamble = article.css("p").map(&:text).find { |t| t =~ /General Conference|CGPM.*meeting/i }
126
+
127
+ current_verb = nil
128
+ current_message_parts = []
129
+ current_list_items = []
130
+
131
+ article.children.each do |node|
132
+ next unless node.element? || node.text?
133
+ next if node.name == "script" || node.name == "style"
134
+
135
+ if node.name == "p"
136
+ b = node.at_css("b")
137
+ if b
138
+ verb_text = Clauses.normalize_verb(b.text)
139
+ rest = node.dup
140
+ rest.at_css("b").remove
141
+ rest_text = rest.text.strip.gsub(/\s+/, " ")
142
+ if current_verb
143
+ flush_clause(current_verb, current_message_parts, current_list_items, considerations, actions, date_str)
144
+ current_message_parts = []
145
+ current_list_items = []
146
+ end
147
+ current_verb = verb_text
148
+ current_message_parts << rest_text if rest_text && !rest_text.empty?
149
+ elsif current_verb && (text = node.text.strip) && !text.empty?
150
+ current_message_parts << text
151
+ end
152
+ elsif node.name == "ul" && current_verb
153
+ node.css("li").each do |li|
154
+ current_list_items << li.text.strip.gsub(/\s+/, " ")
155
+ end
156
+ end
157
+ end
158
+ flush_clause(current_verb, current_message_parts, current_list_items, considerations, actions, date_str) if current_verb
159
+
160
+ if preamble
161
+ approvals << { "message" => preamble.strip, "date_effective" => date_str }
162
+ end
163
+
164
+ {
165
+ "metadata" => {
166
+ "title" => h1_text || "CGPM Resolution",
167
+ "identifier" => res_id&.to_s,
168
+ "date" => date_str,
169
+ "source" => "BIPM - Pavillon de Breteuil",
170
+ "url" => page.uri.to_s,
171
+ },
172
+ "resolutions" => [{
173
+ "dates" => [date_str].compact,
174
+ "subject" => "CGPM",
175
+ "type" => "resolution",
176
+ "title" => subject_h2 || h1_text,
177
+ "identifier" => res_id&.to_s,
178
+ "url" => page.uri.to_s,
179
+ "reference" => reference_url,
180
+ "reference_name" => reference_name,
181
+ "reference_page" => reference_page,
182
+ "approvals" => approvals,
183
+ "considerations" => considerations,
184
+ "actions" => actions,
185
+ }],
186
+ "_meeting_id" => meeting_id&.to_s,
187
+ "_year" => year,
188
+ }
189
+ rescue => e
190
+ warn "[static_index] parse_resolution failed for #{page&.uri}: #{e.message}"
191
+ nil
192
+ end
193
+
194
+ # ---------- Orchestration ----------
195
+
196
+ def fetch_listings(agent)
197
+ listings = {}
198
+ languages.each do |lang|
199
+ url = body.url(lang)
200
+ listings[lang] = agent.get(url)
201
+ puts "[static_index] #{lang.to_s.upcase} listing: #{listings[lang].body.size} bytes"
202
+ rescue => e
203
+ warn "[static_index] FAILED to fetch #{lang.to_s.upcase} listing: #{e.message}"
204
+ listings[lang] = nil
205
+ end
206
+ listings
207
+ end
208
+
209
+ def build_urls_by_language(listings)
210
+ listings.each_with_object({}) do |(lang, page), h|
211
+ h[lang] = page ? extract_resolution_urls(page.body, lang) : []
212
+ end
213
+ end
214
+
215
+ def log_url_summary(urls_by_lang)
216
+ en_urls = urls_by_lang[Language::EN] || []
217
+ fr_urls = urls_by_lang[Language::FR] || []
218
+ en_by_norm = en_urls.each_with_object({}) { |u, h| h[normalize_res_url(u)] = u }
219
+ fr_by_norm = fr_urls.each_with_object({}) { |u, h| h[normalize_res_url(u)] = u }
220
+ all_norm = (en_by_norm.keys + fr_by_norm.keys).uniq.sort
221
+ puts "[static_index] EN: #{en_urls.size}, FR: #{fr_urls.size}, paired: #{all_norm.size}"
222
+ { en_by_norm: en_by_norm, fr_by_norm: fr_by_norm, all_norm: all_norm }
223
+ end
224
+
225
+ def scrape_resolutions(agent, urls_by_lang, meetings)
226
+ en_urls = urls_by_lang[Language::EN] || []
227
+ fr_urls = urls_by_lang[Language::FR] || []
228
+ en_by_norm = en_urls.each_with_object({}) { |u, h| h[normalize_res_url(u)] = u }
229
+ fr_by_norm = fr_urls.each_with_object({}) { |u, h| h[normalize_res_url(u)] = u }
230
+ all_norm = (en_by_norm.keys + fr_by_norm.keys).uniq.sort
231
+
232
+ all_norm.each_with_index do |norm_url, idx|
233
+ parsed = {}
234
+ [en_by_norm[norm_url], fr_by_norm[norm_url]].compact.each do |url|
235
+ lang = url[%r{/(fr)/committees/}, 1] || "en"
236
+ page = agent.get(url)
237
+ sleep 0.5
238
+ parsed[lang] = parse_resolution(page, lang)
239
+ next unless parsed[lang]
240
+ mid = parsed[lang].delete("_meeting_id")
241
+ year = parsed[lang].delete("_year")
242
+ next unless mid
243
+ meetings[mid] ||= { "year" => year, "resolutions" => {}, "meta" => {} }
244
+ meetings[mid]["year"] ||= year
245
+ meetings[mid]["meta"][lang] = parsed[lang]["metadata"]
246
+ parsed[lang]["resolutions"]&.each do |res|
247
+ meetings[mid]["resolutions"][lang] ||= {}
248
+ meetings[mid]["resolutions"][lang][res["identifier"].to_s] = res
249
+ end
250
+ rescue => e
251
+ warn "[static_index] fetch #{lang} #{url} failed: #{e.class}: #{e.message}"
252
+ end
253
+ puts "[static_index] (#{idx + 1}/#{all_norm.size}) #{norm_url} -> EN=#{!!parsed["en"]} FR=#{!!parsed["fr"]}"
254
+ end
255
+ end
256
+
257
+ def enrich_meetings(agent, meetings)
258
+ puts "[static_index] Fetching #{meetings.size} meeting session pages..."
259
+ meetings.each_value do |data|
260
+ data["resolutions"].each do |lang, res_map|
261
+ next if res_map.empty?
262
+ sample_res = res_map.values.first
263
+ next unless sample_res && sample_res["url"]
264
+ meeting_url = sample_res["url"].sub(%r{/resolution-\d+\z}, "")
265
+ page = agent.get(meeting_url)
266
+ sleep 0.3
267
+ doc = Nokogiri::HTML(page.body)
268
+ if (title_node = doc.at_css("h1.session__title, .journal-content-article h1"))
269
+ data["meta"][lang] ||= {}
270
+ data["meta"][lang]["title"] = title_node.text.strip.gsub(/\s+/, " ")
271
+ end
272
+ if (date_node = doc.at_css("p.session__date")) && (parsed_date = parse_date_text(date_node.text))
273
+ data["meta"][lang] ||= {}
274
+ data["meta"][lang]["date"] = parsed_date
275
+ res_map.each_value do |res|
276
+ res["dates"] = [parsed_date]
277
+ res["considerations"]&.each { |c| c["date_effective"] = parsed_date }
278
+ res["actions"]&.each { |x| x["date_effective"] = parsed_date }
279
+ res["approvals"]&.each { |x| x["date_effective"] = parsed_date }
280
+ end
281
+ end
282
+ rescue => e
283
+ warn "[static_index] meeting page fetch #{lang} #{meeting_url} failed: #{e.message}"
284
+ end
285
+ end
286
+ end
287
+
288
+ def write_files(meetings)
289
+ dir = "#{base_dir}/#{body.id}"
290
+ FileUtils.mkdir_p("#{dir}/meetings-en")
291
+ FileUtils.mkdir_p("#{dir}/meetings-fr")
292
+
293
+ written = { Language::EN => 0, Language::FR => 0 }
294
+ meetings.sort.each do |mid, data|
295
+ data["resolutions"].each do |lang, res_map|
296
+ lang_obj = Language.find(lang)
297
+ res_map = res_map.reject { |_, r| r.nil? }
298
+ next if res_map.empty?
299
+ res_list = res_map.sort_by { |rid, _| rid.to_i }.map { |_, r| r }
300
+ first = res_list.first
301
+ next unless first
302
+
303
+ meeting_meta = (data["meta"][lang] || {}).dup
304
+ meeting_meta["title"] ||= "CGPM meeting #{mid}"
305
+ meeting_meta["date"] ||= data["year"] ? "#{data["year"]}-01-01" : nil
306
+ meeting_meta["identifier"] = mid.to_s
307
+ meeting_meta["url"] = (first["url"] || meeting_meta["url"])&.sub(%r{/resolution-\d+\z}, "")
308
+ meeting_meta["source"] ||= "BIPM - Pavillon de Breteuil"
309
+
310
+ payload = { "metadata" => meeting_meta, "resolutions" => res_list }
311
+ path = "#{dir}/meetings-#{lang_obj}/meeting-#{format('%02d', mid.to_i)}.yml"
312
+ File.write(path, YAML.dump(payload))
313
+ written[lang_obj] += 1
314
+ end
315
+ end
316
+
317
+ total_en = meetings.values.sum { |m| (m["resolutions"]["en"] || {}).size }
318
+ puts "[static_index] Wrote #{written[Language::EN]} EN + #{written[Language::FR]} FR meeting files across #{meetings.size} CGPM sessions (#{total_en} EN resolutions)."
319
+ end
320
+ end
321
+ end
322
+ end
323
+ end
324
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bipm
4
+ module Data
5
+ module Importer
6
+ module Strategies
7
+ autoload :Base, "bipm/data/importer/strategies/base"
8
+ autoload :StaticIndex, "bipm/data/importer/strategies/static_index"
9
+ autoload :SpaMeetings, "bipm/data/importer/strategies/spa_meetings"
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+
5
+ module Bipm
6
+ module Data
7
+ module Importer
8
+ module Text
9
+ # French-language text utilities shared by every scraping strategy.
10
+ # Single source of truth for month-name translation and date parsing
11
+ # (previously duplicated between Common.extract_date and CGPM.parse_date_text).
12
+ module French
13
+ MONTH_TRANSLATIONS = {
14
+ /janvier/i => "january",
15
+ /f[eé]vrier/i => "february",
16
+ /mars/i => "march",
17
+ /avril/i => "april",
18
+ /mai/i => "may",
19
+ /juin/i => "june",
20
+ /juillet/i => "july",
21
+ /ao[uû]t/i => "august",
22
+ /septembre/i => "september",
23
+ /octobre/i => "october",
24
+ /novembre/i => "november",
25
+ /d[ée]cembre/i => "december",
26
+ }.freeze
27
+
28
+ RANGE_SEPARATORS = /, | to | au | – | — /.freeze
29
+
30
+ def self.translate_months(str)
31
+ MONTH_TRANSLATIONS.reduce(str) { |s, (fr, en)| s.gsub(fr, en) }
32
+ end
33
+
34
+ def self.parse_date(str)
35
+ return nil if str.nil? || str.to_s.strip.empty?
36
+
37
+ translated = translate_months(str.strip.gsub(/\s+/, " "))
38
+ last = translated.split(RANGE_SEPARATORS).map(&:strip).reject(&:empty?).last
39
+ return nil unless last
40
+
41
+ Date.parse(last)
42
+ rescue Date::Error
43
+ nil
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bipm
4
+ module Data
5
+ module Importer
6
+ module Text
7
+ autoload :French, "bipm/data/importer/text/french"
8
+ end
9
+ end
10
+ end
11
+ end
@@ -3,7 +3,7 @@
3
3
  module Bipm
4
4
  module Data
5
5
  module Importer
6
- VERSION = "0.2.2"
6
+ VERSION = "0.4.0"
7
7
  end
8
8
  end
9
9
  end
@@ -1,15 +1,42 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "importer/asciimath"
4
- require_relative "importer/common"
5
- require_relative "importer/version"
6
- require "date"
3
+ # NOTE: This is the one internal require that does not use autoload.
4
+ # `autoload` only works for module/class constants — it cannot lazily
5
+ # resolve `Bipm::Data::Importer::VERSION` (a String) without eagerly
6
+ # defining it. version.rb is a minimal, dependency-free file that the
7
+ # gemspec also loads at gem-build time, before Bundler has installed
8
+ # mechanize/nokogiri/etc.
9
+ require "bipm/data/importer/version"
7
10
 
8
11
  module Bipm
9
12
  module Data
10
13
  module Importer
11
14
  class Error < StandardError; end
12
15
 
16
+ autoload :AsciiMath, "bipm/data/importer/asciimath"
17
+ autoload :Body, "bipm/data/importer/body"
18
+ autoload :Bodies, "bipm/data/importer/bodies"
19
+ autoload :CGPM, "bipm/data/importer/cgpm"
20
+ autoload :Clauses, "bipm/data/importer/clauses"
21
+ autoload :CLI, "bipm/data/importer/cli"
22
+ autoload :Common, "bipm/data/importer/common"
23
+ autoload :Fetcher, "bipm/data/importer/fetcher"
24
+ autoload :Language, "bipm/data/importer/language"
25
+ autoload :Quirks, "bipm/data/importer/quirks"
26
+ autoload :Strategies, "bipm/data/importer/strategies"
27
+ autoload :Text, "bipm/data/importer/text"
28
+
29
+ # Build a Fetcher for one body. Does not start any work — call .call on
30
+ # the returned object, optionally passing an agent (defaults to a fresh
31
+ # Mechanize instance).
32
+ def self.fetch(body_id, languages: Language.all, base_dir: "data")
33
+ Fetcher.new(body: body_id, languages: languages, base_dir: base_dir)
34
+ end
35
+
36
+ # Build a Fetcher for every known body.
37
+ def self.fetch_all(languages: Language.all, base_dir: "data")
38
+ Bodies.all.map { |b| Fetcher.new(body: b, languages: languages, base_dir: base_dir) }
39
+ end
13
40
  end
14
41
  end
15
42
  end
@@ -12,7 +12,7 @@ module Bipm
12
12
  end
13
13
 
14
14
  def document
15
- @resolution.document.dig("actions", index)
15
+ @document ||= @resolution.document.dig("actions", index)
16
16
  end
17
17
 
18
18
  attr_reader :resolution, :index
@@ -10,7 +10,7 @@ module Bipm
10
10
  end
11
11
 
12
12
  def document
13
- @resolution.document.dig("approvals", index)
13
+ @document ||= @resolution.document.dig("approvals", index)
14
14
  end
15
15
 
16
16
  attr_reader :resolution, :index
@@ -2,7 +2,7 @@ module Bipm
2
2
  module Data
3
3
  module Outcomes
4
4
  class Body
5
- def initialize(body, locale = nil)
5
+ def initialize(body)
6
6
  @body = body
7
7
  end
8
8
 
@@ -12,7 +12,7 @@ module Bipm
12
12
  end
13
13
 
14
14
  def document
15
- @resolution.document.dig("considerations", index)
15
+ @document ||= @resolution.document.dig("considerations", index)
16
16
  end
17
17
 
18
18
  attr_reader :resolution, :index
@@ -3,7 +3,7 @@ module Bipm
3
3
  module Outcomes
4
4
  class LocalizedBody < Body
5
5
  def initialize(body, locale)
6
- super
6
+ super(body)
7
7
  @locale = locale
8
8
  end
9
9
 
@@ -19,7 +19,7 @@ module Bipm
19
19
  end
20
20
 
21
21
  def document
22
- YAML.load(File.open(file_path))
22
+ @document ||= YAML.load(File.open(file_path))
23
23
  end
24
24
 
25
25
 
@@ -36,7 +36,7 @@ module Bipm
36
36
  end
37
37
 
38
38
  def url
39
- document["metadata"]["source"]
39
+ document["metadata"]["url"]
40
40
  end
41
41
 
42
42
  def pdf
@@ -12,7 +12,7 @@ module Bipm
12
12
  end
13
13
 
14
14
  def document
15
- @meeting.document["resolutions"][index]
15
+ @document ||= @meeting.document["resolutions"][index]
16
16
  end
17
17
 
18
18
  attr_reader :meeting, :index
@@ -1,10 +1,16 @@
1
- require_relative "outcomes/body"
2
- require_relative "outcomes/localized_body"
3
- require_relative "outcomes/meeting"
1
+ # frozen_string_literal: true
4
2
 
5
3
  module Bipm
6
4
  module Data
7
5
  module Outcomes
6
+ autoload :Body, "bipm/data/outcomes/body"
7
+ autoload :LocalizedBody, "bipm/data/outcomes/localized_body"
8
+ autoload :Meeting, "bipm/data/outcomes/meeting"
9
+ autoload :Resolution, "bipm/data/outcomes/resolution"
10
+ autoload :Approval, "bipm/data/outcomes/approval"
11
+ autoload :Action, "bipm/data/outcomes/action"
12
+ autoload :Consideration, "bipm/data/outcomes/consideration"
13
+
8
14
  def self.file_path
9
15
  @file_path ||= "#{__dir__}/../../../data/"
10
16
  end
@@ -30,16 +36,6 @@ module Bipm
30
36
  end
31
37
  singleton_class.include Enumerable
32
38
 
33
-
34
- autoload :Body, "bipm/data/outcomes/body"
35
- autoload :LocalizedBody, "bipm/data/outcomes/localized_body"
36
- autoload :Meeting, "bipm/data/outcomes/meeting"
37
- autoload :Resolution, "bipm/data/outcomes/resolution"
38
- autoload :Approval, "bipm/data/outcomes/approval"
39
- autoload :Consideration, "bipm/data/outcomes/consideration"
40
- autoload :Action, "bipm/data/outcomes/action"
41
-
42
- # It may be possible, that we don't have the BIPM data loaded.
43
39
  def self.ensure_downloaded
44
40
  if File.exist?(file_path)
45
41
  return
@@ -1,3 +1,3 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "bipm/data/importer"
3
+ require "bipm/data/importer"