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)
28
from ivle.database import Exercise, ExerciseAttempt, ExerciseSave
31
import ivle.webapp.tutorial.test
33
from ivle.webapp.base.rest import (JSONRESTView, named_operation,
35
from ivle.webapp.errors import NotFound
53
37
# If True, getattempts or getattempt will allow browsing of inactive/disabled
54
38
# attempts. If False, will not allow this.
55
39
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.
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(),
150
complete = test_results['passed'],
41
TIMESTAMP_FORMAT = '%Y-%m-%d %H:%M:%S'
44
class AttemptsRESTView(JSONRESTView):
45
'''REST view of a user's attempts at an exercise.'''
46
def __init__(self, req, subject, year, semester, worksheet,
48
self.user = ivle.database.User.get_by_login(req.store, username)
51
self.exercise = exercise
53
self.context = self.user # XXX: Not quite right.
55
@require_permission('edit')
57
"""Handles a GET Attempts action."""
58
exercise = req.store.find(Exercise, Exercise.id == self.exercise).one()
60
attempts = req.store.find(ExerciseAttempt,
61
ExerciseAttempt.exercise_id == exercise.id,
62
ExerciseAttempt.user_id == self.user.id)
63
# attempts is a list of ExerciseAttempt objects. Convert to dictionaries
64
time_fmt = lambda dt: datetime.datetime.strftime(dt, TIMESTAMP_FORMAT)
65
attempts = [{'date': time_fmt(a.date), 'complete': a.complete}
71
@require_permission('edit')
72
def PUT(self, req, data):
73
""" Tests the given submission """
74
exercise = req.store.find(Exercise, Exercise.id == self.exercise).one()
78
# Start a console to run the tests on
79
jail_path = os.path.join(ivle.conf.jail_base, req.user.login)
80
working_dir = os.path.join("/home", req.user.login)
81
cons = ivle.console.Console(req.user.unixid, jail_path, working_dir)
83
# Parse the file into a exercise object using the test suite
84
exercise_obj = ivle.webapp.tutorial.test.parse_exercise_file(
87
# Run the test cases. Get the result back as a JSONable object.
89
test_results = exercise_obj.run_tests(data['code'])
94
attempt = ivle.database.ExerciseAttempt(user=req.user,
96
date=datetime.datetime.now(),
97
complete=test_results['passed'],
99
text=unicode(data['code']))
101
req.store.add(attempt)
153
103
# Query the DB to get an updated score on whether or not this problem
154
104
# has EVER been completed (may be different from "passed", if it has
155
105
# been completed before), and the total number of attempts.
156
completed, attempts = conn.get_problem_status(req.user.login,
106
completed, attempts = ivle.worksheet.get_exercise_status(req.store,
158
108
test_results["completed"] = completed
159
109
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))
114
class AttemptRESTView(JSONRESTView):
115
'''REST view of an exercise attempt.'''
117
def __init__(self, req, subject, worksheet, exercise, username, date):
118
# TODO: Find exercise within worksheet.
119
user = ivle.database.User.get_by_login(req.store, username)
124
date = datetime.datetime.strptime(date, TIMESTAMP_FORMAT)
128
exercise = ivle.database.Exercise.get_by_name(req.store, exercise)
129
attempt = ivle.worksheet.get_exercise_attempt(req.store, user,
130
exercise, as_of=date, allow_inactive=HISTORY_ALLOW_INACTIVE)
135
self.context = attempt
137
@require_permission('view')
139
return {'code': self.context.text}
142
class ExerciseRESTView(JSONRESTView):
143
'''REST view of an exercise.'''
145
def get_permissions(self, user):
146
# XXX: Do it properly.
152
@named_operation('save')
153
def save(self, req, text):
154
# Need to open JUST so we know this is a real exercise.
155
# (This avoids users submitting code for bogus exercises).
156
exercise = req.store.find(Exercise,
157
Exercise.id == self.exercise).one()
158
ivle.worksheet.save_exercise(req.store, req.user, exercise,
159
unicode(text), datetime.datetime.now())
160
return {"result": "ok"}