old_school 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
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: