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

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

Add new home page for the browse service + implement usage of the
before methods on controllers to check if MILK is set up to
allow the various services before the controller methods are invoked.
Also add a top level default controller to redirect user to the correct
home page depending on the mode of operation + improve styles,
remove unused controller and move error template to more general top
level - for re-use across the MILK stack.

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