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

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