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

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

Create new util to save the current editor state into a session variable + add code to track this so that the tab state can be roughly
maintained when swapping between tabs.

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