xqueue_ruby 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ OGViMTc0NDRmNTM2MGUyMWFhMWIxZWYyMTA1NzdhOTg1ZDJjMjVhNQ==
5
+ data.tar.gz: !binary |-
6
+ NjhmYmUxYzE3Y2MyMjI4MDY1OTE0ZmVhMWIzNjlhOTM4NWViY2E1Zg==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ YzQ2ZWM2MzdkMjhmMDZjYmVmNzgzZDkzODQ1YzNkMWI3MTZlM2M5ODc2NjVh
10
+ MmE4ODRkODFiN2NkZjE0MzA5ZDA4NjQ2MzU4YmUxN2MxOWY2MzVlNTllMjAx
11
+ N2I0YzUwMDQ5NWM4N2NlMWE3NmFhOTVmYmZhMTU2NmE5YjcwNzk=
12
+ data.tar.gz: !binary |-
13
+ MDQ0ZDAyNTViODVmMjgwOTY2NmZlY2FiMmQyNWFkZGNkMmUxNjg1OThhNDIx
14
+ YjYzY2NlNGY5ZDE3OWQwZTBmZmFhYmMwODRjZWE1NjUxMGJjYjQ0ODIzN2M0
15
+ MDBjNTFjZjRlNGE1OGQ5MzRhYWZiODQ4MGQxNmNmM2NmNDdlZWE=
@@ -0,0 +1,8 @@
1
+ require 'mechanize'
2
+ require 'json'
3
+
4
+ $LOAD_PATH.unshift File.expand_path(File.join(File.dirname(__FILE__)))
5
+
6
+ require 'xqueue_ruby/xqueue_ruby'
7
+ require 'xqueue_ruby/xqueue_submission'
8
+
@@ -0,0 +1,184 @@
1
+ class XQueue
2
+ require 'mechanize'
3
+ require 'json'
4
+ require 'ruby-debug'
5
+
6
+ # Ruby interface to the Open edX XQueue class for external checkers
7
+ # (autograders). Lets you pull student-submitted work products from a
8
+ # named queue and post back the results of grading them.
9
+ #
10
+ # All responses from the XQueue server have a JSON object with
11
+ # +return_code+ and +content+ slots. A +return_code+ of 0 normally
12
+ # means success.
13
+ #
14
+ # == Example
15
+ #
16
+ # You need two sets of credentials to authenticate yourself to the
17
+ # xqueue server. For historical reasons, they are called
18
+ # (+django_name+, +django_pass+) and (+user_name+, +user_pass+).
19
+ # You also need to name the queue you want to use; edX creates queues
20
+ # for you. Each +XQueue+ instance is tied to a single queue name.
21
+ #
22
+ # === Retrieving an assignment:
23
+ #
24
+ # queue = XQueue.new('dj_name', 'dj_pass', 'u_name', 'u_pass', 'my_q')
25
+ # queue.length # => an integer showing queue length
26
+ # assignment = queue.get_submission # => returns new +XQueueSubmission+ object
27
+ #
28
+ # === Posting results back
29
+ #
30
+ # The submission includes a secret key that is used in postback,
31
+ # so you should use the +#postback+ method defined on the submission.
32
+ #
33
+
34
+ # The base URI of the production Xqueue server.
35
+ XQUEUE_DEFAULT_BASE_URI = 'https://xqueue.edx.org'
36
+
37
+ # Error message, if any, associated with last unsuccessful operation
38
+ attr_reader :error
39
+
40
+ # Queue from which to pull, established in constructor. You need a
41
+ # new +XQueue+ object if you want to use a different queue.
42
+ attr_reader :queue_name
43
+
44
+ # The base URI used for this queue; won't change for this queue even
45
+ # if you later change the value of +XQueue.base_uri+
46
+ attr_reader :base_uri
47
+
48
+ # The base URI used when new queue instances are created
49
+ def self.base_uri
50
+ @@base_uri ||= URI(XQUEUE_DEFAULT_BASE_URI)
51
+ end
52
+ def self.base_uri=(uri)
53
+ @@base_uri = URI(uri)
54
+ end
55
+
56
+ class XQueueError < StandardError ; end
57
+ # Ancestor class for all XQueue-related exceptions
58
+ class AuthenticationError < XQueueError ; end
59
+ # Raised if XQueue authentication fails
60
+ class IOError < XQueueError ; end
61
+ # Raised if there are network or I/O errors connecting to queue server
62
+ class NoSuchQueueError < XQueueError ; end
63
+ # Raised if queue name doesn't exist
64
+ class UpdateFailedError < XQueueError ; end
65
+ # Raised if a postback to the queue (to post grade) fails at
66
+ # application level
67
+
68
+ # Creates a new instance and attempts to authenticate to the
69
+ # queue server.
70
+ # * +django_name+, +django_pass+: first set of auth credentials (see
71
+ # above)
72
+ # * +user_name+, +user_pass+: second set of auth credentials (see
73
+ # above)
74
+ # * +queue_name+: logical name of the queue
75
+ def initialize(django_name, django_pass, user_name, user_pass, queue_name)
76
+ @queue_name = queue_name
77
+ @base_uri = XQueue.base_uri
78
+ @django_auth = {'username' => django_name, 'password' => django_pass}
79
+ @session = Mechanize.new
80
+ @session.add_auth(@base_uri, user_name, user_pass)
81
+ @valid_queues = nil
82
+ @error = nil
83
+ @authenticated = nil
84
+ end
85
+
86
+ # Authenticates to the server. You can call this explicitly, but it
87
+ # is called automatically if necessary on the first request in a new
88
+ # session.
89
+ def authenticate
90
+ response = request :post, '/xqueue/login/', @django_auth
91
+ if response['return_code'] == 0
92
+ @authenticated = true
93
+ else
94
+
95
+ raise(AuthenticationError, "Authentication failure: #{response['content']}")
96
+ end
97
+ end
98
+
99
+ # Returns +true+ if the session has been properly authenticated to
100
+ # server, that is, after a successful call to +authenticate+ or to any
101
+ # of the request methods that may have called +authenticate+ automatically.
102
+ def authenticated? ; @authenticated ; end
103
+
104
+ # Returns length of the queue as an integer >= 0.
105
+ def queue_length
106
+ authenticate unless authenticated?
107
+ response = request(:get, '/xqueue/get_queuelen/', {:queue_name => @queue_name})
108
+ if response['return_code'] == 0 # success
109
+ response['content'].to_i
110
+ elsif response['return_code'] == 1 && response['content'] =~ /^Valid queue names are: (.*)/i
111
+ @valid_queues = $1.split(/,\s+/)
112
+ raise NoSuchQueueError, "No such queue: valid queues are #{$1}"
113
+ else
114
+ raise IOError, response['content']
115
+ end
116
+ end
117
+
118
+ def list_queues
119
+ authenticate unless authenticated?
120
+ if @valid_queues.nil?
121
+ old, @queue_name = @queue_name, 'I_AM_NOT_A_QUEUE'
122
+ begin queue_length rescue nil end
123
+ end
124
+ @valid_queues
125
+ end
126
+
127
+ # Retrieve a submission from this queue. Returns nil if queue is empty,
128
+ # otherwise a new +XQueue::Submission+ instance.
129
+ def get_submission
130
+ authenticate unless authenticated?
131
+ if queue_length > 0
132
+ begin
133
+ json_response = request(:get, '/xqueue/get_submission/', {:queue_name => @queue_name})
134
+ XQueueSubmission.parse_JSON(self, json_response)
135
+ rescue StandardError => e # TODO: do something more interesting with the error.
136
+ raise e
137
+ end
138
+ else
139
+ nil
140
+ end
141
+ end
142
+ # Record a result of grading something. It may be easier to use
143
+ # +XQueue::Submission#post_back+, which marshals the information
144
+ # needed here automatically.
145
+ #
146
+ # * +header+: secret header key (from 'xqueue_header' slot in the
147
+ # 'content' object of the original retrieved submission)
148
+ # * +score+: integer number of points (not scaled)
149
+ # * +correct+: true (default) means show green checkmark, else red 'x'
150
+ # * +message+: (optional) plain text feedback; will be coerced to UTF-8
151
+
152
+ def put_result(header, score, correct=true, message='')
153
+ payload = JSON.generate({
154
+ :xqueue_header => header,
155
+ :xqueue_body => {
156
+ :correct => (!!correct).to_s.capitalize,
157
+ :score => score,
158
+ :message => message.encode('UTF-8',
159
+ :invalid => :replace, :undef => :replace, :replace => '?'),
160
+ }
161
+ })
162
+ response = request :post, '/xqueue/put_result', payload
163
+ if response['return_code'] != 0
164
+ raise UpdateFailedError, response['content']
165
+ end
166
+ end
167
+
168
+ private
169
+
170
+ # :nodoc:
171
+ def request(method, path, args={})
172
+ begin
173
+ response = @session.send(method, @base_uri + path, args)
174
+ response_json = JSON(response.body)
175
+
176
+ rescue Mechanize::ResponseCodeError => e
177
+ raise IOError, "Error communicating with server: #{e.message}"
178
+ rescue JSON::ParserError => e
179
+ raise IOError, "Non-JSON response from server: #{response.body.force_encoding('UTF-8')}"
180
+ rescue Exception => e
181
+ raise IOError, e.message
182
+ end
183
+ end
184
+ end
@@ -0,0 +1,61 @@
1
+ require 'active_model'
2
+ require 'json'
3
+ require 'debugger'
4
+
5
+ class XQueueSubmission
6
+ include ActiveModel::Validations
7
+
8
+ class InvalidSubmissionError < StandardError ; end
9
+
10
+ attr_reader :queue
11
+ # The +XQueue+ from which this assignment was retrieved (and to which the grade should be posted back)
12
+ attr_reader :secret
13
+ # XQueue-server-supplied nonce that will be needed to post back a grade for this submission
14
+ attr_accessor :files
15
+ attr_reader :submission_time
16
+ # When student submitted assignment via edX (a Time object)
17
+ attr_reader :student_id
18
+ # one-way hash of edX student ID
19
+ attr_accessor :score
20
+ # Numeric: score reported by autograder
21
+ attr_accessor :message
22
+ # String: textual feedback from autograder
23
+ attr_accessor :correct
24
+ # Boolean: if true when posted back, shows green checkmark, otherwise red X
25
+
26
+ validates_presence_of :student_id
27
+ validates_presence_of :submission_time
28
+ validates_presence_of :secret
29
+
30
+ DEFAULTS = {correct: false, score: 0, message: '', errors: ''}
31
+ def initialize(hash)
32
+ begin
33
+ fields_hash = DEFAULTS.merge(hash)
34
+ fields_hash.each {|key, value| instance_variable_set("@#{key}", value)}
35
+ rescue NoMethodError => e
36
+ if e.message == "undefined method `[]' for nil:NilClass"
37
+ raise InvalidSubmissionError, "Missing element(s) in JSON: #{hash}"
38
+ end
39
+ raise StandardError 'yoloswag'
40
+ end
41
+ end
42
+
43
+ def post_back()
44
+ @queue.put_result(@secret, @score, @correct, @message)
45
+ end
46
+
47
+ def self.parse_JSON(xqueue, json_response)
48
+ parsed = JSON.parse(json_response)
49
+ header, files, body = parsed['xqueue_header'], parsed['xqueue_files'], parsed['xqueue_body']
50
+ grader_payload = body['grader_payload']
51
+ anonymous_student_id, submission_time = body['student_info']['anonymous_student_id'], Time.parse(body['student_info']['submission_time'])
52
+ XQueueSubmission.new({queue: xqueue, secret: header, files: files, student_id: anonymous_student_id, submission_time: submission_time })
53
+ end
54
+
55
+ def expand_files
56
+ # @files = @files.map each do
57
+ # do something
58
+ # end
59
+ # self
60
+ end
61
+ end
metadata ADDED
@@ -0,0 +1,75 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: xqueue_ruby
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Armando Fox
8
+ - Aaron Zhang
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2015-06-15 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: builder
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ! '>='
19
+ - !ruby/object:Gem::Version
20
+ version: '0'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ! '>='
26
+ - !ruby/object:Gem::Version
27
+ version: '0'
28
+ - !ruby/object:Gem::Dependency
29
+ name: getopt
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ! '>='
33
+ - !ruby/object:Gem::Version
34
+ version: '0'
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ! '>='
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
42
+ description: Pull interface to Open edX XQueue
43
+ email: fox@cs.berkeley.edu
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - lib/xqueue_ruby.rb
49
+ - lib/xqueue_ruby/xqueue_ruby.rb
50
+ - lib/xqueue_ruby/xqueue_submission.rb
51
+ homepage: http://github.com/saasbook/x_queue
52
+ licenses:
53
+ - BSD
54
+ metadata: {}
55
+ post_install_message:
56
+ rdoc_options: []
57
+ require_paths:
58
+ - lib
59
+ required_ruby_version: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ! '>='
62
+ - !ruby/object:Gem::Version
63
+ version: '0'
64
+ required_rubygems_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ! '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ requirements: []
70
+ rubyforge_project:
71
+ rubygems_version: 2.4.8
72
+ signing_key:
73
+ specification_version: 4
74
+ summary: Pull interface to Open edX XQueue
75
+ test_files: []