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

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

Restructure atom editor controllers - pulling out parent superclass
to allow re-use of shared code + use new routes names + add new method
to handle drop down setting up.

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 xml.parsers.expat import ExpatError
9from formencode import Invalid
10from genshi.filters import HTMLFormFiller
11from genshi import HTML
12from milk_server.lib.base import *
13from milk_server.models.form import *
14from milk_server.lib import mailer
15import milk_server.lib.htmlUtilities as utils
16from ndgUtils.models.Atom import Atom, Person, Link, Category
17from ndgUtils.lib.atomvalidator import ValidationError
18import ndgUtils.lib.existdbclient as edc
19from ndgUtils.models.MolesEntity import MolesEntity as ME
20from ndgUtils.lib.utilities import escapeSpecialCharacters
21from ndgUtils.models.vocabtermdata import VocabTermData as VTD
22from ndgUtils.lib.granulite import granulite
23from editorconstants import *
24from atomeditorcontroller import AtomEditorController
25
26class EditatomController(AtomEditorController):
27    '''
28    Provides the pylons controller for editing NDG Atom documents.
29    '''
30    def upload(self, uri):
31        '''
32        Upload a CSML, CDML or granulite file and store it in the session variable
33        NB, if the uri is specified, we're already dealing with an atom
34        (which this refers to) - so the file is not a granulite - since
35        this is used to create an atom from scratch
36        '''
37        logging.info("Uploading file...")
38        self._setup(uri=uri)
39       
40        granFile = request.POST.get('upload_granulite')
41        csmlOrCdmlFile = request.POST.get('CSMLOrCDML')
42       
43        # check whether we can replace existing atoms
44        replaceAtom = self.inputs.get('replaceAtom')
45       
46        # NB, need to turn from string to boolean - there doesn't seem a reliable
47        # way of doing this using built in methods - so just do simple check
48        if replaceAtom == 'True':
49            replaceAtom = True
50        else:
51            replaceAtom = False
52       
53        # if this is true, then re-extract the inputs from the session data
54        if replaceAtom:
55            if session.get(GRAN_FILE_VALUE):
56                granFile = cgi.FieldStorage()
57                granFile.value = session.get(GRAN_FILE_VALUE)
58                granFile.filename = session.get(GRAN_FILE_NAME)
59                del session[GRAN_FILE_VALUE]
60                del session[GRAN_FILE_NAME]
61               
62            if session.get(CSML_OR_CDML_FILE_VALUE):
63                csmlOrCdmlFile = cgi.FieldStorage()
64                csmlOrCdmlFile.value = session.get(CSML_OR_CDML_FILE_VALUE)
65                csmlOrCdmlFile.filename = session.get(CSML_OR_CDML_FILE_NAME)
66                del session[CSML_OR_CDML_FILE_VALUE]
67                del session[CSML_OR_CDML_FILE_NAME]
68         
69        c.errors = {}
70        try:
71            logging.info("Validating inputs")
72            validator = LoadGranuliteFormSchema()
73            validator.to_python(self.inputs)
74            logging.info("- inputs valid")
75           
76            useCSMLID = True
77            if uri:
78                useCSMLID = False
79
80            if (granFile == '' or granFile == None) and \
81                (csmlOrCdmlFile == '' or csmlOrCdmlFile == None):
82                errorMessage = "Error: could not load file - please try again"
83                logging.error(errorMessage)
84                raise IOError(errorMessage)
85            else:
86                # Prepare the basic data model
87                # NB, if loading a granulite, this will create the displayed atom
88                # with the ID taken from the CSML file, if specified
89                fileContents = None
90                if (granFile is not None and granFile != ''):
91                    fileContents = granFile.value
92                   
93                # use the granulite helper class to add either the full granulite
94                # data or just the CSML/CDML data
95                # NB, we'll be creating the atom in the default local eXist
96                eXistClient = self._getExistClient('local')
97                gran = granulite(fileContents, granuleAtom = c.atom, \
98                                 eXistClient = eXistClient, \
99                                 csmlOrCdmlFile = csmlOrCdmlFile, \
100                                 timeAxis = self.inputs.get('timeAxis'), \
101                                 datasetID = self.inputs.get('datasetID'), \
102                                 useCSMLID = useCSMLID, \
103                                 replaceAtom = replaceAtom)
104
105                # now process the input file and add any extra required data
106                if uri:
107                    c.atom = gran.processCSMLOrCDMLFile()
108
109                    # save new data - NB, for granulites, this is done as part of the
110                    # processing steps
111                    self.saveAtomToExist(c.atom)
112                else:
113                    try:
114                        c.atom = gran.processGranulite()
115
116                        # Now set up the ndgObject with the created atom's vals
117                        self._setup(uri=c.atom.ndgURI, loadAtom = False)
118                        c.atom.ndgObject = self.ndgObject
119                    except edc.DuplicateError, e:
120                        # we've found an existing atom with the same ID
121                        # - give the users the choice of replacing the contents of this atom
122                        # or just exiting
123                        # - NB, do this via a session variable to act as a flag
124                        # for a javascript command
125                        session[OVERWRITE_GRANULE_FLAG] = e.message
126                       
127                        # store the inputs data for easy retrieval
128                        # - NB, file fields don't behave as text fields - for security
129                        # purposes - so need to store their data as session variables
130                        # for easy retrieval
131                        # - Also, cannot pickle the cgi.FieldStorage object so extract
132                        # picklable data and recreate on the return run
133                        if granFile != '':
134                            session[GRAN_FILE_VALUE] = granFile.value
135                            session[GRAN_FILE_NAME] = granFile.filename
136                        if csmlOrCdmlFile != '':
137                            session[CSML_OR_CDML_FILE_VALUE] = csmlOrCdmlFile.value
138                            session[CSML_OR_CDML_FILE_NAME] = csmlOrCdmlFile.filename
139                       
140                        # need to return to original screen - so clear out variables
141                        c.atom = None
142                        uri = None
143                           
144                # now do redirection - NB, this ensures that current atom contents are
145                # reloaded and displayed
146                logging.info("File data loaded and extracted to atom")
147        except Invalid, e:
148            logging.info(" - inputs invalid")
149            c.errors = e.unpack_errors()
150        except Exception, e:
151            c.errors['WARNING'] = ['Error loading data: the displayed data will not be saved - please fix problem and retry']
152            self._unpackErrors(e)
153        except SystemExit, ee:
154            # NB, some of the CSML libraries just sys.exit on problems - catch errors here
155            c.errors['ERROR'] = ['Problem encountered whilst transforming the CDML data into CSML']
156            self._unpackErrors(ee)
157
158        if c.atom and hasattr(c.atom, 'ndgURI'):
159            self.pathInfo = self.pathInfo.replace('upload', 'editAtom')
160
161            h.redirect_to(h.url_for('edit', uri = c.atom.ndgURI))           
162        elif uri:
163            # something has gone wrong here...
164            return render("genshi", 'atom_editor/error')
165        else:
166            return self.createGranule(**self.inputs)
167
168   
169    def saveAtom(self, uri, saveLevel=0):
170        '''
171        Save the atom contents - NB, validation is done by method decoration
172        - if this fails, the action is reran as a GET with htmlfill auto-populating
173        the fields to keep them as they were before the submission
174        '''
175        logging.info("Saving input atom data")
176        c.errors = {}
177        try:
178            self._setup(uri)
179        except Exception, e:
180            return self._handleError(e)
181       
182        # save atom association changes
183        if int(saveLevel) == self.ADD_ASSOCIATIONS:
184            atomLinks = self.extractAtomAssociations(self.inputs)
185            c.atom.addUniqueRelatedLinks(atomLinks)
186        elif int(saveLevel) == self.REMOVE_ASSOCIATIONS:
187            atomLinks = self.extractAtomAssociations(self.inputs)
188            c.atom.removeRelatedLinks(atomLinks)
189        else:
190            authors = self.extractAuthorDetails(self.inputs)
191            c.atom.addAuthors(authors)
192   
193            onlineRefs = self.extractOnlineReferenceDetails(self.inputs)
194            c.atom.addOnlineReferences(onlineRefs)
195
196            params = self.extractParameterDetails(self.inputs)
197            # NB, the atom type and subtype are added to the categories when the
198            # atom is exported to XML - so don't need to worry about overwriting
199            # them now
200            c.atom.parameters = params
201           
202            if self.inputs.get('subtype'):
203                c.atom.subtype = self.getLatestTermURLFromDropDownInput( \
204                        self.inputs.get('subtype'))
205                c.atom.subtypeID = c.atom.subtype.split('/')[-1]
206
207        logging.info("Validating input")
208        try:
209            g.validator.setAtom(c.atom)
210            g.validator.validateAtom()
211            logging.info("- input valid")
212
213            self.saveAtomToExist(c.atom)
214        except Exception, e:
215            self._unpackErrors(e)
216            logging.info("- input invalid")
217            return self.edit(uri)
218                   
219        # now do redirection - NB, this ensures that current atom contents are
220        # reloaded and displayed
221        h.redirect_to(h.url_for(controller = 'atom_editor/editatom', action='edit', \
222                        uri = c.atom.ndgURI))
223
224
225    def prepareEditForm(self, uri):
226        '''
227        Get everything set up for displaying the edit form
228        @param uri: ndg url for the atom to load in edit mode
229        '''
230        if not c.errors:
231            c.errors = {}
232
233        # NB, can get here directly from saveAtom - if there have been errors
234        # - in this case keep original data
235        if not c.atom:
236            self._setup(uri)
237           
238        c.title= EDIT_TITLE %c.atom.ndgURI
239        c.uri = c.atom.ndgURI
240       
241        c.saveLink = h.url_for('save', saveLevel = self.STANDARD_SAVE, uri = c.atom.ndgURI)
242        c.saveAssoc = h.url_for('save', saveLevel = self.REMOVE_ASSOCIATIONS, uri = c.atom.ndgURI)
243        c.deploymentsURL = h.url_for('view', type = VTD.DEPLOYMENT_TERM, \
244                                     uri = c.atom.ndgURI)
245        c.dataEntitiesURL = h.url_for('view', type = VTD.DE_TERM, \
246                                     uri = c.atom.ndgURI)
247       
248        # adjust atom type to cope with activity deployment exception
249        atomType = c.atom.atomTypeID
250        if atomType == g.vtd.ACTIVITY_TERM and \
251            c.atom.subtypeID == g.vtd.DEPLOYMENT_TERM:
252            atomType = g.vtd.DEPLOYMENT_TERM
253       
254        c.addEntityLink = h.url_for('list', searchData = '0', \
255                               associatedAtomID = c.atom.ndgURI, \
256                               associatedAtomType = atomType, 
257                               associationType = utils.ENTITY_ASSOCIATION)
258           
259        c.addGranuleLink = h.url_for('list', searchData = '0', \
260                               associatedAtomID = c.atom.ndgURI, \
261                               associatedAtomType = atomType, 
262                               associationType = utils.GRANULE_ASSOCIATION)
263           
264        c.addDeploymentLink = h.url_for('list', searchData = '0', \
265                               associatedAtomID = c.atom.ndgURI, \
266                               associatedAtomType = atomType, 
267                               associationType = utils.DEPLOYMENT_ASSOCIATION)
268
269        # account for special case where we're dealing with deployments
270        listVals = g.vtd.getValidSubTypes(c.atom.atomTypeID)
271        if c.atom.isDeployment():
272            listVals = [g.vtd.TERM_DATA[g.vtd.DEPLOYMENT_TERM]]
273
274        c.subTypes = utils.getVocabTermDataDropdown(listVals, \
275                                        selected=c.atom.subtype)
276        self.__setDropDownSelectVal('subtype', c.atom.subtype, listVals)
277        self.addRelatedLinksDropDowns()
278
279
280    def __setDropDownSelectVal(self, name, val, vtds):
281        '''
282        Given a list of vocab terms, with the name of a 'select' tag and the selected
283        value, set the proper value in the inputs dict to allow htmlfill to correctly
284        display the list
285        @param name: name of select element to set the select value of
286        @param val: value of the selected item - NB, this need not be the current vocab
287        term url - but should start with the main stem and end with the termID
288        @param vtds: list of vocab term definition objects
289        '''
290        if not val:
291            return
292        for vtd in vtds:
293            if val.endswith(vtd.termID) and \
294                val.startswith(vtd.vocabURL):
295                self.inputs[name] = utils.getVocabTermDataSelectValue(vtd)
296                return
297           
298
299
300    def delete(self, uri):
301        '''
302        Delete the atom associated with the specified uri - and return
303        user to the atom home page.  NB, only granule atoms can be deleted
304        at the moment.
305        '''
306        if uri:
307            try:
308                logging.info("Deleting atom, '%s'" %uri)
309                self._setup(uri)
310                eXistClient = self._getExistClient('local')
311                gran = granulite(None, granuleAtom = c.atom, \
312                                 eXistClient = eXistClient, \
313                                 deleteMode = True)
314   
315                gran.deleteGranuleAndDEReferences()
316                c.deleteResult = "Atom deleted successfully."
317                logging.info("- atom deleted")
318            except Exception, e:
319                logging.error("Problem occured whilst deleting atom: '%s'" %e.message)
320                c.deleteResult = "Warning: a problem occured whilst deleting the atom - this " + \
321                    "may have left the system in an unstable state - please check if the atom, or " + \
322                    "references to the atom still exist"
323
324        return render("genshi", "atom_editor/atom_home")
325       
326   
327    def edit(self, uri):
328        '''
329        Edit the atom with the specified ndg uri
330        '''
331        logging.info("Setting up atom edit template")
332        try:
333            self.prepareEditForm(uri)
334           
335            # NB, there appears to be a bug in htmlfill which automagically
336            # clears out content from textarea - so need to set the content
337            # explicitly for htmlfill to use
338            self.inputs['Content'] = c.atom.Content
339            self.inputs['Summary'] = c.atom.Summary
340            return self.savePageAndRender("atom_editor/atom_editor", **self.inputs)
341       
342        except ExpatError, e:
343            c.xml='XML content is not well formed'
344            c.doc=str(x)
345            logging.error("Error retrieving [%s] - XML content: %s" % (uri, e))
346        except SystemError, e:
347            return self._handleError(e)
348        except Exception, e:
349            errorMessage = traceback.format_exc()
350            c.xml='Unexpected error [%s] viewing [%s]'%(str(e), uri)
351            c.doc=''
352            logging.error(c.xml)
353       
354        response.status_code = 400
355        return render("genshi", 'atom_editor/error')
356
357
358    def addRelatedLinksDropDowns(self):
359        '''
360        Set up the drop down lists required for the selection of online ref links
361        '''
362        # at the very least, we need a simple drop down list with no preselected
363        # values
364        logging.debug("Setting up drop down lists for related links")
365        vtds = g.vtd.getValidTypes(g.vtd.ONLINE_REF_CATEGORY)
366        c.relatedLinkTerms = utils.getVocabTermDataDropdown(vtds)
367
368        # ensure we have set up the correct inputs to allow htmlfill to show
369        # the correct selected value
370        for i, link in enumerate(c.atom.relatedLinks):
371            logging.debug("Adding dropdown for related link, '%s'" %(str(link)))
372            refLabel = Atom.ONLINE_REF_LABEL + "." + str(i) + '.rel'
373           
374            # get the value of the selected list
375            self.__setDropDownSelectVal(refLabel, link.rel, vtds)
376
377        logging.debug("Finished setting up drop down lists")
378
379
380    def extractAuthorDetails(self, inputs):
381        '''
382        Retrieve author data from inputs and set appropriately on Atom, if any
383        found
384        @return: list of Person objects with the author data
385        '''
386        logging.info("Extracting author data from inputs")
387        processedAuthors = []
388        authors = []
389        for key in inputs:
390            keyBits = key.split('.')
391            if len(keyBits) == 3 and keyBits[1] not in processedAuthors:
392               
393                authorType = -1
394                if key.lower().startswith('author'):
395                    authorType = Person.AUTHOR_TYPE
396                elif key.lower().startswith('contributor'):
397                    authorType = Person.CONTRIBUTOR_TYPE
398                elif key.lower().startswith('responsible'):
399                    authorType = Person.RESPONSIBLE_PARTY_TYPE
400                else:
401                    continue
402
403                # NB, adding an empty object here is valid as it will clear out
404                # existing entries, potentially
405                author = Person(personType = authorType)
406                # check if the remove checkbox has been set
407                keyStem = ".".join(keyBits[0:2])
408                if inputs.get(keyStem + ".remove"):
409                    logging.info("Removing author data")
410                else:
411                    author.name = inputs.get(keyStem + '.name') or ""
412                    author.uri = inputs.get(keyStem + '.uri') or ""
413                    author.role = inputs.get(keyStem + '.role') or ""
414                   
415                    logging.info("Adding new author info")
416                    logging.debug("Extracted author (type:'%s', name:'%s', uri:'%s', role:'%s')" \
417                                  %(author.type, author.name, author.uri, author.role))
418                authors.append(author)
419                processedAuthors.append(keyBits[1])
420
421        logging.info("Finished extracting author data")
422        return authors
423
424
425    def extractOnlineReferenceDetails(self, inputs):
426        '''
427        Retrieve online reference data from inputs and set appropriately on Atom, if any
428        found
429        @return: list of Link objects containing the extracted data
430        '''
431        logging.info("Extracting related links data from inputs")
432        processedLinks = []
433        links = []
434
435        for key in inputs:
436            keyBits = key.split('.')
437            if len(keyBits) == 3 and keyBits[1] not in processedLinks:
438               
439                if key.lower().startswith(Atom.ONLINE_REF_LABEL):
440                    link = Link()
441                    keyStem = ".".join(keyBits[0:2])
442                   
443                    if inputs.get(keyStem + ".remove"):
444                        logging.info("Removing online reference data")
445                    else:
446                        # NB, this is in the format vocabURL--termID, so requires further
447                        # processing
448                        link.rel = self.getLatestTermURLFromDropDownInput(inputs.get(keyStem + '.rel'))
449                        link.href = inputs.get(keyStem + '.href') or ""
450                        link.title = inputs.get(keyStem + '.title') or ""
451                       
452                        if not link.hasValue():
453                            continue
454                           
455                        logging.info("Adding new online reference info")
456                        logging.debug("Extracted online reference (href:'%s', title:'%s', rel:'%s')" \
457                                      %(link.href, link.title, link.rel))
458                        links.append(link)
459
460                    processedLinks.append(keyBits[1])
461                else:
462                    continue
463
464        logging.info("Finished extracting links data")
465        return links
466
467
468    def extractParameterDetails(self, inputs):
469        '''
470        Retrieve parameters data from inputs and set appropriately on Atom, if any
471        found
472        @return: list of Category objects containing the extracted data
473        '''
474        logging.info("Extracting parameters data from inputs")
475        processedParameters = []
476        parameters = []
477
478        for key in inputs:
479            keyBits = key.split('.')
480            if len(keyBits) == 3 and keyBits[1] not in processedParameters:
481               
482                if key.lower().startswith(Atom.PARAMETER_LABEL):
483                    parameter = Category()
484                    keyStem = ".".join(keyBits[0:2])
485                   
486                    if inputs.get(keyStem + ".remove"):
487                        logging.info("Removing parameters data")
488                    else:
489                        parameter.term = inputs.get(keyStem + '.term') or ""
490                        parameter.scheme = inputs.get(keyStem + '.scheme') or ""
491                        parameter.label = inputs.get(keyStem + '.label') or ""
492                           
493                        logging.info("Adding new parameter info")
494                        logging.debug("Extracted parameter (vocabURL:'%s', label:'%s', term:'%s')" \
495                                      %(parameter.scheme, parameter.label, parameter.term))
496                        parameters.append(parameter)
497
498                    processedParameters.append(keyBits[1])
499                else:
500                    continue
501
502        logging.info("Finished extracting parameters data")
503        return parameters
504
505
506    def extractAtomAssociations(self, inputs):
507        '''
508        Retrieve atom data from inputs and create related links pointing to
509        this data
510        @return: list of Links representing the related atoms
511        '''
512        logging.info("Extracting related atom ID data from inputs")
513        atoms = []
514        processedAtoms = []
515
516        for key in inputs:
517            if key.lower().startswith(Atom.ATOM_REF_LABEL):
518                (x, href, title, rel) = key.split(Atom.DELIMITER)
519                # NB, we handle removes by effectively ignoring them later on
520                if href not in processedAtoms:
521                    processedAtoms.append(href)
522
523                    link = Link()
524                    link.href = href or ""
525                    link.title = title or ""
526                    link.rel = rel or ""
527                   
528                    # adjust href to point to the view, not the edit version
529                    link.href = link.href.replace('editAtom', 'view')
530                   
531                    logging.debug("Extracted atom info (href:'%s', title:'%s', rel:'%s')" \
532                                  %(link.href, link.title, link.rel))
533                    atoms.append(link)
534            else:
535                continue
536
537        logging.info("Finished extracting atoms data")
538        return atoms
539               
540
541    def getLatestTermURLFromDropDownInput(self, inputVal):
542        '''
543        Term ID and vocabURL are specified in the drop down menus
544        - using the input from this, return the lastest full href to the
545        term ID
546        '''
547        termData = inputVal.split('--')
548        return g.vtd.getCurrentVocabURI(termData[0]) + \
549                        "/" + termData[1]
550
551
552    def saveAtomToExist(self, atom):
553        '''
554        Save the specified atom in eXist
555        @param atom: atom object to save to eXist
556        @return atom: atom object saved in eXist
557        '''
558        logging.info("Saving changes to eXist")
559        eXist = self._getExistClient(atom.ME.providerID)
560        createdAtom = eXist.createAtomInExist(atom)
561        logging.info("Changes successfully saved to eXist")
562        return createdAtom
563
564   
565    def create(self, saveData = None, **inputs):
566        '''
567        Create a new atom
568        '''
569        self._setup()
570        if saveData:
571            logging.info("Validating input")
572            try:
573                validator = CreateAtomFormSchema()
574                validator.to_python(self.inputs)
575                logging.info("- input valid")
576               
577                logging.info("Creating basic atom")
578                atomTypeID = self.inputs.get('atomTypeID').split('--')[1]
579                self.inputs['atomTypeID'] = atomTypeID
580   
581                # activity deployments should have subtype deployment specified automatically
582                if atomTypeID == g.vtd.ACTIVITY_DEPLOYMENT_TERM:
583                    self.inputs['subtypeID'] = g.vtd.DEPLOYMENT_TERM
584                    self.inputs['atomTypeID'] = g.vtd.ACTIVITY_TERM
585                   
586                self.inputs['providerID'] = self.inputs.get('providerID').split('--')[1]
587                atom = self.saveAtomToExist(Atom(**dict(self.inputs)))
588                url = h.url_for('edit', uri = atom.ndgURI, saveData=None)
589               
590                # NB, the redirect throws an exception, so be careful not to catch it
591                h.redirect_to(url)
592            except Invalid, e:
593                c.errors = e.unpack_errors()
594               
595           
596        logging.info("Setting up atom create template")
597        c.title = CREATE_ATOM_TITLE
598       
599        # set up the drop down content - NB, add special case, 'deployment activity'
600        # - this is just a specialised activity - i.e. with subtype preset
601        c.atomTypes = utils.getVocabTermDataDropdown(g.vtd.getValidTypes(g.vtd.ATOM_CATEGORY))
602        c.providerIDs = utils.getVocabTermDataDropdown(g.vtd.getValidTypes(g.vtd.PROVIDER_CATEGORY))
603
604        try:
605            return self.savePageAndRender('atom_editor/atom_creator', **self.inputs)
606
607        except Exception, e:
608            return self._handleError(e)
609
610   
611    def createGranule(self, **inputs):
612        '''
613        Create a new atom from a granulite file
614        '''
615        logging.info("Setting up new atom from granulite template")
616        c.title='Create new data granule atom - from a granulite file'
617        c.errors = {}
618        try:
619            return self.savePageAndRender('atom_editor/atom_granulator', **inputs)
620
621        except Exception, e:
622            return self._handleError(e)
Note: See TracBrowser for help on using the repository browser.