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,27 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Meibo
         | 
| 4 | 
            +
              module JapanProfile
         | 
| 5 | 
            +
                class User < ::Meibo::User
         | 
| 6 | 
            +
                  DataModel.define(
         | 
| 7 | 
            +
                    self,
         | 
| 8 | 
            +
                    attribute_name_to_header_field_map: superclass.attribute_name_to_header_field_map.merge(
         | 
| 9 | 
            +
                      kana_given_name: 'metadata.jp.kanaGivenName',
         | 
| 10 | 
            +
                      kana_family_name: 'metadata.jp.kanaFamilyName',
         | 
| 11 | 
            +
                      kana_middle_name: 'metadata.jp.kanaMiddleName',
         | 
| 12 | 
            +
                      home_class: 'metadata.jp.homeClass'
         | 
| 13 | 
            +
                    ).freeze,
         | 
| 14 | 
            +
                    converters: superclass.converters
         | 
| 15 | 
            +
                  )
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                  # NOTE: enabled_userは必須ではないが固定
         | 
| 18 | 
            +
                  def initialize(enabled_user: true, kana_given_name: nil, kana_family_name: nil, kana_middle_name: nil, home_class: nil, **other_fields)
         | 
| 19 | 
            +
                    super(enabled_user: enabled_user, **other_fields)
         | 
| 20 | 
            +
                    @kana_given_name = kana_given_name
         | 
| 21 | 
            +
                    @kana_family_name = kana_family_name
         | 
| 22 | 
            +
                    @kana_middle_name = kana_middle_name
         | 
| 23 | 
            +
                    @home_class = home_class
         | 
| 24 | 
            +
                  end
         | 
| 25 | 
            +
                end
         | 
| 26 | 
            +
              end
         | 
| 27 | 
            +
            end
         | 
| @@ -0,0 +1,39 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Meibo
         | 
| 4 | 
            +
              module JapanProfile
         | 
| 5 | 
            +
                include BaseProfile
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                BUILDERS = {
         | 
| 8 | 
            +
                  academic_session: Builder::AcademicSessionBuilder.create(AcademicSession),
         | 
| 9 | 
            +
                  class: Builder::ClassroomBuilder.create(Classroom),
         | 
| 10 | 
            +
                  course: Builder::CourseBuilder.create(Course),
         | 
| 11 | 
            +
                  demographic: Builder::DemographicBuilder.create(Demographic),
         | 
| 12 | 
            +
                  enrollment: Builder::EnrollmentBuilder.create(Enrollment),
         | 
| 13 | 
            +
                  org: Builder::OrganizationBuilder.create(Organization),
         | 
| 14 | 
            +
                  role: Builder::RoleBuilder.create(Role),
         | 
| 15 | 
            +
                  user: Builder::UserBuilder.create(User),
         | 
| 16 | 
            +
                  user_profile: Builder::UserProfileBuilder.create(UserProfile)
         | 
| 17 | 
            +
                }
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                DATA_MODELS = {
         | 
| 20 | 
            +
                  file_academic_sessions: Meibo::JapanProfile::AcademicSession,
         | 
| 21 | 
            +
                  file_classes: Meibo::JapanProfile::Classroom,
         | 
| 22 | 
            +
                  file_courses: Meibo::JapanProfile::Course,
         | 
| 23 | 
            +
                  file_demographics: Meibo::JapanProfile::Demographic,
         | 
| 24 | 
            +
                  file_enrollments: Meibo::JapanProfile::Enrollment,
         | 
| 25 | 
            +
                  file_orgs: Meibo::JapanProfile::Organization,
         | 
| 26 | 
            +
                  file_roles: Meibo::JapanProfile::Role,
         | 
| 27 | 
            +
                  file_user_profiles: Meibo::JapanProfile::UserProfile,
         | 
| 28 | 
            +
                  file_users: Meibo::JapanProfile::User
         | 
| 29 | 
            +
                }
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                def self.builder_for(key)
         | 
| 32 | 
            +
                  BUILDERS[key]
         | 
| 33 | 
            +
                end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                def self.data_model_for(file_attribute)
         | 
| 36 | 
            +
                  DATA_MODELS[file_attribute]
         | 
| 37 | 
            +
                end
         | 
| 38 | 
            +
              end
         | 
| 39 | 
            +
            end
         | 
| @@ -0,0 +1,40 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Meibo
         | 
| 4 | 
            +
              class Manifest
         | 
| 5 | 
            +
                class ProcessingMode
         | 
| 6 | 
            +
                  attr_reader :mode
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                  def initialize(mode)
         | 
| 9 | 
            +
                    @mode = mode
         | 
| 10 | 
            +
                    freeze
         | 
| 11 | 
            +
                  end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                  def absent?
         | 
| 14 | 
            +
                    @mode == 'absent'
         | 
| 15 | 
            +
                  end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                  def bulk?
         | 
| 18 | 
            +
                    @mode == 'bulk'
         | 
| 19 | 
            +
                  end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                  def delta?
         | 
| 22 | 
            +
                    @mode == 'delta'
         | 
| 23 | 
            +
                  end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                  def ==(other)
         | 
| 26 | 
            +
                    if other.is_a?(ProcessingMode)
         | 
| 27 | 
            +
                      @mode == other.mode
         | 
| 28 | 
            +
                    elsif other.is_a?(String)
         | 
| 29 | 
            +
                      to_s == other
         | 
| 30 | 
            +
                    else
         | 
| 31 | 
            +
                      false
         | 
| 32 | 
            +
                    end
         | 
| 33 | 
            +
                  end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                  def to_s
         | 
| 36 | 
            +
                    @mode
         | 
| 37 | 
            +
                  end
         | 
| 38 | 
            +
                end
         | 
| 39 | 
            +
              end
         | 
| 40 | 
            +
            end
         | 
| @@ -0,0 +1,163 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Meibo
         | 
| 4 | 
            +
              class Manifest
         | 
| 5 | 
            +
                MANIFEST_VERSION = '1.0'
         | 
| 6 | 
            +
                ONEROSTER_VERSION = '1.2'
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                PROCESSING_MODES = {
         | 
| 9 | 
            +
                  absent: ProcessingMode.new('absent'),
         | 
| 10 | 
            +
                  bulk: ProcessingMode.new('bulk'),
         | 
| 11 | 
            +
                  delta: ProcessingMode.new('delta')
         | 
| 12 | 
            +
                }.freeze
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                PROPERTY_NAME_TO_ATTRIBUTE_MAP = {
         | 
| 15 | 
            +
                  'manifest.version' => :manifest_version,
         | 
| 16 | 
            +
                  'oneroster.version' => :oneroster_version,
         | 
| 17 | 
            +
                  'file.academicSessions' => :file_academic_sessions,
         | 
| 18 | 
            +
                  'file.categories' => :file_categories,
         | 
| 19 | 
            +
                  'file.classes' => :file_classes,
         | 
| 20 | 
            +
                  'file.classResources' => :file_class_resources,
         | 
| 21 | 
            +
                  'file.courses' => :file_courses,
         | 
| 22 | 
            +
                  'file.courseResources' => :file_course_resources,
         | 
| 23 | 
            +
                  'file.demographics' => :file_demographics,
         | 
| 24 | 
            +
                  'file.enrollments' => :file_enrollments,
         | 
| 25 | 
            +
                  'file.lineItemLearningObjectiveIds' => :file_line_item_learning_objective_ids,
         | 
| 26 | 
            +
                  'file.lineItems' => :file_line_items,
         | 
| 27 | 
            +
                  'file.lineItemScoreScales' => :file_line_item_score_scales,
         | 
| 28 | 
            +
                  'file.orgs' => :file_orgs,
         | 
| 29 | 
            +
                  'file.resources' => :file_resources,
         | 
| 30 | 
            +
                  'file.resultLearningObjectiveIds' => :file_result_learning_objective_ids,
         | 
| 31 | 
            +
                  'file.results' => :file_results,
         | 
| 32 | 
            +
                  'file.resultScoreScales' => :file_result_score_scales,
         | 
| 33 | 
            +
                  'file.roles' => :file_roles,
         | 
| 34 | 
            +
                  'file.scoreScales' => :file_score_scales,
         | 
| 35 | 
            +
                  'file.userProfiles' => :file_user_profiles,
         | 
| 36 | 
            +
                  'file.userResources' => :file_user_resources,
         | 
| 37 | 
            +
                  'file.users' => :file_users,
         | 
| 38 | 
            +
                  'source.systemName' => :source_system_name,
         | 
| 39 | 
            +
                  'source.systemCode' => :source_system_code
         | 
| 40 | 
            +
                }.freeze
         | 
| 41 | 
            +
                ATTRIBUTE_TO_PROPERTY_NAME_MAP = PROPERTY_NAME_TO_ATTRIBUTE_MAP.to_h {|property_name, attribute| [attribute, property_name] }.freeze
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                # NOTE: 想定値
         | 
| 44 | 
            +
                DEFAULT_VALUES = {
         | 
| 45 | 
            +
                  manifest_version: MANIFEST_VERSION,
         | 
| 46 | 
            +
                  oneroster_version: ONEROSTER_VERSION,
         | 
| 47 | 
            +
                  file_academic_sessions: PROCESSING_MODES[:bulk],
         | 
| 48 | 
            +
                  file_categories: PROCESSING_MODES[:absent],
         | 
| 49 | 
            +
                  file_classes: PROCESSING_MODES[:bulk],
         | 
| 50 | 
            +
                  file_class_resources: PROCESSING_MODES[:absent],
         | 
| 51 | 
            +
                  file_courses: PROCESSING_MODES[:bulk],
         | 
| 52 | 
            +
                  file_course_resources: PROCESSING_MODES[:absent],
         | 
| 53 | 
            +
                  file_demographics: PROCESSING_MODES[:bulk],
         | 
| 54 | 
            +
                  file_enrollments: PROCESSING_MODES[:bulk],
         | 
| 55 | 
            +
                  file_line_item_learning_objective_ids: PROCESSING_MODES[:absent],
         | 
| 56 | 
            +
                  file_line_items: PROCESSING_MODES[:absent],
         | 
| 57 | 
            +
                  file_line_item_score_scales: PROCESSING_MODES[:absent],
         | 
| 58 | 
            +
                  file_orgs: PROCESSING_MODES[:bulk],
         | 
| 59 | 
            +
                  file_resources: PROCESSING_MODES[:absent],
         | 
| 60 | 
            +
                  file_result_learning_objective_ids: PROCESSING_MODES[:absent],
         | 
| 61 | 
            +
                  file_results: PROCESSING_MODES[:absent],
         | 
| 62 | 
            +
                  file_result_score_scales: PROCESSING_MODES[:absent],
         | 
| 63 | 
            +
                  file_roles: PROCESSING_MODES[:bulk],
         | 
| 64 | 
            +
                  file_score_scales: PROCESSING_MODES[:absent],
         | 
| 65 | 
            +
                  file_user_profiles: PROCESSING_MODES[:bulk],
         | 
| 66 | 
            +
                  file_user_resources: PROCESSING_MODES[:absent],
         | 
| 67 | 
            +
                  file_users: PROCESSING_MODES[:bulk]
         | 
| 68 | 
            +
                }.freeze
         | 
| 69 | 
            +
             | 
| 70 | 
            +
                attr_reader :manifest_version, :oneroster_version, :file_academic_sessions, :file_categories, :file_classes, :file_class_resources, :file_courses, :file_course_resources, :file_demographics, :file_enrollments, :file_line_item_learning_objective_ids, :file_line_items, :file_line_item_score_scales, :file_orgs, :file_resources, :file_result_learning_objective_ids, :file_results, :file_result_score_scales, :file_roles, :file_score_scales, :file_user_profiles, :file_user_resources, :file_users, :source_system_name, :source_system_code
         | 
| 71 | 
            +
             | 
| 72 | 
            +
                def self.build_from_default(**custom_properties)
         | 
| 73 | 
            +
                  new(**Manifest::DEFAULT_VALUES.merge(custom_properties))
         | 
| 74 | 
            +
                end
         | 
| 75 | 
            +
             | 
| 76 | 
            +
                def self.filename
         | 
| 77 | 
            +
                  'manifest.csv'
         | 
| 78 | 
            +
                end
         | 
| 79 | 
            +
             | 
| 80 | 
            +
                attribute_name_to_header_field_map = {
         | 
| 81 | 
            +
                  property_name: 'propertyName',
         | 
| 82 | 
            +
                  value: 'value'
         | 
| 83 | 
            +
                }
         | 
| 84 | 
            +
                header_fields = attribute_name_to_header_field_map.values
         | 
| 85 | 
            +
                define_singleton_method(:header_fields) { header_fields }
         | 
| 86 | 
            +
                DataModel.define_header_converters(self, attribute_name_to_header_field_map)
         | 
| 87 | 
            +
             | 
| 88 | 
            +
                def self.attribute_to_property_name(attribute)
         | 
| 89 | 
            +
                  ATTRIBUTE_TO_PROPERTY_NAME_MAP.fetch(attribute)
         | 
| 90 | 
            +
                end
         | 
| 91 | 
            +
             | 
| 92 | 
            +
                def self.filename_for(attribute)
         | 
| 93 | 
            +
                  property_name = attribute_to_property_name(attribute)
         | 
| 94 | 
            +
             | 
| 95 | 
            +
                  raise Meibo::Error, "#{property_name}はファイルのプロパティではありません" unless property_name.start_with?('file.')
         | 
| 96 | 
            +
             | 
| 97 | 
            +
                  property_name.split('file.', 2).last + '.csv'
         | 
| 98 | 
            +
                end
         | 
| 99 | 
            +
             | 
| 100 | 
            +
                def self.parse(csv)
         | 
| 101 | 
            +
                  properties = CSV.parse(csv, encoding: Meibo::CSV_ENCODING, headers: true, header_converters: header_converters)
         | 
| 102 | 
            +
                  new(**properties.to_h {|property| [PROPERTY_NAME_TO_ATTRIBUTE_MAP.fetch(property[:property_name]), property[:value]] })
         | 
| 103 | 
            +
                end
         | 
| 104 | 
            +
             | 
| 105 | 
            +
                def initialize(manifest_version:, oneroster_version:, file_academic_sessions:, file_categories:, file_classes:, file_class_resources:, file_courses:, file_course_resources:, file_demographics:, file_enrollments:, file_line_item_learning_objective_ids:, file_line_items:, file_line_item_score_scales:, file_orgs:, file_resources:, file_result_learning_objective_ids:, file_results:, file_result_score_scales:, file_roles:, file_score_scales:, file_user_profiles:, file_user_resources:, file_users:, source_system_name: nil, source_system_code: nil)
         | 
| 106 | 
            +
                  @manifest_version = manifest_version
         | 
| 107 | 
            +
                  @oneroster_version = oneroster_version
         | 
| 108 | 
            +
                  @file_academic_sessions = file_academic_sessions
         | 
| 109 | 
            +
                  @file_categories = file_categories
         | 
| 110 | 
            +
                  @file_classes = file_classes
         | 
| 111 | 
            +
                  @file_class_resources = file_class_resources
         | 
| 112 | 
            +
                  @file_courses = file_courses
         | 
| 113 | 
            +
                  @file_course_resources = file_course_resources
         | 
| 114 | 
            +
                  @file_demographics = file_demographics
         | 
| 115 | 
            +
                  @file_enrollments = file_enrollments
         | 
| 116 | 
            +
                  @file_line_item_learning_objective_ids = file_line_item_learning_objective_ids
         | 
| 117 | 
            +
                  @file_line_items = file_line_items
         | 
| 118 | 
            +
                  @file_line_item_score_scales = file_line_item_score_scales
         | 
| 119 | 
            +
                  @file_orgs = file_orgs
         | 
| 120 | 
            +
                  @file_resources = file_resources
         | 
| 121 | 
            +
                  @file_result_learning_objective_ids = file_result_learning_objective_ids
         | 
| 122 | 
            +
                  @file_results = file_results
         | 
| 123 | 
            +
                  @file_result_score_scales = file_result_score_scales
         | 
| 124 | 
            +
                  @file_roles = file_roles
         | 
| 125 | 
            +
                  @file_score_scales = file_score_scales
         | 
| 126 | 
            +
                  @file_user_profiles = file_user_profiles
         | 
| 127 | 
            +
                  @file_user_resources = file_user_resources
         | 
| 128 | 
            +
                  @file_users = file_users
         | 
| 129 | 
            +
                  @source_system_name = source_system_name
         | 
| 130 | 
            +
                  @source_system_code = source_system_code
         | 
| 131 | 
            +
                end
         | 
| 132 | 
            +
             | 
| 133 | 
            +
                def filenames(processing_mode:)
         | 
| 134 | 
            +
                  file_attributes(processing_mode: processing_mode).map do |attribute|
         | 
| 135 | 
            +
                    self.class.filename_for(attribute)
         | 
| 136 | 
            +
                  end
         | 
| 137 | 
            +
                end
         | 
| 138 | 
            +
             | 
| 139 | 
            +
                def file_attributes(processing_mode:)
         | 
| 140 | 
            +
                  PROPERTY_NAME_TO_ATTRIBUTE_MAP.values.filter_map do |attribute|
         | 
| 141 | 
            +
                    attribute.start_with?('file_') && public_send(attribute) == processing_mode.to_s && attribute
         | 
| 142 | 
            +
                  end
         | 
| 143 | 
            +
                end
         | 
| 144 | 
            +
             | 
| 145 | 
            +
                def deconstruct
         | 
| 146 | 
            +
                  to_a
         | 
| 147 | 
            +
                end
         | 
| 148 | 
            +
             | 
| 149 | 
            +
                def deconstruct_keys(_keys)
         | 
| 150 | 
            +
                  to_h
         | 
| 151 | 
            +
                end
         | 
| 152 | 
            +
             | 
| 153 | 
            +
                def to_a
         | 
| 154 | 
            +
                  PROPERTY_NAME_TO_ATTRIBUTE_MAP.map do |property_name, attribute|
         | 
| 155 | 
            +
                    [property_name, public_send(attribute)]
         | 
| 156 | 
            +
                  end
         | 
| 157 | 
            +
                end
         | 
| 158 | 
            +
             | 
| 159 | 
            +
                def to_h
         | 
| 160 | 
            +
                  PROPERTY_NAME_TO_ATTRIBUTE_MAP.values.to_h {|attribute| [attribute, public_send(attribute)] }
         | 
| 161 | 
            +
                end
         | 
| 162 | 
            +
              end
         | 
| 163 | 
            +
            end
         | 
| @@ -0,0 +1,44 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Meibo
         | 
| 4 | 
            +
              class Organization
         | 
| 5 | 
            +
                TYPES = {
         | 
| 6 | 
            +
                  department: 'department',
         | 
| 7 | 
            +
                  school: 'school',
         | 
| 8 | 
            +
                  district: 'district',
         | 
| 9 | 
            +
                  local: 'local',
         | 
| 10 | 
            +
                  state: 'state',
         | 
| 11 | 
            +
                  national: 'national'
         | 
| 12 | 
            +
                }.freeze
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                DataModel.define(
         | 
| 15 | 
            +
                  self,
         | 
| 16 | 
            +
                  attribute_name_to_header_field_map: {
         | 
| 17 | 
            +
                    sourced_id: 'sourcedId',
         | 
| 18 | 
            +
                    status: 'status',
         | 
| 19 | 
            +
                    date_last_modified: 'dateLastModified',
         | 
| 20 | 
            +
                    name: 'name',
         | 
| 21 | 
            +
                    type: 'type',
         | 
| 22 | 
            +
                    identifier: 'identifier',
         | 
| 23 | 
            +
                    parent_sourced_id: 'parentSourcedId'
         | 
| 24 | 
            +
                  },
         | 
| 25 | 
            +
                  converters: {
         | 
| 26 | 
            +
                    datetime: [:date_last_modified],
         | 
| 27 | 
            +
                    enum: { type: [*TYPES.values, ENUM_EXT_PATTERN].freeze },
         | 
| 28 | 
            +
                    required: [:sourced_id, :name, :type],
         | 
| 29 | 
            +
                    status: [:status]
         | 
| 30 | 
            +
                  }
         | 
| 31 | 
            +
                )
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                def initialize(sourced_id:, status: nil, date_last_modified: nil, name:, type:, identifier: nil, parent_sourced_id: (type == TYPES[:district] ? 'NULL' : nil), **extension_fields)
         | 
| 34 | 
            +
                  @sourced_id = sourced_id
         | 
| 35 | 
            +
                  @status = status
         | 
| 36 | 
            +
                  @date_last_modified = date_last_modified
         | 
| 37 | 
            +
                  @name = name
         | 
| 38 | 
            +
                  @type = type
         | 
| 39 | 
            +
                  @identifier = identifier
         | 
| 40 | 
            +
                  @parent_sourced_id = parent_sourced_id
         | 
| 41 | 
            +
                  @extension_fields = extension_fields
         | 
| 42 | 
            +
                end
         | 
| 43 | 
            +
              end
         | 
| 44 | 
            +
            end
         | 
| @@ -0,0 +1,15 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Meibo
         | 
| 4 | 
            +
              class OrganizationSet < DataSet
         | 
| 5 | 
            +
                def check_semantically_consistent
         | 
| 6 | 
            +
                  super
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                  each do |organization|
         | 
| 9 | 
            +
                    next unless organization.parent_sourced_id
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                    find_by_sourced_id(organization.parent_sourced_id)
         | 
| 12 | 
            +
                  end
         | 
| 13 | 
            +
                end
         | 
| 14 | 
            +
              end
         | 
| 15 | 
            +
            end
         | 
    
        data/lib/meibo/reader.rb
    ADDED
    
    | @@ -0,0 +1,138 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'csv'
         | 
| 4 | 
            +
            require 'zip'
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            module Meibo
         | 
| 7 | 
            +
              class Reader
         | 
| 8 | 
            +
                def self.open(file_path, profile: BaseProfile)
         | 
| 9 | 
            +
                  Zip::File.open(file_path) do |zipfile|
         | 
| 10 | 
            +
                    yield new(zipfile: zipfile, profile: profile)
         | 
| 11 | 
            +
                  end
         | 
| 12 | 
            +
                end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                attr_reader :profile, :zipfile
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                def initialize(zipfile:, profile:)
         | 
| 17 | 
            +
                  @profile = profile
         | 
| 18 | 
            +
                  @zipfile = zipfile
         | 
| 19 | 
            +
                end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                def academic_sessions
         | 
| 22 | 
            +
                  each_academic_session.to_a
         | 
| 23 | 
            +
                end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                def classes
         | 
| 26 | 
            +
                  each_class.to_a
         | 
| 27 | 
            +
                end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                def courses
         | 
| 30 | 
            +
                  each_course.to_a
         | 
| 31 | 
            +
                end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                def demographics
         | 
| 34 | 
            +
                  each_demographic.to_a
         | 
| 35 | 
            +
                end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                def enrollments
         | 
| 38 | 
            +
                  each_enrollment.to_a
         | 
| 39 | 
            +
                end
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                def organizations
         | 
| 42 | 
            +
                  each_organization.to_a
         | 
| 43 | 
            +
                end
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                def roles
         | 
| 46 | 
            +
                  each_role.to_a
         | 
| 47 | 
            +
                end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                def users
         | 
| 50 | 
            +
                  each_user.to_a
         | 
| 51 | 
            +
                end
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                def user_profiles
         | 
| 54 | 
            +
                  each_user_profile.to_a
         | 
| 55 | 
            +
                end
         | 
| 56 | 
            +
             | 
| 57 | 
            +
                def each_academic_session(&block)
         | 
| 58 | 
            +
                  read_data(:file_academic_sessions, &block)
         | 
| 59 | 
            +
                end
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                def each_class(&block)
         | 
| 62 | 
            +
                  read_data(:file_classes, &block)
         | 
| 63 | 
            +
                end
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                def each_course(&block)
         | 
| 66 | 
            +
                  read_data(:file_courses, &block)
         | 
| 67 | 
            +
                end
         | 
| 68 | 
            +
             | 
| 69 | 
            +
                def each_demographic(&block)
         | 
| 70 | 
            +
                  read_data(:file_demographics, &block)
         | 
| 71 | 
            +
                end
         | 
| 72 | 
            +
             | 
| 73 | 
            +
                def each_enrollment(&block)
         | 
| 74 | 
            +
                  read_data(:file_enrollments, &block)
         | 
| 75 | 
            +
                end
         | 
| 76 | 
            +
             | 
| 77 | 
            +
                def each_organization(&block)
         | 
| 78 | 
            +
                  read_data(:file_orgs, &block)
         | 
| 79 | 
            +
                end
         | 
| 80 | 
            +
             | 
| 81 | 
            +
                def each_role(&block)
         | 
| 82 | 
            +
                  read_data(:file_roles, &block)
         | 
| 83 | 
            +
                end
         | 
| 84 | 
            +
             | 
| 85 | 
            +
                def each_user_profile(&block)
         | 
| 86 | 
            +
                  read_data(:file_user_profiles, &block)
         | 
| 87 | 
            +
                end
         | 
| 88 | 
            +
             | 
| 89 | 
            +
                def each_user(&block)
         | 
| 90 | 
            +
                  read_data(:file_users, &block)
         | 
| 91 | 
            +
                end
         | 
| 92 | 
            +
             | 
| 93 | 
            +
                def manifest
         | 
| 94 | 
            +
                  @manifest ||= begin
         | 
| 95 | 
            +
                    filename = Meibo::Manifest.filename
         | 
| 96 | 
            +
                    raise CsvFileNotFoundError, "#{filename} not found" unless file_entry?(filename)
         | 
| 97 | 
            +
             | 
| 98 | 
            +
                    csv = @zipfile.read(filename)
         | 
| 99 | 
            +
                    Meibo::Manifest.parse(csv)
         | 
| 100 | 
            +
                  end
         | 
| 101 | 
            +
                end
         | 
| 102 | 
            +
             | 
| 103 | 
            +
                def load_bulk_files
         | 
| 104 | 
            +
                  bulk_file_attributes = manifest.file_attributes(processing_mode: Meibo::Manifest::PROCESSING_MODES[:bulk])
         | 
| 105 | 
            +
                  {
         | 
| 106 | 
            +
                    file_academic_sessions: :academic_sessions,
         | 
| 107 | 
            +
                    file_classes: :classes,
         | 
| 108 | 
            +
                    file_courses: :courses,
         | 
| 109 | 
            +
                    file_demographics: :demographics,
         | 
| 110 | 
            +
                    file_enrollments: :enrollments,
         | 
| 111 | 
            +
                    file_orgs: :organizations,
         | 
| 112 | 
            +
                    file_roles: :roles,
         | 
| 113 | 
            +
                    file_users: :users,
         | 
| 114 | 
            +
                    file_user_profiles: :user_profiles
         | 
| 115 | 
            +
                  }.filter_map {|file_attribute, data_method|
         | 
| 116 | 
            +
                    if bulk_file_attributes.include?(file_attribute)
         | 
| 117 | 
            +
                      [data_method, public_send(data_method).to_a]
         | 
| 118 | 
            +
                    end
         | 
| 119 | 
            +
                  }.to_h
         | 
| 120 | 
            +
                end
         | 
| 121 | 
            +
             | 
| 122 | 
            +
                def file_entry?(filename)
         | 
| 123 | 
            +
                  @zipfile.get_entry(filename).file?
         | 
| 124 | 
            +
                rescue Errno::ENOENT
         | 
| 125 | 
            +
                  false
         | 
| 126 | 
            +
                end
         | 
| 127 | 
            +
             | 
| 128 | 
            +
                private
         | 
| 129 | 
            +
             | 
| 130 | 
            +
                def read_data(file_attribute, &block)
         | 
| 131 | 
            +
                  filename = Manifest.filename_for(file_attribute)
         | 
| 132 | 
            +
                  raise CsvFileNotFoundError, "#{filename} not found" unless file_entry?(filename)
         | 
| 133 | 
            +
             | 
| 134 | 
            +
                  csv = @zipfile.read(filename)
         | 
| 135 | 
            +
                  profile.data_model_for(file_attribute).parse(csv, &block)
         | 
| 136 | 
            +
                end
         | 
| 137 | 
            +
              end
         | 
| 138 | 
            +
            end
         | 
    
        data/lib/meibo/role.rb
    ADDED
    
    | @@ -0,0 +1,65 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Meibo
         | 
| 4 | 
            +
              class Role
         | 
| 5 | 
            +
                TYPES = {
         | 
| 6 | 
            +
                  primary: 'primary',
         | 
| 7 | 
            +
                  secondary: 'secondary'
         | 
| 8 | 
            +
                }.freeze
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                ROLES = {
         | 
| 11 | 
            +
                  aide: 'aide',
         | 
| 12 | 
            +
                  counselor: 'counselor',
         | 
| 13 | 
            +
                  district_administrator: 'districtAdministrator',
         | 
| 14 | 
            +
                  guardian: 'guardian',
         | 
| 15 | 
            +
                  parent: 'parent',
         | 
| 16 | 
            +
                  principal: 'principal',
         | 
| 17 | 
            +
                  proctor: 'proctor',
         | 
| 18 | 
            +
                  relative: 'relative',
         | 
| 19 | 
            +
                  site_administrator: 'siteAdministrator',
         | 
| 20 | 
            +
                  student: 'student',
         | 
| 21 | 
            +
                  system_administrator: 'systemAdministrator',
         | 
| 22 | 
            +
                  teacher: 'teacher'
         | 
| 23 | 
            +
                }.freeze
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                DataModel.define(
         | 
| 26 | 
            +
                  self,
         | 
| 27 | 
            +
                  attribute_name_to_header_field_map: {
         | 
| 28 | 
            +
                    sourced_id: 'sourcedId',
         | 
| 29 | 
            +
                    status: 'status',
         | 
| 30 | 
            +
                    date_last_modified: 'dateLastModified',
         | 
| 31 | 
            +
                    user_sourced_id: 'userSourcedId',
         | 
| 32 | 
            +
                    role_type: 'roleType',
         | 
| 33 | 
            +
                    role: 'role',
         | 
| 34 | 
            +
                    begin_date: 'beginDate',
         | 
| 35 | 
            +
                    end_date: 'endDate',
         | 
| 36 | 
            +
                    org_sourced_id: 'orgSourcedId',
         | 
| 37 | 
            +
                    user_profile_sourced_id: 'userProfileSourcedId'
         | 
| 38 | 
            +
                  }.freeze,
         | 
| 39 | 
            +
                  converters: {
         | 
| 40 | 
            +
                    date: [:begin_date, :end_date].freeze,
         | 
| 41 | 
            +
                    datetime: [:date_last_modified].freeze,
         | 
| 42 | 
            +
                    required: [:sourced_id, :user_sourced_id, :role_type, :role, :org_sourced_id].freeze,
         | 
| 43 | 
            +
                    enum: {
         | 
| 44 | 
            +
                      role: [*ROLES.values, ENUM_EXT_PATTERN].freeze,
         | 
| 45 | 
            +
                      role_type: TYPES.values.freeze
         | 
| 46 | 
            +
                    }.freeze,
         | 
| 47 | 
            +
                    status: [:status].freeze
         | 
| 48 | 
            +
                  }.freeze
         | 
| 49 | 
            +
                )
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                def initialize(sourced_id:, status: nil, date_last_modified: nil, user_sourced_id:, role_type:, role:, begin_date: nil, end_date: nil, org_sourced_id:, user_profile_sourced_id: nil, **extension_fields)
         | 
| 52 | 
            +
                  @sourced_id = sourced_id
         | 
| 53 | 
            +
                  @status = status
         | 
| 54 | 
            +
                  @date_last_modified = date_last_modified
         | 
| 55 | 
            +
                  @user_sourced_id = user_sourced_id
         | 
| 56 | 
            +
                  @role_type = role_type
         | 
| 57 | 
            +
                  @role = role
         | 
| 58 | 
            +
                  @begin_date = begin_date
         | 
| 59 | 
            +
                  @end_date = end_date
         | 
| 60 | 
            +
                  @org_sourced_id = org_sourced_id
         | 
| 61 | 
            +
                  @user_profile_sourced_id = user_profile_sourced_id
         | 
| 62 | 
            +
                  @extension_fields = extension_fields
         | 
| 63 | 
            +
                end
         | 
| 64 | 
            +
              end
         | 
| 65 | 
            +
            end
         | 
| @@ -0,0 +1,25 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Meibo
         | 
| 4 | 
            +
              class RoleSet < DataSet
         | 
| 5 | 
            +
                def initialize(data, organization_set:, user_set:, user_profile_set:)
         | 
| 6 | 
            +
                  super(data)
         | 
| 7 | 
            +
                  @organization_set = organization_set
         | 
| 8 | 
            +
                  @user_set = user_set
         | 
| 9 | 
            +
                  @user_profile_set = user_profile_set
         | 
| 10 | 
            +
                end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                def check_semantically_consistent
         | 
| 13 | 
            +
                  super
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                  each do |role|
         | 
| 16 | 
            +
                    @organization_set.find_by_sourced_id(role.org_sourced_id)
         | 
| 17 | 
            +
                    @user_set.find_by_sourced_id(role.user_sourced_id)
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                    if role.user_profile_sourced_id
         | 
| 20 | 
            +
                      @user_profile_set.find_by_sourced_id(role.user_profile_sourced_id)
         | 
| 21 | 
            +
                    end
         | 
| 22 | 
            +
                  end
         | 
| 23 | 
            +
                end
         | 
| 24 | 
            +
              end
         | 
| 25 | 
            +
            end
         |