source: MILK/trunk/milk_server/milk_server/controllers/atom_editor/editatom.py @ 4514

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/MILK/trunk/milk_server/milk_server/controllers/atom_editor/editatom.py@4514
Revision 4514, 26.7 KB checked in by cbyrom, 11 years ago (diff)

Fix problem with retaining empty category data + improve output error logging + improve robustness of exist file retrieval.

Line 
1'''
2 Class representing pylons controller for the creation and editing of atom
3 data
4 
5 @author: C Byrom, Tessella Sep 2008
6'''
7import logging, traceback, sys, cgi
8from paste.request import parse_querystring
9from xml.parsers.expat import ExpatError
10from formencode import Invalid
11from genshi.filters import HTMLFormFiller
12from genshi import HTML
13from milk_server.lib.base import *
14from milk_server.models.form import *
15from milk_server.lib import mailer
16from milk_server.lib.ndgInterface import interface
17import milk_server.lib.htmlUtilities as utils
18from milk_server.lib.atomutilities import savePageAndRender
19from ndgUtils import ndgObject
20from ndgUtils.models.Atom import Atom, Person, Link, Category
21from ndgUtils.eXistConnector import eXistConnector
22from ndgUtils.lib.atomvalidator import ValidationError
23import ndgUtils.models.existdbclient as edc
24from ndgUtils.models.MolesEntity import MolesEntity as ME
25from ndgUtils.lib.utilities import escapeSpecialCharacters
26from ndgUtils.vocabtermdata import VocabTermData as VTD
27from granulatorTool.granulite import granulite as granulite
28
29class EditatomController(BaseController):
30    '''
31    Provides the pylons controller for editing NDG Atom documents.
32    '''
33    ADD_ASSOCIATIONS = 3
34    REMOVE_ASSOCIATIONS = 4
35   
36    def __setup(self,uri=None):
37        ''' Common setup stuff for all the actions on this controller '''
38        logging.info("Setting up EditatomController")
39        self.cf=request.environ['ndgConfig']
40
41        if uri:
42            try:
43                self.ndgObject = ndgObject(uri, config=self.cf)
44            except ValueError,e:
45                return e
46
47        self.inputs=dict(parse_querystring(request.environ))
48
49        logging.info("EditatomController set up")
50        return 0
51
52
53    def __unpackErrors(self, e):
54        '''
55        Add exception errors to the common error structures - for use
56        in templates
57        @param e: Exception to add
58        '''
59        errorMessage = e.message
60        if g.debugAtomEditor:
61            errorMessage = traceback.format_exc()
62
63        c.xml = escapeSpecialCharacters('Unexpected error loading page [%s]' \
64                                        %str(errorMessage))
65        c.doc=''
66       
67        # unpack errors, if possible - NB, the c.errors value is used by the template
68        # function, displayErrors, which is used by most of the editor templates
69        if hasattr(e, 'unpack_errors'):
70            c.errors.update(e.unpack_errors())
71        else:
72            c.errors['Unexpected error'] = [c.xml]
73
74        # tidy up errors - escaping any xml tags, if necessary
75        for key, errors in c.errors.items():
76            newErrors = []
77            for error in errors:
78                for err in error.split('<br/>'):
79                    newErrors.append(escapeSpecialCharacters(err))
80            c.errors[key] = newErrors
81
82
83    def __handleError(self, e, template='error'):
84        '''
85        Handle exceptions thrown; if debug mode on, display full stack trace
86        in output, otherwise just show basic error message in error template
87        @param e: Exception to process
88        @keyword template: template to render - 'error' is the default - NB, if
89        an alternative is specified it should have a div with class set to 'error'
90        containing the variable, c.xml to display properly
91        '''
92        self.__unpackErrors(e)
93        logging.error(c.xml)
94        response.status_code = 400
95        return render("genshi", template)
96
97   
98    def upload(self, uri):
99        '''
100        Upload a CSML or granulite file and store it in the session variable
101        NB, if the uri is specified, we're already dealing with an atom
102        (which this refers to) - so the file is not a granulite - since
103        this is used to create the atom
104        '''
105        logging.info("Uploading file...")
106        if uri:
107            file = request.POST.get('upload_CSML')
108        else:
109            file = request.POST.get('upload_granulite')
110
111        c.errors = {}
112        try:
113            if file == '':
114                errorMessage = "Error: could not load file - please try again"
115                logging.error(errorMessage)
116                raise IOError(errorMessage)
117            else:
118                logging.debug("- file name: '%s'" %file.name)
119
120                # Prepare the basic data model
121                # NB, if loading a granulite, this will create the displayed atom
122                if uri:
123                    self.prepareDataModel(uri)
124                else:
125                    self.__setup()
126                    c.atom = Atom()
127
128                # now process the input file and add any extra required data
129                if uri:
130                    self.__processCSMLFile(file)
131                else:
132                    self.__processGranuliteFile(file.value)
133
134                # save new data
135                self.saveAtomToExist(c.atom)
136                           
137                # now do redirection - NB, this ensures that current atom contents are
138                # reloaded and displayed
139                logging.info("File data loaded and extracted to atom")
140        except Exception, e:
141            c.errors['WARNING'] = ['Error loading data: the displayed data will not be saved - please fix problem and retry']
142            self.__unpackErrors(e)
143
144        if uri and c.atom:
145            return self.edit(c.atom.ndgURI)
146        elif uri:
147            # something has gone wrong here...
148            return render("genshi", 'error')
149        else:
150            return self.createGranule()
151
152
153    def __processCSMLFile(self, file):
154        '''
155        Accept the contents of a CSML file and extract and add appropriate data
156        to the current atom data model
157        @param fileContents: contents of the file uploaded
158        '''
159        logging.info("Extracting CSML data")
160        csmlDoc = c.atom.addCSMLData(file.filename, file.value)
161        logging.info("Finished extracting CSML data")
162       
163        logging.info("Adding CSML file to eXist")
164        eXist = self.__getExistClient(c.atom.ME.providerID)
165        eXist.createOrUpdateEXistFile(csmlDoc.toPrettyXML(), \
166                                      eXistConnector.NDG_A_COLLECTION_PATH + \
167                                      c.atom.ME.providerID + '/', \
168                                      file.filename)
169        logging.info("CSML file added to eXist")
170
171   
172
173    def __processGranuliteFile(self, fileContents):
174        '''
175        Accept the contents of a granulite file and extract and add appropriate data
176        to the current atom data model
177        @param fileContents: contents of the file uploaded
178        '''
179        logging.info("Processing granulite data")
180        # check for uploaded CSML/CDML file data
181        cdmlFile = request.POST.get('upload_cdml')
182        csmlFile = request.POST.get('upload_csml')
183        if cdmlFile and csmlFile:
184            raise ValueError("Cannot specify both CDML and CSML file - please choose a single one to ingest.")
185       
186        # NB, we'll be creating the atom in the default local eXist
187        eXistClient = self.__getExistClient('local')
188        gran = granulite(fileContents, eXistClient = eXistClient, \
189                         cdmlFile = cdmlFile, csmlFile = csmlFile)
190       
191        logging.info("Finished processing granulite data")
192
193
194    def __getTidyInputs(self):
195        '''
196        The inputs can be used generically to specify atom attributes - however
197        some inputs should not be set against the Atom object since they use slots
198        so cannot be pickled - which means saving the atom in the session with fail.
199        This method clears out any unwanted inputs
200        '''
201        logging.debug("Getting pickleable input data")
202        inputs = request.params
203        tidyInputs = {}
204        for key, val in inputs.items():
205            if not isinstance(val, cgi.FieldStorage):
206                tidyInputs[key] = val
207        logging.debug("Pickleable data extracted")
208        return tidyInputs
209
210   
211    def saveAtom(self, uri, saveLevel=0):
212        '''
213        Save the atom contents - NB, validation is done by method decoration
214        - if this fails, the action is reran as a GET with htmlfill auto-populating
215        the fields to keep them as they were before the submission
216        '''
217        logging.info("Saving input atom data")
218        c.errors = {}
219        try:
220            self.prepareDataModel(uri)
221        except SystemError, e:
222            return self.__handleError(e)
223       
224        inputs = request.params
225
226        # save atom association changes
227        if int(saveLevel) == self.ADD_ASSOCIATIONS:
228            atomLinks = self.extractAtomAssociations(inputs)
229            c.atom.addUniqueRelatedLinks(atomLinks)
230        elif int(saveLevel) == self.REMOVE_ASSOCIATIONS:
231            atomLinks = self.extractAtomAssociations(inputs)
232            c.atom.removeRelatedLinks(atomLinks)
233        else:
234            authors = self.extractAuthorDetails(inputs)
235            if authors:
236                c.atom.addAuthors(authors)
237   
238            onlineRefs = self.extractOnlineReferenceDetails(inputs)
239            c.atom.addOnlineReferences(onlineRefs)
240
241            params = self.extractParameterDetails(inputs)
242            # NB, the atom type and subtype are added to the categories when the
243            # atom is exported to XML - so don't need to worry about overwriting
244            # them now
245            c.atom.parameters = params
246           
247            if inputs.get('subtype'):
248                c.atom.subtype = self.getLatestTermURLFromDropDownInput( \
249                        inputs.get('subtype'))
250                c.atom.subtypeID = c.atom.subtype.split('/')[-1]
251
252        logging.info("Validating input")
253        try:
254            g.validator.setAtom(c.atom)
255            g.validator.validateAtom()
256            logging.info("- input valid")
257        except ValidationError, e:
258            self.__unpackErrors(e)
259            logging.info("- input invalid")
260            return self.edit(uri)
261
262        self.saveAtomToExist(c.atom)
263                   
264        # now do redirection - NB, this ensures that current atom contents are
265        # reloaded and displayed
266        h.redirect_to(controller = 'atom_editor/editatom', action='edit', \
267                        uri = c.atom.ndgURI)
268
269
270    def prepareDataModel(self, uri):
271        '''
272        Set up the underlying atom data model - loading the bulk from eXist
273        then updating any input fields appropriately
274        '''
275        logging.info("Preparing underlying data model")
276        status=self.__setup(uri=uri)
277        if status:
278            c.xml='<p>%s</p>'%status
279            response.status_code = 400
280            raise SystemError('Problem experienced setting up data model')
281
282        logging.info("Retrieving document to edit")
283        # NB, don't use the cache as docs are likely to change
284        # quite a lot during editing; ensure you've always got
285        # the latest updates loaded
286        status,x = interface.GetXML(uri, useCache=False)
287
288        if not status:
289            code=400
290            if x.startswith('<p> Access Denied'):
291                code=401
292
293            c.xml='%s'%x
294            response.status_code = code
295            raise SystemError('Problem experienced retrieving atom doc from eXist')
296
297        # NB, passing in the inputs will overwrite any original values with the
298        # user input ones
299        inputs = self.__getTidyInputs()
300       
301        c.atom = Atom(xmlString=str(x), ndgObject = self.ndgObject, **dict(inputs))
302       
303        # save the current atom - to avoid this needing be recreated by the
304        # asynch viewDeployments call
305        session['currentAtom'] = c.atom
306        session.save()
307        logging.info("Data model set up")
308
309
310    def prepareEditForm(self, uri):
311        '''
312        Get everything set up for displaying the edit form
313        @param uri: ndg url for the atom to load in edit mode
314        '''
315        try:
316            # NB, can get here directly from saveAtom - if there have been errors
317            # - in this case keep original data
318            if not c.atom:
319                self.prepareDataModel(uri)
320        except SystemError, e:
321            return self.__handleError(e)
322
323        c.title='Editing [%s]'%self.ndgObject
324        c.uri = c.atom.ndgURI
325       
326        c.saveLink = h.url_for(controller='atom_editor/editatom',action='saveAtom', \
327                               saveLevel='1',  uri = c.atom.ndgURI)
328        c.saveLink2 = h.url_for(controller='atom_editor/editatom',action='saveAtom', saveLevel='2')
329        c.saveAssoc = h.url_for(controller='atom_editor/editatom',action='saveAtom', \
330                                 saveLevel = self.REMOVE_ASSOCIATIONS)
331       
332        # adjust atom type to cope with activity deployment exception
333        atomType = c.atom.atomTypeID
334        if atomType == g.vtd.ACTIVITY_TERM and \
335            c.atom.subtypeID == g.vtd.DEPLOYMENT_TERM:
336            atomType = g.vtd.DEPLOYMENT_TERM
337       
338        c.addEntityLink = h.url_for(controller='atom_editor/listatom',action='list', searchData = '0', \
339                               associatedAtomID = c.atom.ndgURI, \
340                               associatedAtomType = atomType, 
341                               associationType = utils.ENTITY_ASSOCIATION)
342           
343        c.addGranuleLink = h.url_for(controller='atom_editor/listatom',action='list', searchData = '0', \
344                               associatedAtomID = c.atom.ndgURI, \
345                               associatedAtomType = atomType, 
346                               associationType = utils.GRANULE_ASSOCIATION)
347           
348        c.addDeploymentLink = h.url_for(controller='atom_editor/listatom',action='list', searchData = '0', \
349                               associatedAtomID = c.atom.ndgURI, \
350                               associatedAtomType = atomType, 
351                               associationType = utils.DEPLOYMENT_ASSOCIATION)
352
353        # account for special case where we're dealing with deployments
354        listVals = g.vtd.getValidSubTypes(c.atom.atomTypeID)
355        if c.atom.isDeployment():
356            listVals = [g.vtd.TERM_DATA[g.vtd.DEPLOYMENT_TERM]]
357
358        c.subTypes = utils.getVocabTermDataDropdown(listVals, \
359                                        selected=c.atom.subtype)
360       
361        self.addRelatedLinksDropDowns()
362
363   
364    def edit(self, uri):
365        '''
366        Edit the specified uri
367        '''
368        logging.info("Setting up atom edit template")
369        self.prepareEditForm(uri)
370
371        try:
372            return savePageAndRender(self.pathInfo, "atom_editor/atom_editor")
373       
374        except ExpatError, e:
375            c.xml='XML content is not well formed'
376            c.doc=str(x)
377            logging.error("Error retrieving [%s] - XML content: %s" % (uri, e))
378
379        except Exception, e:
380            #we may be showing an xml document ... but it could go wrong if
381            #we have crap content ...
382            c.xml='Unexpected error [%s] viewing [%s]'%(str(e), uri)
383            c.doc=''
384            logging.error(c.xml)
385       
386        response.status_code = 400
387        return render("genshi", 'atom_editor/error')
388
389
390    def addRelatedLinksDropDowns(self):
391        '''
392        Set up the drop down lists required for the selection of online ref links
393        '''
394        # at the very least, we need a simple drop down list with no preselected
395        # values
396        logging.debug("Setting up drop down lists for related links")
397        c.relatedLinkTerms = utils.getVocabTermDataDropdown(\
398                g.vtd.getValidTypes(g.vtd.ONLINE_REF_CATEGORY))
399       
400        c.relatedLinkSelectedLists = {}
401        for link in c.atom.relatedLinks:
402            logging.debug("Adding dropdown for related link, '%s'" %(str(link)))
403            c.relatedLinkSelectedLists[str(link)] = \
404                utils.getVocabTermDataDropdown(g.vtd.getValidTypes(g.vtd.ONLINE_REF_CATEGORY), \
405                                                     selected=link.rel)
406
407        logging.debug("Finished setting up drop down lists")
408
409
410    def extractAuthorDetails(self, inputs):
411        '''
412        Retrieve author data from inputs and set appropriately on Atom, if any
413        found
414        @return: list of Person objects with the author data
415        '''
416        logging.info("Extracting author data from inputs")
417        processedAuthors = []
418        authors = []
419        for key in inputs:
420            keyBits = key.split('.')
421            if len(keyBits) == 3 and keyBits[1] not in processedAuthors:
422               
423                authorType = -1
424                if key.lower().startswith('author'):
425                    authorType = Person.AUTHOR_TYPE
426                elif key.lower().startswith('contributor'):
427                    authorType = Person.CONTRIBUTOR_TYPE
428                elif key.lower().startswith('responsible'):
429                    authorType = Person.RESPONSIBLE_PARTY_TYPE
430                else:
431                    continue
432
433                # NB, adding an empty object here is valid as it will clear out
434                # existing entries, potentially
435                author = Person(personType = authorType)
436                # check if the remove checkbox has been set
437                keyStem = ".".join(keyBits[0:2])
438                if inputs.get(keyStem + ".remove"):
439                    logging.info("Removing author data")
440                else:
441                    author.name = inputs.get(keyStem + '.name') or ""
442                    author.uri = inputs.get(keyStem + '.uri') or ""
443                    author.role = inputs.get(keyStem + '.role') or ""
444                   
445                    logging.info("Adding new author info")
446                    logging.debug("Extracted author (type:'%s', name:'%s', uri:'%s', role:'%s')" \
447                                  %(author.type, author.name, author.uri, author.role))
448                    authors.append(author)
449                processedAuthors.append(keyBits[1])
450
451        logging.info("Finished extracting author data")
452        return authors
453
454
455    def extractOnlineReferenceDetails(self, inputs):
456        '''
457        Retrieve online reference data from inputs and set appropriately on Atom, if any
458        found
459        @return: list of Link objects containing the extracted data
460        '''
461        logging.info("Extracting related links data from inputs")
462        processedLinks = []
463        links = []
464
465        for key in inputs:
466            keyBits = key.split('.')
467            if len(keyBits) == 3 and keyBits[1] not in processedLinks:
468               
469                if key.lower().startswith(Atom.ONLINE_REF_LABEL):
470                    link = Link()
471                    keyStem = ".".join(keyBits[0:2])
472                   
473                    if inputs.get(keyStem + ".remove"):
474                        logging.info("Removing online reference data")
475                    else:
476                        # NB, this is in the format vocabURL--termID, so requires further
477                        # processing
478                        link.rel = self.getLatestTermURLFromDropDownInput(inputs.get(keyStem + '.rel'))
479                        link.href = inputs.get(keyStem + '.href') or ""
480                        link.title = inputs.get(keyStem + '.title') or ""
481                       
482                        if not link.hasValue():
483                            continue
484                           
485                        logging.info("Adding new online reference info")
486                        logging.debug("Extracted online reference (href:'%s', title:'%s', rel:'%s')" \
487                                      %(link.href, link.title, link.rel))
488                        links.append(link)
489
490                    processedLinks.append(keyBits[1])
491                else:
492                    continue
493
494        logging.info("Finished extracting links data")
495        return links
496
497
498    def extractParameterDetails(self, inputs):
499        '''
500        Retrieve parameters data from inputs and set appropriately on Atom, if any
501        found
502        @return: list of Category objects containing the extracted data
503        '''
504        logging.info("Extracting parameters data from inputs")
505        processedParameters = []
506        parameters = []
507
508        for key in inputs:
509            keyBits = key.split('.')
510            if len(keyBits) == 3 and keyBits[1] not in processedParameters:
511               
512                if key.lower().startswith(Atom.PARAMETER_LABEL):
513                    parameter = Category()
514                    keyStem = ".".join(keyBits[0:2])
515                   
516                    if inputs.get(keyStem + ".remove"):
517                        logging.info("Removing parameters data")
518                    else:
519                        parameter.term = inputs.get(keyStem + '.term') or ""
520                        parameter.scheme = inputs.get(keyStem + '.scheme') or ""
521                        parameter.label = inputs.get(keyStem + '.label') or ""
522                           
523                        logging.info("Adding new parameter info")
524                        logging.debug("Extracted parameter (vocabURL:'%s', label:'%s', term:'%s')" \
525                                      %(parameter.scheme, parameter.label, parameter.term))
526                        parameters.append(parameter)
527
528                    processedParameters.append(keyBits[1])
529                else:
530                    continue
531
532        logging.info("Finished extracting parameters data")
533        return parameters
534
535
536    def extractAtomAssociations(self, inputs):
537        '''
538        Retrieve atom data from inputs and create related links pointing to
539        this data
540        @return: list of Links representing the related atoms
541        '''
542        logging.info("Extracting related atom ID data from inputs")
543        atoms = []
544        processedAtoms = []
545
546        for key in inputs:
547            if key.lower().startswith(Atom.ATOM_REF_LABEL):
548                (x, href, title, rel) = key.split(Atom.DELIMITER)
549                # NB, we handle removes by effectively ignoring them later on
550                if href not in processedAtoms:
551                    processedAtoms.append(href)
552
553                    link = Link()
554                    link.href = href or ""
555                    link.title = title or ""
556                    link.rel = rel or ""
557                   
558                    logging.debug("Extracted atom info (href:'%s', title:'%s', rel:'%s')" \
559                                  %(link.href, link.title, link.rel))
560                    atoms.append(link)
561            else:
562                continue
563
564        logging.info("Finished extracting atoms data")
565        return atoms
566               
567
568    def getLatestTermURLFromDropDownInput(self, inputVal):
569        '''
570        Term ID and vocabURL are specified in the drop down menus
571        - using the input from this, return the lastest full href to the
572        term ID
573        '''
574        termData = inputVal.split('--')
575        return g.vtd.getCurrentVocabURI(termData[0]) + \
576                        "/" + termData[1]
577
578
579    def saveAtomToExist(self, atom):
580        '''
581        Save the specified atom in eXist
582        @param atom: atom object to save to eXist
583        @return atom: atom object saved in eXist
584        '''
585        logging.info("Saving changes to eXist")
586        eXist = self.__getExistClient(atom.ME.providerID)
587        createdAtom = eXist.createAtomInExist(atom)
588        logging.info("Changes successfully saved to eXist")
589        return createdAtom
590
591
592    def __getExistClient(self, providerID):
593        '''
594        Use the config data to set up and return an eXist client
595        '''
596        logging.info("Setting up eXist client to provider, '%s'" %providerID)
597        # firstly check if there is a current connection available
598        eXistClient = g.eXistDBCons.get(providerID)
599        if eXistClient:
600            logging.info("- found existing client to provider - returning this")
601            return eXistClient
602       
603        # lookup the eXist DB to use according to the provider ID for the
604        # data - NB, this is specified in the ndgDiscovery.config file
605        existHost = self.cf.get('NDG_EXIST', providerID)
606        configFile = self.cf.get('NDG_EXIST','passwordFile')
607        eXistClient = edc.eXistDBClient(eXistDBHostname = existHost, \
608                                       configFile = configFile)
609        # add the client to the global variables for re-use
610        g.eXistDBCons[providerID] = eXistClient
611        logging.info("Returning eXist client")
612        return eXistClient
613
614   
615    def create(self, saveData = None, **inputs):
616        '''
617        Create a new atom
618        '''
619        if saveData:
620            logging.info("Validating input")
621            try:
622                inputs = request.params
623                validator = CreateAtomFormSchema()
624                validator.to_python(inputs)
625                logging.info("- input valid")
626               
627                logging.info("Creating basic atom")
628                self.__setup()
629                atomTypeID = inputs.get('atomTypeID').split('--')[1]
630                inputs['atomTypeID'] = atomTypeID
631   
632                # activity deployments should have subtype deployment specified automatically
633                if atomTypeID == g.vtd.ACTIVITY_DEPLOYMENT_TERM:
634                    inputs['subtypeID'] = g.vtd.DEPLOYMENT_TERM
635                    inputs['atomTypeID'] = g.vtd.ACTIVITY_TERM
636                   
637                inputs['providerID'] = inputs.get('providerID').split('--')[1]
638                atom = self.saveAtomToExist(Atom(**dict(inputs)))
639               
640                h.redirect_to(controller = 'atom_editor/editatom', action='edit',
641                               uri = atom.ndgURI)
642            except Invalid, e:
643                c.errors = e.unpack_errors()
644               
645           
646        logging.info("Setting up atom create template")
647        c.title='Create new atom'
648       
649        # set up the drop down content - NB, add special case, 'deployment activity'
650        # - this is just a specialised activity - i.e. with subtype preset
651        deploymentActivity = g.vtd.TERM_DATA[g.vtd.ACTIVITY_DEPLOYMENT_TERM]
652        c.atomTypes = utils.getVocabTermDataDropdown(g.vtd.getValidTypes(g.vtd.ATOM_CATEGORY),
653                                               defaultVal = deploymentActivity, \
654                                               selected = inputs.get('atomTypeID'))
655        c.providerIDs = utils.getVocabTermDataDropdown(
656                            g.vtd.getValidTypes(g.vtd.PROVIDER_CATEGORY), 
657                            selected = inputs.get('providerID'))
658
659        try:
660            return savePageAndRender(self.pathInfo, 'atom_editor/atom_creator')
661
662        except Exception, e:
663            return self.__handleError(e)
664
665   
666    def createGranule(self, **inputs):
667        '''
668        Create a new atom from a granulite file
669        '''
670        logging.info("Setting up create atom from granulite template")
671        c.title='Create new data granule atom - from a granulite file'
672       
673        try:
674            return savePageAndRender(self.pathInfo, 'atom_editor/atom_granulator')
675
676        except Exception, e:
677            return self.__handleError(e)
Note: See TracBrowser for help on using the repository browser.