From e6af452341e7d300c534f5cf1d8a264e5fbd15c6 Mon Sep 17 00:00:00 2001 From: gfawcett Date: Thu, 15 Jul 2010 00:55:50 +0000 Subject: [PATCH] simplified 'reserves search' function 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 | Bin 0 -> 630 bytes conifer/syrup/views/search.py | 308 +++++++++++++++++---------------- conifer/templates/search_results.xhtml | 88 ++++------ 3 files changed, 193 insertions(+), 203 deletions(-) create mode 100644 conifer/static/tango/lock.png diff --git a/conifer/static/tango/lock.png b/conifer/static/tango/lock.png new file mode 100644 index 0000000000000000000000000000000000000000..fe735acd1426db42f969e8a32ba135a11e5bee05 GIT binary patch literal 630 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`EX7WqAsj$Z!;#Vf2?p zUk71ECym(^Ktah8*NBqf{Irtt#G+J&^73-M%)IR4Fc9gr9VxE;Q`PT36LpZoj@ZLLNaqx84N7-4Gi^-3=Ay8|1)1;U|>x4 zba4!^=-oSczqeDM$Z>o9`7cF0cwKqP&n&HuGb{Io zUQ#W*5Hm~Y&_AYXwx8Tv=k5D`rcG&;dK*jaL!R&RitV4zd;gT{?DMF0`-}(r?TknA zX7Bthb1`DhA7{CS&X1gbxb0*n^Et2mP~37mXIhHY(KYF8w@(E@^-Ja@Fo>i`c3*XRq;>FITF2bAx$7=V z9y{iydE(Ri260!$h0c59j_Y~eQ0;u~)||Qgx9#=Q&8NEs%vTmWUku-AmP zzbf)6dAbBo4&Un&y?#NqYRT4)YgLP)>Qss*rP&AlW|5c|XmWeOKJVzY3s&-mY;F7| z*ev$`z1)}8dJW(HGx5m1$l|=X>qJ9@i^=pCGrBp%SSK$FSUaVf`OfC-H+es*%1SDm h?q6%$e_{PSh8a=Q(%${*HU&lrgQu&X%Q~loCIDC({|x{D literal 0 HcmV?d00001 diff --git a/conifer/syrup/views/search.py b/conifer/syrup/views/search.py index 6cee7c6..79cc6ed 100644 --- a/conifer/syrup/views/search.py +++ b/conifer/syrup/views/search.py @@ -1,179 +1,192 @@ 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) - - diff --git a/conifer/templates/search_results.xhtml b/conifer/templates/search_results.xhtml index 586a833..2a27f2c 100644 --- a/conifer/templates/search_results.xhtml +++ b/conifer/templates/search_results.xhtml @@ -1,7 +1,5 @@

${title}

+ lock + +

- You searched: ${query_string} + You searched for: ${query_string}. + Found ${len(results)} matches.

- - - - - - - - - - - -
- - - - - - - - - - - - -
Instructors
${Markup(instructor.instr_name_hl(norm_query))}, - ${instructor.user.first_name}
(${instr_len - count} more...)
-
- - - - - - - - - - - - - -
Site
${site}
(${site_len - count} more...)
-
- - AuthorTitle + + AuthorTitleSite - + + + ${Markup(item.author_hl(norm_query))} - ${Markup(item.title_hl(norm_query))} - ${item.site} - ${item.smallint()} • ${item.barcode()} + ${maybe_lock} ${Markup(item.title_hl(norm_query))} + ${maybe_lock} ${item.site} + + + + — + — + ${maybe_lock} ${item} + ${pagetable(paginator, count, pagerow, pageheader, query=query_string)} +
+ Your searches may return more results if you log in before + searching. +
+ + -- 2.11.0