~azzar1/unity/add-show-desktop-key

« back to all changes in this revision

Viewing changes to ivle/webapp/tutorial/service.py

  • Committer: David Coles
  • Date: 2010-08-30 03:26:13 UTC
  • Revision ID: coles.david@gmail.com-20100830032613-d14vng0jkelniu3l
python-console: Fix globals broken with new JSON library.

simplejson always returns unicode strings. cJSON would return ordinary strings 
if possible. cPickle.loads() only accepts strings. At present we use pickle 
version 0 so they should all works as ASCII strings. Higher versions of pickle 
are not plain ASCII and are likely to break this and so this should be fixed 
at some point.

Also replaced unconditional exception with one that catches Pickle errors. Not 
sure the best way to report failures of these functions.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
# IVLE - Informatics Virtual Learning Environment
2
 
# Copyright (C) 2007-2008 The University of Melbourne
 
2
# Copyright (C) 2007-2009 The University of Melbourne
3
3
#
4
4
# This program is free software; you can redistribute it and/or modify
5
5
# it under the terms of the GNU General Public License as published by
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
17
17
 
18
 
# Module: TutorialService
19
 
# Author: Matt Giuca
20
 
# Date:   25/1/2008
21
 
 
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.
25
 
 
26
 
# Calling syntax
27
 
# Path must be empty.
28
 
# The arguments determine what is to be done on this file.
29
 
 
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)
39
 
#   that date.
40
 
#   Returns JSON string containing code, or null.
41
 
 
42
 
# Returns a JSON response string indicating the results.
43
 
 
44
 
import os
45
 
import time
 
18
# Author: Matt Giuca, Nick Chadwick
 
19
 
 
20
'''AJAX backend for the tutorial application.'''
 
21
 
46
22
import datetime
47
23
 
48
 
import cjson
 
24
import genshi
 
25
from storm.locals import Store
49
26
 
50
 
from ivle import db
51
 
from ivle import util
52
 
from ivle import console
53
27
import ivle.database
54
 
import ivle.worksheet
55
 
import ivle.conf
56
 
import test # XXX: Really .test, not real test.
57
 
 
58
 
# If True, getattempts or getattempt will allow browsing of inactive/disabled
59
 
# attempts. If False, will not allow this.
60
 
HISTORY_ALLOW_INACTIVE = False
61
 
 
62
 
def handle(req):
63
 
    """Handler for Ajax backend TutorialService app."""
64
 
    # Set request attributes
65
 
    req.write_html_head_foot = False     # No HTML
66
 
 
67
 
    if req.path != "":
68
 
        req.throw_error(req.HTTP_BAD_REQUEST)
69
 
    fields = req.get_fieldstorage()
70
 
    act = fields.getfirst('action')
71
 
    exercise = fields.getfirst('exercise')
72
 
    if act is None or exercise is None:
73
 
        req.throw_error(req.HTTP_BAD_REQUEST)
74
 
    act = act.value
75
 
    exercise = exercise.value
76
 
 
77
 
    if act == 'save' or act == 'test':
78
 
        # Must be POST
79
 
        if req.method != 'POST':
80
 
            req.throw_error(req.HTTP_BAD_REQUEST)
81
 
 
82
 
        code = fields.getfirst('code')
83
 
        if code is None:
84
 
            req.throw_error(req.HTTP_BAD_REQUEST)
85
 
        code = code.value
86
 
 
87
 
        if act == 'save':
88
 
            handle_save(req, exercise, code, fields)
89
 
        else:   # act == "test"
90
 
            handle_test(req, exercise, code, fields)
91
 
    elif act == 'getattempts':
92
 
        handle_getattempts(req, exercise)
93
 
    elif act == 'getattempt':
94
 
        date = fields.getfirst('date')
95
 
        if date is None:
96
 
            req.throw_error(req.HTTP_BAD_REQUEST)
97
 
        date = date.value
98
 
        # Convert into a struct_time
99
 
        # The time *should* be in the same format as the DB (since it should
100
 
        # be bounced back to us from the getattempts output). Assume this.
101
 
        try:
102
 
            date = datetime.datetime.strptime(date, db.TIMESTAMP_FORMAT)
103
 
        except ValueError:
104
 
            # Date was not in correct format
105
 
            req.throw_error(req.HTTP_BAD_REQUEST)
106
 
        handle_getattempt(req, exercise, date)
107
 
    else:
108
 
        req.throw_error(req.HTTP_BAD_REQUEST)
109
 
 
110
 
def handle_save(req, exercise, code, fields):
111
 
    """Handles a save action. This saves the user's code without executing it.
112
 
    """
113
 
    # Need to open JUST so we know this is a real exercise.
114
 
    # (This avoids users submitting code for bogus exercises).
115
 
    exercisefile = util.open_exercise_file(exercise)
116
 
    if exercisefile is None:
117
 
        req.throw_error(req.HTTP_NOT_FOUND,
118
 
            "The exercise was not found.")
119
 
    exercisefile.close()
120
 
 
121
 
    req.write('{"result": "ok"}')
122
 
 
123
 
    conn = db.DB()
124
 
 
125
 
    try:
126
 
        conn.write_problem_save(
127
 
            login = req.user.login,
128
 
            exercisename = exercise,
129
 
            date = time.localtime(),
130
 
            text = code)
131
 
    finally:
132
 
        conn.close()
133
 
 
134
 
def handle_test(req, exercisesrc, code, fields):
135
 
    """Handles a test action."""
136
 
 
137
 
    exercisefile = util.open_exercise_file(exercisesrc)
138
 
    if exercisefile is None:
139
 
        req.throw_error(req.HTTP_NOT_FOUND,
140
 
            "The exercise was not found.")
141
 
 
142
 
    # Start a console to run the tests on
143
 
    jail_path = os.path.join(ivle.conf.jail_base, req.user.login)
144
 
    working_dir = os.path.join("/home", req.user.login)
145
 
    cons = console.Console(req.user.unixid, jail_path, working_dir)
146
 
 
147
 
    # Parse the file into a exercise object using the test suite
148
 
    exercise_obj = test.parse_exercise_file(exercisefile, cons)
149
 
    exercisefile.close()
150
 
 
151
 
    # Run the test cases. Get the result back as a JSONable object.
152
 
    # Return it.
153
 
    test_results = exercise_obj.run_tests(code)
154
 
 
155
 
    # Close the console
156
 
    cons.close()
157
 
 
158
 
    # Get the Exercise from the database
159
 
    exercise = ivle.database.Exercise.get_by_name(req.store, exercisesrc)
160
 
 
161
 
    conn = db.DB()
162
 
    try:
163
 
        conn.insert_problem_attempt(
164
 
            login = req.user.login,
165
 
            exercisename = exercisesrc,
166
 
            date = time.localtime(),
 
28
from ivle.database import Exercise, ExerciseAttempt, ExerciseSave, Worksheet, \
 
29
                          Offering, Subject, Semester, User, WorksheetExercise
 
30
import ivle.worksheet.utils
 
31
from ivle.webapp.base.rest import (JSONRESTView, write_operation,
 
32
                                   require_permission)
 
33
from ivle.webapp.errors import NotFound
 
34
 
 
35
 
 
36
TIMESTAMP_FORMAT = '%Y-%m-%d %H:%M:%S'
 
37
 
 
38
 
 
39
class AttemptsRESTView(JSONRESTView):
 
40
    '''REST view of a user's attempts at an exercise.'''
 
41
 
 
42
    @require_permission('edit')
 
43
    def GET(self, req):
 
44
        """Handles a GET Attempts action."""
 
45
        attempts = req.store.find(ExerciseAttempt, 
 
46
                ExerciseAttempt.ws_ex_id == self.context.worksheet_exercise.id,
 
47
                ExerciseAttempt.user_id == self.context.user.id)
 
48
        # attempts is a list of ExerciseAttempt objects. Convert to dictionaries
 
49
        time_fmt = lambda dt: datetime.datetime.strftime(dt, TIMESTAMP_FORMAT)
 
50
        attempts = [{'date': time_fmt(a.date), 'complete': a.complete}
 
51
                for a in attempts]
 
52
 
 
53
        return attempts
 
54
 
 
55
 
 
56
    @require_permission('edit')
 
57
    def PUT(self, req, data):
 
58
        """ Tests the given submission """
 
59
        # Trim off any trailing whitespace (can cause syntax errors in python)
 
60
        # While technically this is a user error, it causes a lot of confusion 
 
61
        # for student since it's "invisible".
 
62
        code = data['code'].rstrip()
 
63
 
 
64
        test_results = ivle.worksheet.utils.test_exercise_submission(
 
65
            req.config, req.user, self.context.worksheet_exercise.exercise,
 
66
            code)
 
67
 
 
68
        attempt = ivle.database.ExerciseAttempt(user=req.user,
 
69
            worksheet_exercise = self.context.worksheet_exercise,
 
70
            date = datetime.datetime.now(),
167
71
            complete = test_results['passed'],
168
 
            attempt = code)
169
 
    finally:
170
 
        conn.close()
171
 
 
172
 
    # Query the DB to get an updated score on whether or not this problem
173
 
    # has EVER been completed (may be different from "passed", if it has
174
 
    # been completed before), and the total number of attempts.
175
 
    completed, attempts = ivle.worksheet.get_exercise_status(req.store,
176
 
        req.user, exercise)
177
 
    test_results["completed"] = completed
178
 
    test_results["attempts"] = attempts
179
 
 
180
 
    req.write(cjson.encode(test_results))
181
 
 
182
 
def handle_getattempts(req, exercisename):
183
 
    """Handles a getattempts action."""
184
 
    exercise = ivle.database.Exercise.get_by_name(req.store, exercisename)
185
 
    attempts = ivle.worksheet.get_exercise_attempts(req.store, req.user,
186
 
        exercise, allow_inactive=HISTORY_ALLOW_INACTIVE)
187
 
    # attempts is a list of ExerciseAttempt objects. Convert to dictionaries.
188
 
    time_fmt = lambda dt: datetime.datetime.strftime(dt, db.TIMESTAMP_FORMAT)
189
 
    attempts = [{'date': time_fmt(a.date), 'complete': a.complete}
190
 
                for a in attempts]
191
 
    req.write(cjson.encode(attempts))
192
 
 
193
 
def handle_getattempt(req, exercisename, date):
194
 
    """Handles a getattempts action. Date is a datetime.datetime."""
195
 
    conn = db.DB()
196
 
    exercise = ivle.database.Exercise.get_by_name(req.store, exercisename)
197
 
    attempt = ivle.worksheet.get_exercise_attempt(req.store, req.user,
198
 
        exercise, as_of=date, allow_inactive=HISTORY_ALLOW_INACTIVE)
199
 
    if attempt is not None:
200
 
        attempt = attempt.text
201
 
    # attempt may be None; will write "null"
202
 
    req.write(cjson.encode({'code': attempt}))
 
72
            text = unicode(code)
 
73
        )
 
74
 
 
75
        req.store.add(attempt)
 
76
 
 
77
        # Query the DB to get an updated score on whether or not this problem
 
78
        # has EVER been completed (may be different from "passed", if it has
 
79
        # been completed before), and the total number of attempts.
 
80
        completed, attempts = ivle.worksheet.utils.get_exercise_status(
 
81
                req.store, req.user, self.context.worksheet_exercise)
 
82
        test_results["completed"] = completed
 
83
        test_results["attempts"] = attempts
 
84
 
 
85
        return test_results
 
86
 
 
87
 
 
88
class AttemptRESTView(JSONRESTView):
 
89
    '''REST view of an exercise attempt.'''
 
90
 
 
91
    @require_permission('view')
 
92
    def GET(self, req):
 
93
        return {'code': self.context.text}
 
94
 
 
95
 
 
96
class WorksheetExerciseRESTView(JSONRESTView):
 
97
    '''REST view of a worksheet exercise.'''
 
98
 
 
99
    @write_operation('view')
 
100
    def save(self, req, text):
 
101
        # Find the appropriate WorksheetExercise to save to. If its not found,
 
102
        # the user is submitting against a non-existant worksheet/exercise
 
103
 
 
104
        old_save = req.store.find(ExerciseSave,
 
105
            ExerciseSave.ws_ex_id == self.context.id,
 
106
            ExerciseSave.user == req.user).one()
 
107
        
 
108
        #Overwrite the old, or create a new if there isn't one
 
109
        if old_save is None:
 
110
            new_save = ExerciseSave()
 
111
            req.store.add(new_save)
 
112
        else:
 
113
            new_save = old_save
 
114
        
 
115
        new_save.worksheet_exercise = self.context
 
116
        new_save.user = req.user
 
117
        new_save.text = unicode(text)
 
118
        new_save.date = datetime.datetime.now()
 
119
 
 
120
        return {"result": "ok"}
 
121
 
 
122
 
 
123
class WorksheetsRESTView(JSONRESTView):
 
124
    """View used to update and create Worksheets."""
 
125
 
 
126
    @write_operation('edit_worksheets')
 
127
    def move_up(self, req, worksheetid):
 
128
        """Takes a list of worksheet-seq_no pairs and updates their 
 
129
        corresponding Worksheet objects to match."""
 
130
        
 
131
        worksheet_below = req.store.find(Worksheet,
 
132
            Worksheet.offering_id == self.context.id,
 
133
            Worksheet.identifier == unicode(worksheetid)).one()
 
134
        if worksheet_below is None:
 
135
            raise NotFound('worksheet_below')
 
136
        worksheet_above = req.store.find(Worksheet,
 
137
            Worksheet.offering_id == self.context.id,
 
138
            Worksheet.seq_no == (worksheet_below.seq_no - 1)).one()
 
139
        if worksheet_above is None:
 
140
            raise NotFound('worksheet_above')
 
141
 
 
142
        worksheet_below.seq_no = worksheet_below.seq_no - 1
 
143
        worksheet_above.seq_no = worksheet_above.seq_no + 1
 
144
        
 
145
        return {'result': 'ok'}
 
146
 
 
147
    @write_operation('edit_worksheets')
 
148
    def move_down(self, req, worksheetid):
 
149
        """Takes a list of worksheet-seq_no pairs and updates their 
 
150
        corresponding Worksheet objects to match."""
 
151
        
 
152
        worksheet_above = req.store.find(Worksheet,
 
153
            Worksheet.offering_id == self.context.id,
 
154
            Worksheet.identifier == unicode(worksheetid)).one()
 
155
        if worksheet_above is None:
 
156
            raise NotFound('worksheet_below')
 
157
        worksheet_below = req.store.find(Worksheet,
 
158
            Worksheet.offering_id == self.context.id,
 
159
            Worksheet.seq_no == (worksheet_above.seq_no + 1)).one()
 
160
        if worksheet_below is None:
 
161
            raise NotFound('worksheet_above')
 
162
 
 
163
        worksheet_below.seq_no = worksheet_below.seq_no - 1
 
164
        worksheet_above.seq_no = worksheet_above.seq_no + 1
 
165
        
 
166
        return {'result': 'ok'}