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

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

Retrieve the 'orderByFieldList' data from the discovery ws + add new
drop down to set the order by direction and implement the logic
for using this.

Line 
1'''
2Controller for the discovery search functionality
3'''
4import socket, logging
5from paste.request import parse_querystring
6from ndg.common.src.clients.ws.discovery.discoveryserviceclient import DiscoveryServiceClient
7from ndg.common.src.clients.xmldb.eXist.searchclient import SearchClient
8from ndg.common.src.clients.http.vocabserverclient import VocabServerClient as VS
9from ndg.common.src.models.vocabtermdata import VocabTermData as VTD
10from ndg.common.src.models.ndgObject import ndgObject
11from ndg.common.src.models.DIF import DIF
12from ndg.common.src.lib.mailer import mailHandler
13from milk_server.lib.base import *
14from milk_server.lib.Date import *
15from milk_server.models.DiscoveryState import DiscoveryState, constraints
16from milk_server.controllers.home import HomeController
17import browserconstants as bc
18from milk_server.lib.Utilities import getURLConstraints
19import milk_server.lib.constants as constants
20
21
22class DiscoveryController(HomeController):
23    '''
24    Provides the pylons controller for NDG discovery
25    '''
26
27    def __setup(self):
28        '''
29        Common setup for controller methods
30        '''
31        self.cf=request.environ['ndgConfig']
32        self.inputs=dict(parse_querystring(request.environ))
33       
34        self.message=''
35        c.inputErrors = {}    # dict to store error messages
36       
37        c.discoveryUrl = h.url_for('discovery')
38                       
39   
40    def index(self):
41        '''
42        Main entry point for doing discovery searches
43        '''
44        self.__setup()
45       
46        # if inputs are not set: if the discovery mode is enabled, display
47        # the search screen, otherwise redirect to the default home page according
48        # to what milk mode is set (i.e. editor/browse)
49        if not self.inputs or 'ClearForm' in self.inputs:
50            if g.discoveryEnabled:
51                return self.__advancedPrompt()
52            else:
53                logging.info("Discovery mode not enabled - redirect to default")
54                return h.redirect_to(h.url_for('default'))
55       
56        self.__getInputs()
57
58        if 'orderBy' not in self.inputs:
59            self.inputs['orderBy'] = g.orderByList[0]
60        c.orderByList = h.options_for_select(g.orderByList, self.inputs['orderBy'])
61
62        if 'orderDirection' not in self.inputs:
63            self.inputs['orderDirection'] = constants.ORDER_BY_DIRECTION[0]
64        c.orderDirection = h.options_for_select(constants.ORDER_BY_DIRECTION,
65                                                self.inputs['orderDirection']) 
66       
67        # if any errors are found, return user to search page
68        if c.inputErrors:
69            return self.__advancedPrompt()
70
71        searchString = self.inputs['searchString']
72        if 'vocabTerm' in self.inputs:
73            searchString += " %s" %self.inputs['vocabTerm']
74           
75        # users can return to search page to refine the search inputs; in this case
76        # they will have a 'constrained' input
77        self.constraints = self.__buildconstraints(self.dateRange, self.bbox, self.scope,
78                                                   searchString, self.inputs['geoSearchType'])
79        if 'constrained' in self.inputs: 
80            return self.__advancedPrompt(searchConstraints = self.constraints)
81
82        # ok, now go do the search
83        try:
84            return self.__runSearch(searchString, self.inputs['textTarget'],
85                                    self.inputs['start'], self.inputs['howmany'], 
86                                    orderBy = self.inputs['orderBy'], 
87                                    orderDirection = self.inputs['orderDirection'],
88                                    scope = self.scope, dateRange = self.dateRange, 
89                                    bbox = self.bbox, geoSearch=self.inputs['geoSearchType'])
90        except Exception, e:
91            if g.debugModeOn == 'True':
92                raise e
93            else:
94                c.xml='Unexpected error: %s'%(str(e))
95                return render('error')
96
97
98    def __getInputs(self):
99        '''
100        Retrieve the user inputs and set defaults.  Values are stored in the
101        self.inputs dict
102        '''
103        logging.debug("Getting user inputs")
104       
105        # restore contraints from input, if set
106        if 'constraints' in self.inputs:
107            constraints = getURLConstraints(self.inputs['constraints'])
108            del self.inputs['constraints']
109            # NB, be careful not to overwrite newly passed in info
110            for key, val in constraints.items():
111                if key not in self.inputs:
112                    self.inputs[key] = val
113                   
114        if 'vocabTerm' in self.inputs and 'searchString' not in self.inputs:
115            self.inputs['searchString'] = ""
116           
117        # see if this is a discovery search or a more complicated search
118        if 'searchTarget' not in self.inputs: 
119            self.inputs['searchTarget']='Discovery'
120       
121        # set default for table paging, if not already set
122        # NB, url arguments need converting back to ints
123        if 'start' not in self.inputs:
124            self.inputs['start'] = 1
125        else:
126            self.inputs['start'] = int(self.inputs['start'])
127           
128        if 'howmany' not in self.inputs:
129            self.inputs['howmany'] = 10
130        else:
131            self.inputs['howmany'] = int(self.inputs['howmany'])
132           
133        # the simplest query we might get is a text search, in which case
134        # the inputs should be start, howmany and searchString (although
135        # maybe not in that order. The next simplest is one with
136        # a specified textTarget, after that we need all the inputs.
137        if 'searchString' in self.inputs and 'textTarget' not in self.inputs:
138            # it's a simple text search
139            self.inputs['textTarget']='All'
140
141        # the next simplest is one that includes texttarget as well ...
142        expected=['searchString','textTarget']
143        missingInputs = self.__checkform(expected)
144        if missingInputs:
145            if bc.INCOMPLETE_SEARCH_INPUT_MESSAGE not in c.inputErrors:
146                c.inputErrors[bc.INCOMPLETE_SEARCH_INPUT_MESSAGE] = []
147            c.inputErrors[bc.INCOMPLETE_SEARCH_INPUT_MESSAGE].extend(missingInputs)
148
149        self.__getSpatioTemporalInputs()
150       
151        logging.debug("User inputs retrieved")
152
153
154    def __getSpatioTemporalInputs(self):
155        '''
156        Get spatiotemporal input data - and set up any defaults, if required
157        '''
158        logging.debug("Getting spatiotemporal inputs")
159        if 'geoSearchType' not in self.inputs:
160            self.inputs['geoSearchType']='overlaps'
161
162        # now we add the defaults... this is kind of historical - NOT SURE THIS IS STILL NEEDED
163        if len(self.inputs)==6:
164            self.bbox=None
165            self.dateRange=None
166            self.scope=None
167            return
168       
169        if 'source' in self.inputs and self.inputs['source'] != 'All':
170            # NB, the WSDL expects a list
171            self.scope = [self.inputs['source']]
172        else:
173            self.scope = None
174           
175        missingInputs = self.__checkform(['bboxN','bboxE','bboxS','bboxW','geoSearchType'])
176        if missingInputs: 
177            self.bbox = None
178        else:
179            # default form has a global bounding box, NB, internal to this routine we use bbox=[N,W,E,S], not [W,S,E,N]!
180            self.bbox = [self.inputs['bboxN'], self.inputs['bboxW'],
181                         self.inputs['bboxE'], self.inputs['bboxS']]
182           
183            errors = self.__checkBBoxValidity(self.bbox)
184            if errors:
185                if bc.INVALID_BBOX_MESSAGE not in c.inputErrors:
186                    c.inputErrors[bc.INVALID_BBOX_MESSAGE] = []
187                c.inputErrors[bc.INVALID_BBOX_MESSAGE].extend(errors)
188
189        missingInputs = self.__checkform(['startDate', 'endDate'])
190        self.dateRange = None
191        if missingInputs:
192            self.dateRange = None
193        elif self.inputs['startDate'] and not self.inputs['endDate']:
194            c.inputErrors[bc.INCOMPLETE_DATERANGE_MESSAGE] = ['End date missing']
195        elif not self.inputs['startDate'] and self.inputs['endDate']:
196            c.inputErrors[bc.INCOMPLETE_DATERANGE_MESSAGE] = ['Start date missing']
197        elif self.inputs['startDate'] and self.inputs['endDate']:
198            dateError = None
199            try:
200                year, month, day = self.inputs['startDate'].split('/')
201                self.dateRange = [(day, month, year)]
202                year, month, day = self.inputs['endDate'].split('/')
203                self.dateRange.append((day, month, year))
204
205                if self.dateRange <> [("","",""),("","","")]:
206                    dateError = self.__checkdates(self.dateRange)
207                   
208                else: 
209                    self.dateRange = None
210                               
211            except:
212                dateError = 'Invalid date provided'
213
214            if dateError:
215                if bc.INVALID_DATERANGE_MESSAGE not in c.inputErrors:
216                    c.inputErrors[bc.INVALID_DATERANGE_MESSAGE] = []
217                c.inputErrors[bc.INVALID_DATERANGE_MESSAGE].append(dateError)
218       
219        logging.debug("Spatiotemporal inputs retrieved")
220
221
222    def __getSearchClient(self, clientType):
223        '''
224        Retrieve the appropriate client to complete the search
225        - currently supported are browse and discovery clients
226        @param clientType: type of search client to use.  Currently accepts,
227        'Discovery', 'Browse' and 'NumSim'
228        @raise ValueError if unrecognised search client entered
229        @return search client adhering to the ndg.common.clients.interfacesearchclient
230        interface
231        '''
232        logging.debug("Getting %s type search client" %clientType)
233        searchClient = None
234        if clientType =='Discovery':
235            logging.info(" - use Discovery service to complete search")
236            if g.discoveryServiceURL:
237                searchClient = DiscoveryServiceClient(HostAndPort = g.discoveryServiceURL)
238            else:
239                searchClient = DiscoveryServiceClient()
240               
241        elif clientType in ['Browse','NumSim']:
242            logging.info(" - use Browse service to complete search")
243            searchClient = SearchClient(dbHostName = g.localEXist,
244                                        configFileName = g.pwFile)
245        else:
246            raise ValueError("Unrecognised search type, '%s'" %clientType)
247       
248        logging.debug("- returning search client")
249        return searchClient
250       
251
252    def __runSearch(self, searchString, textTarget, start,
253                    howmany, orderBy = None, orderDirection = None, scope = None, 
254                    dateRange = None, bbox = None, geoSearch = 'overlaps'):
255        '''
256        Carry out a text search for <searchString>
257        in the <textTarget> where the accepted text target values are controlled
258        by the DiscoveryTemplate GUI, and are: All, Authors, Parameters
259        @param searchString: string to search for
260        @param textTarget: target of the search - either, 'All', 'Authors' or 'Parameters'
261        @param start: starting record to return
262        @param howmany: number of records to return
263        @keyword orderBy: Field to order results by - NB, must be one of the
264        getList('orderByFieldList') values
265        @keyword orderDirection: Direction the 'orderBy' results should be returned in
266        - NB, this is currently 'ascending' or 'descending'
267        @keyword scope: scope of search - either NERC, NERC_DDC, MDIP or DPPP. Default = None
268        @keyword dateRange: date range in format [startDate, endDate] where the
269        date objects are tuples with format (day, month, year). Default = None
270        @keyword bbox: Bounding box in format [N,W,E,S]. Default = None
271        @keyword geoSearch: type of spatial search. Defaults to 'overlaps'.
272        '''
273        logging.debug("Running text search with string, '%s'" %searchString)
274       
275        searchClient = self.__getSearchClient(self.inputs['searchTarget'])
276       
277        if self.inputs['searchTarget'] in ['Browse','NumSim']:
278            textTarget = self.inputs['searchTarget']
279            if textTarget == 'Browse':
280                # NB, this switches the searching to be done on atom format
281                # rather than moles1.3 format docs
282                textTarget = SearchClient.ATOM_TARGET#'ndg_B_metadata'
283           
284        # PJK 04/09/08 Handle errors more gracefully
285        #
286        # http://proj.badc.rl.ac.uk/ndg/ticket/984
287        try:
288            searchClient.search(searchString,
289                                start = start,
290                                howmany = howmany,
291                                target = textTarget,
292                                scope = scope,
293                                dateRange = dateRange,
294                                bbox = bbox,
295                                geoSearchType = geoSearch,
296                                orderBy = orderBy,
297                                orderDirection = orderDirection)
298        except socket.error, e:
299            logging.error("Socket error for discovery service search: %s" % e)
300            c.xml='The Discovery Service is unavailable.  Please check with '+\
301                    'your system administrator'
302            return render('error')
303        except Exception, e:
304            logging.error("Calling discovery service search: %s" % e)
305            c.xml='An internal error occured.  Please check with ' + \
306                    'your system administrator'
307            return render('error')
308           
309        logging.debug("Search returned - now processing results")
310        # DiscoveryState object is a wrapper to the various search config
311        # variables
312        c.state = DiscoveryState(searchClient.serverSessionID, searchString,
313                                 request.environ, searchClient.hits, self.constraints,
314                                 start, howmany)
315
316        return self.__processSearchResults(searchClient, c.state)
317
318
319    def __processSearchResults(self, searchClient, ds):
320        '''
321        Process the results from a search - as ran by the input search client object
322        @param searchClient: search client adhering to the ndg.common.clients.interfacesearchclient
323        interface - which has just ran a search
324        @param ds: DiscoveryState object with info on the search
325        '''
326        if searchClient.error:
327            logging.error("Error encountered whilst running search: %s" %searchClient.error)
328            m=''
329            for i in searchClient.error:
330                m+='<p>%s</p>'%i
331            c.xml = m
332            return render('error')
333       
334        hits = searchClient.hits
335        # NB, this is used in the semantic search function of results.kid and short_results.kid
336        c.querystring = request.environ['QUERY_STRING']
337
338        difs = []
339        errors = []
340
341        if hits == 0 and ds.constraintsInstance['textTarget'] != SearchClient.ATOM_TARGET:
342            outMessage = 'No records found for "%s"[constraints: %s]' \
343                %(ds.searchString, ds.constraints)
344            logging.info(outMessage) 
345            c.xml='<p>' + outMessage + '</p>'
346
347        else:
348            try:
349                # display browse search results differently
350                if self.inputs['searchTarget'] != 'Discovery':
351                    return self.__displayBrowseSearchResults(searchClient)
352   
353                # now actually retrieve the search records
354                results = searchClient.getLabelledDocs(format='DIF')
355   
356                if not results:
357                    c.xml='<p>No results for "%s"!</p>'%ds.searchString
358                else:
359                    for result in results: 
360                        obj=ndgObject(result[0], config = self.cf)
361                        try:
362                            difs.append(DIF(result[1],ndgObj=obj))
363                        except ValueError,e:
364                            errors.append((result[0], str(e)))
365       
366                    if not difs:
367                        c.xml='<p>No usable results for "%s"!</p>'%ds.searchString
368                   
369                    elif errors:
370                        c.xml='<p>Search results for "%s"'%ds.searchString
371                        dp=[]
372                        for e in errors:
373                            n=ndgObject(e[0])
374                            if n.repository not in dp: 
375                                dp.append(n.repository)
376                        if len(dp)<>1: 
377                            dp='[Various Data Providers]'
378                        else:
379                            dp='[%s]'%dp[0] 
380                           
381                        c.xml+=' (unfortunately %s hits matched unformattable documents from %s, an internal error has been logged):</p>'%(len(errors),dp)
382                        status, message=mailHandler([g.metadataMaintainer],'DIF errors',
383                                                    str(errors), server = g.mailServer)
384                        if not status:
385                            c.xml+='<p> Actually, not even an internal error has been logged. <br/>'
386                            c.xml+='Internal sending of mail failed with error [%s]</p>'%message
387                   
388                # if we're here, we're ready to display the dif records
389                c.difs = difs
390                session['results'] = h.current_url()
391                session.save()
392               
393                # set up the displayed tabs
394                if len(c.pageTabs)==1: 
395                    c.pageTabs.append(('Results', session['results']))
396                    c.pageTabs.append(('Selections',
397                                       h.url_for(controller='visualise/selectedItems',
398                                                 action='index')))
399                elif c.pageTabs[1][0]!='Results':
400                        c.pageTabs.insert(1,('Results',session['results']))
401                        selectionsNeeded=1
402                        for tab in c.pageTabs[0]:
403                            if tab == 'Selections':
404                                selectionsNeeded=0
405                        if selectionsNeeded:
406                            c.pageTabs.append(('Selections',
407                                       h.url_for(controller='visualise/selectedItems',
408                                                 action='index')))
409                           
410                   
411            except ValueError,e:
412                if g.debugModeOn == 'True':
413                    raise ValueError,str(e)
414
415                c.xml='<p> Error retrieving documents for %s hits is [%s]</p>'%(hits,e)
416
417        return render('browse/results')
418       
419    def __advancedPrompt(self, searchConstraints = None):
420        '''
421        This provides the advanced search input page
422        @keyword searchConstraints: a DiscoveryState.constraints object with the
423        search filter details in
424        '''
425        #defaults
426        c.title = bc.DISCOVERY_HOME_TITLE
427        c.bbox='90.0','-180.0','180.0','-90.0'
428        c.startDate = ''
429        c.endDate = ''
430        c.textTarget='All'
431        c.searchString=''
432        c.source=['All']
433        c.geoSearchType='overlaps'
434
435        # apply any available constraints
436        if searchConstraints:
437            if searchConstraints['dateRange']:
438                c.startDate = '%s/%s/%s' %searchConstraints['dateRange'][0]
439                c.endDate = '%s/%s/%s' %searchConstraints['dateRange'][1]
440            if searchConstraints['bbox']:
441                c.bbox=searchConstraints['bbox']
442            if searchConstraints['textTarget']:
443                c.textTarget=searchConstraints['textTarget']
444            if searchConstraints['searchString']:
445                c.searchString=searchConstraints['searchString']
446            if searchConstraints['scope']:
447                c.source=searchConstraints['scope']
448            if searchConstraints['geoSearchType']:
449                c.geoSearchType = searchConstraints['geoSearchType']
450       
451        return self.savePageAndRender("browse/discovery_search", **self.inputs)
452
453       
454    def __checkBBoxValidity(self, bbox):
455        '''
456        Check the integrity of the bounding box; return any errors found as list
457        @return: list of errors
458        '''
459        errors = []
460       
461        for name, val in [('North', float(bbox[0])), ('South', float(bbox[3]))]:
462            if val > 90.0 or val < -90.:
463                errors.append("%s latitude exceeds valid range - -90 <= x <= 90" %name)
464               
465        for name, val in [('West', float(bbox[1])), ('East', float(bbox[2]))]:
466            if val > 180.0 or val < -180.:
467                errors.append("%s longitude exceeds valid range - -180 <= x <= 180" %name)
468        return errors
469
470           
471    def __checkform(self,expected):
472        '''
473        Simply checks the inputs to make sure the elements in expected are present
474        - NB, this isn't actually checking that a value for these inputs are set, it
475        is just checking the fields are there
476        @return array of missing inputs
477        '''
478        logging.debug("Checking for missing inputs")
479        missingInputs = []
480        for i in expected:
481            if i not in self.inputs:
482                logging.debug(" - found missing input: %s" %i)
483                missingInputs.append(i)
484        logging.debug("Finished checking for missing inputs")
485        return missingInputs
486       
487               
488    def __checkdates(self,dateRange):
489        '''
490        Check input dates for sanity
491        @return: error message, if invalid, None otherwise
492        '''
493        if not ValidDate(dateRange[0])*ValidDate(dateRange[1]):
494            return str(dateRange)
495        elif JulDay(dateRange[0]) >= JulDay(dateRange[1]):
496            return 'second date must be after first date'
497     
498        return None
499
500       
501    def __buildconstraints(self, dateRange, bbox, scope, searchString, geoSearch):
502        '''
503        Build and return a DiscoveryState.constraints object
504        '''
505        return constraints(dateRange=dateRange, bbox=bbox,
506                           scope=scope, searchString=searchString, 
507                           geoSearchType=geoSearch)
508       
509
510    def semantic(self):
511        self.__setup()
512        vs = VS(proxyServer = g.proxyServer)
513        if 'searchString' in self.inputs:
514            try:
515                [broader,narrower,synonyms] = vs.getRelated(self.inputs['searchString'])
516                #get a base string for the links to new searches
517                if 'start' in self.inputs: del self.inputs['start']
518                if 'howmany' in self.inputs: del self.inputs['howmany']
519                self.inputs['searchString']='###SEARCHSSTRING###'
520                q='%s/discovery?'%g.server
521                for i in self.inputs: q+='%s=%s&'%(i,self.inputs[i])
522                url=q[0:-1]
523                # and now build the links
524                c.narrower=[]
525                c.broader=[]
526                c.synonyms=[]
527                for i in narrower:
528                    c.narrower.append((i,url.replace('###SEARCHSSTRING###',i)))
529                for i in broader:
530                    c.broader.append((i,url.replace('###SEARCHSSTRING###',i)))
531                for i in synonyms:
532                    c.synonyms.append((i,url.replace('###SEARCHSSTRING###',i)))
533                if c.narrower!=[] or c.broader!=[] or c.synonyms!=[]: c.semAvailable=1
534            except IOError,e:
535                c.semAvailable=0
536                c.semError=' (No valid reply from vocabulary service)'
537                #This should go in a log file ...
538                print 'ERROR: Vocabulary Service: %s (for search [%s])'%(str(e),self.inputs['searchString'])
539        else:
540            broader,narrower,synonyms=[],[],[]
541            c.semAvailable=0
542            c.semError='.'
543       
544        return render('browse/semantic',fragment=True)
545
546   
547    def __displayBrowseSearchResults(self, searchClient):
548        '''
549        Provides the search results for Browse and NumSim content
550        @param searchClient: search client adhering to the ndg.common.clients.interfacesearchclient
551        interface - which has just ran a search
552        '''
553        c.results = searchClient.results
554        c.searchTarget = self.inputs['searchTarget']
555        textTarget = self.inputs['textTarget']
556
557        # check if we've done a search against atoms - NB, this should be the
558        # default eventually - so we can remove all the alternative options
559        isAtom = False
560        if textTarget == SearchClient.ATOM_TARGET:
561            isAtom = True
562       
563        for r in c.results:
564            id = r.id
565                # cope with atom docs
566            if isAtom:
567                r.link = r.href
568            else:
569                n=ndgObject(id,config=self.cf)
570                r.link={'Browse':n.BURL,'NumSim':n.URL}[c.searchTarget]
571
572        # filter atom docs according to publication state
573        if isAtom:
574            c.searchTerm = " - for search term, '%s'" %self.inputs['searchString']
575            if not g.atomEditorEnabled:
576                c.results = self.__filterAtomResults(c.results)
577
578            if c.results:
579                c.searchTerm += ' [%s results found]' %len(c.results)
580               
581               
582            html = render('genshi', 'browse/short_atom_results')
583            # make sure the edit links point to the editor, not the browse service
584            html = html.replace(VTD.BROWSE_SERVER_URL + '/editAtom', g.server + '/editAtom')
585            return html
586        else:
587            return render('browse/short_results')
588
589
590    def __filterAtomResults(self, results):
591        '''
592        Given a set of atom docs search results, filter these to only return docs in the
593        'published' or 'Published' state
594        @param results: list of results as returned by SearchClient
595        @return filteredResults: list of results with only published data included
596        '''
597        logging.debug("Filtering results to remove non-published data")
598        filteredResults = []
599        for result in results:
600            if result.collection.find('ublished') == -1:
601                logging.debug("- found non-published doc - ignoring")
602                continue
603            filteredResults.append(result)
604        logging.debug("- returning filtered results")
605        return filteredResults
606
607           
608    def clearSession(self):
609        '''
610        Clear out all session variables - to help when these change in development
611        '''
612        session.clear()
613        session.save()           
614     
Note: See TracBrowser for help on using the repository browser.