[open-ils-commits] r901 - in servres/trunk/conifer: doc/graphs syrup (gfawcett)
svn at svn.open-ils.org
svn at svn.open-ils.org
Wed Jul 14 20:53:02 EDT 2010
Author: gfawcett
Date: 2010-07-14 20:53:00 -0400 (Wed, 14 Jul 2010)
New Revision: 901
Added:
servres/trunk/conifer/syrup/user_lookup.py
Modified:
servres/trunk/conifer/doc/graphs/model_to_graphviz.py
servres/trunk/conifer/syrup/admin.py
servres/trunk/conifer/syrup/models.py
Log:
Model/schema overhaul. These are breaking changes. Application broken.
Modified: servres/trunk/conifer/doc/graphs/model_to_graphviz.py
===================================================================
--- servres/trunk/conifer/doc/graphs/model_to_graphviz.py 2010-07-06 15:17:12 UTC (rev 900)
+++ servres/trunk/conifer/doc/graphs/model_to_graphviz.py 2010-07-15 00:53:00 UTC (rev 901)
@@ -1,21 +1,29 @@
# Generate a graph visualization from the model. Requires Graphviz.
# To run, from conifer directory:
-# DJANGO_SETTINGS_MODULE=conifer.settings PYTHONPATH=.. python syrup/model_to_graphviz.py | fdp -Tpng > /tmp/syrup-model.png
+# DJANGO_SETTINGS_MODULE=conifer.settings PYTHONPATH=.. python doc/graphs/model_to_graphviz.py | dot -Tpng > /tmp/syrup-model.png
+# You might try 'fdp' instead of 'dot'; 'neato' and 'circo' give poorer results.
+import sys
import django.db.models
-#import conifer.syrup.models as models
-import conifer.syrup.direct_models as models
+import conifer.syrup.models as models
+#import conifer.syrup.direct_models as models
-from django.db.models.fields.related import ForeignRelatedObjectsDescriptor
+from django.db.models.fields.related import (ForeignRelatedObjectsDescriptor,
+ ManyRelatedObjectsDescriptor)
print 'digraph a {'
print '{ splines=true; }'
-print 'graph [ label="dotted-end is the foreign (\'many\') end", splines=true ]'
+print 'graph [ label="dotted-end is the \'many\' end", splines=true ]'
all = set(); linked = set(); primary = set()
for name, value in models.__dict__.items():
if isinstance(value, type) and issubclass(value, django.db.models.Model):
+ try:
+ if value._meta.abstract:
+ continue
+ except:
+ pass
local = value.__name__
all.add(local)
for k, v in value.__dict__.items():
@@ -23,6 +31,10 @@
foreign = v.related.model.__name__
print '%s -> %s [ arrowhead=dot, arrowtail=none ];' % (local, foreign)
primary.add(local); linked.add(local); linked.add(foreign)
+ if isinstance(v, ManyRelatedObjectsDescriptor):
+ foreign = v.related.model.__name__
+ print '%s -> %s [ arrowhead=dot, arrowtail=dot ];' % (local, foreign)
+ primary.add(local); linked.add(local); linked.add(foreign)
for n in (all - linked):
print '%s [ style=dashed ]' % n
Modified: servres/trunk/conifer/syrup/admin.py
===================================================================
--- servres/trunk/conifer/syrup/admin.py 2010-07-06 15:17:12 UTC (rev 900)
+++ servres/trunk/conifer/syrup/admin.py 2010-07-15 00:53:00 UTC (rev 901)
@@ -5,8 +5,9 @@
import django.db.models
from conifer.syrup.models import *
-for m in [LibraryUnit, ServiceDesk, Member, Department, Course, Term, UserProfile, NewsItem,
- Target, PhysicalObject]:
+for m in [ServiceDesk, Group, Membership, Course,
+ Department, ReadingList, Term,
+ UserProfile, Config, Z3950Target]:
admin.site.register(m)
Modified: servres/trunk/conifer/syrup/models.py
===================================================================
--- servres/trunk/conifer/syrup/models.py 2010-07-06 15:17:12 UTC (rev 900)
+++ servres/trunk/conifer/syrup/models.py 2010-07-15 00:53:00 UTC (rev 901)
@@ -1,7 +1,6 @@
from django.db import models as m
from django.contrib.auth.models import User
from django.contrib.auth.models import AnonymousUser
-from django.contrib.auth import get_backends
from datetime import datetime
from genshi import Markup
from django.utils.translation import ugettext as _
@@ -13,26 +12,18 @@
# campus and library integration
from django.conf import settings
campus = settings.CAMPUS_INTEGRATION
-from conifer.custom import lib_integration # fixme, not sure if conifer.custom is a good parent.
+# TODO: fixme, not sure if conifer.custom is a good parent.
+from conifer.custom import lib_integration
+#----------------------------------------------------------------------
-def highlight(text, phrase,
- highlighter='<strong class="highlight">\\1</strong>'):
- ''' This may be a lame way to do this, but will want to highlight matches somehow
+class BaseModel(m.Model):
+ class Meta:
+ abstract = True
- >>> highlight('The River is Wide', 'wide')
- 'The River is <strong class="highlight">Wide</strong>'
+ created = m.DateTimeField(auto_now_add=True)
+ last_modified = m.DateTimeField(auto_now=True)
- '''
- if not phrase or not text:
- return text
- highlight_re = re.compile('(%s)' % re.escape(phrase), re.I)
- if hasattr(text, '__html__'):
- return literal(highlight_re.sub(highlighter, text))
- else:
- return highlight_re.sub(highlighter, text)
-
-
#----------------------------------------------------------------------
# USERS
@@ -50,7 +41,7 @@
# the User object (although UserProfile would be another logical
# candidate).
-class UserExtensionHack(object):
+class UserExtensionMixin(object):
def courses(self):
return Course.objects.filter(member__user=self.id)
@@ -60,136 +51,110 @@
# We are using the Django is_active flag to model activeness.
return cls.objects.filter(member__role='INSTR', is_active=True) \
.order_by('-last_name','-first_name').distinct()
-
-for k,v in [(k,v) for k,v in UserExtensionHack.__dict__.items() \
+
+for k,v in [(k,v) for k,v in UserExtensionMixin.__dict__.items() \
if not k.startswith('_')]:
setattr(User, k, v)
+#------------------------------------------------------------
-class UserProfile(m.Model):
- user = m.ForeignKey(User, unique=True)
- home_phone = m.CharField(max_length=100, blank=True)
- home_address = m.TextField(blank=True)
- ils_userid = m.CharField(_('Alternate userid in the ILS, if any'),
- max_length=50, blank=True)
+class UserProfile(BaseModel):
+ user = m.ForeignKey(User)
+ ils_userid = m.CharField(_('ILS patron ID'),
+ max_length=50, blank=True, null=True)
# When we add email notices for new items, this is how we'll set
# the preference, and how far back we'll need to look.
wants_email_notices = m.BooleanField(default=False)
- last_email_notice = m.DateTimeField(default=datetime.now,
- blank=True, null=True)
+ last_email_notice = m.DateTimeField(
+ default=datetime.now, blank=True, null=True)
def __unicode__(self):
return 'UserProfile(%s)' % self.user
-#----------------------------------------------------------------------
-# Initializing an external user account
-# For usernames that come from external authentication sources (LDAP,
-# Evergreen, etc.) we need a general way to look up a user who may not
-# yet have a Django account. For example, you might want to add user
-# 'xsmith' as the instructor for a course. If 'xsmith' is in LDAP but
-# not yet in Django, it would be nice if a Django record were lazily
-# created for him upon lookup.
-
-# That's what 'maybe_initialize_user' is for: participating backends
-# provide a 'maybe_initialize_user' method which creates a new User
-# record if one doesn't exist. Otherwise, 'maybe_initialize_user' is
-# equivalent to 'User.objects.get(username=username)'.
-
-_backends_that_can_initialize_users = [
- be for be in get_backends() if hasattr(be, 'maybe_initialize_user')]
-
-def maybe_initialize_user(username):
- try:
- return User.objects.get(username=username)
- except User.DoesNotExist:
- for be in _backends_that_can_initialize_users:
- user = be.maybe_initialize_user(username, look_local=False)
- if user:
- return user
-
#----------------------------------------------------------------------
-# LIBRARIES, SERVICE DESKS
+# Lookup tables
-class LibraryUnit(m.Model):
- name = m.CharField(max_length=100)
- nickname = m.CharField(max_length=15,blank=True,default='')
- url = m.URLField()
- contact_email = m.EmailField()
+class ServiceDesk(BaseModel):
+ name = m.CharField(max_length=100)
+ active = m.BooleanField(default=True)
+ external_id = m.CharField(max_length=256)
def __unicode__(self):
return self.name
-class ServiceDesk(m.Model):
- library = m.ForeignKey(LibraryUnit)
- abbreviation = m.CharField(max_length=8,db_index=True)
- name = m.CharField(max_length=100)
+class Term(BaseModel):
+ code = m.CharField(max_length=64)
+ name = m.CharField(max_length=256)
+ start = m.DateField('Start (Y-M-D)')
+ finish = m.DateField('Finish (Y-M-D)')
+
+ def __unicode__(self):
+ return self.code or self.name
+
+class Department(BaseModel):
+ name = m.CharField(max_length=256)
active = m.BooleanField(default=True)
+ service_desk = m.ForeignKey(ServiceDesk)
def __unicode__(self):
return self.name
-#----------------------------------------------------------------------
-# TERMS, COURSES, MEMBERSHIP
+class Course(BaseModel):
+ """An abstract course (not a course offering.)"""
+ code = m.CharField(max_length=64)
+ name = m.CharField(max_length=1024)
+ department = m.ForeignKey(Department)
-class Term(m.Model):
- code = m.CharField(max_length=16, blank=True, null=True, unique=True)
- name = m.CharField(max_length=255)
- start = m.DateField('Start (Y-M-D)')
- finish = m.DateField('Finish (Y-M-D)')
-
def __unicode__(self):
- return self.code or self.name
+ return self.name
-class Department(m.Model):
- abbreviation = m.CharField(max_length=8,db_index=True)
- name = m.CharField(max_length=255)
- active = m.BooleanField(default=True)
+class Z3950Target(m.Model):
+ name = m.CharField(max_length=100)
+ host = m.CharField(max_length=50)
+ database = m.CharField(max_length=50)
+ port = m.IntegerField(default=210)
+ syntax = m.CharField(max_length=10, default='USMARC')
+ active = m.BooleanField(default=True)
def __unicode__(self):
return self.name
-class Course(m.Model):
- """An offering of a course."""
+class Config(m.Model):
+ name = m.CharField(max_length=256)
+ value = m.CharField(max_length=8192)
+
+#------------------------------------------------------------
+
+class ReadingList(BaseModel):
+ """A a list of materials for one (or more) course offering(s)."""
# some courses may be ad-hoc and have no code.
- code = m.CharField(max_length=64, blank=True, null=True)
- department = m.ForeignKey(Department)
- term = m.ForeignKey(Term)
- title = m.CharField(max_length=1024)
+ # TODO: constrain there is at least one course and one term (deferred).
+ courses = m.ManyToManyField(Course)
+ terms = m.ManyToManyField(Term)
+ owner = m.ForeignKey(User)
+ service_desk = m.ForeignKey(ServiceDesk)
+
access = m.CharField(max_length=5,
+ default='CLOSE',
choices = [
- ('ANON', _('World-accessible')),
- ('LOGIN', _('Accessible to all logged-in users')),
- ('STUDT', _('Accessible to course students (by section)')),
- ('INVIT', _('Accessible to course students (by invitation code)')),
- ('CLOSE', _('Accessible only to course owners'))],
- default='CLOSE')
+ ('ANON', _('World-accessible')),
+ ('LOGIN', _('Accessible to all logged-in users')),
+ ('STUDT', _('Accessible to course students (by section)')),
+ ('INVIT', _('Accessible to course students (by invitation code)')),
+ ('CLOSE', _('Accessible only to course owners'))])
# For sites that use a passkey as an invitation (INVIT access).
- passkey = m.CharField(db_index=True, blank=True, null=True, max_length=255)
+ # Note: only set this value using 'generate_new_passkey'.
+ # TODO: for postgres, add UNIQUE constraint on 'passkey'.
+ passkey = m.CharField(db_index=True, blank=True, null=True, max_length=256)
- # For sites that have registration-lists from an external system
- # (STUDT access).
- enrol_codes = m.CharField(_('Registrar keys for class lists'),
- max_length=4098,
- blank=True, null=True)
-
-
- def save(self, force_insert=False, force_update=False):
- # We need to override save() to ensure unique passkey
- # values. Django (and some backend databases) will not allow
- # multiple NULL values in a unique column.
- if self.passkey:
- try:
- already = Course.objects.exclude(pk=self.id).get(passkey=self.passkey)
- except Course.DoesNotExist:
- super(Course, self).save(force_insert, force_update)
- else:
- super(Course, self).save(force_insert, force_update)
-
def __unicode__(self):
- return self.code or self.title
+ cc = '%s' % (', '.join([c.code for c in self.courses]))
+ tt = '(%s)' % (', '.join([t.code for t in self.terms]))
+ oo = '(%s)' % self.owner.last_name
+ return u'%s %s %s' % (cc, tt, oo)
def list_display(self):
if self.code:
@@ -202,7 +167,6 @@
def headings(self):
"""A list of all items which are headings."""
- #fixme, not sure 'title' is the best ordering.
return self.item_set.filter(item_type='HEADING').order_by('title')
def item_tree(self, subtree=None):
@@ -219,7 +183,7 @@
"""
items = self.items()
# make a node-lookup table
- dct = {}
+ dct = {}
for item in items:
dct.setdefault(item.parent_heading, []).append(item)
for lst in dct.values():
@@ -235,15 +199,6 @@
walk(subtree, out)
return out
- def can_edit(self, user):
- if user.is_anonymous():
- return False
- try:
- mbr = Member.objects.get(course=self, user=user)
- except Member.DoesNotExist:
- return False
- return mbr.role in (u'INSTR', u'PROXY')
-
def course_url(self, suffix=''):
# I'm not fond of this being here. I think I'll leave this and
# item_url non-implemented, and monkey-patch them in views.py.
@@ -259,83 +214,103 @@
while True:
key = algorithm()
try:
- crs = Course.objects.get(passkey=key)
- except Course.DoesNotExist:
+ crs = ReadingList.objects.get(passkey=key)
+ except ReadingList.DoesNotExist:
self.passkey = key
break
- def sections(self):
- delim = campus.sections_tuple_delimiter
- if not delim:
- return []
- else:
- def inner():
- parts = self.enrol_codes.split(delim)
- while len(parts) > 2:
- yield tuple(parts[:3])
- del parts[:3]
- return set(inner())
+ #--------------------------------------------------
+ # membership-related functions
- def add_sections(self, *sections):
- assert all(len(s)==3 for s in sections), repr(sections)
- current = self.sections()
- sections = set(sections).union(current)
- self.enrol_codes = _merge_sections(sections)
+ def members(self):
+ return Membership.objects.filter(group__reading_list=self)
- def drop_sections(self, *sections):
- assert all(len(s)==3 for s in sections), repr(sections)
- current = self.sections()
- sections = current - set(sections)
- self.enrol_codes = _merge_sections(sections)
-
def get_students(self):
- return User.objects.filter(member__course__exact=self, member__role__exact='STUDT') \
- .order_by('last_name', 'first_name')
-
+ return self.memberships(role='STUDT').order_by(
+ 'user__last_name', 'user__first_name')
+
def get_instructors(self):
- return User.objects.filter(member__course__exact=self, member__role__exact='INSTR') \
- .order_by('last_name', 'first_name')
+ return self.memberships(role='INSTR').order_by(
+ 'user__last_name', 'user__first_name')
+ def can_edit(self, user):
+ if user.is_anonymous():
+ return False
+ if user.id == self.owner_id:
+ return True
+ try:
+ mbr = self.members.get(user=user)
+ except Member.DoesNotExist:
+ return False
+ return mbr.role in (u'INSTR', u'ASSIST')
+
def is_joinable_by(self, user):
"""Return True if the user could feasibly register into this
course: she's not already in it, and the course allows open
registration."""
- return user.is_authenticated() and self.access in ('ANON', 'LOGIN') and \
- not Member.objects.filter(user=user, course=self)
+ return user.is_authenticated() \
+ and self.access in ('ANON', 'LOGIN') \
+ and not user.id == self.owner_id \
+ and not self.members.filter(user=user).exists()
-def _merge_sections(secs):
- delim = campus.sections_tuple_delimiter
- return delim.join(delim.join(sec) for sec in secs)
+#------------------------------------------------------------
+# User membership in reading lists
-def section_decode_safe(secstring):
- if not secstring:
- return None
- return tuple(secstring.decode('base64').split(campus.sections_tuple_delimiter))
+class Group(BaseModel):
+ """
+ A group of users associated with a ReadingList. A ReadingList will
+ have one internal group, but may have zero or more external
+ groups.
-def section_encode_safe(section):
- return campus.sections_tuple_delimiter.join(section).encode('base64').strip()
+ Each ReadingList will have exactly one Group with a NULL
+ external_id, intended for internal memberships. It may have zero
+ or more Groups with non-NULL external_ids, representing various
+ external user-groups that should have access to this ReadingList.
-class Member(m.Model):
+ A consequence of this design is that a user may appear in a
+ ReadingList more than once, with different roles.
+
+ Note, a ReadingList may be open-access, but still have members
+ with 'student' access. In this case memberships won't imply
+ authorization, but can be used for personalization (e.g. to show a
+ list of "my reading lists").
+ """
+
+ # TODO: add constraints to ensure that each ReadingList has
+ # exactly one Group with external_id=NULL, and that (readinglist,
+ # external_id) is unique forall external_id != NULL.
+
+ reading_list = m.ForeignKey(ReadingList)
+ external_id = m.CharField(default=None, null=True, blank=True,
+ max_length=2048)
+
+ def __unicode__(self):
+ return u"Group('%s', '%s')" % (self.reading_list,
+ self.external_id or '(internal)')
+
+class Membership(BaseModel):
+
class Meta:
- unique_together = (('course', 'user'))
+ unique_together = (('group', 'user'))
- course = m.ForeignKey(Course)
user = m.ForeignKey(User)
+ group = m.ForeignKey(Group)
+
role = m.CharField(
choices = (
- ('INSTR', _('Instructor')),
- ('PROXY', _('Proxy Instructor')),
- ('STUDT', _('Student'))),
+ ('INSTR', _('Instructor')),
+ ('ASSIST', _('Assistant/Support')),
+ ('STUDT', _('Student'))),
default = 'STUDT',
- max_length = 5)
+ max_length = 6)
- # a user is 'provided' if s/he was added automatically due to
- # membership in an external registration system. The notion is
- # that these students can be automatically removed by add/drop
- # processors.
- provided = m.BooleanField(default=False)
+ def __unicode__(self):
+ return '%s; %s; %s' % (self.user, self.role, self.group)
+
+ # TODO: these belong elsewhere.
+
def instr_name_hl(self, terms):
hl_instr = self.user.last_name
for term in terms:
@@ -346,125 +321,119 @@
def instr_name(self):
return self.user.last_name
- def __unicode__(self):
- return '%s--%s--%s' % (self.user, self.role, self.course)
-
#------------------------------------------------------------
# ITEMS
-class Item(m.Model):
+class Item(BaseModel):
"""
A reserve item, physical or electronic, as it appears in a given
- course instance.
+ ReadingList instance. If an item appears on multiple reading
+ lists, it will have multiple Item records associated with it.
"""
-
+
# Structure
- # Items comprise both items-proper, as well as headings. In the
+ # Items comprise both items proper, as well as headings. In the
# database, all items are stored as a flat list; the sort_order
# dictates the sequencing of items within their parent group.
-
- course = m.ForeignKey(Course)
+
+ reading_list = m.ForeignKey(ReadingList)
+
ITEM_TYPE_CHOICES = (
('ELEC', _('Attached Electronic Document')), # PDF, Doc, etc.
('PHYS', _('Physical Book or Document')),
('URL', _('URL')),
('HEADING', _('Heading')))
+
item_type = m.CharField(max_length=7, choices=ITEM_TYPE_CHOICES)
- sort_order = m.IntegerField(default=0)
- # parent must be a heading. could use ForeignKey.limit_choices_to,
- # to enforce this in the admin ui.
- parent_heading = m.ForeignKey('Item', blank=True, null=True)
+ # TODO: If we want support for ephemeral physical objects, we
+ # might need to add a catalogue model for tracking local
+ # Ephemerals, and add an EPHEM item-type to refer to them. The
+ # contract would be that an ephmeral ID could be reused over time,
+ # and so it might resolve to the wrong item (or no item at all) if
+ # deferenced after the active timeframe of the ReadingList.
- # the display title may not be the same as the dc:title.
- title = m.CharField(max_length=255,db_index=True)
+ #--------------------------------------------------
+ # ILS integration
- # ditto the URL: this is for display items that are links.
- url = m.URLField(blank=True, null=True)
+ # Ultimately every physical Item needs a bib_id so that we can
+ # process ILS requests. During creation, the bib_id may be absent;
+ # we may only have a MARCXML record (e.g. if the item was
+ # discovered in an external catalogue.) Even that may not be true:
+ # we may only know a few DC attributes (title, creator) if the
+ # Item was manually entered.
- # for items of type ELEC (attached electronic document)
- fileobj = m.FileField(upload_to='uploads/%Y/%m/%d', max_length=255,
- blank=True, null=True, default=None)
+ # Remember that electronic (ELEC and URL) items may also have
+ # bib_id's and MARC records.
- fileobj_mimetype = m.CharField(max_length=128, blank=True, null=True, default=None)
+ bib_id = m.CharField('Bib Record ID', max_length=256, blank=True, null=True)
+ marcxml = m.TextField('MARCXML', blank=True, null=True)
- # basic timestamps
- date_created = m.DateTimeField(auto_now_add=True)
- last_modified = m.DateTimeField(auto_now=True)
+ # Fundamental metadata. These attributes may be populated from
+ # MARCXML or DC attributes. Even if these attributes exist in the
+ # metadata record, we copy them here so that we can efficiently
+ # display search-results in lists.
+ title = m.CharField(max_length=8192, db_index=True)
- # stuff I'm not sure about yet. I don't think it belongs here.
+ # Author(s) in Surname, given. Surname, given. format, for display.
+ author = m.CharField(max_length=8192, db_index=True,
+ null=True, blank=True)
- STATUS_CHOICE = (('INPROCESS', _('In Process')), # physical, pending
- ('ACTIVE', _('Active')), # available
- ('INACTIVE', _('Inactive'))) # no longer on rsrv.
- phys_status = m.CharField(max_length=9,
- null=True, blank=True,
- choices=STATUS_CHOICE,
- default=None) # null if not physical item.
+ # publisher: "Place: Publisher", as in a bibliography, for display.
+ publisher = m.CharField(max_length=8192, null=True, blank=True)
+ published = m.DateField(null=True, blank=True)
- activation_date = m.DateField(auto_now=False, blank=True, null=True)
- expiration_date = m.DateField(auto_now=False, blank=True, null=True)
-
- # requested_loan_period: why is this a text field?
- requested_loan_period = m.CharField(max_length=255,blank=True,default='', null=True)
+ ITEMTYPE_CHOICES = [
+ # From http://www.oclc.org/bibformats/en/fixedfield/type.shtm.
+ # It is hoped that this can be harvested from the 006 field in
+ # the MARC record.
+ (None, 'Unknown/not specified'),
+ ('a', 'Text'),
+ ('c', 'Notated music'),
+ ('d', 'Manuscript notated music'),
+ ('e', 'Cartographic material'),
+ ('f', 'Manuscript cartographic material'),
+ ('g', 'Projected medium'),
+ ('i', 'Sound recording: non-musical'),
+ ('j', 'Sound recording: musical'),
+ ('k', 'Two-dimensional graphic'),
+ ('m', 'Electronic resource'),
+ ('o', 'Kit'),
+ ('p', 'Mixed materials'),
+ ('r', 'Three-dimensional object'),
+ ('t', 'Manuscript'),
+ ]
+ itemtype = m.CharField('Type', max_length=1, db_index=True,
+ null=True, blank=True,
+ choices=ITEMTYPE_CHOICES)
+ # parent must be a heading. could use ForeignKey.limit_choices_to,
+ # to enforce this in the admin ui.
+ parent_heading = m.ForeignKey('Item', blank=True, null=True)
- def title_hl(self, terms):
- hl_title = self.title
- for term in terms:
- hl_title = highlight(hl_title,term)
+ #--------------------------------------------------
+ # Electronic items
- return hl_title
+ # For items of type URL
+ url = m.URLField(blank=True, null=True)
- def author(self):
- creators = self.metadata_set.filter(name='dc:creator')
- return creators and creators[0].value or None
+ # for items of type ELEC (attached electronic document)
+ fileobj = m.FileField(upload_to='uploads/%Y/%m/%d', max_length=255,
+ blank=True, null=True, default=None)
+ fileobj_mimetype = m.CharField(max_length=128, blank=True, null=True)
- def barcode(self):
- bc = self.metadata_set.filter(name='syrup:barcode')
- return bc and bc[0].value or None
-
- def marc(self):
- m = self.metadata_set.filter(name='syrup:marc')
- return m and simplejson.loads(m[0].value) or None
- def smallint(self):
- bc = self.barcode()
- phys = PhysicalObject.by_barcode(bc)
- return phys and phys.smallint or None
-
- @classmethod
- def with_smallint(cls, smallint):
- phys = PhysicalObject.by_smallint(smallint)
- barcode = phys and phys.barcode or None
- if not barcode:
- return cls.objects.filter(pk=-1) # empty set
- else:
- return cls.with_barcode(barcode)
-
- @classmethod
- def with_barcode(cls, barcode):
- return cls.objects.filter(metadata__name='syrup:barcode',
- metadata__value=barcode)
-
- def author_hl(self, terms):
- hl_author = self.author()
-
- for term in terms:
- hl_author = highlight(hl_author,term)
-
- return hl_author
-
-
def __unicode__(self):
return self.title
+
def hierarchy(self):
- """Return a list of items; the first is the topmost ancestor
- of this item in the heading hierarchy; the last is the current
+ """
+ Return a list of items: the first is the topmost ancestor of
+ this item in the heading hierarchy; the last is the current
item.
"""
if self.parent_heading is None:
@@ -477,7 +446,6 @@
def needs_meta_link(self):
"""Should an 'About' link be displayed for this item?"""
-
return self.item_type in ('ELEC', 'URL', 'PHYS')
def item_url(self, suffix='', force_local_url=False):
@@ -487,14 +455,14 @@
prefix = req.META['SCRIPT_NAME']
if self.item_type == 'ELEC' and suffix == '':
return '%s/course/%d/item/%d/dl/%s' % (
- prefix, self.course_id, self.id,
+ prefix, self.course_id, self.id,
self.fileobj.name.split('/')[-1])
if self.item_type == 'URL' and suffix == '' and not force_local_url:
return self.url
- else:
+ else:
return '%s/course/%d/item/%d/%s' % (
prefix, self.course_id, self.id, suffix)
-
+
def parent_url(self, suffix=''):
if self.parent_heading:
return self.parent_heading.item_url()
@@ -504,151 +472,41 @@
def describe_physical_item_status(self):
"""Return a (bool,str) tuple: whether the item is available,
and a friendly description of the physical item's status"""
- if self.item_type != 'PHYS':
- return False, _('(Not a physical item)')
- # An item is 'in Reserves' if we know its barcode, and if we
- # have a live PhysicalObject record for it.
- bc = self.barcode()
- if (not bc) or (not PhysicalObject.by_barcode(bc)):
- return False, _('On order')
- else:
- # We need to check with the ILS to see if anyone has it out.
- status = lib_integration.item_status(bc)
- return status['available'], _(status['status'])
+ # TODO: this needs to be reimplemented, based on copy detail
+ # lookup in the ILS. It also may not belong here!
+ raise NotImplementedError
-metadata_attributes = {
- 'dc:contributor': _('Contributor'),
- 'dc:coverage': _('Coverage'),
- 'dc:creator': _('Creator'),
- 'dc:date': _('Date'),
- 'dc:description': _('Description'),
- 'dc:format': _('Format'),
- 'dc:identifier': _('Identifier'),
- 'dc:language': _('Language'),
- 'dc:publisher': _('Publisher'),
- 'dc:relation': _('Relation'),
- 'dc:rights': _('Rights'),
- 'dc:source': _('Source'),
- 'dc:subject': _('Subject'),
- 'dc:title': _('Title'),
- 'dc:type': _('Type'),
- 'syrup:barcode': _('Barcode'),
- 'syrup:marc': _('MARC'), # MARC in JSON format.
- 'syrup:enumeration': _('Enumeration'),
- 'syrup:chronology': _('Chronology')}
+ # TODO: stuff I'm not sure about yet. I don't think it belongs here.
+ def title_hl(self, terms):
+ hl_title = self.title
+ for term in terms:
+ hl_title = highlight(hl_title,term)
-metadata_attribute_choices = metadata_attributes.items()
-metadata_attribute_choices.sort(key=lambda (a,b): b)
-class Metadata(m.Model):
- """Metadata for items."""
+ return hl_title
- item = m.ForeignKey(Item)
- #fixme, arbitrary sizes.
- name = m.CharField('Attribute', max_length=128, choices=metadata_attribute_choices)
- value = m.CharField(max_length=8192) # on postgres it doesn't matter how big.
+ def author_hl(self, terms):
+ hl_author = self.author()
-#------------------------------------------------------------
-# News items
+ for term in terms:
+ hl_author = highlight(hl_author,term)
-try:
- import markdown
- def do_markdown(txt):
- return markdown.markdown(txt)
-except ImportError:
- def do_markdown(txt):
- return _('(Markdown not installed).')
+ return hl_author
-class NewsItem(m.Model):
- subject = m.CharField(max_length=200)
- body = m.TextField()
- published = m.DateTimeField(default=datetime.now, blank=True, null=True)
- encoding = m.CharField(max_length=10,
- choices = (('plain', _('plain text')),
- ('html', _('HTML')),
- ('markdown', _('Markdown'))),
- default = 'plain')
- def __unicode__(self):
- return u'%s (%s)' % (self.subject, self.published)
+#------------------------------------------------------------
+# TODO: move this to a utility module.
- def generated_body(self):
- if self.encoding == 'plain':
- return self.body
- elif self.encoding == 'html':
- return Markup(self.body)
- elif self.encoding == 'markdown':
- return Markup(do_markdown(self.body))
-
-#----------------------------------------------------------------------
-# Z39.50 Support
-
-class Target(m.Model):
- name = m.CharField(max_length=100)
- host = m.CharField(max_length=50)
- db = m.CharField(max_length=50)
- port = m.IntegerField(default=210)
- syntax = m.CharField(max_length=10,default='USMARC')
- active = m.BooleanField(default=True)
-
- def __unicode__(self):
- return self.name
-
-#----------------------------------------------------------------------
-# SIP checkout
-
-class CheckInOut(m.Model):
- """A log of checkout-to-patron and item-return events."""
-
- is_checkout = m.BooleanField() # in or out?
- is_successful = m.BooleanField() # did the transaction work?
- staff = m.ForeignKey(User) # who processed the request?
- patron = m.CharField(max_length=100) # barcode
- patron_descrip = m.CharField(max_length=512) # ILS descrip
- item = m.CharField(max_length=100, null=True) # item barcode
- item_descrip = m.CharField(max_length=512, null=True)
- outcome = m.CharField(max_length=1024, null=True) # text msg from ILS about transaction
- processed = m.DateTimeField(auto_now_add=True)
-
-
-class PhysicalObject(m.Model):
- """A record of a physical object entering and leaving the Reserves area."""
- barcode = m.CharField(max_length=100) # item barcode
- receiver = m.ForeignKey(User, related_name='receiver') # who received the item?
- received = m.DateTimeField(auto_now_add=True)
- departer = m.ForeignKey(User, blank=True, null=True, related_name='departer') # who sent it away?
- departed = m.DateTimeField(blank=True, null=True)
- # an optional small-integer used as a human-shareable barcode by some institutions.
- smallint = m.IntegerField(blank=True, null=True)
-
- def __unicode__(self):
- return '%s (%s) %s' % (self.barcode, self.smallint, self.departed and 'gone' or 'live')
-
- def save(self, force_insert=False, force_update=False):
- # Must ensure that barcode is unique for non-departed items. Same with smallint
- live_objs = PhysicalObject.objects.exclude(pk=self.id).filter(departed=None)
- same_barcode = live_objs.filter(barcode=self.barcode)
- assert not same_barcode, \
- 'Barcode is not unique in active PhysicalObject collection.'
- if self.smallint:
- same_smallint = live_objs.filter(smallint=self.smallint)
- assert not same_smallint, \
- 'Small Number is not unique in active PhysicalObject collection.'
- super(PhysicalObject, self).save(force_insert, force_update)
-
- @classmethod
- def by_smallint(cls, smallint):
- """Find object by smallint, searching *only* the non-departed items."""
- assert smallint
- res = cls.objects.filter(departed=None, smallint=smallint)
- return res and res[0] or None
-
- @classmethod
- def by_barcode(cls, barcode):
- """Find object by barcode, searching *only* the non-departed items."""
- res = cls.objects.filter(departed=None, barcode=barcode)
- return res and res[0] or None
-
- @classmethod
- def live_objects(cls):
- return cls.objects.filter(departed=None)
+def highlight(text, phrase,
+ highlighter='<strong class="highlight">\\1</strong>'):
+ """
+ >>> highlight('The River is Wide', 'wide')
+ 'The River is <strong class="highlight">Wide</strong>'
+ """
+ if not phrase or not text:
+ return text
+ highlight_re = re.compile('(%s)' % re.escape(phrase), re.I)
+ if hasattr(text, '__html__'):
+ return literal(highlight_re.sub(highlighter, text))
+ else:
+ return highlight_re.sub(highlighter, text)
Added: servres/trunk/conifer/syrup/user_lookup.py
===================================================================
--- servres/trunk/conifer/syrup/user_lookup.py (rev 0)
+++ servres/trunk/conifer/syrup/user_lookup.py 2010-07-15 00:53:00 UTC (rev 901)
@@ -0,0 +1,32 @@
+from django.contrib.auth.models import User
+from django.contrib.auth import get_backends
+
+#----------------------------------------------------------------------
+# Initializing an external user account
+
+# TODO: does it make sense to put maybe_initialize_user on the
+# authentication backends, or does it belong somewhere else?
+
+# For usernames that come from external authentication sources (LDAP,
+# Evergreen, etc.) we need a general way to look up a user who may not
+# yet have a Django account. For example, you might want to add user
+# 'xsmith' as the instructor for a course. If 'xsmith' is in LDAP but
+# not yet in Django, it would be nice if a Django record were lazily
+# created for him upon lookup.
+
+# That's what 'maybe_initialize_user' is for: participating backends
+# provide a 'maybe_initialize_user' method which creates a new User
+# record if one doesn't exist. Otherwise, 'maybe_initialize_user' is
+# equivalent to 'User.objects.get(username=username)'.
+
+_backends_that_can_initialize_users = [
+ be for be in get_backends() if hasattr(be, 'maybe_initialize_user')]
+
+def maybe_initialize_user(username):
+ try:
+ return User.objects.get(username=username)
+ except User.DoesNotExist:
+ for be in _backends_that_can_initialize_users:
+ user = be.maybe_initialize_user(username, look_local=False)
+ if user:
+ return user
More information about the open-ils-commits
mailing list