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

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

Improve handling of errors by adding a method to unpack these in a consistent manner.
Add method to tidy inputs - to avoid unpickleable data being stored in session (which cannot be done). Currently this only deals with FieldStorage? objects.
Add global variable to store eXist client connections to the different provider IDs - to allow re-use of these across the web-app.

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