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 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: