stellar 0.1.1 → 0.2.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.
- data/Gemfile +4 -0
- data/Gemfile.lock +11 -10
- data/Rakefile +3 -0
- data/VERSION +1 -1
- data/lib/stellar.rb +2 -0
- data/lib/stellar/auth.rb +21 -32
- data/lib/stellar/client.rb +7 -4
- data/lib/stellar/courses.rb +5 -0
- data/lib/stellar/gradebook.rb +354 -0
- data/lib/stellar/homework.rb +17 -4
- data/spec/stellar/client_spec.rb +3 -1
- data/spec/stellar/gradebook_spec.rb +100 -0
- data/spec/stellar/gradebook_student_spec.rb +105 -0
- data/stellar.gemspec +8 -2
- metadata +41 -27
data/Gemfile
CHANGED
@@ -4,6 +4,7 @@ source :rubygems
|
|
4
4
|
gem 'mechanize', '>= 2.0.2'
|
5
5
|
gem 'nokogiri', '>= 1.5.0'
|
6
6
|
|
7
|
+
|
7
8
|
# Add dependencies to develop your gem here.
|
8
9
|
# Include everything needed to run rake, tests, features, etc.
|
9
10
|
group :development do
|
@@ -18,4 +19,7 @@ group :development do
|
|
18
19
|
gem 'ruby-debug19', :platform => :mri_19
|
19
20
|
|
20
21
|
gem 'highline', '>= 1.6.2'
|
22
|
+
|
23
|
+
# MRI 1.9 has JSON in the standard library.
|
24
|
+
gem 'json', :platform => :mri_18
|
21
25
|
end
|
data/Gemfile.lock
CHANGED
@@ -23,19 +23,19 @@ GEM
|
|
23
23
|
net-http-digest_auth (1.1.1)
|
24
24
|
net-http-persistent (1.9)
|
25
25
|
nokogiri (1.5.0)
|
26
|
-
rake (0.9.2)
|
26
|
+
rake (0.9.2.2)
|
27
27
|
rbx-require-relative (0.0.5)
|
28
28
|
rcov (0.9.11)
|
29
|
-
rdoc (3.
|
29
|
+
rdoc (3.11)
|
30
30
|
json (~> 1.4)
|
31
|
-
rspec (2.
|
32
|
-
rspec-core (~> 2.
|
33
|
-
rspec-expectations (~> 2.
|
34
|
-
rspec-mocks (~> 2.
|
35
|
-
rspec-core (2.
|
36
|
-
rspec-expectations (2.
|
31
|
+
rspec (2.7.0)
|
32
|
+
rspec-core (~> 2.7.0)
|
33
|
+
rspec-expectations (~> 2.7.0)
|
34
|
+
rspec-mocks (~> 2.7.0)
|
35
|
+
rspec-core (2.7.1)
|
36
|
+
rspec-expectations (2.7.0)
|
37
37
|
diff-lcs (~> 1.1.2)
|
38
|
-
rspec-mocks (2.
|
38
|
+
rspec-mocks (2.7.0)
|
39
39
|
ruby-debug (0.10.4)
|
40
40
|
columnize (>= 0.1)
|
41
41
|
ruby-debug-base (~> 0.10.4.0)
|
@@ -53,7 +53,7 @@ GEM
|
|
53
53
|
archive-tar-minitar (>= 0.5.2)
|
54
54
|
webrobots (0.0.12)
|
55
55
|
nokogiri (>= 1.4.4)
|
56
|
-
yard (0.7.
|
56
|
+
yard (0.7.3)
|
57
57
|
yard-rspec (0.1)
|
58
58
|
yard
|
59
59
|
|
@@ -64,6 +64,7 @@ DEPENDENCIES
|
|
64
64
|
bundler (>= 1.0.21)
|
65
65
|
highline (>= 1.6.2)
|
66
66
|
jeweler (>= 1.6.4)
|
67
|
+
json
|
67
68
|
mechanize (>= 2.0.2)
|
68
69
|
nokogiri (>= 1.5.0)
|
69
70
|
rcov
|
data/Rakefile
CHANGED
@@ -51,6 +51,9 @@ task :spec => :fixtures
|
|
51
51
|
|
52
52
|
krb_file = 'spec/fixtures/kerberos.b64'
|
53
53
|
file krb_file do
|
54
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), 'lib'))
|
55
|
+
require 'stellar'
|
56
|
+
|
54
57
|
kerberos = {}
|
55
58
|
kerberos[:user] = ask('MIT Kerberos Username: ') { |q| q.echo = true }
|
56
59
|
kerberos[:pass] = ask('MIT Kerberos Password: ') { |q| q.echo = '*' }
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.2.0
|
data/lib/stellar.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
# Standard library.
|
2
|
+
require 'json'
|
2
3
|
require 'openssl'
|
3
4
|
require 'uri'
|
4
5
|
|
@@ -20,4 +21,5 @@ end # namespace Stellar
|
|
20
21
|
require 'stellar/auth.rb'
|
21
22
|
require 'stellar/client.rb'
|
22
23
|
require 'stellar/courses.rb'
|
24
|
+
require 'stellar/gradebook.rb'
|
23
25
|
require 'stellar/homework.rb'
|
data/lib/stellar/auth.rb
CHANGED
@@ -18,33 +18,24 @@ module Auth
|
|
18
18
|
# with :user and :pass keys
|
19
19
|
# @return [Stellar::Client] self, for convenient method chaining
|
20
20
|
def auth(options = {})
|
21
|
-
#
|
22
|
-
|
21
|
+
# Create new Mechanize instance to drop any old credentials.
|
22
|
+
|
23
23
|
if options[:cert]
|
24
|
-
log = Logger.new(STDERR)
|
25
|
-
log.level = Logger::INFO
|
26
|
-
@mech.log = log
|
27
|
-
|
28
24
|
key = options[:cert][:key]
|
29
|
-
if key.respond_to?(:to_str)
|
30
|
-
|
31
|
-
@mech.key = key
|
32
|
-
else
|
33
|
-
@mech.key = OpenSSL::PKey::RSA.new key
|
34
|
-
end
|
35
|
-
else
|
36
|
-
@mech.key = key
|
25
|
+
if key.respond_to?(:to_str) && !File.exist?(key)
|
26
|
+
key = OpenSSL::PKey::RSA.new key
|
37
27
|
end
|
38
28
|
cert = options[:cert][:cert]
|
39
|
-
if cert.respond_to?(:to_str)
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
@mech.cert = cert
|
29
|
+
if cert.respond_to?(:to_str) && !File.exist?(cert)
|
30
|
+
cert = OpenSSL::X509::Certificate.new cert
|
31
|
+
end
|
32
|
+
|
33
|
+
@mech = mech do |m|
|
34
|
+
m.key = key
|
35
|
+
m.cert = cert
|
47
36
|
end
|
37
|
+
else
|
38
|
+
@mech = mech
|
48
39
|
end
|
49
40
|
|
50
41
|
# Go to a page that is guaranteed to redirect to shitoleth.
|
@@ -52,28 +43,25 @@ module Auth
|
|
52
43
|
# Fill in the form.
|
53
44
|
step1_form = step1_page.form_with :action => /WAYF/
|
54
45
|
step1_form.checkbox_with(:name => /perm/).checked = :checked
|
55
|
-
step2_page = step1_form.submit
|
46
|
+
step2_page = step1_form.submit step1_form.buttons.first
|
56
47
|
# Click through the stupid confirmation form.
|
57
48
|
step2_form = step2_page.form_with :action => /WAYF/
|
58
|
-
cred_page = step2_form.submit
|
49
|
+
cred_page = step2_form.submit step2_form.button_with(:name => /select/i)
|
59
50
|
|
60
51
|
# Fill in the credentials form.
|
61
52
|
if options[:cert]
|
62
53
|
cred_form = cred_page.form_with :action => /certificate/i
|
63
54
|
cred_form.checkbox_with(:name => /pref/).checked = :checked
|
64
|
-
puts cred_form
|
65
|
-
puts cred_form.submit(cred_form.buttons.first).body
|
66
|
-
return
|
67
55
|
elsif options[:kerberos]
|
68
56
|
cred_form = cred_page.form_with :action => /username/i
|
69
57
|
cred_form.field_with(:name => /user/).value = options[:kerberos][:user]
|
70
58
|
cred_form.field_with(:name => /pass/).value = options[:kerberos][:pass]
|
71
59
|
else
|
72
|
-
raise 'Unsupported credentials'
|
60
|
+
raise ArgumentError, 'Unsupported credentials'
|
73
61
|
end
|
74
62
|
|
75
63
|
# Click through the SAML response form.
|
76
|
-
saml_page = cred_form.submit
|
64
|
+
saml_page = cred_form.submit cred_form.buttons.first
|
77
65
|
unless saml_form = saml_page.form_with(:action => /SAML/)
|
78
66
|
raise ArgumentError, 'Authentication failed due to invalid credentials'
|
79
67
|
end
|
@@ -101,9 +89,10 @@ module Auth
|
|
101
89
|
# @return [Hash] a Hash with a :cert key (the OpenSSL::X509::Certificate)
|
102
90
|
# and a :key key (the matching OpenSSL::PKey::PKey private key)
|
103
91
|
def get_certificate(kerberos)
|
104
|
-
mech = Mechanize.new
|
105
|
-
|
106
|
-
|
92
|
+
mech = Mechanize.new do |m|
|
93
|
+
m.user_agent_alias = 'Linux Firefox'
|
94
|
+
# NOTE: ca.mit.edu uses a Geotrust certificate, not the self-signed one
|
95
|
+
end
|
107
96
|
login_page = mech.get 'https://ca.mit.edu/ca/'
|
108
97
|
login_form = login_page.form_with :action => /login/
|
109
98
|
login_form.field_with(:name => /login/).value = kerberos[:user]
|
data/lib/stellar/client.rb
CHANGED
@@ -15,10 +15,13 @@ class Client
|
|
15
15
|
end
|
16
16
|
|
17
17
|
# New Mechanize instance.
|
18
|
-
def mech
|
19
|
-
m = Mechanize.new
|
20
|
-
|
21
|
-
|
18
|
+
def mech(&block)
|
19
|
+
m = Mechanize.new do |m|
|
20
|
+
# m.ca_file = mitca_path
|
21
|
+
m.user_agent_alias = 'Linux Firefox'
|
22
|
+
yield m if block
|
23
|
+
m
|
24
|
+
end
|
22
25
|
m
|
23
26
|
end
|
24
27
|
|
data/lib/stellar/courses.rb
CHANGED
@@ -107,6 +107,11 @@ class Course
|
|
107
107
|
def homework
|
108
108
|
@homework ||= Stellar::HomeworkList.new self
|
109
109
|
end
|
110
|
+
|
111
|
+
# Client scoped to the course's Gradebook module.
|
112
|
+
def gradebook
|
113
|
+
@gradebook ||= Stellar::Gradebook.new self
|
114
|
+
end
|
110
115
|
end # class Stellar::Course
|
111
116
|
|
112
117
|
end # namespace Stellar
|
@@ -0,0 +1,354 @@
|
|
1
|
+
# :nodoc: namespace
|
2
|
+
module Stellar
|
3
|
+
|
4
|
+
# Stellar client scoped to a course's Gradebook module.
|
5
|
+
class Gradebook
|
6
|
+
# Maps the text in navigation links to URI objects.
|
7
|
+
#
|
8
|
+
# Example: navigation['Homework'] => <# URI: .../ >
|
9
|
+
attr_reader :navigation
|
10
|
+
|
11
|
+
# The course whose Gradebook is exposed by this client.
|
12
|
+
attr_reader :course
|
13
|
+
|
14
|
+
# Generic Stellar client used to make requests.
|
15
|
+
attr_reader :client
|
16
|
+
|
17
|
+
# Creates a Stellar client scoped to a course's Gradebook module.
|
18
|
+
#
|
19
|
+
# @param [Stellar::Course] the course whose gradebook is desired
|
20
|
+
def initialize(course)
|
21
|
+
@course = course
|
22
|
+
@client = course.client
|
23
|
+
@url = course.navigation['Gradebook']
|
24
|
+
|
25
|
+
page = @client.get_nokogiri @url
|
26
|
+
@navigation = Hash[page.css('#toolBox.dashboard dd a').map { |link|
|
27
|
+
[link.inner_text, URI.join(page.url, link['href'])]
|
28
|
+
}]
|
29
|
+
end
|
30
|
+
|
31
|
+
# All assignments in this course's Gradebook module.
|
32
|
+
# @return [Stellar::Gradebook::AssignmentList] list of assignments in this
|
33
|
+
# gradebook
|
34
|
+
def assignments
|
35
|
+
@assignments ||= Stellar::Gradebook::AssignmentList.new self
|
36
|
+
end
|
37
|
+
|
38
|
+
# All students in this course's Gradebook module.
|
39
|
+
# @return [Stellar::Gradebook::StudentList] list of students in this gradebook
|
40
|
+
def students
|
41
|
+
@students ||= Stellar::Gradebook::StudentList.new self
|
42
|
+
end
|
43
|
+
|
44
|
+
# Collection of assignments in a course's Gradebook module.
|
45
|
+
class AssignmentList
|
46
|
+
# The course's
|
47
|
+
attr_reader :gradebook
|
48
|
+
|
49
|
+
# Generic Stellar client used to make requests.
|
50
|
+
attr_reader :client
|
51
|
+
|
52
|
+
# Creates a list of Gradebook assignments for a class.
|
53
|
+
#
|
54
|
+
# @param [Stellar::Gradebook] gradebook client scoped to a course's Gradebook
|
55
|
+
def initialize(gradebook)
|
56
|
+
@gradebook = gradebook
|
57
|
+
@client = gradebook.client
|
58
|
+
|
59
|
+
@url = gradebook.navigation['Assignments']
|
60
|
+
reload!
|
61
|
+
end
|
62
|
+
|
63
|
+
# All assignments in this course's Gradebook module.
|
64
|
+
# @return [Array<Stellar::Gradebook::Assignment>] list of assignments posted
|
65
|
+
# by this course
|
66
|
+
def all
|
67
|
+
@assignments
|
68
|
+
end
|
69
|
+
|
70
|
+
# An assignment in the course's homework module.
|
71
|
+
# @param [String] name the name of the desired assignment
|
72
|
+
# @return [Stellar::Homework] an assignment with the given name, or nil if no
|
73
|
+
# such assignment exists
|
74
|
+
def named(name)
|
75
|
+
@assignments.find { |a| a.name == name }
|
76
|
+
end
|
77
|
+
|
78
|
+
# Reloads the contents of this assignment list.
|
79
|
+
#
|
80
|
+
# @return [Stellar::Gradebook::AssignmentList] self, for easy call chaining
|
81
|
+
def reload!
|
82
|
+
assignment_page = @client.get_nokogiri @url
|
83
|
+
|
84
|
+
@assignments = assignment_page.css('.gradeTable tbody tr').map { |tr|
|
85
|
+
begin
|
86
|
+
Stellar::Gradebook::Assignment.new tr, self
|
87
|
+
rescue ArgumentError
|
88
|
+
nil
|
89
|
+
end
|
90
|
+
}.reject(&:nil?)
|
91
|
+
|
92
|
+
self
|
93
|
+
end
|
94
|
+
|
95
|
+
# Creates a new assignment in the Gradebook.
|
96
|
+
#
|
97
|
+
# @param [String] long_name the assignment's full name
|
98
|
+
# @param [String] short_name a shorter name?
|
99
|
+
# @param [Numeric] max_score maximum score that a student can receive
|
100
|
+
# @param [Time] due_on date when the pset is due
|
101
|
+
# @param [Numeric] weight score weight in total score for the course
|
102
|
+
# @return [Stellar::Gradebook::AssignmentList] self, for easy call chaining
|
103
|
+
def add(long_name, short_name, max_points, due_on = Time.now, weight = nil)
|
104
|
+
add_page = @client.get @gradebook.navigation['Add Assignment']
|
105
|
+
add_form = add_page.form_with :action => /new/i
|
106
|
+
add_form.field_with(:name => /title/i).value = long_name
|
107
|
+
add_form.field_with(:name => /short/i).value = short_name
|
108
|
+
add_form.field_with(:name => /points/i).value = max_points.to_s
|
109
|
+
add_form.field_with(:name => /short/i).value = due_on.strftime('%m.%d.%Y')
|
110
|
+
if weight
|
111
|
+
add_form.field_with(:name => /weight/i).value = weight.to_s
|
112
|
+
end
|
113
|
+
add_form.submit add_form.button_with(:class => /active/)
|
114
|
+
|
115
|
+
reload!
|
116
|
+
end
|
117
|
+
end # class Stellar::Gradebook::AssignmentList
|
118
|
+
|
119
|
+
# One assignment in the Gradebook tab.
|
120
|
+
class Assignment
|
121
|
+
# Assignment name.
|
122
|
+
attr_reader :name
|
123
|
+
|
124
|
+
# URL of the assignment's main page.
|
125
|
+
attr_reader :url
|
126
|
+
|
127
|
+
# True if the homework was deleted.
|
128
|
+
attr_reader :deleted
|
129
|
+
# True if the homework was deleted.
|
130
|
+
alias_method :deleted?, :deleted
|
131
|
+
|
132
|
+
# The gradebook that this assignment is in.
|
133
|
+
attr_reader :gradebook
|
134
|
+
|
135
|
+
# Generic Stellar client used to make requests.
|
136
|
+
attr_reader :client
|
137
|
+
|
138
|
+
# Creates a submission from a <tr> element in the Gradebook assignments page.
|
139
|
+
#
|
140
|
+
# @param [Nokogiri::XML::Element] tr a <tr> element in the Gradebook
|
141
|
+
# assignments page describing this assignment
|
142
|
+
# @param [Stellar::Gradebook] gradebook Stellar client scoped to the
|
143
|
+
# course gradebook containing this assignment
|
144
|
+
def initialize(tr, gradebook)
|
145
|
+
@gradebook = gradebook
|
146
|
+
@client = gradebook.client
|
147
|
+
|
148
|
+
link = tr.css('a[href*=".html"]').find { |link| link.css('img').empty? }
|
149
|
+
raise ArgumentError, 'Invalid assignment-listing <tr>' unless link
|
150
|
+
@name = link.inner_text
|
151
|
+
@url = URI.join tr.document.url, link['href']
|
152
|
+
|
153
|
+
page = client.get_nokogiri @url
|
154
|
+
summary_table = page.css('.gradeTable').find do |table|
|
155
|
+
/summary/i =~ table.css('caption').inner_text
|
156
|
+
end
|
157
|
+
raise ArgumentError, 'Invalid assignment-listing <tr>' unless summary_table
|
158
|
+
|
159
|
+
edit_link = summary_table.css('tbody tr a[href*="edit"]').first
|
160
|
+
raise ArgumentError, 'Invalid assignment-listing <tr>' unless edit_link
|
161
|
+
@edit_url = URI.join @url.to_s, edit_link['href']
|
162
|
+
|
163
|
+
@deleted = false
|
164
|
+
end
|
165
|
+
|
166
|
+
# Deletes this assignment from the Gradebook.
|
167
|
+
def delete!
|
168
|
+
return if @deleted
|
169
|
+
|
170
|
+
edit_page = @client.get @edit_url
|
171
|
+
edit_form = edit_page.form_with :action => /edit/i
|
172
|
+
confirm_page = edit_form.submit edit_form.button_with(:name => /del/i)
|
173
|
+
|
174
|
+
@deleted = true
|
175
|
+
self
|
176
|
+
end
|
177
|
+
end # class Stellar::Gradebook::Assignment
|
178
|
+
|
179
|
+
# Collection of students in a course's Gradebook module.
|
180
|
+
class StudentList
|
181
|
+
# The course's
|
182
|
+
attr_reader :gradebook
|
183
|
+
|
184
|
+
# Generic Stellar client used to make requests.
|
185
|
+
attr_reader :client
|
186
|
+
|
187
|
+
# Creates a list of students in a class' Gradebook.
|
188
|
+
#
|
189
|
+
# @param [Stellar::Gradebook] gradebook client scoped to a course's Gradebook
|
190
|
+
def initialize(gradebook)
|
191
|
+
@gradebook = gradebook
|
192
|
+
@client = gradebook.client
|
193
|
+
|
194
|
+
@url = gradebook.navigation['Students']
|
195
|
+
reload!
|
196
|
+
end
|
197
|
+
|
198
|
+
# All students listed in this course's Gradebook module.
|
199
|
+
# @return [Array<Stellar::Gradebook::Student>] list of students in this course
|
200
|
+
def all
|
201
|
+
@students
|
202
|
+
end
|
203
|
+
|
204
|
+
# A student in the course's Gradebook module.
|
205
|
+
# @param [String] name the name of the desired student
|
206
|
+
# @return [Stellar::Gradebook::Student] a student with the given name, or nil
|
207
|
+
# if no such student exists
|
208
|
+
def named(name)
|
209
|
+
@students.find { |a| a.name == name }
|
210
|
+
end
|
211
|
+
|
212
|
+
# A student in the course's Gradebook module.
|
213
|
+
# @param [String] email the e-mail of the desired student
|
214
|
+
# @return [Stellar::Gradebook::Student] a student with the given e-mail
|
215
|
+
# address, or nil if no such student exists
|
216
|
+
def with_email(email)
|
217
|
+
@students.find { |a| a.email == email }
|
218
|
+
end
|
219
|
+
|
220
|
+
# Reloads the contents of this student list.
|
221
|
+
#
|
222
|
+
# @return [Stellar::Gradebook::StudentList] self, for easy call chaining
|
223
|
+
def reload!
|
224
|
+
student_page = @client.get_nokogiri @url
|
225
|
+
|
226
|
+
data_script = student_page.css('script').find do |script|
|
227
|
+
/var rows\s*\=.*\;/m =~ script.inner_text
|
228
|
+
end
|
229
|
+
data = JSON.parse((/var rows\s*\=([^;]*)\;/m).
|
230
|
+
match(data_script.inner_text)[1].gsub("'", '"'))
|
231
|
+
|
232
|
+
@students = data.map { |student_line|
|
233
|
+
email = student_line[0]
|
234
|
+
url = URI.join @url.to_s, student_line[1]
|
235
|
+
name = student_line[2].split(',', 2).map(&:strip).reverse.join(' ')
|
236
|
+
|
237
|
+
begin
|
238
|
+
Stellar::Gradebook::Student.new url, email, name, gradebook
|
239
|
+
rescue ArgumentError
|
240
|
+
nil
|
241
|
+
end
|
242
|
+
}.reject(&:nil?)
|
243
|
+
|
244
|
+
self
|
245
|
+
end
|
246
|
+
end # class Stellar::Gradebook::StudentsList
|
247
|
+
|
248
|
+
# Stellar client scoped to a student's Gradebook page.
|
249
|
+
class Student
|
250
|
+
# The student's full name.
|
251
|
+
attr_reader :name
|
252
|
+
|
253
|
+
# The student's e-mail.
|
254
|
+
attr_reader :email
|
255
|
+
|
256
|
+
# URL of the student's page of grades in the Gradebook.
|
257
|
+
attr_reader :url
|
258
|
+
|
259
|
+
# The course Gradebook that this student entry belongs to.
|
260
|
+
attr_reader :gradebook
|
261
|
+
|
262
|
+
# Generic Stellar client used to make requests.
|
263
|
+
attr_reader :client
|
264
|
+
|
265
|
+
# Creates a Stellar client scoped to a student's Gradebook page.
|
266
|
+
#
|
267
|
+
# @param [URI] url URL to the student's grade page
|
268
|
+
# @param [Stellar::Gradebook] gradebook Stellar client scoped to the
|
269
|
+
# course gradebook containing this assignment
|
270
|
+
def initialize(url, email, name, gradebook)
|
271
|
+
@url = url
|
272
|
+
@email = email
|
273
|
+
@name = name
|
274
|
+
@gradebook = gradebook
|
275
|
+
@client = gradebook.client
|
276
|
+
|
277
|
+
@grades = nil
|
278
|
+
@input_names = nil
|
279
|
+
@comment = nil
|
280
|
+
end
|
281
|
+
|
282
|
+
# The student's grades for all assignments.
|
283
|
+
#
|
284
|
+
# @return [Hash] map between assignment names and the student's scores
|
285
|
+
def grades
|
286
|
+
reload! unless @grades
|
287
|
+
@grades
|
288
|
+
end
|
289
|
+
|
290
|
+
# The instructor's comment for the student.
|
291
|
+
#
|
292
|
+
# @return [String] the content of the comment
|
293
|
+
def comment
|
294
|
+
reload! unless @comment
|
295
|
+
@comment
|
296
|
+
end
|
297
|
+
|
298
|
+
# Reloads the information in the student's grades page.
|
299
|
+
#
|
300
|
+
# @return [Stellar::Gradebook::Student] self, for easy call chaining
|
301
|
+
def reload!
|
302
|
+
page = @client.get_nokogiri @url
|
303
|
+
|
304
|
+
@grades = {}
|
305
|
+
@input_names = {}
|
306
|
+
page.css('.gradeTable tbody tr').each do |tr|
|
307
|
+
name = tr.css('a[href*="assignment"]').inner_text
|
308
|
+
input_field = tr.css('input[type="text"][name*="oints"]').first
|
309
|
+
@input_names[name] = input_field['name']
|
310
|
+
@grades[name] = input_field['value'].empty? ? nil :
|
311
|
+
input_field['value'].to_f
|
312
|
+
end
|
313
|
+
@comment = page.css('textarea[name*="comment"]').inner_text
|
314
|
+
|
315
|
+
self
|
316
|
+
end
|
317
|
+
|
318
|
+
# Changes some of the student's grades.
|
319
|
+
#
|
320
|
+
# @param [Hash] new_grades maps assignment names to the desired scores
|
321
|
+
# @return [Stellar::Gradebook::Student] self, for easy call chaining
|
322
|
+
def update_grades(new_grades)
|
323
|
+
reload! unless @input_names
|
324
|
+
|
325
|
+
page = @client.get @url
|
326
|
+
grade_form = page.form_with :action => /detail/i
|
327
|
+
new_grades.each do |assignment_name, new_grade|
|
328
|
+
unless input_name = @input_names[assignment_name]
|
329
|
+
raise ArgumentError, "Invalid assignment #{assignment_name}"
|
330
|
+
end
|
331
|
+
grade_form.field_with(:name => input_name).value = new_grade.to_s
|
332
|
+
end
|
333
|
+
grade_form.submit grade_form.button_with(:class => /save/)
|
334
|
+
|
335
|
+
reload!
|
336
|
+
end
|
337
|
+
|
338
|
+
# Changes the comment on the student's grades page.
|
339
|
+
#
|
340
|
+
# @param [String] text the new comment text
|
341
|
+
# @return [Stellar::Gradebook::Student] self, for easy call chaining
|
342
|
+
def update_comment(text)
|
343
|
+
page = @client.get @url
|
344
|
+
grade_form = page.form_with :action => /detail/i
|
345
|
+
grade_form.field_with(:name => /comment/i).value = text
|
346
|
+
grade_form.submit grade_form.button_with(:class => /save/)
|
347
|
+
|
348
|
+
reload!
|
349
|
+
end
|
350
|
+
end # class Stellar::Gradebook::Assignment::Student
|
351
|
+
|
352
|
+
end # class Stellar::Gradebook
|
353
|
+
|
354
|
+
end # namespace Stellar
|
data/lib/stellar/homework.rb
CHANGED
@@ -29,8 +29,10 @@ class HomeworkList
|
|
29
29
|
@assignments
|
30
30
|
end
|
31
31
|
|
32
|
-
#
|
33
|
-
# @
|
32
|
+
# An assignment in the course's homework module.
|
33
|
+
# @param [String] name the name of the desired assignment
|
34
|
+
# @return [Stellar::Homework] an assignment with the given name, or nil if no
|
35
|
+
# such assignment exists
|
34
36
|
def named(name)
|
35
37
|
@assignments.find { |a| a.name == name }
|
36
38
|
end
|
@@ -98,9 +100,14 @@ class Submission
|
|
98
100
|
|
99
101
|
# Creates a submission from a <tr> element in the Stellar homework page.
|
100
102
|
#
|
101
|
-
# @param [Nokogiri::XML::Element]
|
103
|
+
# @param [Nokogiri::XML::Element] tr a <tr> element in the Stellar homework
|
104
|
+
# page describing this submission
|
105
|
+
# @param [Stellar::Homework] homework Stellar client scoped to the assignment
|
106
|
+
# that this submission is for
|
102
107
|
def initialize(tr, homework)
|
103
|
-
link = tr.css('a').find
|
108
|
+
link = tr.css('a').find do |link|
|
109
|
+
(/^\s*\d+\s*$/ =~ link.inner_text) && !(/grade/ =~ link['href'])
|
110
|
+
end
|
104
111
|
raise ArgumentError, 'Invalid submission-listing <tr>' unless link
|
105
112
|
|
106
113
|
@url = URI.join tr.document.url, link['href']
|
@@ -185,6 +192,12 @@ class Comment
|
|
185
192
|
# Generic Stellar client used to make requests.
|
186
193
|
attr_reader :client
|
187
194
|
|
195
|
+
# Creates a comment from a <table> in a Stellar submission details page.
|
196
|
+
#
|
197
|
+
# @param [Nokogiri::XML::Element] table a <table> element in a Stellar
|
198
|
+
# submission details showing this comment
|
199
|
+
# @param [Stellar::Homework::Submission] submission Stellar client scoped to
|
200
|
+
# the submission that was commented on
|
188
201
|
def initialize(table, submission)
|
189
202
|
@submission = submission
|
190
203
|
@client = @submission.client
|
data/spec/stellar/client_spec.rb
CHANGED
@@ -36,7 +36,9 @@ describe Stellar::Client do
|
|
36
36
|
|
37
37
|
describe 'with bad Kerberos credentials' do
|
38
38
|
it 'should raise ArgumentError' do
|
39
|
-
|
39
|
+
lambda {
|
40
|
+
client.auth :kerberos => test_mit_kerberos.merge(:pass => 'fail')
|
41
|
+
}.should raise_error(ArgumentError)
|
40
42
|
end
|
41
43
|
end
|
42
44
|
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
describe Stellar::Gradebook do
|
4
|
+
before :all do
|
5
|
+
@gradebook = test_client.course("6.006", 2011, :fall).gradebook
|
6
|
+
end
|
7
|
+
|
8
|
+
describe '#navigation' do
|
9
|
+
it 'should have an Assignments section' do
|
10
|
+
@gradebook.navigation['Assignments'].should be_kind_of(URI)
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'should have an Add Assignment section' do
|
14
|
+
@gradebook.navigation['Add Assignment'].should be_kind_of(URI)
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'should have a Students section' do
|
18
|
+
@gradebook.navigation['Students'].should be_kind_of(URI)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
let(:assignments) { @gradebook.assignments }
|
23
|
+
|
24
|
+
describe '#assignments' do
|
25
|
+
describe '#all' do
|
26
|
+
it 'should have at least one assignment' do
|
27
|
+
assignments.all.should have_at_least(1).assignment
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'should have an assignment named Quiz 1' do
|
31
|
+
assignments.named('Quiz 1').
|
32
|
+
should be_kind_of(Stellar::Gradebook::Assignment)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
describe '#add_assignment' do
|
38
|
+
before do
|
39
|
+
@old_length = assignments.all.length
|
40
|
+
assignments.add 'RSpec Test PS', 'rspec-test', 42,
|
41
|
+
Time.now + 5, 1.01
|
42
|
+
end
|
43
|
+
|
44
|
+
after do
|
45
|
+
assignment = assignments.named('RSpec Test PS')
|
46
|
+
assignment.delete!
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'should create a new assignment' do
|
50
|
+
assignments.all.length.should == 1 + @old_length
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'should create an assignment with the right name' do
|
54
|
+
assignments.named('RSpec Test PS').
|
55
|
+
should be_kind_of(Stellar::Gradebook::Assignment)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
describe Stellar::Gradebook::Assignment do
|
60
|
+
before do
|
61
|
+
assignments.add 'RSpec Test PS', 'rspec-test',
|
62
|
+
42, Time.now + 5, 1.01
|
63
|
+
@assignment = assignments.named 'RSpec Test PS'
|
64
|
+
end
|
65
|
+
|
66
|
+
after { @assignment.delete! }
|
67
|
+
|
68
|
+
it 'should have a name' do
|
69
|
+
@assignment.name.should == 'RSpec Test PS'
|
70
|
+
end
|
71
|
+
|
72
|
+
it 'should have an URL' do
|
73
|
+
@assignment.url.should be_kind_of(URI)
|
74
|
+
end
|
75
|
+
|
76
|
+
it 'should not be deleted' do
|
77
|
+
@assignment.should_not be_deleted
|
78
|
+
end
|
79
|
+
|
80
|
+
describe '#delete!' do
|
81
|
+
before do
|
82
|
+
@old_length = assignments.all.length
|
83
|
+
@assignment.delete!
|
84
|
+
assignments.reload!
|
85
|
+
end
|
86
|
+
|
87
|
+
it 'should remove an assignment' do
|
88
|
+
assignments.all.length.should == @old_length - 1
|
89
|
+
end
|
90
|
+
|
91
|
+
it 'should remove the correct assignment' do
|
92
|
+
assignments.named('RSpec Test PS').should be_nil
|
93
|
+
end
|
94
|
+
|
95
|
+
it 'should mark the assignment deleted' do
|
96
|
+
@assignment.should be_deleted
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
describe Stellar::Gradebook do
|
4
|
+
before :all do
|
5
|
+
@gradebook = test_client.course("6.006", 2011, :fall).gradebook
|
6
|
+
@gradebook.assignments.add 'RSpec Test PS', 'rspec-test', 42
|
7
|
+
@assignment = @gradebook.assignments.named('RSpec Test PS')
|
8
|
+
end
|
9
|
+
|
10
|
+
after :all do
|
11
|
+
@assignment.delete!
|
12
|
+
end
|
13
|
+
|
14
|
+
let(:test_name) { 'Christianne Swartz' }
|
15
|
+
let(:test_email) { 'ude.tim@ytsirhc'.reverse }
|
16
|
+
|
17
|
+
describe '#students' do
|
18
|
+
describe '#all' do
|
19
|
+
let(:all) { @gradebook.students.all }
|
20
|
+
|
21
|
+
it 'should have at least 1 student' do
|
22
|
+
all.should have_at_least(1).student
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'should only contain Student instances' do
|
26
|
+
all.each do |student|
|
27
|
+
student.should be_kind_of(Stellar::Gradebook::Student)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
shared_examples_for 'a student query' do
|
33
|
+
it 'should return a student' do
|
34
|
+
student.should be_kind_of(Stellar::Gradebook::Student)
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'should return a student with the correct name' do
|
38
|
+
student.name.should == test_name
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'should return a student with the correct email' do
|
42
|
+
student.email.should == test_email
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
describe '#named' do
|
47
|
+
let(:student) { @gradebook.students.named test_name }
|
48
|
+
|
49
|
+
it_should_behave_like 'a student query'
|
50
|
+
end
|
51
|
+
|
52
|
+
describe '#with_email' do
|
53
|
+
let(:student) { @gradebook.students.with_email test_email }
|
54
|
+
|
55
|
+
it_should_behave_like 'a student query'
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
describe Stellar::Gradebook::Student do
|
60
|
+
let(:student) { @gradebook.students.named test_name }
|
61
|
+
|
62
|
+
describe '#grades' do
|
63
|
+
let(:grades) { student.grades }
|
64
|
+
|
65
|
+
it 'should have a score for the test assignment' do
|
66
|
+
grades.should have_key('RSpec Test PS')
|
67
|
+
end
|
68
|
+
|
69
|
+
it 'should have an empty score for the test assignment' do
|
70
|
+
grades['RSpec Test PS'].should be_nil
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
describe '#comment' do
|
75
|
+
it 'should be some string' do
|
76
|
+
student.comment.should be_kind_of(String)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
describe '#update_grades' do
|
81
|
+
before do
|
82
|
+
student.update_grades 'RSpec Test PS' => 41.59
|
83
|
+
end
|
84
|
+
|
85
|
+
it 'should store the right grade' do
|
86
|
+
student.grades['RSpec Test PS'].should == 41.59
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
describe '#update_comment' do
|
91
|
+
before do
|
92
|
+
@old_comment = student.comment
|
93
|
+
student.update_comment 'RSpec Test Comment'
|
94
|
+
end
|
95
|
+
|
96
|
+
after do
|
97
|
+
student.update_comment @old_comment
|
98
|
+
end
|
99
|
+
|
100
|
+
it 'should store the right comment' do
|
101
|
+
student.comment.should == 'RSpec Test Comment'
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
data/stellar.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = "stellar"
|
8
|
-
s.version = "0.
|
8
|
+
s.version = "0.2.0"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Victor Costan"]
|
12
|
-
s.date = "2011-10-
|
12
|
+
s.date = "2011-10-28"
|
13
13
|
s.description = "So we don't have to put up with Stellar's craptastic ui"
|
14
14
|
s.email = "victor@costan.us"
|
15
15
|
s.extra_rdoc_files = [
|
@@ -30,6 +30,7 @@ Gem::Specification.new do |s|
|
|
30
30
|
"lib/stellar/auth.rb",
|
31
31
|
"lib/stellar/client.rb",
|
32
32
|
"lib/stellar/courses.rb",
|
33
|
+
"lib/stellar/gradebook.rb",
|
33
34
|
"lib/stellar/homework.rb",
|
34
35
|
"lib/stellar/mitca.crt",
|
35
36
|
"spec/fixtures/.gitkeep",
|
@@ -37,6 +38,8 @@ Gem::Specification.new do |s|
|
|
37
38
|
"spec/stellar/auth_spec.rb",
|
38
39
|
"spec/stellar/client_spec.rb",
|
39
40
|
"spec/stellar/courses_spec.rb",
|
41
|
+
"spec/stellar/gradebook_spec.rb",
|
42
|
+
"spec/stellar/gradebook_student_spec.rb",
|
40
43
|
"spec/stellar/homework_spec.rb",
|
41
44
|
"spec/stellar/homework_submissions_spec.rb",
|
42
45
|
"spec/stellar_spec.rb",
|
@@ -65,6 +68,7 @@ Gem::Specification.new do |s|
|
|
65
68
|
s.add_development_dependency(%q<ruby-debug>, [">= 0"])
|
66
69
|
s.add_development_dependency(%q<ruby-debug19>, [">= 0"])
|
67
70
|
s.add_development_dependency(%q<highline>, [">= 1.6.2"])
|
71
|
+
s.add_development_dependency(%q<json>, [">= 0"])
|
68
72
|
else
|
69
73
|
s.add_dependency(%q<mechanize>, [">= 2.0.2"])
|
70
74
|
s.add_dependency(%q<nokogiri>, [">= 1.5.0"])
|
@@ -78,6 +82,7 @@ Gem::Specification.new do |s|
|
|
78
82
|
s.add_dependency(%q<ruby-debug>, [">= 0"])
|
79
83
|
s.add_dependency(%q<ruby-debug19>, [">= 0"])
|
80
84
|
s.add_dependency(%q<highline>, [">= 1.6.2"])
|
85
|
+
s.add_dependency(%q<json>, [">= 0"])
|
81
86
|
end
|
82
87
|
else
|
83
88
|
s.add_dependency(%q<mechanize>, [">= 2.0.2"])
|
@@ -92,6 +97,7 @@ Gem::Specification.new do |s|
|
|
92
97
|
s.add_dependency(%q<ruby-debug>, [">= 0"])
|
93
98
|
s.add_dependency(%q<ruby-debug19>, [">= 0"])
|
94
99
|
s.add_dependency(%q<highline>, [">= 1.6.2"])
|
100
|
+
s.add_dependency(%q<json>, [">= 0"])
|
95
101
|
end
|
96
102
|
end
|
97
103
|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: stellar
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,11 +9,11 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2011-10-
|
12
|
+
date: 2011-10-28 00:00:00.000000000Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: mechanize
|
16
|
-
requirement: &
|
16
|
+
requirement: &19824800 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ! '>='
|
@@ -21,10 +21,10 @@ dependencies:
|
|
21
21
|
version: 2.0.2
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
24
|
+
version_requirements: *19824800
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: nokogiri
|
27
|
-
requirement: &
|
27
|
+
requirement: &19823840 !ruby/object:Gem::Requirement
|
28
28
|
none: false
|
29
29
|
requirements:
|
30
30
|
- - ! '>='
|
@@ -32,10 +32,10 @@ dependencies:
|
|
32
32
|
version: 1.5.0
|
33
33
|
type: :runtime
|
34
34
|
prerelease: false
|
35
|
-
version_requirements: *
|
35
|
+
version_requirements: *19823840
|
36
36
|
- !ruby/object:Gem::Dependency
|
37
37
|
name: rdoc
|
38
|
-
requirement: &
|
38
|
+
requirement: &19812300 !ruby/object:Gem::Requirement
|
39
39
|
none: false
|
40
40
|
requirements:
|
41
41
|
- - ! '>='
|
@@ -43,10 +43,10 @@ dependencies:
|
|
43
43
|
version: 3.9.4
|
44
44
|
type: :development
|
45
45
|
prerelease: false
|
46
|
-
version_requirements: *
|
46
|
+
version_requirements: *19812300
|
47
47
|
- !ruby/object:Gem::Dependency
|
48
48
|
name: rspec
|
49
|
-
requirement: &
|
49
|
+
requirement: &19811720 !ruby/object:Gem::Requirement
|
50
50
|
none: false
|
51
51
|
requirements:
|
52
52
|
- - ! '>='
|
@@ -54,10 +54,10 @@ dependencies:
|
|
54
54
|
version: 2.6.0
|
55
55
|
type: :development
|
56
56
|
prerelease: false
|
57
|
-
version_requirements: *
|
57
|
+
version_requirements: *19811720
|
58
58
|
- !ruby/object:Gem::Dependency
|
59
59
|
name: yard
|
60
|
-
requirement: &
|
60
|
+
requirement: &19811080 !ruby/object:Gem::Requirement
|
61
61
|
none: false
|
62
62
|
requirements:
|
63
63
|
- - ! '>='
|
@@ -65,10 +65,10 @@ dependencies:
|
|
65
65
|
version: 0.7.2
|
66
66
|
type: :development
|
67
67
|
prerelease: false
|
68
|
-
version_requirements: *
|
68
|
+
version_requirements: *19811080
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: yard-rspec
|
71
|
-
requirement: &
|
71
|
+
requirement: &19810280 !ruby/object:Gem::Requirement
|
72
72
|
none: false
|
73
73
|
requirements:
|
74
74
|
- - ! '>='
|
@@ -76,10 +76,10 @@ dependencies:
|
|
76
76
|
version: '0.1'
|
77
77
|
type: :development
|
78
78
|
prerelease: false
|
79
|
-
version_requirements: *
|
79
|
+
version_requirements: *19810280
|
80
80
|
- !ruby/object:Gem::Dependency
|
81
81
|
name: bundler
|
82
|
-
requirement: &
|
82
|
+
requirement: &19809620 !ruby/object:Gem::Requirement
|
83
83
|
none: false
|
84
84
|
requirements:
|
85
85
|
- - ! '>='
|
@@ -87,10 +87,10 @@ dependencies:
|
|
87
87
|
version: 1.0.21
|
88
88
|
type: :development
|
89
89
|
prerelease: false
|
90
|
-
version_requirements: *
|
90
|
+
version_requirements: *19809620
|
91
91
|
- !ruby/object:Gem::Dependency
|
92
92
|
name: jeweler
|
93
|
-
requirement: &
|
93
|
+
requirement: &19809040 !ruby/object:Gem::Requirement
|
94
94
|
none: false
|
95
95
|
requirements:
|
96
96
|
- - ! '>='
|
@@ -98,10 +98,10 @@ dependencies:
|
|
98
98
|
version: 1.6.4
|
99
99
|
type: :development
|
100
100
|
prerelease: false
|
101
|
-
version_requirements: *
|
101
|
+
version_requirements: *19809040
|
102
102
|
- !ruby/object:Gem::Dependency
|
103
103
|
name: rcov
|
104
|
-
requirement: &
|
104
|
+
requirement: &19808300 !ruby/object:Gem::Requirement
|
105
105
|
none: false
|
106
106
|
requirements:
|
107
107
|
- - ! '>='
|
@@ -109,10 +109,10 @@ dependencies:
|
|
109
109
|
version: '0'
|
110
110
|
type: :development
|
111
111
|
prerelease: false
|
112
|
-
version_requirements: *
|
112
|
+
version_requirements: *19808300
|
113
113
|
- !ruby/object:Gem::Dependency
|
114
114
|
name: ruby-debug
|
115
|
-
requirement: &
|
115
|
+
requirement: &19806780 !ruby/object:Gem::Requirement
|
116
116
|
none: false
|
117
117
|
requirements:
|
118
118
|
- - ! '>='
|
@@ -120,10 +120,10 @@ dependencies:
|
|
120
120
|
version: '0'
|
121
121
|
type: :development
|
122
122
|
prerelease: false
|
123
|
-
version_requirements: *
|
123
|
+
version_requirements: *19806780
|
124
124
|
- !ruby/object:Gem::Dependency
|
125
125
|
name: ruby-debug19
|
126
|
-
requirement: &
|
126
|
+
requirement: &19805900 !ruby/object:Gem::Requirement
|
127
127
|
none: false
|
128
128
|
requirements:
|
129
129
|
- - ! '>='
|
@@ -131,10 +131,10 @@ dependencies:
|
|
131
131
|
version: '0'
|
132
132
|
type: :development
|
133
133
|
prerelease: false
|
134
|
-
version_requirements: *
|
134
|
+
version_requirements: *19805900
|
135
135
|
- !ruby/object:Gem::Dependency
|
136
136
|
name: highline
|
137
|
-
requirement: &
|
137
|
+
requirement: &19804800 !ruby/object:Gem::Requirement
|
138
138
|
none: false
|
139
139
|
requirements:
|
140
140
|
- - ! '>='
|
@@ -142,7 +142,18 @@ dependencies:
|
|
142
142
|
version: 1.6.2
|
143
143
|
type: :development
|
144
144
|
prerelease: false
|
145
|
-
version_requirements: *
|
145
|
+
version_requirements: *19804800
|
146
|
+
- !ruby/object:Gem::Dependency
|
147
|
+
name: json
|
148
|
+
requirement: &19793180 !ruby/object:Gem::Requirement
|
149
|
+
none: false
|
150
|
+
requirements:
|
151
|
+
- - ! '>='
|
152
|
+
- !ruby/object:Gem::Version
|
153
|
+
version: '0'
|
154
|
+
type: :development
|
155
|
+
prerelease: false
|
156
|
+
version_requirements: *19793180
|
146
157
|
description: So we don't have to put up with Stellar's craptastic ui
|
147
158
|
email: victor@costan.us
|
148
159
|
executables: []
|
@@ -164,6 +175,7 @@ files:
|
|
164
175
|
- lib/stellar/auth.rb
|
165
176
|
- lib/stellar/client.rb
|
166
177
|
- lib/stellar/courses.rb
|
178
|
+
- lib/stellar/gradebook.rb
|
167
179
|
- lib/stellar/homework.rb
|
168
180
|
- lib/stellar/mitca.crt
|
169
181
|
- spec/fixtures/.gitkeep
|
@@ -171,6 +183,8 @@ files:
|
|
171
183
|
- spec/stellar/auth_spec.rb
|
172
184
|
- spec/stellar/client_spec.rb
|
173
185
|
- spec/stellar/courses_spec.rb
|
186
|
+
- spec/stellar/gradebook_spec.rb
|
187
|
+
- spec/stellar/gradebook_student_spec.rb
|
174
188
|
- spec/stellar/homework_spec.rb
|
175
189
|
- spec/stellar/homework_submissions_spec.rb
|
176
190
|
- spec/stellar_spec.rb
|
@@ -191,7 +205,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
191
205
|
version: '0'
|
192
206
|
segments:
|
193
207
|
- 0
|
194
|
-
hash: -
|
208
|
+
hash: -1478260872774288007
|
195
209
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
196
210
|
none: false
|
197
211
|
requirements:
|