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

193 by mattgiuca
Apps: Added stubs for the 3 new apps, Editor, Console and Tutorial.
1
# IVLE
2
# Copyright (C) 2007-2008 The University of Melbourne
3
#
4
# This program is free software; you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation; either version 2 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU General Public License for more details.
13
#
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
17
1099.1.19 by William Grant
ivle.webapp.tutorial: Port www/apps/tutorial to new framework.
18
# Author: Matt Giuca, Will Grant
19
20
'''Tutorial/worksheet/exercise application.
21
22
Displays tutorial content with editable exercises, allowing students to test
23
and submit their solutions to exercises and have them auto-tested.
24
'''
287 by mattgiuca
setup.py: Added new conf.py variable: subjects_base. This is for storing the
25
26
import os
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
27
import urllib
28
import re
1099.1.36 by William Grant
ivle.webapp.tutorial: Clean up.
29
import mimetypes
30
from datetime import datetime
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
31
from xml.dom import minidom
193 by mattgiuca
Apps: Added stubs for the 3 new apps, Editor, Console and Tutorial.
32
1384.1.1 by William Grant
De-AJAXify WorksheetAddView.
33
import formencode
34
import formencode.validators
1099.1.19 by William Grant
ivle.webapp.tutorial: Port www/apps/tutorial to new framework.
35
import genshi
1384.1.11 by William Grant
If genshi fails to parse a worksheet while saving, render the error nicely.
36
import genshi.input
1384.1.1 by William Grant
De-AJAXify WorksheetAddView.
37
from genshi.filters import HTMLFormFiller
307 by mattgiuca
tutorial: Now each problem div has an ID. Added submit buttons which call
38
1080.1.32 by me at id
www/app/{subjects,tutorial}: Use the new Storm API to get enrolled subjects.
39
import ivle.database
1099.1.180 by Nick Chadwick
This commit changes the tutorial service, which now almost exclusively
40
from ivle.database import Subject, Offering, Semester, Exercise, \
1294.2.64 by William Grant
Port tutorial stuff.
41
                          ExerciseSave, WorksheetExercise, ExerciseAttempt
1442.1.27 by William Grant
Undo i.w.tutorial's renaming of i.d.Worksheet to DBWorksheet, since the other Worksheet is gone.
42
from ivle.database import Worksheet
1099.1.220 by Nick Chadwick
Merged from trunk
43
import ivle.worksheet.utils
1294.2.64 by William Grant
Port tutorial stuff.
44
from ivle.webapp import ApplicationRoot
1099.1.34 by William Grant
Split up ivle.webapp.base.views into ivle.webapp.base.{rest,xhtml}, as it was
45
from ivle.webapp.base.views import BaseView
46
from ivle.webapp.base.xhtml import XHTMLView
1099.1.99 by William Grant
Require that plugins providing media subclass MediaPlugin.
47
from ivle.webapp.base.plugins import ViewPlugin, MediaPlugin
1294.2.131 by William Grant
Restore SubjectMediaView.
48
from ivle.webapp.media import media_url
1294.2.71 by William Grant
Move i.w.tutorial traversal stuff into i.w.tutorial.traversal.
49
from ivle.webapp.errors import NotFound
1099.1.220 by Nick Chadwick
Merged from trunk
50
from ivle.worksheet.rst import rst as rstfunc
1294.2.71 by William Grant
Move i.w.tutorial traversal stuff into i.w.tutorial.traversal.
51
1294.2.64 by William Grant
Port tutorial stuff.
52
from ivle.webapp.tutorial.service import (AttemptsRESTView, AttemptRESTView,
1384.1.14 by William Grant
Remove unused WorksheetRESTView and WorksheetsRESTView.add_worksheet.
53
            WorksheetExerciseRESTView, WorksheetsRESTView)
1099.1.216 by Nick Chadwick
Started adding in add and save options in the exercise edit view, to
54
from ivle.webapp.tutorial.exercise_service import ExercisesRESTView, \
55
                                                  ExerciseRESTView
1294.3.2 by William Grant
Router->Publisher
56
from ivle.webapp.tutorial.publishing import (root_to_exercise, exercise_url,
1294.2.71 by William Grant
Move i.w.tutorial traversal stuff into i.w.tutorial.traversal.
57
            offering_to_worksheet, worksheet_url,
58
            worksheet_to_worksheetexercise, worksheetexercise_url,
59
            ExerciseAttempts, worksheetexercise_to_exerciseattempts,
60
            exerciseattempts_url, exerciseattempts_to_attempt,
61
            exerciseattempt_url)
1294.2.100 by William Grant
Add exercise/worksheet breadcrumbs.
62
from ivle.webapp.tutorial.breadcrumbs import (ExerciseBreadcrumb,
63
            WorksheetBreadcrumb)
1294.2.131 by William Grant
Restore SubjectMediaView.
64
from ivle.webapp.tutorial.media import (SubjectMediaFile, SubjectMediaView,
65
    subject_to_media)
523 by stevenbird
Adding ReStructured Text preprocessing of exercise descriptions,
66
1099.1.19 by William Grant
ivle.webapp.tutorial: Port www/apps/tutorial to new framework.
67
68
class WorksheetView(XHTMLView):
69
    '''The view of a worksheet with exercises.'''
1099.1.192 by Nick Chadwick
Moved the tutorial templates in a new directory to keep tutorial cleaner
70
    template = 'templates/worksheet.html'
1116 by William Grant
Move the old tutorial views into the 'subjects' tab, so they get the right
71
    tab = 'subjects'
1099.1.110 by William Grant
Implement an authorization system in the new framework. This breaks the REST
72
    permission = 'view'
1099.1.19 by William Grant
ivle.webapp.tutorial: Port www/apps/tutorial to new framework.
73
74
    def populate(self, req, ctx):
1099.1.64 by William Grant
Move ivle.webapp.tutorial's media to the new framework. This also fixes the
75
        self.plugin_scripts[Plugin] = ['tutorial.js']
1099.1.218 by Nick Chadwick
tutorials can now use RST
76
        self.plugin_styles[Plugin] = ['tutorial.css', 'worksheet.css']
1099.1.19 by William Grant
ivle.webapp.tutorial: Port www/apps/tutorial to new framework.
77
1099.1.110 by William Grant
Implement an authorization system in the new framework. This breaks the REST
78
        if not self.context:
1099.1.19 by William Grant
ivle.webapp.tutorial: Port www/apps/tutorial to new framework.
79
            raise NotFound()
80
1099.1.207 by William Grant
Replace most of the tutorial headings and titles.
81
        ctx['subject'] = self.context.offering.subject
82
        ctx['worksheet'] = self.context
1294.2.64 by William Grant
Port tutorial stuff.
83
        ctx['semester'] = self.context.offering.semester.semester
84
        ctx['year'] = self.context.offering.semester.year
1099.1.220 by Nick Chadwick
Merged from trunk
85
86
        ctx['worksheetstream'] = genshi.Stream(list(genshi.XML(self.context.get_xml())))
1141 by William Grant
Display an edit link in WorksheetView, if we have privileges.
87
        ctx['user'] = req.user
1544 by Matt Giuca
Added an argument 'config' to every single get_permissions method throughout the program. All calls to get_permissions pass a config. This is to allow per-site policy configurations on permissions.
88
        ctx['config'] = req.config
1099.1.19 by William Grant
ivle.webapp.tutorial: Port www/apps/tutorial to new framework.
89
1099.1.150 by Nick Chadwick
Modified worksheets to properly link attempts to worksheets and
90
        generate_worksheet_data(ctx, req, self.context)
1099.1.19 by William Grant
ivle.webapp.tutorial: Port www/apps/tutorial to new framework.
91
92
        ctx['worksheetstream'] = add_exercises(ctx['worksheetstream'], ctx, req)
93
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
94
def get_worksheets(subjectfile):
95
    '''Given a subject stream, get all the worksheets and put them in ctx'''
96
    worksheets = []
97
    for kind, data, pos in subjectfile:
98
        if kind is genshi.core.START:
99
            if data[0] == 'worksheet':
100
                worksheetid = ''
101
                worksheetname = ''
102
                worksheetasses = False
103
                for attr in data[1]:
104
                    if attr[0] == 'id':
105
                        worksheetid = attr[1]
106
                    elif attr[0] == 'name':
107
                        worksheetname = attr[1]
108
                    elif attr[0] == 'assessable':
109
                        worksheetasses = attr[1] == 'true'
110
                worksheets.append(Worksheet(worksheetid, worksheetname, \
111
                                                            worksheetasses))
112
    return worksheets
113
1099.1.180 by Nick Chadwick
This commit changes the tutorial service, which now almost exclusively
114
# This generator adds in the exercises as they are required. This is returned.
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
115
def add_exercises(stream, ctx, req):
1099.1.42 by Nick Chadwick
Fixed an oversight in the tutorial code which was printing <worksheet>
116
    """A filter which adds exercises into the stream."""
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
117
    exid = 0
118
    for kind, data, pos in stream:
119
        if kind is genshi.core.START:
1099.1.42 by Nick Chadwick
Fixed an oversight in the tutorial code which was printing <worksheet>
120
            # Remove the worksheet tags, as they are not xhtml valid.
121
            if data[0] == 'worksheet':
122
                continue
123
            # If we have an exercise node, replace it with the content of the
1384.1.12 by William Grant
When substituting <exercise> nodes with actual exercises, only consider nodes with src attributes.
124
            # exercise. Note that we only consider exercises with a
125
            # 'src' attribute, the same condition that generate_worksheet_data
126
            # uses to create ctx['exercises'].
127
            elif data[0] == 'exercise' and 'src' in dict(data[1]):
1099.1.180 by Nick Chadwick
This commit changes the tutorial service, which now almost exclusively
128
                # XXX: Note that we presume ctx['exercises'] has a correct list
129
                #      of exercises. If it doesn't, something has gone wrong.
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
130
                new_stream = ctx['exercises'][exid]['stream']
131
                exid += 1
132
                for item in new_stream:
133
                    yield item
134
            else:
135
                yield kind, data, pos
1099.1.42 by Nick Chadwick
Fixed an oversight in the tutorial code which was printing <worksheet>
136
        # Remove the end tags for exercises and worksheets
137
        elif kind is genshi.core.END:
138
            if data == 'exercise':
139
                continue
140
            elif data == 'worksheet':
141
                continue
142
            else:
143
                yield kind, data, pos
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
144
        else:
145
            yield kind, data, pos
146
147
# This function runs through the worksheet, to get data on the exercises to
148
# build a Table of Contents, as well as fill in details in ctx
1099.1.150 by Nick Chadwick
Modified worksheets to properly link attempts to worksheets and
149
def generate_worksheet_data(ctx, req, worksheet):
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
150
    """Runs through the worksheetstream, generating the exericises"""
151
    ctx['exercises'] = []
152
    ctx['exerciselist'] = []
153
    for kind, data, pos in ctx['worksheetstream']:
154
        if kind is genshi.core.START:
155
            if data[0] == 'exercise':
156
                src = ""
157
                optional = False
158
                for attr in data[1]:
159
                    if attr[0] == 'src':
160
                        src = attr[1]
161
                    if attr[0] == 'optional':
162
                        optional = attr[1] == 'true'
163
                # Each item in toc is of type (name, complete, stream)
1099.4.3 by Nick Chadwick
Updated the tutorial service, to now allow users to edit worksheets
164
                if src != "":
165
                    ctx['exercises'].append(present_exercise(req, src, worksheet))
166
                    ctx['exerciselist'].append((src, optional))
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
167
            elif data[0] == 'worksheet':
168
                ctx['worksheetname'] = 'bob'
169
                for attr in data[1]:
170
                    if attr[0] == 'name':
171
                        ctx['worksheetname'] = attr[1]
425 by mattgiuca
tutorial: Refactored present_worksheet so it has a separate function for
172
297 by mattgiuca
tutorial: Now presents problems correctly, by parsing XML source and writing
173
def innerXML(elem):
174
    """Given an element, returns its children as XML strings concatenated
175
    together."""
176
    s = ""
177
    for child in elem.childNodes:
178
        s += child.toxml()
179
    return s
180
181
def getTextData(element):
182
    """ Get the text and cdata inside an element
183
    Leading and trailing whitespace are stripped
184
    """
185
    data = ''
186
    for child in element.childNodes:
187
        if child.nodeType == child.CDATA_SECTION_NODE:
188
            data += child.data
710 by mattgiuca
Tutorial: The tutorial system now presents a table of contents at the top.
189
        elif child.nodeType == child.TEXT_NODE:
297 by mattgiuca
tutorial: Now presents problems correctly, by parsing XML source and writing
190
            data += child.data
710 by mattgiuca
Tutorial: The tutorial system now presents a table of contents at the top.
191
        elif child.nodeType == child.ELEMENT_NODE:
192
            data += getTextData(child)
297 by mattgiuca
tutorial: Now presents problems correctly, by parsing XML source and writing
193
194
    return data.strip()
195
1394.2.2 by William Grant
present_exercise() now works on exercises without worksheets.
196
def present_exercise(req, identifier, worksheet=None):
197
    """Render an HTML representation of an exercise.
198
199
    identifier: The exercise identifier (URL name).
200
    worksheet: An optional worksheet from which to retrieve saved results.
201
               If omitted, a clean exercise will be presented.
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
202
    """
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
203
    # Exercise-specific context is used here, as we already have all the data
204
    # we need
205
    curctx = genshi.template.Context()
1394.2.3 by William Grant
Only show exercise save/reset buttons if we are in a worksheet, as otherwise we can store no state.
206
    curctx['worksheet'] = worksheet
1099.1.180 by Nick Chadwick
This commit changes the tutorial service, which now almost exclusively
207
1394.2.2 by William Grant
present_exercise() now works on exercises without worksheets.
208
    if worksheet is not None:
209
        worksheet_exercise = req.store.find(WorksheetExercise,
210
            WorksheetExercise.worksheet_id == worksheet.id,
211
            WorksheetExercise.exercise_id == identifier).one()
1099.1.180 by Nick Chadwick
This commit changes the tutorial service, which now almost exclusively
212
1394.2.2 by William Grant
present_exercise() now works on exercises without worksheets.
213
        if worksheet_exercise is None:
214
            raise NotFound()
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
215
216
    # Retrieve the exercise details from the database
1099.1.180 by Nick Chadwick
This commit changes the tutorial service, which now almost exclusively
217
    exercise = req.store.find(Exercise, 
1394.2.2 by William Grant
present_exercise() now works on exercises without worksheets.
218
        Exercise.id == identifier).one()
1099.1.180 by Nick Chadwick
This commit changes the tutorial service, which now almost exclusively
219
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
220
    if exercise is None:
1394.2.2 by William Grant
present_exercise() now works on exercises without worksheets.
221
        raise ExerciseNotFound(identifier)
1099.1.93 by William Grant
Remove remaining uses of req.throw_error in the new webapps.
222
515 by stevenbird
Propagated "problem" -> "exercise" nomenclature change.
223
    # Read exercise file and present the exercise
297 by mattgiuca
tutorial: Now presents problems correctly, by parsing XML source and writing
224
    # Note: We do not use the testing framework because it does a lot more
515 by stevenbird
Propagated "problem" -> "exercise" nomenclature change.
225
    # work than we need. We just need to get the exercise name and a few other
297 by mattgiuca
tutorial: Now presents problems correctly, by parsing XML source and writing
226
    # fields from the XML.
227
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
228
    curctx['exercise'] = exercise
229
    if exercise.description is not None:
1099.4.1 by Nick Chadwick
Working on putting worksheets into the database.
230
        desc = rstfunc(exercise.description)
231
        curctx['description'] = genshi.XML('<div id="description">' + desc + 
232
                                           '</div>')
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
233
    else:
234
        curctx['description'] = None
297 by mattgiuca
tutorial: Now presents problems correctly, by parsing XML source and writing
235
702 by mattgiuca
tutorial:
236
    # If the user has already saved some text for this problem, or submitted
237
    # an attempt, then use that text instead of the supplied "partial".
1099.1.180 by Nick Chadwick
This commit changes the tutorial service, which now almost exclusively
238
    # Get exercise stored text will return a save, or the most recent attempt,
239
    # whichever is more recent
1394.2.2 by William Grant
present_exercise() now works on exercises without worksheets.
240
    if worksheet is not None:
241
        save = ivle.worksheet.utils.get_exercise_stored_text(
242
                            req.store, req.user, worksheet_exercise)
1099.1.180 by Nick Chadwick
This commit changes the tutorial service, which now almost exclusively
243
1394.2.2 by William Grant
present_exercise() now works on exercises without worksheets.
244
        # Also get the number of attempts taken and whether this is complete.
245
        complete, curctx['attempts'] = \
246
                ivle.worksheet.utils.get_exercise_status(req.store, req.user, 
247
                                                         worksheet_exercise)
248
        if save is not None:
249
            curctx['exercisesave'] = save.text
250
        else:
251
            curctx['exercisesave']= exercise.partial
252
        curctx['complete'] = 'Complete' if complete else 'Incomplete'
253
        curctx['complete_class'] = curctx['complete'].lower()
1099.1.150 by Nick Chadwick
Modified worksheets to properly link attempts to worksheets and
254
    else:
1394.2.2 by William Grant
present_exercise() now works on exercises without worksheets.
255
        curctx['exercisesave'] = exercise.partial
256
        curctx['complete'] = 'Incomplete'
257
        curctx['complete_class'] = curctx['complete'].lower()
258
        curctx['attempts'] = 0
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
259
260
    #Save the exercise details to the Table of Contents
261
262
    loader = genshi.template.TemplateLoader(".", auto_reload=True)
1109 by matt.giuca
tutorial/__init__.py: Fixed path to exercises template
263
    tmpl = loader.load(os.path.join(os.path.dirname(__file__),
1394.2.1 by William Grant
Move exercise.html to exercise_fragment.html, as it will be reused soon.
264
        "templates/exercise_fragment.html"))
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
265
    ex_stream = tmpl.generate(curctx)
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
266
    return {'name': exercise.name,
1099.1.45 by William Grant
ivle.webapp.tutorial: Recapitalise 'Complete' and 'Incomplete' in body text.
267
            'complete': curctx['complete_class'],
268
            'stream': ex_stream,
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
269
            'exid': exercise.id}
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
270
1099.1.19 by William Grant
ivle.webapp.tutorial: Port www/apps/tutorial to new framework.
271
1482 by Matt Giuca
Worksheet editor: Made the list of formats an assoc list, not a dict, so it preserves ordering. Put rst first, since it is the recommended.
272
# The first element is the default format
273
WORKSHEET_FORMATS = (('reStructuredText', 'rst'), ('XHTML (legacy)', 'xml'))
1384.1.1 by William Grant
De-AJAXify WorksheetAddView.
274
1384.1.7 by William Grant
De-AJAXify WorksheetEditView.
275
1384.1.2 by William Grant
Verify that the worksheet format is valid.
276
class WorksheetFormatValidator(formencode.FancyValidator):
277
    """A FormEncode validator that turns a username into a user.
278
279
    The state must have a 'store' attribute, which is the Storm store
280
    to use."""
281
    def _to_python(self, value, state):
1482 by Matt Giuca
Worksheet editor: Made the list of formats an assoc list, not a dict, so it preserves ordering. Put rst first, since it is the recommended.
282
        if value not in [x for (_,x) in WORKSHEET_FORMATS]:
1384.1.2 by William Grant
Verify that the worksheet format is valid.
283
            raise formencode.Invalid('Unsupported format', value, state)
284
        return value
285
286
1384.1.4 by William Grant
Validate worksheet identifer uniqueness.
287
class WorksheetIdentifierUniquenessValidator(formencode.FancyValidator):
288
    """A FormEncode validator that checks that a worksheet name is unused.
289
1384.1.8 by William Grant
Correctly validate the identifier when renaming a worksheet.
290
    The worksheet referenced by state.existing_worksheet is permitted
1384.1.4 by William Grant
Validate worksheet identifer uniqueness.
291
    to hold that name. If any other object holds it, the input is rejected.
292
293
    The state must have an 'offering' attribute.
294
    """
295
    def __init__(self, matching=None):
296
        self.matching = matching
297
298
    def _to_python(self, value, state):
299
        if (state.store.find(
1442.1.27 by William Grant
Undo i.w.tutorial's renaming of i.d.Worksheet to DBWorksheet, since the other Worksheet is gone.
300
            Worksheet, offering=state.offering,
1384.1.8 by William Grant
Correctly validate the identifier when renaming a worksheet.
301
            identifier=value).one() not in (None, state.existing_worksheet)):
1384.1.4 by William Grant
Validate worksheet identifer uniqueness.
302
            raise formencode.Invalid(
303
                'Short name already taken', value, state)
304
        return value
305
306
1384.1.1 by William Grant
De-AJAXify WorksheetAddView.
307
class WorksheetSchema(formencode.Schema):
1384.1.8 by William Grant
Correctly validate the identifier when renaming a worksheet.
308
    identifier = formencode.All(
309
        WorksheetIdentifierUniquenessValidator(),
310
        formencode.validators.UnicodeString(not_empty=True))
1384.1.1 by William Grant
De-AJAXify WorksheetAddView.
311
    name = formencode.validators.UnicodeString(not_empty=True)
312
    assessable = formencode.validators.StringBoolean(if_missing=False)
313
    data = formencode.validators.UnicodeString(not_empty=True)
1384.1.2 by William Grant
Verify that the worksheet format is valid.
314
    format = formencode.All(
1384.1.4 by William Grant
Validate worksheet identifer uniqueness.
315
        WorksheetFormatValidator(),
316
        formencode.validators.UnicodeString(not_empty=True))
317
318
1384.1.6 by William Grant
Factor out the bits of WorksheetAddView that will be handy for WorksheetEditView.
319
class WorksheetFormView(XHTMLView):
320
    """An abstract form for a worksheet in an offering."""
1384.1.1 by William Grant
De-AJAXify WorksheetAddView.
321
322
    def filter(self, stream, ctx):
323
        return stream | HTMLFormFiller(data=ctx['data'])
1099.1.182 by Nick Chadwick
Added a view to allow admins to edit worksheets
324
1384.1.8 by William Grant
Correctly validate the identifier when renaming a worksheet.
325
    def populate_state(self, state):
326
        state.existing_worksheet = None
327
1099.1.182 by Nick Chadwick
Added a view to allow admins to edit worksheets
328
    def populate(self, req, ctx):
1384.1.1 by William Grant
De-AJAXify WorksheetAddView.
329
        if req.method == 'POST':
330
            data = dict(req.get_fieldstorage())
331
            try:
1384.1.8 by William Grant
Correctly validate the identifier when renaming a worksheet.
332
                validator = WorksheetSchema()
333
                req.offering = self.offering # XXX: Getting into state.
334
                self.populate_state(req)
1384.1.1 by William Grant
De-AJAXify WorksheetAddView.
335
                data = validator.to_python(data, state=req)
336
1384.1.6 by William Grant
Factor out the bits of WorksheetAddView that will be handy for WorksheetEditView.
337
                worksheet = self.get_worksheet_object(req, data)
338
                ivle.worksheet.utils.update_exerciselist(worksheet)
1384.1.1 by William Grant
De-AJAXify WorksheetAddView.
339
340
                req.store.commit()
1384.1.6 by William Grant
Factor out the bits of WorksheetAddView that will be handy for WorksheetEditView.
341
                req.throw_redirect(req.publisher.generate(worksheet))
1384.1.1 by William Grant
De-AJAXify WorksheetAddView.
342
            except formencode.Invalid, e:
343
                errors = e.unpack_errors()
1384.1.11 by William Grant
If genshi fails to parse a worksheet while saving, render the error nicely.
344
            except genshi.input.ParseError, e:
345
                errors = {'data': 'Could not parse XML: %s' % e.message}
1384.1.10 by William Grant
Display a nice error message if a worksheet attempts to reference a non-existent exercise.
346
            except ivle.worksheet.utils.ExerciseNotFound, e:
347
                errors = {'data': 'Could not find exercise "%s"' % e.message}
1384.1.1 by William Grant
De-AJAXify WorksheetAddView.
348
        else:
1384.1.7 by William Grant
De-AJAXify WorksheetEditView.
349
            data = self.get_default_data(req)
1384.1.1 by William Grant
De-AJAXify WorksheetAddView.
350
            errors = {}
351
1384.1.13 by William Grant
If a worksheet add/edit form results in errors, roll back any DB changes.
352
        if errors:
353
            req.store.rollback()
354
1384.1.1 by William Grant
De-AJAXify WorksheetAddView.
355
        ctx['data'] = data or {}
356
        ctx['offering'] = self.context
357
        ctx['errors'] = errors
358
        ctx['formats'] = WORKSHEET_FORMATS
1099.1.182 by Nick Chadwick
Added a view to allow admins to edit worksheets
359
1384.1.6 by William Grant
Factor out the bits of WorksheetAddView that will be handy for WorksheetEditView.
360
361
class WorksheetAddView(WorksheetFormView):
362
    """An form to create a worksheet in an offering."""
363
    template = 'templates/worksheet_add.html'
1542 by Matt Giuca
Tutors can now (once again) edit worksheets.
364
    permission = 'edit_worksheets'
1523 by William Grant
Declare appropriate tabs on the rest of the views.
365
    tab = 'subjects'
1384.1.6 by William Grant
Factor out the bits of WorksheetAddView that will be handy for WorksheetEditView.
366
1384.1.7 by William Grant
De-AJAXify WorksheetEditView.
367
    @property
368
    def offering(self):
369
        return self.context
370
371
    def get_default_data(self, req):
372
        return {}
373
1384.1.6 by William Grant
Factor out the bits of WorksheetAddView that will be handy for WorksheetEditView.
374
    def get_worksheet_object(self, req, data):
1442.1.27 by William Grant
Undo i.w.tutorial's renaming of i.d.Worksheet to DBWorksheet, since the other Worksheet is gone.
375
        new_worksheet = Worksheet()
1384.1.6 by William Grant
Factor out the bits of WorksheetAddView that will be handy for WorksheetEditView.
376
        new_worksheet.seq_no = self.context.worksheets.count()
377
        # Setting new_worksheet.offering implicitly adds new_worksheet,
378
        # hence worksheets.count MUST be called above it
379
        new_worksheet.offering = self.context
380
        new_worksheet.identifier = data['identifier']
381
        new_worksheet.name = data['name']
382
        new_worksheet.assessable = data['assessable']
383
        new_worksheet.data = data['data']
384
        new_worksheet.format = data['format']
385
386
        req.store.add(new_worksheet)
387
        return new_worksheet
388
389
1384.1.7 by William Grant
De-AJAXify WorksheetEditView.
390
class WorksheetEditView(WorksheetFormView):
391
    """An form to alter a worksheet in an offering."""
392
    template = 'templates/worksheet_edit.html'
393
    permission = 'edit'
1523 by William Grant
Declare appropriate tabs on the rest of the views.
394
    tab = 'subjects'
1384.1.7 by William Grant
De-AJAXify WorksheetEditView.
395
1384.1.8 by William Grant
Correctly validate the identifier when renaming a worksheet.
396
    def populate_state(self, state):
397
        state.existing_worksheet = self.context
398
1384.1.7 by William Grant
De-AJAXify WorksheetEditView.
399
    @property
400
    def offering(self):
401
        return self.context.offering
402
403
    def get_default_data(self, req):
404
        return {
405
            'identifier': self.context.identifier,
406
            'name': self.context.name,
407
            'assessable': self.context.assessable,
408
            'data': self.context.data,
409
            'format': self.context.format
410
            }
411
412
    def get_worksheet_object(self, req, data):
413
        self.context.identifier = data['identifier']
414
        self.context.name = data['name']
415
        self.context.assessable = data['assessable']
416
        self.context.data = data['data']
417
        self.context.format = data['format']
418
419
        return self.context
420
421
1099.1.197 by Nick Chadwick
Modified worksheets edit view, so now there are links to edit, add,
422
class WorksheetsEditView(XHTMLView):
423
    """View for arranging worksheets."""
1542 by Matt Giuca
Tutors can now (once again) edit worksheets.
424
    permission = 'edit_worksheets'
1099.1.197 by Nick Chadwick
Modified worksheets edit view, so now there are links to edit, add,
425
    template = 'templates/worksheets_edit.html'
1523 by William Grant
Declare appropriate tabs on the rest of the views.
426
    tab = 'subjects'
1294.2.64 by William Grant
Port tutorial stuff.
427
1099.1.197 by Nick Chadwick
Modified worksheets edit view, so now there are links to edit, add,
428
    def populate(self, req, ctx):
429
        self.plugin_styles[Plugin] = ['tutorial_admin.css']
430
        self.plugin_scripts[Plugin] = ['tutorial_admin.js']
431
        
1099.1.207 by William Grant
Replace most of the tutorial headings and titles.
432
        ctx['subject'] = self.context.subject
1294.2.64 by William Grant
Port tutorial stuff.
433
        ctx['year'] = self.context.semester.year
434
        ctx['semester'] = self.context.semester.semester
1099.1.197 by Nick Chadwick
Modified worksheets edit view, so now there are links to edit, add,
435
        
436
        ctx['worksheets'] = self.context.worksheets
437
        
438
        ctx['mediapath'] = media_url(req, Plugin, 'images/')
1099.1.212 by Nick Chadwick
Added a new page to display exercises. This will then be modified to
439
440
1394.2.6 by William Grant
Add a basic ExerciseView, which will soon allow testing.
441
class ExerciseView(XHTMLView):
1394.2.11 by William Grant
Add an edit link to ExerciseView.
442
    """View of an exercise.
1394.2.6 by William Grant
Add a basic ExerciseView, which will soon allow testing.
443
1394.2.11 by William Grant
Add an edit link to ExerciseView.
444
    Primarily to preview and test an exercise without adding it to a
445
    worksheet for all to see.
446
    """
1394.2.6 by William Grant
Add a basic ExerciseView, which will soon allow testing.
447
    permission = 'edit'
448
    template = 'templates/exercise.html'
1523 by William Grant
Declare appropriate tabs on the rest of the views.
449
    tab = 'subjects'
1394.2.6 by William Grant
Add a basic ExerciseView, which will soon allow testing.
450
451
    def populate(self, req, ctx):
452
        self.plugin_scripts[Plugin] = ['tutorial.js']
453
        self.plugin_styles[Plugin] = ['tutorial.css']
454
1394.2.11 by William Grant
Add an edit link to ExerciseView.
455
        ctx['req'] = req
1394.2.6 by William Grant
Add a basic ExerciseView, which will soon allow testing.
456
        ctx['mediapath'] = media_url(req, Plugin, 'images/')
457
        ctx['exercise'] = self.context
458
        ctx['exercise_fragment'] = present_exercise(
459
            req, self.context.id)['stream']
1394.2.11 by William Grant
Add an edit link to ExerciseView.
460
        ctx['ExerciseEditView'] = ExerciseEditView
1463.1.3 by William Grant
ExercisesView links only to the exercise's index, which now has edit and delete links.
461
        ctx['ExerciseDeleteView'] = ExerciseDeleteView
1394.2.6 by William Grant
Add a basic ExerciseView, which will soon allow testing.
462
463
1099.1.212 by Nick Chadwick
Added a new page to display exercises. This will then be modified to
464
class ExerciseEditView(XHTMLView):
465
    """View for editing a worksheet."""
466
    permission = 'edit'
467
    template = 'templates/exercise_edit.html'
1394.2.13 by William Grant
Add an Edit breadcrumb for ExerciseEditView.
468
    breadcrumb_text = 'Edit'
1523 by William Grant
Declare appropriate tabs on the rest of the views.
469
    tab = 'subjects'
1394.2.13 by William Grant
Add an Edit breadcrumb for ExerciseEditView.
470
1099.1.212 by Nick Chadwick
Added a new page to display exercises. This will then be modified to
471
    def populate(self, req, ctx):
472
        self.plugin_styles[Plugin] = ['exercise_admin.css']
473
        self.plugin_scripts[Plugin] = ['exercise_admin.js']
1394.2.13 by William Grant
Add an Edit breadcrumb for ExerciseEditView.
474
1099.6.4 by Nick Chadwick
Exercise UI is now ready to be merged into trunk.
475
        ctx['mediapath'] = media_url(req, Plugin, 'images/')
1394.2.13 by William Grant
Add an Edit breadcrumb for ExerciseEditView.
476
1099.1.212 by Nick Chadwick
Added a new page to display exercises. This will then be modified to
477
        ctx['exercise'] = self.context
478
        #XXX: These should come from somewhere else
479
1394.1.8 by William Grant
Prettify part/test/var types.
480
        ctx['var_types'] = {
481
            'var': 'variable',
482
            'arg': 'function argument',
1463.1.7 by William Grant
Disable and XXX exception variable support. It has been broken for ages.
483
            # XXX: wgrant 2010-01-29 bug=514160: Need to
484
            # restore support for this.
485
            #'exception': 'exception',
1394.1.8 by William Grant
Prettify part/test/var types.
486
            }
487
        ctx['part_types'] = {
488
            'stdout': 'standard output',
489
            'stderr': 'standard error',
1420 by William Grant
Redo the exercise test part type (check/norm) selection, with radio buttons. Also add an exact match option, as yet unfunctional.
490
            'result': 'function result',
491
            'exception': 'raised exception',
492
            'code': 'code',
1394.1.8 by William Grant
Prettify part/test/var types.
493
            }
1394.1.9 by William Grant
Prettify testcasepart UI a bit.
494
        ctx['test_types'] = {'norm': 'normalisation', 'check': 'comparison'}
1099.1.182 by Nick Chadwick
Added a view to allow admins to edit worksheets
495
1394.2.13 by William Grant
Add an Edit breadcrumb for ExerciseEditView.
496
1099.6.2 by Nick Chadwick
Added a listing of all exercises
497
class ExerciseDeleteView(XHTMLView):
498
    """View for confirming the deletion of an exercise."""
499
    
500
    permission = 'edit'
1099.6.3 by Nick Chadwick
Edited the exercise service to delete individual parts of an exercise.
501
    template = 'templates/exercise_delete.html'
1523 by William Grant
Declare appropriate tabs on the rest of the views.
502
    tab = 'subjects'
1099.6.2 by Nick Chadwick
Added a listing of all exercises
503
    
504
    def populate(self, req, ctx):
1099.1.233 by Nick Chadwick
Exercise objects in the database module, along with their test cases,
505
506
        # If post, delete the exercise, or display a message explaining that
507
        # the exercise cannot be deleted
508
        if req.method == 'POST':
1099.1.242 by Nick Chadwick
Fixed a problem with exercise editor, which wasn't editing or adding
509
            try:
510
                self.context.delete()
1463.1.4 by William Grant
Clean up exercise deletion a lot.
511
                self.template = 'templates/exercise_deleted.html'
512
            except Exception:
513
                self.template = 'templates/exercise_undeletable.html'
1099.1.233 by Nick Chadwick
Exercise objects in the database module, along with their test cases,
514
515
        # If get, display a delete confirmation page
516
        else:
517
            if self.context.worksheet_exercises.count() is not 0:
1463.1.4 by William Grant
Clean up exercise deletion a lot.
518
                self.template = 'templates/exercise_undeletable.html'
519
1099.1.233 by Nick Chadwick
Exercise objects in the database module, along with their test cases,
520
        # Variables for the template
1099.6.2 by Nick Chadwick
Added a listing of all exercises
521
        ctx['exercise'] = self.context
522
1099.1.229 by Nick Chadwick
Fixed a slight oversight, which meant there was no way to add a new
523
class ExerciseAddView(XHTMLView):
524
    """View for creating a new exercise."""
1523 by William Grant
Declare appropriate tabs on the rest of the views.
525
1099.1.229 by Nick Chadwick
Fixed a slight oversight, which meant there was no way to add a new
526
    permission = 'edit'
527
    template = 'templates/exercise_add.html'
1523 by William Grant
Declare appropriate tabs on the rest of the views.
528
    tab = 'subjects'
529
1099.1.229 by Nick Chadwick
Fixed a slight oversight, which meant there was no way to add a new
530
    def authorize(self, req):
1544 by Matt Giuca
Added an argument 'config' to every single get_permissions method throughout the program. All calls to get_permissions pass a config. This is to allow per-site policy configurations on permissions.
531
        return ('edit' in
532
            ivle.database.Exercise.global_permissions(req.user, req.config))
1536 by Matt Giuca
Fixed policy on who is able to view the list of exercises and create a new one. Rather than being 'if you can edit any offering', it is now the same rule as determining whether you can edit exercises.
533
1099.1.229 by Nick Chadwick
Fixed a slight oversight, which meant there was no way to add a new
534
    def populate(self, req, ctx):
535
        self.plugin_scripts[Plugin] = ['exercise_admin.js']
536
537
1099.6.2 by Nick Chadwick
Added a listing of all exercises
538
class ExercisesView(XHTMLView):
539
    """View for seeing the list of all exercises"""
1465.1.2 by William Grant
Add an 'Exercises' breadcrumb.
540
1099.6.2 by Nick Chadwick
Added a listing of all exercises
541
    permission = 'edit'
542
    template = 'templates/exercises.html'
1465.1.2 by William Grant
Add an 'Exercises' breadcrumb.
543
    breadcrumb_text = 'Exercises'
1523 by William Grant
Declare appropriate tabs on the rest of the views.
544
    tab = 'subjects'
1465.1.2 by William Grant
Add an 'Exercises' breadcrumb.
545
1099.6.2 by Nick Chadwick
Added a listing of all exercises
546
    def authorize(self, req):
1544 by Matt Giuca
Added an argument 'config' to every single get_permissions method throughout the program. All calls to get_permissions pass a config. This is to allow per-site policy configurations on permissions.
547
        return ('edit' in
548
            ivle.database.Exercise.global_permissions(req.user, req.config))
1465.1.2 by William Grant
Add an 'Exercises' breadcrumb.
549
1099.6.2 by Nick Chadwick
Added a listing of all exercises
550
    def populate(self, req, ctx):
551
        self.plugin_styles[Plugin] = ['exercise_admin.css']
1463.1.3 by William Grant
ExercisesView links only to the exercise's index, which now has edit and delete links.
552
        ctx['req'] = req
1506 by Matt Giuca
Exercises page redone to be consistent with worksheets page. Now looks much better, and contains an edit button for each exercise. Also renamed page headings for both pages to 'manage' rather than 'edit' (consistent with links to these pages).
553
        ctx['mediapath'] = media_url(req, Plugin, 'images/')
1099.6.2 by Nick Chadwick
Added a listing of all exercises
554
        ctx['exercises'] = req.store.find(Exercise).order_by(Exercise.id)
555
1294.2.64 by William Grant
Port tutorial stuff.
556
1099.1.99 by William Grant
Require that plugins providing media subclass MediaPlugin.
557
class Plugin(ViewPlugin, MediaPlugin):
1294.2.71 by William Grant
Move i.w.tutorial traversal stuff into i.w.tutorial.traversal.
558
    forward_routes = (root_to_exercise, offering_to_worksheet,
559
        worksheet_to_worksheetexercise, worksheetexercise_to_exerciseattempts,
1294.2.131 by William Grant
Restore SubjectMediaView.
560
        exerciseattempts_to_attempt, subject_to_media)
1294.2.71 by William Grant
Move i.w.tutorial traversal stuff into i.w.tutorial.traversal.
561
562
    reverse_routes = (exercise_url, worksheet_url, worksheetexercise_url,
563
        exerciseattempts_url, exerciseattempt_url)
1294.2.64 by William Grant
Port tutorial stuff.
564
1294.2.100 by William Grant
Add exercise/worksheet breadcrumbs.
565
    breadcrumbs = {Exercise: ExerciseBreadcrumb,
1442.1.27 by William Grant
Undo i.w.tutorial's renaming of i.d.Worksheet to DBWorksheet, since the other Worksheet is gone.
566
                   Worksheet: WorksheetBreadcrumb
1294.2.100 by William Grant
Add exercise/worksheet breadcrumbs.
567
                  }
568
1442.1.32 by William Grant
Remove WorksheetsView; superseded by OfferingView.
569
    views = [(Offering, ('+worksheets', '+new'), WorksheetAddView),
1294.2.64 by William Grant
Port tutorial stuff.
570
             (Offering, ('+worksheets', '+edit'), WorksheetsEditView),
1442.1.27 by William Grant
Undo i.w.tutorial's renaming of i.d.Worksheet to DBWorksheet, since the other Worksheet is gone.
571
             (Worksheet, '+index', WorksheetView),
572
             (Worksheet, '+edit', WorksheetEditView),
1294.2.64 by William Grant
Port tutorial stuff.
573
             (ApplicationRoot, ('+exercises', '+index'), ExercisesView),
574
             (ApplicationRoot, ('+exercises', '+add'), ExerciseAddView),
1394.2.6 by William Grant
Add a basic ExerciseView, which will soon allow testing.
575
             (Exercise, '+index', ExerciseView),
1294.2.64 by William Grant
Port tutorial stuff.
576
             (Exercise, '+edit', ExerciseEditView),
577
             (Exercise, '+delete', ExerciseDeleteView),
1294.2.131 by William Grant
Restore SubjectMediaView.
578
             (SubjectMediaFile, '+index', SubjectMediaView),
1294.2.64 by William Grant
Port tutorial stuff.
579
580
             (Offering, ('+worksheets', '+index'), WorksheetsRESTView, 'api'),
581
             (WorksheetExercise, '+index', WorksheetExerciseRESTView, 'api'),
582
             (ExerciseAttempts, '+index', AttemptsRESTView, 'api'),
583
             (ExerciseAttempt, '+index', AttemptRESTView, 'api'),
584
             (ApplicationRoot, ('+exercises', '+index'), ExercisesRESTView,
585
              'api'),
586
             (Exercise, '+index', ExerciseRESTView, 'api'),
587
             ]
1099.1.64 by William Grant
Move ivle.webapp.tutorial's media to the new framework. This also fixes the
588
589
    media = 'media'
1099.1.100 by Nick Chadwick
Created a new help system.
590
    help = {'Tutorial': 'help.html'}