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']
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
# 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]])
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)
-
-
<?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"
<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()} • ${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>—</td>
+ <td>—</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>