~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.4.1 by Nick Chadwick
Working on putting worksheets into the database.
270
class OfferingAdminView(XHTMLView):
271
    """The admin view for an Offering.
272
    
273
    This class is designed to check the user has admin privileges, and
274
    then allow them to edit the RST for the offering, which controls which
275
    worksheets are actually displayed on the page."""
276
    pass
277
1099.1.19 by William Grant
ivle.webapp.tutorial: Port www/apps/tutorial to new framework.
278
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.
279
# The first element is the default format
280
WORKSHEET_FORMATS = (('reStructuredText', 'rst'), ('XHTML (legacy)', 'xml'))
1384.1.1 by William Grant
De-AJAXify WorksheetAddView.
281
1384.1.7 by William Grant
De-AJAXify WorksheetEditView.
282
1384.1.2 by William Grant
Verify that the worksheet format is valid.
283
class WorksheetFormatValidator(formencode.FancyValidator):
284
    """A FormEncode validator that turns a username into a user.
285
286
    The state must have a 'store' attribute, which is the Storm store
287
    to use."""
288
    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.
289
        if value not in [x for (_,x) in WORKSHEET_FORMATS]:
1384.1.2 by William Grant
Verify that the worksheet format is valid.
290
            raise formencode.Invalid('Unsupported format', value, state)
291
        return value
292
293
1384.1.4 by William Grant
Validate worksheet identifer uniqueness.
294
class WorksheetIdentifierUniquenessValidator(formencode.FancyValidator):
295
    """A FormEncode validator that checks that a worksheet name is unused.
296
1384.1.8 by William Grant
Correctly validate the identifier when renaming a worksheet.
297
    The worksheet referenced by state.existing_worksheet is permitted
1384.1.4 by William Grant
Validate worksheet identifer uniqueness.
298
    to hold that name. If any other object holds it, the input is rejected.
299
300
    The state must have an 'offering' attribute.
301
    """
302
    def __init__(self, matching=None):
303
        self.matching = matching
304
305
    def _to_python(self, value, state):
306
        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.
307
            Worksheet, offering=state.offering,
1384.1.8 by William Grant
Correctly validate the identifier when renaming a worksheet.
308
            identifier=value).one() not in (None, state.existing_worksheet)):
1384.1.4 by William Grant
Validate worksheet identifer uniqueness.
309
            raise formencode.Invalid(
310
                'Short name already taken', value, state)
311
        return value
312
313
1384.1.1 by William Grant
De-AJAXify WorksheetAddView.
314
class WorksheetSchema(formencode.Schema):
1384.1.8 by William Grant
Correctly validate the identifier when renaming a worksheet.
315
    identifier = formencode.All(
316
        WorksheetIdentifierUniquenessValidator(),
317
        formencode.validators.UnicodeString(not_empty=True))
1384.1.1 by William Grant
De-AJAXify WorksheetAddView.
318
    name = formencode.validators.UnicodeString(not_empty=True)
319
    assessable = formencode.validators.StringBoolean(if_missing=False)
320
    data = formencode.validators.UnicodeString(not_empty=True)
1384.1.2 by William Grant
Verify that the worksheet format is valid.
321
    format = formencode.All(
1384.1.4 by William Grant
Validate worksheet identifer uniqueness.
322
        WorksheetFormatValidator(),
323
        formencode.validators.UnicodeString(not_empty=True))
324
325
1384.1.6 by William Grant
Factor out the bits of WorksheetAddView that will be handy for WorksheetEditView.
326
class WorksheetFormView(XHTMLView):
327
    """An abstract form for a worksheet in an offering."""
1384.1.1 by William Grant
De-AJAXify WorksheetAddView.
328
329
    def filter(self, stream, ctx):
330
        return stream | HTMLFormFiller(data=ctx['data'])
1099.1.182 by Nick Chadwick
Added a view to allow admins to edit worksheets
331
1384.1.8 by William Grant
Correctly validate the identifier when renaming a worksheet.
332
    def populate_state(self, state):
333
        state.existing_worksheet = None
334
1099.1.182 by Nick Chadwick
Added a view to allow admins to edit worksheets
335
    def populate(self, req, ctx):
1384.1.1 by William Grant
De-AJAXify WorksheetAddView.
336
        if req.method == 'POST':
337
            data = dict(req.get_fieldstorage())
338
            try:
1384.1.8 by William Grant
Correctly validate the identifier when renaming a worksheet.
339
                validator = WorksheetSchema()
340
                req.offering = self.offering # XXX: Getting into state.
341
                self.populate_state(req)
1384.1.1 by William Grant
De-AJAXify WorksheetAddView.
342
                data = validator.to_python(data, state=req)
343
1384.1.6 by William Grant
Factor out the bits of WorksheetAddView that will be handy for WorksheetEditView.
344
                worksheet = self.get_worksheet_object(req, data)
345
                ivle.worksheet.utils.update_exerciselist(worksheet)
1384.1.1 by William Grant
De-AJAXify WorksheetAddView.
346
347
                req.store.commit()
1384.1.6 by William Grant
Factor out the bits of WorksheetAddView that will be handy for WorksheetEditView.
348
                req.throw_redirect(req.publisher.generate(worksheet))
1384.1.1 by William Grant
De-AJAXify WorksheetAddView.
349
            except formencode.Invalid, e:
350
                errors = e.unpack_errors()
1384.1.11 by William Grant
If genshi fails to parse a worksheet while saving, render the error nicely.
351
            except genshi.input.ParseError, e:
352
                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.
353
            except ivle.worksheet.utils.ExerciseNotFound, e:
354
                errors = {'data': 'Could not find exercise "%s"' % e.message}
1384.1.1 by William Grant
De-AJAXify WorksheetAddView.
355
        else:
1384.1.7 by William Grant
De-AJAXify WorksheetEditView.
356
            data = self.get_default_data(req)
1384.1.1 by William Grant
De-AJAXify WorksheetAddView.
357
            errors = {}
358
1384.1.13 by William Grant
If a worksheet add/edit form results in errors, roll back any DB changes.
359
        if errors:
360
            req.store.rollback()
361
1384.1.1 by William Grant
De-AJAXify WorksheetAddView.
362
        ctx['data'] = data or {}
363
        ctx['offering'] = self.context
364
        ctx['errors'] = errors
365
        ctx['formats'] = WORKSHEET_FORMATS
1099.1.182 by Nick Chadwick
Added a view to allow admins to edit worksheets
366
1384.1.6 by William Grant
Factor out the bits of WorksheetAddView that will be handy for WorksheetEditView.
367
368
class WorksheetAddView(WorksheetFormView):
369
    """An form to create a worksheet in an offering."""
370
    template = 'templates/worksheet_add.html'
371
    permission = 'edit'
372
1384.1.7 by William Grant
De-AJAXify WorksheetEditView.
373
    @property
374
    def offering(self):
375
        return self.context
376
377
    def get_default_data(self, req):
378
        return {}
379
1384.1.6 by William Grant
Factor out the bits of WorksheetAddView that will be handy for WorksheetEditView.
380
    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.
381
        new_worksheet = Worksheet()
1384.1.6 by William Grant
Factor out the bits of WorksheetAddView that will be handy for WorksheetEditView.
382
        new_worksheet.seq_no = self.context.worksheets.count()
383
        # Setting new_worksheet.offering implicitly adds new_worksheet,
384
        # hence worksheets.count MUST be called above it
385
        new_worksheet.offering = self.context
386
        new_worksheet.identifier = data['identifier']
387
        new_worksheet.name = data['name']
388
        new_worksheet.assessable = data['assessable']
389
        new_worksheet.data = data['data']
390
        new_worksheet.format = data['format']
391
392
        req.store.add(new_worksheet)
393
        return new_worksheet
394
395
1384.1.7 by William Grant
De-AJAXify WorksheetEditView.
396
class WorksheetEditView(WorksheetFormView):
397
    """An form to alter a worksheet in an offering."""
398
    template = 'templates/worksheet_edit.html'
399
    permission = 'edit'
400
1384.1.8 by William Grant
Correctly validate the identifier when renaming a worksheet.
401
    def populate_state(self, state):
402
        state.existing_worksheet = self.context
403
1384.1.7 by William Grant
De-AJAXify WorksheetEditView.
404
    @property
405
    def offering(self):
406
        return self.context.offering
407
408
    def get_default_data(self, req):
409
        return {
410
            'identifier': self.context.identifier,
411
            'name': self.context.name,
412
            'assessable': self.context.assessable,
413
            'data': self.context.data,
414
            'format': self.context.format
415
            }
416
417
    def get_worksheet_object(self, req, data):
418
        self.context.identifier = data['identifier']
419
        self.context.name = data['name']
420
        self.context.assessable = data['assessable']
421
        self.context.data = data['data']
422
        self.context.format = data['format']
423
424
        return self.context
425
426
1099.1.197 by Nick Chadwick
Modified worksheets edit view, so now there are links to edit, add,
427
class WorksheetsEditView(XHTMLView):
428
    """View for arranging worksheets."""
429
    permission = 'edit'
430
    template = 'templates/worksheets_edit.html'
1294.2.64 by William Grant
Port tutorial stuff.
431
1099.1.197 by Nick Chadwick
Modified worksheets edit view, so now there are links to edit, add,
432
    def populate(self, req, ctx):
433
        self.plugin_styles[Plugin] = ['tutorial_admin.css']
434
        self.plugin_scripts[Plugin] = ['tutorial_admin.js']
435
        
1099.1.207 by William Grant
Replace most of the tutorial headings and titles.
436
        ctx['subject'] = self.context.subject
1294.2.64 by William Grant
Port tutorial stuff.
437
        ctx['year'] = self.context.semester.year
438
        ctx['semester'] = self.context.semester.semester
1099.1.197 by Nick Chadwick
Modified worksheets edit view, so now there are links to edit, add,
439
        
440
        ctx['worksheets'] = self.context.worksheets
441
        
442
        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
443
444
1394.2.6 by William Grant
Add a basic ExerciseView, which will soon allow testing.
445
class ExerciseView(XHTMLView):
1394.2.11 by William Grant
Add an edit link to ExerciseView.
446
    """View of an exercise.
1394.2.6 by William Grant
Add a basic ExerciseView, which will soon allow testing.
447
1394.2.11 by William Grant
Add an edit link to ExerciseView.
448
    Primarily to preview and test an exercise without adding it to a
449
    worksheet for all to see.
450
    """
1394.2.6 by William Grant
Add a basic ExerciseView, which will soon allow testing.
451
    permission = 'edit'
452
    template = 'templates/exercise.html'
453
454
    def populate(self, req, ctx):
455
        self.plugin_scripts[Plugin] = ['tutorial.js']
456
        self.plugin_styles[Plugin] = ['tutorial.css']
457
1394.2.11 by William Grant
Add an edit link to ExerciseView.
458
        ctx['req'] = req
1394.2.6 by William Grant
Add a basic ExerciseView, which will soon allow testing.
459
        ctx['mediapath'] = media_url(req, Plugin, 'images/')
460
        ctx['exercise'] = self.context
461
        ctx['exercise_fragment'] = present_exercise(
462
            req, self.context.id)['stream']
1394.2.11 by William Grant
Add an edit link to ExerciseView.
463
        ctx['ExerciseEditView'] = ExerciseEditView
1463.1.3 by William Grant
ExercisesView links only to the exercise's index, which now has edit and delete links.
464
        ctx['ExerciseDeleteView'] = ExerciseDeleteView
1394.2.6 by William Grant
Add a basic ExerciseView, which will soon allow testing.
465
466
1099.1.212 by Nick Chadwick
Added a new page to display exercises. This will then be modified to
467
class ExerciseEditView(XHTMLView):
468
    """View for editing a worksheet."""
469
    permission = 'edit'
470
    template = 'templates/exercise_edit.html'
1394.2.13 by William Grant
Add an Edit breadcrumb for ExerciseEditView.
471
    breadcrumb_text = 'Edit'
472
1099.1.212 by Nick Chadwick
Added a new page to display exercises. This will then be modified to
473
    def populate(self, req, ctx):
474
        self.plugin_styles[Plugin] = ['exercise_admin.css']
475
        self.plugin_scripts[Plugin] = ['exercise_admin.js']
1394.2.13 by William Grant
Add an Edit breadcrumb for ExerciseEditView.
476
1099.6.4 by Nick Chadwick
Exercise UI is now ready to be merged into trunk.
477
        ctx['mediapath'] = media_url(req, Plugin, 'images/')
1394.2.13 by William Grant
Add an Edit breadcrumb for ExerciseEditView.
478
1099.1.212 by Nick Chadwick
Added a new page to display exercises. This will then be modified to
479
        ctx['exercise'] = self.context
480
        #XXX: These should come from somewhere else
481
1394.1.8 by William Grant
Prettify part/test/var types.
482
        ctx['var_types'] = {
483
            'var': 'variable',
484
            'arg': 'function argument',
1463.1.7 by William Grant
Disable and XXX exception variable support. It has been broken for ages.
485
            # XXX: wgrant 2010-01-29 bug=514160: Need to
486
            # restore support for this.
487
            #'exception': 'exception',
1394.1.8 by William Grant
Prettify part/test/var types.
488
            }
489
        ctx['part_types'] = {
490
            'stdout': 'standard output',
491
            '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.
492
            'result': 'function result',
493
            'exception': 'raised exception',
494
            'code': 'code',
1394.1.8 by William Grant
Prettify part/test/var types.
495
            }
1394.1.9 by William Grant
Prettify testcasepart UI a bit.
496
        ctx['test_types'] = {'norm': 'normalisation', 'check': 'comparison'}
1099.1.182 by Nick Chadwick
Added a view to allow admins to edit worksheets
497
1394.2.13 by William Grant
Add an Edit breadcrumb for ExerciseEditView.
498
1099.6.2 by Nick Chadwick
Added a listing of all exercises
499
class ExerciseDeleteView(XHTMLView):
500
    """View for confirming the deletion of an exercise."""
501
    
502
    permission = 'edit'
1099.6.3 by Nick Chadwick
Edited the exercise service to delete individual parts of an exercise.
503
    template = 'templates/exercise_delete.html'
1099.6.2 by Nick Chadwick
Added a listing of all exercises
504
    
505
    def populate(self, req, ctx):
1099.1.233 by Nick Chadwick
Exercise objects in the database module, along with their test cases,
506
507
        # If post, delete the exercise, or display a message explaining that
508
        # the exercise cannot be deleted
509
        if req.method == 'POST':
1099.1.242 by Nick Chadwick
Fixed a problem with exercise editor, which wasn't editing or adding
510
            try:
511
                self.context.delete()
1463.1.4 by William Grant
Clean up exercise deletion a lot.
512
                self.template = 'templates/exercise_deleted.html'
513
            except Exception:
514
                self.template = 'templates/exercise_undeletable.html'
1099.1.233 by Nick Chadwick
Exercise objects in the database module, along with their test cases,
515
516
        # If get, display a delete confirmation page
517
        else:
518
            if self.context.worksheet_exercises.count() is not 0:
1463.1.4 by William Grant
Clean up exercise deletion a lot.
519
                self.template = 'templates/exercise_undeletable.html'
520
1099.1.233 by Nick Chadwick
Exercise objects in the database module, along with their test cases,
521
        # Variables for the template
1099.6.2 by Nick Chadwick
Added a listing of all exercises
522
        ctx['exercise'] = self.context
523
1099.1.229 by Nick Chadwick
Fixed a slight oversight, which meant there was no way to add a new
524
class ExerciseAddView(XHTMLView):
525
    """View for creating a new exercise."""
526
    
527
    permission = 'edit'
528
    template = 'templates/exercise_add.html'
1099.1.234 by Nick Chadwick
Permissions for editing and deleting exercises now come from the
529
    #XXX: This should be done somewhere else
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):
531
        for offering in req.store.find(Offering):
532
            if 'edit' in offering.get_permissions(req.user):
533
                return True
534
        return False
535
        
536
    def populate(self, req, ctx):
537
        self.plugin_scripts[Plugin] = ['exercise_admin.js']
538
539
1099.6.2 by Nick Chadwick
Added a listing of all exercises
540
class ExercisesView(XHTMLView):
541
    """View for seeing the list of all exercises"""
1465.1.2 by William Grant
Add an 'Exercises' breadcrumb.
542
1099.6.2 by Nick Chadwick
Added a listing of all exercises
543
    permission = 'edit'
544
    template = 'templates/exercises.html'
1465.1.2 by William Grant
Add an 'Exercises' breadcrumb.
545
    breadcrumb_text = 'Exercises'
546
1099.1.234 by Nick Chadwick
Permissions for editing and deleting exercises now come from the
547
    #XXX: This should be done somewhere else
1099.6.2 by Nick Chadwick
Added a listing of all exercises
548
    def authorize(self, req):
549
        for offering in req.store.find(Offering):
550
            if 'edit' in offering.get_permissions(req.user):
551
                return True
552
        return False
1465.1.2 by William Grant
Add an 'Exercises' breadcrumb.
553
1099.6.2 by Nick Chadwick
Added a listing of all exercises
554
    def populate(self, req, ctx):
555
        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.
556
        ctx['req'] = req
1099.6.2 by Nick Chadwick
Added a listing of all exercises
557
        ctx['exercises'] = req.store.find(Exercise).order_by(Exercise.id)
558
1294.2.64 by William Grant
Port tutorial stuff.
559
1099.1.99 by William Grant
Require that plugins providing media subclass MediaPlugin.
560
class Plugin(ViewPlugin, MediaPlugin):
1294.2.71 by William Grant
Move i.w.tutorial traversal stuff into i.w.tutorial.traversal.
561
    forward_routes = (root_to_exercise, offering_to_worksheet,
562
        worksheet_to_worksheetexercise, worksheetexercise_to_exerciseattempts,
1294.2.131 by William Grant
Restore SubjectMediaView.
563
        exerciseattempts_to_attempt, subject_to_media)
1294.2.71 by William Grant
Move i.w.tutorial traversal stuff into i.w.tutorial.traversal.
564
565
    reverse_routes = (exercise_url, worksheet_url, worksheetexercise_url,
566
        exerciseattempts_url, exerciseattempt_url)
1294.2.64 by William Grant
Port tutorial stuff.
567
1294.2.100 by William Grant
Add exercise/worksheet breadcrumbs.
568
    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.
569
                   Worksheet: WorksheetBreadcrumb
1294.2.100 by William Grant
Add exercise/worksheet breadcrumbs.
570
                  }
571
1442.1.32 by William Grant
Remove WorksheetsView; superseded by OfferingView.
572
    views = [(Offering, ('+worksheets', '+new'), WorksheetAddView),
1294.2.64 by William Grant
Port tutorial stuff.
573
             (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.
574
             (Worksheet, '+index', WorksheetView),
575
             (Worksheet, '+edit', WorksheetEditView),
1294.2.64 by William Grant
Port tutorial stuff.
576
             (ApplicationRoot, ('+exercises', '+index'), ExercisesView),
577
             (ApplicationRoot, ('+exercises', '+add'), ExerciseAddView),
1394.2.6 by William Grant
Add a basic ExerciseView, which will soon allow testing.
578
             (Exercise, '+index', ExerciseView),
1294.2.64 by William Grant
Port tutorial stuff.
579
             (Exercise, '+edit', ExerciseEditView),
580
             (Exercise, '+delete', ExerciseDeleteView),
1294.2.131 by William Grant
Restore SubjectMediaView.
581
             (SubjectMediaFile, '+index', SubjectMediaView),
1294.2.64 by William Grant
Port tutorial stuff.
582
583
             (Offering, ('+worksheets', '+index'), WorksheetsRESTView, 'api'),
584
             (WorksheetExercise, '+index', WorksheetExerciseRESTView, 'api'),
585
             (ExerciseAttempts, '+index', AttemptsRESTView, 'api'),
586
             (ExerciseAttempt, '+index', AttemptRESTView, 'api'),
587
             (ApplicationRoot, ('+exercises', '+index'), ExercisesRESTView,
588
              'api'),
589
             (Exercise, '+index', ExerciseRESTView, 'api'),
590
             ]
1099.1.64 by William Grant
Move ivle.webapp.tutorial's media to the new framework. This also fixes the
591
592
    media = 'media'
1099.1.100 by Nick Chadwick
Created a new help system.
593
    help = {'Tutorial': 'help.html'}