meibo 0.1.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 +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +13 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/Gemfile +16 -0
- data/Gemfile.lock +84 -0
- data/LICENSE.txt +21 -0
- data/README.md +41 -0
- data/Rakefile +12 -0
- data/lib/meibo/academic_session.rb +48 -0
- data/lib/meibo/academic_session_set.rb +15 -0
- data/lib/meibo/base_profile.rb +37 -0
- data/lib/meibo/builder/academic_session_builder.rb +26 -0
- data/lib/meibo/builder/base_builder.rb +13 -0
- data/lib/meibo/builder/classroom_builder.rb +38 -0
- data/lib/meibo/builder/course_builder.rb +32 -0
- data/lib/meibo/builder/demographic_builder.rb +22 -0
- data/lib/meibo/builder/enrollment_builder.rb +30 -0
- data/lib/meibo/builder/organization_builder.rb +34 -0
- data/lib/meibo/builder/role_builder.rb +30 -0
- data/lib/meibo/builder/user_builder.rb +36 -0
- data/lib/meibo/builder/user_profile_builder.rb +22 -0
- data/lib/meibo/builder.rb +60 -0
- data/lib/meibo/classroom.rb +67 -0
- data/lib/meibo/classroom_set.rb +29 -0
- data/lib/meibo/converter.rb +259 -0
- data/lib/meibo/course.rb +45 -0
- data/lib/meibo/course_set.rb +23 -0
- data/lib/meibo/data_model.rb +84 -0
- data/lib/meibo/data_set.rb +44 -0
- data/lib/meibo/demographic.rb +70 -0
- data/lib/meibo/demographic_set.rb +18 -0
- data/lib/meibo/enrollment.rb +51 -0
- data/lib/meibo/enrollment_set.rb +22 -0
- data/lib/meibo/errors.rb +13 -0
- data/lib/meibo/factory_bot/academic_session.rb +23 -0
- data/lib/meibo/factory_bot/all.rb +13 -0
- data/lib/meibo/factory_bot/classroom.rb +28 -0
- data/lib/meibo/factory_bot/course.rb +21 -0
- data/lib/meibo/factory_bot/demographic.rb +16 -0
- data/lib/meibo/factory_bot/enrollment.rb +37 -0
- data/lib/meibo/factory_bot/manifest.rb +33 -0
- data/lib/meibo/factory_bot/memory_package.rb +11 -0
- data/lib/meibo/factory_bot/organization.rb +47 -0
- data/lib/meibo/factory_bot/role.rb +41 -0
- data/lib/meibo/factory_bot/user.rb +22 -0
- data/lib/meibo/factory_bot/user_profile.rb +21 -0
- data/lib/meibo/japan_profile/academic_session.rb +26 -0
- data/lib/meibo/japan_profile/classroom.rb +21 -0
- data/lib/meibo/japan_profile/course.rb +18 -0
- data/lib/meibo/japan_profile/demographic.rb +15 -0
- data/lib/meibo/japan_profile/enrollment.rb +35 -0
- data/lib/meibo/japan_profile/organization.rb +20 -0
- data/lib/meibo/japan_profile/role.rb +26 -0
- data/lib/meibo/japan_profile/user.rb +27 -0
- data/lib/meibo/japan_profile/user_profile.rb +7 -0
- data/lib/meibo/japan_profile.rb +39 -0
- data/lib/meibo/manifest/processing_mode.rb +40 -0
- data/lib/meibo/manifest.rb +163 -0
- data/lib/meibo/organization.rb +44 -0
- data/lib/meibo/organization_set.rb +15 -0
- data/lib/meibo/reader.rb +138 -0
- data/lib/meibo/role.rb +65 -0
- data/lib/meibo/role_set.rb +25 -0
- data/lib/meibo/roster.rb +166 -0
- data/lib/meibo/user.rb +71 -0
- data/lib/meibo/user_profile.rb +42 -0
- data/lib/meibo/user_profile_set.rb +18 -0
- data/lib/meibo/user_set.rb +24 -0
- data/lib/meibo/version.rb +5 -0
- data/lib/meibo.rb +17 -0
- data/meibo.gemspec +38 -0
- data/sig/meibo.rbs +4 -0
- metadata +148 -0
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'securerandom'
|
4
|
+
|
5
|
+
module Meibo
|
6
|
+
class Builder
|
7
|
+
module UserProfileBuilder
|
8
|
+
extend BaseBuilder
|
9
|
+
|
10
|
+
def self.builder_attribute_names
|
11
|
+
[:builder, :user]
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(builder:, sourced_id: SecureRandom.uuid, user:, **kw)
|
15
|
+
super(sourced_id: sourced_id, user_sourced_id: user.sourced_id, **kw)
|
16
|
+
@builder = builder
|
17
|
+
@user = user
|
18
|
+
builder.user_profiles << self
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
|
5
|
+
module Meibo
|
6
|
+
class Builder
|
7
|
+
extend Forwardable
|
8
|
+
|
9
|
+
attr_reader :package, :profile
|
10
|
+
|
11
|
+
def_delegators :@package, :academic_sessions, :classes, :courses, :demographics, :enrollments, :organizations, :roles, :users, :user_profiles
|
12
|
+
|
13
|
+
def initialize(package:, profile: BaseProfile)
|
14
|
+
@package = package
|
15
|
+
@profile = profile
|
16
|
+
end
|
17
|
+
|
18
|
+
def build_academic_session(**kw)
|
19
|
+
builder_for(:academic_session).new(builder: self, **kw)
|
20
|
+
end
|
21
|
+
|
22
|
+
def build_classroom(**kw)
|
23
|
+
builder_for(:class).new(builder: self, **kw)
|
24
|
+
end
|
25
|
+
|
26
|
+
def build_course(**kw)
|
27
|
+
builder_for(:course).new(builder: self, **kw)
|
28
|
+
end
|
29
|
+
|
30
|
+
def build_demographic(**kw)
|
31
|
+
builder_for(:demographic).new(builder: self, **kw)
|
32
|
+
end
|
33
|
+
|
34
|
+
def build_enrollment(**kw)
|
35
|
+
builder_for(:enrollment).new(builder: self, **kw)
|
36
|
+
end
|
37
|
+
|
38
|
+
def build_organization(**kw)
|
39
|
+
builder_for(:org).new(builder: self, **kw)
|
40
|
+
end
|
41
|
+
|
42
|
+
def build_role(**kw)
|
43
|
+
builder_for(:role).new(builder: self, **kw)
|
44
|
+
end
|
45
|
+
|
46
|
+
def build_user(**kw)
|
47
|
+
builder_for(:user).new(builder: self, **kw)
|
48
|
+
end
|
49
|
+
|
50
|
+
def build_user_profile(**kw)
|
51
|
+
builder_for(:user_profile).new(builder: self, **kw)
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def builder_for(key)
|
57
|
+
profile.builder_for(key)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Meibo
|
4
|
+
class Classroom
|
5
|
+
TYPES = {
|
6
|
+
homeroom: 'homeroom',
|
7
|
+
scheduled: 'scheduled'
|
8
|
+
}.freeze
|
9
|
+
|
10
|
+
DataModel.define(
|
11
|
+
self,
|
12
|
+
attribute_name_to_header_field_map: {
|
13
|
+
sourced_id: 'sourcedId',
|
14
|
+
status: 'status',
|
15
|
+
date_last_modified: 'dateLastModified',
|
16
|
+
title: 'title',
|
17
|
+
grades: 'grades',
|
18
|
+
course_sourced_id: 'courseSourcedId',
|
19
|
+
class_code: 'classCode',
|
20
|
+
class_type: 'classType',
|
21
|
+
location: 'location',
|
22
|
+
school_sourced_id: 'schoolSourcedId',
|
23
|
+
term_sourced_ids: 'termSourcedIds',
|
24
|
+
subjects: 'subjects',
|
25
|
+
subject_codes: 'subjectCodes',
|
26
|
+
periods: 'periods'
|
27
|
+
}.freeze,
|
28
|
+
converters: {
|
29
|
+
datetime: [:date_last_modified].freeze,
|
30
|
+
enum: {
|
31
|
+
class_type: [*TYPES.values, ENUM_EXT_PATTERN].freeze
|
32
|
+
}.freeze,
|
33
|
+
list: [
|
34
|
+
:grades,
|
35
|
+
:term_sourced_ids,
|
36
|
+
:subjects,
|
37
|
+
:subject_codes,
|
38
|
+
:periods
|
39
|
+
].freeze,
|
40
|
+
required: [:sourced_id, :title, :class_type, :course_sourced_id, :term_sourced_ids, :school_sourced_id].freeze,
|
41
|
+
status: [:status].freeze
|
42
|
+
}
|
43
|
+
)
|
44
|
+
|
45
|
+
def initialize(sourced_id:, status: nil, date_last_modified: nil, title:, grades: [], course_sourced_id:, class_code: nil, class_type:, location: nil, school_sourced_id:, term_sourced_ids:, subjects: [], subject_codes: [], periods: [], **extension_fields)
|
46
|
+
unless subjects.is_a?(Array) && subject_codes.is_a?(Array) && subjects.size == subject_codes.size
|
47
|
+
raise InvalidDataTypeError
|
48
|
+
end
|
49
|
+
|
50
|
+
@sourced_id = sourced_id
|
51
|
+
@status = status
|
52
|
+
@date_last_modified = date_last_modified
|
53
|
+
@title = title
|
54
|
+
@grades = grades
|
55
|
+
@course_sourced_id = course_sourced_id
|
56
|
+
@class_code = class_code
|
57
|
+
@class_type = class_type
|
58
|
+
@location = location
|
59
|
+
@school_sourced_id = school_sourced_id
|
60
|
+
@term_sourced_ids = term_sourced_ids
|
61
|
+
@subjects = subjects
|
62
|
+
@subject_codes = subject_codes
|
63
|
+
@periods = periods
|
64
|
+
@extension_fields = extension_fields
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Meibo
|
4
|
+
class ClassroomSet < DataSet
|
5
|
+
def initialize(data, academic_session_set:, course_set:, organization_set:)
|
6
|
+
super(data)
|
7
|
+
@academic_session_set = academic_session_set
|
8
|
+
@course_set = course_set
|
9
|
+
@organization_set = organization_set
|
10
|
+
end
|
11
|
+
|
12
|
+
def check_semantically_consistent
|
13
|
+
super
|
14
|
+
|
15
|
+
each do |classroom|
|
16
|
+
@organization_set.find_by_sourced_id(classroom.school_sourced_id)
|
17
|
+
@course_set.find_by_sourced_id(classroom.course_sourced_id)
|
18
|
+
|
19
|
+
if classroom.term_sourced_ids.empty?
|
20
|
+
raise DataNotFoundError, "termSourcedIdは1つ以上指定してください"
|
21
|
+
end
|
22
|
+
|
23
|
+
classroom.term_sourced_ids.each do |term_sourced_id|
|
24
|
+
@academic_session_set.find_by_sourced_id(term_sourced_id)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,259 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'date'
|
4
|
+
require 'time'
|
5
|
+
|
6
|
+
module Meibo
|
7
|
+
module Converter
|
8
|
+
TYPES = %i[
|
9
|
+
list
|
10
|
+
required
|
11
|
+
boolean
|
12
|
+
date
|
13
|
+
datetime
|
14
|
+
enum
|
15
|
+
integer
|
16
|
+
status
|
17
|
+
user_ids
|
18
|
+
year
|
19
|
+
].freeze
|
20
|
+
|
21
|
+
class << self
|
22
|
+
def build_header_field_to_attribute_converter(attribute_name_to_header_field_map)
|
23
|
+
header_field_to_attribute_name_map = attribute_name_to_header_field_map.to_h {|attribute, header_field|
|
24
|
+
[header_field, attribute]
|
25
|
+
}.freeze
|
26
|
+
lambda {|field| header_field_to_attribute_name_map.fetch(field) }
|
27
|
+
end
|
28
|
+
|
29
|
+
def build_parser_converter(fields:, converters:)
|
30
|
+
build_converter(fields: fields, converters: converters, write_or_parser: 'parser')
|
31
|
+
end
|
32
|
+
|
33
|
+
def build_write_converter(fields:, converters:)
|
34
|
+
build_converter(fields: fields, converters: converters, write_or_parser: 'write')
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def build_converter(fields:, converters:, write_or_parser:)
|
40
|
+
converter_list = TYPES.filter_map do |converter_type|
|
41
|
+
fields_to_be_converted = converters[converter_type]
|
42
|
+
method_name = "build_#{converter_type}_field_#{write_or_parser}_converter"
|
43
|
+
if fields_to_be_converted && respond_to?(method_name, true)
|
44
|
+
if converter_type == :enum
|
45
|
+
enum_definition = fields_to_be_converted.to_h {|field, enum| [fields.index(field), enum] }
|
46
|
+
send(method_name, enum_definition)
|
47
|
+
else
|
48
|
+
indexes = fields_to_be_converted.map {|field| fields.index(field) }
|
49
|
+
send(method_name, indexes)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
lambda do |field, field_info|
|
54
|
+
# NOTE: convert blank sourcedId to nil
|
55
|
+
if field_info.index.zero?
|
56
|
+
field = nil if field.empty?
|
57
|
+
end
|
58
|
+
converter_list.each {|converter| field = converter[field, field_info] }
|
59
|
+
field
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def build_boolean_field_parser_converter(boolean_field_indexes)
|
64
|
+
boolean_field_indexes = boolean_field_indexes.dup.freeze
|
65
|
+
lambda do |field, field_info|
|
66
|
+
if boolean_field_indexes.include?(field_info.index)
|
67
|
+
case field
|
68
|
+
when 'true'
|
69
|
+
true
|
70
|
+
when 'false'
|
71
|
+
false
|
72
|
+
when nil
|
73
|
+
nil
|
74
|
+
else
|
75
|
+
raise InvalidDataTypeError
|
76
|
+
end
|
77
|
+
else
|
78
|
+
field
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def build_date_field_write_converter(date_field_indexes)
|
84
|
+
date_field_indexes = date_field_indexes.dup.freeze
|
85
|
+
lambda do |field, field_info|
|
86
|
+
if date_field_indexes.include?(field_info.index)
|
87
|
+
field&.iso8601
|
88
|
+
else
|
89
|
+
field
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def build_date_field_parser_converter(date_field_indexes)
|
95
|
+
date_field_indexes = date_field_indexes.dup.freeze
|
96
|
+
lambda do |field, field_info|
|
97
|
+
if field && date_field_indexes.include?(field_info.index)
|
98
|
+
begin
|
99
|
+
Date.strptime(field, '%Y-%m-%d')
|
100
|
+
rescue
|
101
|
+
raise InvalidDataTypeError
|
102
|
+
end
|
103
|
+
else
|
104
|
+
field
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def build_datetime_field_write_converter(datetime_field_indexes)
|
110
|
+
datetime_field_indexes = datetime_field_indexes.dup.freeze
|
111
|
+
lambda do |field, field_info|
|
112
|
+
if datetime_field_indexes.include?(field_info.index)
|
113
|
+
field&.utc&.iso8601
|
114
|
+
else
|
115
|
+
field
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def build_datetime_field_parser_converter(datetime_field_indexes)
|
121
|
+
datetime_field_indexes = datetime_field_indexes.dup.freeze
|
122
|
+
lambda do |field, field_info|
|
123
|
+
if field && datetime_field_indexes.include?(field_info.index)
|
124
|
+
begin
|
125
|
+
Time.iso8601(field)
|
126
|
+
rescue
|
127
|
+
raise InvalidDataTypeError
|
128
|
+
end
|
129
|
+
else
|
130
|
+
field
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def build_enum_field_parser_converter(enum_definition)
|
136
|
+
enum_definition = enum_definition.dup.freeze
|
137
|
+
lambda do |field, field_info|
|
138
|
+
return field unless field
|
139
|
+
|
140
|
+
enum = enum_definition[field_info.index]
|
141
|
+
if enum
|
142
|
+
unless enum.any? {|e| e.match?(field) }
|
143
|
+
raise InvalidDataTypeError
|
144
|
+
end
|
145
|
+
end
|
146
|
+
field
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def build_integer_field_parser_converter(integer_field_indexes)
|
151
|
+
integer_field_indexes = integer_field_indexes.dup.freeze
|
152
|
+
lambda do |field, field_info|
|
153
|
+
if field && integer_field_indexes.include?(field_info.index)
|
154
|
+
begin
|
155
|
+
Integer(field, 10)
|
156
|
+
rescue
|
157
|
+
raise InvalidDataTypeError
|
158
|
+
end
|
159
|
+
else
|
160
|
+
field
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
def build_list_field_write_converter(list_field_indexes)
|
166
|
+
list_field_indexes = list_field_indexes.dup.freeze
|
167
|
+
lambda do |field, field_info|
|
168
|
+
if list_field_indexes.include?(field_info.index)
|
169
|
+
if field
|
170
|
+
if field.empty?
|
171
|
+
nil
|
172
|
+
else
|
173
|
+
field.join(',')
|
174
|
+
end
|
175
|
+
end
|
176
|
+
else
|
177
|
+
field
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
def build_list_field_parser_converter(list_field_indexes)
|
183
|
+
list_field_indexes = list_field_indexes.dup.freeze
|
184
|
+
lambda do |field, field_info|
|
185
|
+
if list_field_indexes.include?(field_info.index)
|
186
|
+
if field
|
187
|
+
field.split(',').map(&:strip)
|
188
|
+
else
|
189
|
+
[]
|
190
|
+
end
|
191
|
+
else
|
192
|
+
field
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
def build_required_field_parser_converter(required_field_indexes)
|
198
|
+
required_field_indexes = required_field_indexes.dup.freeze
|
199
|
+
lambda do |field, field_info|
|
200
|
+
if required_field_indexes.include?(field_info.index)
|
201
|
+
raise MissingDataError if field.nil?
|
202
|
+
raise MissingDataError if field.respond_to?(:empty?) && field.empty?
|
203
|
+
end
|
204
|
+
field
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
# TODO
|
209
|
+
# def build_role_field_write_converter(role_field_indexes)
|
210
|
+
|
211
|
+
def build_status_field_parser_converter(status_field_indexes)
|
212
|
+
status_field_indexes = status_field_indexes.dup.freeze
|
213
|
+
lambda do |field, field_info|
|
214
|
+
if field && status_field_indexes.include?(field_info.index)
|
215
|
+
raise InvalidDataTypeError, "invalid status: #{field}" unless field == 'active' || field == 'tobedeleted'
|
216
|
+
else
|
217
|
+
field
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
def build_user_ids_field_parser_converter(user_ids_field_indexes)
|
223
|
+
user_ids_field_indexes = user_ids_field_indexes.dup.freeze
|
224
|
+
lambda do |field, field_info|
|
225
|
+
if user_ids_field_indexes.include?(field_info.index)
|
226
|
+
raise InvalidDataTypeError unless field.all? {|user_id| Meibo::User::USER_ID_FORMAT_REGEXP.match?(user_id) }
|
227
|
+
end
|
228
|
+
field
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
def build_year_field_write_converter(year_field_indexes)
|
233
|
+
year_field_indexes = year_field_indexes.dup.freeze
|
234
|
+
lambda do |field, field_info|
|
235
|
+
if year_field_indexes.include?(field_info.index)
|
236
|
+
field && ("%04d" % field)
|
237
|
+
else
|
238
|
+
field
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
def build_year_field_parser_converter(year_field_indexes)
|
244
|
+
year_field_indexes = year_field_indexes.dup.freeze
|
245
|
+
lambda do |field, field_info|
|
246
|
+
if field && year_field_indexes.include?(field_info.index)
|
247
|
+
begin
|
248
|
+
Integer(field, 10)
|
249
|
+
rescue
|
250
|
+
raise InvalidDataTypeError
|
251
|
+
end
|
252
|
+
else
|
253
|
+
field
|
254
|
+
end
|
255
|
+
end
|
256
|
+
end
|
257
|
+
end
|
258
|
+
end
|
259
|
+
end
|
data/lib/meibo/course.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Meibo
|
4
|
+
class Course
|
5
|
+
DataModel.define(
|
6
|
+
self,
|
7
|
+
attribute_name_to_header_field_map: {
|
8
|
+
sourced_id: 'sourcedId',
|
9
|
+
status: 'status',
|
10
|
+
date_last_modified: 'dateLastModified',
|
11
|
+
school_year_sourced_id: 'schoolYearSourcedId',
|
12
|
+
title: 'title',
|
13
|
+
course_code: 'courseCode',
|
14
|
+
grades: 'grades',
|
15
|
+
org_sourced_id: 'orgSourcedId',
|
16
|
+
subjects: 'subjects',
|
17
|
+
subject_codes: 'subjectCodes'
|
18
|
+
},
|
19
|
+
converters: {
|
20
|
+
datetime: [:date_last_modified],
|
21
|
+
list: [:grades, :subjects, :subject_codes],
|
22
|
+
required: [:sourced_id, :title, :org_sourced_id],
|
23
|
+
status: [:status]
|
24
|
+
}
|
25
|
+
)
|
26
|
+
|
27
|
+
def initialize(sourced_id:, status: nil, date_last_modified: nil, school_year_sourced_id: nil, title:, course_code: nil, grades: [], org_sourced_id:, subjects: [], subject_codes: [], **extension_fields)
|
28
|
+
unless subjects.is_a?(Array) && subject_codes.is_a?(Array) && subjects.size == subject_codes.size
|
29
|
+
raise InvalidDataTypeError
|
30
|
+
end
|
31
|
+
|
32
|
+
@sourced_id = sourced_id
|
33
|
+
@status = status
|
34
|
+
@date_last_modified = date_last_modified
|
35
|
+
@school_year_sourced_id = school_year_sourced_id
|
36
|
+
@title = title
|
37
|
+
@course_code = course_code
|
38
|
+
@grades = grades
|
39
|
+
@org_sourced_id = org_sourced_id
|
40
|
+
@subjects = subjects
|
41
|
+
@subject_codes = subject_codes
|
42
|
+
@extension_fields = extension_fields
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Meibo
|
4
|
+
class CourseSet < DataSet
|
5
|
+
def initialize(data, academic_session_set:, organization_set:)
|
6
|
+
super(data)
|
7
|
+
@academic_session_set = academic_session_set
|
8
|
+
@organization_set = organization_set
|
9
|
+
end
|
10
|
+
|
11
|
+
def check_semantically_consistent
|
12
|
+
super
|
13
|
+
|
14
|
+
each do |course|
|
15
|
+
if course.school_year_sourced_id
|
16
|
+
@academic_session_set.find_by_sourced_id(course.school_year_sourced_id)
|
17
|
+
end
|
18
|
+
|
19
|
+
@organization_set.find_by_sourced_id(course.org_sourced_id)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'csv'
|
4
|
+
|
5
|
+
module Meibo
|
6
|
+
module DataModel
|
7
|
+
module ClassMethods
|
8
|
+
def parse(csv)
|
9
|
+
return to_enum(:parse, csv) unless block_given?
|
10
|
+
|
11
|
+
actual_headers = CSV.parse_line(csv)
|
12
|
+
missing_headers = header_fields - actual_headers
|
13
|
+
unless missing_headers.empty?
|
14
|
+
raise MissingHeadersError, "missing headers: #{missing_headers.join(',')}"
|
15
|
+
end
|
16
|
+
unless actual_headers.take(header_fields.size) == header_fields
|
17
|
+
raise ScrambledHeadersError
|
18
|
+
end
|
19
|
+
|
20
|
+
CSV.parse(csv, encoding: Meibo::CSV_ENCODING, headers: true, converters: parser_converters, header_converters: header_converters).each do |row|
|
21
|
+
yield new(**row.to_h)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.define(klass, attribute_name_to_header_field_map:, converters: {})
|
27
|
+
attribute_name_to_header_field_map = attribute_name_to_header_field_map.dup.freeze
|
28
|
+
attribute_names = attribute_name_to_header_field_map.keys.freeze
|
29
|
+
header_fields = attribute_name_to_header_field_map.values.freeze
|
30
|
+
converters = converters.dup.freeze
|
31
|
+
define_class_attribute(klass, :attribute_name_to_header_field_map, attribute_name_to_header_field_map)
|
32
|
+
define_class_attribute(klass, :attribute_names, attribute_names)
|
33
|
+
define_class_attribute(klass, :header_fields, header_fields)
|
34
|
+
define_class_attribute(klass, :converters, converters)
|
35
|
+
|
36
|
+
define_header_converters(klass, attribute_name_to_header_field_map)
|
37
|
+
define_parser_converters(klass, attribute_names: attribute_names, converters: converters)
|
38
|
+
define_write_converters(klass, attribute_names: attribute_names, converters: converters)
|
39
|
+
|
40
|
+
klass.attr_reader(*attribute_names, :extension_fields)
|
41
|
+
klass.extend(ClassMethods)
|
42
|
+
klass.include(self)
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.define_class_attribute(klass, attribute, value)
|
46
|
+
klass.define_singleton_method(attribute) { value }
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.define_header_converters(klass, attribute_name_to_header_field_map)
|
50
|
+
header_converters = Converter.build_header_field_to_attribute_converter(attribute_name_to_header_field_map)
|
51
|
+
klass.define_singleton_method(:header_converters) { header_converters }
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.define_parser_converters(klass, attribute_names:, converters:)
|
55
|
+
parser_converter = Converter.build_parser_converter(fields: attribute_names, converters: converters)
|
56
|
+
klass.define_singleton_method(:parser_converters) { parser_converter }
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.define_write_converters(klass, attribute_names:, converters:)
|
60
|
+
write_converter = Converter.build_write_converter(fields: attribute_names, converters: converters)
|
61
|
+
klass.define_singleton_method(:write_converters) { write_converter }
|
62
|
+
end
|
63
|
+
|
64
|
+
def to_csv(...)
|
65
|
+
to_a.to_csv(...)
|
66
|
+
end
|
67
|
+
|
68
|
+
def deconstruct
|
69
|
+
to_a
|
70
|
+
end
|
71
|
+
|
72
|
+
def deconstruct_keys(_keys)
|
73
|
+
to_h
|
74
|
+
end
|
75
|
+
|
76
|
+
def to_a
|
77
|
+
self.class.attribute_names.map {|attribute| public_send(attribute) }
|
78
|
+
end
|
79
|
+
|
80
|
+
def to_h
|
81
|
+
self.class.attribute_names.zip(to_a).to_h
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Meibo
|
2
|
+
class DataSet
|
3
|
+
def initialize(data)
|
4
|
+
@data = data
|
5
|
+
@data_hash = data.to_h {|datum| [datum.sourced_id, datum] }
|
6
|
+
end
|
7
|
+
|
8
|
+
def <<(new_data)
|
9
|
+
raise DataNotFoundError, "sourcedIdがありません" unless new_data.sourced_id
|
10
|
+
raise SourcedIdDuplicatedError, 'sourcedIdが重複しています' if @data_hash.key?(new_data.sourced_id)
|
11
|
+
|
12
|
+
@data << new_data
|
13
|
+
@data_hash[new_data.sourced_id] = new_data
|
14
|
+
end
|
15
|
+
|
16
|
+
def check_semantically_consistent
|
17
|
+
unless @data.size == @data_hash.size
|
18
|
+
raise SourcedIdDuplicatedError, 'sourcedIdが重複しています'
|
19
|
+
end
|
20
|
+
|
21
|
+
unless @data_hash[nil].nil?
|
22
|
+
raise DataNotFoundError, "sourcedIdがありません"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def each(...)
|
27
|
+
@data.each(...)
|
28
|
+
end
|
29
|
+
|
30
|
+
def empty?
|
31
|
+
@data.empty?
|
32
|
+
end
|
33
|
+
|
34
|
+
def find_by_sourced_id(sourced_id)
|
35
|
+
@data_hash.fetch(sourced_id)
|
36
|
+
rescue KeyError
|
37
|
+
raise DataNotFoundError, "sourcedId: #{sourced_id} が見つかりません"
|
38
|
+
end
|
39
|
+
|
40
|
+
def to_a
|
41
|
+
@data
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|