~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
1676 by David Coles
exercises: Show error for bad reStructuredText rather than crashing
38
import docutils.utils
307 by mattgiuca
tutorial: Now each problem div has an ID. Added submit buttons which call
39
1080.1.32 by me at id
www/app/{subjects,tutorial}: Use the new Storm API to get enrolled subjects.
40
import ivle.database
1099.1.180 by Nick Chadwick
This commit changes the tutorial service, which now almost exclusively
41
from ivle.database import Subject, Offering, Semester, Exercise, \
1294.2.64 by William Grant
Port tutorial stuff.
42
                          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.
43
from ivle.database import Worksheet
1099.1.220 by Nick Chadwick
Merged from trunk
44
import ivle.worksheet.utils
1294.2.64 by William Grant
Port tutorial stuff.
45
from ivle.webapp import ApplicationRoot
1606.1.3 by William Grant
Use URLNameValidator in existing schemas.
46
from ivle.webapp.base.forms import URLNameValidator
1099.1.34 by William Grant
Split up ivle.webapp.base.views into ivle.webapp.base.{rest,xhtml}, as it was
47
from ivle.webapp.base.views import BaseView
48
from ivle.webapp.base.xhtml import XHTMLView
1099.1.99 by William Grant
Require that plugins providing media subclass MediaPlugin.
49
from ivle.webapp.base.plugins import ViewPlugin, MediaPlugin
1294.2.131 by William Grant
Restore SubjectMediaView.
50
from ivle.webapp.media import media_url
1294.2.71 by William Grant
Move i.w.tutorial traversal stuff into i.w.tutorial.traversal.
51
from ivle.webapp.errors import NotFound
52
1294.2.64 by William Grant
Port tutorial stuff.
53
from ivle.webapp.tutorial.service import (AttemptsRESTView, AttemptRESTView,
1384.1.14 by William Grant
Remove unused WorksheetRESTView and WorksheetsRESTView.add_worksheet.
54
            WorksheetExerciseRESTView, WorksheetsRESTView)
1099.1.216 by Nick Chadwick
Started adding in add and save options in the exercise edit view, to
55
from ivle.webapp.tutorial.exercise_service import ExercisesRESTView, \
56
                                                  ExerciseRESTView
1294.3.2 by William Grant
Router->Publisher
57
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.
58
            offering_to_worksheet, worksheet_url,
59
            worksheet_to_worksheetexercise, worksheetexercise_url,
60
            ExerciseAttempts, worksheetexercise_to_exerciseattempts,
61
            exerciseattempts_url, exerciseattempts_to_attempt,
62
            exerciseattempt_url)
1294.2.100 by William Grant
Add exercise/worksheet breadcrumbs.
63
from ivle.webapp.tutorial.breadcrumbs import (ExerciseBreadcrumb,
64
            WorksheetBreadcrumb)
1294.2.131 by William Grant
Restore SubjectMediaView.
65
from ivle.webapp.tutorial.media import (SubjectMediaFile, SubjectMediaView,
66
    subject_to_media)
1689.1.9 by Matt Giuca
Added worksheet marks CSV view, which presents the same table as the worksheet marks table, but as a downloadable CSV file.
67
from ivle.webapp.tutorial.marks import (WorksheetsMarksView,
68
            WorksheetsMarksCSVView)
523 by stevenbird
Adding ReStructured Text preprocessing of exercise descriptions,
69
1099.1.19 by William Grant
ivle.webapp.tutorial: Port www/apps/tutorial to new framework.
70
71
class WorksheetView(XHTMLView):
72
    '''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
73
    template = 'templates/worksheet.html'
1116 by William Grant
Move the old tutorial views into the 'subjects' tab, so they get the right
74
    tab = 'subjects'
1099.1.110 by William Grant
Implement an authorization system in the new framework. This breaks the REST
75
    permission = 'view'
1099.1.19 by William Grant
ivle.webapp.tutorial: Port www/apps/tutorial to new framework.
76
77
    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
78
        self.plugin_scripts[Plugin] = ['tutorial.js']
1099.1.218 by Nick Chadwick
tutorials can now use RST
79
        self.plugin_styles[Plugin] = ['tutorial.css', 'worksheet.css']
1099.1.19 by William Grant
ivle.webapp.tutorial: Port www/apps/tutorial to new framework.
80
1099.1.110 by William Grant
Implement an authorization system in the new framework. This breaks the REST
81
        if not self.context:
1099.1.19 by William Grant
ivle.webapp.tutorial: Port www/apps/tutorial to new framework.
82
            raise NotFound()
83
1099.1.207 by William Grant
Replace most of the tutorial headings and titles.
84
        ctx['subject'] = self.context.offering.subject
85
        ctx['worksheet'] = self.context
1822.1.1 by William Grant
Replace semester.semester with semester.{code,url_name,display_name}.
86
        ctx['semester'] = self.context.offering.semester.url_name
1294.2.64 by William Grant
Port tutorial stuff.
87
        ctx['year'] = self.context.offering.semester.year
1099.1.220 by Nick Chadwick
Merged from trunk
88
1720.1.1 by William Grant
Abstract reStructuredText rendering into ivle.database.
89
        ctx['worksheetstream'] = genshi.Stream(list(genshi.XML(self.context.data_xhtml)))
1141 by William Grant
Display an edit link in WorksheetView, if we have privileges.
90
        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.
91
        ctx['config'] = req.config
1099.1.19 by William Grant
ivle.webapp.tutorial: Port www/apps/tutorial to new framework.
92
1689.1.16 by Matt Giuca
worksheet page: If the viewer has permission to edit, the list of exercises in the contents is replaced by a table showing the statistics on each exercise, including the number of students who have attempted and completed the exercise. Fixed up JavaScript so the dynamic change to a green ball still works on this modified view.
93
        ctx['show_exercise_stats'] = \
1689.1.17 by Matt Giuca
tutorial: Whether or not to show the detailed exercise stats table is now based on the 'edit' permission of worksheet, not 'edit_worksheets'.
94
            'edit' in self.context.get_permissions(req.user,
95
                                                   req.config)
1689.1.16 by Matt Giuca
worksheet page: If the viewer has permission to edit, the list of exercises in the contents is replaced by a table showing the statistics on each exercise, including the number of students who have attempted and completed the exercise. Fixed up JavaScript so the dynamic change to a green ball still works on this modified view.
96
1720 by William Grant
Share one TemplateLoader between every instance of every view, so we cache EVERYTHING.
97
        generate_worksheet_data(ctx, req, self._loader, self.context)
1099.1.19 by William Grant
ivle.webapp.tutorial: Port www/apps/tutorial to new framework.
98
99
        ctx['worksheetstream'] = add_exercises(ctx['worksheetstream'], ctx, req)
100
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
101
def get_worksheets(subjectfile):
102
    '''Given a subject stream, get all the worksheets and put them in ctx'''
103
    worksheets = []
104
    for kind, data, pos in subjectfile:
105
        if kind is genshi.core.START:
106
            if data[0] == 'worksheet':
107
                worksheetid = ''
108
                worksheetname = ''
109
                worksheetasses = False
110
                for attr in data[1]:
111
                    if attr[0] == 'id':
112
                        worksheetid = attr[1]
113
                    elif attr[0] == 'name':
114
                        worksheetname = attr[1]
115
                    elif attr[0] == 'assessable':
116
                        worksheetasses = attr[1] == 'true'
117
                worksheets.append(Worksheet(worksheetid, worksheetname, \
118
                                                            worksheetasses))
119
    return worksheets
120
1099.1.180 by Nick Chadwick
This commit changes the tutorial service, which now almost exclusively
121
# 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.
122
def add_exercises(stream, ctx, req):
1099.1.42 by Nick Chadwick
Fixed an oversight in the tutorial code which was printing <worksheet>
123
    """A filter which adds exercises into the stream."""
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
124
    exid = 0
125
    for kind, data, pos in stream:
126
        if kind is genshi.core.START:
1099.1.42 by Nick Chadwick
Fixed an oversight in the tutorial code which was printing <worksheet>
127
            # Remove the worksheet tags, as they are not xhtml valid.
128
            if data[0] == 'worksheet':
129
                continue
130
            # 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.
131
            # exercise. Note that we only consider exercises with a
132
            # 'src' attribute, the same condition that generate_worksheet_data
133
            # uses to create ctx['exercises'].
134
            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
135
                # XXX: Note that we presume ctx['exercises'] has a correct list
136
                #      of exercises. If it doesn't, something has gone wrong.
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
137
                new_stream = ctx['exercises'][exid]['stream']
138
                exid += 1
139
                for item in new_stream:
140
                    yield item
141
            else:
142
                yield kind, data, pos
1099.1.42 by Nick Chadwick
Fixed an oversight in the tutorial code which was printing <worksheet>
143
        # Remove the end tags for exercises and worksheets
144
        elif kind is genshi.core.END:
145
            if data == 'exercise':
146
                continue
147
            elif data == 'worksheet':
148
                continue
149
            else:
150
                yield kind, data, pos
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
151
        else:
152
            yield kind, data, pos
153
154
# This function runs through the worksheet, to get data on the exercises to
155
# build a Table of Contents, as well as fill in details in ctx
1720 by William Grant
Share one TemplateLoader between every instance of every view, so we cache EVERYTHING.
156
def generate_worksheet_data(ctx, req, loader, worksheet):
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
157
    """Runs through the worksheetstream, generating the exericises"""
158
    ctx['exercises'] = []
159
    ctx['exerciselist'] = []
160
    for kind, data, pos in ctx['worksheetstream']:
161
        if kind is genshi.core.START:
162
            if data[0] == 'exercise':
163
                src = ""
164
                optional = False
165
                for attr in data[1]:
166
                    if attr[0] == 'src':
167
                        src = attr[1]
168
                    if attr[0] == 'optional':
169
                        optional = attr[1] == 'true'
170
                # 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
171
                if src != "":
1720 by William Grant
Share one TemplateLoader between every instance of every view, so we cache EVERYTHING.
172
                    ctx['exercises'].append(
173
                        present_exercise(req, loader, src, worksheet))
1099.4.3 by Nick Chadwick
Updated the tutorial service, to now allow users to edit worksheets
174
                    ctx['exerciselist'].append((src, optional))
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
175
            elif data[0] == 'worksheet':
176
                ctx['worksheetname'] = 'bob'
177
                for attr in data[1]:
178
                    if attr[0] == 'name':
179
                        ctx['worksheetname'] = attr[1]
425 by mattgiuca
tutorial: Refactored present_worksheet so it has a separate function for
180
297 by mattgiuca
tutorial: Now presents problems correctly, by parsing XML source and writing
181
def innerXML(elem):
182
    """Given an element, returns its children as XML strings concatenated
183
    together."""
184
    s = ""
185
    for child in elem.childNodes:
186
        s += child.toxml()
187
    return s
188
189
def getTextData(element):
190
    """ Get the text and cdata inside an element
191
    Leading and trailing whitespace are stripped
192
    """
193
    data = ''
194
    for child in element.childNodes:
195
        if child.nodeType == child.CDATA_SECTION_NODE:
196
            data += child.data
710 by mattgiuca
Tutorial: The tutorial system now presents a table of contents at the top.
197
        elif child.nodeType == child.TEXT_NODE:
297 by mattgiuca
tutorial: Now presents problems correctly, by parsing XML source and writing
198
            data += child.data
710 by mattgiuca
Tutorial: The tutorial system now presents a table of contents at the top.
199
        elif child.nodeType == child.ELEMENT_NODE:
200
            data += getTextData(child)
297 by mattgiuca
tutorial: Now presents problems correctly, by parsing XML source and writing
201
202
    return data.strip()
203
1720 by William Grant
Share one TemplateLoader between every instance of every view, so we cache EVERYTHING.
204
def present_exercise(req, loader, identifier, worksheet=None):
1394.2.2 by William Grant
present_exercise() now works on exercises without worksheets.
205
    """Render an HTML representation of an exercise.
206
207
    identifier: The exercise identifier (URL name).
208
    worksheet: An optional worksheet from which to retrieve saved results.
209
               If omitted, a clean exercise will be presented.
291 by mattgiuca
tutorial: Added code to handle top-level menu and subject menu (reads dir
210
    """
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
211
    # Exercise-specific context is used here, as we already have all the data
212
    # we need
213
    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.
214
    curctx['worksheet'] = worksheet
1099.1.180 by Nick Chadwick
This commit changes the tutorial service, which now almost exclusively
215
1394.2.2 by William Grant
present_exercise() now works on exercises without worksheets.
216
    if worksheet is not None:
217
        worksheet_exercise = req.store.find(WorksheetExercise,
218
            WorksheetExercise.worksheet_id == worksheet.id,
219
            WorksheetExercise.exercise_id == identifier).one()
1099.1.180 by Nick Chadwick
This commit changes the tutorial service, which now almost exclusively
220
1394.2.2 by William Grant
present_exercise() now works on exercises without worksheets.
221
        if worksheet_exercise is None:
222
            raise NotFound()
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
223
224
    # Retrieve the exercise details from the database
1099.1.180 by Nick Chadwick
This commit changes the tutorial service, which now almost exclusively
225
    exercise = req.store.find(Exercise, 
1394.2.2 by William Grant
present_exercise() now works on exercises without worksheets.
226
        Exercise.id == identifier).one()
1099.1.180 by Nick Chadwick
This commit changes the tutorial service, which now almost exclusively
227
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
228
    if exercise is None:
1619 by Matt Giuca
ivle.webapp.tutorial: Fixed unqualified reference to exception ExerciseNotFound.
229
        raise ivle.worksheet.utils.ExerciseNotFound(identifier)
1099.1.93 by William Grant
Remove remaining uses of req.throw_error in the new webapps.
230
515 by stevenbird
Propagated "problem" -> "exercise" nomenclature change.
231
    # Read exercise file and present the exercise
297 by mattgiuca
tutorial: Now presents problems correctly, by parsing XML source and writing
232
    # Note: We do not use the testing framework because it does a lot more
515 by stevenbird
Propagated "problem" -> "exercise" nomenclature change.
233
    # 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
234
    # fields from the XML.
235
1741 by Matt Giuca
Exercise display: Shows a warning if the worksheet cutoff has passed for this subject, that it will not count towards your marks.
236
    curctx['req'] = req
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
237
    curctx['exercise'] = exercise
1676 by David Coles
exercises: Show error for bad reStructuredText rather than crashing
238
    curctx['description'] = None
239
    curctx['error'] = None
1720.1.1 by William Grant
Abstract reStructuredText rendering into ivle.database.
240
    try:
1720.1.2 by William Grant
Correctly handle empty and null exercise descriptions.
241
        desc_xhtml = exercise.description_xhtml
242
        if desc_xhtml:
243
            curctx['description'] = genshi.XML(desc_xhtml)
244
        else:
245
            curctx['description'] = None
1720.1.1 by William Grant
Abstract reStructuredText rendering into ivle.database.
246
    except docutils.utils.SystemMessage, e:
247
        curctx['error'] = "Error processing reStructuredText: '%s'" % str(e)
297 by mattgiuca
tutorial: Now presents problems correctly, by parsing XML source and writing
248
702 by mattgiuca
tutorial:
249
    # If the user has already saved some text for this problem, or submitted
250
    # 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
251
    # Get exercise stored text will return a save, or the most recent attempt,
252
    # whichever is more recent
1394.2.2 by William Grant
present_exercise() now works on exercises without worksheets.
253
    if worksheet is not None:
254
        save = ivle.worksheet.utils.get_exercise_stored_text(
255
                            req.store, req.user, worksheet_exercise)
1099.1.180 by Nick Chadwick
This commit changes the tutorial service, which now almost exclusively
256
1394.2.2 by William Grant
present_exercise() now works on exercises without worksheets.
257
        # Also get the number of attempts taken and whether this is complete.
258
        complete, curctx['attempts'] = \
259
                ivle.worksheet.utils.get_exercise_status(req.store, req.user, 
260
                                                         worksheet_exercise)
261
        if save is not None:
262
            curctx['exercisesave'] = save.text
263
        else:
264
            curctx['exercisesave']= exercise.partial
265
        curctx['complete'] = 'Complete' if complete else 'Incomplete'
266
        curctx['complete_class'] = curctx['complete'].lower()
1099.1.150 by Nick Chadwick
Modified worksheets to properly link attempts to worksheets and
267
    else:
1394.2.2 by William Grant
present_exercise() now works on exercises without worksheets.
268
        curctx['exercisesave'] = exercise.partial
269
        curctx['complete'] = 'Incomplete'
270
        curctx['complete_class'] = curctx['complete'].lower()
271
        curctx['attempts'] = 0
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
272
273
    #Save the exercise details to the Table of Contents
274
1109 by matt.giuca
tutorial/__init__.py: Fixed path to exercises template
275
    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.
276
        "templates/exercise_fragment.html"))
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
277
    ex_stream = tmpl.generate(curctx)
1689.1.16 by Matt Giuca
worksheet page: If the viewer has permission to edit, the list of exercises in the contents is replaced by a table showing the statistics on each exercise, including the number of students who have attempted and completed the exercise. Fixed up JavaScript so the dynamic change to a green ball still works on this modified view.
278
    # Store exercise statistics
279
    if (worksheet is not None and
1689.1.17 by Matt Giuca
tutorial: Whether or not to show the detailed exercise stats table is now based on the 'edit' permission of worksheet, not 'edit_worksheets'.
280
        'edit' in worksheet.get_permissions(req.user, req.config)):
1689.1.16 by Matt Giuca
worksheet page: If the viewer has permission to edit, the list of exercises in the contents is replaced by a table showing the statistics on each exercise, including the number of students who have attempted and completed the exercise. Fixed up JavaScript so the dynamic change to a green ball still works on this modified view.
281
        exercise_stats = ivle.worksheet.utils.get_exercise_statistics(
282
            req.store, worksheet_exercise)
283
    else:
284
        exercise_stats = None
1099.1.141 by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file.
285
    return {'name': exercise.name,
1099.1.45 by William Grant
ivle.webapp.tutorial: Recapitalise 'Complete' and 'Incomplete' in body text.
286
            'complete': curctx['complete_class'],
287
            'stream': ex_stream,
1689.1.16 by Matt Giuca
worksheet page: If the viewer has permission to edit, the list of exercises in the contents is replaced by a table showing the statistics on each exercise, including the number of students who have attempted and completed the exercise. Fixed up JavaScript so the dynamic change to a green ball still works on this modified view.
288
            'exid': exercise.id,
289
            'stats': exercise_stats}
1093 by chadnickbok
Adding the changes from my genshi branch into trunk.
290
1099.1.19 by William Grant
ivle.webapp.tutorial: Port www/apps/tutorial to new framework.
291
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.
292
# The first element is the default format
293
WORKSHEET_FORMATS = (('reStructuredText', 'rst'), ('XHTML (legacy)', 'xml'))
1384.1.1 by William Grant
De-AJAXify WorksheetAddView.
294
1384.1.7 by William Grant
De-AJAXify WorksheetEditView.
295
1384.1.2 by William Grant
Verify that the worksheet format is valid.
296
class WorksheetFormatValidator(formencode.FancyValidator):
297
    """A FormEncode validator that turns a username into a user.
298
299
    The state must have a 'store' attribute, which is the Storm store
300
    to use."""
301
    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.
302
        if value not in [x for (_,x) in WORKSHEET_FORMATS]:
1384.1.2 by William Grant
Verify that the worksheet format is valid.
303
            raise formencode.Invalid('Unsupported format', value, state)
304
        return value
305
306
1384.1.4 by William Grant
Validate worksheet identifer uniqueness.
307
class WorksheetIdentifierUniquenessValidator(formencode.FancyValidator):
308
    """A FormEncode validator that checks that a worksheet name is unused.
309
1384.1.8 by William Grant
Correctly validate the identifier when renaming a worksheet.
310
    The worksheet referenced by state.existing_worksheet is permitted
1384.1.4 by William Grant
Validate worksheet identifer uniqueness.
311
    to hold that name. If any other object holds it, the input is rejected.
312
313
    The state must have an 'offering' attribute.
314
    """
315
    def __init__(self, matching=None):
316
        self.matching = matching
317
318
    def _to_python(self, value, state):
319
        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.
320
            Worksheet, offering=state.offering,
1384.1.8 by William Grant
Correctly validate the identifier when renaming a worksheet.
321
            identifier=value).one() not in (None, state.existing_worksheet)):
1384.1.4 by William Grant
Validate worksheet identifer uniqueness.
322
            raise formencode.Invalid(
323
                'Short name already taken', value, state)
324
        return value
325
326
1384.1.1 by William Grant
De-AJAXify WorksheetAddView.
327
class WorksheetSchema(formencode.Schema):
1384.1.8 by William Grant
Correctly validate the identifier when renaming a worksheet.
328
    identifier = formencode.All(
329
        WorksheetIdentifierUniquenessValidator(),
1606.1.3 by William Grant
Use URLNameValidator in existing schemas.
330
        URLNameValidator(not_empty=True))
1384.1.1 by William Grant
De-AJAXify WorksheetAddView.
331
    name = formencode.validators.UnicodeString(not_empty=True)
332
    assessable = formencode.validators.StringBoolean(if_missing=False)
1695.1.3 by William Grant
Expose Worksheet.published in the forms.
333
    published = formencode.validators.StringBoolean(if_missing=False)
1384.1.1 by William Grant
De-AJAXify WorksheetAddView.
334
    data = formencode.validators.UnicodeString(not_empty=True)
1384.1.2 by William Grant
Verify that the worksheet format is valid.
335
    format = formencode.All(
1384.1.4 by William Grant
Validate worksheet identifer uniqueness.
336
        WorksheetFormatValidator(),
337
        formencode.validators.UnicodeString(not_empty=True))
338
339
1384.1.6 by William Grant
Factor out the bits of WorksheetAddView that will be handy for WorksheetEditView.
340
class WorksheetFormView(XHTMLView):
341
    """An abstract form for a worksheet in an offering."""
1384.1.1 by William Grant
De-AJAXify WorksheetAddView.
342
343
    def filter(self, stream, ctx):
344
        return stream | HTMLFormFiller(data=ctx['data'])
1099.1.182 by Nick Chadwick
Added a view to allow admins to edit worksheets
345
1384.1.8 by William Grant
Correctly validate the identifier when renaming a worksheet.
346
    def populate_state(self, state):
347
        state.existing_worksheet = None
348
1099.1.182 by Nick Chadwick
Added a view to allow admins to edit worksheets
349
    def populate(self, req, ctx):
1384.1.1 by William Grant
De-AJAXify WorksheetAddView.
350
        if req.method == 'POST':
351
            data = dict(req.get_fieldstorage())
352
            try:
1384.1.8 by William Grant
Correctly validate the identifier when renaming a worksheet.
353
                validator = WorksheetSchema()
354
                req.offering = self.offering # XXX: Getting into state.
355
                self.populate_state(req)
1384.1.1 by William Grant
De-AJAXify WorksheetAddView.
356
                data = validator.to_python(data, state=req)
357
1384.1.6 by William Grant
Factor out the bits of WorksheetAddView that will be handy for WorksheetEditView.
358
                worksheet = self.get_worksheet_object(req, data)
359
                ivle.worksheet.utils.update_exerciselist(worksheet)
1384.1.1 by William Grant
De-AJAXify WorksheetAddView.
360
361
                req.store.commit()
1384.1.6 by William Grant
Factor out the bits of WorksheetAddView that will be handy for WorksheetEditView.
362
                req.throw_redirect(req.publisher.generate(worksheet))
1384.1.1 by William Grant
De-AJAXify WorksheetAddView.
363
            except formencode.Invalid, e:
364
                errors = e.unpack_errors()
1384.1.11 by William Grant
If genshi fails to parse a worksheet while saving, render the error nicely.
365
            except genshi.input.ParseError, e:
366
                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.
367
            except ivle.worksheet.utils.ExerciseNotFound, e:
368
                errors = {'data': 'Could not find exercise "%s"' % e.message}
1743 by David Coles
worksheets: Catch and nicely display SystemMessage thrown by docutils if we can't generate XML from rST when saving a worksheet
369
            except docutils.utils.SystemMessage, e:
370
                errors = {'data': 'Could not parse reStructuredText: %s'%(
371
                        e.message)}
1384.1.1 by William Grant
De-AJAXify WorksheetAddView.
372
        else:
1384.1.7 by William Grant
De-AJAXify WorksheetEditView.
373
            data = self.get_default_data(req)
1384.1.1 by William Grant
De-AJAXify WorksheetAddView.
374
            errors = {}
375
1384.1.13 by William Grant
If a worksheet add/edit form results in errors, roll back any DB changes.
376
        if errors:
377
            req.store.rollback()
378
1384.1.1 by William Grant
De-AJAXify WorksheetAddView.
379
        ctx['data'] = data or {}
380
        ctx['offering'] = self.context
381
        ctx['errors'] = errors
1699 by William Grant
Don't hide global form errors on worksheet forms.
382
        # If all of the fields validated, set the global form error.
383
        if isinstance(errors, basestring):
384
            ctx['error_value'] = errors
1384.1.1 by William Grant
De-AJAXify WorksheetAddView.
385
        ctx['formats'] = WORKSHEET_FORMATS
1099.1.182 by Nick Chadwick
Added a view to allow admins to edit worksheets
386
1384.1.6 by William Grant
Factor out the bits of WorksheetAddView that will be handy for WorksheetEditView.
387
388
class WorksheetAddView(WorksheetFormView):
389
    """An form to create a worksheet in an offering."""
390
    template = 'templates/worksheet_add.html'
1542 by Matt Giuca
Tutors can now (once again) edit worksheets.
391
    permission = 'edit_worksheets'
1523 by William Grant
Declare appropriate tabs on the rest of the views.
392
    tab = 'subjects'
1384.1.6 by William Grant
Factor out the bits of WorksheetAddView that will be handy for WorksheetEditView.
393
1384.1.7 by William Grant
De-AJAXify WorksheetEditView.
394
    @property
395
    def offering(self):
396
        return self.context
397
398
    def get_default_data(self, req):
399
        return {}
400
1384.1.6 by William Grant
Factor out the bits of WorksheetAddView that will be handy for WorksheetEditView.
401
    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.
402
        new_worksheet = Worksheet()
1384.1.6 by William Grant
Factor out the bits of WorksheetAddView that will be handy for WorksheetEditView.
403
        new_worksheet.seq_no = self.context.worksheets.count()
404
        # Setting new_worksheet.offering implicitly adds new_worksheet,
405
        # hence worksheets.count MUST be called above it
406
        new_worksheet.offering = self.context
407
        new_worksheet.identifier = data['identifier']
408
        new_worksheet.name = data['name']
409
        new_worksheet.assessable = data['assessable']
1695.1.3 by William Grant
Expose Worksheet.published in the forms.
410
        new_worksheet.published = data['published']
1720.1.3 by William Grant
Abstract worksheet and exercise data setting.
411
        new_worksheet.set_data(data['data'])
1384.1.6 by William Grant
Factor out the bits of WorksheetAddView that will be handy for WorksheetEditView.
412
        new_worksheet.format = data['format']
413
414
        req.store.add(new_worksheet)
415
        return new_worksheet
416
417
1384.1.7 by William Grant
De-AJAXify WorksheetEditView.
418
class WorksheetEditView(WorksheetFormView):
419
    """An form to alter a worksheet in an offering."""
420
    template = 'templates/worksheet_edit.html'
421
    permission = 'edit'
1523 by William Grant
Declare appropriate tabs on the rest of the views.
422
    tab = 'subjects'
1384.1.7 by William Grant
De-AJAXify WorksheetEditView.
423
1384.1.8 by William Grant
Correctly validate the identifier when renaming a worksheet.
424
    def populate_state(self, state):
425
        state.existing_worksheet = self.context
426
1384.1.7 by William Grant
De-AJAXify WorksheetEditView.
427
    @property
428
    def offering(self):
429
        return self.context.offering
430
431
    def get_default_data(self, req):
432
        return {
433
            'identifier': self.context.identifier,
434
            'name': self.context.name,
435
            'assessable': self.context.assessable,
1695.1.3 by William Grant
Expose Worksheet.published in the forms.
436
            'published': self.context.published,
1384.1.7 by William Grant
De-AJAXify WorksheetEditView.
437
            'data': self.context.data,
438
            'format': self.context.format
439
            }
440
441
    def get_worksheet_object(self, req, data):
442
        self.context.identifier = data['identifier']
443
        self.context.name = data['name']
444
        self.context.assessable = data['assessable']
1695.1.3 by William Grant
Expose Worksheet.published in the forms.
445
        self.context.published = data['published']
1720.1.3 by William Grant
Abstract worksheet and exercise data setting.
446
        self.context.set_data(data['data'])
1384.1.7 by William Grant
De-AJAXify WorksheetEditView.
447
        self.context.format = data['format']
448
449
        return self.context
450
451
1099.1.197 by Nick Chadwick
Modified worksheets edit view, so now there are links to edit, add,
452
class WorksheetsEditView(XHTMLView):
453
    """View for arranging worksheets."""
1542 by Matt Giuca
Tutors can now (once again) edit worksheets.
454
    permission = 'edit_worksheets'
1099.1.197 by Nick Chadwick
Modified worksheets edit view, so now there are links to edit, add,
455
    template = 'templates/worksheets_edit.html'
1523 by William Grant
Declare appropriate tabs on the rest of the views.
456
    tab = 'subjects'
1294.2.64 by William Grant
Port tutorial stuff.
457
1099.1.197 by Nick Chadwick
Modified worksheets edit view, so now there are links to edit, add,
458
    def populate(self, req, ctx):
459
        self.plugin_styles[Plugin] = ['tutorial_admin.css']
460
        self.plugin_scripts[Plugin] = ['tutorial_admin.js']
461
        
1099.1.207 by William Grant
Replace most of the tutorial headings and titles.
462
        ctx['subject'] = self.context.subject
1294.2.64 by William Grant
Port tutorial stuff.
463
        ctx['year'] = self.context.semester.year
1822.1.1 by William Grant
Replace semester.semester with semester.{code,url_name,display_name}.
464
        ctx['semester'] = self.context.semester.url_name
1099.1.197 by Nick Chadwick
Modified worksheets edit view, so now there are links to edit, add,
465
        
466
        ctx['worksheets'] = self.context.worksheets
467
        
468
        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
469
470
1394.2.6 by William Grant
Add a basic ExerciseView, which will soon allow testing.
471
class ExerciseView(XHTMLView):
1394.2.11 by William Grant
Add an edit link to ExerciseView.
472
    """View of an exercise.
1394.2.6 by William Grant
Add a basic ExerciseView, which will soon allow testing.
473
1394.2.11 by William Grant
Add an edit link to ExerciseView.
474
    Primarily to preview and test an exercise without adding it to a
475
    worksheet for all to see.
476
    """
1394.2.6 by William Grant
Add a basic ExerciseView, which will soon allow testing.
477
    permission = 'edit'
478
    template = 'templates/exercise.html'
1523 by William Grant
Declare appropriate tabs on the rest of the views.
479
    tab = 'subjects'
1394.2.6 by William Grant
Add a basic ExerciseView, which will soon allow testing.
480
481
    def populate(self, req, ctx):
482
        self.plugin_scripts[Plugin] = ['tutorial.js']
483
        self.plugin_styles[Plugin] = ['tutorial.css']
484
1394.2.11 by William Grant
Add an edit link to ExerciseView.
485
        ctx['req'] = req
1394.2.6 by William Grant
Add a basic ExerciseView, which will soon allow testing.
486
        ctx['mediapath'] = media_url(req, Plugin, 'images/')
487
        ctx['exercise'] = self.context
488
        ctx['exercise_fragment'] = present_exercise(
1720 by William Grant
Share one TemplateLoader between every instance of every view, so we cache EVERYTHING.
489
            req, self._loader, self.context.id)['stream']
1394.2.11 by William Grant
Add an edit link to ExerciseView.
490
        ctx['ExerciseEditView'] = ExerciseEditView
1463.1.3 by William Grant
ExercisesView links only to the exercise's index, which now has edit and delete links.
491
        ctx['ExerciseDeleteView'] = ExerciseDeleteView
1394.2.6 by William Grant
Add a basic ExerciseView, which will soon allow testing.
492
493
1099.1.212 by Nick Chadwick
Added a new page to display exercises. This will then be modified to
494
class ExerciseEditView(XHTMLView):
495
    """View for editing a worksheet."""
496
    permission = 'edit'
497
    template = 'templates/exercise_edit.html'
1394.2.13 by William Grant
Add an Edit breadcrumb for ExerciseEditView.
498
    breadcrumb_text = 'Edit'
1523 by William Grant
Declare appropriate tabs on the rest of the views.
499
    tab = 'subjects'
1394.2.13 by William Grant
Add an Edit breadcrumb for ExerciseEditView.
500
1099.1.212 by Nick Chadwick
Added a new page to display exercises. This will then be modified to
501
    def populate(self, req, ctx):
502
        self.plugin_styles[Plugin] = ['exercise_admin.css']
503
        self.plugin_scripts[Plugin] = ['exercise_admin.js']
1394.2.13 by William Grant
Add an Edit breadcrumb for ExerciseEditView.
504
1099.6.4 by Nick Chadwick
Exercise UI is now ready to be merged into trunk.
505
        ctx['mediapath'] = media_url(req, Plugin, 'images/')
1394.2.13 by William Grant
Add an Edit breadcrumb for ExerciseEditView.
506
1099.1.212 by Nick Chadwick
Added a new page to display exercises. This will then be modified to
507
        ctx['exercise'] = self.context
508
        #XXX: These should come from somewhere else
509
1394.1.8 by William Grant
Prettify part/test/var types.
510
        ctx['var_types'] = {
511
            'var': 'variable',
512
            'arg': 'function argument',
1691.1.2 by David Coles
Reenable ability to create allowed exception varibles in the exercise editing UI
513
            'exception': 'exception',
1394.1.8 by William Grant
Prettify part/test/var types.
514
            }
515
        ctx['part_types'] = {
516
            'stdout': 'standard output',
517
            '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.
518
            'result': 'function result',
519
            'exception': 'raised exception',
520
            'code': 'code',
1394.1.8 by William Grant
Prettify part/test/var types.
521
            }
1394.1.9 by William Grant
Prettify testcasepart UI a bit.
522
        ctx['test_types'] = {'norm': 'normalisation', 'check': 'comparison'}
1099.1.182 by Nick Chadwick
Added a view to allow admins to edit worksheets
523
1394.2.13 by William Grant
Add an Edit breadcrumb for ExerciseEditView.
524
1099.6.2 by Nick Chadwick
Added a listing of all exercises
525
class ExerciseDeleteView(XHTMLView):
526
    """View for confirming the deletion of an exercise."""
527
    
528
    permission = 'edit'
1099.6.3 by Nick Chadwick
Edited the exercise service to delete individual parts of an exercise.
529
    template = 'templates/exercise_delete.html'
1523 by William Grant
Declare appropriate tabs on the rest of the views.
530
    tab = 'subjects'
1099.6.2 by Nick Chadwick
Added a listing of all exercises
531
    
532
    def populate(self, req, ctx):
1099.1.233 by Nick Chadwick
Exercise objects in the database module, along with their test cases,
533
534
        # If post, delete the exercise, or display a message explaining that
535
        # the exercise cannot be deleted
536
        if req.method == 'POST':
1099.1.242 by Nick Chadwick
Fixed a problem with exercise editor, which wasn't editing or adding
537
            try:
538
                self.context.delete()
1463.1.4 by William Grant
Clean up exercise deletion a lot.
539
                self.template = 'templates/exercise_deleted.html'
540
            except Exception:
541
                self.template = 'templates/exercise_undeletable.html'
1099.1.233 by Nick Chadwick
Exercise objects in the database module, along with their test cases,
542
543
        # If get, display a delete confirmation page
544
        else:
1723 by Matt Giuca
Replace 'is not 0' with '!= 0'. 'is not' is a reference check which is dangerous on ints -- it assumes they are interned which is probably, but not necessarily, true.
545
            if self.context.worksheet_exercises.count() != 0:
1463.1.4 by William Grant
Clean up exercise deletion a lot.
546
                self.template = 'templates/exercise_undeletable.html'
547
1099.1.233 by Nick Chadwick
Exercise objects in the database module, along with their test cases,
548
        # Variables for the template
1099.6.2 by Nick Chadwick
Added a listing of all exercises
549
        ctx['exercise'] = self.context
550
1099.1.229 by Nick Chadwick
Fixed a slight oversight, which meant there was no way to add a new
551
class ExerciseAddView(XHTMLView):
552
    """View for creating a new exercise."""
1523 by William Grant
Declare appropriate tabs on the rest of the views.
553
1099.1.229 by Nick Chadwick
Fixed a slight oversight, which meant there was no way to add a new
554
    permission = 'edit'
555
    template = 'templates/exercise_add.html'
1523 by William Grant
Declare appropriate tabs on the rest of the views.
556
    tab = 'subjects'
557
1099.1.229 by Nick Chadwick
Fixed a slight oversight, which meant there was no way to add a new
558
    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.
559
        return ('edit' in
560
            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.
561
1099.1.229 by Nick Chadwick
Fixed a slight oversight, which meant there was no way to add a new
562
    def populate(self, req, ctx):
563
        self.plugin_scripts[Plugin] = ['exercise_admin.js']
564
565
1099.6.2 by Nick Chadwick
Added a listing of all exercises
566
class ExercisesView(XHTMLView):
567
    """View for seeing the list of all exercises"""
1465.1.2 by William Grant
Add an 'Exercises' breadcrumb.
568
1099.6.2 by Nick Chadwick
Added a listing of all exercises
569
    permission = 'edit'
570
    template = 'templates/exercises.html'
1465.1.2 by William Grant
Add an 'Exercises' breadcrumb.
571
    breadcrumb_text = 'Exercises'
1523 by William Grant
Declare appropriate tabs on the rest of the views.
572
    tab = 'subjects'
1465.1.2 by William Grant
Add an 'Exercises' breadcrumb.
573
1099.6.2 by Nick Chadwick
Added a listing of all exercises
574
    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.
575
        return ('edit' in
576
            ivle.database.Exercise.global_permissions(req.user, req.config))
1465.1.2 by William Grant
Add an 'Exercises' breadcrumb.
577
1099.6.2 by Nick Chadwick
Added a listing of all exercises
578
    def populate(self, req, ctx):
579
        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.
580
        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).
581
        ctx['mediapath'] = media_url(req, Plugin, 'images/')
1099.6.2 by Nick Chadwick
Added a listing of all exercises
582
        ctx['exercises'] = req.store.find(Exercise).order_by(Exercise.id)
583
1294.2.64 by William Grant
Port tutorial stuff.
584
1099.1.99 by William Grant
Require that plugins providing media subclass MediaPlugin.
585
class Plugin(ViewPlugin, MediaPlugin):
1294.2.71 by William Grant
Move i.w.tutorial traversal stuff into i.w.tutorial.traversal.
586
    forward_routes = (root_to_exercise, offering_to_worksheet,
587
        worksheet_to_worksheetexercise, worksheetexercise_to_exerciseattempts,
1294.2.131 by William Grant
Restore SubjectMediaView.
588
        exerciseattempts_to_attempt, subject_to_media)
1294.2.71 by William Grant
Move i.w.tutorial traversal stuff into i.w.tutorial.traversal.
589
590
    reverse_routes = (exercise_url, worksheet_url, worksheetexercise_url,
591
        exerciseattempts_url, exerciseattempt_url)
1294.2.64 by William Grant
Port tutorial stuff.
592
1294.2.100 by William Grant
Add exercise/worksheet breadcrumbs.
593
    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.
594
                   Worksheet: WorksheetBreadcrumb
1294.2.100 by William Grant
Add exercise/worksheet breadcrumbs.
595
                  }
596
1442.1.32 by William Grant
Remove WorksheetsView; superseded by OfferingView.
597
    views = [(Offering, ('+worksheets', '+new'), WorksheetAddView),
1294.2.64 by William Grant
Port tutorial stuff.
598
             (Offering, ('+worksheets', '+edit'), WorksheetsEditView),
1689.1.14 by Matt Giuca
tutorial: CSV marks is now located at +marks/marks.csv (now that our routes system allows it).
599
             (Offering, ('+worksheets', '+marks', '+index'),
600
              WorksheetsMarksView),
601
             (Offering, ('+worksheets', '+marks', 'marks.csv'),
1689.1.9 by Matt Giuca
Added worksheet marks CSV view, which presents the same table as the worksheet marks table, but as a downloadable CSV file.
602
              WorksheetsMarksCSVView),
1442.1.27 by William Grant
Undo i.w.tutorial's renaming of i.d.Worksheet to DBWorksheet, since the other Worksheet is gone.
603
             (Worksheet, '+index', WorksheetView),
604
             (Worksheet, '+edit', WorksheetEditView),
1294.2.64 by William Grant
Port tutorial stuff.
605
             (ApplicationRoot, ('+exercises', '+index'), ExercisesView),
606
             (ApplicationRoot, ('+exercises', '+add'), ExerciseAddView),
1394.2.6 by William Grant
Add a basic ExerciseView, which will soon allow testing.
607
             (Exercise, '+index', ExerciseView),
1294.2.64 by William Grant
Port tutorial stuff.
608
             (Exercise, '+edit', ExerciseEditView),
609
             (Exercise, '+delete', ExerciseDeleteView),
1294.2.131 by William Grant
Restore SubjectMediaView.
610
             (SubjectMediaFile, '+index', SubjectMediaView),
1294.2.64 by William Grant
Port tutorial stuff.
611
612
             (Offering, ('+worksheets', '+index'), WorksheetsRESTView, 'api'),
613
             (WorksheetExercise, '+index', WorksheetExerciseRESTView, 'api'),
614
             (ExerciseAttempts, '+index', AttemptsRESTView, 'api'),
615
             (ExerciseAttempt, '+index', AttemptRESTView, 'api'),
616
             (ApplicationRoot, ('+exercises', '+index'), ExercisesRESTView,
617
              'api'),
618
             (Exercise, '+index', ExerciseRESTView, 'api'),
619
             ]
1099.1.64 by William Grant
Move ivle.webapp.tutorial's media to the new framework. This also fixes the
620
621
    media = 'media'
1658 by Matt Giuca
Help: Renamed 'Tutorial' to 'Worksheets' and 'Filesystem' to 'Files'.
622
    help = {'Worksheets': 'help.html'}