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

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

Make handling of parameters common - so granulite + form input both
use same route + add extra code to deal with input of double quotes
here (cannot have this in the data since the data is an XML attribute)
+ more tidying of editor/browse links.

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