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

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

Write README.txt for MILK with basic install and config info +
tidy up config files, removing obsolete data + remove unused help
page + fix proper use of global discovery url to simplify use.

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        #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('browse/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 = g.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('browse/semantic',fragment=True)
379
380   
381    def moreSearch(self,ws):
382        ''' Provides the search on Browse and NumSim content '''
383        c.results=ws.results
384        c.searchTarget=self.inputs['searchTarget']
385        textTarget = self.inputs['textTarget']
386
387        # check if we're doing a search against atoms - NB, this should be the
388        # default eventually - so we can remove all the alternative options
389        isAtom = False
390        if textTarget == SearchClient.ATOM_TARGET:
391            isAtom = True
392       
393        for r in c.results:
394            id = r.id
395                # cope with atom docs
396            if isAtom:
397                r.link = r.href
398            else:
399                n=ndgObject(id,config=self.cf)
400                r.link={'Browse':n.BURL,'NumSim':n.URL}[c.searchTarget]
401
402        if isAtom:
403            c.searchTerm = " - for search term, '%s'" %self.inputs['searchString']
404            if not g.atomEditorEnabled:
405                c.results = self.__filterAtomResults(c.results)
406
407            if c.results:
408                c.searchTerm += ' [%s results found]' %len(c.results)
409               
410               
411            html = render('genshi', 'browse/short_atom_results')
412            # make sure the edit links point to the editor, not the browse service
413            html = html.replace(VTD.BROWSE_SERVER_URL + '/editAtom', g.server + '/editAtom')
414            return html
415        else:
416            return render('browse/short_results')
417
418
419    def __filterAtomResults(self, results):
420        '''
421        Given a set of atom docs search results, filter these to only return docs in the
422        'published' or 'Published' state
423        @param results: list of results as returned by SearchClient
424        @return filteredResults: list of results with only published data included
425        '''
426        logging.debug("Filtering results to remove non-published data")
427        filteredResults = []
428        for result in results:
429            if result.collection.find('ublished') == -1:
430                logging.debug("- found non-published doc - ignoring")
431                continue
432            filteredResults.append(result)
433        logging.debug("- returning filtered results")
434        return filteredResults
435
436           
437    def clearSession(self):
438        ''' Clear out all session variables - to help when these change in development '''
439        session.clear()
440        session.save()           
441     
Note: See TracBrowser for help on using the repository browser.