stellar 0.1.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.
@@ -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