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

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

Tidy up imports and use proper config object for ndgObject initialisation.

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
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, ValidationError
21import ndgUtils.models.existdbclient as edc
22from ndgUtils.models.MolesEntity import MolesEntity as ME
23from ndgUtils.models.utilities import escapeSpecialCharacters
24from ndgUtils.vocabtermdata import VocabTermData as VTD
25from granulatorTool.granulite import granulite as granulite
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)
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        self.prepareEditForm(uri)
314
315        try:
316            return savePageAndRender(self.pathInfo, "atom_editor/atom_editor")
317       
318        except ExpatError, e:
319            c.xml='XML content is not well formed'
320            c.doc=str(x)
321            logging.error("Error retrieving [%s] - XML content: %s" % (uri, e))
322
323        except Exception, e:
324            #we may be showing an xml document ... but it could go wrong if
325            #we have crap content ...
326            c.xml='Unexpected error [%s] viewing [%s]'%(str(e), uri)
327            c.doc=''
328            logging.error(c.xml)
329       
330        response.status_code = 400
331        return render('atom_editor/error')
332
333
334    def addRelatedLinksDropDowns(self):
335        '''
336        Set up the drop down lists required for the selection of online ref links
337        '''
338        # at the very least, we need a simple drop down list with no preselected
339        # values
340        logging.debug("Setting up drop down lists for related links")
341        c.relatedLinkTerms = utils.getVocabTermDataDropdown(\
342                g.vtd.getValidTypes(g.vtd.ONLINE_REF_CATEGORY))
343       
344        c.relatedLinkSelectedLists = {}
345        for link in c.atom.relatedLinks:
346            logging.debug("Adding dropdown for related link, '%s'" %(str(link)))
347            c.relatedLinkSelectedLists[str(link)] = \
348                utils.getVocabTermDataDropdown(g.vtd.getValidTypes(g.vtd.ONLINE_REF_CATEGORY), \
349                                                     selected=link.rel)
350
351        logging.debug("Finished setting up drop down lists")
352
353
354    def extractAuthorDetails(self, inputs):
355        '''
356        Retrieve author data from inputs and set appropriately on Atom, if any
357        found
358        @return: list of Person objects with the author data
359        '''
360        logging.info("Extracting author data from inputs")
361        processedAuthors = []
362        authors = []
363        for key in inputs:
364            keyBits = key.split('.')
365            if len(keyBits) == 3 and keyBits[1] not in processedAuthors:
366               
367                authorType = -1
368                if key.lower().startswith('author'):
369                    authorType = Person.AUTHOR_TYPE
370                elif key.lower().startswith('contributor'):
371                    authorType = Person.CONTRIBUTOR_TYPE
372                elif key.lower().startswith('responsible'):
373                    authorType = Person.RESPONSIBLE_PARTY_TYPE
374                else:
375                    continue
376
377                # NB, adding an empty object here is valid as it will clear out
378                # existing entries, potentially
379                author = Person(personType = authorType)
380                # check if the remove checkbox has been set
381                keyStem = ".".join(keyBits[0:2])
382                if inputs.get(keyStem + ".remove"):
383                    logging.info("Removing author data")
384                else:
385                    author.name = inputs.get(keyStem + '.name') or ""
386                    author.uri = inputs.get(keyStem + '.uri') or ""
387                    author.role = inputs.get(keyStem + '.role') or ""
388                   
389                    logging.info("Adding new author info")
390                    logging.debug("Extracted author (type:'%s', name:'%s', uri:'%s', role:'%s')" \
391                                  %(author.type, author.name, author.uri, author.role))
392                    authors.append(author)
393                processedAuthors.append(keyBits[1])
394
395        logging.info("Finished extracting author data")
396        return authors
397
398
399    def extractOnlineReferenceDetails(self, inputs):
400        '''
401        Retrieve online reference data from inputs and set appropriately on Atom, if any
402        found
403        @return: list of Link objects containing the extracted data
404        '''
405        logging.info("Extracting related links data from inputs")
406        processedLinks = []
407        links = []
408
409        for key in inputs:
410            keyBits = key.split('.')
411            if len(keyBits) == 3 and keyBits[1] not in processedLinks:
412               
413                if key.lower().startswith(Atom.ONLINE_REF_LABEL):
414                    link = Link()
415                    keyStem = ".".join(keyBits[0:2])
416                   
417                    if inputs.get(keyStem + ".remove"):
418                        logging.info("Removing online reference data")
419                    else:
420                        # NB, this is in the format vocabURL--termID, so requires further
421                        # processing
422                        link.rel = self.getLatestTermURLFromDropDownInput(inputs.get(keyStem + '.rel'))
423                        link.href = inputs.get(keyStem + '.href') or ""
424                        link.title = inputs.get(keyStem + '.title') or ""
425                           
426                        logging.info("Adding new online reference info")
427                        logging.debug("Extracted online reference (href:'%s', title:'%s', rel:'%s')" \
428                                      %(link.href, link.title, link.rel))
429                        links.append(link)
430
431                    processedLinks.append(keyBits[1])
432                else:
433                    continue
434
435        logging.info("Finished extracting links data")
436        return links
437
438
439    def extractParameterDetails(self, inputs):
440        '''
441        Retrieve parameters data from inputs and set appropriately on Atom, if any
442        found
443        @return: list of Category objects containing the extracted data
444        '''
445        logging.info("Extracting parameters data from inputs")
446        processedParameters = []
447        parameters = []
448
449        for key in inputs:
450            keyBits = key.split('.')
451            if len(keyBits) == 3 and keyBits[1] not in processedParameters:
452               
453                if key.lower().startswith(Atom.PARAMETER_LABEL):
454                    parameter = Category()
455                    keyStem = ".".join(keyBits[0:2])
456                   
457                    if inputs.get(keyStem + ".remove"):
458                        logging.info("Removing parameters data")
459                    else:
460                        parameter.term = inputs.get(keyStem + '.term') or ""
461                        parameter.scheme = inputs.get(keyStem + '.scheme') or ""
462                        parameter.label = inputs.get(keyStem + '.label') or ""
463                           
464                        logging.info("Adding new parameter info")
465                        logging.debug("Extracted parameter (vocabURL:'%s', label:'%s', term:'%s')" \
466                                      %(parameter.scheme, parameter.label, parameter.term))
467                        parameters.append(parameter)
468
469                    processedParameters.append(keyBits[1])
470                else:
471                    continue
472
473        logging.info("Finished extracting parameters data")
474        return parameters
475
476
477    def extractAtomAssociations(self, inputs):
478        '''
479        Retrieve atom data from inputs and create related links pointing to
480        this data
481        @return: list of Links representing the related atoms
482        '''
483        logging.info("Extracting related atom ID data from inputs")
484        atoms = []
485        processedAtoms = []
486
487        for key in inputs:
488            if key.lower().startswith(Atom.ATOM_REF_LABEL):
489                (x, href, title, rel) = key.split(Atom.DELIMITER)
490                # NB, we handle removes by effectively ignoring them later on
491                if href not in processedAtoms:
492                    processedAtoms.append(href)
493
494                    link = Link()
495                    link.href = href or ""
496                    link.title = title or ""
497                    link.rel = rel or ""
498                   
499                    logging.debug("Extracted atom info (href:'%s', title:'%s', rel:'%s')" \
500                                  %(link.href, link.title, link.rel))
501                    atoms.append(link)
502            else:
503                continue
504
505        logging.info("Finished extracting atoms data")
506        return atoms
507               
508
509    def getLatestTermURLFromDropDownInput(self, inputVal):
510        '''
511        Term ID and vocabURL are specified in the drop down menus
512        - using the input from this, return the lastest full href to the
513        term ID
514        '''
515        termData = inputVal.split('--')
516        return g.vtd.getCurrentVocabURI(termData[0]) + \
517                        "/" + termData[1]
518
519
520    def saveAtomToExist(self, atom):
521        '''
522        Save the specified atom in eXist
523        @param atom: atom object to save to eXist
524        @return atom: atom object saved in eXist
525        '''
526        logging.info("Saving changes to eXist")
527        self.eXist = self.__getExistClient(atom.ME.providerID)
528        createdAtom = self.eXist.createAtomInExist(atom)
529        logging.info("Changes successfully saved to eXist")
530        return createdAtom
531
532    def __getExistClient(self, providerID):
533        '''
534        Use the config data to set up and return an eXist client
535        '''
536        logging.info("Setting up eXist client")
537        # lookup the eXist DB to use according to the provider ID for the
538        # data - NB, this is specified in the ndgDiscovery.config file
539        existHost = self.cf.get('NDG_EXIST', providerID)
540        configFile = self.cf.get('NDG_EXIST','passwordFile')
541        eXistClient = edc.eXistDBClient(eXistDBHostname = existHost, \
542                                       configFile = configFile)
543        logging.info("Returning eXist client")
544        return eXistClient
545   
546    def create(self, saveData = None, **inputs):
547        '''
548        Create a new atom
549        '''
550        if saveData:
551            logging.info("Validating input")
552            try:
553                inputs = request.params
554                validator = CreateAtomFormSchema()
555                validator.to_python(inputs)
556                logging.info("- input valid")
557               
558                logging.info("Creating basic atom")
559                self.__setup()
560                atomTypeID = inputs.get('atomTypeID').split('--')[1]
561                inputs['atomTypeID'] = atomTypeID
562   
563                # activity deployments should have subtype deployment specified automatically
564                if atomTypeID == g.vtd.ACTIVITY_DEPLOYMENT_TERM:
565                    inputs['subtypeID'] = g.vtd.DEPLOYMENT_TERM
566                    inputs['atomTypeID'] = g.vtd.ACTIVITY_TERM
567                   
568                inputs['providerID'] = inputs.get('providerID').split('--')[1]
569                atom = self.saveAtomToExist(Atom(**dict(inputs)))
570               
571                h.redirect_to(controller = 'atom_editor/editatom', action='edit',
572                               uri = atom.ndgURI)
573            except Invalid, e:
574                c.errors = e.unpack_errors()
575               
576           
577        logging.info("Setting up atom create template")
578        c.title='Create new atom'
579       
580        # set up the drop down content - NB, add special case, 'deployment activity'
581        # - this is just a specialised activity - i.e. with subtype preset
582        deploymentActivity = g.vtd.TERM_DATA[g.vtd.ACTIVITY_DEPLOYMENT_TERM]
583        c.atomTypes = utils.getVocabTermDataDropdown(g.vtd.getValidTypes(g.vtd.ATOM_CATEGORY),
584                                               defaultVal = deploymentActivity, \
585                                               selected = inputs.get('atomTypeID'))
586        c.providerIDs = utils.getVocabTermDataDropdown(
587                            g.vtd.getValidTypes(g.vtd.PROVIDER_CATEGORY), 
588                            selected = inputs.get('providerID'))
589
590        try:
591            return savePageAndRender(self.pathInfo, 'atom_editor/atom_creator')
592
593        except Exception, e:
594            return self.__handleError(e)
595
596   
597    def createGranule(self, **inputs):
598        '''
599        Create a new atom from a granulite file
600        '''
601        logging.info("Setting up create atom from granulite template")
602        c.title='Create new data granule atom - from a granulite file'
603       
604        try:
605            return savePageAndRender(self.pathInfo, 'atom_editor/atom_granulator')
606
607        except Exception, e:
608            return self.__handleError(e)
Note: See TracBrowser for help on using the repository browser.