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