source: TI05-delivery/ows_framework/trunk/ows_server/ows_server/controllers/discovery.py @ 4259

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI05-delivery/ows_framework/trunk/ows_server/ows_server/controllers/discovery.py@4259
Revision 4259, 17.5 KB checked in by cbyrom, 11 years ago (diff)

Add ability to set the discovery service URL from the ndgDiscovery.config
file + remove unused 'layoutdir' config property.

Line 
1import socket # Handle Discovery service down socket.error
2import logging
3
4from ows_server.lib.base import *
5from paste.request import parse_querystring
6from ows_server.lib.Date import *
7from ows_server.templates import DiscoveryTemplate
8from ndgUtils import ndgSearch
9from ndgUtils import DocumentRetrieve
10from ndgUtils.DocumentRetrieve import ndgVocabPOX as VS
11from ndgUtils import ndgObject
12from ows_server.models.DIF import DIF
13from ows_server.models.DiscoveryState import DiscoveryState,constraints
14from ows_server.lib.mailer import mailHandler
15from ndgUtils.xmlHandler2 import xmlHandler
16
17logging.basicConfig(level=logging.DEBUG,
18   format='%(asctime)s %(filename)s:%(lineno)d %(levelname)s %(message)s')
19
20debug=0
21
22class DiscoveryController(BaseController):
23    ''' Provides the pylons controller for NDG discovery '''
24   
25    def __setup(self):
26        ''' Common setup for controller methods '''
27        self.cf=request.environ['ndgConfig']
28        self.exist=(self.cf.get('NDG_EXIST','local'),\
29                    self.cf.get('NDG_EXIST','passwordFile'))
30        self.inputs=dict(parse_querystring(request.environ))
31        self.message=''
32               
33   
34    def index(self):
35       
36        self.__setup()
37        # parse the query string and hand off to a discovery engine
38       
39        if self.inputs=={} or 'ClearForm' in self.inputs: 
40            return self.__advancedPrompt()
41       
42        # see if this is a discovery search or a more complicated search
43        if 'searchTarget' not in self.inputs: self.inputs['searchTarget']='Discovery'
44       
45        #the following need to be defined
46        continuations={'start':1,'howmany':10}
47        for i in continuations:
48            if i not in self.inputs: self.inputs[i]=continuations[i]
49           
50           
51        # the simplest query we might get is a text search, in which case
52        # the inputs should be start, howmany and searchString (although
53        # maybe not in that order. The next simplest is one with
54        # a specified textTarget, after that we need all the inputs.
55       
56        if 'searchString' in self.inputs and 'textTarget' not in self.inputs:
57            # it's a simple text search
58            self.inputs['textTarget']='All'
59           
60        # the next simplest is one that includes texttarget as well ...
61        expected=['searchString','textTarget','start','howmany','searchTarget']
62        self.__checkform(expected)
63   
64        if self.message!='':
65            c.xml='Simple %s:'%self.message
66            return render('content')
67       
68       
69        if 'geoSearchType' not in self.inputs:self.inputs['geoSearchType']='overlaps'
70        if len(self.inputs)==6:
71           
72            # now we add the defaults ...
73            # this is kind of historical ...
74            bbox=None
75            dateRange=None
76            scope=None
77           
78        else:
79       
80            # ------------- Handle scope from radio button on form -------
81            if 'source' in self.inputs:
82                # the WSDL expects a list, we're just providing one ... via a radio ...
83                scope=[self.inputs['source']]
84                if scope==['All']: scope=None
85            else:
86                scope=None
87               
88            expected=['bboxN','bboxE','bboxS','bboxW','geoSearchType']
89            self.__checkform(expected)
90            if self.message!='': 
91                self.message=''
92                bbox=None
93            else:
94                # default form has a global bounding box, NB, internal to this routine we use bbox=[N,W,E,S], not [W,S,E,N]!
95                bbox=[self.inputs['bboxN'],self.inputs['bboxW'],self.inputs['bboxE'],self.inputs['bboxS']]
96               
97                self.__checkbox(bbox)
98                if self.message!='': 
99                    c.xml=self.message
100                    return render('content')
101                   
102           
103            expected=['startDateDay','startDateMon','startDateYear',
104                            'endDateDay','endDateMon','endDateYear']
105            self.__checkform(expected)
106            if self.message!='': 
107                self.message=''
108                dateRange=None
109            else:
110                try:
111                    dateRange=[(self.inputs['startDateDay'],self.inputs['startDateMon'],self.inputs['startDateYear']),
112                                (self.inputs['endDateDay'],self.inputs['endDateMon'],self.inputs['endDateYear'])]
113                    #default form has blanks, in which case we don't want to check for date range
114                    if dateRange<>[("","",""),("","","")]:
115                        self.__checkdates(dateRange)
116                    else: dateRange=None           
117                except:
118                    self.message='Invalid date provided'
119                if self.message!='': 
120                    c.xml=self.message
121                    return render('content')
122       
123        if 'constrained' in self.inputs: 
124            con=self.__buildconstraints(dateRange,bbox,scope,self.inputs['searchString'],self.inputs['geoSearchType'])
125            return self.__advancedPrompt(searchConstraints=con)
126        else:
127            # ------------- ok, now go do the search -----------
128            response=self.doText(self.inputs['searchString'],self.inputs['textTarget'],
129                self.inputs['start'],self.inputs['howmany'],scope=scope,dateRange=dateRange,bbox=bbox,
130                geoSearch=self.inputs['geoSearchType'])
131            return response
132
133    def doText(self,searchString,textTarget,start,howmany,scope=None,dateRange=None,bbox=None,geoSearch='overlaps'):
134       
135        ''' Carry out a text search for <searchString>
136        in the <textTarget> where the accepted text target values are controlled
137        by the DiscoveryTemplate GUI, and are: All, Authors, Parameters '''
138        logging.info("'doText' invoke with string, '%s'" %searchString)
139        start,howmany=int(start),int(howmany)  # url arguments need conversion ...
140       
141        if self.inputs['searchTarget']=='Discovery':
142            logging.info(" - use Discovery service to complete search")
143            url = None
144            if hasattr(g, 'discoveryServiceURL'):
145                url = g.discoveryServiceURL
146            ws=ndgSearch(HostAndPort=url)
147        elif self.inputs['searchTarget'] in ['Browse','NumSim']:
148            logging.info(" - use Browse service to complete search")
149            ws=DocumentRetrieve(self.exist[0],pwfile=self.exist[1])
150            #overriding text target which is ignored currently ... yuck ...
151            textTarget=self.inputs['searchTarget']
152            if textTarget=='Browse':textTarget='ndg_B_metadata'
153        else:
154            logging.error("Unrecognised search type, '%s'" \
155                          %self.inputs['searchTarget'])
156            c.xml='Unknown searchTarget %s'%self.inputs['searchTarget']
157            return render('error')
158           
159        # PJK 04/09/08 Handle errors more gracefully
160        #
161        # http://proj.badc.rl.ac.uk/ndg/ticket/984
162        try:
163            documents=ws.search(searchString,
164                                start=start,
165                                howmany=howmany,
166                                target=textTarget,
167                                scope=scope,
168                                dateRange=dateRange,
169                                bbox=bbox,
170                                geoSearchType=geoSearch)
171        except socket.error, e:
172            logging.error("Socket error for discovery service search: %s" % e)
173            c.xml='The Discovery Service is unavailable.  Please check with '+\
174                    'your system administrator'
175            return render('error')
176        except Exception, e:
177            logging.error("Calling discovery service search: %s" % e)
178            c.xml='An internal error occured.  Please check with ' + \
179                    'your system administrator'
180            return render('error')
181           
182        logging.info("'doText()' returned - now processing results")
183        if ws.error !=None:
184            logging.error("Error encountered whilst running search: %s" %ws.error)
185            m=''
186            for i in ws.error:m+='<p>%s</p>'%i
187            c.xml=m
188            return render('content')
189       
190        #build constraints info for report
191        searchConstraints=self.__buildconstraints(dateRange,bbox,scope,\
192                                                  searchString,geoSearch)
193        hits=ws.hits
194        if hits==0:
195            outMessage = 'No records found [contraints: %s]' %searchConstraints
196            logging.info(outMessage) 
197            c.xml='<p>' + outMessage + '</p>'
198            return render('content')
199       
200        id=ws.serverSessionID
201       
202        if hits < howmany:
203            howmany = hits
204       
205        # DiscoveryState object is a wrapper to the various search config
206        # variables
207        c.state=DiscoveryState(id,searchString,request.environ,\
208                               hits,searchConstraints,start,howmany)
209        c.querystring=request.environ['QUERY_STRING']
210     
211        try:
212            if self.inputs['searchTarget']=='Discovery':
213                results=ws.getLabelledDocs(format='DIF')
214            else:
215                return self.moreSearch(ws)
216
217            if results==[]:
218                c.xml='<p> No results for "%s"!</p>'%searchString
219                return render('content')
220
221            difs=[]
222            errors=[]
223            for result in results: 
224                obj=ndgObject(result[0], config = self.cf.config)
225                try:
226                    difs.append(DIF(result[1],ndgObj=obj))
227                except ValueError,e:
228                    errors.append((result[0],str(e)))
229
230            if difs==[]:
231                c.xml='<p>No usable results for "%s"!</p>'%searchString
232                return render('content')
233            elif errors:
234                c.xml='<p>Search results for "%s"'%searchString
235                dp=[]
236                for e in errors:
237                    n=ndgObject(e[0])
238                    if n.repository not in dp: dp.append(n.repository)
239                if len(dp)<>1: 
240                    dp='[Various Data Providers]'
241                else:
242                    dp='[%s]'%dp[0] 
243                c.xml+=' (unfortunately %s hits matched unformattable documents from %s, an internal error has been logged):</p>'%(len(errors),dp)
244                status,message=mailHandler(['b.n.lawrence@rl.ac.uk'],'DIF errors',str(errors),
245                                server=self.cf.get('DEFAULT','mailserver'))
246                if not status:
247                    c.xml+='<p> Actually, not even an internal error has been logged. <br/>'
248                    c.xml+='Internal sending of mail failed with error [%s]</p>'%message
249                return render('content')
250            else:
251                c.difs=difs
252                session['results']=h.current_url()
253                session.save()
254               
255                # set up the displayed tabs
256                if len(c.pageTabs)==1: 
257                    c.pageTabs.append(('Results',session['results']))
258                    c.pageTabs.append(('Selections',
259                                       h.url_for(controller='selectedItems',
260                                                 action='index')))
261                elif c.pageTabs[1][0]!='Results':
262                        c.pageTabs.insert(1,('Results',session['results']))
263                        selectionsNeeded=1
264                        for tab in c.pageTabs[0]:
265                            if tab == 'Selections':
266                                selectionsNeeded=0
267                        if selectionsNeeded:
268                            c.pageTabs.append(('Selections',
269                                       h.url_for(controller='selectedItems',
270                                                 action='index')))
271                           
272                return render('results')
273               
274        except ValueError,e:
275            if debug: 
276                raise ValueError,str(e)
277            else:
278                c.xml='<p> Error retrieving documents for %s hits is [%s]</p>'%(hits,e)
279                return render('content')
280        except Exception,e:
281                c.xml='Unknown error %s,%s'%(str(Exception),e)
282                return render('error')                       
283       
284    def __advancedPrompt(self,searchConstraints=None):
285        ''' This provides the advanced search input page '''
286        try:
287            discoveryURL=self.cf.get('SEARCH','discoveryURL')
288            advancedURL=self.cf.get('SEARCH','advancedURL')
289        except:
290            return 'Error, invalid configuration for search interface'
291        #defaults
292        c.bbox='90.0','-180.0','180.0','-90.0'
293        c.startDateDay,c.startDateMon,c.startDateYear='','',''
294        c.endDateDay,c.endDateMon,c.endDateYear='','',''
295        c.textTarget='All'
296        c.searchString=''
297        c.source=['All']
298        c.geoSearchType='overlaps'
299        #constraints
300
301        if searchConstraints is not None:
302            if searchConstraints['dateRange'] is not None:
303                c.startDateDay,c.startDateMon,c.startDateYear=searchConstraints['dateRange'][0]
304                c.endDateDay,c.endDateMon,c.endDateYear=searchConstraints['dateRange'][1]
305            if searchConstraints['bbox'] is not None:
306                c.bbox=searchConstraints['bbox']
307            if searchConstraints['textTarget'] is not None:
308                c.textTarget=searchConstraints['textTarget']
309            if searchConstraints['searchString'] is not None:
310                c.searchString=searchConstraints['searchString']
311            if searchConstraints['scope'] is not None:
312                c.source=searchConstraints['scope']
313            c.geoSearchType=(searchConstraints['geoSearchType'] or 'overlaps')
314        return render('advanced')
315       
316    def __checkbox(self,bbox):
317        m='Invalid bounding box dimensions entered - limits are '
318        if float(bbox[0])>90.0 or float(bbox[3])<-90.:
319            self.message=m+'+90 (N), -90 (S)!'
320        if float(bbox[1])<-180. or float(bbox[2])>180.:
321            if self.message=='':self.message=m
322            self.message=self.message[:-1]+' -180 (W), 180 (E)!'
323           
324    def __checkform(self,expected):
325        ''' Simply checks the inputs to make sure the elements in expected are present '''
326        message="An incomplete NDG search form was received: "
327        for i in expected:
328            if i not in self.inputs: 
329                self.message=message+i
330        if self.message!='':self.message+='[%s]'%self.inputs
331               
332    def __checkdates(self,dateRange):
333        ''' Check input dates for sanity '''
334       
335        if not ValidDate(dateRange[0])*ValidDate(dateRange[1]):
336            self.message='Input dates are not valid [%s]'%dateRange
337        elif JulDay(dateRange[0])>=JulDay(dateRange[1]):
338            self.message='Second date must be after first date'
339       
340    def __buildconstraints(self,dateRange,bbox,scope,searchString,geoSearch):
341        ''' Just build a constraint string '''
342        return constraints(dateRange=dateRange,bbox=bbox,scope=scope,searchString=searchString,geoSearchType=geoSearch)
343       
344    #def oneLineSearch(self):
345    #    try:
346    #        discoveryURL=self.cf.get('SEARCH','discoveryURL')
347    #        oneLiner=DiscoveryTemplate.oneLiner
348    #    except:
349    #        return 'Error, invalid configuration for search interface'
350    #    return DiscoveryTemplate.searchTextOneLine%locals() 
351   
352    def semantic(self):
353        self.__setup()
354        vs=VS(proxyServer=self.cf.get('DEFAULT','proxyServer'))
355        if 'searchString' in self.inputs:
356            try:
357                [broader,narrower,synonyms]=vs.getRelated(self.inputs['searchString'])
358                #get a base string for the links to new searches
359                if 'start' in self.inputs: del self.inputs['start']
360                if 'howmany' in self.inputs: del self.inputs['howmany']
361                self.inputs['searchString']='###SEARCHSSTRING###'
362                q='%s/discovery?'%g.server
363                for i in self.inputs: q+='%s=%s&'%(i,self.inputs[i])
364                url=q[0:-1]
365                # and now build the links
366                c.narrower=[]
367                c.broader=[]
368                c.synonyms=[]
369                for i in narrower:
370                    c.narrower.append((i,url.replace('###SEARCHSSTRING###',i)))
371                for i in broader:
372                    c.broader.append((i,url.replace('###SEARCHSSTRING###',i)))
373                for i in synonyms:
374                    c.synonyms.append((i,url.replace('###SEARCHSSTRING###',i)))
375                if c.narrower!=[] or c.broader!=[] or c.synonyms!=[]: c.semAvailable=1
376            except IOError,e:
377                c.semAvailable=0
378                c.semError=' (No valid reply from vocabulary service)'
379                #This should go in a log file ...
380                print 'ERROR: Vocabulary Service: %s (for search [%s])'%(str(e),self.inputs['searchString'])
381        else:
382            broader,narrower,synonyms=[],[],[]
383            c.semAvailable=0
384            c.semError='.'
385       
386        return render('semantic',fragment=True)
387   
388    def moreSearch(self,ws):
389        ''' Provides the search on Browse and NumSim content '''
390        c.results=ws.results
391        c.searchTarget=self.inputs['searchTarget']
392        for r in c.results:
393            n=ndgObject(r.id,config=self.cf)
394            r.link={'Browse':n.BURL,'NumSim':n.URL}[c.searchTarget]
395       
396        return render('short_results')
397           
398    def clearSession(self):
399        ''' Clear out all session variables - to help when these change in development '''
400        session.clear()
401        session.save()           
402       
403
404   
405       
406       
Note: See TracBrowser for help on using the repository browser.