xqueue_ruby 0.0.1
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.
- checksums.yaml +15 -0
- data/lib/xqueue_ruby.rb +8 -0
- data/lib/xqueue_ruby/xqueue_ruby.rb +184 -0
- data/lib/xqueue_ruby/xqueue_submission.rb +61 -0
- metadata +75 -0
checksums.yaml
ADDED
@@ -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=
|
data/lib/xqueue_ruby.rb
ADDED
@@ -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: []
|