#
# 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
"""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
# 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 xml.etree import ElementTree as ET
-import re
-import uwindsor_campus_info
+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
# 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
'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)),
"""
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
-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);
+});
# 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
"""
@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).
+ """
+
+@disable
def user_needs_decoration(user_obj):
"""
User objects are sometimes created automatically, with only a
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('_')]:
(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'),
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:
group=group, role='STUDT')
mbr.save()
return HttpResponseRedirect(site.site_url())
+
+
+@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')
+
<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>
<!-- <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>
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"
<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>
<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>
<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)} -->