stellar 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -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.10)
29
+ rdoc (3.11)
30
30
  json (~> 1.4)
31
- rspec (2.6.0)
32
- rspec-core (~> 2.6.0)
33
- rspec-expectations (~> 2.6.0)
34
- rspec-mocks (~> 2.6.0)
35
- rspec-core (2.6.4)
36
- rspec-expectations (2.6.0)
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.6.0)
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.2)
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.1
1
+ 0.2.0
@@ -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'
@@ -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
- # Reset any prior credentials.
22
- @mech = mech
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
- if File.exist?(key)
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
- if File.exist?(cert)
41
- @mech.cert = cert
42
- else
43
- @mech.cert = OpenSSL::X509::Certificate.new cert
44
- end
45
- else
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
- mech.ca_file = mitca_path
106
- mech.user_agent_alias = 'Linux Firefox'
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]
@@ -15,10 +15,13 @@ class Client
15
15
  end
16
16
 
17
17
  # New Mechanize instance.
18
- def mech
19
- m = Mechanize.new
20
- m.ca_file = mitca_path
21
- m.user_agent_alias = 'Linux Firefox'
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
 
@@ -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
@@ -29,8 +29,10 @@ class HomeworkList
29
29
  @assignments
30
30
  end
31
31
 
32
- # All assignments in this course's homework module.
33
- # @return [Array<Stellar::Homework>] list of assignments posted by this course
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 { |link| /submission\s+details/ =~ link.inner_text }
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
@@ -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
- client.auth :kerberos => test_mit_kerberos.merge(:pass => 'fail')
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
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = "stellar"
8
- s.version = "0.1.1"
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-16"
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.1.1
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-16 00:00:00.000000000Z
12
+ date: 2011-10-28 00:00:00.000000000Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: mechanize
16
- requirement: &15419440 !ruby/object:Gem::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: *15419440
24
+ version_requirements: *19824800
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: nokogiri
27
- requirement: &15418740 !ruby/object:Gem::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: *15418740
35
+ version_requirements: *19823840
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: rdoc
38
- requirement: &15418020 !ruby/object:Gem::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: *15418020
46
+ version_requirements: *19812300
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: rspec
49
- requirement: &15413920 !ruby/object:Gem::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: *15413920
57
+ version_requirements: *19811720
58
58
  - !ruby/object:Gem::Dependency
59
59
  name: yard
60
- requirement: &15413080 !ruby/object:Gem::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: *15413080
68
+ version_requirements: *19811080
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: yard-rspec
71
- requirement: &15412400 !ruby/object:Gem::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: *15412400
79
+ version_requirements: *19810280
80
80
  - !ruby/object:Gem::Dependency
81
81
  name: bundler
82
- requirement: &15411660 !ruby/object:Gem::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: *15411660
90
+ version_requirements: *19809620
91
91
  - !ruby/object:Gem::Dependency
92
92
  name: jeweler
93
- requirement: &15410740 !ruby/object:Gem::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: *15410740
101
+ version_requirements: *19809040
102
102
  - !ruby/object:Gem::Dependency
103
103
  name: rcov
104
- requirement: &15409860 !ruby/object:Gem::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: *15409860
112
+ version_requirements: *19808300
113
113
  - !ruby/object:Gem::Dependency
114
114
  name: ruby-debug
115
- requirement: &15409140 !ruby/object:Gem::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: *15409140
123
+ version_requirements: *19806780
124
124
  - !ruby/object:Gem::Dependency
125
125
  name: ruby-debug19
126
- requirement: &15408460 !ruby/object:Gem::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: *15408460
134
+ version_requirements: *19805900
135
135
  - !ruby/object:Gem::Dependency
136
136
  name: highline
137
- requirement: &15406380 !ruby/object:Gem::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: *15406380
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: -746749934758506157
208
+ hash: -1478260872774288007
195
209
  required_rubygems_version: !ruby/object:Gem::Requirement
196
210
  none: false
197
211
  requirements: