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

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

Strip out code not relevant to MILK - mainly WCS and WMS stuff - also including the CSML server code + trackback code
Also tidy up structure of 'public' dir - setting up new 'style' dir and
centralising icons in icons dir + remove all unused icons, javascript and stylesheets.
Also strip out testcase code and populate new test directory structure.

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
25
26class EditatomController(BaseController):
27    '''
28    Provides the pylons controller for editing NDG Atom documents.
29    '''
30    ADD_ASSOCIATIONS = 3
31    REMOVE_ASSOCIATIONS = 4
32   
33    def __setup(self,uri=None):
34        ''' Common setup stuff for all the actions on this controller '''
35        logging.info("Setting up EditatomController")
36        self.cf=request.environ['ndgConfig']
37
38        if uri:
39            try:
40                self.ndgObject = ndgObject(uri, config=self.cf.config)
41            except ValueError,e:
42                return e
43
44        self.inputs=dict(parse_querystring(request.environ))
45
46        logging.info("EditatomController set up")
47        return 0
48
49
50    def __handleError(self, e, template='error'):
51        '''
52        Handle exceptions thrown; if debug mode on, display full stack trace
53        in output, otherwise just show basic error message in error template
54        @param e: Exception to process
55        @keyword template: template to render - 'error' is the default - NB, if
56        an alternative is specified it should have a div with class set to 'error'
57        containing the variable, c.xml to display properly
58        '''
59        errorMessage = e.message
60        if g.debugAtomEditor:
61            errorMessage = traceback.format_exc()#print_exception(*sys.exc_info())
62
63        c.xml = 'Unexpected error loading page [%s]' %str(errorMessage)
64        c.doc=''
65        logging.error(c.xml)
66       
67        response.status_code = 400
68        return render("genshi", template)
69
70   
71    def upload(self, uri):
72        '''
73        Upload a CSML or granulite file and store it in the session variable
74        '''
75        logging.info("Uploading file...")
76        file = request.POST.get('upload_CSML')
77        isGranulite = False
78        if file is None:
79            # check for granulite
80            file = request.POST.get('upload_granulite')
81            isGranulite = True
82           
83        if file == '':
84            errorMessage = "Error: could not load file - please try again"
85            logging.error(errorMessage)
86            c.errors = {'Load error' : errorMessage}
87            return hc.index(hc())
88
89        logging.debug("- file name: '%s'" %file.name)
90        # Prepare the basic data model
91        c.errors = {}
92        try:
93            # NB, if loading a granulite, this will create the displayed atom
94            if uri != None:
95                self.prepareDataModel(uri)
96            else:
97                self.__setup()
98                c.atom = Atom()
99        except SystemError, e:
100            return self.__handleError(e)
101
102        # now process the input file and add any extra required data
103        try:
104            if isGranulite:
105                self.__processGranuliteFile(file.value)
106            else:
107                self.__processCSMLFile(file)
108        except Exception, e:
109            return self.__handleError(e, template='atom_editor/atom_granulator')
110
111        # save new data
112        self.saveAtomToExist(c.atom)
113                   
114        # now do redirection - NB, this ensures that current atom contents are
115        # reloaded and displayed
116        logging.info("File data loaded and extracted to atom")
117        h.redirect_to(controller = 'atom_editor/editatom', action='edit', \
118                        uri = c.atom.ndgURI)
119
120
121    def __processCSMLFile(self, file):
122        '''
123        Accept the contents of a CSML file and extract and add appropriate data
124        to the current atom data model
125        @param fileContents: contents of the file uploaded
126        '''
127        logging.info("Extracting CSML data")
128        c.atom.addCSMLData(file)
129        logging.info("Finished extracting CSML data")
130   
131
132    def __processGranuliteFile(self, fileContents):
133        '''
134        Accept the contents of a granulite file and extract and add appropriate data
135        to the current atom data model
136        @param fileContents: contents of the file uploaded
137        '''
138        logging.info("Processing granulite data")
139        # check for uploaded CSML/CDML file data
140        cdmlFile = request.POST.get('upload_cdml')
141        csmlFile = request.POST.get('upload_csml')
142        if cdmlFile and csmlFile:
143            raise ValueError("Cannot specify both CDML and CSML file - please choose a single one to ingest.")
144       
145        # NB, we'll be creating the atom in the default local eXist
146        eXistClient = self.__getExistClient('local')
147        gran = granulite(fileContents, eXistClient = eXistClient, \
148                         cdmlFile = cdmlFile, csmlFile = csmlFile)
149       
150        logging.info("Finished processing granulite data")
151
152   
153    def saveAtom(self, uri, saveLevel=0):
154        '''
155        Save the atom contents - NB, validation is done by method decoration
156        - if this fails, the action is reran as a GET with htmlfill auto-populating
157        the fields to keep them as they were before the submission
158        '''
159        logging.info("Saving input atom data")
160        c.errors = {}
161        try:
162            self.prepareDataModel(uri)
163        except SystemError, e:
164            return self.__handleError(e)
165       
166        inputs = request.params
167
168        # save atom association changes
169        if int(saveLevel) == self.ADD_ASSOCIATIONS:
170            atomLinks = self.extractAtomAssociations(inputs)
171            c.atom.addUniqueRelatedLinks(atomLinks)
172        elif int(saveLevel) == self.REMOVE_ASSOCIATIONS:
173            atomLinks = self.extractAtomAssociations(inputs)
174            c.atom.removeRelatedLinks(atomLinks)
175        else:
176            authors = self.extractAuthorDetails(inputs)
177            if authors:
178                c.atom.addAuthors(authors)
179   
180            onlineRefs = self.extractOnlineReferenceDetails(inputs)
181            c.atom.addOnlineReferences(onlineRefs)
182
183            params = self.extractParameterDetails(inputs)
184            # NB, the atom type and subtype are added to the categories when the
185            # atom is exported to XML - so don't need to worry about overwriting
186            # them now
187            c.atom.parameters = params
188           
189            if inputs.get('subtype'):
190                c.atom.subtype = self.getLatestTermURLFromDropDownInput( \
191                        inputs.get('subtype'))
192                c.atom.subtypeID = c.atom.subtype.split('/')[-1]
193
194        logging.info("Validating input")
195        try:
196        #    validator = AtomFormSchema()
197        #    validator.to_python(inputs)
198            c.atom.validate()
199            logging.info("- input valid")
200        except ValidationError, e:
201        #    c.error = e.message
202            c.errors = e.unpack_errors()
203            return self.edit(uri)
204
205        self.saveAtomToExist(c.atom)
206                   
207        # now do redirection - NB, this ensures that current atom contents are
208        # reloaded and displayed
209        h.redirect_to(controller = 'atom_editor/editatom', action='edit', \
210                        uri = c.atom.ndgURI)
211
212
213    def prepareDataModel(self, uri):
214        '''
215        Set up the underlying atom data model - loading the bulk from eXist
216        then updating any input fields appropriately
217        '''
218        logging.info("Preparing underlying data model")
219        status=self.__setup(uri=uri)
220        if status:
221            c.xml='<p>%s</p>'%status
222            response.status_code = 400
223            raise SystemError('Problem experienced setting up data model')
224
225        logging.info("Retrieving document to edit")
226        # NB, don't use the cache as docs are likely to change
227        # quite a lot during editing; ensure you've always got
228        # the latest updates loaded
229        status,x = interface.GetXML(uri, useCache=False)
230
231        if not status:
232            code=400
233            if x.startswith('<p> Access Denied'):
234                code=401
235
236            c.xml='%s'%x
237            response.status_code = code
238            raise SystemError('Problem experienced retrieving atom doc from eXist')
239
240        # NB, passing in the inputs will overwrite any original values with the
241        # user input ones
242        inputs = request.params
243        c.atom = Atom(xmlString=str(x), ndgObject = self.ndgObject, **dict(inputs))
244       
245        # save the current atom - to avoid this needing be recreated by the
246        # asynch viewDeployments call
247        session['currentAtom'] = c.atom
248        session.save()
249        logging.info("Data model set up")
250
251
252    def prepareEditForm(self, uri):
253        '''
254        Get everything set up for displaying the edit form
255        @param uri: ndg url for the atom to load in edit mode
256        '''
257        try:
258            # NB, can get here directly from saveAtom - if there have been errors
259            # - in this case keep original data
260            if not c.atom:
261                self.prepareDataModel(uri)
262        except SystemError, e:
263            return self.__handleError(e)
264
265        c.title='Editing [%s]'%self.ndgObject
266        c.uri = c.atom.ndgURI
267       
268        c.saveLink = h.url_for(controller='atom_editor/editatom',action='saveAtom', \
269                               saveLevel='1',  uri = c.atom.ndgURI)
270        c.saveLink2 = h.url_for(controller='atom_editor/editatom',action='saveAtom', saveLevel='2')
271        c.saveAssoc = h.url_for(controller='atom_editor/editatom',action='saveAtom', \
272                                 saveLevel = self.REMOVE_ASSOCIATIONS)
273       
274        # adjust atom type to cope with activity deployment exception
275        atomType = c.atom.atomTypeID
276        if atomType == g.vtd.ACTIVITY_TERM and \
277            c.atom.subtypeID == g.vtd.DEPLOYMENT_TERM:
278            atomType = g.vtd.DEPLOYMENT_TERM
279       
280        c.addEntityLink = h.url_for(controller='atom_editor/listatom',action='list', searchData = '0', \
281                               associatedAtomID = c.atom.ndgURI, \
282                               associatedAtomType = atomType, 
283                               associationType = utils.ENTITY_ASSOCIATION)
284           
285        c.addGranuleLink = h.url_for(controller='atom_editor/listatom',action='list', searchData = '0', \
286                               associatedAtomID = c.atom.ndgURI, \
287                               associatedAtomType = atomType, 
288                               associationType = utils.GRANULE_ASSOCIATION)
289           
290        c.addDeploymentLink = h.url_for(controller='atom_editor/listatom',action='list', searchData = '0', \
291                               associatedAtomID = c.atom.ndgURI, \
292                               associatedAtomType = atomType, 
293                               associationType = utils.DEPLOYMENT_ASSOCIATION)
294
295        # account for special case where we're dealing with deployments
296        listVals = g.vtd.getValidSubTypes(c.atom.atomTypeID)
297        if c.atom.isDeployment():
298            listVals = [g.vtd.TERM_DATA[g.vtd.DEPLOYMENT_TERM]]
299
300        c.subTypes = utils.getVocabTermDataDropdown(listVals, \
301                                        selected=c.atom.subtype)
302       
303       
304        self.addRelatedLinksDropDowns()
305
306   
307    def edit(self, uri):
308        '''
309        Edit the specified uri
310        '''
311        logging.info("Setting up atom edit template")
312
313        self.prepareEditForm(uri)
314
315        try:
316            return render("genshi", '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('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 render("genshi", '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 render("genshi", 'atom_editor/atom_granulator')
606
607        except Exception, e:
608            return self.__handleError(e)
Note: See TracBrowser for help on using the repository browser.