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.
- checksums.yaml +4 -4
- data/README.adoc +68 -9
- data/TODO.refactor/01-remove-debug-code.md +18 -0
- data/TODO.refactor/02-fix-gemspec.md +21 -0
- data/TODO.refactor/03-update-readme.md +28 -0
- data/TODO.refactor/04-replace-require-relative-with-autoload.md +34 -0
- data/TODO.refactor/05-fix-outcomes-bugs.md +22 -0
- data/TODO.refactor/06-unify-clause-taxonomy.md +31 -0
- data/TODO.refactor/07-extract-french-parsing.md +21 -0
- data/TODO.refactor/08-extract-data-quirks.md +25 -0
- data/TODO.refactor/09-introduce-body-registry.md +23 -0
- data/TODO.refactor/10-introduce-strategy-pattern.md +34 -0
- data/TODO.refactor/11-extract-cli-class.md +20 -0
- data/TODO.refactor/12-specs-common.md +23 -0
- data/TODO.refactor/13-specs-cgpm.md +22 -0
- data/TODO.refactor/14-specs-asciimath.md +23 -0
- data/TODO.refactor/15-specs-outcomes.md +24 -0
- data/TODO.refactor/16-quarantine-failing-specs.md +23 -0
- data/TODO.refactor/17-typed-domain-models.md +32 -0
- data/TODO.refactor/README.md +62 -0
- data/bipm-data-importer.gemspec +11 -7
- data/exe/bipm-fetch +2 -440
- data/lib/bipm/data/importer/bodies.rb +45 -0
- data/lib/bipm/data/importer/body.rb +43 -0
- data/lib/bipm/data/importer/cgpm.rb +21 -0
- data/lib/bipm/data/importer/clauses.rb +239 -0
- data/lib/bipm/data/importer/cli.rb +83 -0
- data/lib/bipm/data/importer/common.rb +31 -120
- data/lib/bipm/data/importer/fetcher.rb +62 -0
- data/lib/bipm/data/importer/language.rb +66 -0
- data/lib/bipm/data/importer/quirks.rb +73 -0
- data/lib/bipm/data/importer/strategies/base.rb +40 -0
- data/lib/bipm/data/importer/strategies/spa_meetings.rb +340 -0
- data/lib/bipm/data/importer/strategies/static_index.rb +324 -0
- data/lib/bipm/data/importer/strategies.rb +13 -0
- data/lib/bipm/data/importer/text/french.rb +49 -0
- data/lib/bipm/data/importer/text.rb +11 -0
- data/lib/bipm/data/importer/version.rb +1 -1
- data/lib/bipm/data/importer.rb +31 -4
- data/lib/bipm/data/outcomes/action.rb +1 -1
- data/lib/bipm/data/outcomes/approval.rb +1 -1
- data/lib/bipm/data/outcomes/body.rb +1 -1
- data/lib/bipm/data/outcomes/consideration.rb +1 -1
- data/lib/bipm/data/outcomes/localized_body.rb +1 -1
- data/lib/bipm/data/outcomes/meeting.rb +2 -2
- data/lib/bipm/data/outcomes/resolution.rb +1 -1
- data/lib/bipm/data/outcomes.rb +9 -13
- data/lib/bipm-data-importer.rb +1 -1
- metadata +46 -19
- data/.hound.yml +0 -5
- data/.rspec +0 -3
- data/.rubocop.yml +0 -10
- data/exe/bipm-fetch-cgpm +0 -3
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bipm
|
|
4
|
+
module Data
|
|
5
|
+
module Importer
|
|
6
|
+
# First-class language value object. English and French are equal
|
|
7
|
+
# citizens — neither is the canonical form, neither is a translation
|
|
8
|
+
# of the other. Both directions of any linguistic operation must be
|
|
9
|
+
# expressible through this type without one being the "default".
|
|
10
|
+
class Language
|
|
11
|
+
attr_reader :code
|
|
12
|
+
|
|
13
|
+
def initialize(code)
|
|
14
|
+
@code = code
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
EN = new(:en)
|
|
18
|
+
FR = new(:fr)
|
|
19
|
+
ALL = [EN, FR].freeze
|
|
20
|
+
|
|
21
|
+
def self.find(name)
|
|
22
|
+
sym = name.to_sym
|
|
23
|
+
ALL.find { |l| l.code == sym } ||
|
|
24
|
+
raise(ArgumentError, "unknown language: #{name.inspect} (supported: #{ALL.map(&:to_s).inspect})")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.all
|
|
28
|
+
ALL
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def to_s
|
|
32
|
+
@code.to_s
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def to_sym
|
|
36
|
+
@code
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def suffix
|
|
40
|
+
case @code
|
|
41
|
+
when :en then ""
|
|
42
|
+
when :fr then "-fr"
|
|
43
|
+
else raise(ArgumentError, "no suffix defined for #{@code}")
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def directory_suffix
|
|
48
|
+
case @code
|
|
49
|
+
when :en then "-en"
|
|
50
|
+
when :fr then "-fr"
|
|
51
|
+
else raise(ArgumentError, "no directory suffix for #{@code}")
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def eql?(other)
|
|
56
|
+
other.is_a?(Language) && @code == other.code
|
|
57
|
+
end
|
|
58
|
+
alias == eql?
|
|
59
|
+
|
|
60
|
+
def hash
|
|
61
|
+
@code.hash
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bipm
|
|
4
|
+
module Data
|
|
5
|
+
module Importer
|
|
6
|
+
# Registry of upstream-data patches. Each quirk documents a specific
|
|
7
|
+
# mistake on bipm.org (or in a cassette) and the transformation that
|
|
8
|
+
# corrects it. Adding a new quirk = adding a method here, not editing
|
|
9
|
+
# the scrape loop.
|
|
10
|
+
#
|
|
11
|
+
# Categories:
|
|
12
|
+
# - HREF_TRANSFORMS: URL path corrections applied unconditionally.
|
|
13
|
+
# - per-meeting predicates/rewrites that depend on (body, lang, id).
|
|
14
|
+
module Quirks
|
|
15
|
+
HREF_TRANSFORMS = [
|
|
16
|
+
# Legacy URL typo: /jen/ should be /en/.
|
|
17
|
+
->(href) { href.gsub(%r{\A/jen/}, "/en/") },
|
|
18
|
+
# Legacy CGPM document database path.
|
|
19
|
+
->(href) { href.gsub(%r{\A/en/CGPM/jsp/}, "/en/CGPM/db/") },
|
|
20
|
+
# CIPM fr recommendation listing points to the wrong meeting URL
|
|
21
|
+
# for two specific resolutions on the 106-2017 page.
|
|
22
|
+
->(href) do
|
|
23
|
+
next href unless href =~ %r{/ci/cipm/106-2017/resolution-[12]\z}
|
|
24
|
+
href.gsub("/106-2017/", "/104-_1-2015/")
|
|
25
|
+
end,
|
|
26
|
+
# CIPM 104-2015 actually lives at 104-_1-2015.
|
|
27
|
+
->(href) { href.gsub("/104-2015/", "/104-_1-2015/") },
|
|
28
|
+
# Normalise the legacy Liferay /web/guest/ prefix to the locale.
|
|
29
|
+
# (Caller supplies the language via Quirks.localize_guest_path.)
|
|
30
|
+
].freeze
|
|
31
|
+
|
|
32
|
+
def self.fix_href(href, language: nil)
|
|
33
|
+
href = href.gsub(%r{\Ahttps://www\.bipm\.org}, "")
|
|
34
|
+
if language && href.include?("/web/guest/")
|
|
35
|
+
href = href.gsub("/web/guest/", "/#{language}/")
|
|
36
|
+
end
|
|
37
|
+
HREF_TRANSFORMS.reduce(href) { |h, fn| fn.call(h) }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# CIPM 104-_2-2015 is a duplicate of 104-_1-2015; clear the year
|
|
41
|
+
# so the scrape skips the recommendations branch for it.
|
|
42
|
+
def self.skip_meeting_year?(body_id, meeting_id)
|
|
43
|
+
[body_id, meeting_id] == [:cipm, "104-2"]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# CIPM fr/94-2005 lists resolution 2 twice on the website.
|
|
47
|
+
def self.deduplicate_resolutions(body_id, language, meeting_id, resolutions)
|
|
48
|
+
return resolutions unless [body_id, language.to_s, meeting_id] == [:cipm, "fr", "94"]
|
|
49
|
+
return resolutions unless resolutions.sort.uniq != resolutions.sort
|
|
50
|
+
|
|
51
|
+
(1..3).map do |i|
|
|
52
|
+
"https://www.bipm.org/fr/committees/ci/cipm/94-2005/resolution-#{i}"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# JCRB fr/43-2021 has duplicate identifiers for one action.
|
|
57
|
+
def self.renumber_jcrb_43_fr(resolutions)
|
|
58
|
+
ids = resolutions.map { |i| [i["type"], i["identifier"]] }.sort
|
|
59
|
+
return resolutions unless ids != ids.uniq
|
|
60
|
+
|
|
61
|
+
idx = 1
|
|
62
|
+
resolutions.map do |i|
|
|
63
|
+
next i unless i["identifier"] == "43-1" && i["type"] == "action"
|
|
64
|
+
i = i.dup
|
|
65
|
+
i["identifier"] = "#{i['identifier']}-xxx-#{idx}"
|
|
66
|
+
idx += 1
|
|
67
|
+
i
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module Bipm
|
|
6
|
+
module Data
|
|
7
|
+
module Importer
|
|
8
|
+
module Strategies
|
|
9
|
+
# Abstract strategy. Subclasses implement `call` and override
|
|
10
|
+
# `recording_mode`. The Fetcher queries `recording_mode` to decide
|
|
11
|
+
# whether to disable VCR/WebMock before invoking — the CLI never
|
|
12
|
+
# touches VCR directly.
|
|
13
|
+
class Base
|
|
14
|
+
attr_reader :body, :base_dir, :languages
|
|
15
|
+
|
|
16
|
+
def initialize(body:, base_dir:, languages: Language.all)
|
|
17
|
+
@body = body
|
|
18
|
+
@base_dir = base_dir
|
|
19
|
+
@languages = languages.map { |l| l.is_a?(Language) ? l : Language.find(l) }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def call(agent:)
|
|
23
|
+
raise NotImplementedError, "#{self.class}#call not implemented"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# :live -> HTTP requests bypass VCR/WebMock entirely.
|
|
27
|
+
# :cassette -> VCR replays recorded responses (records new
|
|
28
|
+
# episodes when BIPM_VCR_RECORD is set).
|
|
29
|
+
def self.recording_mode
|
|
30
|
+
:cassette
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def recording_mode
|
|
34
|
+
self.class.recording_mode
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "mechanize"
|
|
4
|
+
require "nokogiri"
|
|
5
|
+
require "yaml"
|
|
6
|
+
require "json"
|
|
7
|
+
require "date"
|
|
8
|
+
require "fileutils"
|
|
9
|
+
|
|
10
|
+
module Bipm
|
|
11
|
+
module Data
|
|
12
|
+
module Importer
|
|
13
|
+
module Strategies
|
|
14
|
+
# Scrapes bodies rendered through BIPM's Liferay SPA. Each body
|
|
15
|
+
# exposes index/meetings/publications/(recommendations)/(outcomes)
|
|
16
|
+
# pages whose DOM uses the `.meetings-list__*`, `.bipm-resolutions`,
|
|
17
|
+
# `.bipm-decisions` selectors. Per-meeting and per-recommendation
|
|
18
|
+
# pages are linked from the meetings listing.
|
|
19
|
+
#
|
|
20
|
+
# All requests are wrapped in VCR cassettes for deterministic replays
|
|
21
|
+
# (set `BIPM_VCR_RECORD` env var to refresh).
|
|
22
|
+
class SpaMeetings < Base
|
|
23
|
+
def call(agent:)
|
|
24
|
+
languages.each { |lang| scrape_language(agent, lang) }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def scrape_language(agent, lang)
|
|
30
|
+
index = { "meetings" => Hash[languages.map { |l| [l.to_s, []] }],
|
|
31
|
+
"decisions" => Hash[languages.map { |l| [l.to_s, []] }] }
|
|
32
|
+
|
|
33
|
+
pages = fetch_pages(agent, lang)
|
|
34
|
+
process_meetings(agent, lang, pages, index)
|
|
35
|
+
write_meetings(lang, index)
|
|
36
|
+
write_publications(agent, lang, pages[:publications])
|
|
37
|
+
puts "* #{body.display_name}/#{lang} parsing done"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def fetch_pages(agent, lang)
|
|
41
|
+
base = body.url(lang)
|
|
42
|
+
{
|
|
43
|
+
index: get(agent, lang, "#{base}", "index"),
|
|
44
|
+
meetings: get(agent, lang, "#{base}/meetings", "meetings"),
|
|
45
|
+
publications: get(agent, lang, "#{base}/publications", "publications"),
|
|
46
|
+
recommendations: get_optional(agent, lang, "#{base}/recommendations", "recommendations"),
|
|
47
|
+
outcomes: get_optional(agent, lang, "#{base}/#{outcomes_subpath}", "outcomes"),
|
|
48
|
+
}
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def outcomes_subpath
|
|
52
|
+
body.id == :cipm ? "outcomes" : "meeting-outcomes"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def get(agent, lang, url, kind)
|
|
56
|
+
VCR.use_cassette("#{cassette_scope}/#{body.id}-#{kind}#{lang.suffix}") { agent.get(url) }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def get_optional(agent, lang, url, kind)
|
|
60
|
+
VCR.use_cassette("#{cassette_scope}/#{body.id}-#{kind}#{lang.suffix}") { agent.get(url) }
|
|
61
|
+
rescue Mechanize::ResponseCodeError
|
|
62
|
+
nil
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def cassette_scope
|
|
66
|
+
body.id.to_s
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def process_meetings(agent, lang, pages, index)
|
|
70
|
+
meetings_page = pages[:meetings]
|
|
71
|
+
publications = pages[:publications]
|
|
72
|
+
recommendations = pages[:recommendations]
|
|
73
|
+
outcomes = pages[:outcomes]
|
|
74
|
+
|
|
75
|
+
meetings_page.css(".meetings-list__item").each do |meeting_div|
|
|
76
|
+
date = Common.extract_date(meeting_div.at_css(".meetings-list__informations-date").text)
|
|
77
|
+
title = meeting_div.at_css(".meetings-list__informations-title").text.strip
|
|
78
|
+
href = meeting_div.at_css(".meetings-list__informations-title").attr("href")
|
|
79
|
+
href = "/#{lang}" + href unless href.start_with?("/#{lang}/")
|
|
80
|
+
|
|
81
|
+
ident = href.split("/#{body.id}/").last.gsub("/", ".")
|
|
82
|
+
yr, wg, meeting_id = parse_meeting_identifiers(href)
|
|
83
|
+
|
|
84
|
+
meeting = VCR.use_cassette("#{cassette_scope}/#{body.id}-meeting-#{ident}#{lang.suffix}") do
|
|
85
|
+
agent.get(href)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
yr = nil if Quirks.skip_meeting_year?(body.id, meeting_id)
|
|
89
|
+
|
|
90
|
+
if yr
|
|
91
|
+
build_meeting_entry(agent, lang, meeting, ident, yr, meeting_id, title, date, recommendations, index)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
if meeting_id
|
|
95
|
+
build_decision_entry(lang, meeting, ident, meeting_id, title, date, wg, outcomes, index)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def parse_meeting_identifiers(href)
|
|
101
|
+
wg = nil
|
|
102
|
+
yr = href.include?("/wg/") ? nil : href.split("-").last
|
|
103
|
+
meeting_id =
|
|
104
|
+
if href.include?("/wg/")
|
|
105
|
+
wg = href.split("/wg/").last.split("/").first
|
|
106
|
+
href.split("/").last
|
|
107
|
+
else
|
|
108
|
+
parts = href.split("/").last.split("-")
|
|
109
|
+
if parts.length == 2
|
|
110
|
+
parts[0]
|
|
111
|
+
else
|
|
112
|
+
parts[0] + parts[1].sub("_", "-")
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
[yr, wg, meeting_id]
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def build_meeting_entry(agent, lang, meeting, ident, yr, meeting_id, title, date, recommendations, index)
|
|
119
|
+
pdf = Common.extract_pdf(meeting, lang)
|
|
120
|
+
|
|
121
|
+
h = {
|
|
122
|
+
"metadata" => {
|
|
123
|
+
"title" => title,
|
|
124
|
+
"identifier" => meeting_id,
|
|
125
|
+
"date" => date.to_s,
|
|
126
|
+
"source" => "BIPM - Pavillon de Breteuil",
|
|
127
|
+
"url" => meeting.uri.to_s,
|
|
128
|
+
},
|
|
129
|
+
}
|
|
130
|
+
h["pdf"] = pdf if pdf
|
|
131
|
+
|
|
132
|
+
resolutions = meeting.css(".bipm-resolutions .publications__content").map do |res_div|
|
|
133
|
+
res_div.at_css("a").attr("href")
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
resolutions_additional =
|
|
137
|
+
recommendations&.css(".bipm-resolutions .publications__content")&.map do |res_div|
|
|
138
|
+
Quirks.fix_href(res_div.at_css("a").attr("href"), language: lang.to_s)
|
|
139
|
+
end&.select { |href| href.include?("/#{ident}/") } || []
|
|
140
|
+
|
|
141
|
+
resolutions = Quirks.deduplicate_resolutions(body.id, lang, meeting_id, resolutions)
|
|
142
|
+
resolutions = (resolutions + resolutions_additional).uniq
|
|
143
|
+
|
|
144
|
+
h["resolutions"] = resolutions.map do |href|
|
|
145
|
+
href = Quirks.fix_href(href, language: lang.to_s)
|
|
146
|
+
res_id = href.split("-").last.to_i
|
|
147
|
+
res = VCR.use_cassette("#{cassette_scope}/#{body.id}-recommendation-#{yr}-#{res_id}#{lang.suffix}") do
|
|
148
|
+
agent.get(href)
|
|
149
|
+
end
|
|
150
|
+
Common.parse_resolution(res, res_id, date, body.id, lang.to_s, "recommendation?")
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
index["meetings"][lang.to_s] << h
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def build_decision_entry(lang, meeting, ident, meeting_id, title, date, wg, outcomes, index)
|
|
157
|
+
h = {
|
|
158
|
+
"metadata" => {
|
|
159
|
+
"title" => title,
|
|
160
|
+
"identifier" => meeting_id,
|
|
161
|
+
"date" => date.to_s,
|
|
162
|
+
"source" => "BIPM - Pavillon de Breteuil",
|
|
163
|
+
"url" => meeting.uri.to_s,
|
|
164
|
+
},
|
|
165
|
+
}
|
|
166
|
+
h["metadata"]["workgroup"] = wg if wg
|
|
167
|
+
|
|
168
|
+
decisions = meeting.css(".bipm-decisions .decisions")
|
|
169
|
+
|
|
170
|
+
if outcomes
|
|
171
|
+
decisions_additional = outcomes.css(".bipm-decisions .decisions").select do |i|
|
|
172
|
+
i["data-meeting_key"] == meeting_id ||
|
|
173
|
+
i["data-meeting_key"] == "#{meeting_id}-0" ||
|
|
174
|
+
i["data-meeting"] == meeting_id
|
|
175
|
+
end
|
|
176
|
+
decisions = decisions.to_a + decisions_additional.to_a
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
h["resolutions"] = decisions.map { |tr| build_decision(tr, meeting, lang, date) }
|
|
180
|
+
h["resolutions"] = Quirks.renumber_jcrb_43_fr(h["resolutions"]) if [body.id, lang.to_s, meeting_id] == [:jcrb, "fr", "43"]
|
|
181
|
+
h["resolutions"] = h["resolutions"].sort_by do |i|
|
|
182
|
+
[i["type"], i["identifier"].scan(/([0-9]+|[^0-9]+)/).map(&:first).map { |x| x =~ /[0-9]/ ? x.to_i : x }]
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
h["working_documents"] = meeting.css(".portlet-boundary_CommitteePublications_ .publications__content").map do |d|
|
|
186
|
+
build_working_document(d)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
index["decisions"][lang.to_s] << h
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def build_decision(titletr, meeting, lang, date)
|
|
193
|
+
title = titletr.at_css(".title-third").text.strip
|
|
194
|
+
type = classify_decision_title(title)
|
|
195
|
+
categories = JSON.parse(titletr.attr("data-decisioncategories") || "[]").map(&:strip).uniq
|
|
196
|
+
|
|
197
|
+
r = {
|
|
198
|
+
"dates" => [date.to_s],
|
|
199
|
+
"subject" => body.display_name,
|
|
200
|
+
"type" => type,
|
|
201
|
+
"title" => title,
|
|
202
|
+
"identifier" => "#{titletr.attr('data-meeting')}-#{titletr.attr('data-number')}",
|
|
203
|
+
"url" => meeting.uri.to_s,
|
|
204
|
+
"categories" => categories,
|
|
205
|
+
"considerations" => [],
|
|
206
|
+
"actions" => [],
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
contenttr = titletr.attr("data-text").to_s
|
|
210
|
+
if contenttr.empty?
|
|
211
|
+
contenttr = titletr.css("p").map(&:inner_html).join("\n")
|
|
212
|
+
end
|
|
213
|
+
contenttr = Nokogiri::HTML(contenttr)
|
|
214
|
+
|
|
215
|
+
Common.replace_links(contenttr, meeting, lang.to_s)
|
|
216
|
+
part = Common.ng_to_string(contenttr)
|
|
217
|
+
part = part.gsub(%r"<a name=\"haut\">(.*?)</a>"m, '\1')
|
|
218
|
+
parse = Nokogiri::HTML(part).text.strip
|
|
219
|
+
|
|
220
|
+
parse =~ /\A(((the|le|la|Le secrétaire du|Le président du|Les membres du|Le directeur du) +[BC][IG]PM( Director| President| Secretary| members)?|Dr May|W\.E\. May)[, ]+)/i
|
|
221
|
+
subject = $1
|
|
222
|
+
if subject
|
|
223
|
+
parse = parse[subject.length..-1]
|
|
224
|
+
part = part.gsub(/\A(<html><body>\n*<p>)#{Regexp.escape subject}/, '\1')
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
xparse = parse
|
|
228
|
+
(1..1024).each do |pass|
|
|
229
|
+
if try_clause!(r, "considerations", xparse, part, date) then break end
|
|
230
|
+
if try_clause!(r, "actions", xparse, part, date) then break end
|
|
231
|
+
case pass
|
|
232
|
+
when 1, 2, 3
|
|
233
|
+
xparse = xparse.gsub(/\A.*?(CIPM( President| in its meeting)?:?\n*|\((2018|CIPM)\)|de la 103e session) /m, "")
|
|
234
|
+
when 4
|
|
235
|
+
xparse = parse.gsub(/\A.*?, /m, "")
|
|
236
|
+
when 5
|
|
237
|
+
xparse = parse.gsub(/\A.*? (and|et) /m, "")
|
|
238
|
+
else
|
|
239
|
+
r["x-unparsed"] ||= []
|
|
240
|
+
r["x-unparsed"] << parse
|
|
241
|
+
break
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
r
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def try_clause!(result, key, xparse, part, date)
|
|
249
|
+
map = key == "actions" ? Clauses::ACTIONS : Clauses::CONSIDERATIONS
|
|
250
|
+
map.each do |k, _|
|
|
251
|
+
if xparse =~ /\A#{Clauses::PREFIX}#{k}\b/i
|
|
252
|
+
result[key] << {
|
|
253
|
+
"type" => map[k],
|
|
254
|
+
"date_effective" => date.to_s,
|
|
255
|
+
"message" => Common.format_message(part),
|
|
256
|
+
}
|
|
257
|
+
return true
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
false
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def classify_decision_title(title)
|
|
264
|
+
case title
|
|
265
|
+
when /\AD[eé]cision/ then "decision"
|
|
266
|
+
when /\AR[eé]solution/ then "resolution"
|
|
267
|
+
when /\AAction/ then "action"
|
|
268
|
+
when /\ARecomm[ae]ndation/ then "recommendation"
|
|
269
|
+
else "decision"
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def build_working_document(d)
|
|
274
|
+
date_str = d.at_css(".publications__date")&.text&.strip
|
|
275
|
+
date_str = Date.parse(date_str).strftime("%d/%m/%Y") if date_str
|
|
276
|
+
{
|
|
277
|
+
"title" => d.at_css(".title-third").text.strip,
|
|
278
|
+
"pdf" => d.at_css(".title-third").attr("href"),
|
|
279
|
+
"description" => d.css(".publications__body").first.text.strip,
|
|
280
|
+
"author" => d.css(".publications__body").last.text.strip,
|
|
281
|
+
"date" => date_str,
|
|
282
|
+
}.compact
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def write_meetings(lang, index)
|
|
286
|
+
parts = index["meetings"][lang.to_s] + index["decisions"][lang.to_s]
|
|
287
|
+
parts = parts.group_by { |i| [i["metadata"]["workgroup"].to_s, i["metadata"]["identifier"].to_s] }
|
|
288
|
+
parts = parts.sort_by { |(wg, i),| [wg, i.to_i, i] }.to_h
|
|
289
|
+
|
|
290
|
+
parts.each do |(wg, mid), hs|
|
|
291
|
+
h = {
|
|
292
|
+
"metadata" => hs.first["metadata"],
|
|
293
|
+
"pdf" => hs.first["pdf"],
|
|
294
|
+
"resolutions" => hs.map { |i| i["resolutions"] }.sum([]),
|
|
295
|
+
"working_documents" => hs.map { |i| i["working_documents"] || [] }.sum([]),
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
h.delete("pdf") unless h["pdf"]
|
|
299
|
+
h.delete("working_documents") if h["working_documents"].empty?
|
|
300
|
+
|
|
301
|
+
fn = if wg
|
|
302
|
+
"#{base_dir}/#{body.id}/workgroups/#{wg}/meetings#{lang.directory_suffix}/meeting-#{mid}.yml"
|
|
303
|
+
elsif body.id == :cgpm
|
|
304
|
+
"#{base_dir}/#{body.id}/meetings#{lang.directory_suffix}/meeting-#{format('%02d', mid.to_i)}.yml"
|
|
305
|
+
else
|
|
306
|
+
"#{base_dir}/#{body.id}/meetings#{lang.directory_suffix}/meeting-#{mid}.yml"
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
FileUtils.mkdir_p(File.dirname(fn))
|
|
310
|
+
File.write(fn, YAML.dump(h))
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def write_publications(agent, lang, publications)
|
|
315
|
+
return unless publications
|
|
316
|
+
categories = publications.css(".publications").map do |i|
|
|
317
|
+
title = i.previous_element&.text&.strip || "Misc"
|
|
318
|
+
{
|
|
319
|
+
"title" => title,
|
|
320
|
+
"documents" => i.css(".publications__content").map do |d|
|
|
321
|
+
{
|
|
322
|
+
"title" => d.at_css(".title-third").text.strip.gsub(/\s+/, " "),
|
|
323
|
+
"pdf" => d.at_css(".title-third")&.attr("href")&.split("?")&.first,
|
|
324
|
+
}.compact
|
|
325
|
+
end,
|
|
326
|
+
}
|
|
327
|
+
end
|
|
328
|
+
return if categories.empty?
|
|
329
|
+
|
|
330
|
+
doc = {
|
|
331
|
+
"metadata" => { "url" => "#{body.url(lang)}/publications/" },
|
|
332
|
+
"categories" => categories,
|
|
333
|
+
}
|
|
334
|
+
File.write("#{base_dir}/#{body.id}/publications-#{lang}.yml", YAML.dump(doc))
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
end
|