simplified 'reserves search' function
authorgfawcett <gfawcett@6d9bc8c9-1ec2-4278-b937-99fde70a366f>
Thu, 15 Jul 2010 00:55:50 +0000 (00:55 +0000)
committergfawcett <gfawcett@6d9bc8c9-1ec2-4278-b937-99fde70a366f>
Thu, 15 Jul 2010 00:55:50 +0000 (00:55 +0000)
I broke the search() function into two parts, a request/response outer
function, and an inner function focussed on getting the actual result
sets. Much cleaning up ensued.

git-svn-id: svn://svn.open-ils.org/ILS-Contrib/servres/trunk@921 6d9bc8c9-1ec2-4278-b937-99fde70a366f

conifer/static/tango/lock.png [new file with mode: 0644]
conifer/syrup/views/search.py
conifer/templates/search_results.xhtml

diff --git a/conifer/static/tango/lock.png b/conifer/static/tango/lock.png
new file mode 100644 (file)
index 0000000..fe735ac
Binary files /dev/null and b/conifer/static/tango/lock.png differ
index 6cee7c6..79cc6ed 100644 (file)
 from _common import *
 from PyZ3950 import zoom, zmarc
 
+
+# ENABLE_USER_FILTERS: if True, then search results will not contain
+# anything that the logged-in user would not be permitted to view. For
+# example, if the user is not logged in, only "anonymous" site
+# contents would be included in any search results.
+
+ENABLE_USER_FILTERS = True
+
+
+#----------------------------------------------------------------------
+# Some combinators for building up Q() queries
+
+def OR(clauses_list):
+    return reduce(lambda a, b: a | b, clauses_list, Q())
+
+def AND(clauses_list):
+    return reduce(lambda a, b: a & b, clauses_list, Q())
+
+
+#----------------------------------------------------------------------
+
 def normalize_query(query_string,
                     findterms=re.compile(r'"([^"]+)"|(\S+)').findall,
                     normspace=re.compile(r'\s{2,}').sub):
-    ''' Splits the query string in invidual keywords, getting rid of unecessary spaces
-        and grouping quoted words together.
+    ''' Splits the query string in invidual keywords, getting rid of
+        unecessary spaces and grouping quoted words together.
         Example:
-        
-        >>> normalize_query('  some random  words "with   quotes  " and   spaces')
+
+        >>> normalize_query(
+             '  some random  words "with   quotes  " and   spaces')
         ['some', 'random', 'words', 'with quotes', 'and', 'spaces']
-    
-    '''
-    return [normspace(' ', (t[0] or t[1]).strip()) for t in findterms(query_string)] 
 
-def get_query(query_string, search_fields):
-    ''' Returns a query, that is a combination of Q objects. That combination
-        aims to search keywords within a model by testing the given search fields.
-    
     '''
-    query = None # Query to search for every search term        
-    terms = normalize_query(query_string)
-    for term in terms:
-        or_query = None # Query to search for a given term in each field
-        for field_name in search_fields:
-            q = Q(**{"%s__icontains" % field_name: term})
-            if or_query is None:
-                or_query = q
-            else:
-                or_query = or_query | q
-        if query is None:
-            query = or_query
-        else:
-            query = query & or_query
+    norm = [normspace(' ', (t[0] or t[1]).strip())
+            for t in findterms(query_string)]
+    return norm
+
+
+
+def build_query(query_string, search_fields):
+    """
+    Returns a Q() query filter for searching for keywords within a
+    model by testing the given search fields.
+
+    For example, the query "foot wash" with search_fields ['title',
+    'author'] will return a Q() object representing the filter:
+
+    (AND: (OR: ('title__icontains', u'foot'),
+               ('author__icontains', u'foot')),
+          (OR: ('title__icontains', u'wash'),
+               ('author__icontains', u'wash')))
+    """
+
+    def clause(field_name, expression):
+        return Q(**{"%s__icontains" % field_name: expression})
+
+    terms   = normalize_query(query_string)
+
+    clauses = [ [ clause(field_name, term) for field_name in search_fields ]
+                for term in terms ]
+
+    query   = AND([OR(inner) for inner in clauses])
+
     return query
 
 
-#-----------------------------------------------------------------------------
-# Search and search support
-def search(request, in_site=None, with_instructor=None):
-    ''' Need to work on this, the basic idea is
-        - put an entry point for instructor and site listings
-        - page through item entries
-        If in_site is provided, then limit search to the contents of the specified site.
-        If with_instructor is provided, then limit search to instructors
-    '''
-        
-    print("in_couse is %s" % in_site)
-    print("with_instructor is %s" % with_instructor)
-    found_entries = None
-    page_num = int(request.GET.get('page', 1))
-    count = int(request.GET.get('count', 5))
-    norm_query = ''
-    query_string = ''
-
-
-    #TODO: need to block or do something useful with blank query (seems dumb to do entire list)
-    #if ('q' in request.GET) and request.GET['q']:
-        
-    if ('q' in request.GET):
-        query_string = request.GET['q'].strip()
-
-    if len(query_string) > 0:
-        norm_query = normalize_query(query_string)
-        # we start with an empty results_list, as a default
-        results_list = models.Item.objects.filter(pk=-1)
-
-        # Next, we filter based on user permissions.
-        flt = user_filters(request.user)
-        user_filter_for_items, user_filter_for_sites = flt['items'], flt['sites'] 
-        # Note, we haven't user-filtered anything yet; we've just set
-        # up the filters.
-
-        # numeric search: If the query-string is a single number, then
-        # we do a short-number search, or a barcode search.
-
-        if re.match(r'\d+', query_string):
-            # Search by short ID.
-            results_list = models.Item.with_smallint(query_string)
-            if not results_list:
-                # Search by barcode.
-                results_list = models.Item.objects.filter(
-                    item_type='PHYS',
-                    metadata__name='syrup:barcode', 
-                    metadata__value=query_string)
-        else:
-            if not with_instructor:
-                # Textual (non-numeric) queries.
-                item_query = get_query(query_string, ['title', 'author', 'publisher', 'marcxml'])
-                #need to think about sort order here, probably better by author (will make sortable at display level)
-                results_list = models.Item.objects.filter(item_query)
 
-        if in_site:
-            # For an in-site search, we know the user has
-            # permissions to view the site; no need for
-            # user_filter_for_items.
-            results_list = results_list.filter(site=in_site)
-        elif with_instructor:
-            print("in instructor")
-            results_list = results_list.filter(instructor=with_instructor)
-        else:
-            results_list = results_list.filter(user_filter_for_items)
+def _search(query_string, for_site=None, for_owner=None, user=None):
+    """
+    Given a query_string, return two lists (Items and Sites) of
+    results that match the query. For_site, for_owner and user may be
+    used to limit the results to a given site, a given site owner, or
+    based on a given user's permissions (but see ENABLE_USER_FILTERS).
+    """
 
-        results_list = results_list.distinct() #.order_by('title')
-        results_len = len(results_list)
-        paginator = Paginator(results_list, count)
+    #--------------------------------------------------
+    # ITEMS
 
-        #site search
-        if in_site:
-            # then no site search is necessary.
-            site_list = []; site_len = 0
+    # Build up four clauses: one based on search terms, one based on
+    # the current user's permissions, one based on site-owner
+    # restrictions, and one based on site restrictions. Then we join
+    # them all up.
+
+    term_filter = build_query(query_string, ['title', 'author',
+                                             'publisher', 'marcxml'])
+    if ENABLE_USER_FILTERS and user:
+        user_filter = models.Item.filter_for_user(user)
+    else:
+        user_filter = Q()
+
+    owner_filter = Q(site__owner=for_owner) if for_owner else Q()
+    site_filter  = Q(site=for_site)         if for_site  else Q()
+
+    _items       = models.Item.objects.select_related()
+    print (term_filter & user_filter &
+                                 site_filter & owner_filter)
+    items        = _items.filter(term_filter & user_filter &
+                                 site_filter & owner_filter)
+
+    #--------------------------------------------------
+    # SITES
+
+    if for_site:
+        # if we're searching within a site, we don't want to return
+        # any sites as results.
+        sites = models.Site.objects.none()
+    else:
+        term_filter = build_query(query_string, ['course__name',
+                                                 'course__department__name',
+                                                 'owner__last_name',
+                                                 'owner__first_name'])
+        if ENABLE_USER_FILTERS and user:
+            user_filter  = models.Site.filter_for_user(user)
         else:
-            site_query = get_query(query_string, ['course__name', 'course__department__name'])
-            # apply the search-filter and the user-filter
-            site_results = models.Site.objects.filter(site_query).filter(user_filter_for_sites)
-            site_list = site_results.order_by('course__name')
-            site_len = len(site_results)
+            user_filter = Q()
+
+        owner_filter = Q(owner=for_owner) if for_owner else Q()
+
+        _sites       = models.Site.objects.select_related()
+        sites        = _sites.filter(term_filter & user_filter &
+                                     owner_filter)
+
+    #--------------------------------------------------
+
+    results = (list(items), list(sites))
+    return results
+
 
-        #instructor search
+
+#-----------------------------------------------------------------------------
+
+def search(request, in_site=None, for_owner=None):
+    ''' Search within the reserves system. If in_site is provided,
+        then limit search to the contents of the specified site.  If
+        for_owner is provided, then limit search to sites owned by
+        this instructor.
+    '''
+
+    print("in_site is %s" % in_site)
+    print("for_owner is %s" % for_owner)
+
+    query_string = request.GET.get('q', '').strip()
+
+    if not query_string:        # empty query?
         if in_site:
-            instructor_list = []; instr_len = 0
+            return HttpResponseRedirect(reverse('site_detail', in_site))
         else:
-            instr_query = get_query(query_string, ['user__last_name'])
-            instructor_results = models.Membership.objects.filter(instr_query).filter(role='INSTR')
-            if in_site:
-                instructor_results = instructor_results.filter(site=in_site)
-            instructor_list = instructor_results.order_by('user__last_name')[0:5]
-            instr_len = len(instructor_results)
-    elif in_site:
-        # we are in a site, but have no query? Return to the site-home page.
-        return HttpResponseRedirect('../')
+            return HttpResponseRedirect(reverse('browse'))
     else:
-        results_list = models.Item.objects.order_by('title')
-        results_len = len(results_list)
-        paginator = Paginator( results_list,
-            count)
-        site_results = models.Site.objects.filter(active=True)
-        site_list = site_results.order_by('course__name')[0:5]
-        site_len = len(site_results)
-        instructor_results = models.Member.objects.filter(role='INSTR')
-        instructor_list = instructor_results.order_by('user__last_name')[0:5]
-        instr_len = len(instructor_results)
-
-    #info for debugging
-    '''
-        print get_query(query_string, ['user__last_name'])
-        print instructor_list
-        print(norm_query)
-        for term in norm_query:
-            print term
-    '''
+        _items, _sites = _search(query_string, in_site, for_owner, request.user)
+        results        = _sites + _items
+        page_num       = int(request.GET.get('page', 1))
+        count          = int(request.GET.get('count', 5))
+        paginator      = Paginator(results, count)
+        norm_query     = normalize_query(query_string)
+
+        return g.render('search_results.xhtml', **locals())
+
+
+
+
+
 
-    return g.render('search_results.xhtml', **locals())
 
 #-----------------------------------------------------------------------------
-# Z39.50 support
+# Z39.50 support (for testing)
 
 def zsearch(request):
-    ''' 
     '''
-        
+    '''
+
     page_num = int(request.GET.get('page', 1))
     count = int(request.POST.get('count', 5))
 
     if request.GET.get('page')==None and request.method == 'GET':
-        targets_list = models.Z3950Target.objects.filter(active=True).order_by('name')
+        targets_list = models.Z3950Target.objects.filter(active=True) \
+            .order_by('name')
         targets_len = len(targets_list)
         return g.render('zsearch.xhtml', **locals())
     else:
-            
+
         target = request.GET.get('target')
         if request.method == 'POST':
             target = request.POST['target']
         print("target is %s" % target)
-            
+
         tquery = request.GET.get('query')
         if request.method == 'POST':
             tquery = request.POST['ztitle']
@@ -189,13 +202,14 @@ def zsearch(request):
         start = (page_num - 1) * count
         end = (page_num * count) + 1
 
-        idx = start; 
+        idx = start;
         for r in res[start : end]:
-                
+
             print("-> %d" % idx)
             if r.syntax <> 'USMARC':
                 collector.pop(idx)
-                collector.insert (idx,(None, 'Unsupported syntax: ' + r.syntax, None))
+                collector.insert (idx,(None, 'Unsupported syntax: ' + r.syntax,
+                                       None))
             else:
                 raw = r.data
 
@@ -209,7 +223,7 @@ def zsearch(request):
 
                 # How to Remove non-ascii characters (in case this is a problem)
                 #marcxmlascii = unicode(marcxml, 'ascii', 'ignore').encode('ascii')
-                
+
                 bibid = marcdata.fields[1][0]
                 title = " ".join ([v[1] for v in marcdata.fields [245][0][2]])
 
@@ -221,23 +235,21 @@ def zsearch(request):
                 if len(title)>0:
                     title = t[0].xml_text_content()
                 '''
-                
+
                 # collector.append ((bibid, title))
                 #this is not a good situation but will leave for now
                 #collector.append ((bibid, unicode(title, 'ascii', 'ignore')))
 
                 collector.pop(idx)
-                # collector.insert (idx,(bibid, unicode(title, 'ascii', 'ignore')))
-                collector.insert (idx,(bibid, unicode(title, 'utf-8', 'ignore')))
+                # collector.insert(idx,(bibid, unicode(title,'ascii','ignore')))
+                collector.insert(idx,(bibid, unicode(title, 'utf-8', 'ignore')))
             idx+=1
 
         conn.close ()
-        paginator = Paginator(collector, count) 
+        paginator = Paginator(collector, count)
 
     print("returning...")
     #return g.render('zsearch_results.xhtml', **locals())
     return g.render('zsearch_results.xhtml', paginator=paginator,
                     page_num=page_num,
                     count=count, target=target, tquery=tquery)
-
-
index 586a833..2a27f2c 100644 (file)
@@ -1,7 +1,5 @@
 <?python
 title = _('Search Results')
-instructors = instructor_list
-sites = site_list
 ?>
 <html xmlns="http://www.w3.org/1999/xhtml"
       xmlns:xi="http://www.w3.org/2001/XInclude"
@@ -18,67 +16,47 @@ sites = site_list
 <body>
     <h1>${title}</h1>
     
+    <img py:def="lock(condition=True)" 
+        py:if="condition"
+        src="${ROOT}/static/tango/lock.png"
+        alt="lock" title="This resource is access-controlled."/>
+    
+
     <h2 py:if="query_string">
-        You searched: <i>${query_string}</i>
+        You searched for: <i>${query_string}.</i>
+       Found ${len(results)} matches.
     </h2>
-
-    <!-- not sure if this is the best way to do this -->
-    <!-- 
-        probably need a simple css option to hide instructors and site info
-    -->
-    <table py:if="instructors or sites" width="100%">
-      <tr>
-        <!-- instructors -->
-        <td py:if="instructors" valign="top" class="topbox">
-         <table class="topheading">
-           <tr>
-             <!--
-                 <th>Last Name</th><th>First Name</th>
-             -->
-             <th>Instructors</th>
-           </tr>
-           <tr py:for="instructor in instructors">
-             <td> ${Markup(instructor.instr_name_hl(norm_query))},
-             ${instructor.user.first_name}</td>
-           </tr>
-           <tr py:if="instr_len > count">
-             <!-- will tap into open list here -->
-             <td>(${instr_len - count} more...)</td>
-           </tr>
-         </table>
-        </td>
-       
-        <!-- sites -->
-        <td py:if="sites" valign="top" class="topbox">
-         <table class="topheading">
-           <tr>
-             <th align="left">Site</th>
-           </tr>
-           <tr py:for="site in sites">
-             <!-- will highlight this, probably pull in dept info -->
-             <td><a href="../site/${site.id}/">${site}</a></td>
-           </tr>
-           <tr py:if="site_len > count">
-             <td></td>
-             <!-- will tap into open list here -->
-             <td>(${site_len - count} more...)</td>
-           </tr>
-         </table>
-        </td>
-      </tr>
-    </table>
   
-    <tr py:if="results_len > 0" py:def="pageheader()">
-        <th>Author</th><th>Title</th>
+    <tr py:if="results" py:def="pageheader()">
+        <th>Author</th><th>Title</th><th>Site</th>
     </tr>
-  
+
     <span py:def="pagerow(item)">
+      <tr py:strip="True" py:if="isinstance(item, models.Item)">
+       <?python
+         maybe_lock = list(lock(not item.site.is_open_to(request.user)))
+       ?>
         <td>${Markup(item.author_hl(norm_query))}</td>
-        <td><a href="${item.item_url('meta')}">${Markup(item.title_hl(norm_query))}</a></td>
-       <td><a href="${item.site.site_url()}">${item.site}</a></td>
-       <td><span py:if="item.item_type=='PHYS'">${item.smallint()} &bull; ${item.barcode()}</span></td>
+        <td>${maybe_lock} <a href="${item.item_url('meta')}">${Markup(item.title_hl(norm_query))}</a></td>
+       <td>${maybe_lock} <a href="${item.site.site_url()}">${item.site}</a></td>
+      </tr>
+      <tr py:strip="True" py:if="isinstance(item, models.Site)">
+       <?python
+         maybe_lock = list(lock(not item.is_open_to(request.user)))
+       ?>
+       <td>&mdash;</td>
+       <td>&mdash;</td>
+        <td>${maybe_lock} <a href="${item.site_url()}">${item}</a></td>
+      </tr>
     </span>
     ${pagetable(paginator, count, pagerow, pageheader, query=query_string)}
 
+    <div py:if="user.is_anonymous()">
+      Your searches may return more results if you <a
+      href="${ROOT}/accounts/login/?next=${ROOT}/">log in</a> before
+      searching.
+    </div>
+
+
 </body>
 </html>