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

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/mauRepo/MolesManager/trunk/cedaMoles/MolesManager/moles3epb.py@8513
Revision 8513, 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

Fixes blocking error on classes initialisation and migration

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        if not hasattr(collection, 'member'):
209            return bboxes
210        for member in collection.member:
211            for ge in member.geographicExtent:
212                bboxes.append(getGeograpicExtentGeometry(ge))
213       
214        return unifyGeometriesAsBBox(bboxes, self) 
215       
216        #return unifyGeometriesAsBBox(bboxes, self)
217
218
219    def retrieveGUIDFromInstance(self, instance):
220        """
221            Returns the CedaGUID object associated with the given instance.
222            @param instance: an instance of CEDA_Observation os CEDA_ObservationCollection 
223        """
224        if instance is None or not hasattr(instance, 'id'):
225            return None
226        if type(instance) == CEDA_ObservationCollection:
227            return self._session.query(CedaGUID).filter(CedaGUID.ceda_observationcollection==instance.id).first()
228        elif type(instance) == CEDA_Observation:
229            return self._session.query(CedaGUID).filter(CedaGUID.ceda_observation==instance.id).first()
230        elif type(instance) == CEDA_Project:
231            return self._session.query(CedaGUID).filter(CedaGUID.ceda_project==instance.id).first()               
232   
233    def observationCollectionHasObservation(self, obs_coll_id, obs_id):
234        """
235            Checks if a CEDA_Collection contains a given CEDA_Observation.
236            @param obs_coll_id: the CEDA_ObservationColleciton id
237            @param obs_id: the CEDA_Observation id
238            @return: True if the collection contains the given observation, False otherwise 
239        """
240        coll = self._session.query(CEDA_ObservationCollection).filter(CEDA_ObservationCollection.id==obs_coll_id).first()
241        obs = self._session.query(CEDA_Observation).filter(CEDA_Observation.id==obs_id).first()
242        return obs in coll.member
243
244    def observationAuthor(self, observation):
245        """
246            Lists the CEDA_Observation author.
247            @param observation: the CEDA_Observation inside which look for the author           
248            @return: True if the collection contains the given observation, False otherwise 
249        """
250       
251        # TO FIX!!!
252        for partyInfo in observation.relatedParty:
253            if partyInfo.role == getCLValue(MM_RoleValue.cl_author):
254                return partyInfo.party       
255
256       
257
258    def extractObservationByTitleKeywords(self, keywords):
259        """
260            Loooks for CEDA_Observation containing a specific title (observation.identifier.code)
261            @param keywords: a space separated terms string
262            @returns: dictionary where the keys are the GUID of the CEDA_Observation in the value 
263        """               
264        # search_vector is a ts_vector column. To search for terms, you use the
265        # @@ operator. plainto_tsquery turns a string into a query that can be
266        # used with @@. So this adds a where clause like "WHERE search_vector
267        # @@ plaint_tsquery(<search string>)"
268        q = self._session.query(CEDA_Observation). \
269            join(MO_Observation).join(MO_Observation.identifier). \
270            filter('md_identifier.code_search_vector @@ to_tsquery(:terms)')
271        # This binds the :terms placeholder to the searchterms string. User input
272        # should always be put into queries this way to prevent SQL injection.
273        q = q.params(terms=keywords)
274       
275        ret = {}
276        for item in q.all():
277            guid = self.retrieveGUIDFromInstance(item)
278            if guid is not None:
279                ret[guid.id] = item           
280        return ret
281        return 
282
283
284    def extractCollectionIdentifierByTitle(self, i_title):
285        """
286            Searches for an MD_Identifier from a CEDA_ObservationCollection contains a specific title (observation.identifier.code)
287            @param i_title: the CEDA_ObservationCollection.identifier.title value to search for
288            @return: a tuple containing a CEDA_ObservationCollection and the CEDA_ObservationCollection.idenfitier element having the title 
289        """
290        return self._session.query(CEDA_ObservationCollection, MD_Identifier). \
291            join(MO_ObservationCollection).join(MO_ObservationCollection.identifier). \
292            join(MD_Identifier.authority).filter(CI_Citation.title.like('%' + i_title + '%'))
293
294    def extractObservationsForProject(self, project):
295        """
296            Searches for the CEDA_Observation associated with a CEDA_Project
297            @param project: a CEDA_Project instance
298            @return: a tuple containing the associated CEDA_Observation 
299        """
300        return self._session.query(CEDA_Observation). \
301            join(CEDA_Observation, MO_Observation.inSupportOf).filter(CEDA_Project.id == project.id)
302
303    def extractProjectObservationCollections(self, project):
304        """
305            Searches for the Observation_Collections associated with a CEDA_Project
306            @param project: a CEDA_Project instance
307            @return: a tuple containing the associated CEDA_ObservationCollection 
308        """
309        mo_obs = self._session.query(MO_Observation).join(CEDA_Project).\
310            filter(CEDA_Project.id == project.id).subquery()     
311        obsers = self._session.query(CEDA_Observation).\
312            join(mo_obs, CEDA_Observation.id == mo_obs.c.id).one()
313       
314        '''
315        cos = self._session.query(CEDA_ObservationCollection).all()
316        co = self._session.query(MO_ObservationCollection).\
317            join(MO_ObservationCollection.member).\
318            filter(MO_ObservationCollection.member.contains(obsers))
319        '''
320        observations = self._session.query(MO_ObservationCollection).\
321            join(CEDA_Observation).\
322            filter(obsers.any(CEDA_Observation.id==obsers.c.id))
323        print "observation:" + str(observations.count())
324        return observations
325
326    def search(self, clazz, inst_id = None):
327        ret = EPB.search(clazz, inst_id, self._session)
328        return ret
329     
330    def search_object_and_gui(self, clazz, inst_id = None):       
331        result = EPB.search(clazz, inst_id, self._session)
332        ret = {}
333        for item in result.all():
334            guid = self.retrieveGUIDFromInstance(item)
335            if guid is not None:
336                ret[guid.id] = item           
337        return ret     
338     
339    def searchSelectiveLoad(self, clazz, inst_id, attributes): 
340        """
341            Searches a required instance by id loading selectively \
342            the specified fields. The parameter "attributes" is a single string or a list of attributes
343            owned by the instance of "clazz". Furthermore such list may contain
344            also the children of the main attributes. For example "attrs" may look
345            like
346            ['resultAccumulation', 'identifier.authority', 'resultTime.position.dateTime8601.month', \
347                      'relatedParty.party', 'result.source.function', 'permission', \
348                      'geographicExtent', 'phenomenonTime', 'keywords', 'description', \
349                      'inSupportOf.abstract', 'dataLineage']
350            the first parameter refers to the main class so is equivalent to
351            clazz.resultAccumulation
352            the second parameter is equivalent to invoke
353            clazz.identifier.authority
354            As single string "attributes" could be as well just 'identifier.authority'
355            @param clazz: the class type to search for
356            @param inst_id: the instance id for which the search is done
357            @param attributes: a single string or a list of attributes to load
358            @param session: a session to use for the query. By default a new one is created automatically at start and closed at the end
359            @return the required instance             
360        """               
361        ret = EPB.searchSelectiveLoad(clazz, inst_id, attributes, self._session)
362        return ret   
363   
364    def loadAttributes(self, instance, attributes):
365        """
366            Returns the attribute of an instance. The parameter "attributes" is a single string or a list of attributes
367            owned by the instance of "clazz". Furthermore such list may contain
368            also the children of the main attributes. For example "attrs" may look
369            like
370            ['resultAccumulation', 'identifier.authority', 'resultTime.position.dateTime8601.month', \
371                      'relatedParty.party', 'result.source.function', 'permission', \
372                      'geographicExtent', 'phenomenonTime', 'keywords', 'description', \
373                      'inSupportOf.abstract', 'dataLineage']
374            the first parameter refers to the main class so is equivalent to
375            clazz.resultAccumulation
376            the second parameter is equivalent to invoke
377            clazz.identifier.authority
378            As single string "attributes" could be as well just 'identifier.authority'
379            @param instance: an instance containing the appropriate id
380            @param attributes: the attribute value required
381            @param session: the session to use for the operation
382            @return: the given instance filled with the required attributes.                     
383        """
384        instance = self._session.merge(instance)
385        EPB.loadAttributes(instance, attributes, self._session)                 
386        return instance
387
388    def executeNative(self, sqlNative):
389        return EPB.executeNative(sqlNative, self._session) 
390
391
392def getGeograpicExtentGeometry(ge):
393    '''
394        Creates the appropriate postgis geometry from a EX_GeographicExtent
395        @param ge: an EX_GeographicExtent instance
396        @return: a postgix text geometry
397    '''
398    if isinstance(ge, EX_GeographicBoundingBox):
399        return create_st_setSRID(ge.westBoundLongitude, ge.southBoundLatitude, \
400                       ge.eastBoundLongitude, ge.northBoundLatitude)
401    return None
402
403def _tmpstrftime(dt):
404    """
405        Returns a string from a datastring. This function is necessary because
406        python <3.2 strftime method is not able to handle date < 1900
407        @param dt: a datetime object
408    """
409    return "%s-%s-%s" % (dt.year, dt.month, dt.day)
410
411def unify_observation_collection_phenomenon_time(collection):
412    """
413        Returns the time period of the collections.member'a  phenomenonTime(s)
414        @param collection: an CEDA_ObservationColleciton instance
415        @return: a tuple (startDate, endDate) strings
416    """
417    dateFormat = '%Y-%m-%d'
418    ptStart = []
419    ptEnd = []     
420    for member in collection.member:
421        if member.phenomenonTime is None:
422            continue
423           
424        pt =  member.phenomenonTime
425        ptString = from_pt_to_string(pt)
426        if ptString[0] is not None:                   
427            ptStart.append(datetime.strptime(ptString[0], dateFormat))
428        if ptString[1] is not None:                   
429            ptEnd.append(datetime.strptime(ptString[1], dateFormat))
430    ptStart.sort()                   
431    ptEnd.sort()
432    start = None
433    end = None
434    #takes the earlier date
435    if len(ptStart) > 0:
436        start = _tmpstrftime(ptStart[0]) 
437    #takes the latest date
438    if len(ptEnd) > 0:
439        end = _tmpstrftime(ptEnd[len(ptEnd) - 1]) 
440    return start, end
Note: See TracBrowser for help on using the repository browser.