From 28a90c3b761838ec83f3b429db60a0751343c897 Mon Sep 17 00:00:00 2001
From: gfawcett
Date: Tue, 27 Jul 2010 02:45:15 +0000
Subject: [PATCH] progress on sakai (linktool) association with reserves lists.
Still more to do on the instructor side, re: creating new reserves
Sites, or associating with existing ones.
git-svn-id: svn://svn.open-ils.org/ILS-Contrib/servres/trunk@938 6d9bc8c9-1ec2-4278-b937-99fde70a366f
---
conifer/integration/linktool/app.py | 47 ++++--
.../integration/linktool/templates/associate.xhtml | 18 +++
conifer/integration/linktool/templates/index.xhtml | 7 +-
.../linktool/templates/linktoolmaster.xhtml | 15 ++
.../integration/linktool/templates/whichsite.xhtml | 15 ++
conifer/integration/uwindsor.py | 5 +
conifer/syrup/external_groups.py | 93 +++++++++++
...eld_userprofile_external_memberships_checked.py | 171 +++++++++++++++++++++
conifer/syrup/models.py | 71 +++++++--
9 files changed, 420 insertions(+), 22 deletions(-)
create mode 100644 conifer/integration/linktool/templates/associate.xhtml
create mode 100644 conifer/integration/linktool/templates/linktoolmaster.xhtml
create mode 100644 conifer/integration/linktool/templates/whichsite.xhtml
create mode 100644 conifer/syrup/external_groups.py
create mode 100644 conifer/syrup/migrations/0004_auto__add_field_userprofile_external_memberships_checked.py
diff --git a/conifer/integration/linktool/app.py b/conifer/integration/linktool/app.py
index f2a1595..40cf88b 100644
--- a/conifer/integration/linktool/app.py
+++ b/conifer/integration/linktool/app.py
@@ -1,16 +1,19 @@
-from conifer.syrup.views._common import *
-from django.contrib.auth import authenticate, login
-from conifer.syrup import models
-from conifer.syrup.views import genshi_namespace
+from conifer.here import HERE
from conifer.plumbing.genshi_support import TemplateSet
+from django.http import (HttpResponse, HttpResponseRedirect,
+ HttpResponseNotFound,
+ HttpResponseForbidden)
+from django.contrib.auth import authenticate, login
+from conifer.syrup import models
+from django.utils.translation import ugettext as _
+from conifer.plumbing.hooksystem import gethook, callhook
g = TemplateSet([HERE('integration/linktool/templates'),
HERE('templates')],
- genshi_namespace)
+ {'models': models, '_': _})
def linktool_welcome(request, command=u''):
- #return g.render('index.xhtml')
user = authenticate(request=request)
if user is None:
return HttpResponseForbidden('You are not allowed here.')
@@ -18,8 +21,32 @@ def linktool_welcome(request, command=u''):
login(request, user)
extsite = request.session['clew-site'] = request.GET['site']
extrole = request.session['clew-role'] = request.GET['role']
+ related_sites = list(models.Site.objects.filter(group__external_id=extsite))
+ if len(related_sites) == 1:
+ return HttpResponse("
"
+ "Redirecting..."
+ "" % (
+ request.META['SCRIPT_NAME'] or '/',
+ related_sites[0].id))
+ elif len(related_sites):
+ return g.render('whichsite.xhtml', **locals())
+ elif extrole == 'Instructor':
+ # This isn't quite right yet. I want to give the instructor a
+ # chance to associate with an existing unassociated site in the
+ # same term as the Sakai site. I don't want them to associate with
+ # a site that's in an older Term, but should give them the change
+ # to copy/reuse an old site. Or, they can make a brand-new Site if
+ # they want.
+
+ # This reminds me that it should be a warning to edit an old site
+ # (one in a past Term). Otherwise, profs will add items to old
+ # sites, and think they are actually ordering stuff.
+
+ possibles = models.Site.taught_by(user)
+ if possibles:
+ return g.render('associate.xhtml', **locals())
+ else:
+ return g.render('create_new.xhtml', **locals())
+ else:
+ return g.render('choose_dest.xhtml', **locals())
-
- return HttpResponse(""""""
- """Redirecting to the library reserves system...""" % (
- request.META['SCRIPT_NAME'] or '/'))
diff --git a/conifer/integration/linktool/templates/associate.xhtml b/conifer/integration/linktool/templates/associate.xhtml
new file mode 100644
index 0000000..d162dba
--- /dev/null
+++ b/conifer/integration/linktool/templates/associate.xhtml
@@ -0,0 +1,18 @@
+
+
+
+ Associate?
+ There is currently no set of reserves materials associated with
+ this site. As an instructor in this site, you can choose one of
+ the following options:
+ todo: finish this...
+
+
+
diff --git a/conifer/integration/linktool/templates/index.xhtml b/conifer/integration/linktool/templates/index.xhtml
index bd88c1e..8917db5 100644
--- a/conifer/integration/linktool/templates/index.xhtml
+++ b/conifer/integration/linktool/templates/index.xhtml
@@ -4,8 +4,13 @@
testing
+
-testing
+Welcome
+${repr(list(related_sites))}
diff --git a/conifer/integration/linktool/templates/linktoolmaster.xhtml b/conifer/integration/linktool/templates/linktoolmaster.xhtml
new file mode 100644
index 0000000..7bcb8c3
--- /dev/null
+++ b/conifer/integration/linktool/templates/linktoolmaster.xhtml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+ ${select('*')}
+
+
+
+
diff --git a/conifer/integration/linktool/templates/whichsite.xhtml b/conifer/integration/linktool/templates/whichsite.xhtml
new file mode 100644
index 0000000..f121df1
--- /dev/null
+++ b/conifer/integration/linktool/templates/whichsite.xhtml
@@ -0,0 +1,15 @@
+
+
+
+ Please choose a set of reserves materials
+ There is more than one set of reserves materials related to this
+ site. Please choose from the list below:
+
+
+
diff --git a/conifer/integration/uwindsor.py b/conifer/integration/uwindsor.py
index 664a3ad..c46f09e 100644
--- a/conifer/integration/uwindsor.py
+++ b/conifer/integration/uwindsor.py
@@ -121,3 +121,8 @@ def external_person_lookup(userid):
return uwindsor_campus_info.call('person_lookup', userid)
+def external_memberships(userid, include_titles=False):
+ memberships = uwindsor_campus_info.call('membership_ids', userid)
+ for m in memberships:
+ m['role'] = 'INSTR' if m['role'] == 'Instructor' else 'STUDT'
+ return memberships
diff --git a/conifer/syrup/external_groups.py b/conifer/syrup/external_groups.py
new file mode 100644
index 0000000..3096d01
--- /dev/null
+++ b/conifer/syrup/external_groups.py
@@ -0,0 +1,93 @@
+# -*- encoding: utf-8 -*-
+
+from conifer.syrup.models import *
+from conifer.plumbing.hooksystem import callhook
+
+VALID_ROLES = [code for code, desc in Membership.ROLE_CHOICES]
+
+#-----------------------------------------------------------------------------
+
+def reconcile_user_memberships(user):
+ """
+ Polling an externally-defined system to find out what externally-defined
+ groups this user belongs to, perform a series of "adds" and "drops" in any
+ internal groups that mirror those external groups.
+ """
+
+ # This function may be called frequently, so it's important that it runs
+ # efficiently. In the case where no group-memberships change, this
+ # function should execute no more than two SQL queries: one to fetch the
+ # list of 'interesting' groups, and another to fetch the list of the
+ # user's current memberships. (Of course it's also important that the
+ # external hook-function runs as efficiently as possible, but that's
+ # outside of our scope.)
+
+ # The 'external_memberships' hook function must return a list of
+ # (groupcode, role) tuples (assuming the hook function has been defined;
+ # otherwise, the hook system will return None). All of our membership
+ # comparisons are based on groupcodes, which internally are stored in the
+ # Group.external_id attribute. We only consider roles if we are adding a
+ # user to a group.
+
+ # This design assumes (but does not assert) that each groupcode is
+ # associated with exactly zero or one internal Groups. Specifically, you
+ # will get unexpected results if (a) there are multiple Groups with the
+ # same code, and (b) the user is currently a member of some of those
+ # Groups, but not others.
+
+ _externals = callhook('external_memberships', user.username) or []
+ externals = [(e['group'], e['role']) for e in _externals]
+ extgroups = set(g for g, role in externals)
+ role_lookup = dict(externals) # a map of groupcodes to roles.
+ assert all((role in VALID_ROLES) for g, role in externals)
+
+ # What group-codes are currently 'of interest' (i.e., in use in the
+ # system) and in which groups is the user already known to be a member?
+
+ _base = Group.objects.filter(external_id__isnull=False)
+ of_interest = set(r[0] for r in _base.values_list('external_id'))
+ current = set(r[0] for r in _base.filter(membership__user=user) \
+ .values_list('external_id'))
+
+ # to_add: external ⩠of_interest ⩠¬current
+ # to_drop: current ⩠of_interest ⩠¬external
+
+ to_add = extgroups.intersection(of_interest).difference(current)
+ to_drop = current.intersection(of_interest).difference(extgroups)
+
+ # Since we assert that external groupcodes can be associated with no more
+ # than one internal Group, an external groupcode can be used as a map to
+ # exactly zero or one internal Groups. We take care that 'to_add_groups'
+ # does not include any groups in which the user is already a member, which
+ # is imposible under this assertion. This is just a bit of defensive
+ # programming, in case the larger algorithm is changed.
+
+ to_add_groups = Group.objects.filter(
+ ~Q(membership__user=user), # "user is not already a member"
+ external_id__in=to_add)
+
+ # process the adds and drops.
+
+ for group in to_add_groups:
+ Membership.objects.create(group=group, user=user,
+ role=role_lookup[group.external_id])
+
+ to_drop_memberships = Membership.objects.filter(
+ group__external_id__in=to_drop,
+ user=user)
+ to_drop_memberships.delete()
+
+ return (to_add, to_drop)
+
+
+
+if __name__ == '__main__':
+ from django.db import connection
+ from pprint import pprint
+
+ user = User.objects.get(username='fawcett')
+ add, drop = reconcile_user_memberships(user)
+ print (add, drop)
+
+ for q in connection.queries:
+ pprint(q)
diff --git a/conifer/syrup/migrations/0004_auto__add_field_userprofile_external_memberships_checked.py b/conifer/syrup/migrations/0004_auto__add_field_userprofile_external_memberships_checked.py
new file mode 100644
index 0000000..a7ce060
--- /dev/null
+++ b/conifer/syrup/migrations/0004_auto__add_field_userprofile_external_memberships_checked.py
@@ -0,0 +1,171 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+
+ # Adding field 'UserProfile.external_memberships_checked'
+ db.add_column('syrup_userprofile', 'external_memberships_checked', self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True), keep_default=False)
+
+
+ def backwards(self, orm):
+
+ # Deleting field 'UserProfile.external_memberships_checked'
+ db.delete_column('syrup_userprofile', 'external_memberships_checked')
+
+
+ models = {
+ 'auth.group': {
+ 'Meta': {'object_name': 'Group'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'blank': 'True'})
+ },
+ 'auth.permission': {
+ 'Meta': {'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+ },
+ 'auth.user': {
+ 'Meta': {'object_name': 'User'},
+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}),
+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'blank': 'True'}),
+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+ },
+ 'contenttypes.contenttype': {
+ 'Meta': {'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ },
+ 'syrup.config': {
+ 'Meta': {'object_name': 'Config'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '256'}),
+ 'value': ('django.db.models.fields.CharField', [], {'max_length': '8192'})
+ },
+ 'syrup.course': {
+ 'Meta': {'object_name': 'Course'},
+ 'code': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '64'}),
+ 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'department': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['syrup.Department']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'last_modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '1024'})
+ },
+ 'syrup.department': {
+ 'Meta': {'object_name': 'Department'},
+ 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}),
+ 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'last_modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '256'}),
+ 'service_desk': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['syrup.ServiceDesk']"})
+ },
+ 'syrup.group': {
+ 'Meta': {'object_name': 'Group'},
+ 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'external_id': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '2048', 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'last_modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
+ 'site': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['syrup.Site']"})
+ },
+ 'syrup.item': {
+ 'Meta': {'object_name': 'Item'},
+ 'author': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '8192', 'null': 'True', 'blank': 'True'}),
+ 'bib_id': ('django.db.models.fields.CharField', [], {'max_length': '256', 'null': 'True', 'blank': 'True'}),
+ 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'fileobj': ('django.db.models.fields.files.FileField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}),
+ 'fileobj_mimetype': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'item_type': ('django.db.models.fields.CharField', [], {'max_length': '7'}),
+ 'itemtype': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '1', 'null': 'True', 'blank': 'True'}),
+ 'last_modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
+ 'marcxml': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+ 'parent_heading': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['syrup.Item']", 'null': 'True', 'blank': 'True'}),
+ 'published': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'}),
+ 'publisher': ('django.db.models.fields.CharField', [], {'max_length': '8192', 'null': 'True', 'blank': 'True'}),
+ 'site': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['syrup.Site']"}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '8192', 'db_index': 'True'}),
+ 'url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'})
+ },
+ 'syrup.membership': {
+ 'Meta': {'unique_together': "(('group', 'user'),)", 'object_name': 'Membership'},
+ 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['syrup.Group']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'last_modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
+ 'role': ('django.db.models.fields.CharField', [], {'default': "'STUDT'", 'max_length': '6'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+ },
+ 'syrup.servicedesk': {
+ 'Meta': {'object_name': 'ServiceDesk'},
+ 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}),
+ 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'external_id': ('django.db.models.fields.CharField', [], {'max_length': '256', 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'last_modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'})
+ },
+ 'syrup.site': {
+ 'Meta': {'unique_together': "(('course', 'term', 'owner'),)", 'object_name': 'Site'},
+ 'access': ('django.db.models.fields.CharField', [], {'default': "'ANON'", 'max_length': '5'}),
+ 'course': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['syrup.Course']"}),
+ 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'last_modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
+ 'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
+ 'service_desk': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['syrup.ServiceDesk']"}),
+ 'term': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['syrup.Term']"})
+ },
+ 'syrup.term': {
+ 'Meta': {'object_name': 'Term'},
+ 'code': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '64'}),
+ 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'finish': ('django.db.models.fields.DateField', [], {}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'last_modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '256'}),
+ 'start': ('django.db.models.fields.DateField', [], {})
+ },
+ 'syrup.userprofile': {
+ 'Meta': {'object_name': 'UserProfile'},
+ 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'external_memberships_checked': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'ils_userid': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'blank': 'True'}),
+ 'last_email_notice': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'null': 'True', 'blank': 'True'}),
+ 'last_modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
+ 'wants_email_notices': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'})
+ },
+ 'syrup.z3950target': {
+ 'Meta': {'object_name': 'Z3950Target'},
+ 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}),
+ 'database': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
+ 'host': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'port': ('django.db.models.fields.IntegerField', [], {'default': '210'}),
+ 'syntax': ('django.db.models.fields.CharField', [], {'default': "'USMARC'", 'max_length': '10'})
+ }
+ }
+
+ complete_apps = ['syrup']
diff --git a/conifer/syrup/models.py b/conifer/syrup/models.py
index 9a7fb51..8a903ba 100644
--- a/conifer/syrup/models.py
+++ b/conifer/syrup/models.py
@@ -1,10 +1,11 @@
import random
import re
+from collections import defaultdict
from conifer.libsystems import marcxml as MX
from conifer.plumbing.genshi_support import get_request
from conifer.plumbing.hooksystem import *
-from datetime import datetime
+from datetime import datetime, timedelta
from django.conf import settings
from django.contrib.auth.models import AnonymousUser, User
from django.db import models as m
@@ -40,7 +41,9 @@ class BaseModel(m.Model):
# candidate).
class UserExtensionMixin(object):
+
def sites(self):
+ self.maybe_refresh_external_memberships()
return Site.objects.filter(group__membership__user=self.id)
def can_create_sites(self):
@@ -50,6 +53,12 @@ class UserExtensionMixin(object):
def get_list_name(self):
return '%s, %s' % (self.last_name, self.first_name)
+ # this is an override of User.get_profile. The original version will not
+ # create a UserProfile which does not already exist.
+ def get_profile(self):
+ profile, just_created = UserProfile.objects.get_or_create(user=self)
+ return profile
+
@classmethod
def active_instructors(cls):
"""Return a queryset of all active instructors."""
@@ -58,6 +67,24 @@ class UserExtensionMixin(object):
.order_by('-last_name','-first_name').distinct()
+ # --------------------------------------------------
+ # Membership in external groups
+
+ EXT_MEMBERSHIP_CHECK_FREQUENCY = timedelta(seconds=3600)
+
+ def maybe_refresh_external_memberships(self):
+ profile = self.get_profile()
+ last_checked = profile.external_memberships_checked
+ if (not last_checked or last_checked <
+ (datetime.now() - self.EXT_MEMBERSHIP_CHECK_FREQUENCY)):
+ added, dropped = external_groups.reconcile_user_memberships(self)
+ profile.external_memberships_checked = datetime.now()
+ profile.save()
+ return (added or dropped)
+
+ def external_memberships(self):
+ return callhook('external_memberships', self.username) or []
+
for k,v in [(k,v) for k,v in UserExtensionMixin.__dict__.items() \
if not k.startswith('_')]:
@@ -77,6 +104,9 @@ class UserProfile(BaseModel):
last_email_notice = m.DateTimeField(
default=datetime.now, blank=True, null=True)
+ # when did we last check user's membership in externally-defined groups?
+ external_memberships_checked = m.DateTimeField(blank=True, null=True)
+
def __unicode__(self):
return 'UserProfile(%s)' % self.user
@@ -217,7 +247,8 @@ class Site(BaseModel):
dct.setdefault(item.parent_heading, []).append(item)
for lst in dct.values():
# TODO: what's the sort order?
- lst.sort(key=lambda item: (item.item_type=='HEADING', item.title)) # sort in place
+ lst.sort(key=lambda item: (item.item_type=='HEADING',
+ item.title)) # sort in place
# walk the tree
out = []
def walk(parent, accum):
@@ -298,6 +329,12 @@ class Site(BaseModel):
or user.is_staff \
or self.is_member(user)
+ @classmethod
+ def taught_by(cls, user):
+ """Return a set of Sites for which this user is an Instructor."""
+ return cls.objects.filter(group__membership__user=user,
+ group__membership__role='INSTR')
+
#--------------------------------------------------
@classmethod
@@ -340,6 +377,10 @@ class Group(BaseModel):
# TODO: add constraints to ensure that each Site has
# exactly one Group with external_id=NULL, and that (site,
# external_id) is unique forall external_id != NULL.
+
+ # TODO: On second thought, for now make it:
+ # external_id is unique forall external_id != NULL.
+ # That is, only one Site may use a given external group.
site = m.ForeignKey(Site)
external_id = m.CharField(null=True, blank=True,
@@ -358,11 +399,13 @@ class Membership(BaseModel):
user = m.ForeignKey(User)
group = m.ForeignKey(Group)
+ ROLE_CHOICES = (
+ ('INSTR', _('Instructor')),
+ ('ASSIST', _('Assistant/Support')),
+ ('STUDT', _('Student')))
+
role = m.CharField(
- choices = (
- ('INSTR', _('Instructor')),
- ('ASSIST', _('Assistant/Support')),
- ('STUDT', _('Student'))),
+ choices = ROLE_CHOICES,
default = 'STUDT',
max_length = 6)
@@ -494,7 +537,7 @@ class Item(BaseModel):
# MARC
def marc_as_dict(self):
return MX.record_to_dictionary(self.marcxml)
-
+
def marc_dc_subset(self):
return json.dumps(self.marc_as_dict())
@@ -554,8 +597,9 @@ class Item(BaseModel):
else:
lib, desk, avail = stat
return (avail > 0,
- '%d of %d copies available at reserves desk; %d total copies in library system' % (
- avail, desk, lib))
+ '%d of %d copies available at reserves desk; '
+ '%d total copies in library system'
+ % (avail, desk, lib))
# TODO: stuff I'm not sure about yet. I don't think it belongs here.
@@ -588,8 +632,8 @@ class Item(BaseModel):
else:
return (Q(site__access__in=('LOGIN','ANON')) \
| Q(site__group__membership__user=user))
-
-
+
+
#------------------------------------------------------------
# TODO: move this to a utility module.
@@ -616,3 +660,8 @@ if hasattr(settings, 'INTEGRATION_MODULE'):
if callable(v):
setattr(conifer.syrup.integration, k, v)
+
+#-----------------------------------------------------------------------------
+# this can't be imported until Membership is defined...
+
+import external_groups
--
2.11.0