source: mauRepo/MolesManager/trunk/cedaMoles/MolesManager/moles3epb.py @ 8514

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/mauRepo/MolesManager/trunk/cedaMoles/MolesManager/moles3epb.py@8514
Revision 8514, 20.2 KB checked in by mnagni, 8 years ago (diff)

Incomplete - # 22534: Add versiojn number to the gui page
 http://team.ceda.ac.uk/trac/ceda/ticket/22534
Correct party_indexes definition (from now on, ON DELETE = cascade)

Line 
1'''
2BSD Licence
3Copyright (c) 2012, Science & Technology Facilities Council (STFC)
4All rights reserved.
5
6Redistribution and use in source and binary forms, with or without modification,
7are permitted provided that the following conditions are met:
8
9    * Redistributions of source code must retain the above copyright notice,
10        this list of conditions and the following disclaimer.
11    * Redistributions in binary form must reproduce the above copyright notice,
12        this list of conditions and the following disclaimer in the documentation
13        and/or other materials provided with the distribution.
14    * Neither the name of the Science & Technology Facilities Council (STFC)
15        nor the names of its contributors may be used to endorse or promote
16        products derived from this software without specific prior written permission.
17
18THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
20THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
21PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
22BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
23OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
24SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
25HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
29Created on 10 Jan 2012
30
31@author: mnagni
32'''
33from cedaMoles.libs.epb import EPB
34from ea_model.moles3_4.observationcollection.mo_observationcollection \
35    import MO_ObservationCollection
36from ea_model.moles3_4.observation.mo_observation import MO_Observation
37from sqlalchemy import Table, Column, ForeignKey, Integer, String, event
38from sqlalchemy.orm import mapper
39from cedaMoles.MolesManager.ceda_guid import CedaGUID
40from sqlalchemy.orm.collections import InstrumentedList
41from ea_model.iso_19115_2006_metadata_corrigendum.\
42    reference_system_information.md_identifier import MD_Identifier
43from ea_model.iso_19115_2006_metadata_corrigendum.\
44    citation_and_responsible_party_information.ci_citation import CI_Citation
45from ea_model.iso_19115_2006_metadata_corrigendum.\
46    extent_information.ex_geographicboundingbox import EX_GeographicBoundingBox
47from cedaMoles.libs.postgisutil import create_st_setSRID, unifyGeometriesAsBBox
48from ea_model.ceda_metadatamodel.ceda_observationcollection.\
49    ceda_observationcollection import CEDA_ObservationCollection
50from ea_model.ceda_metadatamodel.ceda_observation.\
51    ceda_observation import CEDA_Observation
52from ea_model.ceda_metadatamodel.ceda_project.ceda_project import CEDA_Project
53from cedaMoles.MolesManager.codelist import MM_RoleValue, getCLValue
54from cedaMoles.libs.migration.processor.commons import from_pt_to_string
55from datetime import datetime
56from ascore.utils import synchAttributes
57from cedaMoles.MolesManager.db.partyIndexes import associateMOParty_indexes
58from cedaMoles.MolesManager.db.after_flush import afterFlush
59
60class Moles3EPBFactory(EPB):   
61    def __init__(self, db_manager):
62        super(Moles3EPBFactory, self).__init__(db_manager)
63        self._init_ceda_customization()
64
65    def _init_ceda_customization(self):
66        self._associate_ceda_guid()
67        associateMOParty_indexes(self._db_manager.metadata)
68        self._initSearchIndexes() 
69       
70    def _associate_ceda_guid(self):
71        guid_table = Table('ceda_guid', self._db_manager.metadata, \
72                           Column('id', String, primary_key=True), \
73                           Column('ceda_observationcollection', Integer, ForeignKey('ceda_observationcollection.id')), 
74                           Column('ceda_observation', Integer, ForeignKey('ceda_observation.id')),
75                           Column('ceda_project', Integer, ForeignKey('ceda_project.id')))
76        mapper(CedaGUID, guid_table)
77        self._db_manager.metadata.create_all()
78
79    def _initSearchIndexes(self):
80        #To Be Done - CHECK IF THE COLUMN ALREADY EXISTS!
81        # We don't want sqlalchemy to know about this column so we add it externally.
82        try:
83            self._db_manager.engine.execute("alter table md_identifier add column code_search_vector tsvector")                 
84
85            # This indexes the tsvector column
86
87            self._db_manager.engine.execute("create index md_identifier_code_search_index on md_identifier using gin(code_search_vector)")
88
89            # This sets up the trigger that keeps the tsvector column up to date.
90            self._db_manager.engine.execute("create trigger md_identifier_code_search_update before update or insert on md_identifier \
91                for each row execute procedure tsvector_update_trigger('code_search_vector', 'pg_catalog.english', code)")                       
92        except Exception:
93            pass
94       
95    def createEPB(self):
96        session = self._get_session()
97        event.listen(session, 'after_flush', afterFlush)
98        return Moles3EPB(session)
99
100class Moles3EPB(object):
101
102    def __init__(self, session):
103        self._session = session
104       
105    def close(self):
106        return self._session.close()       
107       
108    def searchEager(self, clazz, inst_id):
109        return EPB.searchEager(clazz, inst_id, self._session)     
110     
111    def _controlledCommit(self):
112        try:
113            self._session.commit()
114        except Exception as e:
115            print e
116
117    def rollback(self):
118        """
119        Rolls back the session and one a new transaction
120        """
121        EPB.rollback(self._session) 
122     
123    def persistInstance(self, instance):
124        """
125        Adds a new migration object.
126        **Parameters**
127            * instance: the object to add
128           
129        **Returns**
130            An updated, session independent, object instance reflecting the new persisted object
131        """
132        EPB.persistInstance(instance, self._session)       
133
134    def mergeInstance(self, instance):
135        """
136        Copy the state an instance onto the persistent instance with the same identifier.
137        **Parameters**
138            * instance: the object to add
139           
140        **Returns**
141            An updated object instance reflecting the new persisted object
142        """
143        return EPB.mergeInstance(instance, self._session) 
144
145    def refresh(self, instance):
146        """
147        Expires and refresh the attributes on the given instance.
148        **Parameters**
149            * instance: the object to refresh
150        """
151        EPB.refresh(instance, self._session) 
152
153    def deleteInstance(self, instance):
154        """
155        Deletes a new migration object.
156        **Parameters**
157            * instance: the object to delete
158        """
159        EPB.deleteInstance(instance, self._session) 
160
161    def expunge(self, instance):
162        """
163        Expunges an object from the session
164        **Parameters**
165            * instance: the object to expunge
166        """
167        EPB.expunge(instance, self._session) 
168
169    def _mergeOrAddInSession(self, ceda_object):
170        try:
171            return self._session.merge(ceda_object)   
172        except:
173            self._session.add(ceda_object)
174            return ceda_object 
175     
176    def updateCedaObject(self, ceda_object, cols_to_update):
177        """
178            Update, eventually add to the session, and commit a CEDA Object in MOLES3 db.
179            @param ceda_object: the CEDA object to update
180            @param cols_to_update: a dictionary containing the columns to update for the given ceda_object and the desired value.
181            If the attribute is a list of objects the new instances are appended only if do not exist in the actual list
182            @return: the given instance with the updated attributes.
183        """
184        coll = self._mergeOrAddInSession(ceda_object)
185        if coll != None:       
186            for k,v in cols_to_update.items():
187                if v is None:
188                    continue
189                if hasattr(coll, k):                   
190                    coll_k = getattr(coll, k)                       
191                    if type(coll_k) == list or type(coll_k) == InstrumentedList:
192                        tmp_coll = []
193                        if type(v) == list or type(v) == InstrumentedList:
194                            tmp_coll.extend(v)
195                        else:
196                            tmp_coll.append(v)
197                        for item in tmp_coll:
198                            el = self._mergeOrAddInSession(item)
199                            if el not in coll_k:
200                                coll_k.append(el)
201                    else:
202                        el = self._mergeOrAddInSession(v)
203                        setattr(coll, k, el)
204        synchAttributes(coll)                                             
205        self._controlledCommit()
206        #return coll                                     
207
208    def getUnifyObservationCollectionGEAsBBox(self, collection):
209        """
210            Returns the union of the collections.member'a  GeographicExtension(s)
211            @param collection: an CEDA_ObservationColleciton instance 
212        """
213        bboxes = []
214        if not hasattr(collection, 'member'):
215            return bboxes
216        for member in collection.member:
217            for ge in member.geographicExtent:
218                bboxes.append(getGeograpicExtentGeometry(ge))
219       
220        return unifyGeometriesAsBBox(bboxes, self) 
221       
222        #return unifyGeometriesAsBBox(bboxes, self)
223
224
225    def retrieveGUIDFromInstance(self, instance):
226        """
227            Returns the CedaGUID object associated with the given instance.
228            @param instance: an instance of CEDA_Observation os CEDA_ObservationCollection 
229        """
230        if instance is None or not hasattr(instance, 'id'):
231            return None
232        if type(instance) == CEDA_ObservationCollection:
233            return self._session.query(CedaGUID).filter(CedaGUID.ceda_observationcollection==instance.id).first()
234        elif type(instance) == CEDA_Observation:
235            return self._session.query(CedaGUID).filter(CedaGUID.ceda_observation==instance.id).first()
236        elif type(instance) == CEDA_Project:
237            return self._session.query(CedaGUID).filter(CedaGUID.ceda_project==instance.id).first()               
238   
239    def observationCollectionHasObservation(self, obs_coll_id, obs_id):
240        """
241            Checks if a CEDA_Collection contains a given CEDA_Observation.
242            @param obs_coll_id: the CEDA_ObservationColleciton id
243            @param obs_id: the CEDA_Observation id
244            @return: True if the collection contains the given observation, False otherwise 
245        """
246        coll = self._session.query(CEDA_ObservationCollection).filter(CEDA_ObservationCollection.id==obs_coll_id).first()
247        obs = self._session.query(CEDA_Observation).filter(CEDA_Observation.id==obs_id).first()
248        return obs in coll.member
249
250    def observationAuthor(self, observation):
251        """
252            Lists the CEDA_Observation author.
253            @param observation: the CEDA_Observation inside which look for the author           
254            @return: True if the collection contains the given observation, False otherwise 
255        """
256       
257        # TO FIX!!!
258        for partyInfo in observation.relatedParty:
259            if partyInfo.role == getCLValue(MM_RoleValue.cl_author):
260                return partyInfo.party       
261
262       
263
264    def extractObservationByTitleKeywords(self, keywords):
265        """
266            Loooks for CEDA_Observation containing a specific title (observation.identifier.code)
267            @param keywords: a space separated terms string
268            @returns: dictionary where the keys are the GUID of the CEDA_Observation in the value 
269        """               
270        # search_vector is a ts_vector column. To search for terms, you use the
271        # @@ operator. plainto_tsquery turns a string into a query that can be
272        # used with @@. So this adds a where clause like "WHERE search_vector
273        # @@ plaint_tsquery(<search string>)"
274        q = self._session.query(CEDA_Observation). \
275            join(MO_Observation).join(MO_Observation.identifier). \
276            filter('md_identifier.code_search_vector @@ to_tsquery(:terms)')
277        # This binds the :terms placeholder to the searchterms string. User input
278        # should always be put into queries this way to prevent SQL injection.
279        q = q.params(terms=keywords)
280       
281        ret = {}
282        for item in q.all():
283            guid = self.retrieveGUIDFromInstance(item)
284            if guid is not None:
285                ret[guid.id] = item           
286        return ret
287        return 
288
289
290    def extractCollectionIdentifierByTitle(self, i_title):
291        """
292            Searches for an MD_Identifier from a CEDA_ObservationCollection contains a specific title (observation.identifier.code)
293            @param i_title: the CEDA_ObservationCollection.identifier.title value to search for
294            @return: a tuple containing a CEDA_ObservationCollection and the CEDA_ObservationCollection.idenfitier element having the title 
295        """
296        return self._session.query(CEDA_ObservationCollection, MD_Identifier). \
297            join(MO_ObservationCollection).join(MO_ObservationCollection.identifier). \
298            join(MD_Identifier.authority).filter(CI_Citation.title.like('%' + i_title + '%'))
299
300    def extractObservationsForProject(self, project):
301        """
302            Searches for the CEDA_Observation associated with a CEDA_Project
303            @param project: a CEDA_Project instance
304            @return: a tuple containing the associated CEDA_Observation 
305        """
306        return self._session.query(CEDA_Observation). \
307            join(CEDA_Observation, MO_Observation.inSupportOf).filter(CEDA_Project.id == project.id)
308
309    def extractProjectObservationCollections(self, project):
310        """
311            Searches for the Observation_Collections associated with a CEDA_Project
312            @param project: a CEDA_Project instance
313            @return: a tuple containing the associated CEDA_ObservationCollection 
314        """
315        mo_obs = self._session.query(MO_Observation).join(CEDA_Project).\
316            filter(CEDA_Project.id == project.id).subquery()     
317        obsers = self._session.query(CEDA_Observation).\
318            join(mo_obs, CEDA_Observation.id == mo_obs.c.id).one()
319       
320        '''
321        cos = self._session.query(CEDA_ObservationCollection).all()
322        co = self._session.query(MO_ObservationCollection).\
323            join(MO_ObservationCollection.member).\
324            filter(MO_ObservationCollection.member.contains(obsers))
325        '''
326        observations = self._session.query(MO_ObservationCollection).\
327            join(CEDA_Observation).\
328            filter(obsers.any(CEDA_Observation.id==obsers.c.id))
329        print "observation:" + str(observations.count())
330        return observations
331
332    def search(self, clazz, inst_id = None):
333        ret = EPB.search(clazz, inst_id, self._session)
334        return ret
335     
336    def search_object_and_gui(self, clazz, inst_id = None):       
337        result = EPB.search(clazz, inst_id, self._session)
338        ret = {}
339        for item in result.all():
340            guid = self.retrieveGUIDFromInstance(item)
341            if guid is not None:
342                ret[guid.id] = item           
343        return ret     
344     
345    def searchSelectiveLoad(self, clazz, inst_id, attributes): 
346        """
347            Searches a required instance by id loading selectively \
348            the specified fields. The parameter "attributes" is a single string or a list of attributes
349            owned by the instance of "clazz". Furthermore such list may contain
350            also the children of the main attributes. For example "attrs" may look
351            like
352            ['resultAccumulation', 'identifier.authority', 'resultTime.position.dateTime8601.month', \
353                      'relatedParty.party', 'result.source.function', 'permission', \
354                      'geographicExtent', 'phenomenonTime', 'keywords', 'description', \
355                      'inSupportOf.abstract', 'dataLineage']
356            the first parameter refers to the main class so is equivalent to
357            clazz.resultAccumulation
358            the second parameter is equivalent to invoke
359            clazz.identifier.authority
360            As single string "attributes" could be as well just 'identifier.authority'
361            @param clazz: the class type to search for
362            @param inst_id: the instance id for which the search is done
363            @param attributes: a single string or a list of attributes to load
364            @param session: a session to use for the query. By default a new one is created automatically at start and closed at the end
365            @return the required instance             
366        """               
367        ret = EPB.searchSelectiveLoad(clazz, inst_id, attributes, self._session)
368        return ret   
369   
370    def loadAttributes(self, instance, attributes):
371        """
372            Returns the attribute of an instance. The parameter "attributes" is a single string or a list of attributes
373            owned by the instance of "clazz". Furthermore such list may contain
374            also the children of the main attributes. For example "attrs" may look
375            like
376            ['resultAccumulation', 'identifier.authority', 'resultTime.position.dateTime8601.month', \
377                      'relatedParty.party', 'result.source.function', 'permission', \
378                      'geographicExtent', 'phenomenonTime', 'keywords', 'description', \
379                      'inSupportOf.abstract', 'dataLineage']
380            the first parameter refers to the main class so is equivalent to
381            clazz.resultAccumulation
382            the second parameter is equivalent to invoke
383            clazz.identifier.authority
384            As single string "attributes" could be as well just 'identifier.authority'
385            @param instance: an instance containing the appropriate id
386            @param attributes: the attribute value required
387            @param session: the session to use for the operation
388            @return: the given instance filled with the required attributes.                     
389        """
390        instance = self._session.merge(instance)
391        EPB.loadAttributes(instance, attributes, self._session)                 
392        return instance
393
394    def executeNative(self, sqlNative):
395        return EPB.executeNative(sqlNative, self._session) 
396
397
398def getGeograpicExtentGeometry(ge):
399    '''
400        Creates the appropriate postgis geometry from a EX_GeographicExtent
401        @param ge: an EX_GeographicExtent instance
402        @return: a postgix text geometry
403    '''
404    if isinstance(ge, EX_GeographicBoundingBox):
405        return create_st_setSRID(ge.westBoundLongitude, ge.southBoundLatitude, \
406                       ge.eastBoundLongitude, ge.northBoundLatitude)
407    return None
408
409def _tmpstrftime(dt):
410    """
411        Returns a string from a datastring. This function is necessary because
412        python <3.2 strftime method is not able to handle date < 1900
413        @param dt: a datetime object
414    """
415    return "%s-%s-%s" % (dt.year, dt.month, dt.day)
416
417def unify_observation_collection_phenomenon_time(collection):
418    """
419        Returns the time period of the collections.member'a  phenomenonTime(s)
420        @param collection: an CEDA_ObservationColleciton instance
421        @return: a tuple (startDate, endDate) strings
422    """
423    dateFormat = '%Y-%m-%d'
424    ptStart = []
425    ptEnd = []     
426    for member in collection.member:
427        if member.phenomenonTime is None:
428            continue
429           
430        pt =  member.phenomenonTime
431        ptString = from_pt_to_string(pt)
432        if ptString[0] is not None:                   
433            ptStart.append(datetime.strptime(ptString[0], dateFormat))
434        if ptString[1] is not None:                   
435            ptEnd.append(datetime.strptime(ptString[1], dateFormat))
436    ptStart.sort()                   
437    ptEnd.sort()
438    start = None
439    end = None
440    #takes the earlier date
441    if len(ptStart) > 0:
442        start = _tmpstrftime(ptStart[0]) 
443    #takes the latest date
444    if len(ptEnd) > 0:
445        end = _tmpstrftime(ptEnd[len(ptEnd) - 1]) 
446    return start, end
Note: See TracBrowser for help on using the repository browser.