source: MILK/trunk/milk_server/milk_server/controllers/discovery.py @ 4482

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/MILK/trunk/milk_server/milk_server/controllers/discovery.py@4482
Revision 4482, 17.1 KB checked in by cbyrom, 12 years ago (diff)

Replace 'viewItems' controller and flesh out selected Items controller - to allow user to plot results out in Google Earth and conTerra +
add wmc client section to config file to allow this to be specified as
an alternative service + tidy up some logging and unused code + update
prototype version to avoid IE problems.

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