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

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/mauRepo/MolesManager/trunk/cedaMoles/MolesManager/moles3epb.py@8502
Revision 8502, 20.1 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
SearchByKeyword?, returns a dictionary of objects where the key is the GUID of the object in the value

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