[open-ils-commits] r335 - in servres/trunk/conifer/syrup: . views (gfawcett)

svn at svn.open-ils.org svn at svn.open-ils.org
Wed Apr 15 20:09:03 EDT 2009


Author: gfawcett
Date: 2009-04-15 20:09:02 -0400 (Wed, 15 Apr 2009)
New Revision: 335

Added:
   servres/trunk/conifer/syrup/views/
   servres/trunk/conifer/syrup/views/README-VIEWS
   servres/trunk/conifer/syrup/views/__init__.py
   servres/trunk/conifer/syrup/views/_common.py
   servres/trunk/conifer/syrup/views/admin.py
   servres/trunk/conifer/syrup/views/courses.py
   servres/trunk/conifer/syrup/views/feeds.py
   servres/trunk/conifer/syrup/views/general.py
   servres/trunk/conifer/syrup/views/generics.py
   servres/trunk/conifer/syrup/views/items.py
   servres/trunk/conifer/syrup/views/search.py
Removed:
   servres/trunk/conifer/syrup/generics.py
   servres/trunk/conifer/syrup/views.py
Log:
broke up our enormous views.py to several views/* submodules.

See the new README-VIEWS file for a quick overview.


Deleted: servres/trunk/conifer/syrup/generics.py
===================================================================
--- servres/trunk/conifer/syrup/generics.py	2009-04-16 00:06:14 UTC (rev 334)
+++ servres/trunk/conifer/syrup/generics.py	2009-04-16 00:09:02 UTC (rev 335)
@@ -1,55 +0,0 @@
-import conifer.genshi_support as g
-from django.http import HttpResponse, HttpResponseRedirect
-from django.http import HttpResponseForbidden
-from django.shortcuts import get_object_or_404
-from django.forms import ModelForm, ValidationError
-
-
-def generic_handler(form, decorator=lambda x: x):
-    def handler(request, obj_id=None, action=None):
-        if obj_id is None and action is None:
-            return generic_index(form)
-        elif action is None:
-            return generic_edit(form, request, obj_id)
-        elif action == 'delete':
-            return generic_delete(form, request, obj_id)
-    return decorator(handler)
-
-
-def generic_index(form):
-    assert hasattr(form, 'Index')
-    return g.render('generic/index.xhtml', form=form)
-
-def generic_edit(form, request, obj_id):
-    if obj_id == '0':
-        instance = None
-    else:
-        instance = get_object_or_404(form.Meta.model, pk=obj_id)
-    if request.method != 'POST':
-        form = form(instance=instance)
-        return g.render('generic/edit.xhtml', **locals())
-    else:
-        form = form(request.POST, instance=instance)
-        if not form.is_valid():
-            return g.render('generic/edit.xhtml', **locals())
-        else:
-            form.save()
-            return HttpResponseRedirect('../')
-
-def generic_delete(form, request, obj_id):
-    instance = get_object_or_404(form.Meta.model, pk=obj_id)
-    if request.method != 'POST':
-        form = form(instance=instance)
-        return g.render('generic/delete.xhtml', **locals())
-    else:
-        instance.delete()
-        return HttpResponseRedirect('../')
-
-
-def strip_and_nonblank(fieldname):
-    def clean(self):
-        v = self.cleaned_data.get(fieldname) or ''
-        if not v.strip():
-            raise ValidationError('Cannot be blank.')
-        return v.strip()
-    return clean

Added: servres/trunk/conifer/syrup/views/README-VIEWS
===================================================================
--- servres/trunk/conifer/syrup/views/README-VIEWS	                        (rev 0)
+++ servres/trunk/conifer/syrup/views/README-VIEWS	2009-04-16 00:09:02 UTC (rev 335)
@@ -0,0 +1,10 @@
+Just a quick note on the directory structure here. 
+
+views/__init__.py is the equivalent of 'views.py' from Django's
+perspective.
+
+__init__.py loads each of the submodules, which represent the various
+components of the user interface.
+
+_common.py is a common set of functions which are used by many of the
+other modules. When in doubt, put your imports in _common.py.

Added: servres/trunk/conifer/syrup/views/__init__.py
===================================================================
--- servres/trunk/conifer/syrup/views/__init__.py	                        (rev 0)
+++ servres/trunk/conifer/syrup/views/__init__.py	2009-04-16 00:09:02 UTC (rev 335)
@@ -0,0 +1,6 @@
+from general import *
+from courses import *
+from items import *
+from search import *
+from admin import *
+from feeds import *

Added: servres/trunk/conifer/syrup/views/_common.py
===================================================================
--- servres/trunk/conifer/syrup/views/_common.py	                        (rev 0)
+++ servres/trunk/conifer/syrup/views/_common.py	2009-04-16 00:09:02 UTC (rev 335)
@@ -0,0 +1,218 @@
+#-----------------------------------------------------------------------------
+# todo: break this up. It's getting long. I think we should have
+# something like:
+#
+#   views/__init__.py                     # which imports:
+#   views/course_site_handlers.py
+#   views/search_stuff.py
+#   views/add_edit_course.py
+#   ...
+#   views/common_imports.py              # imported by all.
+#
+# though these are just examples. Everything in views/* would include
+# 'from common_imports import *' just to keep the imports
+# tidy. Views/__init__ would import all the other bits: that ought to
+# satisfy Django.
+
+import warnings
+from conifer.syrup import models
+from datetime import datetime
+from django.contrib.auth import authenticate, login, logout
+from django.contrib.auth.decorators import login_required
+from django.contrib.auth.models import User, SiteProfileNotAvailable
+from django.core.paginator import Paginator
+from django.db.models import Q
+from django.http import HttpResponse, HttpResponseRedirect, HttpResponseNotFound
+from django.http import HttpResponseForbidden
+from django.shortcuts import get_object_or_404
+from django.utils import simplejson
+from generics import *
+#from gettext import gettext as _ # fixme, is this the right function to import?
+from django.utils.translation import ugettext as _
+import conifer.genshi_support as g
+import django.forms
+import re
+import sys
+from django.forms.models import modelformset_factory
+from conifer.custom import lib_integration
+from conifer.libsystems.z3950.marcxml import marcxml_to_dictionary, marcxml_dictionary_to_dc
+from conifer.syrup.fuzzy_match import rank_pending_items
+from django.core.urlresolvers import reverse
+
+#-----------------------------------------------------------------------------
+# Z39.50 Support
+#
+# This is experimental at this time, and requires some tricky Python
+# imports as far as I can tell. For that reason, let's keep the Z39.50
+# support optional for now. If you have Ply and PyZ3950, we'll load
+# and use it; if not, no worries, everything else will workk.
+
+try:
+    # Graham needs this import hackery to get PyZ3950 working. Presumably
+    # Art can 'import profile; import lex', so this hack won't run for
+    # him.
+    try:
+        import profile
+        import lex
+        import yacc
+    except ImportError:
+        sys.modules['profile'] = sys # just get something called 'profile';
+                                     # it's not actually used.
+        import ply.lex              
+        import ply.yacc             # pyz3950 thinks these are toplevel modules.
+        sys.modules['lex'] = ply.lex
+        sys.modules['yacc'] = ply.yacc
+
+    # for Z39.50 support, not sure whether this is the way to go yet but
+    # as generic as it gets
+    from PyZ3950 import zoom, zmarc
+except:
+    warnings.warn('Could not load Z39.50 support.')
+
+#-----------------------------------------------------------------------------
+# poor-man's logging. Not sure we need more yet.
+
+def log(level, msg):
+    print >> sys.stderr, '[%s] %s: %s' % (datetime.now(), level.upper(), msg)
+
+#-----------------------------------------------------------------------------
+# Authentication
+
+def auth_handler(request, path):
+    default_url = reverse(welcome) #request.META['SCRIPT_NAME'] + '/'
+    if path == 'login/':
+        if request.method == 'GET':
+            next=request.GET.get('next', default_url)
+            if request.user.is_authenticated():
+                return HttpResponseRedirect(next)
+            else:
+                return g.render('auth/login.xhtml', 
+                                next=request.GET.get('next'))
+        else:
+            userid, password = request.POST['userid'], request.POST['password']
+            next = request.POST['next']
+            user = authenticate(username=userid, password=password)
+            def _error_page(msg):
+                return g.render('auth/login.xhtml', err=msg, next=next)
+            if user is None:
+                return _error_page(
+                    _('Invalid username or password. Please try again.'))
+            elif not user.is_active:
+                return _error_age(
+                    _('Sorry, this account has been disabled.'))
+            else:
+                login(request, user)
+                # initialize the profile if it doesn't exist.
+                try:
+                    user.get_profile()
+                except models.UserProfile.DoesNotExist:
+                    profile = models.UserProfile.objects.create(user=user)
+                    profile.save()
+                return HttpResponseRedirect(request.POST.get('next', default_url))
+    elif path == 'logout':
+        logout(request)
+        return HttpResponseRedirect(default_url)
+    else:
+        return HttpResponse('auth_handler: ' + path)
+
+#-----------------------------------------------------------------------------
+# Authorization
+
+def _fast_user_membership_query(user_id, course_id, where=None):
+    # I use a raw SQL query here because I want the lookup to be as
+    # fast as possible. Caching would help too, but let's try this
+    # first. (todo, review later.)
+    query = ('select count(*) from syrup_member '
+             'where user_id=%s and course_id=%s ')
+    if where:
+        query += (' and ' + where)
+    cursor = django.db.connection.cursor()
+    cursor.execute(query, [user_id, int(course_id)])
+    res = cursor.fetchall()
+    cursor.close()
+    allowed = bool(res[0][0])
+    return allowed
+    
+def _access_denied(request, message):
+    if request.user.is_anonymous():
+        # then take them to login screen....
+        dest = request.META['SCRIPT_NAME'] + '/accounts/login/?next=' + request.META['PATH_INFO']
+        return HttpResponseRedirect(dest)
+    else:
+        return simple_message(_('Access denied.'), message,
+                              _django_type=HttpResponseForbidden)
+
+# todo, these decorators could be refactored.
+
+# decorator
+def instructors_only(handler):
+    def hdlr(request, course_id, *args, **kwargs):
+        allowed = request.user.is_superuser
+        if not allowed:
+            allowed = _fast_user_membership_query(
+                request.user.id, course_id, "role in ('INSTR','PROXY')")
+        if allowed:
+            return handler(request, course_id, *args, **kwargs)
+        else:
+            return _access_denied(request, _('Only instructors are allowed here.'))
+    return hdlr
+
+# decorator
+def members_only(handler):
+    def hdlr(request, course_id, *args, **kwargs):
+        user = request.user
+        allowed = user.is_superuser
+        if not allowed:
+            course = models.Course.objects.get(pk=course_id)
+            allowed = ((user.is_anonymous() and course.access=='ANON') or \
+                       (user.is_authenticated() and course.access=='LOGIN'))
+        if not allowed:
+            allowed = _fast_user_membership_query(user.id, course_id)
+        if allowed:
+            return handler(request, course_id, *args, **kwargs)
+        else:
+            if course.access=='LOGIN':
+                msg = _('Please log in, so that you can enter this site.')
+            else:
+                msg = _('Only course members are allowed here.')
+            return _access_denied(request, msg)
+    return hdlr
+
+# decorator
+def admin_only(handler):
+    # fixme, 'admin' is vaguely defined for now as anyone who is
+    # 'staff', i.e. who has access to the Django admin interface.
+    def hdlr(request, *args, **kwargs):
+        allowed = request.user.is_staff
+        if allowed:
+            return handler(request, *args, **kwargs)
+        else:
+            return _access_denied(request, _('Only administrators are allowed here.'))
+    return hdlr
+
+#decorator
+def public(handler):
+    # A no-op! Just here to be used to explicitly decorate methods
+    # that are supposed to be public.
+    return handler
+
+#-----------------------------------------------------------------------------
+# Simple Message: just a quick title-and-message web page.
+
+def simple_message(title, content, go_back=True, **kwargs):
+    kwargs.update(**locals())
+    return g.render('simplemessage.xhtml', **kwargs)
+
+
+#------------------------------------------------------------
+
+def custom_500_handler(request):
+    cls, inst, tb = sys.exc_info()
+    msg = simple_message(_('Error: %s') % repr(inst),
+                         repr((request.__dict__, inst)))
+    return HttpResponse(msg._container, status=501)
+
+def custom_400_handler(request):
+    msg = simple_message(_('Not found'), 
+                          _('The page you requested could not be found'))
+    return HttpResponse(msg._container, status=404)

Added: servres/trunk/conifer/syrup/views/admin.py
===================================================================
--- servres/trunk/conifer/syrup/views/admin.py	                        (rev 0)
+++ servres/trunk/conifer/syrup/views/admin.py	2009-04-16 00:09:02 UTC (rev 335)
@@ -0,0 +1,86 @@
+from _common import *
+from django.utils.translation import ugettext as _
+
+#-----------------------------------------------------------------------------
+# Administrative options
+
+ at admin_only
+def admin_index(request):
+    return g.render('admin/index.xhtml')
+
+
+class TermForm(ModelForm):
+    class Meta:
+        model = models.Term
+
+    class Index:
+        title = _('Terms')
+        all   = models.Term.objects.order_by('start', 'code').all
+        cols  = ['code', 'name', 'start', 'finish']
+        links = [0,1]
+
+    clean_name = strip_and_nonblank('name')
+    clean_code = strip_and_nonblank('code')
+
+    def clean(self):
+        cd = self.cleaned_data
+        s, f = cd.get('start'), cd.get('finish')
+        if (s and f) and s >= f:
+            raise ValidationError, _('start must precede finish')
+        return cd
+
+admin_terms = generic_handler(TermForm, decorator=admin_only)
+
+
+class DeptForm(ModelForm):
+    class Meta:
+        model = models.Department
+
+    class Index:
+        title = _('Departments')
+        all   = models.Department.objects.order_by('abbreviation').all
+        cols  = ['abbreviation', 'name']
+        links = [0,1]
+
+    clean_abbreviation = strip_and_nonblank('abbreviation')
+    clean_name = strip_and_nonblank('name')
+
+admin_depts = generic_handler(DeptForm, decorator=admin_only)
+
+###
+# graham - zap this if it messes anything up :-)
+###
+class TargetForm(ModelForm):
+    class Meta:
+        model = models.Target
+
+    class Index:
+        title = _('Targets')
+        all   = models.Target.objects.order_by('name').all
+        cols  = ['name', 'host']
+        links = [0,1]
+
+    clean_name = strip_and_nonblank('name')
+    clean_host = strip_and_nonblank('host')
+
+admin_targets = generic_handler(TargetForm, decorator=admin_only)
+###
+
+
+class NewsForm(ModelForm):
+    class Meta:
+        model = models.NewsItem
+
+    class Index:
+        title = _('News Items')
+        all   = models.NewsItem.objects.order_by('-id').all
+        cols  = ['id', 'subject', 'published']
+        links = [0, 1]
+
+    clean_subject = strip_and_nonblank('subject')
+    clean_body = strip_and_nonblank('body')
+
+admin_news = generic_handler(NewsForm, decorator=admin_only)
+
+
+

Added: servres/trunk/conifer/syrup/views/courses.py
===================================================================
--- servres/trunk/conifer/syrup/views/courses.py	                        (rev 0)
+++ servres/trunk/conifer/syrup/views/courses.py	2009-04-16 00:09:02 UTC (rev 335)
@@ -0,0 +1,242 @@
+from _common import *
+from django.utils.translation import ugettext as _
+from search import *
+
+#-----------------------------------------------------------------------------
+# Creating a new course
+
+class NewCourseForm(ModelForm):
+    class Meta:
+        model = models.Course
+        exclude = ('passkey','access')
+
+    def clean_code(self):
+        v = (self.cleaned_data.get('code') or '').strip()
+        is_valid_func = models.course_codes.course_code_is_valid
+        if (not is_valid_func) or is_valid_func(v):
+            return v
+        else:
+            raise ValidationError, _('invalid course code')
+
+# if we have course-code lookup, hack lookup support into the new-course form.
+
+COURSE_CODE_LIST = bool(models.course_codes.course_code_list)
+COURSE_CODE_LOOKUP_TITLE = bool(models.course_codes.course_code_lookup_title)
+
+if COURSE_CODE_LIST:
+    from django.forms import Select
+    course_list = models.course_codes.course_code_list()
+    choices = [(a,a) for a in course_list]
+    choices.sort()
+    empty_label = u'---------'
+    choices.insert(0, ('', empty_label))
+    NewCourseForm.base_fields['code'].widget = Select(
+        choices = choices)
+    NewCourseForm.base_fields['code'].empty_label = empty_label
+
+#--------------------
+    
+ at login_required
+def add_new_course(request):
+    if not request.user.has_perm('add_course'):
+        return _access_denied(_('You are not allowed to create course sites.'))
+    return _add_or_edit_course(request)
+
+ at instructors_only
+def edit_course(request, course_id):
+    instance = get_object_or_404(models.Course, pk=course_id)
+    return _add_or_edit_course(request, instance=instance)
+    
+def _add_or_edit_course(request, instance=None):
+    is_add = (instance is None)
+    if is_add:
+        instance = models.Course()
+    current_access_level = not is_add and instance.access or None
+    example = models.course_codes.course_code_example
+    if request.method != 'POST':
+        form = NewCourseForm(instance=instance)
+        return g.render('edit_course.xhtml', **locals())
+    else:
+        form = NewCourseForm(request.POST, instance=instance)
+        if not form.is_valid():
+            return g.render('edit_course.xhtml', **locals())
+        else:
+            form.save()
+            course = form.instance
+            if course.access == u'INVIT' and not course.passkey:
+                course.generate_new_passkey()
+                course.save()
+            assert course.id
+            user_in_course = models.Member.objects.filter(user=request.user,course=course)
+            if not user_in_course: # for edits, might already be!
+                mbr = course.member_set.create(user=request.user, role='INSTR')
+                mbr.save()
+            
+            if is_add or (current_access_level != course.access):
+                # we need to configure permissions.
+                return HttpResponseRedirect(course.course_url('edit/permission/'))
+            else:
+                return HttpResponseRedirect('../') # back to main view.
+
+# no access-control needed to protect title lookup.
+def add_new_course_ajax_title(request):
+    course_code = request.GET['course_code']
+    title = models.course_codes.course_code_lookup_title(course_code)
+    return HttpResponse(simplejson.dumps({'title':title}))
+
+ at instructors_only
+def edit_course_permissions(request, course_id):
+    course = get_object_or_404(models.Course, pk=course_id)
+    # choices: make the access-choice labels more personalized than
+    # the ones in 'models'.
+    choices = [
+        # note: I'm leaving ANON out for now, until we discuss it further.
+        (u'CLOSE', _(u'No students: this site is closed.')),
+        (u'STUDT', _(u'Students in my course -- I will provide section numbers')),
+        (u'INVIT', _(u'Students in my course -- I will share an Invitation Code with them')),
+        (u'LOGIN', _(u'All Reserves patrons'))]
+    if models.course_sections.sections_tuple_delimiter is None:
+        # no course-sections support? Then STUDT cannot be an option.
+        del choices[1]
+    choose_access = django.forms.Select(choices=choices)
+        
+    if request.method != 'POST':
+        return g.render('edit_course_permissions.xhtml', **locals())
+    else:
+        POST = request.POST
+
+        if 'action_change_code' in POST:
+            # update invitation code -------------------------------------
+            course.generate_new_passkey()
+            course.access = u'INVIT'
+            course.save()
+            return HttpResponseRedirect('.#student_access')
+
+        elif 'action_save_instructor' in POST:
+            # update instructor details ----------------------------------
+            iname = POST.get('new_instructor_name','').strip()
+            irole = POST.get('new_instructor_role')
+
+            def get_record_for(username):
+                instr = models.maybe_initialize_user(iname)
+                if instr:
+                    try:
+                        return models.Member.objects.get(user=instr, course=course)
+                    except models.Member.DoesNotExist:
+                        return models.Member.objects.create(user=instr, course=course)
+
+            # add a new instructor
+            if iname:
+                instr = get_record_for(iname)
+                if instr:       # else? should have an error.
+                    instr.role = irole
+                    instr.save()
+                else:
+                    instructor_error = 'No such user: %s' % iname
+                    return g.render('edit_course_permissions.xhtml', **locals())
+
+
+            # removing and changing roles of instructors
+            to_change_role = [(int(name.rsplit('_', 1)[-1]), POST[name]) \
+                                  for name in POST if name.startswith('instructor_role_')]
+            to_remove = [int(name.rsplit('_', 1)[-1]) \
+                             for name in POST if name.startswith('instructor_remove_')]
+            for instr_id, newrole in to_change_role:
+                if not instr_id in to_remove:
+                    instr = models.Member.objects.get(pk=instr_id, course=course)
+                    instr.role = newrole
+                    instr.save()
+            for instr_id in to_remove:
+                # todo, should warn if deleting yourself!
+                instr = models.Member.objects.get(pk=instr_id, course=course)
+                instr.delete()
+            # todo, should have some error-reporting.
+            return HttpResponseRedirect('.')
+
+        elif 'action_save_student' in POST:
+            # update student details ------------------------------------
+            access = POST.get('access')
+            course.access = access
+            # drop all provided users. fixme, this could be optimized to do add/drops.
+            models.Member.objects.filter(course=course, provided=True).delete()
+            if course.access == u'STUDT':
+                initial_sections = course.sections()
+                # add the 'new section' if any
+                new_sec = request.POST.get('add_section')
+                new_sec = models.section_decode_safe(new_sec)
+                if new_sec:
+                    course.add_sections(new_sec)
+                # remove the sections to be dropped
+                to_remove = [models.section_decode_safe(name.rsplit('_',1)[1]) \
+                                 for name in POST \
+                                 if name.startswith('remove_section_')]
+                course.drop_sections(*to_remove)
+                student_names = models.course_sections.students_in(*course.sections())
+                for name in student_names:
+                    user = models.maybe_initialize_user(name)
+                    if user:
+                        if not models.Member.objects.filter(course=course, user=user):
+                            mbr = models.Member.objects.create(
+                                course=course, user=user, 
+                                role='STUDT', provided=True)
+                            mbr.save()
+            else:
+                pass
+            course.save()
+            return HttpResponseRedirect('.#student_access')
+
+ at instructors_only
+def delete_course(request, course_id):
+    course = get_object_or_404(models.Course, pk=course_id)
+    if request.POST.get('confirm_delete'):
+        course.delete()
+        return HttpResponseRedirect(reverse('my_courses'))
+    else:
+        return HttpResponseRedirect('../')
+
+#-----------------------------------------------------------------------------
+# Course Invitation Code handler
+
+ at login_required                 # must be, to avoid/audit brute force attacks.
+def course_invitation(request):
+    if request.method != 'POST':
+        return g.render('course_invitation.xhtml', code='', error='',
+                        **locals())
+    else:
+        code = request.POST.get('code', '').strip()
+        # todo, a pluggable passkey implementation would normalize the code here.
+        if not code:
+            return HttpResponseRedirect('.')
+        try:
+            # note, we only allow the passkey if access='INVIT'.
+            crs = models.Course.objects.filter(access='INVIT').get(passkey=code)
+        except models.Course.DoesNotExist:
+            # todo, do we need a formal logging system? Or a table for
+            # invitation failures? They should be captured somehow, I
+            # think. Should we temporarily disable accounts after
+            # multiple failures?
+            log('WARN', 'Invitation failure, user %r gave code %r' % \
+                (datetime.now(), request.user.username, code))
+            error = _('The code you provided is not valid.')
+            return g.render('course_invitation.xhtml', **locals())
+
+        # the passkey is good; add the user if not already a member.
+        if not models.Member.objects.filter(user=request.user, course=crs):
+            mbr = models.Member.objects.create(user=request.user, course=crs, 
+                                               role='STUDT')
+            mbr.save()
+        return HttpResponseRedirect(crs.course_url())
+
+#-----------------------------------------------------------------------------
+# Course-instance handlers
+
+ at members_only
+def course_detail(request, course_id):
+    course = get_object_or_404(models.Course, pk=course_id)
+    return g.render('course_detail.xhtml', course=course)
+
+ at members_only
+def course_search(request, course_id):
+    course = get_object_or_404(models.Course, pk=course_id)
+    return search(request, in_course=course)
+

Added: servres/trunk/conifer/syrup/views/feeds.py
===================================================================
--- servres/trunk/conifer/syrup/views/feeds.py	                        (rev 0)
+++ servres/trunk/conifer/syrup/views/feeds.py	2009-04-16 00:09:02 UTC (rev 335)
@@ -0,0 +1,47 @@
+from _common import *
+from django.utils.translation import ugettext as _
+
+#-----------------------------------------------------------------------------
+# Course feeds
+
+ at public                         # and proud of it!
+def course_feeds(request, course_id, feed_type):
+    course = get_object_or_404(models.Course, pk=course_id)
+    if feed_type == '':
+        return g.render('feeds/course_feed_index.xhtml', 
+                        course=course)
+    else:
+        items = course.items()
+        def render_title(item):
+            return item.title
+        if feed_type == 'top-level':
+            items = items.filter(parent_heading=None).order_by('-sort_order')
+        elif feed_type == 'recent-changes':
+            items = items.order_by('-last_modified')
+        elif feed_type == 'tree':
+            def flatten(nodes, acc):
+                for node in nodes:
+                    item, kids = node
+                    acc.append(item)
+                    flatten(kids, acc)
+                return acc
+            items = flatten(course.item_tree(), [])
+            def render_title(item):
+                if item.parent_heading:
+                    return '%s :: %s' % (item.parent_heading.title, item.title)
+                else:
+                    return item.title
+        lastmod = items and max(i.last_modified for i in items) or datetime.now()
+        resp = g.render('feeds/course_atom.xml',
+                        course=course,
+                        feed_type=feed_type,
+                        lastmod=lastmod,
+                        render_title=render_title,
+                        items=items,
+                        root='http://%s' % request.get_host(),
+                        _serialization='xml')
+        resp['Content-Type'] = 'application/atom+xml'
+        return resp
+
+
+

Added: servres/trunk/conifer/syrup/views/general.py
===================================================================
--- servres/trunk/conifer/syrup/views/general.py	                        (rev 0)
+++ servres/trunk/conifer/syrup/views/general.py	2009-04-16 00:09:02 UTC (rev 335)
@@ -0,0 +1,139 @@
+from _common import *
+from django.utils.translation import ugettext as _
+
+#-----------------------------------------------------------------------------
+
+def welcome(request):
+    return g.render('welcome.xhtml')
+
+# MARK: propose we get rid of this. We already have a 'Courses' browser.
+def open_courses(request):
+    page_num = int(request.GET.get('page', 1))
+    count = int(request.GET.get('count', 5))
+    paginator = Paginator(models.Course.objects.all(), count) # fixme, what filter?
+    return g.render('open_courses.xhtml', paginator=paginator,
+                    page_num=page_num,
+                    count=count)
+# MARK: propose we drop this too. We have a browse.
+def instructors(request):
+    page_num = int(request.GET.get('page', 1))
+    count = int(request.GET.get('count', 5))
+    action = request.GET.get('action', 'browse')
+    if action == 'join':
+        paginator = Paginator(models.User.active_instructors(), count)
+    elif action == 'drop':
+        paginator = Paginator(models.Course.objects.all(), count) # fixme, what filter?
+    else:
+        paginator = Paginator(models.Course.objects.all(), count) # fixme, what filter?
+        
+    return g.render('instructors.xhtml', paginator=paginator,
+                    page_num=page_num,
+                    count=count)
+
+# MARK: propose we get rid of this. We have browse.
+def departments(request):
+    raise NotImplementedError
+
+
+def user_prefs(request):
+    if request.method != 'POST':
+        return g.render('prefs.xhtml')
+    else:
+        profile = request.user.get_profile()
+        profile.wants_email_notices = bool(request.POST.get('wants_email_notices'))
+        profile.save()
+        return HttpResponseRedirect('../')
+
+def z3950_test(request):
+    #testing JZKitZ3950 - it seems to work, but i have a character set problem
+    #with the returned marc
+    #nope - the problem is weak mapping with the limited solr test set
+    #i think this can be sorted out
+
+    #conn = zoom.Connection ('z3950.loc.gov', 7090)
+    #conn = zoom.Connection ('webvoy.uwindsor.ca', 9000)
+    #solr index with JZKitZ3950 wrapping
+    conn = zoom.Connection ('127.0.0.1', 2100)
+    # conn = zoom.Connection ('127.0.0.1', 2100)
+    print("connecting...")
+    conn.databaseName = 'Test'
+    # conn.preferredRecordSyntax = 'XML'
+    conn.preferredRecordSyntax = 'USMARC'
+    query = zoom.Query ('CCL', 'ti="agar"')
+    res = conn.search (query)
+    collector = []
+    # if we wanted to get into funkiness
+    m = zmarc.MARC8_to_Unicode ()
+    for r in res:
+        print(type(r.data))
+        print(type(m.translate(r.data)))
+        rec = zmarc.MARC (r.data, strict=0)
+        # rec = zmarc.MARC (rec, strict=0)
+        collector.append(str(rec))
+
+    conn.close ()
+    res_str = "" . join(collector)
+    return g.render('z3950_test.xhtml', res_str=res_str)
+
+def browse(request, browse_option=''):
+    #the defaults should be moved into a config file or something...
+    page_num = int(request.GET.get('page', 1))
+    count    = int(request.GET.get('count', 5))
+
+    if browse_option == '':
+        queryset = None
+        template = 'browse_index.xhtml'
+    elif browse_option == 'instructors':
+        queryset = models.User.active_instructors()
+        template = 'instructors.xhtml'
+    elif browse_option == 'departments':
+        queryset = models.Department.objects.filter(active=True)
+        template = 'departments.xhtml'
+    elif browse_option == 'courses':
+        # fixme, course filter should not be (active=True) but based on user identity.
+        queryset = models.Course.objects.all()
+        template = 'courses.xhtml'
+
+    paginator = queryset and Paginator(queryset, count) or None # index has no queryset.
+    return g.render(template, paginator=paginator,
+                    page_num=page_num,
+                    count=count)
+
+ at login_required
+def my_courses(request):
+    return g.render('my_courses.xhtml')
+
+def instructor_detail(request, instructor_id):
+    page_num = int(request.GET.get('page', 1))
+    count = int(request.GET.get('count', 5))
+    '''
+    i am not sure this is the best way to go from instructor
+    to course
+    '''
+    courses = models.Course.objects.filter(member__user=instructor_id,
+                                           member__role='INSTR')
+    paginator = Paginator(courses.order_by('title'), count)
+
+    '''
+    no concept of active right now, maybe suppressed is a better
+    description anyway?
+    '''
+        # filter(active=True).order_by('title'), count)
+    instructor = models.User.objects.get(pk=instructor_id)
+    return g.render('courses.xhtml', 
+                    custom_title=_('Courses taught by %s') % instructor.get_full_name(),
+                    paginator=paginator,
+                    page_num=page_num,
+                    count=count)
+
+def department_detail(request, department_id):
+    page_num = int(request.GET.get('page', 1))
+    count = int(request.GET.get('count', 5))
+    paginator = Paginator(models.Course.objects.
+        filter(department__id=department_id).
+        filter(active=True).order_by('title'), count)
+
+    return g.render('courses.xhtml', paginator=paginator,
+            page_num=page_num,
+            count=count)
+

Copied: servres/trunk/conifer/syrup/views/generics.py (from rev 334, servres/trunk/conifer/syrup/generics.py)
===================================================================
--- servres/trunk/conifer/syrup/views/generics.py	                        (rev 0)
+++ servres/trunk/conifer/syrup/views/generics.py	2009-04-16 00:09:02 UTC (rev 335)
@@ -0,0 +1,55 @@
+import conifer.genshi_support as g
+from django.http import HttpResponse, HttpResponseRedirect
+from django.http import HttpResponseForbidden
+from django.shortcuts import get_object_or_404
+from django.forms import ModelForm, ValidationError
+
+
+def generic_handler(form, decorator=lambda x: x):
+    def handler(request, obj_id=None, action=None):
+        if obj_id is None and action is None:
+            return generic_index(form)
+        elif action is None:
+            return generic_edit(form, request, obj_id)
+        elif action == 'delete':
+            return generic_delete(form, request, obj_id)
+    return decorator(handler)
+
+
+def generic_index(form):
+    assert hasattr(form, 'Index')
+    return g.render('generic/index.xhtml', form=form)
+
+def generic_edit(form, request, obj_id):
+    if obj_id == '0':
+        instance = None
+    else:
+        instance = get_object_or_404(form.Meta.model, pk=obj_id)
+    if request.method != 'POST':
+        form = form(instance=instance)
+        return g.render('generic/edit.xhtml', **locals())
+    else:
+        form = form(request.POST, instance=instance)
+        if not form.is_valid():
+            return g.render('generic/edit.xhtml', **locals())
+        else:
+            form.save()
+            return HttpResponseRedirect('../')
+
+def generic_delete(form, request, obj_id):
+    instance = get_object_or_404(form.Meta.model, pk=obj_id)
+    if request.method != 'POST':
+        form = form(instance=instance)
+        return g.render('generic/delete.xhtml', **locals())
+    else:
+        instance.delete()
+        return HttpResponseRedirect('../')
+
+
+def strip_and_nonblank(fieldname):
+    def clean(self):
+        v = self.cleaned_data.get(fieldname) or ''
+        if not v.strip():
+            raise ValidationError('Cannot be blank.')
+        return v.strip()
+    return clean

Added: servres/trunk/conifer/syrup/views/items.py
===================================================================
--- servres/trunk/conifer/syrup/views/items.py	                        (rev 0)
+++ servres/trunk/conifer/syrup/views/items.py	2009-04-16 00:09:02 UTC (rev 335)
@@ -0,0 +1,459 @@
+from _common import *
+from django.utils.translation import ugettext as _
+
+ at members_only
+def item_detail(request, course_id, item_id):
+    """Display an item (however that makes sense).""" 
+    # really, displaying an item will vary based on what type of item
+    # it is -- e.g. a URL item would redirect to the target URL. I'd
+    # like this URL to be the generic dispatcher, but for now let's
+    # just display some metadata about the item.
+    item = get_object_or_404(models.Item, pk=item_id, course__id=course_id)
+    if item.url:
+        return _heading_url(request, item)
+    else:
+        return item_metadata(request, course_id, item_id)
+
+ at members_only
+def item_metadata(request, course_id, item_id):
+    """Display a metadata page for the item."""
+    item = get_object_or_404(models.Item, pk=item_id, course__id=course_id)
+    if item.item_type == 'HEADING':
+        return _heading_detail(request, item)
+    else:
+        return g.render('item_metadata.xhtml', course=item.course,
+                        item=item)
+
+def _heading_url(request, item):
+    return HttpResponseRedirect(item.url)
+
+def _heading_detail(request, item):
+    """Display a heading. Show the subitems for this heading."""
+    return g.render('item_heading_detail.xhtml', item=item)
+
+
+ at instructors_only
+def item_add(request, course_id, item_id):
+    # The parent_item_id is the id for the parent-heading item. Zero
+    # represents 'top-level', i.e. the new item should have no
+    # heading. 
+    #For any other number, we must check that the parent
+    # item is of the Heading type.
+    parent_item_id = item_id
+    if parent_item_id=='0':
+        parent_item = None
+        course = get_object_or_404(models.Course, pk=course_id)
+    else:
+        parent_item = get_object_or_404(models.Item, pk=parent_item_id, course__id=course_id)
+        assert parent_item.item_type == 'HEADING', _('You can only add items to headings!')
+        course = parent_item.course
+
+    if not course.can_edit(request.user):
+        return _access_denied(_('You are not an editor.'))
+
+    item_type = request.GET.get('item_type')
+    assert item_type, _('No item_type parameter was provided.')
+
+    # for the moment, only HEADINGs, URLs and ELECs can be added. fixme.
+    assert item_type in ('HEADING', 'URL', 'ELEC', 'PHYS'), \
+        _('Sorry, only HEADINGs, URLs and ELECs can be added right now.')
+
+    if request.method != 'POST' and item_type == 'PHYS':
+        # special handling: send to catalogue search
+        return HttpResponseRedirect('cat_search/')
+
+    if request.method != 'POST':
+        item = models.Item()    # dummy object
+        metadata_formset = metadata_formset_class(queryset=item.metadata_set.all())
+        return g.render('item_add_%s.xhtml' % item_type.lower(),
+                        **locals())
+    else:
+        # fixme, this will need refactoring. But not yet.
+        author = request.user.get_full_name() or request.user.username
+        item = models.Item()    # dummy object
+        metadata_formset = metadata_formset_class(request.POST, queryset=item.metadata_set.all())
+        assert metadata_formset.is_valid()
+        def do_metadata(item):
+            for obj in [obj for obj in metadata_formset.cleaned_data if obj]: # ignore empty dicts
+                if not obj.get('DELETE'):
+                    item.metadata_set.create(name=obj['name'], value=obj['value'])
+            
+        if item_type == 'HEADING':
+            title = request.POST.get('title', '').strip()
+            if not title:
+                # fixme, better error handling.
+                return HttpResponseRedirect(request.get_full_path())
+            else:
+                item = models.Item(
+                    course=course,
+                    item_type='HEADING',
+                    parent_heading=parent_item,
+                    title=title,
+                    )
+                item.save()
+                do_metadata(item)
+                item.save()
+        elif item_type == 'URL':
+            title = request.POST.get('title', '').strip()
+            url = request.POST.get('url', '').strip()
+            if not (title and url):
+                # fixme, better error handling.
+                return HttpResponseRedirect(request.get_full_path())
+            else:
+                item = models.Item(
+                    course=course,
+                    item_type='URL',
+                    parent_heading=parent_item,
+                    title=title,
+                    url = url)
+                item.save()
+                do_metadata(item)
+                item.save()
+        elif item_type == 'ELEC':
+            title = request.POST.get('title', '').strip()
+            upload = request.FILES.get('file')
+            if not (title and upload):
+                # fixme, better error handling.
+                return HttpResponseRedirect(request.get_full_path())
+            item = models.Item(
+                course=course,
+                item_type='ELEC',
+                parent_heading=parent_item,
+                title=title,
+                fileobj_mimetype = upload.content_type,
+                )
+            item.fileobj.save(upload.name, upload)
+            item.save()
+            do_metadata(item)
+            item.save()
+        else:
+            raise NotImplementedError
+
+        if parent_item:
+            return HttpResponseRedirect(parent_item.item_url('meta'))
+        else:
+            return HttpResponseRedirect(course.course_url())
+
+ at instructors_only
+def item_add_cat_search(request, course_id, item_id):
+    # this chunk stolen from item_add(). Refactor.
+    parent_item_id = item_id
+    if parent_item_id=='0':
+        parent_item = None
+        course = get_object_or_404(models.Course, pk=course_id)
+    else:
+        parent_item = get_object_or_404(models.Item, pk=parent_item_id, course__id=course_id)
+        assert parent_item.item_type == 'HEADING', _('You can only add items to headings!')
+        course = parent_item.course
+    #----------
+
+    if request.method != 'POST':
+        return g.render('item_add_cat_search.xhtml', results=[], query='', 
+                        course=course, parent_item=parent_item)
+
+    # POST handler
+    query     = request.POST.get('query','').strip()
+    raw_pickitem = request.POST.get('pickitem', '').strip()
+    if not raw_pickitem:
+        # process the query.
+        assert query, 'must provide a query.'
+        results = lib_integration.cat_search(query)
+        return g.render('item_add_cat_search.xhtml', 
+                        results=results, query=query, 
+                        course=course, parent_item=parent_item)
+    else:
+        # User has selected an item; add it to course site.
+        #fixme, this block copied from item_add. refactor.
+        parent_item_id = item_id
+        if parent_item_id == '0': 
+            # no heading (toplevel)
+            parent_item = None
+            course = get_object_or_404(models.Course, pk=course_id)
+        else:
+            parent_item = get_object_or_404(models.Item, pk=parent_item_id, course__id=course_id)
+            assert parent_item.item_type == 'HEADING', _('You can only add items to headings!')
+            course = parent_item.course
+        if not course.can_edit(request.user):
+            return _access_denied(_('You are not an editor.'))
+
+        pickitem = simplejson.loads(raw_pickitem)
+        dublin = marcxml_dictionary_to_dc(pickitem)
+
+        item = course.item_set.create(parent_heading=parent_item,
+                                      title=dublin.get('dc:title','Untitled'),
+                                      item_type='PHYS')
+        item.save()
+
+        for dc, value in dublin.items():
+            md = item.metadata_set.create(item=item, name=dc, value=value)
+        # store the whole darn MARC-dict as well (JSON)
+        item.metadata_set.create(item=item, name='syrup:marc', value=raw_pickitem)
+        item.save()
+        return HttpResponseRedirect('../../../%d/' % item.id)
+
+#------------------------------------------------------------
+
+#this is used in item_edit.
+metadata_formset_class = modelformset_factory(models.Metadata, 
+                                              fields=['name','value'], 
+                                              extra=3, can_delete=True)
+
+ at instructors_only
+def item_edit(request, course_id, item_id):
+    course = get_object_or_404(models.Course, pk=course_id)
+    item = get_object_or_404(models.Item, pk=item_id, course__id=course_id)
+    item_type = item.item_type
+    template = 'item_add_%s.xhtml' % item_type.lower()
+    parent_item = item.parent_heading
+
+    if request.method != 'POST':
+        metadata_formset = metadata_formset_class(queryset=item.metadata_set.all())
+        return g.render(template, **locals())
+    else:
+        metadata_formset = metadata_formset_class(request.POST, queryset=item.metadata_set.all())
+        assert metadata_formset.is_valid()
+        if 'file' in request.FILES:
+            # this is a 'replace-current-file' action.
+            upload = request.FILES.get('file')
+            item.fileobj.save(upload.name, upload)
+            item.fileobj_mimetype = upload.content_type
+        else:
+            # generally update the item.
+            [setattr(item, k, v) for (k,v) in request.POST.items()]
+            # generally update the metadata
+            item.metadata_set.all().delete()
+            for obj in [obj for obj in metadata_formset.cleaned_data if obj]: # ignore empty dicts
+                if not obj.get('DELETE'):
+                    item.metadata_set.create(name=obj['name'], value=obj['value'])
+                    
+        item.save()
+        return HttpResponseRedirect(item.parent_url())
+        
+ at instructors_only
+def item_delete(request, course_id, item_id):
+    course = get_object_or_404(models.Course, pk=course_id)
+    item = get_object_or_404(models.Item, pk=item_id, course__id=course_id)
+    if request.method != 'POST':
+        return g.render('item_delete_confirm.xhtml', **locals())
+    else:
+        if 'yes' in request.POST:
+            # I think Django's ON DELETE CASCADE-like behaviour will
+            # take care of the sub-items.
+            if item.parent_heading:
+                redir = HttpResponseRedirect(item.parent_heading.item_url('meta'))
+            else:
+                redir = HttpResponseRedirect(course.course_url())
+            item.delete()
+            return redir
+        else:
+            return HttpResponseRedirect('../meta')
+    
+ at members_only
+def item_download(request, course_id, item_id, filename):
+    course = get_object_or_404(models.Course, pk=course_id)
+    item = get_object_or_404(models.Item, pk=item_id, course__id=course_id)
+    assert item.item_type == 'ELEC', _('Can only download ELEC documents!')
+    fileiter = item.fileobj.chunks()
+    resp = HttpResponse(fileiter)
+    resp['Content-Type'] = item.fileobj_mimetype or 'application/octet-stream'
+    #resp['Content-Disposition'] = 'attachment; filename=%s' % name
+    return resp
+    
+
+
+#------------------------------------------------------------
+# resequencing items
+
+def _reseq(request, course, parent_heading):
+    new_order = request.POST['new_order'].split(',')
+    # new_order is now a list like this: ['item_3', 'item_8', 'item_1', ...].
+    # get at the ints.
+    new_order = [int(n.split('_')[1]) for n in new_order]
+    print >> sys.stderr, new_order
+    the_items = list(course.item_set.filter(parent_heading=parent_heading).order_by('sort_order'))
+    # sort the items by position in new_order
+    the_items.sort(key=lambda item: new_order.index(item.id))
+    for newnum, item in enumerate(the_items):
+        item.sort_order = newnum
+        item.save()
+    return HttpResponse("'ok'");
+
+ at instructors_only
+def course_reseq(request, course_id):
+    course = get_object_or_404(models.Course, pk=course_id)
+    parent_heading = None
+    return _reseq(request, course, parent_heading)
+
+ at instructors_only
+def item_heading_reseq(request, course_id, item_id):
+    course = get_object_or_404(models.Course, pk=course_id)
+    item = get_object_or_404(models.Item, pk=item_id, course__id=course_id)
+    parent_heading = item
+    return _reseq(request, course, parent_heading)
+
+
+ at instructors_only
+def item_relocate(request, course_id, item_id):
+    """Move an item from its current subheading to another one."""
+    course = get_object_or_404(models.Course, pk=course_id)
+    item = get_object_or_404(models.Item, pk=item_id, course__id=course_id)
+    if request.method != 'POST':
+        return g.render('item_relocate.xhtml', **locals())
+    else:
+        newheading = int(request.POST['heading'])
+        if newheading == 0:
+            new_parent = None
+        else:
+            new_parent = course.item_set.get(pk=newheading)
+            if item in new_parent.hierarchy():
+                # then we would create a cycle. Bail out.
+                return simple_message(_('Impossible item-move!'), 
+                                      _('You cannot make an item a descendant of itself!'))
+        item.parent_heading = new_parent
+        item.save()
+        if new_parent:
+            return HttpResponseRedirect(new_parent.item_url('meta'))
+        else:
+            return HttpResponseRedirect(course.course_url())
+        
+        
+
+#-----------------------------------------------------------------------------
+# Physical item processing
+
+ at admin_only                     # fixme, is this the right permission?
+def phys_index(request):
+    return g.render('phys/index.xhtml')
+
+ at admin_only                     # fixme, is this the right permission?
+def phys_checkout(request):
+    if request.method != 'POST':
+        return g.render('phys/checkout.xhtml', step=1)
+    else:
+        post = lambda k: request.POST.get(k, '').strip()
+        # dispatch based on what 'step' we are at.
+        step = post('step')     
+        func = {'1': _phys_checkout_get_patron,
+                '2':_phys_checkout_do_checkout,
+                '3':_phys_checkout_do_another,
+                }[step]
+        return func(request)
+
+def _phys_checkout_get_patron(request):
+    post           = lambda k: request.POST.get(k, '').strip()
+    patron, item   = post('patron'), post('item')
+    msg            = lib_integration.patron_info(patron)
+    if not msg['success']:
+        return simple_message(_('Invalid patron barcode'),
+                              _('No such patron could be found.'))
+    else:
+        patron_descrip = '%s (%s) — %s' % (
+            msg['personal'], msg['home_library'], msg['screenmsg'])
+        return g.render('phys/checkout.xhtml', step=2, 
+                        patron=patron, patron_descrip=patron_descrip)
+
+def _phys_checkout_do_checkout(request):
+    post           = lambda k: request.POST.get(k, '').strip()
+    patron, item   = post('patron'), post('item')
+    patron_descrip = post('patron_descrip')
+
+    # make sure the barcode actually matches with a known barcode in
+    # Syrup. We only checkout what we know about.
+    matches = models.Item.with_barcode(item)
+    if not matches:
+        is_successful = False
+        item_descrip  = None
+    else:
+        msg_status   = lib_integration.item_status(item)
+        msg_checkout = lib_integration.checkout(patron, item)
+        is_successful = msg_checkout['success']
+        item_descrip = '%s — %s' % (
+            msg_status['title'], msg_status['status'])
+
+    # log the checkout attempt.
+    log_entry = models.CheckInOut.objects.create(
+        is_checkout = True,
+        is_successful = is_successful,
+        staff = request.user,
+        patron = patron,
+        patron_descrip = patron_descrip,
+        item = item,
+        item_descrip = item_descrip)
+    log_entry.save()
+
+    if not matches:
+        return simple_message(
+            _('Item not found in Reserves'),
+            _('This item does not exist in the Reserves database! '
+              'Cannot check it out.'))
+    else:
+        return g.render('phys/checkout.xhtml', step=3, 
+                        patron=patron, item=item,
+                        patron_descrip=patron_descrip,
+                        checkout_result=msg_checkout,
+                        item_descrip=item_descrip)
+
+def _phys_checkout_do_another(request):
+    post           = lambda k: request.POST.get(k, '').strip()
+    patron         = post('patron')
+    patron_descrip = post('patron_descrip')
+    return g.render('phys/checkout.xhtml', step=2, 
+                    patron=patron,
+                    patron_descrip=patron_descrip)
+
+#------------------------------------------------------------
+
+ at admin_only        
+def phys_mark_arrived(request):
+    if request.method != 'POST':
+        return g.render('phys/mark_arrived.xhtml')
+    else:
+        barcode = request.POST.get('item', '').strip()
+        already = models.PhysicalObject.by_barcode(barcode)
+        if already:
+            msg = _('This item has already been marked as received. Date received: %s')
+            msg = msg % str(already.received)
+            return simple_message(_('Item already marked as received'), msg)
+        bib_id  = lib_integration.barcode_to_bib_id(barcode)
+        if not bib_id:
+            return simple_message(_('Item not found'), 
+                                  _('No item matching this barcode could be found.'))
+
+        marcxml = lib_integration.bib_id_to_marcxml(bib_id)
+        dct     = marcxml_to_dictionary(marcxml)
+        dublin  = marcxml_dictionary_to_dc(dct)
+        # merge them
+        dct.update(dublin)
+        ranked = rank_pending_items(dct)
+        return g.render('phys/mark_arrived_choose.xhtml', 
+                        barcode=barcode,
+                        bib_id=bib_id,
+                        ranked=ranked,
+                        metadata=dct)
+
+ at admin_only        
+def phys_mark_arrived_match(request):
+    choices = [int(k.split('_')[1]) for k in request.POST if k.startswith('choose_')]
+    if not choices:
+        return simple_message(_('No matching items selected!'),
+                              _('You must select one or more matching items from the list.'))
+    else:
+        barcode = request.POST.get('barcode', '').strip()
+        assert barcode
+        smallint = request.POST.get('smallint', '').strip() or None
+        try:
+            phys = models.PhysicalObject(barcode=barcode,
+                                         receiver = request.user,
+                                         smallint = smallint)
+            phys.save()
+        except Exception, e:
+            return simple_message(_('Error'), repr(e), go_back=True)
+
+        for c in choices:
+            item = models.Item.objects.get(pk=c)
+            current_bc = item.barcode()
+            if current_bc:
+                item.metadata_set.filter(name='syrup:barcode').delete()
+            item.metadata_set.create(name='syrup:barcode', value=barcode)
+            item.save()
+    return g.render('phys/mark_arrived_outcome.xhtml')

Added: servres/trunk/conifer/syrup/views/search.py
===================================================================
--- servres/trunk/conifer/syrup/views/search.py	                        (rev 0)
+++ servres/trunk/conifer/syrup/views/search.py	2009-04-16 00:09:02 UTC (rev 335)
@@ -0,0 +1,228 @@
+from _common import *
+from django.utils.translation import ugettext as _
+
+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.
+        Example:
+        
+        >>> 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
+    return query
+
+
+#-----------------------------------------------------------------------------
+# Search and search support
+
+def search(request, in_course=None):
+    ''' Need to work on this, the basic idea is
+        - put an entry point for instructor and course listings
+        - page through item entries
+        If in_course is provided, then limit search to the contents of the specified course.
+    '''
+    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)
+
+        # numeric search: If the query-string is a single number, then
+        # we do an item-ID search, or a barcode search.  fixme:
+        # item-ID is not a good short-id, since the physical item may
+        # be represented in multiple Item records. We need a
+        # short-number for barcodes.
+
+        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:
+            # Textual (non-numeric) queries.
+            item_query = get_query(query_string, ['title', 'metadata__value'])
+                #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_course:
+            results_list = results_list.filter(course=in_course)
+        results_list = results_list.order_by('title')
+        results_len = len(results_list)
+        paginator = Paginator(results_list, count)
+
+        #course search
+        if in_course:
+            # then no course search is necessary.
+            course_list = []; course_len = 0
+        else:
+            course_query = get_query(query_string, ['title', 'department__name'])
+            print 'course_query'
+            print course_query
+            course_results = models.Course.objects.filter(course_query).all()
+            # course_list = models.Course.objects.filter(course_query).filter(active=True).order_by('title')[0:5]
+            course_list = course_results.order_by('title')[0:5]
+            #there might be a better way of doing this, though instr and course tables should not be expensive to query
+            #len directly on course_list will reflect limit
+            course_len = len(course_results)
+
+        #instructor search
+        instr_query = get_query(query_string, ['user__last_name'])
+        instructor_results = models.Member.objects.filter(instr_query).filter(role='INSTR')
+        if in_course:
+            instructor_results = instructor_results.filter(course=in_course)
+        instructor_list = instructor_results.order_by('user__last_name')[0:5]
+        instr_len = len(instructor_results)
+    elif in_course:
+        # we are in a course, but have no query? Return to the course-home page.
+        return HttpResponseRedirect('../')
+    else:
+        results_list = models.Item.objects.order_by('title')
+        results_len = len(results_list)
+        paginator = Paginator( results_list,
+            count)
+        course_results = models.Course.objects.filter(active=True)
+        course_list = course_results.order_by('title')[0:5]
+        course_len = len(course_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
+    '''
+
+    return g.render('search_results.xhtml', **locals())
+
+#-----------------------------------------------------------------------------
+# Z39.50 support
+
+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.Target.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']
+        search_target= models.Target.objects.get(name=target)
+        conn = zoom.Connection (search_target.host, search_target.port)
+        conn.databaseName = search_target.db
+        conn.preferredRecordSyntax = search_target.syntax
+        query = zoom.Query ('CCL', '%s="%s"' % ('ti',tquery))
+        res = conn.search (query)
+        print("results are %d" % len(res))
+        collector = [(None,None)] * len(res)
+
+        start = (page_num - 1) * count
+        end = (page_num * count) + 1
+
+        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))
+            else:
+                raw = r.data
+
+                # Convert to MARC
+                marcdata = zmarc.MARC(raw)
+                #print marcdata
+
+                # Convert to MARCXML
+                # marcxml = marcdata.toMARCXML()
+                # print marcxml
+
+                # 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]])
+
+                # Amara XML tools would allow using xpath
+                '''
+                title = ""
+                doc = binderytools.bind_string(marcxml)
+                t = doc.xml_xpath("//datafield[@tag='245']/subfield[@code='a']")
+                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')))
+            idx+=1
+
+        conn.close ()
+        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)
+
+

Deleted: servres/trunk/conifer/syrup/views.py
===================================================================
--- servres/trunk/conifer/syrup/views.py	2009-04-16 00:06:14 UTC (rev 334)
+++ servres/trunk/conifer/syrup/views.py	2009-04-16 00:09:02 UTC (rev 335)
@@ -1,1397 +0,0 @@
-#-----------------------------------------------------------------------------
-# todo: break this up. It's getting long. I think we should have
-# something like:
-#
-#   views/__init__.py                     # which imports:
-#   views/course_site_handlers.py
-#   views/search_stuff.py
-#   views/add_edit_course.py
-#   ...
-#   views/common_imports.py              # imported by all.
-#
-# though these are just examples. Everything in views/* would include
-# 'from common_imports import *' just to keep the imports
-# tidy. Views/__init__ would import all the other bits: that ought to
-# satisfy Django.
-
-import warnings
-from conifer.syrup import models
-from datetime import datetime
-from django.contrib.auth import authenticate, login, logout
-from django.contrib.auth.decorators import login_required
-from django.contrib.auth.models import User, SiteProfileNotAvailable
-from django.core.paginator import Paginator
-from django.db.models import Q
-from django.http import HttpResponse, HttpResponseRedirect, HttpResponseNotFound
-from django.http import HttpResponseForbidden
-from django.shortcuts import get_object_or_404
-from django.utils import simplejson
-from generics import *
-#from gettext import gettext as _ # fixme, is this the right function to import?
-from django.utils.translation import ugettext as _
-import conifer.genshi_support as g
-import django.forms
-import re
-import sys
-from django.forms.models import modelformset_factory
-from conifer.custom import lib_integration
-from conifer.libsystems.z3950.marcxml import marcxml_to_dictionary, marcxml_dictionary_to_dc
-from fuzzy_match import rank_pending_items
-from django.core.urlresolvers import reverse
-
-#-----------------------------------------------------------------------------
-# Z39.50 Support
-#
-# This is experimental at this time, and requires some tricky Python
-# imports as far as I can tell. For that reason, let's keep the Z39.50
-# support optional for now. If you have Ply and PyZ3950, we'll load
-# and use it; if not, no worries, everything else will workk.
-
-try:
-    # Graham needs this import hackery to get PyZ3950 working. Presumably
-    # Art can 'import profile; import lex', so this hack won't run for
-    # him.
-    try:
-        import profile
-        import lex
-        import yacc
-    except ImportError:
-        sys.modules['profile'] = sys # just get something called 'profile';
-                                     # it's not actually used.
-        import ply.lex              
-        import ply.yacc             # pyz3950 thinks these are toplevel modules.
-        sys.modules['lex'] = ply.lex
-        sys.modules['yacc'] = ply.yacc
-
-    # for Z39.50 support, not sure whether this is the way to go yet but
-    # as generic as it gets
-    from PyZ3950 import zoom, zmarc
-except:
-    warnings.warn('Could not load Z39.50 support.')
-
-#-----------------------------------------------------------------------------
-# poor-man's logging. Not sure we need more yet.
-
-def log(level, msg):
-    print >> sys.stderr, '[%s] %s: %s' % (datetime.now(), level.upper(), msg)
-
-#-----------------------------------------------------------------------------
-# Authentication
-
-def auth_handler(request, path):
-    default_url = reverse(welcome) #request.META['SCRIPT_NAME'] + '/'
-    if path == 'login/':
-        if request.method == 'GET':
-            next=request.GET.get('next', default_url)
-            if request.user.is_authenticated():
-                return HttpResponseRedirect(next)
-            else:
-                return g.render('auth/login.xhtml', 
-                                next=request.GET.get('next'))
-        else:
-            userid, password = request.POST['userid'], request.POST['password']
-            next = request.POST['next']
-            user = authenticate(username=userid, password=password)
-            def _error_page(msg):
-                return g.render('auth/login.xhtml', err=msg, next=next)
-            if user is None:
-                return _error_page(
-                    _('Invalid username or password. Please try again.'))
-            elif not user.is_active:
-                return _error_age(
-                    _('Sorry, this account has been disabled.'))
-            else:
-                login(request, user)
-                # initialize the profile if it doesn't exist.
-                try:
-                    user.get_profile()
-                except models.UserProfile.DoesNotExist:
-                    profile = models.UserProfile.objects.create(user=user)
-                    profile.save()
-                return HttpResponseRedirect(request.POST.get('next', default_url))
-    elif path == 'logout':
-        logout(request)
-        return HttpResponseRedirect(default_url)
-    else:
-        return HttpResponse('auth_handler: ' + path)
-
-#-----------------------------------------------------------------------------
-# Authorization
-
-def _fast_user_membership_query(user_id, course_id, where=None):
-    # I use a raw SQL query here because I want the lookup to be as
-    # fast as possible. Caching would help too, but let's try this
-    # first. (todo, review later.)
-    query = ('select count(*) from syrup_member '
-             'where user_id=%s and course_id=%s ')
-    if where:
-        query += (' and ' + where)
-    cursor = django.db.connection.cursor()
-    cursor.execute(query, [user_id, int(course_id)])
-    res = cursor.fetchall()
-    cursor.close()
-    allowed = bool(res[0][0])
-    return allowed
-    
-def _access_denied(request, message):
-    if request.user.is_anonymous():
-        # then take them to login screen....
-        dest = request.META['SCRIPT_NAME'] + '/accounts/login/?next=' + request.META['PATH_INFO']
-        return HttpResponseRedirect(dest)
-    else:
-        return simple_message(_('Access denied.'), message,
-                              _django_type=HttpResponseForbidden)
-
-# todo, these decorators could be refactored.
-
-# decorator
-def instructors_only(handler):
-    def hdlr(request, course_id, *args, **kwargs):
-        allowed = request.user.is_superuser
-        if not allowed:
-            allowed = _fast_user_membership_query(
-                request.user.id, course_id, "role in ('INSTR','PROXY')")
-        if allowed:
-            return handler(request, course_id, *args, **kwargs)
-        else:
-            return _access_denied(request, _('Only instructors are allowed here.'))
-    return hdlr
-
-# decorator
-def members_only(handler):
-    def hdlr(request, course_id, *args, **kwargs):
-        user = request.user
-        allowed = user.is_superuser
-        if not allowed:
-            course = models.Course.objects.get(pk=course_id)
-            allowed = ((user.is_anonymous() and course.access=='ANON') or \
-                       (user.is_authenticated() and course.access=='LOGIN'))
-        if not allowed:
-            allowed = _fast_user_membership_query(user.id, course_id)
-        if allowed:
-            return handler(request, course_id, *args, **kwargs)
-        else:
-            if course.access=='LOGIN':
-                msg = _('Please log in, so that you can enter this site.')
-            else:
-                msg = _('Only course members are allowed here.')
-            return _access_denied(request, msg)
-    return hdlr
-
-# decorator
-def admin_only(handler):
-    # fixme, 'admin' is vaguely defined for now as anyone who is
-    # 'staff', i.e. who has access to the Django admin interface.
-    def hdlr(request, *args, **kwargs):
-        allowed = request.user.is_staff
-        if allowed:
-            return handler(request, *args, **kwargs)
-        else:
-            return _access_denied(request, _('Only administrators are allowed here.'))
-    return hdlr
-
-#decorator
-def public(handler):
-    # A no-op! Just here to be used to explicitly decorate methods
-    # that are supposed to be public.
-    return handler
-
-#-----------------------------------------------------------------------------
-# Simple Message: just a quick title-and-message web page.
-
-def simple_message(title, content, go_back=True, **kwargs):
-    kwargs.update(**locals())
-    return g.render('simplemessage.xhtml', **kwargs)
-
-#-----------------------------------------------------------------------------
-
-def welcome(request):
-    return g.render('welcome.xhtml')
-
-# MARK: propose we get rid of this. We already have a 'Courses' browser.
-def open_courses(request):
-    page_num = int(request.GET.get('page', 1))
-    count = int(request.GET.get('count', 5))
-    paginator = Paginator(models.Course.objects.all(), count) # fixme, what filter?
-    return g.render('open_courses.xhtml', paginator=paginator,
-                    page_num=page_num,
-                    count=count)
-# MARK: propose we drop this too. We have a browse.
-def instructors(request):
-    page_num = int(request.GET.get('page', 1))
-    count = int(request.GET.get('count', 5))
-    action = request.GET.get('action', 'browse')
-    if action == 'join':
-        paginator = Paginator(models.User.active_instructors(), count)
-    elif action == 'drop':
-        paginator = Paginator(models.Course.objects.all(), count) # fixme, what filter?
-    else:
-        paginator = Paginator(models.Course.objects.all(), count) # fixme, what filter?
-        
-    return g.render('instructors.xhtml', paginator=paginator,
-                    page_num=page_num,
-                    count=count)
-
-# MARK: propose we get rid of this. We have browse.
-def departments(request):
-    raise NotImplementedError
-
-
-def user_prefs(request):
-    if request.method != 'POST':
-        return g.render('prefs.xhtml')
-    else:
-        profile = request.user.get_profile()
-        profile.wants_email_notices = bool(request.POST.get('wants_email_notices'))
-        profile.save()
-        return HttpResponseRedirect('../')
-
-def z3950_test(request):
-    #testing JZKitZ3950 - it seems to work, but i have a character set problem
-    #with the returned marc
-    #nope - the problem is weak mapping with the limited solr test set
-    #i think this can be sorted out
-
-    #conn = zoom.Connection ('z3950.loc.gov', 7090)
-    #conn = zoom.Connection ('webvoy.uwindsor.ca', 9000)
-    #solr index with JZKitZ3950 wrapping
-    conn = zoom.Connection ('127.0.0.1', 2100)
-    # conn = zoom.Connection ('127.0.0.1', 2100)
-    print("connecting...")
-    conn.databaseName = 'Test'
-    # conn.preferredRecordSyntax = 'XML'
-    conn.preferredRecordSyntax = 'USMARC'
-    query = zoom.Query ('CCL', 'ti="agar"')
-    res = conn.search (query)
-    collector = []
-    # if we wanted to get into funkiness
-    m = zmarc.MARC8_to_Unicode ()
-    for r in res:
-        print(type(r.data))
-        print(type(m.translate(r.data)))
-        rec = zmarc.MARC (r.data, strict=0)
-        # rec = zmarc.MARC (rec, strict=0)
-        collector.append(str(rec))
-
-    conn.close ()
-    res_str = "" . join(collector)
-    return g.render('z3950_test.xhtml', res_str=res_str)
-
-def browse(request, browse_option=''):
-    #the defaults should be moved into a config file or something...
-    page_num = int(request.GET.get('page', 1))
-    count    = int(request.GET.get('count', 5))
-
-    if browse_option == '':
-        queryset = None
-        template = 'browse_index.xhtml'
-    elif browse_option == 'instructors':
-        queryset = models.User.active_instructors()
-        template = 'instructors.xhtml'
-    elif browse_option == 'departments':
-        queryset = models.Department.objects.filter(active=True)
-        template = 'departments.xhtml'
-    elif browse_option == 'courses':
-        # fixme, course filter should not be (active=True) but based on user identity.
-        queryset = models.Course.objects.all()
-        template = 'courses.xhtml'
-
-    paginator = queryset and Paginator(queryset, count) or None # index has no queryset.
-    return g.render(template, paginator=paginator,
-                    page_num=page_num,
-                    count=count)
-
- at login_required
-def my_courses(request):
-    return g.render('my_courses.xhtml')
-
-def instructor_detail(request, instructor_id):
-    page_num = int(request.GET.get('page', 1))
-    count = int(request.GET.get('count', 5))
-    '''
-    i am not sure this is the best way to go from instructor
-    to course
-    '''
-    courses = models.Course.objects.filter(member__user=instructor_id,
-                                           member__role='INSTR')
-    paginator = Paginator(courses.order_by('title'), count)
-
-    '''
-    no concept of active right now, maybe suppressed is a better
-    description anyway?
-    '''
-        # filter(active=True).order_by('title'), count)
-    instructor = models.User.objects.get(pk=instructor_id)
-    return g.render('courses.xhtml', 
-                    custom_title=_('Courses taught by %s') % instructor.get_full_name(),
-                    paginator=paginator,
-                    page_num=page_num,
-                    count=count)
-
-def department_detail(request, department_id):
-    page_num = int(request.GET.get('page', 1))
-    count = int(request.GET.get('count', 5))
-    paginator = Paginator(models.Course.objects.
-        filter(department__id=department_id).
-        filter(active=True).order_by('title'), count)
-
-    return g.render('courses.xhtml', paginator=paginator,
-            page_num=page_num,
-            count=count)
-
-#-----------------------------------------------------------------------------
-# Creating a new course
-
-class NewCourseForm(ModelForm):
-    class Meta:
-        model = models.Course
-        exclude = ('passkey','access')
-
-    def clean_code(self):
-        v = (self.cleaned_data.get('code') or '').strip()
-        is_valid_func = models.course_codes.course_code_is_valid
-        if (not is_valid_func) or is_valid_func(v):
-            return v
-        else:
-            raise ValidationError, _('invalid course code')
-
-# if we have course-code lookup, hack lookup support into the new-course form.
-
-COURSE_CODE_LIST = bool(models.course_codes.course_code_list)
-COURSE_CODE_LOOKUP_TITLE = bool(models.course_codes.course_code_lookup_title)
-
-if COURSE_CODE_LIST:
-    from django.forms import Select
-    course_list = models.course_codes.course_code_list()
-    choices = [(a,a) for a in course_list]
-    choices.sort()
-    empty_label = u'---------'
-    choices.insert(0, ('', empty_label))
-    NewCourseForm.base_fields['code'].widget = Select(
-        choices = choices)
-    NewCourseForm.base_fields['code'].empty_label = empty_label
-
-#--------------------
-    
- at login_required
-def add_new_course(request):
-    if not request.user.has_perm('add_course'):
-        return _access_denied(_('You are not allowed to create course sites.'))
-    return _add_or_edit_course(request)
-
- at instructors_only
-def edit_course(request, course_id):
-    instance = get_object_or_404(models.Course, pk=course_id)
-    return _add_or_edit_course(request, instance=instance)
-    
-def _add_or_edit_course(request, instance=None):
-    is_add = (instance is None)
-    if is_add:
-        instance = models.Course()
-    current_access_level = not is_add and instance.access or None
-    example = models.course_codes.course_code_example
-    if request.method != 'POST':
-        form = NewCourseForm(instance=instance)
-        return g.render('edit_course.xhtml', **locals())
-    else:
-        form = NewCourseForm(request.POST, instance=instance)
-        if not form.is_valid():
-            return g.render('edit_course.xhtml', **locals())
-        else:
-            form.save()
-            course = form.instance
-            if course.access == u'INVIT' and not course.passkey:
-                course.generate_new_passkey()
-                course.save()
-            assert course.id
-            user_in_course = models.Member.objects.filter(user=request.user,course=course)
-            if not user_in_course: # for edits, might already be!
-                mbr = course.member_set.create(user=request.user, role='INSTR')
-                mbr.save()
-            
-            if is_add or (current_access_level != course.access):
-                # we need to configure permissions.
-                return HttpResponseRedirect(course.course_url('edit/permission/'))
-            else:
-                return HttpResponseRedirect('../') # back to main view.
-
-# no access-control needed to protect title lookup.
-def add_new_course_ajax_title(request):
-    course_code = request.GET['course_code']
-    title = models.course_codes.course_code_lookup_title(course_code)
-    return HttpResponse(simplejson.dumps({'title':title}))
-
- at instructors_only
-def edit_course_permissions(request, course_id):
-    course = get_object_or_404(models.Course, pk=course_id)
-    # choices: make the access-choice labels more personalized than
-    # the ones in 'models'.
-    choices = [
-        # note: I'm leaving ANON out for now, until we discuss it further.
-        (u'CLOSE', _(u'No students: this site is closed.')),
-        (u'STUDT', _(u'Students in my course -- I will provide section numbers')),
-        (u'INVIT', _(u'Students in my course -- I will share an Invitation Code with them')),
-        (u'LOGIN', _(u'All Reserves patrons'))]
-    if models.course_sections.sections_tuple_delimiter is None:
-        # no course-sections support? Then STUDT cannot be an option.
-        del choices[1]
-    choose_access = django.forms.Select(choices=choices)
-        
-    if request.method != 'POST':
-        return g.render('edit_course_permissions.xhtml', **locals())
-    else:
-        POST = request.POST
-
-        if 'action_change_code' in POST:
-            # update invitation code -------------------------------------
-            course.generate_new_passkey()
-            course.access = u'INVIT'
-            course.save()
-            return HttpResponseRedirect('.#student_access')
-
-        elif 'action_save_instructor' in POST:
-            # update instructor details ----------------------------------
-            iname = POST.get('new_instructor_name','').strip()
-            irole = POST.get('new_instructor_role')
-
-            def get_record_for(username):
-                instr = models.maybe_initialize_user(iname)
-                if instr:
-                    try:
-                        return models.Member.objects.get(user=instr, course=course)
-                    except models.Member.DoesNotExist:
-                        return models.Member.objects.create(user=instr, course=course)
-
-            # add a new instructor
-            if iname:
-                instr = get_record_for(iname)
-                if instr:       # else? should have an error.
-                    instr.role = irole
-                    instr.save()
-                else:
-                    instructor_error = 'No such user: %s' % iname
-                    return g.render('edit_course_permissions.xhtml', **locals())
-
-
-            # removing and changing roles of instructors
-            to_change_role = [(int(name.rsplit('_', 1)[-1]), POST[name]) \
-                                  for name in POST if name.startswith('instructor_role_')]
-            to_remove = [int(name.rsplit('_', 1)[-1]) \
-                             for name in POST if name.startswith('instructor_remove_')]
-            for instr_id, newrole in to_change_role:
-                if not instr_id in to_remove:
-                    instr = models.Member.objects.get(pk=instr_id, course=course)
-                    instr.role = newrole
-                    instr.save()
-            for instr_id in to_remove:
-                # todo, should warn if deleting yourself!
-                instr = models.Member.objects.get(pk=instr_id, course=course)
-                instr.delete()
-            # todo, should have some error-reporting.
-            return HttpResponseRedirect('.')
-
-        elif 'action_save_student' in POST:
-            # update student details ------------------------------------
-            access = POST.get('access')
-            course.access = access
-            # drop all provided users. fixme, this could be optimized to do add/drops.
-            models.Member.objects.filter(course=course, provided=True).delete()
-            if course.access == u'STUDT':
-                initial_sections = course.sections()
-                # add the 'new section' if any
-                new_sec = request.POST.get('add_section')
-                new_sec = models.section_decode_safe(new_sec)
-                if new_sec:
-                    course.add_sections(new_sec)
-                # remove the sections to be dropped
-                to_remove = [models.section_decode_safe(name.rsplit('_',1)[1]) \
-                                 for name in POST \
-                                 if name.startswith('remove_section_')]
-                course.drop_sections(*to_remove)
-                student_names = models.course_sections.students_in(*course.sections())
-                for name in student_names:
-                    user = models.maybe_initialize_user(name)
-                    if user:
-                        if not models.Member.objects.filter(course=course, user=user):
-                            mbr = models.Member.objects.create(
-                                course=course, user=user, 
-                                role='STUDT', provided=True)
-                            mbr.save()
-            else:
-                pass
-            course.save()
-            return HttpResponseRedirect('.#student_access')
-
- at instructors_only
-def delete_course(request, course_id):
-    course = get_object_or_404(models.Course, pk=course_id)
-    if request.POST.get('confirm_delete'):
-        course.delete()
-        return HttpResponseRedirect(reverse('my_courses'))
-    else:
-        return HttpResponseRedirect('../')
-
-#-----------------------------------------------------------------------------
-# Course Invitation Code handler
-
- at login_required                 # must be, to avoid/audit brute force attacks.
-def course_invitation(request):
-    if request.method != 'POST':
-        return g.render('course_invitation.xhtml', code='', error='',
-                        **locals())
-    else:
-        code = request.POST.get('code', '').strip()
-        # todo, a pluggable passkey implementation would normalize the code here.
-        if not code:
-            return HttpResponseRedirect('.')
-        try:
-            # note, we only allow the passkey if access='INVIT'.
-            crs = models.Course.objects.filter(access='INVIT').get(passkey=code)
-        except models.Course.DoesNotExist:
-            # todo, do we need a formal logging system? Or a table for
-            # invitation failures? They should be captured somehow, I
-            # think. Should we temporarily disable accounts after
-            # multiple failures?
-            log('WARN', 'Invitation failure, user %r gave code %r' % \
-                (datetime.now(), request.user.username, code))
-            error = _('The code you provided is not valid.')
-            return g.render('course_invitation.xhtml', **locals())
-
-        # the passkey is good; add the user if not already a member.
-        if not models.Member.objects.filter(user=request.user, course=crs):
-            mbr = models.Member.objects.create(user=request.user, course=crs, 
-                                               role='STUDT')
-            mbr.save()
-        return HttpResponseRedirect(crs.course_url())
-
-#-----------------------------------------------------------------------------
-# Course-instance handlers
-
- at members_only
-def course_detail(request, course_id):
-    course = get_object_or_404(models.Course, pk=course_id)
-    return g.render('course_detail.xhtml', course=course)
-
- at members_only
-def course_search(request, course_id):
-    course = get_object_or_404(models.Course, pk=course_id)
-    return search(request, in_course=course)
-
- at members_only
-def item_detail(request, course_id, item_id):
-    """Display an item (however that makes sense).""" 
-    # really, displaying an item will vary based on what type of item
-    # it is -- e.g. a URL item would redirect to the target URL. I'd
-    # like this URL to be the generic dispatcher, but for now let's
-    # just display some metadata about the item.
-    item = get_object_or_404(models.Item, pk=item_id, course__id=course_id)
-    if item.url:
-        return _heading_url(request, item)
-    else:
-        return item_metadata(request, course_id, item_id)
-
- at members_only
-def item_metadata(request, course_id, item_id):
-    """Display a metadata page for the item."""
-    item = get_object_or_404(models.Item, pk=item_id, course__id=course_id)
-    if item.item_type == 'HEADING':
-        return _heading_detail(request, item)
-    else:
-        return g.render('item_metadata.xhtml', course=item.course,
-                        item=item)
-
-def _heading_url(request, item):
-    return HttpResponseRedirect(item.url)
-
-def _heading_detail(request, item):
-    """Display a heading. Show the subitems for this heading."""
-    return g.render('item_heading_detail.xhtml', item=item)
-
-
- at instructors_only
-def item_add(request, course_id, item_id):
-    # The parent_item_id is the id for the parent-heading item. Zero
-    # represents 'top-level', i.e. the new item should have no
-    # heading. 
-    #For any other number, we must check that the parent
-    # item is of the Heading type.
-    parent_item_id = item_id
-    if parent_item_id=='0':
-        parent_item = None
-        course = get_object_or_404(models.Course, pk=course_id)
-    else:
-        parent_item = get_object_or_404(models.Item, pk=parent_item_id, course__id=course_id)
-        assert parent_item.item_type == 'HEADING', _('You can only add items to headings!')
-        course = parent_item.course
-
-    if not course.can_edit(request.user):
-        return _access_denied(_('You are not an editor.'))
-
-    item_type = request.GET.get('item_type')
-    assert item_type, _('No item_type parameter was provided.')
-
-    # for the moment, only HEADINGs, URLs and ELECs can be added. fixme.
-    assert item_type in ('HEADING', 'URL', 'ELEC', 'PHYS'), \
-        _('Sorry, only HEADINGs, URLs and ELECs can be added right now.')
-
-    if request.method != 'POST' and item_type == 'PHYS':
-        # special handling: send to catalogue search
-        return HttpResponseRedirect('cat_search/')
-
-    if request.method != 'POST':
-        item = models.Item()    # dummy object
-        metadata_formset = metadata_formset_class(queryset=item.metadata_set.all())
-        return g.render('item_add_%s.xhtml' % item_type.lower(),
-                        **locals())
-    else:
-        # fixme, this will need refactoring. But not yet.
-        author = request.user.get_full_name() or request.user.username
-        item = models.Item()    # dummy object
-        metadata_formset = metadata_formset_class(request.POST, queryset=item.metadata_set.all())
-        assert metadata_formset.is_valid()
-        def do_metadata(item):
-            for obj in [obj for obj in metadata_formset.cleaned_data if obj]: # ignore empty dicts
-                if not obj.get('DELETE'):
-                    item.metadata_set.create(name=obj['name'], value=obj['value'])
-            
-        if item_type == 'HEADING':
-            title = request.POST.get('title', '').strip()
-            if not title:
-                # fixme, better error handling.
-                return HttpResponseRedirect(request.get_full_path())
-            else:
-                item = models.Item(
-                    course=course,
-                    item_type='HEADING',
-                    parent_heading=parent_item,
-                    title=title,
-                    )
-                item.save()
-                do_metadata(item)
-                item.save()
-        elif item_type == 'URL':
-            title = request.POST.get('title', '').strip()
-            url = request.POST.get('url', '').strip()
-            if not (title and url):
-                # fixme, better error handling.
-                return HttpResponseRedirect(request.get_full_path())
-            else:
-                item = models.Item(
-                    course=course,
-                    item_type='URL',
-                    parent_heading=parent_item,
-                    title=title,
-                    url = url)
-                item.save()
-                do_metadata(item)
-                item.save()
-        elif item_type == 'ELEC':
-            title = request.POST.get('title', '').strip()
-            upload = request.FILES.get('file')
-            if not (title and upload):
-                # fixme, better error handling.
-                return HttpResponseRedirect(request.get_full_path())
-            item = models.Item(
-                course=course,
-                item_type='ELEC',
-                parent_heading=parent_item,
-                title=title,
-                fileobj_mimetype = upload.content_type,
-                )
-            item.fileobj.save(upload.name, upload)
-            item.save()
-            do_metadata(item)
-            item.save()
-        else:
-            raise NotImplementedError
-
-        if parent_item:
-            return HttpResponseRedirect(parent_item.item_url('meta'))
-        else:
-            return HttpResponseRedirect(course.course_url())
-
- at instructors_only
-def item_add_cat_search(request, course_id, item_id):
-    # this chunk stolen from item_add(). Refactor.
-    parent_item_id = item_id
-    if parent_item_id=='0':
-        parent_item = None
-        course = get_object_or_404(models.Course, pk=course_id)
-    else:
-        parent_item = get_object_or_404(models.Item, pk=parent_item_id, course__id=course_id)
-        assert parent_item.item_type == 'HEADING', _('You can only add items to headings!')
-        course = parent_item.course
-    #----------
-
-    if request.method != 'POST':
-        return g.render('item_add_cat_search.xhtml', results=[], query='', 
-                        course=course, parent_item=parent_item)
-
-    # POST handler
-    query     = request.POST.get('query','').strip()
-    raw_pickitem = request.POST.get('pickitem', '').strip()
-    if not raw_pickitem:
-        # process the query.
-        assert query, 'must provide a query.'
-        results = lib_integration.cat_search(query)
-        return g.render('item_add_cat_search.xhtml', 
-                        results=results, query=query, 
-                        course=course, parent_item=parent_item)
-    else:
-        # User has selected an item; add it to course site.
-        #fixme, this block copied from item_add. refactor.
-        parent_item_id = item_id
-        if parent_item_id == '0': 
-            # no heading (toplevel)
-            parent_item = None
-            course = get_object_or_404(models.Course, pk=course_id)
-        else:
-            parent_item = get_object_or_404(models.Item, pk=parent_item_id, course__id=course_id)
-            assert parent_item.item_type == 'HEADING', _('You can only add items to headings!')
-            course = parent_item.course
-        if not course.can_edit(request.user):
-            return _access_denied(_('You are not an editor.'))
-
-        pickitem = simplejson.loads(raw_pickitem)
-        dublin = marcxml_dictionary_to_dc(pickitem)
-
-        item = course.item_set.create(parent_heading=parent_item,
-                                      title=dublin.get('dc:title','Untitled'),
-                                      item_type='PHYS')
-        item.save()
-
-        for dc, value in dublin.items():
-            md = item.metadata_set.create(item=item, name=dc, value=value)
-        # store the whole darn MARC-dict as well (JSON)
-        item.metadata_set.create(item=item, name='syrup:marc', value=raw_pickitem)
-        item.save()
-        return HttpResponseRedirect('../../../%d/' % item.id)
-
-#------------------------------------------------------------
-
-#this is used in item_edit.
-metadata_formset_class = modelformset_factory(models.Metadata, 
-                                              fields=['name','value'], 
-                                              extra=3, can_delete=True)
-
- at instructors_only
-def item_edit(request, course_id, item_id):
-    course = get_object_or_404(models.Course, pk=course_id)
-    item = get_object_or_404(models.Item, pk=item_id, course__id=course_id)
-    item_type = item.item_type
-    template = 'item_add_%s.xhtml' % item_type.lower()
-    parent_item = item.parent_heading
-
-    if request.method != 'POST':
-        metadata_formset = metadata_formset_class(queryset=item.metadata_set.all())
-        return g.render(template, **locals())
-    else:
-        metadata_formset = metadata_formset_class(request.POST, queryset=item.metadata_set.all())
-        assert metadata_formset.is_valid()
-        if 'file' in request.FILES:
-            # this is a 'replace-current-file' action.
-            upload = request.FILES.get('file')
-            item.fileobj.save(upload.name, upload)
-            item.fileobj_mimetype = upload.content_type
-        else:
-            # generally update the item.
-            [setattr(item, k, v) for (k,v) in request.POST.items()]
-            # generally update the metadata
-            item.metadata_set.all().delete()
-            for obj in [obj for obj in metadata_formset.cleaned_data if obj]: # ignore empty dicts
-                if not obj.get('DELETE'):
-                    item.metadata_set.create(name=obj['name'], value=obj['value'])
-                    
-        item.save()
-        return HttpResponseRedirect(item.parent_url())
-        
- at instructors_only
-def item_delete(request, course_id, item_id):
-    course = get_object_or_404(models.Course, pk=course_id)
-    item = get_object_or_404(models.Item, pk=item_id, course__id=course_id)
-    if request.method != 'POST':
-        return g.render('item_delete_confirm.xhtml', **locals())
-    else:
-        if 'yes' in request.POST:
-            # I think Django's ON DELETE CASCADE-like behaviour will
-            # take care of the sub-items.
-            if item.parent_heading:
-                redir = HttpResponseRedirect(item.parent_heading.item_url('meta'))
-            else:
-                redir = HttpResponseRedirect(course.course_url())
-            item.delete()
-            return redir
-        else:
-            return HttpResponseRedirect('../meta')
-    
- at members_only
-def item_download(request, course_id, item_id, filename):
-    course = get_object_or_404(models.Course, pk=course_id)
-    item = get_object_or_404(models.Item, pk=item_id, course__id=course_id)
-    assert item.item_type == 'ELEC', _('Can only download ELEC documents!')
-    fileiter = item.fileobj.chunks()
-    resp = HttpResponse(fileiter)
-    resp['Content-Type'] = item.fileobj_mimetype or 'application/octet-stream'
-    #resp['Content-Disposition'] = 'attachment; filename=%s' % name
-    return resp
-    
-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.
-        Example:
-        
-        >>> 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
-    return query
-
-
-#-----------------------------------------------------------------------------
-# Search and search support
-
-def search(request, in_course=None):
-    ''' Need to work on this, the basic idea is
-        - put an entry point for instructor and course listings
-        - page through item entries
-        If in_course is provided, then limit search to the contents of the specified course.
-    '''
-    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)
-
-        # numeric search: If the query-string is a single number, then
-        # we do an item-ID search, or a barcode search.  fixme:
-        # item-ID is not a good short-id, since the physical item may
-        # be represented in multiple Item records. We need a
-        # short-number for barcodes.
-
-        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:
-            # Textual (non-numeric) queries.
-            item_query = get_query(query_string, ['title', 'metadata__value'])
-                #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_course:
-            results_list = results_list.filter(course=in_course)
-        results_list = results_list.order_by('title')
-        results_len = len(results_list)
-        paginator = Paginator(results_list, count)
-
-        #course search
-        if in_course:
-            # then no course search is necessary.
-            course_list = []; course_len = 0
-        else:
-            course_query = get_query(query_string, ['title', 'department__name'])
-            print 'course_query'
-            print course_query
-            course_results = models.Course.objects.filter(course_query).all()
-            # course_list = models.Course.objects.filter(course_query).filter(active=True).order_by('title')[0:5]
-            course_list = course_results.order_by('title')[0:5]
-            #there might be a better way of doing this, though instr and course tables should not be expensive to query
-            #len directly on course_list will reflect limit
-            course_len = len(course_results)
-
-        #instructor search
-        instr_query = get_query(query_string, ['user__last_name'])
-        instructor_results = models.Member.objects.filter(instr_query).filter(role='INSTR')
-        if in_course:
-            instructor_results = instructor_results.filter(course=in_course)
-        instructor_list = instructor_results.order_by('user__last_name')[0:5]
-        instr_len = len(instructor_results)
-    elif in_course:
-        # we are in a course, but have no query? Return to the course-home page.
-        return HttpResponseRedirect('../')
-    else:
-        results_list = models.Item.objects.order_by('title')
-        results_len = len(results_list)
-        paginator = Paginator( results_list,
-            count)
-        course_results = models.Course.objects.filter(active=True)
-        course_list = course_results.order_by('title')[0:5]
-        course_len = len(course_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
-    '''
-
-    return g.render('search_results.xhtml', **locals())
-
-#-----------------------------------------------------------------------------
-# Z39.50 support
-
-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.Target.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']
-        search_target= models.Target.objects.get(name=target)
-        conn = zoom.Connection (search_target.host, search_target.port)
-        conn.databaseName = search_target.db
-        conn.preferredRecordSyntax = search_target.syntax
-        query = zoom.Query ('CCL', '%s="%s"' % ('ti',tquery))
-        res = conn.search (query)
-        print("results are %d" % len(res))
-        collector = [(None,None)] * len(res)
-
-        start = (page_num - 1) * count
-        end = (page_num * count) + 1
-
-        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))
-            else:
-                raw = r.data
-
-                # Convert to MARC
-                marcdata = zmarc.MARC(raw)
-                #print marcdata
-
-                # Convert to MARCXML
-                # marcxml = marcdata.toMARCXML()
-                # print marcxml
-
-                # 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]])
-
-                # Amara XML tools would allow using xpath
-                '''
-                title = ""
-                doc = binderytools.bind_string(marcxml)
-                t = doc.xml_xpath("//datafield[@tag='245']/subfield[@code='a']")
-                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')))
-            idx+=1
-
-        conn.close ()
-        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)
-
-
-#-----------------------------------------------------------------------------
-# Administrative options
-
- at admin_only
-def admin_index(request):
-    return g.render('admin/index.xhtml')
-
-
-class TermForm(ModelForm):
-    class Meta:
-        model = models.Term
-
-    class Index:
-        title = _('Terms')
-        all   = models.Term.objects.order_by('start', 'code').all
-        cols  = ['code', 'name', 'start', 'finish']
-        links = [0,1]
-
-    clean_name = strip_and_nonblank('name')
-    clean_code = strip_and_nonblank('code')
-
-    def clean(self):
-        cd = self.cleaned_data
-        s, f = cd.get('start'), cd.get('finish')
-        if (s and f) and s >= f:
-            raise ValidationError, _('start must precede finish')
-        return cd
-
-admin_terms = generic_handler(TermForm, decorator=admin_only)
-
-
-class DeptForm(ModelForm):
-    class Meta:
-        model = models.Department
-
-    class Index:
-        title = _('Departments')
-        all   = models.Department.objects.order_by('abbreviation').all
-        cols  = ['abbreviation', 'name']
-        links = [0,1]
-
-    clean_abbreviation = strip_and_nonblank('abbreviation')
-    clean_name = strip_and_nonblank('name')
-
-admin_depts = generic_handler(DeptForm, decorator=admin_only)
-
-###
-# graham - zap this if it messes anything up :-)
-###
-class TargetForm(ModelForm):
-    class Meta:
-        model = models.Target
-
-    class Index:
-        title = _('Targets')
-        all   = models.Target.objects.order_by('name').all
-        cols  = ['name', 'host']
-        links = [0,1]
-
-    clean_name = strip_and_nonblank('name')
-    clean_host = strip_and_nonblank('host')
-
-admin_targets = generic_handler(TargetForm, decorator=admin_only)
-###
-
-
-class NewsForm(ModelForm):
-    class Meta:
-        model = models.NewsItem
-
-    class Index:
-        title = _('News Items')
-        all   = models.NewsItem.objects.order_by('-id').all
-        cols  = ['id', 'subject', 'published']
-        links = [0, 1]
-
-    clean_subject = strip_and_nonblank('subject')
-    clean_body = strip_and_nonblank('body')
-
-admin_news = generic_handler(NewsForm, decorator=admin_only)
-
-
-
-#-----------------------------------------------------------------------------
-# Course feeds
-
- at public                         # and proud of it!
-def course_feeds(request, course_id, feed_type):
-    course = get_object_or_404(models.Course, pk=course_id)
-    if feed_type == '':
-        return g.render('feeds/course_feed_index.xhtml', 
-                        course=course)
-    else:
-        items = course.items()
-        def render_title(item):
-            return item.title
-        if feed_type == 'top-level':
-            items = items.filter(parent_heading=None).order_by('-sort_order')
-        elif feed_type == 'recent-changes':
-            items = items.order_by('-last_modified')
-        elif feed_type == 'tree':
-            def flatten(nodes, acc):
-                for node in nodes:
-                    item, kids = node
-                    acc.append(item)
-                    flatten(kids, acc)
-                return acc
-            items = flatten(course.item_tree(), [])
-            def render_title(item):
-                if item.parent_heading:
-                    return '%s :: %s' % (item.parent_heading.title, item.title)
-                else:
-                    return item.title
-        lastmod = items and max(i.last_modified for i in items) or datetime.now()
-        resp = g.render('feeds/course_atom.xml',
-                        course=course,
-                        feed_type=feed_type,
-                        lastmod=lastmod,
-                        render_title=render_title,
-                        items=items,
-                        root='http://%s' % request.get_host(),
-                        _serialization='xml')
-        resp['Content-Type'] = 'application/atom+xml'
-        return resp
-
-
-
-#------------------------------------------------------------
-# resequencing items
-
-def _reseq(request, course, parent_heading):
-    new_order = request.POST['new_order'].split(',')
-    # new_order is now a list like this: ['item_3', 'item_8', 'item_1', ...].
-    # get at the ints.
-    new_order = [int(n.split('_')[1]) for n in new_order]
-    print >> sys.stderr, new_order
-    the_items = list(course.item_set.filter(parent_heading=parent_heading).order_by('sort_order'))
-    # sort the items by position in new_order
-    the_items.sort(key=lambda item: new_order.index(item.id))
-    for newnum, item in enumerate(the_items):
-        item.sort_order = newnum
-        item.save()
-    return HttpResponse("'ok'");
-
- at instructors_only
-def course_reseq(request, course_id):
-    course = get_object_or_404(models.Course, pk=course_id)
-    parent_heading = None
-    return _reseq(request, course, parent_heading)
-
- at instructors_only
-def item_heading_reseq(request, course_id, item_id):
-    course = get_object_or_404(models.Course, pk=course_id)
-    item = get_object_or_404(models.Item, pk=item_id, course__id=course_id)
-    parent_heading = item
-    return _reseq(request, course, parent_heading)
-
-
- at instructors_only
-def item_relocate(request, course_id, item_id):
-    """Move an item from its current subheading to another one."""
-    course = get_object_or_404(models.Course, pk=course_id)
-    item = get_object_or_404(models.Item, pk=item_id, course__id=course_id)
-    if request.method != 'POST':
-        return g.render('item_relocate.xhtml', **locals())
-    else:
-        newheading = int(request.POST['heading'])
-        if newheading == 0:
-            new_parent = None
-        else:
-            new_parent = course.item_set.get(pk=newheading)
-            if item in new_parent.hierarchy():
-                # then we would create a cycle. Bail out.
-                return simple_message(_('Impossible item-move!'), 
-                                      _('You cannot make an item a descendant of itself!'))
-        item.parent_heading = new_parent
-        item.save()
-        if new_parent:
-            return HttpResponseRedirect(new_parent.item_url('meta'))
-        else:
-            return HttpResponseRedirect(course.course_url())
-        
-        
-
-#-----------------------------------------------------------------------------
-# Physical item processing
-
- at admin_only                     # fixme, is this the right permission?
-def phys_index(request):
-    return g.render('phys/index.xhtml')
-
- at admin_only                     # fixme, is this the right permission?
-def phys_checkout(request):
-    if request.method != 'POST':
-        return g.render('phys/checkout.xhtml', step=1)
-    else:
-        post = lambda k: request.POST.get(k, '').strip()
-        # dispatch based on what 'step' we are at.
-        step = post('step')     
-        func = {'1': _phys_checkout_get_patron,
-                '2':_phys_checkout_do_checkout,
-                '3':_phys_checkout_do_another,
-                }[step]
-        return func(request)
-
-def _phys_checkout_get_patron(request):
-    post           = lambda k: request.POST.get(k, '').strip()
-    patron, item   = post('patron'), post('item')
-    msg            = lib_integration.patron_info(patron)
-    if not msg['success']:
-        return simple_message(_('Invalid patron barcode'),
-                              _('No such patron could be found.'))
-    else:
-        patron_descrip = '%s (%s) &mdash; %s' % (
-            msg['personal'], msg['home_library'], msg['screenmsg'])
-        return g.render('phys/checkout.xhtml', step=2, 
-                        patron=patron, patron_descrip=patron_descrip)
-
-def _phys_checkout_do_checkout(request):
-    post           = lambda k: request.POST.get(k, '').strip()
-    patron, item   = post('patron'), post('item')
-    patron_descrip = post('patron_descrip')
-
-    # make sure the barcode actually matches with a known barcode in
-    # Syrup. We only checkout what we know about.
-    matches = models.Item.with_barcode(item)
-    if not matches:
-        is_successful = False
-        item_descrip  = None
-    else:
-        msg_status   = lib_integration.item_status(item)
-        msg_checkout = lib_integration.checkout(patron, item)
-        is_successful = msg_checkout['success']
-        item_descrip = '%s &mdash; %s' % (
-            msg_status['title'], msg_status['status'])
-
-    # log the checkout attempt.
-    log_entry = models.CheckInOut.objects.create(
-        is_checkout = True,
-        is_successful = is_successful,
-        staff = request.user,
-        patron = patron,
-        patron_descrip = patron_descrip,
-        item = item,
-        item_descrip = item_descrip)
-    log_entry.save()
-
-    if not matches:
-        return simple_message(
-            _('Item not found in Reserves'),
-            _('This item does not exist in the Reserves database! '
-              'Cannot check it out.'))
-    else:
-        return g.render('phys/checkout.xhtml', step=3, 
-                        patron=patron, item=item,
-                        patron_descrip=patron_descrip,
-                        checkout_result=msg_checkout,
-                        item_descrip=item_descrip)
-
-def _phys_checkout_do_another(request):
-    post           = lambda k: request.POST.get(k, '').strip()
-    patron         = post('patron')
-    patron_descrip = post('patron_descrip')
-    return g.render('phys/checkout.xhtml', step=2, 
-                    patron=patron,
-                    patron_descrip=patron_descrip)
-
-#------------------------------------------------------------
-
- at admin_only        
-def phys_mark_arrived(request):
-    if request.method != 'POST':
-        return g.render('phys/mark_arrived.xhtml')
-    else:
-        barcode = request.POST.get('item', '').strip()
-        already = models.PhysicalObject.by_barcode(barcode)
-        if already:
-            msg = _('This item has already been marked as received. Date received: %s')
-            msg = msg % str(already.received)
-            return simple_message(_('Item already marked as received'), msg)
-        bib_id  = lib_integration.barcode_to_bib_id(barcode)
-        if not bib_id:
-            return simple_message(_('Item not found'), 
-                                  _('No item matching this barcode could be found.'))
-
-        marcxml = lib_integration.bib_id_to_marcxml(bib_id)
-        dct     = marcxml_to_dictionary(marcxml)
-        dublin  = marcxml_dictionary_to_dc(dct)
-        # merge them
-        dct.update(dublin)
-        ranked = rank_pending_items(dct)
-        return g.render('phys/mark_arrived_choose.xhtml', 
-                        barcode=barcode,
-                        bib_id=bib_id,
-                        ranked=ranked,
-                        metadata=dct)
-
- at admin_only        
-def phys_mark_arrived_match(request):
-    choices = [int(k.split('_')[1]) for k in request.POST if k.startswith('choose_')]
-    if not choices:
-        return simple_message(_('No matching items selected!'),
-                              _('You must select one or more matching items from the list.'))
-    else:
-        barcode = request.POST.get('barcode', '').strip()
-        assert barcode
-        smallint = request.POST.get('smallint', '').strip() or None
-        try:
-            phys = models.PhysicalObject(barcode=barcode,
-                                         receiver = request.user,
-                                         smallint = smallint)
-            phys.save()
-        except Exception, e:
-            return simple_message(_('Error'), repr(e), go_back=True)
-
-        for c in choices:
-            item = models.Item.objects.get(pk=c)
-            current_bc = item.barcode()
-            if current_bc:
-                item.metadata_set.filter(name='syrup:barcode').delete()
-            item.metadata_set.create(name='syrup:barcode', value=barcode)
-            item.save()
-    return g.render('phys/mark_arrived_outcome.xhtml')
-
-
-def custom_500_handler(request):
-    cls, inst, tb = sys.exc_info()
-    msg = simple_message(_('Error: %s') % repr(inst),
-                         repr((request.__dict__, inst)))
-    return HttpResponse(msg._container, status=501)
-
-def custom_400_handler(request):
-    msg = simple_message(_('Not found'), 
-                          _('The page you requested could not be found'))
-    return HttpResponse(msg._container, status=404)



More information about the open-ils-commits mailing list