15
15
# along with this program; if not, write to the Free Software
16
16
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
18
# Module: TutorialService
22
# Provides the AJAX backend for the tutorial application.
23
# This allows several actions to be performed on the code the student has
24
# typed into one of the exercise boxes.
28
# The arguments determine what is to be done on this file.
30
# "action". One of the tutorialservice actions.
31
# "exercise" - The path to a exercise file (including the .xml extension),
32
# relative to the subjects base directory.
33
# action "save" or "test" (POST only):
34
# "code" - Full text of the student's code being submitted.
35
# action "getattempts": No arguments. Returns a list of
36
# {'date': 'formatted_date', 'complete': bool} dicts.
37
# action "getattempt":
38
# "date" - Formatted date. Gets most recent attempt before (and including)
40
# Returns JSON string containing code, or null.
42
# Returns a JSON response string indicating the results.
18
# Author: Matt Giuca, Nick Chadwick
20
'''AJAX backend for the tutorial application.'''
49
from common import (db, util)
26
from storm.locals import Store
30
from ivle.database import Exercise, ExerciseAttempt, ExerciseSave, Worksheet, \
31
Offering, Subject, Semester, User, WorksheetExercise
32
import ivle.worksheet.utils
33
import ivle.webapp.tutorial.test
34
from ivle.webapp.base.rest import (JSONRESTView, named_operation,
36
from ivle.webapp.errors import NotFound
53
38
# If True, getattempts or getattempt will allow browsing of inactive/disabled
54
39
# attempts. If False, will not allow this.
55
40
HISTORY_ALLOW_INACTIVE = False
58
"""Handler for Ajax backend TutorialService app."""
59
# Set request attributes
60
req.write_html_head_foot = False # No HTML
63
req.throw_error(req.HTTP_BAD_REQUEST)
64
fields = req.get_fieldstorage()
65
act = fields.getfirst('action')
66
exercise = fields.getfirst('exercise')
67
if act is None or exercise is None:
68
req.throw_error(req.HTTP_BAD_REQUEST)
70
exercise = exercise.value
72
if act == 'save' or act == 'test':
74
if req.method != 'POST':
75
req.throw_error(req.HTTP_BAD_REQUEST)
77
code = fields.getfirst('code')
79
req.throw_error(req.HTTP_BAD_REQUEST)
83
handle_save(req, exercise, code, fields)
85
handle_test(req, exercise, code, fields)
86
elif act == 'getattempts':
87
handle_getattempts(req, exercise)
88
elif act == 'getattempt':
89
date = fields.getfirst('date')
91
req.throw_error(req.HTTP_BAD_REQUEST)
93
# Convert into a struct_time
94
# The time *should* be in the same format as the DB (since it should
95
# be bounced back to us from the getattempts output). Assume this.
97
date = time.strptime(date, db.TIMESTAMP_FORMAT)
99
# Date was not in correct format
100
req.throw_error(req.HTTP_BAD_REQUEST)
101
handle_getattempt(req, exercise, date)
103
req.throw_error(req.HTTP_BAD_REQUEST)
105
def handle_save(req, exercise, code, fields):
106
"""Handles a save action. This saves the user's code without executing it.
42
TIMESTAMP_FORMAT = '%Y-%m-%d %H:%M:%S'
45
class ExerciseAttempts(object):
46
"""The set of exercise attempts for a user and exercise.
48
A combination of a User and WorksheetExercise, this provides access to
49
the User's ExerciseAttempts.
108
# Need to open JUST so we know this is a real exercise.
109
# (This avoids users submitting code for bogus exercises).
110
exercisefile = util.open_exercise_file(exercise)
111
if exercisefile is None:
112
req.throw_error(req.HTTP_NOT_FOUND,
113
"The exercise was not found.")
116
req.write('{"result": "ok"}')
121
conn.write_problem_save(
122
login = req.user.login,
123
exercisename = exercise,
124
date = time.localtime(),
129
def handle_test(req, exercise, code, fields):
130
"""Handles a test action."""
132
exercisefile = util.open_exercise_file(exercise)
133
if exercisefile is None:
134
req.throw_error(req.HTTP_NOT_FOUND,
135
"The exercise was not found.")
137
# Parse the file into a exercise object using the test suite
138
exercise_obj = test.parse_exercise_file(exercisefile)
140
# Run the test cases. Get the result back as a JSONable object.
142
test_results = exercise_obj.run_tests(code)
146
conn.insert_problem_attempt(
147
login = req.user.login,
148
exercisename = exercise,
149
date = time.localtime(),
52
def __init__(self, worksheet_exercise, user):
53
self.worksheet_exercise = worksheet_exercise
56
def get_permissions(self, user):
57
return self.user.get_permissions(user)
60
def exerciseattempts_to_attempt(exercise_attempts, date):
62
date = datetime.datetime.strptime(date, TIMESTAMP_FORMAT)
66
# XXX Hack around Google Code issue #87
67
# Query from the given date +1 secnod.
68
# Date is in seconds (eg. 3:47:12), while the data is in finer time
69
# (eg. 3:47:12.3625). The query "date <= 3:47:12" will fail because
70
# 3:47:12.3625 is greater. Hence we do the query from +1 second,
71
# "date <= 3:47:13", and it finds the correct submission, UNLESS there
72
# are multiple submissions inside the same second.
73
date += datetime.timedelta(seconds=1)
75
return ivle.worksheet.utils.get_exercise_attempt(
76
Store.of(exercise_attempts.user),
77
exercise_attempts.user, exercise_attempts.worksheet_exercise,
78
as_of=date, allow_inactive=HISTORY_ALLOW_INACTIVE)
81
def worksheet_exercise_to_user_attempts(worksheet_exercise, login):
82
user = User.get_by_login(Store.of(worksheet_exercise), login)
85
return ExerciseAttempts(worksheet_exercise, user)
88
def worksheet_to_worksheet_exercise(worksheet, exercise_name):
89
return Store.of(worksheet).find(
91
WorksheetExercise.exercise_id == exercise_name,
92
WorksheetExercise.worksheet == worksheet
96
class AttemptsRESTView(JSONRESTView):
97
'''REST view of a user's attempts at an exercise.'''
99
@require_permission('edit')
101
"""Handles a GET Attempts action."""
102
attempts = req.store.find(ExerciseAttempt,
103
ExerciseAttempt.ws_ex_id == self.context.worksheet_exercise.id,
104
ExerciseAttempt.user_id == self.context.user.id)
105
# attempts is a list of ExerciseAttempt objects. Convert to dictionaries
106
time_fmt = lambda dt: datetime.datetime.strftime(dt, TIMESTAMP_FORMAT)
107
attempts = [{'date': time_fmt(a.date), 'complete': a.complete}
113
@require_permission('edit')
114
def PUT(self, req, data):
115
""" Tests the given submission """
116
# Start a console to run the tests on
117
jail_path = os.path.join(req.config['paths']['jails']['mounts'],
119
working_dir = os.path.join("/home", req.user.login)
120
cons = ivle.console.Console(req.config, req.user.unixid, jail_path,
123
# Parse the file into a exercise object using the test suite
124
exercise_obj = ivle.webapp.tutorial.test.parse_exercise_file(
125
self.context.worksheet_exercise.exercise, cons)
127
# Run the test cases. Get the result back as a JSONable object.
129
test_results = exercise_obj.run_tests(data['code'])
134
attempt = ivle.database.ExerciseAttempt(user=req.user,
135
worksheet_exercise = self.context.worksheet_exercise,
136
date = datetime.datetime.now(),
150
137
complete = test_results['passed'],
138
text = unicode(data['code'])
141
req.store.add(attempt)
153
143
# Query the DB to get an updated score on whether or not this problem
154
144
# has EVER been completed (may be different from "passed", if it has
155
145
# been completed before), and the total number of attempts.
156
completed, attempts = conn.get_problem_status(req.user.login,
146
completed, attempts = ivle.worksheet.utils.get_exercise_status(
147
req.store, req.user, self.context.worksheet_exercise)
158
148
test_results["completed"] = completed
159
149
test_results["attempts"] = attempts
161
req.write(cjson.encode(test_results))
165
def handle_getattempts(req, exercise):
166
"""Handles a getattempts action."""
169
attempts = conn.get_problem_attempts(
170
login=req.user.login,
171
exercisename=exercise,
172
allow_inactive=HISTORY_ALLOW_INACTIVE)
173
req.write(cjson.encode(attempts))
177
def handle_getattempt(req, exercise, date):
178
"""Handles a getattempts action. Date is a struct_time."""
181
attempt = conn.get_problem_attempt(
182
login=req.user.login,
183
exercisename=exercise,
185
allow_inactive=HISTORY_ALLOW_INACTIVE)
186
# attempt may be None; will write "null"
187
req.write(cjson.encode(attempt))
154
class AttemptRESTView(JSONRESTView):
155
'''REST view of an exercise attempt.'''
157
@require_permission('view')
159
return {'code': self.context.text}
162
class WorksheetExerciseRESTView(JSONRESTView):
163
'''REST view of a worksheet exercise.'''
165
@named_operation('view')
166
def save(self, req, text):
167
# Find the appropriate WorksheetExercise to save to. If its not found,
168
# the user is submitting against a non-existant worksheet/exercise
170
old_save = req.store.find(ExerciseSave,
171
ExerciseSave.ws_ex_id == self.context.id,
172
ExerciseSave.user == req.user).one()
174
#Overwrite the old, or create a new if there isn't one
176
new_save = ExerciseSave()
177
req.store.add(new_save)
181
new_save.worksheet_exercise = self.context
182
new_save.user = req.user
183
new_save.text = unicode(text)
184
new_save.date = datetime.datetime.now()
186
return {"result": "ok"}
189
# Note that this is the view of an existing worksheet. Creation is handled
190
# by OfferingRESTView (as offerings have worksheets)
191
class WorksheetRESTView(JSONRESTView):
192
"""View used to update a worksheet."""
194
@named_operation('edit')
195
def save(self, req, name, assessable, data, format):
196
"""Takes worksheet data and saves it."""
197
self.context.name = unicode(name)
198
self.context.assessable = self.convert_bool(assessable)
199
self.context.data = unicode(data)
200
self.context.format = unicode(format)
201
ivle.worksheet.utils.update_exerciselist(self.context)
203
return {"result": "ok"}
205
class WorksheetsRESTView(JSONRESTView):
206
"""View used to update and create Worksheets."""
208
@named_operation('edit')
209
def add_worksheet(self, req, identifier, name, assessable, data, format):
210
"""Takes worksheet data and adds it."""
212
new_worksheet = Worksheet()
213
new_worksheet.seq_no = self.context.worksheets.count()
214
# Setting new_worksheet.offering implicitly adds new_worksheet,
215
# hence worksheets.count MUST be called above it
216
new_worksheet.offering = self.context
217
new_worksheet.identifier = unicode(identifier)
218
new_worksheet.name = unicode(name)
219
new_worksheet.assessable = self.convert_bool(assessable)
220
new_worksheet.data = unicode(data)
221
new_worksheet.format = unicode(format)
223
# This call is added for clarity, as the worksheet is implicitly added.
224
req.store.add(new_worksheet)
226
ivle.worksheet.utils.update_exerciselist(new_worksheet)
228
return {"result": "ok"}
230
@named_operation('edit')
231
def move_up(self, req, worksheetid):
232
"""Takes a list of worksheet-seq_no pairs and updates their
233
corresponding Worksheet objects to match."""
235
worksheet_below = req.store.find(Worksheet,
236
Worksheet.offering_id == self.context.id,
237
Worksheet.identifier == unicode(worksheetid)).one()
238
if worksheet_below is None:
239
raise NotFound('worksheet_below')
240
worksheet_above = req.store.find(Worksheet,
241
Worksheet.offering_id == self.context.id,
242
Worksheet.seq_no == (worksheet_below.seq_no - 1)).one()
243
if worksheet_above is None:
244
raise NotFound('worksheet_above')
246
worksheet_below.seq_no = worksheet_below.seq_no - 1
247
worksheet_above.seq_no = worksheet_above.seq_no + 1
249
return {'result': 'ok'}
251
@named_operation('edit')
252
def move_down(self, req, worksheetid):
253
"""Takes a list of worksheet-seq_no pairs and updates their
254
corresponding Worksheet objects to match."""
256
worksheet_above = req.store.find(Worksheet,
257
Worksheet.offering_id == self.context.id,
258
Worksheet.identifier == unicode(worksheetid)).one()
259
if worksheet_above is None:
260
raise NotFound('worksheet_below')
261
worksheet_below = req.store.find(Worksheet,
262
Worksheet.offering_id == self.context.id,
263
Worksheet.seq_no == (worksheet_above.seq_no + 1)).one()
264
if worksheet_below is None:
265
raise NotFound('worksheet_above')
267
worksheet_below.seq_no = worksheet_below.seq_no - 1
268
worksheet_above.seq_no = worksheet_above.seq_no + 1
270
return {'result': 'ok'}