old_school 0.0.5

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.
data/README.md ADDED
@@ -0,0 +1,4 @@
1
+ old_school
2
+ ==========
3
+
4
+ Ruby Gem to interface with a Powerful SIS
data/lib/old_school.rb ADDED
@@ -0,0 +1,19 @@
1
+ require 'virtus'
2
+
3
+ require 'old_school/virtus_ext'
4
+ require 'old_school/core_ext'
5
+ require 'old_school/csv'
6
+
7
+ require 'old_school/api'
8
+ require 'old_school/models/student'
9
+ require 'old_school/models/name'
10
+ require 'old_school/models/contact_info'
11
+ require 'old_school/models/demographics'
12
+ require 'old_school/models/school'
13
+ require 'old_school/models/section'
14
+ require 'old_school/models/course'
15
+ require 'old_school/models/term'
16
+ require 'old_school/models/assignment'
17
+ require 'old_school/models/assignment_score'
18
+ require 'old_school/models/section_enrollment'
19
+ require 'old_school/models/staff'
@@ -0,0 +1,209 @@
1
+ require_relative 'api/utils'
2
+ require_relative 'api/url_factory'
3
+
4
+ module OldSchool
5
+ class API
6
+ include Utils
7
+
8
+ def initialize(host, id, secret)
9
+ @url_factory = URLFactory.new(host)
10
+ self.id = id
11
+ self.secret = secret
12
+ self.token = nil
13
+ end
14
+
15
+ #Assignment Resource
16
+ def get_assignment(assignment_id)
17
+ hash_from_response get(@url_factory.assignment_url(assignment_id)), 'assignment'
18
+ end
19
+
20
+ def get_assignments(assignment_ids)
21
+ get_many assignment_ids, 'assignment', ->(id){ @url_factory.assignment_url(id) }
22
+ end
23
+
24
+ def update_assignment(assignment_id, assignment)
25
+ put @url_factory.assignment_url(assignment_id), {assignment: assignment}.to_json
26
+ end
27
+
28
+ def delete_assignment(assignment_id)
29
+ delete @url_factory.assignment_url(assignment_id)
30
+ end
31
+
32
+ def update_student_assignment_score(assignment_id, student_id, assignment_score)
33
+ put @url_factory.assignment_student_score_url(assignment_id, student_id), {assignment_score: assignment_score}.to_json
34
+ end
35
+
36
+ def get_student_assignment_score(assignment_id, student_id)
37
+ hash_from_response get(@url_factory.assignment_student_score_url(assignment_id, student_id)), 'assignment_score'
38
+ end
39
+
40
+ def get_student_assignment_scores(assignment_id, student_ids)
41
+ get_many student_ids, 'assignment_score', ->(id){ @url_factory.assignment_student_score_url(assignment_id, id) }
42
+ end
43
+
44
+ def update_multiple_student_assignment_scores(assignment_id, assignment_scores)
45
+ put @url_factory.assignment_score_url(assignment_id), {assignment_scores: {assignment_score: assignment_scores}}.to_json
46
+ end
47
+
48
+ def get_multiple_student_assignment_scores(assignment_id)
49
+ hash_from_response get(@url_factory.assignment_score_url(assignment_id)), %w(assignment_scores assignment_score)
50
+ end
51
+
52
+ def delete_assignment_score(assignment_id, student_id)
53
+ delete @url_factory.assignment_student_score_url(assignment_id, student_id)
54
+ end
55
+
56
+ #School Resource
57
+ def get_students_by_school(school_id)
58
+ num_students = get_students_count_by_school(school_id)
59
+ results = get_with_pagination_url('student', num_students) {|page|
60
+ @url_factory.school_students_url(school_id, {
61
+ page: page,
62
+ expansions: 'contact_info,demographics'
63
+ })
64
+ }
65
+ results.map { |item| OldSchool::Student.new({school_id: school_id}.merge(item)) }
66
+ end
67
+
68
+ def get_students_count_by_school(school_id)
69
+ hash_from_response get(@url_factory.school_student_count_url(school_id)), %w(resource count)
70
+ end
71
+
72
+ def get_staff_by_school(school_id)
73
+ num_staff = get_staff_count_by_school(school_id)
74
+ results = get_with_pagination_url('staff', num_staff) {|page|
75
+ @url_factory.school_staff_index_url(school_id, {page: page})
76
+ }
77
+ results.map { |item|
78
+ attributes = {school_id: school_id}.merge(item)
79
+ OldSchool::Staff.new(attributes)
80
+ }
81
+ end
82
+
83
+ def get_staff_count_by_school(school_id)
84
+ hash_from_response get(@url_factory.school_staff_count_url(school_id)), %w(resource count)
85
+ end
86
+
87
+ def get_sections_by_school(school_id, start_year = nil)
88
+ num_sections = get_section_count_by_school(school_id, start_year)
89
+ results = get_with_pagination_url('section', num_sections) { |page|
90
+ query = {page: page}
91
+ query["q"] = "term.start_year==#{ start_year }" unless start_year.nil?
92
+ @url_factory.school_sections_url(school_id, query)
93
+ }
94
+ results.map { |item| OldSchool::Section.new({school_id: school_id}.merge(item)) }
95
+ end
96
+
97
+ def get_section_count_by_school(school_id, start_year = nil)
98
+ query = {}
99
+ query["q"] = "term.start_year==#{ start_year }" unless start_year.nil?
100
+ url = @url_factory.school_section_count_url(school_id, query)
101
+ hash_from_response get(url), %w(resource count)
102
+ end
103
+
104
+ def get_terms_by_school(school_id, start_year = nil)
105
+ num_terms = get_terms_count_by_school(school_id, start_year)
106
+ results = get_with_pagination_url('term', num_terms) do |page|
107
+ query = {page: page}
108
+ query["q"] = "start_year==#{ start_year }" unless start_year.nil?
109
+ @url_factory.school_terms_url(school_id, query)
110
+ end
111
+ results.map { |item| OldSchool::Term.new({school_id: school_id}.merge(item)) }
112
+ end
113
+
114
+ def get_terms_count_by_school(school_id, start_year = nil)
115
+ query = {}
116
+ query["q"] = "start_year==#{ start_year }" unless start_year.nil?
117
+ url = @url_factory.school_term_count_url(school_id, query)
118
+ hash_from_response get(url), %w(resource count)
119
+ end
120
+
121
+ def get_courses_by_school(school_id)
122
+ num_courses = get_course_count_by_school(school_id)
123
+ results = get_with_pagination_url('course', num_courses) {|page|
124
+ @url_factory.school_courses_url(school_id, {page: page})
125
+ }
126
+ results.map { |item| OldSchool::Course.new({school_id: school_id}.merge(item)) }
127
+ end
128
+
129
+ def get_course_count_by_school(school_id)
130
+ hash_from_response get(@url_factory.school_course_count_url(school_id)), %w(resource count)
131
+ end
132
+
133
+ def get_schools_in_current_district
134
+ num_schools = get_school_count_in_current_district
135
+ results = get_with_pagination_url('school', num_schools) {|page|
136
+ @url_factory.district_schools_url({
137
+ page: page,
138
+ })
139
+ }
140
+ results.map { |item| OldSchool::School.new(item) }
141
+ end
142
+
143
+ def get_school_count_in_current_district
144
+ hash_from_response get(@url_factory.district_school_count_url), %w(resource count)
145
+ end
146
+
147
+ def get_school_by_id(school_id)
148
+ hash_from_response get(@url_factory.school_url(school_id)), 'school'
149
+ end
150
+
151
+ def get_current_district
152
+ hash_from_response get(@url_factory.district_url), 'district'
153
+ end
154
+
155
+ #staff resources
156
+ def get_staff_by_id(staff_id)
157
+ hash_from_response get(@url_factory.staff_url(staff_id)), 'staff'
158
+ end
159
+
160
+ #student resource
161
+ def get_student_by_id(student_id)
162
+ hash_from_response get(@url_factory.student_url(student_id)), 'student'
163
+ end
164
+
165
+ #term resource
166
+ def get_term_by_id(term_id)
167
+ hash_from_response get(@url_factory.term_url(term_id)), 'term'
168
+ end
169
+
170
+ #Section Enrollment Resource
171
+ def get_section_enrollment_by_id(section_enrollment_id)
172
+ hash_from_response get(@url_factory.section_enrollment_url(section_enrollment_id)), 'section_enrollment'
173
+ end
174
+
175
+ def get_section_by_id(section_id)
176
+ hash_from_response get(@url_factory.section_url(section_id)),'section'
177
+ end
178
+
179
+ def get_section_enrollments_by_section_id(section_id)
180
+ results = collection_from_response get(@url_factory.section_section_enrollments_url(section_id)), %w(section_enrollments section_enrollment)
181
+ results.map { |item| OldSchool::SectionEnrollment.new(item) }
182
+ end
183
+
184
+ #Section Resource
185
+ def add_assignment_to_section(section_id, assignment)
186
+ url = @url_factory.section_assignments_url(section_id)
187
+ data = { assignment: hash_without_nil_values(assignment) }.to_json
188
+ response = post url, data
189
+ case response.code
190
+ when 201
191
+ assignment_id_from_response response
192
+ else
193
+ code, msg = parse_error(response)
194
+ case error["errors"]["code"]
195
+ when "MISSING_DATE_ASSIGNMENT_DUE"
196
+ raise MissingRequiredField, msg
197
+ when "ASSIGNMENT_NAME_OR_ABBREVIATION_IN_USE"
198
+ raise AlreadyExists, msg
199
+ else
200
+ raise ResponseError, msg
201
+ end
202
+ end
203
+ end
204
+
205
+ def assignment_id_from_response(response)
206
+ response.headers["Location"].split(/[\s\/]/).last
207
+ end
208
+ end
209
+ end
@@ -0,0 +1,181 @@
1
+ require 'uri'
2
+
3
+ module OldSchool
4
+ class API
5
+ class URLFactory
6
+ PTG_V2_NAMESPACE = '/powerschool-ptg-api/v2'
7
+
8
+ WS_V1_NAMESPACE = '/ws/v1'
9
+ WS_V1_RESOURCES = %w{school staff student term section_enrollment section}
10
+ WS_V1_RESOURCES_NESTED_IN_DISTRICT = %w{school}
11
+ WS_V1_RESOURCES_NESTED_IN_SCHOOL = %w{student section term course}
12
+
13
+ def initialize(host_with_protocol)
14
+ @base_uri = URI.parse(host_with_protocol)
15
+ if @base_uri.scheme.nil?
16
+ @base_uri = URI.parse('https://' + host_with_protocol)
17
+ end
18
+ end
19
+
20
+ def base_url
21
+ @base_uri.to_s
22
+ end
23
+
24
+ def access_token_url
25
+ merge_with_base '/oauth/access_token'
26
+ end
27
+
28
+ def assignment_url(id)
29
+ merge_with_base ptg_v2_resource_path('assignment', id)
30
+ end
31
+
32
+ def assignment_score_url(id)
33
+ merge_with_base assignment_score_path(id)
34
+ end
35
+
36
+ def assignment_student_score_url(assignment_id, student_id)
37
+ merge_with_base assignment_student_score_path(assignment_id, student_id)
38
+ end
39
+
40
+ def district_url
41
+ merge_with_base ws_v1_resource_path('district')
42
+ end
43
+
44
+ WS_V1_RESOURCES_NESTED_IN_DISTRICT.each do |resource|
45
+ define_method "district_#{ resource }_count_url" do |query = {}|
46
+ merge_with_base district_resource_uri(resource, 'count', query)
47
+ end
48
+
49
+ define_method "district_#{ resource }s_url" do |query = {}|
50
+ merge_with_base district_resource_uri(resource, query)
51
+ end
52
+ end
53
+
54
+ WS_V1_RESOURCES.each do |resource|
55
+ define_method "#{ resource }_url" do |id|
56
+ merge_with_base ws_v1_resource_path(resource, id)
57
+ end
58
+ end
59
+
60
+ def metadata_url
61
+ merge_with_base ws_v1_resource_path('metadata')
62
+ end
63
+
64
+ def merge_with_base(path_or_uri)
65
+ new_uri = @base_uri.dup
66
+ if String === path_or_uri
67
+ new_uri.path += path_or_uri
68
+ new_uri.path.gsub!(/\/\//, '/')
69
+ else
70
+ new_uri.path += path_or_uri.path
71
+ new_uri.query = path_or_uri.query
72
+ end
73
+ new_uri.to_s
74
+ end
75
+
76
+ def section_assignments_url(section_id)
77
+ merge_with_base section_assignments_path(section_id)
78
+ end
79
+
80
+ def section_section_enrollments_url(section_id)
81
+ merge_with_base section_section_enrollments_path(section_id)
82
+ end
83
+
84
+ def school_url(id)
85
+ merge_with_base school_resource_uri(id)
86
+ end
87
+
88
+ WS_V1_RESOURCES_NESTED_IN_SCHOOL.each do |resource|
89
+ define_method "school_#{ resource }_count_url" do |id, query = {}|
90
+ merge_with_base school_resource_uri(id, resource, 'count', query)
91
+ end
92
+
93
+ define_method "school_#{ resource }s_url" do |id, query = {}|
94
+ merge_with_base school_resource_uri(id, resource, query)
95
+ end
96
+ end
97
+
98
+ # because staffs isn't the correct pluralization and we aren't
99
+ # going to pull in active support for this pair of methods
100
+ def school_staff_count_url(id, query = {})
101
+ merge_with_base school_resource_uri(id, 'staff', 'count', query)
102
+ end
103
+
104
+ def school_staff_index_url(id, query = {})
105
+ merge_with_base school_resource_uri(id, 'staff', query)
106
+ end
107
+
108
+ private
109
+
110
+ def assignment_score_path(id)
111
+ tuple_to_path(ptg_v2_resource_path_tuple('assignment',id) + ['score'])
112
+ end
113
+
114
+ def assignment_student_path_tuple(assignment_id, student_id)
115
+ ptg_v2_resource_path_tuple('assignment',assignment_id) + resource_path_tuple('student', student_id)
116
+ end
117
+
118
+ def assignment_student_score_path_tuple(assignment_id, student_id)
119
+ assignment_student_path_tuple(assignment_id, student_id) + ['score']
120
+ end
121
+
122
+ def assignment_student_score_path(assignment_id, student_id)
123
+ tuple_to_path(assignment_student_score_path_tuple(assignment_id, student_id))
124
+ end
125
+
126
+ def district_resource_uri(*nested_resources_and_query)
127
+ query = nested_resources_and_query.pop if Hash === nested_resources_and_query.last
128
+ URI::Generic.build({}).tap {|uri|
129
+ uri.path = tuple_to_path(ws_v1_resource_path_tuple('district') + nested_resources_and_query)
130
+ uri.query = encode_query(query) unless query.nil? || query.empty?
131
+ }
132
+ end
133
+
134
+ def encode_query(enumerable)
135
+ enumerable.map{|k, v|
136
+ [k.to_s, v.to_s].join('=')
137
+ }.join('&')
138
+ end
139
+
140
+ def ptg_v2_resource_path_tuple(resource, id)
141
+ resource_path_tuple(resource, id).unshift(PTG_V2_NAMESPACE)
142
+ end
143
+
144
+ def ptg_v2_resource_path(resource, id)
145
+ tuple_to_path ptg_v2_resource_path_tuple(resource, id)
146
+ end
147
+
148
+ def resource_path_tuple(resource, id)
149
+ [resource, id].compact
150
+ end
151
+
152
+ def section_assignments_path(section_id)
153
+ tuple_to_path(ptg_v2_resource_path_tuple('section', section_id) + resource_path_tuple('assignment', nil))
154
+ end
155
+
156
+ def section_section_enrollments_path(section_id)
157
+ tuple_to_path(ws_v1_resource_path_tuple('section', section_id) + resource_path_tuple('section_enrollment', nil))
158
+ end
159
+
160
+ def school_resource_uri(id, *nested_resources_and_query)
161
+ query = nested_resources_and_query.pop if Hash === nested_resources_and_query.last
162
+ URI::Generic.build({}).tap {|uri|
163
+ uri.path = tuple_to_path(ws_v1_resource_path_tuple('school', id) + nested_resources_and_query)
164
+ uri.query = encode_query(query) unless query.nil? || query.empty?
165
+ }
166
+ end
167
+
168
+ def tuple_to_path(tuple)
169
+ tuple.join('/')
170
+ end
171
+
172
+ def ws_v1_resource_path_tuple(resource, id = nil)
173
+ resource_path_tuple(resource, id).unshift(WS_V1_NAMESPACE)
174
+ end
175
+
176
+ def ws_v1_resource_path(resource, id = nil)
177
+ tuple_to_path ws_v1_resource_path_tuple(resource, id)
178
+ end
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,198 @@
1
+ require 'typhoeus'
2
+ require 'json'
3
+
4
+ module OldSchool
5
+
6
+ class ResponseError < StandardError; end
7
+ class MissingRequiredField < ResponseError; end
8
+ class AlreadyExists < ResponseError; end
9
+
10
+ class API
11
+ module Utils
12
+ attr_accessor :id, :secret, :url_factory
13
+
14
+ def resource_metadata
15
+ @resource_metadata ||= hash_from_response(get(@url_factory.metadata_url),'metadata')
16
+ end
17
+
18
+ def token
19
+ return @token unless @token.nil?
20
+ response = Typhoeus.post(
21
+ @url_factory.access_token_url,
22
+ userpwd: "#{@id}:#{@secret}",
23
+ params: {
24
+ grant_type: 'client_credentials'
25
+ })
26
+
27
+ @token = hash_from_response(response, 'access_token')
28
+ end
29
+
30
+ def token=(token)
31
+ @token = token
32
+ end
33
+
34
+ def collection_from_response(response, keys = nil)
35
+ as_array hash_from_response!(response, keys) { [] }
36
+ end
37
+
38
+ def hash_from_response(response, keys = nil)
39
+ hash_from_response!(response, keys) do |body, key|
40
+ raise ResponseError, "\"#{key}\" key not in response object:\n #{response.body}:\nfrom original:\n#{response.inspect.to_s}" unless body.has_key?(key)
41
+ end
42
+ end
43
+
44
+ def hash_from_response!(response, keys)
45
+ keys = as_array keys
46
+ validate_response response
47
+ body = parse_json response
48
+
49
+ while keys.size > 0
50
+ key = keys.shift
51
+ if body.has_key?(key)
52
+ body = body[key]
53
+ else
54
+ return yield(body, key) if block_given?
55
+ return nil
56
+ end
57
+ end
58
+
59
+ body
60
+ end
61
+
62
+ def hash_without_nil_values(hash)
63
+ hash.to_hash.delete_if{ |k,v| v.nil? }
64
+ end
65
+
66
+ def parse_json(response)
67
+ begin
68
+ body = JSON.parse(response.body)
69
+ rescue JSON::ParserError
70
+ raise ResponseError, "Could not parse body as JSON: #{response.body}"
71
+ end
72
+ end
73
+
74
+ def parse_error(response)
75
+ error = parse_json(response)["errorMessage"]
76
+ begin
77
+ msg = "#{error["message"]} ("
78
+ msg += "#{error["errors"]["resource"]}#{error["errors"]["field"] ? "." : ""}#{error["errors"]["field"]}"
79
+ msg +=": #{error["errors"]["code"]})"
80
+ return [error["errors"]["code"], msg]
81
+ rescue
82
+ return ["ERROR", response.body]
83
+ end
84
+ end
85
+
86
+ def validate_response(response)
87
+ if response.success?
88
+ return
89
+ elsif response.timed_out?
90
+ raise ResponseError, 'HTTP request timed out'
91
+ elsif response.code == 0
92
+ raise ResponseError, response.return_message
93
+ else
94
+ reason = extract_from_response_options(response, :response_headers)
95
+ raise ResponseError, "HTTP request failed: #{response.code}:\n#{reason}"
96
+ end
97
+ end
98
+
99
+ def extract_from_response_options(response, key)
100
+ begin
101
+ return response.options[key]
102
+ rescue
103
+ return nil
104
+ end
105
+ end
106
+
107
+ def default_headers
108
+ {
109
+ Authorization: "Bearer #{token}",
110
+ Accept: 'application/JSON',
111
+ 'Content-Type'=>'application/JSON'
112
+ }
113
+ end
114
+
115
+ def get(uri)
116
+ Typhoeus.get(
117
+ make_sane_uri(uri),
118
+ headers: default_headers
119
+ )
120
+ end
121
+
122
+ def get_wait(uri)
123
+ Typhoeus::Request.new(
124
+ make_sane_uri(uri),
125
+ method: :get,
126
+ headers: default_headers)
127
+ end
128
+
129
+ def put(uri, body)
130
+ Typhoeus.put(
131
+ make_sane_uri(uri),
132
+ headers: default_headers,
133
+ body: body
134
+ )
135
+ end
136
+
137
+ def delete(uri)
138
+ Typhoeus.delete(
139
+ make_sane_uri(uri),
140
+ headers: default_headers
141
+ )
142
+ end
143
+
144
+ def post(uri, body)
145
+ Typhoeus.post(
146
+ make_sane_uri(uri),
147
+ headers: default_headers,
148
+ body: body
149
+ )
150
+ end
151
+
152
+ def make_sane_uri(uri)
153
+ if /\Ahttps?/.match(uri)
154
+ uri
155
+ else
156
+ @url_factory.merge_with_base(uri)
157
+ end
158
+ end
159
+
160
+ def get_with_pagination_url(resource, num_items, &uri_proc)
161
+ return [] if num_items == 0 #if paginating for zero items, just return an empty list
162
+
163
+ #calculate page size based on
164
+ page_size = resource_metadata["#{resource}_max_page_size"]
165
+ pages = (num_items - 1) / page_size + 1
166
+
167
+ results = get_many((1..pages), ["#{resource}s", resource], uri_proc)
168
+ results.values.flatten
169
+ end
170
+
171
+ def as_array(item)
172
+ return item.clone if item.is_a?(Array)
173
+ return [] if item.nil?
174
+ return [item]
175
+ end
176
+
177
+ def get_many(items, keys, uri_proc, complete_proc = nil)
178
+ hydra = Typhoeus::Hydra.hydra
179
+ requests = []
180
+ items.each do |item|
181
+ uri = uri_proc.call(item)
182
+ request = get_wait(uri)
183
+ request.on_complete {|response| complete_proc.call(response)} unless complete_proc.nil?
184
+ requests << [item, request]
185
+ hydra.queue request
186
+ end
187
+
188
+ hydra.run
189
+
190
+ responses = {}
191
+ requests.each do |request|
192
+ responses[request[0]] = hash_from_response(request[1].response, keys)
193
+ end
194
+ responses
195
+ end
196
+ end
197
+ end
198
+ end
@@ -0,0 +1,6 @@
1
+ require_relative 'core_ext/object'
2
+
3
+ module OldSchool
4
+ module CoreExt
5
+ end
6
+ end
@@ -0,0 +1,23 @@
1
+ require 'virtus/model'
2
+
3
+ module OldSchool
4
+ module CoreExt
5
+ module Object
6
+ def self.included(base)
7
+ base.extend ClassMethods
8
+ end
9
+
10
+ def virtus_model?
11
+ self.class.virtus_model?
12
+ end
13
+
14
+ module ClassMethods
15
+ def virtus_model?
16
+ ancestors.include?(Virtus::Model::Core)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+ Object.send(:include, OldSchool::CoreExt::Object)
@@ -0,0 +1,7 @@
1
+ require_relative 'csv/header_row'
2
+ require_relative 'csv/content_row'
3
+
4
+ module OldSchool
5
+ module CSV
6
+ end
7
+ end
@@ -0,0 +1,38 @@
1
+ module OldSchool
2
+ module CSV
3
+ class ContentRow
4
+ def self.for(model)
5
+ new(model).to_csv
6
+ end
7
+
8
+ def initialize(model)
9
+ @model = model
10
+ @public_attributes = model.publicly_readable_attributes
11
+ end
12
+
13
+ def to_csv
14
+ @public_attributes.map{|attribute|
15
+ values_for(attribute)
16
+ }.flatten
17
+ end
18
+
19
+ private
20
+
21
+ def values_for(attribute)
22
+ primitive = attribute.primitive
23
+ value = @model.public_send(attribute.name)
24
+ raise 'TODO: figure out how to handle Array and Hash attributes in CSV conversion' if Array == primitive || Hash == primitive
25
+
26
+ primitive.virtus_model? ? values_from_nested_model(value, primitive) : value
27
+ end
28
+
29
+ def values_from_nested_model(value, primitive)
30
+ if value.nil?
31
+ Array.new(primitive.publicly_readable_attributes.length)
32
+ else
33
+ ContentRow.for(value)
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,34 @@
1
+ module OldSchool
2
+ module CSV
3
+ class HeaderRow
4
+ def self.for(klass)
5
+ new(klass).to_csv
6
+ end
7
+
8
+ def initialize(klass)
9
+ @klass = klass
10
+ end
11
+
12
+ def to_csv
13
+ @klass.publicly_readable_attributes.map{|attribute|
14
+ column_headers_for(attribute)
15
+ }.flatten
16
+ end
17
+
18
+ private
19
+
20
+ def column_headers_for(attribute)
21
+ primitive = attribute.primitive
22
+ raise 'TODO: figure out how to handle Array and Hash attributes in CSV conversion' if Array == primitive || Hash == primitive
23
+
24
+ primitive.virtus_model? ? columns_for_nested_model(attribute) : attribute.name.to_s
25
+ end
26
+
27
+ def columns_for_nested_model(attribute)
28
+ HeaderRow.for(attribute.primitive).map{|nested_header|
29
+ [attribute.name, nested_header].join('.')
30
+ }
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,26 @@
1
+ module OldSchool
2
+ class Assignment
3
+
4
+ include Virtus.model
5
+
6
+ attribute :id, Integer
7
+ attribute :name, String
8
+ attribute :abbreviation, String
9
+ attribute :description, String
10
+ attribute :date_assignment_due, Date #YYYY-MM-DD
11
+ attribute :category, String
12
+ attribute :extra_credit_points, Integer
13
+ attribute :include_final_grades, Boolean
14
+ attribute :points_possible, Integer
15
+ attribute :publish_scores, Boolean
16
+ attribute :publish_state, String # PUBLISH_NEVER,
17
+ # PUBLISH_IMMEDIATELY, PUBLISH_ON_DUE_DATE,
18
+ # PUBLISH_ON_DUE_DATE, PUBLISH_ON_SPECIFIC_DATE,
19
+ # PUBLISH_DAYS_BEFORE_DUE
20
+ attribute :publish_days_before_due, Integer
21
+ attribute :publish_specific_date, Date #YYYY-MM-DD
22
+ attribute :scoring_type, String #POINTS, PERCENTAGE, LETTERGRADE
23
+ attribute :weight, Float
24
+
25
+ end
26
+ end
@@ -0,0 +1,17 @@
1
+ module OldSchool
2
+ class AssignmentScore
3
+
4
+ include Virtus.model
5
+
6
+ attribute :student_id, String
7
+ attribute :score_entered, String
8
+ attribute :points_possible, Integer
9
+ attribute :comment, String
10
+ attribute :collected, Boolean
11
+ attribute :missing, Boolean
12
+ attribute :exempt, Boolean
13
+ attribute :turned_in_late, Boolean
14
+ attribute :scoring_type, String # POINTS, PERCENTAGE, LETTERGRADE
15
+
16
+ end
17
+ end
@@ -0,0 +1,15 @@
1
+ module OldSchool
2
+ class ContactInfo
3
+
4
+ include Virtus.model
5
+
6
+ def initialize(arg, *args)
7
+ if arg != ''
8
+ super
9
+ end
10
+ end
11
+
12
+ attribute :email, String
13
+
14
+ end
15
+ end
@@ -0,0 +1,12 @@
1
+ module OldSchool
2
+ class Course
3
+
4
+ include Virtus.model
5
+
6
+ attribute :id, Integer
7
+ attribute :course_number, String
8
+ attribute :course_name, String
9
+
10
+ attribute :school_id, String
11
+ end
12
+ end
@@ -0,0 +1,13 @@
1
+ module OldSchool
2
+ class Demographics
3
+
4
+ include Virtus.model
5
+
6
+ attribute :gender, String
7
+ attribute :birth_date, DateTime
8
+ attribute :district_entry_date, DateTime
9
+ attribute :project_graduation_year, String
10
+ attribute :ssn, String
11
+
12
+ end
13
+ end
@@ -0,0 +1,11 @@
1
+ module OldSchool
2
+ class Name
3
+
4
+ include Virtus.model
5
+
6
+ attribute :last_name, String
7
+ attribute :first_name, String
8
+ attribute :middle_name, String
9
+
10
+ end
11
+ end
@@ -0,0 +1,17 @@
1
+
2
+ module OldSchool
3
+ class School
4
+
5
+ include Virtus.model
6
+
7
+ attribute :id, Integer
8
+ attribute :name, String
9
+ attribute :school_number, String
10
+ attribute :state_province_id, String
11
+ attribute :low_grade, Integer
12
+ attribute :high_grade, Integer
13
+ attribute :alternate_school_number, Integer
14
+ #attribute :assistant_principal, AssistantPrincipal
15
+
16
+ end
17
+ end
@@ -0,0 +1,14 @@
1
+ module OldSchool
2
+ class Section
3
+
4
+ include Virtus.model
5
+
6
+ attribute :id, Integer
7
+ attribute :school_id, Integer
8
+ attribute :course_id, Integer
9
+ attribute :term_id, Integer
10
+ attribute :section_number, Integer
11
+ attribute :expression, String
12
+ attribute :staff_id, Integer
13
+ end
14
+ end
@@ -0,0 +1,25 @@
1
+ require 'date'
2
+
3
+ module OldSchool
4
+ class SectionEnrollment
5
+
6
+ include Virtus.model
7
+
8
+ attribute :id, Integer
9
+ attribute :section_id, Integer
10
+ attribute :student_id, Integer
11
+ attribute :entry_date, DateTime
12
+ attribute :exit_date, DateTime
13
+ attribute :dropped, Boolean
14
+
15
+ def future?
16
+ return true if entry_date > Date.today
17
+ return false
18
+ end
19
+
20
+ def past?
21
+ return true if exit_date > Date.today
22
+ return false
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,26 @@
1
+ require_relative 'name'
2
+
3
+ module OldSchool
4
+ class Staff
5
+
6
+ include Virtus.model
7
+
8
+ attribute :id, Integer
9
+ attribute :local_id, String
10
+ attribute :admin_username, String
11
+ attribute :teacher_username, String
12
+ attribute :name, Name
13
+ #TODO attribute :phones, Phones
14
+ #TODO attribute :schedule_setup, String
15
+ #TODO attribute :school_enrollment, String
16
+
17
+ attribute :school_id, Integer
18
+ def login_id
19
+ if !self.admin_username && !self.teacher_username
20
+ return nil
21
+ end
22
+ return self.teacher_username if self.teacher_username != nil
23
+ return self.admin_username
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,27 @@
1
+ require_relative 'name'
2
+ require_relative 'contact_info'
3
+ require_relative 'demographics'
4
+
5
+ module OldSchool
6
+ class Student
7
+
8
+ include Virtus.model
9
+
10
+ attribute :id, Integer
11
+ attribute :local_id, String
12
+ attribute :state_province_id, String
13
+ attribute :student_username, String
14
+ #TODO attribute :addresses, Address
15
+ #TODO attribute :alerts, Alerts
16
+ #TODO attribute :contact, Contact
17
+ attribute :contact_info, ContactInfo # TODO figure out blank string issues
18
+ attribute :demographics, Demographics
19
+ #TODO attribute :ethnicity_race, EthnicityRace
20
+ attribute :name, Name
21
+ #TODO attribute :phones, Phones
22
+ #TODO attribute :schedule_setup, String
23
+ #TODO attribute :school_enrollment, String
24
+
25
+ attribute :school_id, Integer
26
+ end
27
+ end
@@ -0,0 +1,15 @@
1
+ module OldSchool
2
+ class Term
3
+
4
+ include Virtus.model
5
+
6
+ attribute :id, Integer
7
+ attribute :school_id, Integer
8
+ attribute :start_year, Integer
9
+ attribute :portion, Integer
10
+ attribute :start_date, DateTime
11
+ attribute :end_date, DateTime
12
+ attribute :abbreviation, String
13
+ attribute :name, String
14
+ end
15
+ end
@@ -0,0 +1,3 @@
1
+ module OldSchool
2
+ VERSION = '0.0.5'
3
+ end
@@ -0,0 +1,7 @@
1
+ require_relative 'virtus_ext/class_methods'
2
+ require_relative 'virtus_ext/model'
3
+
4
+ module OldSchool
5
+ module VirtusExt
6
+ end
7
+ end
@@ -0,0 +1,18 @@
1
+ require 'virtus'
2
+ require 'old_school/csv'
3
+
4
+ module OldSchool
5
+ module VirtusExt
6
+ module ClassMethods
7
+ def publicly_readable_attributes
8
+ @attribute_set.select(&:public_reader?)
9
+ end
10
+
11
+ def csv_header
12
+ OldSchool::CSV::HeaderRow.for(self)
13
+ end
14
+ end
15
+ end
16
+ end
17
+
18
+ Virtus::ClassMethods.send(:include, OldSchool::VirtusExt::ClassMethods)
@@ -0,0 +1,20 @@
1
+ require 'virtus'
2
+ require 'old_school/csv'
3
+
4
+ module OldSchool
5
+ module VirtusExt
6
+ module Model
7
+ module Core
8
+ def publicly_readable_attributes
9
+ self.class.publicly_readable_attributes
10
+ end
11
+
12
+ def to_csv
13
+ OldSchool::CSV::ContentRow.for(self)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+
20
+ Virtus::Model::Core.send(:include, OldSchool::VirtusExt::Model::Core)
@@ -0,0 +1,27 @@
1
+ require File.expand_path('../lib/old_school/version', __FILE__)
2
+
3
+ Gem::Specification.new do |gem|
4
+ gem.name = 'old_school'
5
+ gem.date = '2013-01-10'
6
+ gem.summary = 'Ruby Gem for a Powerful SIS'
7
+ gem.description = 'Provides an interface to work with a Powerful SIS REST API'
8
+ gem.authors = ['kaiged']
9
+ gem.email = 'kaiged@gmail.com'
10
+ gem.homepage = 'https://github.com/kaiged/old_school'
11
+
12
+ gem.version = OldSchool::VERSION
13
+
14
+ gem.files = %w[old_school.gemspec README.md]
15
+ gem.files += Dir.glob("lib/**/*")
16
+
17
+ gem.require_paths = ["lib"]
18
+
19
+ gem.add_dependency 'typhoeus', '>= 0.6.7'
20
+ gem.add_dependency 'json', '>=1.8.1'
21
+ gem.add_dependency 'virtus'
22
+
23
+ gem.add_development_dependency 'debugger'
24
+ gem.add_development_dependency 'rspec'
25
+ gem.add_development_dependency 'guard'
26
+ gem.add_development_dependency 'guard-rspec'
27
+ end
metadata ADDED
@@ -0,0 +1,184 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: old_school
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.5
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - kaiged
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-01-10 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: typhoeus
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: 0.6.7
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: 0.6.7
30
+ - !ruby/object:Gem::Dependency
31
+ name: json
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: 1.8.1
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: 1.8.1
46
+ - !ruby/object:Gem::Dependency
47
+ name: virtus
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: debugger
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: rspec
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ - !ruby/object:Gem::Dependency
95
+ name: guard
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ - !ruby/object:Gem::Dependency
111
+ name: guard-rspec
112
+ requirement: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ! '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ! '>='
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ description: Provides an interface to work with a Powerful SIS REST API
127
+ email: kaiged@gmail.com
128
+ executables: []
129
+ extensions: []
130
+ extra_rdoc_files: []
131
+ files:
132
+ - old_school.gemspec
133
+ - README.md
134
+ - lib/old_school/api/url_factory.rb
135
+ - lib/old_school/api/utils.rb
136
+ - lib/old_school/api.rb
137
+ - lib/old_school/core_ext/object.rb
138
+ - lib/old_school/core_ext.rb
139
+ - lib/old_school/csv/content_row.rb
140
+ - lib/old_school/csv/header_row.rb
141
+ - lib/old_school/csv.rb
142
+ - lib/old_school/models/assignment.rb
143
+ - lib/old_school/models/assignment_score.rb
144
+ - lib/old_school/models/contact_info.rb
145
+ - lib/old_school/models/course.rb
146
+ - lib/old_school/models/demographics.rb
147
+ - lib/old_school/models/name.rb
148
+ - lib/old_school/models/school.rb
149
+ - lib/old_school/models/section.rb
150
+ - lib/old_school/models/section_enrollment.rb
151
+ - lib/old_school/models/staff.rb
152
+ - lib/old_school/models/student.rb
153
+ - lib/old_school/models/term.rb
154
+ - lib/old_school/version.rb
155
+ - lib/old_school/virtus_ext/class_methods.rb
156
+ - lib/old_school/virtus_ext/model.rb
157
+ - lib/old_school/virtus_ext.rb
158
+ - lib/old_school.rb
159
+ homepage: https://github.com/kaiged/old_school
160
+ licenses: []
161
+ post_install_message:
162
+ rdoc_options: []
163
+ require_paths:
164
+ - lib
165
+ required_ruby_version: !ruby/object:Gem::Requirement
166
+ none: false
167
+ requirements:
168
+ - - ! '>='
169
+ - !ruby/object:Gem::Version
170
+ version: '0'
171
+ required_rubygems_version: !ruby/object:Gem::Requirement
172
+ none: false
173
+ requirements:
174
+ - - ! '>='
175
+ - !ruby/object:Gem::Version
176
+ version: '0'
177
+ requirements: []
178
+ rubyforge_project:
179
+ rubygems_version: 1.8.23
180
+ signing_key:
181
+ specification_version: 3
182
+ summary: Ruby Gem for a Powerful SIS
183
+ test_files: []
184
+ has_rdoc: