eco-helpers 2.0.26 → 2.0.27
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +19 -1
- data/lib/eco/api/common/people/default_parsers/csv_parser.rb +47 -11
- data/lib/eco/api/common/people/person_entry.rb +7 -6
- data/lib/eco/api/common/people/person_entry_attribute_mapper.rb +55 -16
- data/lib/eco/api/usecases/default_cases/to_csv_case.rb +1 -37
- data/lib/eco/api/usecases/default_cases/to_csv_detailed_case.rb +42 -0
- data/lib/eco/cli/config/default/options.rb +6 -1
- data/lib/eco/cli/config/filters.rb +1 -1
- data/lib/eco/cli/config/options_set.rb +1 -1
- data/lib/eco/cli/config/use_cases.rb +1 -1
- data/lib/eco/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 144eb5a5b4c0069c07c1af3bac6c23c43b975643f45b8b7d8e5edd05daa8defd
|
4
|
+
data.tar.gz: 6f2c5e8f1744380cb543d11087b3d5bbab822697e876c21c8a4ca1984ea5f3a9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8bb382dbc126f15308cdd51f3ec8d520a154450080388e329243102e0fa2c4d9dcbba395ec91f59ea645ea734f222a5af8fe25a1000a39b2474aba2c89a725dd
|
7
|
+
data.tar.gz: 11874a475a744e673e8741777079d54081c0b6be855a609cf7ed6e8a032f29c6e074cefe7e8a94d57944d70be4b8bebbcb5d6a7ff81e25d6e2798ce4259779b1
|
data/CHANGELOG.md
CHANGED
@@ -1,7 +1,25 @@
|
|
1
1
|
# Change Log
|
2
2
|
All notable changes to this project will be documented in this file.
|
3
3
|
|
4
|
-
## [2.0.
|
4
|
+
## [2.0.27] - 2021-06-xx
|
5
|
+
|
6
|
+
### Added
|
7
|
+
|
8
|
+
### Changed
|
9
|
+
- Parent class `Eco::API::UseCases::DefaultCases::ToCsvCase` shouldn't know header maps necessary for `Eco::API::UseCases::DefaultCases::ToCsvDetailedCase`
|
10
|
+
* Moved related header maps to `ToCsvDetailedCase`
|
11
|
+
|
12
|
+
### Fixed
|
13
|
+
- `--help` should work fine now
|
14
|
+
- Attribute parsers that depended on indirect parser attributes were **not** included
|
15
|
+
- **changed** a couple of classes
|
16
|
+
- `Eco::API::Common::People::PersonEntryAttributeMapper`, where methods should receive `data` to re-scope if the data is available (most specifically `#internal_attrs`)
|
17
|
+
- `Eco::API::Common::People::PersonEntry`, where when setting the final values on `Person`, it should include any internal attribute that is present in the `final_entry` (the result of all the parsing process)
|
18
|
+
- `Eco::API::Common::People::DefaultParsers::CSVParser`
|
19
|
+
- indirect attributes that depended on other indirect attributes would show as missing even if they were active
|
20
|
+
- i.e. `name` depends on `first-name` & `surname`, **AND** `details-name` depends on `name`
|
21
|
+
|
22
|
+
## [2.0.26] - 2021-06-25
|
5
23
|
|
6
24
|
### Added
|
7
25
|
- `Eco::API::MicroCases#set_supervisor`, tries to keep in sync the `#subordinates` **count** of previous and new supervisor
|
@@ -70,20 +70,14 @@ class Eco::API::Common::People::DefaultParsers::CSVParser < Eco::API::Common::Lo
|
|
70
70
|
end
|
71
71
|
|
72
72
|
def missing_headers(headers)
|
73
|
-
|
74
|
-
hext = headers - hint
|
75
|
-
int_head = hint + hext.map {|e| fields_mapper.to_internal(e)}.compact
|
76
|
-
known_as_int = known_headers.select do |e|
|
77
|
-
i = fields_mapper.to_internal(e)
|
78
|
-
int_head.include?(i)
|
79
|
-
end
|
73
|
+
int_head = internal_present_or_active(headers)
|
80
74
|
ext = headers.select do |e|
|
81
75
|
i = fields_mapper.to_internal(e)
|
82
76
|
int_head.include?(i)
|
83
77
|
end
|
84
|
-
ext_present =
|
78
|
+
ext_present = known_headers_present(int_head) | ext
|
85
79
|
ext_miss = known_headers - ext_present
|
86
|
-
|
80
|
+
|
87
81
|
ext_miss.each_with_object({}) do |ext, missing|
|
88
82
|
next unless int = fields_mapper.to_internal(ext)
|
89
83
|
if all_internal_attrs.include?(int)
|
@@ -91,7 +85,10 @@ class Eco::API::Common::People::DefaultParsers::CSVParser < Eco::API::Common::Lo
|
|
91
85
|
missing[:direct] << ext
|
92
86
|
end
|
93
87
|
related_attrs_requirements = required_attrs.values.select do |req|
|
94
|
-
req.dependant?(int)
|
88
|
+
dep = req.dependant?(int)
|
89
|
+
affects = dep && !int_head.include?(int)
|
90
|
+
in_header = int_head.include?(req.attr)
|
91
|
+
affects || (dep && !in_header)
|
95
92
|
end
|
96
93
|
next if related_attrs_requirements.empty?
|
97
94
|
missing[:indirect] ||= {}
|
@@ -106,6 +103,41 @@ class Eco::API::Common::People::DefaultParsers::CSVParser < Eco::API::Common::Lo
|
|
106
103
|
end
|
107
104
|
end
|
108
105
|
|
106
|
+
def known_headers_present(headers_internal)
|
107
|
+
@known_internal ||= known_headers.select do |ext|
|
108
|
+
int = fields_mapper.to_internal(ext)
|
109
|
+
headers_internal.include?(int)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def internal_present_or_active(headers, inactive_requirements = {})
|
114
|
+
hint = headers & all_internal_attrs
|
115
|
+
hext = headers - hint
|
116
|
+
int_present = hint + hext.map {|e| fields_mapper.to_internal(e)}.compact
|
117
|
+
|
118
|
+
update_inactive = Proc.new do
|
119
|
+
inactive_requirements.dup.each do |attr, req|
|
120
|
+
if req.active?(*int_present)
|
121
|
+
inactive_requirements.delete(attr)
|
122
|
+
int_present << attr
|
123
|
+
update_inactive.call
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
required_attrs.values.each do |req|
|
129
|
+
next if int_present.include?(req)
|
130
|
+
if req.active?(*int_present)
|
131
|
+
inactive_requirements.delete(req.attr)
|
132
|
+
int_present << req.attr
|
133
|
+
update_inactive.call
|
134
|
+
else
|
135
|
+
inactive_requirements[req.attr] = req
|
136
|
+
end
|
137
|
+
end
|
138
|
+
int_present
|
139
|
+
end
|
140
|
+
|
109
141
|
def known_headers
|
110
142
|
@known_headers ||= fields_mapper.list(:external).compact
|
111
143
|
end
|
@@ -121,7 +153,11 @@ class Eco::API::Common::People::DefaultParsers::CSVParser < Eco::API::Common::Lo
|
|
121
153
|
end
|
122
154
|
|
123
155
|
def all_internal_attrs
|
124
|
-
|
156
|
+
@all_internal_attrs ||= [].tap do |int_attrs|
|
157
|
+
known_int_attrs = person_parser.all_attrs(include_defined_parsers: true)
|
158
|
+
known_int_attrs |= fields_mapper.list(:internal).compact
|
159
|
+
int_attrs.concat(known_int_attrs)
|
160
|
+
end
|
125
161
|
end
|
126
162
|
|
127
163
|
def person_parser
|
@@ -188,7 +188,7 @@ module Eco
|
|
188
188
|
# @param person [Ecoportal::API::V1::Person] the person we want to set the core values to.
|
189
189
|
# @param exclude [String, Array<String>] core attributes that should not be set/changed to the person.
|
190
190
|
def set_core(person, exclude: nil)
|
191
|
-
scoped_attrs = @emap.core_attrs - into_a(exclude)
|
191
|
+
scoped_attrs = @emap.core_attrs(@final_entry) - into_a(exclude)
|
192
192
|
@final_entry.slice(*scoped_attrs).each do |attr, value|
|
193
193
|
begin
|
194
194
|
set_part(person, attr, value)
|
@@ -210,7 +210,7 @@ module Eco
|
|
210
210
|
# @param exclude [String, Array<String>] account properties that should not be set/changed to the person.
|
211
211
|
def set_account(person, exclude: nil)
|
212
212
|
person.account = {} if !person.account
|
213
|
-
scoped_attrs = @emap.account_attrs - into_a(exclude)
|
213
|
+
scoped_attrs = @emap.account_attrs(@final_entry) - into_a(exclude)
|
214
214
|
@final_entry.slice(*scoped_attrs).each do |attr, value|
|
215
215
|
set_part(person.account, attr, value)
|
216
216
|
end
|
@@ -224,7 +224,7 @@ module Eco
|
|
224
224
|
# @param exclude [String, Array<String>] schema field attributes that should not be set/changed to the person.
|
225
225
|
def set_details(person, exclude: nil)
|
226
226
|
person.add_details(@person_parser.schema) if !person.details || !person.details.schema_id
|
227
|
-
scoped_attrs = @emap.details_attrs - into_a(exclude)
|
227
|
+
scoped_attrs = @emap.details_attrs(@final_entry) - into_a(exclude)
|
228
228
|
@final_entry.slice(*scoped_attrs).each do |attr, value|
|
229
229
|
set_part(person.details, attr, value)
|
230
230
|
end
|
@@ -326,12 +326,13 @@ module Eco
|
|
326
326
|
# @param internal_entry [Hash] the entry with the **internal** _attribute_ names and values but the **external** types.
|
327
327
|
# @return [Hash] the `parsed entry` with the **internal** final attributes names, values and types.
|
328
328
|
def _final_parsing(internal_entry)
|
329
|
-
|
330
|
-
core_account_hash
|
329
|
+
core_account_attrs = @emap.account_attrs(internal_entry) + @emap.core_attrs(internal_entry)
|
330
|
+
core_account_hash = internal_entry.slice(*core_account_attrs).each_with_object({}) do |(attr, value), hash|
|
331
331
|
hash[attr] = _parse_type(attr, value)
|
332
332
|
end
|
333
333
|
|
334
|
-
|
334
|
+
details_attrs = @emap.details_attrs(internal_entry)
|
335
|
+
details_hash = internal_entry.slice(*details_attrs).each_with_object({}) do |(attr, value), hash|
|
335
336
|
hash[attr] = _parse_type(attr, value, schema: @person_parser.schema)
|
336
337
|
end
|
337
338
|
|
@@ -3,18 +3,11 @@ module Eco
|
|
3
3
|
module Common
|
4
4
|
module People
|
5
5
|
|
6
|
-
# @attr_reader core_attrs [Array<String>] core attributes that are present in the person entry.
|
7
|
-
# @attr_reader details_attrs [Array<String>] schema details attributes that are present in the person entry.
|
8
|
-
# @attr_reader account_attrs [Array<String>] account attributes that are present in the person entry.
|
9
|
-
# @attr_reader all_model_attrs [Array<String>] all the attrs that are present in the person entry.
|
10
|
-
# @attr_reader internal_attrs [Array<String>] all the internally named attributes that the person entry has.
|
11
|
-
# @attr_reader aliased_attrs [Array<String>] only those internal attributes present in the person entry that have an internal/external name mapping.
|
12
6
|
# @attr_reader direct_attrs [Array<String>] only those internal attributes present in the person entry that do **not** have an internal/external name mapping.
|
13
7
|
class PersonEntryAttributeMapper
|
14
8
|
@@cached_warnings = {}
|
15
9
|
|
16
|
-
attr_reader :
|
17
|
-
attr_reader :internal_attrs, :aliased_attrs, :direct_attrs
|
10
|
+
attr_reader :aliased_attrs, :direct_attrs
|
18
11
|
|
19
12
|
# Helper class tied to `PersonEntry` that allows to track which attributes of a person entry are present
|
20
13
|
# and how they should be mapped between internal and external names if applicable.
|
@@ -38,17 +31,64 @@ module Eco
|
|
38
31
|
|
39
32
|
if parsing?
|
40
33
|
@external_entry = data
|
41
|
-
init_attr_trackers
|
42
34
|
else # SERIALIZING
|
43
35
|
@person = data
|
44
|
-
@internal_attrs = @person_parser.all_model_attrs
|
45
|
-
@aliased_attrs = @attr_map.list(:internal)
|
46
36
|
end
|
37
|
+
end
|
47
38
|
|
48
|
-
|
49
|
-
|
50
|
-
@
|
51
|
-
|
39
|
+
# @return [Array<String>] only those internal attributes present in the person entry that have an internal/external name mapping.
|
40
|
+
def aliased_attrs
|
41
|
+
return @aliased_attrs unless !@aliased_attrs
|
42
|
+
if parsing?
|
43
|
+
init_attr_trackers
|
44
|
+
else
|
45
|
+
@aliased_attrs = @attr_map.list(:internal)
|
46
|
+
end
|
47
|
+
@aliased_attrs
|
48
|
+
end
|
49
|
+
|
50
|
+
# @return [Array<String>] all the internally named attributes that the person entry has.
|
51
|
+
def internal_attrs(data = nil)
|
52
|
+
return @internal_attrs unless data || !@internal_attrs
|
53
|
+
if parsing?
|
54
|
+
init_attr_trackers unless @internal_attrs
|
55
|
+
if data
|
56
|
+
return data.keys & @person_parser.all_model_attrs
|
57
|
+
end
|
58
|
+
else
|
59
|
+
@internal_attrs = @person_parser.all_model_attrs
|
60
|
+
end
|
61
|
+
@internal_attrs
|
62
|
+
end
|
63
|
+
|
64
|
+
|
65
|
+
# @return [Array<String>] all the attrs that are present in the person entry.
|
66
|
+
def all_model_attrs(data = nil)
|
67
|
+
core_attrs(data) | account_attrs(data) | details_attrs(data)
|
68
|
+
end
|
69
|
+
|
70
|
+
# @return [Array<String>] core attributes that are present in the person entry.
|
71
|
+
def core_attrs(data = nil)
|
72
|
+
return @core_attrs unless data || !@core_attrs
|
73
|
+
@person_parser.target_attrs_core(internal_attrs(data)).tap do |core_attrs|
|
74
|
+
@core_attrs ||= core_attrs
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# @return [Array<String>] schema details attributes that are present in the person entry.
|
79
|
+
def details_attrs(data = nil)
|
80
|
+
return @details_attrs unless data || !@core_attrs
|
81
|
+
@person_parser.target_attrs_details(internal_attrs(data)).tap do |details_attrs|
|
82
|
+
@details_attrs ||= details_attrs
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# @return [Array<String>] account attributes that are present in the person entry.
|
87
|
+
def account_attrs(data = nil)
|
88
|
+
return @account_attrs unless data || !@account_attrs
|
89
|
+
@person_parser.target_attrs_account(internal_attrs(data)).tap do |account_attrs|
|
90
|
+
@account_attrs ||= account_attrs
|
91
|
+
end
|
52
92
|
end
|
53
93
|
|
54
94
|
# To know if currently the object is in parse or serialize mode.
|
@@ -151,7 +191,6 @@ module Eco
|
|
151
191
|
def_unlinked = @person_parser.undefined_model_attrs.select { |attr| !to_external(attr) }
|
152
192
|
# (def) those with parser or alias:
|
153
193
|
def_linked = def_all_attrs - def_unlinked
|
154
|
-
|
155
194
|
# (data) data attributes (actual attributes of the entry)
|
156
195
|
data_attrs = attributes(@external_entry)
|
157
196
|
# (data) attributes of the data that come directly as internal attribute names
|
@@ -125,43 +125,7 @@ class Eco::API::UseCases::DefaultCases::ToCsvCase < Eco::API::Common::Loaders::U
|
|
125
125
|
"id" => "ecoPortal ID",
|
126
126
|
"external_id" => "Reference ID (ext_id)",
|
127
127
|
"login_provider_ids" => "Login Methods",
|
128
|
-
"landing_page_id" => "Landing Page ID"
|
129
|
-
"show_sidebar" => "(pref) Sidebar Open?",
|
130
|
-
"show_shortcuts" => "(pref) Link to Registers?",
|
131
|
-
"show_coming_soon" => "(pref) Coming Soon List?",
|
132
|
-
"show_recently_visited_forms" => "(pref) Recently Visited Forms List?",
|
133
|
-
"show_tasks" => "(pref) Tasks List?",
|
134
|
-
"show_task_bubbles" => "(pref) Task Count Bubbles",
|
135
|
-
"kiosk_enabled" => "Kiosk User?",
|
136
|
-
"freemium" => "Freemium User?",
|
137
|
-
"files" => "(able) on Files",
|
138
|
-
"reports" => "(able) on Report Structures",
|
139
|
-
"data" => "(able) on Data (hours, datasets)",
|
140
|
-
"organization" => "(able) on Organization Config",
|
141
|
-
"pages" => "(able) on Page/Entries",
|
142
|
-
"page_editor" => "(able) page Editor Level",
|
143
|
-
"registers" => "(able) on Registers",
|
144
|
-
"tasks" => "(able) on Tasks",
|
145
|
-
"person_core" => "(able) on People",
|
146
|
-
"person_core_create" => "(able) Create People?",
|
147
|
-
"person_core_edit" => "(able) Edit People?",
|
148
|
-
"person_details" => "(able) on People Schema Details",
|
149
|
-
"person_account" => "(able) on Users",
|
150
|
-
"person_abilities" => "(able) on Users' Abilities",
|
151
|
-
"custom_files" => "(min) on Files",
|
152
|
-
"custom_reports" => "(min) on Report Structures",
|
153
|
-
"custom_data" => "(min) on Data (hours, datasets)",
|
154
|
-
"custom_organization" => "(min) on Organization Config",
|
155
|
-
"custom_pages" => "(min) on Page/Entries",
|
156
|
-
"custom_page_editor" => "(min) page Editor Level",
|
157
|
-
"custom_registers" => "(min) on Registers",
|
158
|
-
"custom_tasks" => "(min) on Tasks",
|
159
|
-
"custom_person_core" => "(min) on People",
|
160
|
-
"custom_person_core_create" => "(min) Create People?",
|
161
|
-
"custom_person_core_edit" => "(min) Edit People?",
|
162
|
-
"custom_person_details" => "(min) on People Schema Details",
|
163
|
-
"custom_person_account" => "(min) on Users",
|
164
|
-
"custom_person_abilities" => "(min) on Users' Abilities"
|
128
|
+
"landing_page_id" => "Landing Page ID"
|
165
129
|
}
|
166
130
|
end
|
167
131
|
|
@@ -75,4 +75,46 @@ class Eco::API::UseCases::DefaultCases::ToCsvDetailedCase < Eco::API::UseCases::
|
|
75
75
|
]
|
76
76
|
end
|
77
77
|
|
78
|
+
def nice_header_maps
|
79
|
+
@nice_header_maps ||= super.merge({
|
80
|
+
"landing_page_id" => "Landing Page ID",
|
81
|
+
"show_sidebar" => "(pref) Sidebar Open?",
|
82
|
+
"show_shortcuts" => "(pref) Link to Registers?",
|
83
|
+
"show_coming_soon" => "(pref) Coming Soon List?",
|
84
|
+
"show_recently_visited_forms" => "(pref) Recently Visited Forms List?",
|
85
|
+
"show_tasks" => "(pref) Tasks List?",
|
86
|
+
"show_task_bubbles" => "(pref) Task Count Bubbles",
|
87
|
+
"kiosk_enabled" => "Kiosk User?",
|
88
|
+
"freemium" => "Freemium User?",
|
89
|
+
"files" => "(able) on Files",
|
90
|
+
"reports" => "(able) on Report Structures",
|
91
|
+
"data" => "(able) on Data (hours, datasets)",
|
92
|
+
"organization" => "(able) on Organization Config",
|
93
|
+
"pages" => "(able) on Page/Entries",
|
94
|
+
"page_editor" => "(able) page Editor Level",
|
95
|
+
"registers" => "(able) on Registers",
|
96
|
+
"tasks" => "(able) on Tasks",
|
97
|
+
"person_core" => "(able) on People",
|
98
|
+
"person_core_create" => "(able) Create People?",
|
99
|
+
"person_core_edit" => "(able) Edit People?",
|
100
|
+
"person_details" => "(able) on People Schema Details",
|
101
|
+
"person_account" => "(able) on Users",
|
102
|
+
"person_abilities" => "(able) on Users' Abilities",
|
103
|
+
"custom_files" => "(min) on Files",
|
104
|
+
"custom_reports" => "(min) on Report Structures",
|
105
|
+
"custom_data" => "(min) on Data (hours, datasets)",
|
106
|
+
"custom_organization" => "(min) on Organization Config",
|
107
|
+
"custom_pages" => "(min) on Page/Entries",
|
108
|
+
"custom_page_editor" => "(min) page Editor Level",
|
109
|
+
"custom_registers" => "(min) on Registers",
|
110
|
+
"custom_tasks" => "(min) on Tasks",
|
111
|
+
"custom_person_core" => "(min) on People",
|
112
|
+
"custom_person_core_create" => "(min) Create People?",
|
113
|
+
"custom_person_core_edit" => "(min) Edit People?",
|
114
|
+
"custom_person_details" => "(min) on People Schema Details",
|
115
|
+
"custom_person_account" => "(min) on Users",
|
116
|
+
"custom_person_abilities" => "(min) on Users' Abilities"
|
117
|
+
})
|
118
|
+
end
|
119
|
+
|
78
120
|
end
|
@@ -2,7 +2,12 @@ ASSETS.cli.config do |cnf|
|
|
2
2
|
cnf.options_set do |options_set, options|
|
3
3
|
options_set.add("--help", "Offers a HELP") do |options, sesssion|
|
4
4
|
conf = ASSETS.cli.config
|
5
|
-
active = Proc.new
|
5
|
+
active = Proc.new do |opt|
|
6
|
+
if there = SCR.get_arg(opt)
|
7
|
+
refine = SCR.get_arg(opt, with_param: true)
|
8
|
+
end
|
9
|
+
refine || there
|
10
|
+
end
|
6
11
|
|
7
12
|
if hpf = active.call("-filters")
|
8
13
|
puts conf.people_filters.help(refine: hpf)
|
@@ -18,7 +18,7 @@ module Eco
|
|
18
18
|
[msg].yield_self do |lines|
|
19
19
|
max_len = keys_max_len(@filters.keys)
|
20
20
|
@filters.keys.sort.select do |key|
|
21
|
-
refine.is_a?(String)
|
21
|
+
!refine.is_a?(String) || key.include?(refine)
|
22
22
|
end.each do |key|
|
23
23
|
lines << help_line(key, @description[key], max_len)
|
24
24
|
end
|
@@ -25,7 +25,7 @@ module Eco
|
|
25
25
|
str_indent = is_general ? "" : " " * indent
|
26
26
|
lines << help_line(namespace, "", max_len) unless is_general
|
27
27
|
options_set(namespace).select do |arg, option|
|
28
|
-
refine.is_a?(String)
|
28
|
+
!refine.is_a?(String) || option.name.include?(refine)
|
29
29
|
end.each do |arg, option|
|
30
30
|
lines << help_line(" " * indent + "#{option.name}", option.description, max_len)
|
31
31
|
end
|
@@ -34,7 +34,7 @@ module Eco
|
|
34
34
|
["The following are the available use cases#{refinement}:"].yield_self do |lines|
|
35
35
|
max_len = keys_max_len(@linked_cases.keys)
|
36
36
|
@linked_cases.keys.sort.select do |key|
|
37
|
-
refine.is_a?(String)
|
37
|
+
!refine.is_a?(String) || key.include?(refine)
|
38
38
|
end.each do |option_case|
|
39
39
|
lines << help_line(option_case, @linked_cases[option_case].description, max_len)
|
40
40
|
end
|
data/lib/eco/version.rb
CHANGED