[open-ils-commits] r1120 - in servres/trunk/conifer: integration static syrup syrup/views templates templates/admin (gfawcett)

svn at svn.open-ils.org svn at svn.open-ils.org
Mon Dec 27 16:58:13 EST 2010


Author: gfawcett
Date: 2010-12-27 16:58:07 -0500 (Mon, 27 Dec 2010)
New Revision: 1120

Modified:
   servres/trunk/conifer/integration/cas.py
   servres/trunk/conifer/integration/uwindsor.py
   servres/trunk/conifer/static/edit_site.js
   servres/trunk/conifer/syrup/external_groups.py
   servres/trunk/conifer/syrup/integration.py
   servres/trunk/conifer/syrup/models.py
   servres/trunk/conifer/syrup/urls.py
   servres/trunk/conifer/syrup/views/sites.py
   servres/trunk/conifer/templates/admin/index.xhtml
   servres/trunk/conifer/templates/edit_site.xhtml
Log:
on site add/edit, fuzzy-lookup of site owner, if fuzzy-lookup hook is available.

For UWindsor, I'm using an external program called SpeedLookup for the
fuzzy search. If the program isn't found on the system, fuzzy search
will be disabled.

Modified: servres/trunk/conifer/integration/cas.py
===================================================================
--- servres/trunk/conifer/integration/cas.py	2010-12-27 21:58:04 UTC (rev 1119)
+++ servres/trunk/conifer/integration/cas.py	2010-12-27 21:58:07 UTC (rev 1120)
@@ -8,9 +8,10 @@
 #
 # You will probably also want to define two customization hooks:
 # external_person_lookup and user_needs_decoration. See:
-# conifer/syrup/integration.py.
+# conifer/syrup/integration.py and 'maybe_decorate' in
+# conifer/syrup/models.py.
 
-from conifer.plumbing.hooksystem import gethook, callhook
+
 import django_cas.backends
 
 
@@ -20,34 +21,6 @@
         """Authenticates CAS ticket and retrieves user data"""
 
         user = super(CASBackend, self).authenticate(ticket, service)
-        if user and gethook('external_person_lookup'):
-            decorate_user(user)
+        if user:
+            user.maybe_decorate()
         return user
-
-
-# TODO is this really CAS specific? Wouldn't linktool (for example)
-# also need such a decorator?
-
-def decorate_user(user):
-    dectest = gethook('user_needs_decoration', default=_user_needs_decoration)
-    if not dectest(user):
-        return
-
-    dir_entry = callhook('external_person_lookup', user.username)
-    if dir_entry is None:
-        return
-
-    user.first_name = dir_entry['given_name']
-    user.last_name  = dir_entry['surname']
-    user.email      = dir_entry.get('email', user.email)
-    user.save()
-
-    if 'patron_id' in dir_entry:
-        # note, we overrode user.get_profile() to automatically create
-        # missing profiles. See models.py.
-        user.get_profile().ils_userid = dir_entry['patron_id']
-        profile.save()
-
-
-def _user_needs_decoration(user):
-    return user.last_name is not None

Modified: servres/trunk/conifer/integration/uwindsor.py
===================================================================
--- servres/trunk/conifer/integration/uwindsor.py	2010-12-27 21:58:04 UTC (rev 1119)
+++ servres/trunk/conifer/integration/uwindsor.py	2010-12-27 21:58:07 UTC (rev 1120)
@@ -1,18 +1,19 @@
 # See conifer/syrup/integration.py for documentation.
 
-from datetime import date
-from django.conf import settings
 from conifer.libsystems import ezproxy
-from conifer.libsystems.evergreen.support import initialize, E1
 from conifer.libsystems import marcxml as M
 from conifer.libsystems.evergreen import item_status as I
+from conifer.libsystems.evergreen.support import initialize, E1
 from conifer.libsystems.z3950 import pyz3950_search as PZ
+from datetime import date
+from django.conf import settings
+from memoization import memoize
 from xml.etree import ElementTree as ET
+import csv
+import os
 import re
+import subprocess
 import uwindsor_campus_info
-from memoization import memoize
-import csv
-import subprocess
 
 # USE_Z3950: if True, use Z39.50 for catalogue search; if False, use OpenSRF.
 # Don't set this value directly here: rather, if there is a valid Z3950_CONFIG
@@ -42,6 +43,7 @@
     'start-date', 'end-date'), where the dates are instances of the
     datetime.date class.
     """
+    # TODO: make this algorithmic.
     return [
         ('2011S', '2011 Summer', date(2011,5,1), date(2011,9,1)),
         ('2011F', '2011 Fall', date(2011,9,1), date(2011,12,31)),
@@ -210,18 +212,59 @@
     """
     return uwindsor_campus_info.call('person_lookup', userid)
 
-def decode_role(role):
+
+def external_memberships(userid):
+    """
+    Given a userid, return a list of dicts, representing the user's
+    memberships in known external groups. Each dict must include the
+    following key/value pairs:
+    'group': a group-code, externally defined;
+    'role':  the user's role in that group, one of (INSTR, ASSIST, STUDT).
+    """
+    memberships = uwindsor_campus_info.call('membership_ids', userid)
+    for m in memberships:
+        m['role'] = _decode_role(m['role'])
+    return memberships
+
+def _decode_role(role):
     if role == 'Instructor':
         return 'INSTR'
     else:
         return 'STUDT'
 
-def external_memberships(userid, include_titles=False):
-    memberships = uwindsor_campus_info.call('membership_ids', userid)
-    for m in memberships:
-        m['role'] = decode_role(m['role'])
-    return memberships
+FUZZY_LOOKUP_BIN = '/usr/local/bin/SpeedLookup'
 
+if os.path.isfile(FUZZY_LOOKUP_BIN):
+
+    def fuzzy_person_lookup(query, include_students=False):
+        """
+        Given a query, return a list of users who probably match the
+        query. The result is a list of (userid, display), where userid
+        is the campus userid of the person, and display is a string
+        suitable for display in a results-list. Include_students
+        indicates that students, and not just faculty/staff, should be
+        included in the results.
+        """
+
+        cmd = [FUZZY_LOOKUP_BIN, query]
+        if include_students:
+            cmd.append('students')
+
+        p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
+        try:
+            rdr = csv.reader(p.stdout)
+            rdr.next()              # skip header row,
+            data = list(rdr)        # eagerly fetch the rest
+        finally:
+            p.stdout.close()
+
+        out = []
+        for uid, sn, given, role, dept, mail in data:
+            display = '%s %s. %s, %s. <%s>. [%s]' % (given, sn, role, dept, mail, uid)
+            out.append((uid, display))
+        return out
+
+
 #--------------------------------------------------
 # proxy server integration
 

Modified: servres/trunk/conifer/static/edit_site.js
===================================================================
--- servres/trunk/conifer/static/edit_site.js	2010-12-27 21:58:04 UTC (rev 1119)
+++ servres/trunk/conifer/static/edit_site.js	2010-12-27 21:58:07 UTC (rev 1120)
@@ -1,16 +1,78 @@
-function do_init() {
-    if ($('#id_code').attr('tagName') == 'SELECT') {
-	// code is a SELECT, so we add a callback to lookup titles.
-	$('#id_code').change(function() {
-	    $('#id_title')[0].disabled=true;
-	    $.getJSON(ROOT + '/site/new/ajax_title', {course_code: $(this).val()},
-		      function(resp) {
-			  $('#id_title').val(resp.title)
-			  $('#id_title')[0].disabled=false;
+var fuzzyLookup = {
 
-		      });
-	});
-    }
+	lastText: null,
+	lastPress: null,
+	waiting: false,
+	
+	lookup: function() {
+		var text = $(this).val();
+		if (text != fuzzyLookup.lastText) {
+			fuzzyLookup.lastText = text;
+			if (text.length < 3) {
+				return;
+			}
+			fuzzyLookup.lastPress = new Date();
+		}
+	},
+
+	minWait: 500,
+
+	interval: function() {
+		var now = new Date();
+
+		if (fuzzyLookup.lastPress == null || (now - fuzzyLookup.lastPress) < fuzzyLookup.minWait) {
+			return;
+		}
+
+		if (fuzzyLookup.waiting) {
+			return;
+		}
+
+		fuzzyLookup.waiting = true;
+		$('#fuzzyinput').css({backgroundColor: 'yellow'}); // debugging
+		$.post('site_fuzzy_user_lookup', {'q': fuzzyLookup.lastText},
+			   function(data) {
+				   fuzzyLookup.waiting = false;
+				   fuzzyLookup.lastPress = null;
+				   $('#fuzzyinput').css({backgroundColor: 'white'}); // debugging
+				   $('#fuzzypanel').text('');
+				   if (data.results.length == 0) {
+					   $('#fuzzypanel').append('No matches.');
+				   }
+				   $.each(data.results, function(i,val) {
+					   var link = $('<a class="fuzzychoice" href="#"/>');
+					   link.text(val[1]);
+					   link.data('userid', val[0]);
+					   link.data('display', val[1]);
+					   link.click(fuzzyLookup.pick);
+					   $('#fuzzypanel').append(link);
+				   });
+				   if (data.notshown > 0) {
+					   $('#fuzzypanel').append('<div>and ' + data.notshown + ' more.</div>');
+				   }
+			   }, 'json');
+	},
+
+	pick: function(uid) {
+		$('#fuzzyedit').hide();
+		$('#fuzzyview').show();
+
+		var inp = $('#owner');
+		inp.val($(this).data('userid'));
+
+		$('#fuzzyname').text($(this).data('display'));
+	},
+
+	edit: function() {
+		$('#fuzzyview').hide();
+		$('#fuzzyedit').show();
+		$('#fuzzyinput').focus();
+		fuzzyLookup.lastText = $('#fuzzyinput').val();
+		fuzzyLookup.lastPress = new Date(new Date() - 450);
+	}
 }
 
-$(do_init);
+$(function() {
+	$('#fuzzyinput').keyup(fuzzyLookup.lookup);
+	setInterval(fuzzyLookup.interval, 250);
+});

Modified: servres/trunk/conifer/syrup/external_groups.py
===================================================================
--- servres/trunk/conifer/syrup/external_groups.py	2010-12-27 21:58:04 UTC (rev 1119)
+++ servres/trunk/conifer/syrup/external_groups.py	2010-12-27 21:58:07 UTC (rev 1120)
@@ -23,11 +23,11 @@
     # outside of our scope.)
 
     # The 'external_memberships' hook function must return a list of
-    # (groupcode, role) tuples (assuming the hook function has been defined;
-    # otherwise, the hook system will return None). All of our membership
-    # comparisons are based on groupcodes, which internally are stored in the
-    # Group.external_id attribute. We only consider roles if we are adding a
-    # user to a group.
+    # group-membership objects (assuming the hook function has been
+    # defined; otherwise, the hook system will return None). All of
+    # our membership comparisons are based on groupcodes, which
+    # internally are stored in the Group.external_id attribute. We
+    # only consider roles if we are adding a user to a group.
 
     # This design assumes (but does not assert) that each groupcode is
     # associated with exactly zero or one internal Groups. Specifically, you

Modified: servres/trunk/conifer/syrup/integration.py
===================================================================
--- servres/trunk/conifer/syrup/integration.py	2010-12-27 21:58:04 UTC (rev 1119)
+++ servres/trunk/conifer/syrup/integration.py	2010-12-27 21:58:07 UTC (rev 1120)
@@ -110,6 +110,16 @@
     """
 
 @disable
+def external_memberships(userid):
+    """
+    Given a userid, return a list of dicts,
+    representing the user's memberships in known external groups.
+    Each dict must include the following key/value pairs:
+    'group': a group-code, externally defined;
+    'role':  the user's role in that group, one of (INSTR, ASSIST, STUDT).
+    """
+
+ at disable
 def user_needs_decoration(user_obj):
     """
     User objects are sometimes created automatically, with only a

Modified: servres/trunk/conifer/syrup/models.py
===================================================================
--- servres/trunk/conifer/syrup/models.py	2010-12-27 21:58:04 UTC (rev 1119)
+++ servres/trunk/conifer/syrup/models.py	2010-12-27 21:58:07 UTC (rev 1120)
@@ -88,7 +88,40 @@
     def external_memberships(self):
         return callhook('external_memberships', self.username) or []
 
+    def maybe_decorate(self):
+        """
+        If necessary, and if possible, fill in missing personal
+        information about this user from an external diectory.
+        """
 
+        # can we look up users externally?
+        if not gethook('external_person_lookup'):
+            return
+
+        # does this user need decorating?
+        dectest = gethook('user_needs_decoration', 
+                          default=lambda user: user.last_name == '')
+        if not dectest(self):
+            return
+
+        # can we find this user in the external directory?
+        dir_entry = callhook('external_person_lookup', self.username)
+        if dir_entry is None:
+            return
+
+        self.first_name = dir_entry['given_name']
+        self.last_name  = dir_entry['surname']
+        self.email      = dir_entry.get('email', self.email)
+        self.save()
+
+        if 'patron_id' in dir_entry:
+            # note, we overrode user.get_profile() to automatically create
+            # missing profiles. 
+            self.get_profile().ils_userid = dir_entry['patron_id']
+            profile.save()
+
+
+
 for k,v in [(k,v) for k,v in UserExtensionMixin.__dict__.items() \
                 if not k.startswith('_')]:
     setattr(User, k, v)

Modified: servres/trunk/conifer/syrup/urls.py
===================================================================
--- servres/trunk/conifer/syrup/urls.py	2010-12-27 21:58:04 UTC (rev 1119)
+++ servres/trunk/conifer/syrup/urls.py	2010-12-27 21:58:07 UTC (rev 1120)
@@ -33,6 +33,7 @@
     (r'^site/(?P<site_id>\d+)/edit/permission/$', 'edit_site_permissions'),
     (r'^site/(?P<site_id>\d+)/feeds/(?P<feed_type>.*)$', 'site_feeds'),
     (r'^site/(?P<site_id>\d+)/join/$', 'site_join'),
+    (r'^site/.*fuzzy_user_lookup$', 'site_fuzzy_user_lookup'),
     (ITEM_PREFIX + r'$', 'item_detail'),
     (ITEM_PREFIX + r'dl/(?P<filename>.*)$', 'item_download'),
     (ITEM_PREFIX + r'meta$', 'item_metadata'),

Modified: servres/trunk/conifer/syrup/views/sites.py
===================================================================
--- servres/trunk/conifer/syrup/views/sites.py	2010-12-27 21:58:04 UTC (rev 1119)
+++ servres/trunk/conifer/syrup/views/sites.py	2010-12-27 21:58:07 UTC (rev 1120)
@@ -41,13 +41,32 @@
 
 def _add_or_edit_site(request, instance=None):
     is_add = (instance is None)
+    
+    # Are we looking up owners, or selecting them from a fixed list?
+    owner_mode = 'lookup' if gethook('fuzzy_person_lookup') else 'select'
+
     if is_add:
         instance = models.Site()
     if request.method != 'POST':
         form = NewSiteForm(instance=instance)
         return g.render('edit_site.xhtml', **locals())
     else:
-        form = NewSiteForm(request.POST, instance=instance)
+        POST = request.POST.copy() # because we may mutate it.
+        if owner_mode == 'lookup':
+            # then the owner may be a username instead of an ID, and
+            # the user may not exist in the local database.
+            userid = POST.get('owner', '').strip()
+            if userid and not userid.isdigit():
+                try:
+                    user = User.objects.get(username=userid)
+                except User.DoesNotExist:
+                    user = User.objects.create(username=userid)
+                    user.save()
+                    user.maybe_decorate()
+                    user.save()
+                POST['owner'] = user.id
+
+        form = NewSiteForm(POST, instance=instance)
         if not form.is_valid():
             return g.render('edit_site.xhtml', **locals())
         else:
@@ -136,3 +155,15 @@
                                                group=group, role='STUDT')
         mbr.save()
         return HttpResponseRedirect(site.site_url())
+
+
+ at admin_only
+def site_fuzzy_user_lookup(request):
+    query = request.POST.get('q').lower().strip()
+    results = callhook('fuzzy_person_lookup', query) or []
+    limit = 10
+    resp = {'results': results[:limit], 
+            'notshown': max(0, len(results) - limit)}
+    return HttpResponse(simplejson.dumps(resp),
+                        content_type='application/json')
+

Modified: servres/trunk/conifer/templates/admin/index.xhtml
===================================================================
--- servres/trunk/conifer/templates/admin/index.xhtml	2010-12-27 21:58:04 UTC (rev 1119)
+++ servres/trunk/conifer/templates/admin/index.xhtml	2010-12-27 21:58:07 UTC (rev 1120)
@@ -14,6 +14,9 @@
 <body>
   <h1>${title}</h1>
   <div class="itemadd">
+    <ul>
+	  <li><a href="../site/new/">Create a new course site</a></li>
+	</ul>
   <ul>
     <li><a href="terms/">Terms</a></li>
     <li><a href="desks/">Service Desks</a></li>
@@ -27,9 +30,6 @@
   <!--   <li><a href="../zsearch/">Search Z39.50 Targets</a></li> -->
   <!-- </ul> -->
   <ul>
-    <li><a href="../site/new/">Create a new course site</a></li>
-  </ul>
-  <ul>
     <li py:if="gethook('department_course_catalogue')">
       <a href="update_depts_courses">Automatically update departments and courses</a>
     </li>

Modified: servres/trunk/conifer/templates/edit_site.xhtml
===================================================================
--- servres/trunk/conifer/templates/edit_site.xhtml	2010-12-27 21:58:04 UTC (rev 1119)
+++ servres/trunk/conifer/templates/edit_site.xhtml	2010-12-27 21:58:07 UTC (rev 1120)
@@ -3,6 +3,7 @@
     title = _('Site setup')
 else:
     title = _('Create a new site')
+owner = instance.owner if instance.owner_id else None
 ?>
 <html xmlns="http://www.w3.org/1999/xhtml"
       xmlns:xi="http://www.w3.org/2001/XInclude"
@@ -12,6 +13,10 @@
 <head>
   <title>${title}</title>
   <script type="text/javascript" src="${ROOT}/static/edit_site.js"/>
+  <style>
+	.fuzzychoice { display: block; margin: 0.5em 0;  font-size: 90%; }
+	#fuzzyview { display: block; margin: 0.5em 0;  font-size: 90%; }
+  </style>
 </head>
 <body>
   <div py:if="instance.id">${site_banner(instance)}</div>
@@ -21,7 +26,7 @@
       <li py:for="err in nfe">${err}</li>
     </ul>
   </div>
-  <form action="." method="POST">
+  <form action="." method="POST" autocomplete="off">
     <tr py:def="field_row(field, example=None)">
       <th>${field.label}</th>
       <td>
@@ -33,10 +38,34 @@
       <td class="example" py:if="example">e.g., ${example}</td>
     </tr>
     <table class="metadata_table">
-    ${field_row(form.owner)}
+	  <py:if test="owner_mode=='select'">
+	      ${field_row(form.owner)}
+	  </py:if>
+	  <tr py:if="owner_mode=='lookup'">
+		<th>Primary Instructor</th>
+		<td>
+		  <input type="hidden" id="owner" name="owner" value="${form.owner.data}"/>
+		  <div id="fuzzyedit"
+			   style="display: ${'none' if owner else 'block'}">
+			<div style="font-size: 80%; margin: 0.5em 0;">Type a partial name or userid into the box; then select one of the matches.</div>
+			<input type="text" id="fuzzyinput" autocomplete="off" value="${owner.username if owner else ''}"/>
+			<div id="fuzzypanel">
+			</div>
+		  </div>
+		  <div id="fuzzyview" style="display: ${owner and 'block' or 'none'}">
+			<span id="fuzzyname">
+			<span py:if="owner">
+			  ${owner.get_full_name()} [${owner}]
+			</span>
+			</span>
+			<input type="button" value="change" onclick="fuzzyLookup.edit();"
+					 style="margin-left: 1em;"/>
+		  </div>
+		</td>
+	  </tr>
+    ${field_row(form.course)}
     ${field_row(form.start_term)}
     ${field_row(form.end_term)}
-    ${field_row(form.course)}
     ${field_row(form.service_desk)}
 
     <!-- ${field_row(form.department)} -->



More information about the open-ils-commits mailing list