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 ivle import (db, util, console)
28
from ivle.database import Exercise, ExerciseAttempt, ExerciseSave, Worksheet, Offering, Subject, Semester
51
import test # XXX: Really .test, not real test.
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
# Start a console to run the tests on
138
jail_path = os.path.join(ivle.conf.jail_base, req.user.login)
139
working_dir = os.path.join("/home", req.user.login)
140
cons = console.Console(req.user.unixid, jail_path, working_dir)
142
# Parse the file into a exercise object using the test suite
143
exercise_obj = test.parse_exercise_file(exercisefile, cons)
146
# Run the test cases. Get the result back as a JSONable object.
148
test_results = exercise_obj.run_tests(code)
155
conn.insert_problem_attempt(
156
login = req.user.login,
157
exercisename = exercise,
158
date = time.localtime(),
159
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.worksheet = req.store.find(Worksheet,
54
Worksheet.name == worksheet,
55
Worksheet.offering_id == Offering.id,
56
Offering.subject_id == Subject.id,
57
Subject.code == subject,
58
Offering.semester_id == Semester.id,
59
Semester.year == year,
60
Semester.semester == semester).one()
62
self.context = self.user # XXX: Not quite right.
64
@require_permission('edit')
66
"""Handles a GET Attempts action."""
67
exercise = req.store.find(Exercise, Exercise.id == self.exercise).one()
69
attempts = req.store.find(ExerciseAttempt,
70
ExerciseAttempt.exercise_id == exercise.id,
71
ExerciseAttempt.user_id == self.user.id)
72
# attempts is a list of ExerciseAttempt objects. Convert to dictionaries
73
time_fmt = lambda dt: datetime.datetime.strftime(dt, TIMESTAMP_FORMAT)
74
attempts = [{'date': time_fmt(a.date), 'complete': a.complete}
80
@require_permission('edit')
81
def PUT(self, req, data):
82
""" Tests the given submission """
83
exercise = req.store.find(Exercise, Exercise.id == self.exercise).one()
87
# Start a console to run the tests on
88
jail_path = os.path.join(ivle.conf.jail_base, req.user.login)
89
working_dir = os.path.join("/home", req.user.login)
90
cons = ivle.console.Console(req.user.unixid, jail_path, working_dir)
92
# Parse the file into a exercise object using the test suite
93
exercise_obj = ivle.webapp.tutorial.test.parse_exercise_file(
96
# Run the test cases. Get the result back as a JSONable object.
98
test_results = exercise_obj.run_tests(data['code'])
103
attempt = ivle.database.ExerciseAttempt(user=req.user,
105
date=datetime.datetime.now(),
106
complete=test_results['passed'],
107
worksheet=self.worksheet,
108
text=unicode(data['code']))
110
req.store.add(attempt)
162
112
# Query the DB to get an updated score on whether or not this problem
163
113
# has EVER been completed (may be different from "passed", if it has
164
114
# been completed before), and the total number of attempts.
165
completed, attempts = conn.get_problem_status(req.user.login,
115
completed, attempts = ivle.worksheet.get_exercise_status(req.store,
116
req.user, exercise, self.worksheet)
167
117
test_results["completed"] = completed
168
118
test_results["attempts"] = attempts
170
req.write(cjson.encode(test_results))
174
def handle_getattempts(req, exercise):
175
"""Handles a getattempts action."""
178
attempts = conn.get_problem_attempts(
179
login=req.user.login,
180
exercisename=exercise,
181
allow_inactive=HISTORY_ALLOW_INACTIVE)
182
req.write(cjson.encode(attempts))
186
def handle_getattempt(req, exercise, date):
187
"""Handles a getattempts action. Date is a struct_time."""
190
attempt = conn.get_problem_attempt(
191
login=req.user.login,
192
exercisename=exercise,
194
allow_inactive=HISTORY_ALLOW_INACTIVE)
195
# attempt may be None; will write "null"
196
req.write(cjson.encode({'code': attempt}))
123
class AttemptRESTView(JSONRESTView):
124
'''REST view of an exercise attempt.'''
126
def __init__(self, req, subject, worksheet, exercise, username, date):
127
# TODO: Find exercise within worksheet.
128
user = ivle.database.User.get_by_login(req.store, username)
133
date = datetime.datetime.strptime(date, TIMESTAMP_FORMAT)
137
exercise = ivle.database.Exercise.get_by_name(req.store, exercise)
138
worksheet = req.store.find(Worksheet,
139
Worksheet.name == self.worksheet,
140
Worksheet.offering_id == Offering.id,
141
Offering.subject_id == Subject.id,
142
Subject.code == self.subject,
143
Offering.semester_id == Semester.id,
144
Semester.year == self.year,
145
Semester.semester == self.semester).one()
147
attempt = ivle.worksheet.get_exercise_attempt(req.store, user,
148
exercise, worksheet, as_of=date, allow_inactive=HISTORY_ALLOW_INACTIVE)
153
self.context = attempt
155
@require_permission('view')
157
return {'code': self.context.text}
160
class ExerciseRESTView(JSONRESTView):
161
'''REST view of an exercise.'''
163
def get_permissions(self, user):
164
# XXX: Do it properly.
170
@named_operation('save')
171
def save(self, req, text):
172
# Need to open JUST so we know this is a real exercise.
173
# (This avoids users submitting code for bogus exercises).
174
exercise = req.store.find(Exercise,
175
Exercise.id == self.exercise).one()
176
worksheet = req.store.find(Worksheet,
177
Worksheet.name == self.worksheet,
178
Worksheet.offering_id == Offering.id,
179
Offering.subject_id == Subject.id,
180
Subject.code == self.subject,
181
Offering.semester_id == Semester.id,
182
Semester.year == self.year,
183
Semester.semester == self.semester).one()
184
ivle.worksheet.save_exercise(req.store, req.user, exercise, worksheet,
185
unicode(text), datetime.datetime.now())
186
return {"result": "ok"}