stellar 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,112 @@
1
+ # :nodoc: namespace
2
+ module Stellar
3
+
4
+ # Course search functionality.
5
+ class Courses
6
+ def initialize(client)
7
+ @client = client
8
+ end
9
+
10
+ # My classes.
11
+ # @return [Array] array with one Hash per class; Hashes have :number and
12
+ # :url keys
13
+ def mine
14
+ page = @client.get_nokogiri '/atstellar'
15
+ class_links = page.css('a[href*="/S/course/"]').
16
+ map { |link| Stellar::Course.from_link link, @client }.reject(&:nil?)
17
+ end
18
+ end # class Stellar::Courses
19
+
20
+ # Stellar client scoped to a course.
21
+ class Course
22
+ # Official MIT course ID, e.g. "6.006".
23
+ attr_reader :number
24
+
25
+ # URL to the course's main page on Stellar.
26
+ #
27
+ # Example: "https://stellar.mit.edu/S/course/6/fa11/6.006/"
28
+ attr_reader :url
29
+
30
+ # Maps the text in navigation links to URI objects.
31
+ #
32
+ # Example: navigation['Homework'] => <# URI: .../ >
33
+ attr_reader :navigation
34
+
35
+ # True if the client has administrative rights for this course.
36
+ attr_reader :is_admin
37
+
38
+ # The generic Stellar client used to query the server.
39
+ attr_reader :client
40
+
41
+ # Creates a scoped Stellar client from a link to the course's page.
42
+ #
43
+ # @param [Nokogiri::XML::Element] nokogiri_link a link pointing to the
44
+ # course's main page
45
+ # @param [Stellar::Client] client generic Stellar client
46
+ # @return [Stellar::Course] client scoped to the course, or nil if the link
47
+ # is not valid
48
+ def self.from_link(nokogiri_link, client)
49
+ number = nokogiri_link.css('span.courseNo').inner_text
50
+ return nil if number.empty?
51
+ url = nokogiri_link['href']
52
+ return nil unless url.index(number)
53
+ return nil unless /\/S\/course\// =~ url
54
+
55
+ return self.new(client, url, number)
56
+ end
57
+
58
+ # Creates a scoped Stellar client from a link to the course's page.
59
+ #
60
+ # @param [String] number the official MIT course ID, e.g. "6.006"
61
+ # @param [Fixnum] year the year the course was taught e.g. 2011
62
+ # @param [Symbol] semester :fall, :iap, :spring, :summer
63
+ # @return [Stellar::Course] client scoped to the course
64
+ def self.for(number, year, semester, client)
65
+ semester_string = case semester
66
+ when :fall
67
+ 'fa'
68
+ when :spring
69
+ 'sp'
70
+ when :summer
71
+ 'su'
72
+ when :iap
73
+ 'ia'
74
+ end
75
+ term = "#{semester_string}#{year.to_s[-2..-1]}"
76
+ major = number.split('.', 2).first
77
+ url = "/S/course/#{major}/#{term}/#{number}/index.html"
78
+
79
+ return self.new(client, url, number)
80
+ end
81
+
82
+ # Creates a scoped Stellar client from detailed specifications.
83
+ #
84
+ # @param [Stellar::Client] client generic Stellar client
85
+ # @param [String] course_url HTTP URI to the course's main Stellar page
86
+ # @param [String] course_number official course ID, e.g. "6.006"
87
+ # @raise ArgumentError if the course URL does not point to a course page
88
+ def initialize(client, course_url, course_number)
89
+ @client = client
90
+ @url = course_url
91
+ @number = course_number
92
+
93
+ course_page = @client.get_nokogiri course_url
94
+
95
+ @is_admin = course_page.css('p#toolset').length > 0
96
+
97
+ navbar_elems = course_page.css('#mainnav')
98
+ unless navbar_elems.length == 1
99
+ raise ArgumentError, "#{course_url} is not a course page"
100
+ end
101
+ @navigation = Hash[navbar_elems.first.css('a').map do |link|
102
+ [link.inner_text, URI.join(course_page.url, link['href'])]
103
+ end]
104
+ end
105
+
106
+ # Client scoped to the course's Homework module.
107
+ def homework
108
+ @homework ||= Stellar::HomeworkList.new self
109
+ end
110
+ end # class Stellar::Course
111
+
112
+ end # namespace Stellar
@@ -0,0 +1,242 @@
1
+ # :nodoc: namespace
2
+ module Stellar
3
+
4
+ # Homework listing functionality.
5
+ class HomeworkList
6
+ # Creates a Stellar client scoped to a course's Homework module.
7
+ #
8
+ # @param [Stellar::Course] the course whose homework is desired
9
+ def initialize(course)
10
+ @course = course
11
+ @client = course.client
12
+ @url = course.navigation['Homework']
13
+
14
+ page = @client.get_nokogiri @url
15
+ @assignments = page.css('#content a[href*="assignment"]').map { |link|
16
+ name = link.inner_text
17
+ url = URI.join page.url, link['href']
18
+ begin
19
+ Stellar::Homework.new url, name, course
20
+ rescue ArgumentError
21
+ nil
22
+ end
23
+ }.reject(&:nil?)
24
+ end
25
+
26
+ # All assignments in this course's homework module.
27
+ # @return [Array<Stellar::Homework>] list of assignments posted by this course
28
+ def all
29
+ @assignments
30
+ end
31
+
32
+ # All assignments in this course's homework module.
33
+ # @return [Array<Stellar::Homework>] list of assignments posted by this course
34
+ def named(name)
35
+ @assignments.find { |a| a.name == name }
36
+ end
37
+ end # class Stellar::HomeworkList
38
+
39
+ # One assignment in the homework tab.
40
+ class Homework
41
+ # Assignment name.
42
+ attr_reader :name
43
+
44
+ # The course that this assignment is for.
45
+ attr_reader :course
46
+
47
+ # Generic Stellar client used to make requests.
48
+ attr_reader :client
49
+
50
+ # Creates a Stellar client scoped to an assignment.
51
+ #
52
+ # @param [URI, String] page_url URL to the assignment's main Stellar page
53
+ # @param [String] assignment name, e.g. "name"
54
+ # @param [Course] the course that issued the assignment
55
+ def initialize(page_url, name, course)
56
+ @name = name
57
+ @url = page_url
58
+ @course = course
59
+ @client = course.client
60
+
61
+ page = @client.get_nokogiri @url
62
+ unless page.css('#content p b').any? { |dom| dom.inner_text.strip == name }
63
+ raise ArgumentError, 'Invalid homework page URL'
64
+ end
65
+ end
66
+
67
+ # List of submissions associated with this problem set.
68
+ def submissions
69
+ page = @client.get_nokogiri @url
70
+ @submissions ||= page.css('.gradeTable tbody tr').map { |tr|
71
+ begin
72
+ Stellar::Homework::Submission.new tr, self
73
+ rescue ArgumentError
74
+ nil
75
+ end
76
+ }.reject(&:nil?)
77
+ end
78
+
79
+ # A student's submission for an assignment.
80
+ class Submission
81
+ # URL to the last file that the student submitted.
82
+ attr_reader :file_url
83
+
84
+ # Name of the student who authored this submission.
85
+ attr_reader :name
86
+
87
+ # Email of the student who authorted this submission.
88
+ attr_reader :email
89
+
90
+ # Comments posted on this submission.
91
+ attr_reader :comments
92
+
93
+ # Homework that the submission belongs to.
94
+ attr_reader :homework
95
+
96
+ # Generic Stellar client used to make requests.
97
+ attr_reader :client
98
+
99
+ # Creates a submission from a <tr> element in the Stellar homework page.
100
+ #
101
+ # @param [Nokogiri::XML::Element]
102
+ def initialize(tr, homework)
103
+ link = tr.css('a').find { |link| /submission\s+details/ =~ link.inner_text }
104
+ raise ArgumentError, 'Invalid submission-listing <tr>' unless link
105
+
106
+ @url = URI.join tr.document.url, link['href']
107
+ @homework = homework
108
+ @client = homework.client
109
+
110
+ page = @client.get_nokogiri @url
111
+
112
+ unless author_link = page.css('#content h4 a[href^="mailto:"]').first
113
+ raise ArgumentError, 'Invalud submission-listing <tr>'
114
+ end
115
+ @name = author_link.inner_text
116
+ @email = author_link['href'].sub /^mailto:/, ''
117
+ @file_url = page.css('#rosterBox a[href*="studentWork"]').map { |link|
118
+ next nil unless link.inner_text == homework.name
119
+ URI.join @url.to_s, link['href']
120
+ }.reject(&:nil?).first
121
+
122
+ @add_comment_url = URI.join @url.to_s,
123
+ page.css('#comments a[href*="add"]').first['href']
124
+ reload_comments! page
125
+ end
126
+
127
+ # The contents of the file attached to this Stellar submission.
128
+ #
129
+ # @return [String] raw file data
130
+ def file_data
131
+ @client.get_file @file_url
132
+ end
133
+
134
+ # Adds a comment to the student's submission.
135
+ #
136
+ # @param [String] text the comment text
137
+ # @param [String] file_data the content of the file attached to the comment;
138
+ # by default, no file is attached
139
+ # @param [String] file_mime_type if a file is attached, indicates its type;
140
+ # examples: 'text/plain', 'application/pdf'
141
+ # @param [String] file_name name of the file attached to the comment; by
142
+ # default, 'attachment.txt'
143
+ # @return [Stellar::Homework::Submission] self
144
+ def add_comment(text, file_data = nil, file_mime_type = 'text/plain',
145
+ file_name = 'attachment.txt')
146
+ add_page = @client.get @add_comment_url
147
+ add_form = add_page.form_with :action => /addcomment/i
148
+
149
+ add_form.field_with(:name => /newCommentRaw/i).value = text
150
+ add_form.field_with(:name => /newComment/i).value = text
151
+ add_form.checkbox_with(:name => /privateComment/i).checked = :checked
152
+ if file_data
153
+ upload = add_form.file_uploads.first
154
+ upload.file_name = file_name
155
+ upload.mime_type = file_mime_type
156
+ upload.file_data = file_data
157
+ end
158
+ add_form.submit add_form.button_with(:name => /submit/i)
159
+ self
160
+ end
161
+
162
+ # Reloads the problem set's comments page.
163
+ def reload_comments!(page = nil)
164
+ page ||= @client.get_nokogiri @url
165
+ @comments = page.css('#comments ~ table.dataTable').map { |table|
166
+ Comment.new table, self
167
+ }.reject(&:nil?)
168
+ end
169
+
170
+ # A comment on a Stellar submission.
171
+ class Comment
172
+ # Person who posted the comment.
173
+ attr_reader :author
174
+ # Comment text.
175
+ attr_reader :text
176
+ # URL to the file attached to the comment. Can be nil.
177
+ attr_reader :attachment_url
178
+ # True if the comment was deleted.
179
+ attr_reader :deleted
180
+ # True if the comment was deleted.
181
+ alias_method :deleted?, :deleted
182
+
183
+ # The submission that the comment was posted on.
184
+ attr_reader :submission
185
+ # Generic Stellar client used to make requests.
186
+ attr_reader :client
187
+
188
+ def initialize(table, submission)
189
+ @submission = submission
190
+ @client = @submission.client
191
+
192
+ page_url = table.document.url
193
+ @author = table.css('thead tr th.announcedBy').first.inner_text
194
+
195
+ unless content = table.css('tbody tr td.announcement').first
196
+ raise 'Invalid submission comment table'
197
+ end
198
+ if (deleted_text = table.css('tbody tr td.announcement > em').first) &&
199
+ deleted_text.inner_text == 'deleted'
200
+ @deleted = true
201
+ @text = nil
202
+ @attachment_url = nil
203
+ else
204
+ @deleted = false
205
+
206
+ unless delete_link = table.css('thead a[href*="delete"]').first
207
+ raise ArgumentError, 'Invalid submission comment table'
208
+ end
209
+ @delete_url = URI.join page_url, delete_link['href']
210
+ @text = content.css('p').inner_text
211
+ attachment_links = table.css('tbody tr td.announcement > a')
212
+ if attachment_links.empty?
213
+ @attachment_url = nil
214
+ else
215
+ @attachment_url = URI.join page_url, attachment_links.first['href']
216
+ end
217
+ end
218
+ end
219
+
220
+ # Deletes this comment from Stellar.
221
+ def delete!
222
+ return if @deleted
223
+ delete_page = @client.get @delete_url
224
+ delete_form = delete_page.form_with(:action => /delete/i)
225
+ delete_button = delete_form.button_with(:name => /delete/i)
226
+ delete_form.submit delete_button
227
+ @deleted = true
228
+ end
229
+
230
+ # The contents of the file attached to this Stellar submission comment.
231
+ #
232
+ # @return [String] raw file data
233
+ def attachment_data
234
+ @attachment_url && @client.get_file(@attachment_url)
235
+ end
236
+ end # class Stellar::Homework::Submission::Comment
237
+
238
+ end # class Stellar::Homework::Submission
239
+
240
+ end # class Stellar::Homework
241
+
242
+ end # namespace Stellar
@@ -0,0 +1,21 @@
1
+ -----BEGIN CERTIFICATE-----
2
+ MIIDZTCCAs6gAwIBAgIBATANBgkqhkiG9w0BAQUFADB7MQswCQYDVQQGEwJVUzEW
3
+ MBQGA1UECBMNTWFzc2FjaHVzZXR0czEuMCwGA1UEChMlTWFzc2FjaHVzZXR0cyBJ
4
+ bnN0aXR1dGUgb2YgVGVjaG5vbG9neTEkMCIGA1UECxMbTUlUIENlcnRpZmljYXRp
5
+ b24gQXV0aG9yaXR5MB4XDTA2MDQwODE2NTAwNFoXDTI2MDgwMTE2NTAwNFowezEL
6
+ MAkGA1UEBhMCVVMxFjAUBgNVBAgTDU1hc3NhY2h1c2V0dHMxLjAsBgNVBAoTJU1h
7
+ c3NhY2h1c2V0dHMgSW5zdGl0dXRlIG9mIFRlY2hub2xvZ3kxJDAiBgNVBAsTG01J
8
+ VCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCBnzANBgkqhkiG9w0BAQEFAAOBjQAw
9
+ gYkCgYEA09Dr51G1M3Wm2KOE6gJwXM+cIOALA4uORm4VJeF39mvEcN3UFgvMEYgx
10
+ OAvufFkkV+mNzXX4UmPdMwzwT5+1/JGuMoWMGnVjGZiGHpIjsofz9cmmopdo8uyy
11
+ Gq2z9e0J6sznvLRkUBXmVwAaesbe/uEwWFpdq7u0HBHsZMHTpFUCAwEAAaOB+DCB
12
+ 9TAdBgNVHQ4EFgQUU/WjDwZdZdiKj1JtafrrVS29iwwwgaUGA1UdIwSBnTCBmoAU
13
+ U/WjDwZdZdiKj1JtafrrVS29iwyhf6R9MHsxCzAJBgNVBAYTAlVTMRYwFAYDVQQI
14
+ Ew1NYXNzYWNodXNldHRzMS4wLAYDVQQKEyVNYXNzYWNodXNldHRzIEluc3RpdHV0
15
+ ZSBvZiBUZWNobm9sb2d5MSQwIgYDVQQLExtNSVQgQ2VydGlmaWNhdGlvbiBBdXRo
16
+ b3JpdHmCAQEwDAYDVR0TBAUwAwEB/zALBgNVHQ8EBAMCAQYwEQYJYIZIAYb4QgEB
17
+ BAQDAgEGMA0GCSqGSIb3DQEBBQUAA4GBAMTjXyVdM89JlPTzoe3o5CIvUP6TrWMN
18
+ Bm3/mSX5pXeZWbWLtdVfUgQ9mW6UBYXaQSUPmz9C09ZNBH8N3vOoDS5/jD8MMcV/
19
+ U/rOAIb4v2bMRKpPweSINGm72Pv/Pg15t1sRcnatBK94orekYvfJa3PiPU/3pfge
20
+ RYhCd9zByXr2
21
+ -----END CERTIFICATE-----
File without changes
@@ -0,0 +1,12 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
3
+ require 'rspec'
4
+ require 'stellar'
5
+
6
+ # Requires supporting files with custom matchers and macros, etc,
7
+ # in ./support/ and its subdirectories.
8
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
9
+
10
+ RSpec.configure do |config|
11
+
12
+ end
@@ -0,0 +1,32 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe Stellar::Auth do
4
+ describe 'get_certificate' do
5
+ before :all do
6
+ @result = Stellar::Auth.get_certificate test_mit_kerberos
7
+ end
8
+
9
+ it 'should contain a key pair' do
10
+ @result[:key].should be_kind_of(OpenSSL::PKey::PKey)
11
+ @result[:key].should be_private
12
+ end
13
+ it 'should contain a certificate' do
14
+ @result[:cert].should be_kind_of(OpenSSL::X509::Certificate)
15
+ end
16
+ it 'should contain a MIT certificate' do
17
+ @result[:cert].subject.to_s.
18
+ should match(/O=Massachusetts Institute of Technology/)
19
+ end
20
+ it 'should contain a certificate matching the key' do
21
+ @result[:cert].public_key.to_pem.should == @result[:key].public_key.to_pem
22
+ end
23
+ end
24
+
25
+ describe 'get_certificate with bad credentials' do
26
+ it 'should raise ArgumentError' do
27
+ lambda {
28
+ Stellar::Auth.get_certificate test_mit_kerberos.merge(:pass => 'fail')
29
+ }.should raise_error(ArgumentError)
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,58 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe Stellar::Client do
4
+ let(:client) { Stellar::Client.new }
5
+
6
+ shared_examples_for 'an authenticated client' do
7
+ describe 'get_nokogiri' do
8
+ let(:page) { client.get_nokogiri '/atstellar' }
9
+
10
+ it 'should be a Nokogiri document' do
11
+ page.should be_kind_of(Nokogiri::HTML::Document)
12
+ end
13
+
14
+ it 'should have course links' do
15
+ page.css('a[title*="class site"]').length.should > 0
16
+ end
17
+ end
18
+ end
19
+
20
+ describe '#auth' do
21
+ describe 'with Kerberos credentials' do
22
+ before do
23
+ client.auth :kerberos => test_mit_kerberos
24
+ end
25
+
26
+ it_should_behave_like 'an authenticated client'
27
+ end
28
+
29
+ describe 'with a certificate' do
30
+ before do
31
+ client.auth :cert => test_mit_cert
32
+ end
33
+
34
+ it_should_behave_like 'an authenticated client'
35
+ end
36
+
37
+ describe 'with bad Kerberos credentials' do
38
+ it 'should raise ArgumentError' do
39
+ client.auth :kerberos => test_mit_kerberos.merge(:pass => 'fail')
40
+ end
41
+ end
42
+ end
43
+
44
+ describe '#course' do
45
+ before do
46
+ client.auth :kerberos => test_mit_kerberos
47
+ end
48
+ let(:six) { client.course('6.006', 2011, :fall) }
49
+
50
+ it 'should return a Course instance' do
51
+ six.should be_kind_of(Stellar::Course)
52
+ end
53
+
54
+ it 'should return a 6.006 course' do
55
+ six.number.should == '6.006'
56
+ end
57
+ end
58
+ end