xqueue_ruby 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|