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'} |