source: MILK/trunk/milk_server/milk_server/controllers/browse/discovery.py @ 4487

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

Further restructure of codebase - moving datamodels into ndgUtils to allow reuse + create visualise and browse sub structures to better
organise related code. Separate out some inner classes into their own
modules to make more visible.

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