xsip
TAGS
private_local_settings.py
+/conifer/.dired
--- /dev/null
+* Tasks for the =2010-02-campus-integration-reorg= branch
+
+ The goal of this branch is to reorganize and document the two major
+ integration points in Syrup: the library systems and the campus
+ information systems. Both of these integrations existed prior to the
+ branch, but were undocumented and messy.
+
+** The Evergreen-or-not question.
+ - Prepare to sync with the =eg-schema-experiment= branch
+ - "in evergreen database" vs. "other database with OpenSRF calls"
+
+** Enumerate the ways that campus integration is currently used.
+ Put this in the campus-integration documentation.
+
+** A Library Integration module which is readable and documented
+ - integrate via local_settings.py
+ - Prepare to sync with the =eg-schema-experiment= branch
+
+** How much of the integration data belongs in the database?
+ Should the Django ADMINS list be pulled from the db? What about
+ Z39.50 targets? What are the deciding principles when figuring out
+ where to store config data?
+
+** Campus integration for departments.
+ - how to address the faculty/campus/dept/etc. hierarchy?
+ - list of departments
+ - look up department based on course-code
+ - instructors in a given department
+
+** question: Campus integration for terms?
+ Even just a "feed of terms you might not yet know about?"
+
+** question: when looking up membership info, always update membership table?
+ Should just asking an external campus system, 'What sections is
+ John in?' automatically add membership records for John, for
+ course-sites related to those sections? Should it also (only during
+ the active period of a term) drop John from current sections that
+ he's no longer part of?
--- /dev/null
+This directory is going away.
+
+Default integrations are being moved to 'conifer.integration'. The
+active integration modules are to be specified in local_settings.
+++ /dev/null
-# Validation and lookup of course codes.
-
-# This modules specifies an "course-code interface" and a null
-# implementation of that interface. If your local system has rules for
-# valid course codes, and a mechanism for looking up details of these
-# codes, you can implement the interface according to your local
-# rules.
-
-
-# ------------------------------------------------------------
-# Overview and definitions
-
-# A course code identifies a specific course offering. Course codes
-# map 1:N onto formal course titles: by looking up a code, we can
-# derive a formal title (in theory, though it may not be possible for
-# external reasons).
-
-# A course code is insufficient to specify a class list: we need a
-# course section for that. A section ties a course code and term to an
-# instructor(s) and a list of students.
-
-# Course codes may have cross-listings, i.e., other codes which refer
-# to the same course, but which appear under a different department
-# for various academic purposes. In our system, we make no attempt to
-# subordinate cross-listings to a "primary" course code.
-
-
-#------------------------------------------------------------
-# Notes on the interface
-#
-# The `course_code_is_valid` function will be used ONLY if
-# course_code_list() returns None (it is a null implementation). If a
-# course-list is available, the system will use a membership test for
-# course-code validity.
-#
-# `course_code_lookup_title` will be used ONLY if `course_code_list`
-# is implemented.
-#
-#
-# "types" of the interface members
-#
-# course_code_is_valid (string) --> boolean.
-# course_code_example : a string constant.
-# course_code_list () --> list of strings
-# course_code_lookup_title (string) --> string, or None.
-# course_code_cross_listings (string) --> list of strings
-#
-# For each member, you MUST provide either a valid implementation, or
-# set the member to None. See the null implementation below.
-
-#------------------------------------------------------------
-# Implementations
-
-# ------------------------------------------------------------
-# Here is a 'null implementation' of the course-code interface. No
-# validation is done, nor are lookups.
-#
-# course_code_is_valid = None # anything is OK;
-# course_code_example = None # no examples;
-# course_code_lookup_title = None # no codes to list;
-# course_code_cross_listings = None # no cross lists.
-
-# ------------------------------------------------------------
-# This one specifies a valid course-code format using a regular
-# expression, and offers some example codes, but does not have a
-# lookup system.
-#
-# import re
-#
-# def course_code_is_valid(course_code):
-# pattern = re.compile(r'^\d{2}-\d{3}$')
-# return bool(pattern.match(course_code))
-#
-# course_code_example = '55-203; 99-105'
-#
-# course_code_list = None
-# course_code_lookup_title = None
-# course_code_cross_listings = None
-
-
-
-# ------------------------------------------------------------
-# This is a complete implementation, based on a hard-coded list of
-# course codes and titles, and two cross-listed course codes.
-#
-# _codes = [('ENG100', 'Introduction to English'),
-# ('ART108', 'English: An Introduction'),
-# ('FRE238', 'Modern French Literature'),
-# ('WEB203', 'Advanced Web Design'),]
-#
-# _crosslists = set(['ENG100', 'ART108'])
-#
-# course_code_is_valid = None
-# course_code_example = 'ENG100; FRE238'
-#
-# def course_code_list():
-# return [a for (a,b) in _codes]
-#
-# def course_code_lookup_title(course_code):
-# return dict(_codes).get(course_code)
-#
-# def course_code_cross_listings(course_code):
-# if course_code in _crosslists:
-# return list(_crosslists - set([course_code]))
-
-
-# ------------------------------------------------------------
-# Provide your own implementation below.
-
-
-#_codes = [('ENG100', 'Introduction to English'),
-# ('ART108', 'English: An Introduction'),
-# ('FRE238', 'Modern French Literature'),
-# ('LIB201', 'Intro to Library Science'),
-# ('WEB203', 'Advanced Web Design'),]
-
-_codes = [('ART99-100', 'Art History'),
- ('BIOL55-350', 'Molecular Cell Biology'),
- ('CRIM48-567', 'Current Issues in Criminology'),
- ('ENGL26-280', 'Contemporary Literary Theory'),
- ('ENGL26-420', 'Word and Image: The Contemporary Graphic Novel'),
- ('SOCWK47-457', 'Advanced Social Work Research'),]
-
-_crosslists = set(['ENGL26-280', 'ENGL26-420'])
-
-
-course_code_is_valid = None
-
-course_code_example = 'BIOL55-350; SOCWK47-457'
-
-def course_code_list():
- return [a for (a,b) in _codes]
-
-def course_code_lookup_title(course_code):
- return dict(_codes).get(course_code)
-
-def course_code_cross_listings(course_code):
- if course_code in _crosslists:
- return list(_crosslists - set([course_code]))
-
+++ /dev/null
-# Operations on course-section identifiers
-
-# A course section is an instance of a course offered in a term.
-
-# A section is specified by a 'section-id', a 3-tuple (course-code,
-# term, section-code), where section-code is usually a short
-# identifier (e.g., "1" representing "section 1 in this term"). Note
-# that multiple sections of the same course are possible in a given
-# term.
-
-# Within the reserves system, a course-site can be associated with
-# zero or more sections, granting access to students in those
-# sections. We need two representations of a section-id.
-
-# The section_tuple_delimiter must be a string which will never appear
-# in a course-code, term, or section-code in your database. It may be
-# a nonprintable character (e.g. NUL or CR). It is used to delimit
-# parts of the tuples in a course's database record.
-
-#------------------------------------------------------------
-# Notes on the interface
-#
-# 'sections_taught_by(username)' returns a set of sections for which
-# username is an instructor. It is acceptable if 'sections_taught_by'
-# only returns current and future sections: historical information is
-# not required by the reserves system.
-#
-# It is expected that the reserves system will be able to resolve any
-# usernames into user records. If there are students on a section-list
-# which do not resolve into user accounts, they will probably be
-# ignored and will not get access to their course sites. So if you're
-# updating your users and sections in a batch-run, you might want to
-# update your users first.
-#
-#------------------------------------------------------------
-# Implementations
-
-# The reserves system will work with a null-implementation of the
-# course-section interface, but tasks related to course-sections will
-# be unavailable.
-
-# ------------------------------------------------------------
-# The null implementation:
-#
-# sections_tuple_delimiter = None
-# sections_taught_by = None
-# students_in = None
-# instructors_in = None
-# sections_for_code_and_term = None
-
-# ------------------------------------------------------------
-#
-# The minimal non-null implementation. At the least you must provide
-# sections_tuple_delimiter and students_in. Lookups for instructors
-# may be skipped. Note that sections passed to students_in are
-# (term, course-code, section-code) tuples (string, string, string).
-#
-# sections_tuple_delimiter = '|'
-#
-# def students_in(*sections):
-# ...
-# return set_of_usernames
-#
-# instructors_in = None
-# sections_for_code_and_term = None
-
-# ------------------------------------------------------------
-# A complete implementation, with a static database.
-
-# sections_tuple_delimiter = '|'
-#
-# _db = [
-# ('fred', ('2009W', 'ENG203', '1'), 'jim joe jack ellen ed'),
-# ('fred', ('2009W', 'ENG327', '1'), 'ed paul bill'),
-# ('bill', ('2009S', 'BIO323', '1'), 'alan june jack'),
-# ('bill', ('2009S', 'BIO323', '2'), 'emmet'),
-# ]
-#
-# def sections_taught_by(username):
-# return set([s[1] for s in _db if s[0] == username])
-#
-# def students_in(*sections):
-# def inner():
-# for instr, sec, studs in _db:
-# if sec in sections:
-# for s in studs.split(' '):
-# yield s
-# return set(inner())
-#
-# def instructors_in(*sections):
-# def inner():
-# for instr, sec, studs in _db:
-# if sec in sections:
-# yield instr
-# return set(inner())
-#
-# def sections_for_code_and_term(code, term):
-# return [(t, c, s) for (instr, (t, c, s), ss) in _db \
-# if c == code and t == term]
-#
-
-
-# ------------------------------------------------------------
-# Provide your own implementation below.
-
-sections_tuple_delimiter = None
-sections_taught_by = None
-students_in = None
-instructors_in = None
-sections_for_code_and_term = None
-
-
-
-# ------------------------------------------------------------
-# a temporary implementation, while I write up the UI.
-
-sections_tuple_delimiter = '|'
-
-# For any of the students to actually appear in a course site, they
-# must also exist as Django users (or be in an authentication backend
-# that supports 'maybe_initialize_user'; see auth_evergreen.py).
-
-_db = [
- #(instructor, (term, code, sec-code), 'student1 student2 ... studentN'),
- ('fred', ('2009W', 'ENG203', '1'), 'jim joe jack ellen ed'),
- ('fred', ('2009W', 'ENG327', '1'), 'ed paul bill'),
- ('art', ('2009W', 'LIB201', '1'), 'graham bill ed'),
- ('graham', ('2009S', 'ART108', '1'), 'alan june jack'),
- ('graham', ('2009S', 'ART108', '2'), 'emmet'),
- ('graham', ('2009S', 'ART108', '3'), 'freda hugo bill'),
-]
-
-def sections_taught_by(username):
- return set([s[1] for s in _db if s[0] == username])
-
-def students_in(*sections):
- def inner():
- for instr, sec, studs in _db:
- if sec in sections:
- for s in studs.split(' '):
- yield s
- return set(inner())
-
-def instructors_in(*sections):
- def inner():
- for instr, sec, studs in _db:
- if sec in sections:
- yield instr
- return set(inner())
-
-def sections_for_code_and_term(code, term):
- return [(t, c, s) for (instr, (t, c, s), ss) in _db \
- if c == code and t == term]
--- /dev/null
+%% pandoc -s -N -t latex --template .header.tex < campus-interface.md > /tmp/campus.tex
+\documentclass[english]{article}
+\usepackage{charter}
+\usepackage[T1]{fontenc}
+\usepackage[latin9]{inputenc}
+\usepackage[letterpaper]{geometry}
+\geometry{verbose,tmargin=3cm,bmargin=3cm,lmargin=3cm,rmargin=3cm}
+\makeatletter
+\newcommand{\href}[2]{\textsc{#2}}
+\makeatother
+\usepackage{babel}
+
+$if(title)$
+\title{$title$}
+$endif$
+$if(author)$
+\author{$for(author)$$author$$sep$\\$endfor$}
+$endif$
+$if(date)$
+\date{$date$}
+$endif$
+
+\begin{document}
+$if(title)$
+\maketitle
+$endif$
+
+$for(include-before)$
+$include-before$
+
+$endfor$
+$if(toc)$
+\tableofcontents
+
+$endif$
+$body$
+\end{document}
--- /dev/null
+# Validation and lookup of course codes.
+
+# This modules specifies an "course-code interface" and a null
+# implementation of that interface. If your local system has rules for
+# valid course codes, and a mechanism for looking up details of these
+# codes, you can implement the interface according to your local
+# rules.
+
+
+# ------------------------------------------------------------
+# Overview and definitions
+
+# A course code identifies a specific course offering. Course codes
+# map 1:N onto formal course titles: by looking up a code, we can
+# derive a formal title (in theory, though it may not be possible for
+# external reasons).
+
+# A course code is insufficient to specify a class list: we need a
+# course section for that. A section ties a course code and term to an
+# instructor(s) and a list of students.
+
+# Course codes may have cross-listings, i.e., other codes which refer
+# to the same course, but which appear under a different department
+# for various academic purposes. In our system, we make no attempt to
+# subordinate cross-listings to a "primary" course code.
+
+
+#------------------------------------------------------------
+# Notes on the interface
+#
+# The `course_code_is_valid` function will be used ONLY if
+# course_code_list() returns None (it is a null implementation). If a
+# course-list is available, the system will use a membership test for
+# course-code validity.
+#
+# `course_code_lookup_title` will be used ONLY if `course_code_list`
+# is implemented.
+#
+#
+# "types" of the interface members
+#
+# course_code_is_valid (string) --> boolean.
+# course_code_example : a string constant.
+# course_code_list () --> list of strings
+# course_code_lookup_title (string) --> string, or None.
+# course_code_cross_listings (string) --> list of strings
+#
+# For each member, you MUST provide either a valid implementation, or
+# set the member to None. See the null implementation below.
+
+#------------------------------------------------------------
+# Implementations
+
+# ------------------------------------------------------------
+# Here is a 'null implementation' of the course-code interface. No
+# validation is done, nor are lookups.
+#
+# course_code_is_valid = None # anything is OK;
+# course_code_example = None # no examples;
+# course_code_lookup_title = None # no codes to list;
+# course_code_cross_listings = None # no cross lists.
+
+# ------------------------------------------------------------
+# This one specifies a valid course-code format using a regular
+# expression, and offers some example codes, but does not have a
+# lookup system.
+#
+# import re
+#
+# def course_code_is_valid(course_code):
+# pattern = re.compile(r'^\d{2}-\d{3}$')
+# return bool(pattern.match(course_code))
+#
+# course_code_example = '55-203; 99-105'
+#
+# course_code_list = None
+# course_code_lookup_title = None
+# course_code_cross_listings = None
+
+
+
+# ------------------------------------------------------------
+# This is a complete implementation, based on a hard-coded list of
+# course codes and titles, and two cross-listed course codes.
+#
+# _codes = [('ENG100', 'Introduction to English'),
+# ('ART108', 'English: An Introduction'),
+# ('FRE238', 'Modern French Literature'),
+# ('WEB203', 'Advanced Web Design'),]
+#
+# _crosslists = set(['ENG100', 'ART108'])
+#
+# course_code_is_valid = None
+# course_code_example = 'ENG100; FRE238'
+#
+# def course_code_list():
+# return [a for (a,b) in _codes]
+#
+# def course_code_lookup_title(course_code):
+# return dict(_codes).get(course_code)
+#
+# def course_code_cross_listings(course_code):
+# if course_code in _crosslists:
+# return list(_crosslists - set([course_code]))
+
+
+# ------------------------------------------------------------
+# Provide your own implementation below.
+
+
+#_codes = [('ENG100', 'Introduction to English'),
+# ('ART108', 'English: An Introduction'),
+# ('FRE238', 'Modern French Literature'),
+# ('LIB201', 'Intro to Library Science'),
+# ('WEB203', 'Advanced Web Design'),]
+
+_codes = [('ART99-100', 'Art History'),
+ ('BIOL55-350', 'Molecular Cell Biology'),
+ ('CRIM48-567', 'Current Issues in Criminology'),
+ ('ENGL26-280', 'Contemporary Literary Theory'),
+ ('ENGL26-420', 'Word and Image: The Contemporary Graphic Novel'),
+ ('SOCWK47-457', 'Advanced Social Work Research'),]
+
+_crosslists = set(['ENGL26-280', 'ENGL26-420'])
+
+
+course_code_is_valid = None
+
+course_code_example = 'BIOL55-350; SOCWK47-457'
+
+def course_code_list():
+ return [a for (a,b) in _codes]
+
+def course_code_lookup_title(course_code):
+ return dict(_codes).get(course_code)
+
+def course_code_cross_listings(course_code):
+ if course_code in _crosslists:
+ return list(_crosslists - set([course_code]))
+
--- /dev/null
+# Operations on course-section identifiers
+
+# A course section is an instance of a course offered in a term.
+
+# A section is specified by a 'section-id', a 3-tuple (course-code,
+# term, section-code), where section-code is usually a short
+# identifier (e.g., "1" representing "section 1 in this term"). Note
+# that multiple sections of the same course are possible in a given
+# term.
+
+# Within the reserves system, a course-site can be associated with
+# zero or more sections, granting access to students in those
+# sections. We need two representations of a section-id.
+
+# The section_tuple_delimiter must be a string which will never appear
+# in a course-code, term, or section-code in your database. It may be
+# a nonprintable character (e.g. NUL or CR). It is used to delimit
+# parts of the tuples in a course's database record.
+
+#------------------------------------------------------------
+# Notes on the interface
+#
+# 'sections_taught_by(username)' returns a set of sections for which
+# username is an instructor. It is acceptable if 'sections_taught_by'
+# only returns current and future sections: historical information is
+# not required by the reserves system.
+#
+# It is expected that the reserves system will be able to resolve any
+# usernames into user records. If there are students on a section-list
+# which do not resolve into user accounts, they will probably be
+# ignored and will not get access to their course sites. So if you're
+# updating your users and sections in a batch-run, you might want to
+# update your users first.
+#
+#------------------------------------------------------------
+# Implementations
+
+# The reserves system will work with a null-implementation of the
+# course-section interface, but tasks related to course-sections will
+# be unavailable.
+
+# ------------------------------------------------------------
+# The null implementation:
+#
+# sections_tuple_delimiter = None
+# sections_taught_by = None
+# students_in = None
+# instructors_in = None
+# sections_for_code_and_term = None
+
+# ------------------------------------------------------------
+#
+# The minimal non-null implementation. At the least you must provide
+# sections_tuple_delimiter and students_in. Lookups for instructors
+# may be skipped. Note that sections passed to students_in are
+# (term, course-code, section-code) tuples (string, string, string).
+#
+# sections_tuple_delimiter = '|'
+#
+# def students_in(*sections):
+# ...
+# return set_of_usernames
+#
+# instructors_in = None
+# sections_for_code_and_term = None
+
+# ------------------------------------------------------------
+# A complete implementation, with a static database.
+
+# sections_tuple_delimiter = '|'
+#
+# _db = [
+# ('fred', ('2009W', 'ENG203', '1'), 'jim joe jack ellen ed'),
+# ('fred', ('2009W', 'ENG327', '1'), 'ed paul bill'),
+# ('bill', ('2009S', 'BIO323', '1'), 'alan june jack'),
+# ('bill', ('2009S', 'BIO323', '2'), 'emmet'),
+# ]
+#
+# def sections_taught_by(username):
+# return set([s[1] for s in _db if s[0] == username])
+#
+# def students_in(*sections):
+# def inner():
+# for instr, sec, studs in _db:
+# if sec in sections:
+# for s in studs.split(' '):
+# yield s
+# return set(inner())
+#
+# def instructors_in(*sections):
+# def inner():
+# for instr, sec, studs in _db:
+# if sec in sections:
+# yield instr
+# return set(inner())
+#
+# def sections_for_code_and_term(code, term):
+# return [(t, c, s) for (instr, (t, c, s), ss) in _db \
+# if c == code and t == term]
+#
+
+
+# ------------------------------------------------------------
+# Provide your own implementation below.
+
+sections_tuple_delimiter = None
+sections_taught_by = None
+students_in = None
+instructors_in = None
+sections_for_code_and_term = None
+
+
+
+# ------------------------------------------------------------
+# a temporary implementation, while I write up the UI.
+
+sections_tuple_delimiter = '|'
+
+# For any of the students to actually appear in a course site, they
+# must also exist as Django users (or be in an authentication backend
+# that supports 'maybe_initialize_user'; see auth_evergreen.py).
+
+_db = [
+ #(instructor, (term, code, sec-code), 'student1 student2 ... studentN'),
+ ('fred', ('2009W', 'ENG203', '1'), 'jim joe jack ellen ed'),
+ ('fred', ('2009W', 'ENG327', '1'), 'ed paul bill'),
+ ('art', ('2009W', 'LIB201', '1'), 'graham bill ed'),
+ ('graham', ('2009S', 'ART108', '1'), 'alan june jack'),
+ ('graham', ('2009S', 'ART108', '2'), 'emmet'),
+ ('graham', ('2009S', 'ART108', '3'), 'freda hugo bill'),
+]
+
+def sections_taught_by(username):
+ return set([s[1] for s in _db if s[0] == username])
+
+def students_in(*sections):
+ def inner():
+ for instr, sec, studs in _db:
+ if sec in sections:
+ for s in studs.split(' '):
+ yield s
+ return set(inner())
+
+def instructors_in(*sections):
+ def inner():
+ for instr, sec, studs in _db:
+ if sec in sections:
+ yield instr
+ return set(inner())
+
+def sections_for_code_and_term(code, term):
+ return [(t, c, s) for (instr, (t, c, s), ss) in _db \
+ if c == code and t == term]
--- /dev/null
+% DRAFT: Interface specification for an OpenSRF campus-information service
+% Graham Fawcett
+% March 26, 2010
+
+# Introduction
+
+This document specifies the interface for an OpenSRF-based service
+which gives OpenSRF applications access to *campus information*, such
+as the names of courses taught at a given university, who is teaching
+them, and which students are enrolled in them.
+
+This service is designed to meet the needs of our reserves
+application, "Syrup." We hope the service will be useful in a wide
+range of library applications that could benefit from access to course-related
+information.
+
+This document specifies the OpenSRF interface of the campus
+information service, but it does not dictate how the service must be
+implemented. Each institution will need to implement the service,
+using the tools of their choice, in a way that makes local sense.
+
+# Design considerations
+
+## Partial implementations
+
+In an ideal world, a library would have unlimited access to all the
+course-related information they wanted; but many libraries do not
+enjoy such access. Not all applications need the same types of
+information, and many applications can adapt to different levels of
+available campus information. Given this, it is acceptable to
+*partially* implement the campus-information service, skipping the
+parts that you cannot (or choose not to) implement.
+
+For example, if you don't have access to any class-list information,
+but you do have a machine-readable version of the academic calendar,
+you could implement the course-lookup and term-lookup parts of the
+interface, but skip the course-offering parts.
+
+An application must be able to determine what parts of the interface
+you have implemented. Therefore, you must implement the
+`methods-supported` method (see [Static informational methods]). Since
+this method-list is essentially static (it will change only if you
+modify your implementation), an application may test it infrequently,
+e.g. just once upon startup.
+
+## Caching
+
+OpenSRF provides a high-performance caching framework. You should
+consider using this framework when designing your
+implementation.
+
+Applications are discouraged from caching campus information:
+especially information on people and course offerings, which both
+change relatively frequently. It makes more sense to centralize policy
+decisions about the lifespans of campus data at the service layer. If
+applications must cache campus information (e.g. for demonstrated
+performance reasons), they are encouraged to keep the cache-lifetimes
+as short as possible.
+
+# Data types
+
+All of these data types are needed in a complete implementation of the
+interface. Since you are free to implement only parts of the interface
+(see [Partial implementations]), all of these data types might not
+apply in your case.
+
+## Identifier types
+
+ COURSE-ID = string (matching a local COURSE-ID-FORMAT)
+ TERM-ID = string (matching a local TERM-ID-FORMAT)
+ OFFERING-ID = string (matching a local OFFERING-ID-FORMAT)
+ PERSON-ID = string (matching a local PERSON-ID-FORMAT)
+
+The four identifier types are used respectively as unique keys for
+courses, terms, course offerings, and people. (`String` is the
+primitive type of strings of Unicode characers.)
+
+Since the PERSON-ID may be exposed in reports and user interfaces, it
+should be a common public identifier (such as a 'single-sign-on ID',
+'email prefix', or 'campus username') that can be displayed beside the
+person's name without violating privacy regulations.
+
+Your institution may use 'section numbers' to differentiate multiple
+offerings of a course in the same term. You may embed them in your
+identifiers: for example, the offering ID `ENG100-2010W-03` might
+represent Section 3 of English 100 being taught in Winter 2010. But it
+isn't required that your offering IDs are so structured.
+
+**Formats:** Each type of identifier complies with a respective,
+locally-defined format. You should define a (private, internal)
+function for each format, that verifies whether a given string matches
+the format. For example, a Java implementation might define a
+function, `boolean isValidCourseID(String)`. You might use regular
+expressions to define your formats, but it's not a requirement. At the
+very least, your local formats should reject empty strings as IDs. You
+may expose these functions for application use: see
+[Format-matching methods].
+
+## Record types
+
+Record types are modelled as associative arrays (sets of key/value
+pairs). \[Is this acceptable in OpenSRF? It's valid JSON, but I'm not clear on OpenSRF conventions.\]
+The following notation is used in the type definitions:
+
+ string (a string primitive)
+ [string]* (an unordered set of zero or more strings)
+ (string)? (an optional string: it may be NULL.)
+
+Strictly, unordered sets *do* have an order, since they are
+implemented as JSON lists. But the specification does not guarantee
+that the order of the list is significant.
+
+Missing optional values may be indicated in two equivalent ways:
+either include the key, and pair it with a `null` value (`{key: null,
+...}`), or simply omit the key/value pair from the record.
+
+ COURSE = { id: COURSE-ID,
+ title: string }
+
+A COURSE record describes a course in the abstract sense, as it would
+appear in an academic calendar. It must have at least a unique course
+ID and a descriptive (possibly non-unique) title. It may include other
+attributes if you wish, but we specify `id` and `title` as required
+attributes.
+
+ TERM = { id: TERM-ID,
+ name: string,
+ start-date: date,
+ end-date: date }
+
+A TERM record describes a typical period in which a course is offered
+(a 'term' or 'semester'). It must have a unique term-ID, a
+probably-unique name, and start and end dates. (`Date` is a primitive
+type, representing a calendar date.)
+
+ PERSON = { id: PERSON-ID,
+ surname: string,
+ given-name: string,
+ email: (string)? }
+
+A PERSON record describes a person! It must include a unique
+person-ID, a surname and given name. A value for `email` is
+optional. You may also add other attributes as you see fit.
+
+ OFFERING = { id: OFFERING-ID,
+ course: COURSE-ID,
+ starting-term: TERM-ID,
+ ending-term: TERM-ID,
+ students: [PERSON-ID]*,
+ assistants: [PERSON-ID]*,
+ instructors: [PERSON-ID]* }
+
+An OFFERING record describes a course offering: your institution might
+call this a 'class' or a 'section'. It has specific start- and and
+end-dates (derived from its starting and ending terms: some
+institutions have courses that span multiple terms). The `course`
+attribute refers to the single course of which it is an instance (our
+specification punts on the issue of cross-listed offerings). It has
+unordered sets of zero-or-more students, teaching assistants and
+instructors.
+
+Each OFFERING record is a snapshot of a course offering at a given
+time. It is assumed that people may join or leave the course
+offering at any point during its duration.
+
+The set of "assistants" is loosely defined. It might include teaching
+assistants (TAs and GAs) but also technical assistants, departmental
+support staff, and other ancillary support staff.
+
+ OFFERING-FLESHED = { id: OFFERING-ID,
+ course: COURSE,
+ starting-term: TERM,
+ ending-term: TERM,
+ students: [PERSON]*,
+ assistants: [PERSON]*,
+ instructors: [PERSON]* }
+
+A OFFERING-FLESHED record is like an OFFERING record, except that the
+course, term, and people-set attributes have been 'fleshed out', so
+that they contain not codes, but actual copies of the COURSE, TERM and
+PERSON records.
+
+# Method signatures
+
+The following notation is used for method signatures:
+
+ method-name: arg1-type, ... argN-type -> result-type
+
+The `void` type is used to express empty argument-lists.
+
+## Static informational methods
+
+ methods-supported: void -> [string]*
+
+The `methods-supported` method is the only method that you *must*
+implement (see [Partial implementations]). It returns a list of the
+names of the methods for which you've provided
+implementations. Applications can use this list to determine the
+capabilities of your implementation.
+
+## Course methods
+
+ course-lookup: COURSE-ID -> (COURSE)?
+ course-id-list: void -> [COURSE-ID]*
+ course-list: void -> [COURSE]*
+ course-id-example: void -> (COURSE-ID)?
+
+Given a COURSE-ID string, `course-lookup` will return the matching
+COURSE record, or `null` if no such course exists.
+
+The methods `course-id-list` and `course-list` return a list of the
+IDs (or records, respectively) of *all* known courses in the campus
+system. An application might use these to populate option-lists or
+report headings. The lists may be limited to the courses which are
+defined in the current academic calendar (that is, your implementation
+may omit obsolete course descriptions).
+
+The `course-id-example` method returns a course ID *example*. In
+user-interfaces where a course ID must be typed in, this example can
+be used to offer some guidance to the user. If the method returns
+`null`, or if the method is not implemented, an application should
+simply omit any example from the user interface.
+
+## Term methods
+
+ term-lookup: TERM-ID -> (TERM)?
+ term-list: void -> [TERM]*
+ terms-at-date: date -> [TERM]*
+
+The `term-lookup` and `term-list` are analogous to the `course-lookup`
+and `course-list` methods. The `terms-at-date` method takes a date
+argument, and returns a list of all TERM records such that `term.start
+<= date <= term.finish`. (We do not specify that terms are
+non-overlapping.)
+
+## Person methods
+
+ person-lookup: PERSON-ID -> (PERSON)?
+
+## Offering methods
+
+To describe the return-values of some of the Offering methods, we
+introduce the notation `MBR(X)` as an abbreviation for the type
+`([X]*, [X]*, [X]*)`, that is, a trio of sets representing the three
+membership groups associated with a course offering: teachers,
+assistants, and students. The types of elements contained in the sets
+is specified by the specializing type, `X`: so, `MBR(PERSON)` is a
+trio of sets of PERSON records.
+
+ MBR(TYPE) = ([TYPE]*, # memberships as a teacher,
+ [TYPE]*, # as an assistant,
+ [TYPE]*) # as a student.
+
+
+ course-term-offerings: (COURSE-ID, TERM-ID) -> [OFFERING]*
+ course-term-offerings-fleshed: (COURSE-ID, TERM-ID) -> [OFFERING-FLESHED]*
+
+Given a COURSE-ID and a TERM-ID, these methods will return records for
+all course offerings for the course represented by COURSE-ID, whose
+`starting-term` *or* `ending-term` is equal to TERM-ID.
+
+ memberships: PERSON-ID -> MBR(OFFERING)
+ membership-ids: PERSON-ID -> MBR(OFFERING-ID)
+ memberships-fleshed: PERSON-ID -> MBR(OFFERING-FLESHED)
+
+These methods take a PERSON-ID and return a trio of sets whose
+elements represent the course-offerings in which the person is
+(respectively) a teacher, assistant, or student.
+
+Within a given course-offering, a person must belong to no more than
+one of the three sets. For example, it is not permitted to be both a
+teacher and student for the same offering.
+
+If the PERSON-ID is invalid, or if the person is not a member of any
+offerings, the return value should be a trio of three empty sets --
+`[[], [], []]` -- *not* a `null` value or an error.
+
+ member-ids: OFFERING-ID -> MBR(PERSON-ID)
+ members: OFFERING-ID -> MBR(PERSON)
+
+These methods take an OFFERING-ID and return a trio of sets whose
+elements represent (respectively) the teachers, assistants, and
+students in the offering.
+
+If the OFFERING-ID is invalid, or if the offering is "empty", the
+return value should be a trio of three empty sets -- `[[], [], []]` --
+*not* a `null` value or an error.
+
+ teacher-ids: OFFERING-ID -> ([PERSON-ID]*, [PERSON-ID]*)
+ teachers: OFFERING-ID -> ([PERSON]*, [PERSON]*)
+
+
+The `teacher` methods are identical to the `member` methods, except
+that the student set is omitted: the return-value is a *pair* of sets
+representing teachers and assistants. These are essentially optimized
+versions of the `members` methods for cases when you only need to know
+about the teaching and support teams (typically, very small groups)
+and can avoid the cost of calculating and transmitting the student list
+(typically, 10-100 times larger).
+
+## Format-matching methods
+
+ resembles-course-id: string -> boolean
+ resembles-offering-id: string -> boolean
+ resembles-term-id: string -> boolean
+ resembles-person-id: string -> boolean
+
+Applications can use these to implement data-input validation tests in
+user interfaces, primarily where lookups are not possible. They
+determine whether a given string falls within the general guidelines
+of what your IDs are supposed to look like. At some institutions, this
+might be the best you can offer: you might not have access to
+databases in which you can look records up, but at least you can offer
+a means to avoid basic typographic errors.
+
+You could implement these methods by exposing the functions you
+defined for your COURSE-ID-FORMAT, TERM-ID-FORMAT, OFFERING-ID-FORMAT
+and PERSON-ID-FORMAT tests (see [Identifier types]). At the least,
+these formats should ensure that empty strings are rejected.
+
+You might choose to use implement these as lookup functions, returning
+`true` only if a matching record was found. For example, if your
+school offers only two courses (say, `ENG100` and `ENG200`), you could
+choose to implement a `resembles-course-id` method that only returned
+`true` if the argument was exactly one of those two course codes. No
+matter how you implement it, the intent of the `resembles` methods is
+to help avoid typographic errors, not to act as a membership test.
+
+[Partial implementations]: #partial-implementations
+[Static informational methods]: #static-informational-methods
+[Identifier types]: #identifier-types
+[Format-matching methods]: #format-matching-methods
+
+<!--
+Local Variables:
+mode: markdown
+End:
+-->
--- /dev/null
+# Do not edit this file: make your own, instead.
+
+# See COURSE_CODES.txt for information.
+
+course_code_is_valid = None
+course_code_example = None
+course_code_list = None
+course_code_lookup_title = None
+course_code_cross_listings = None
+
+# See COURSE_SECTIONS.txt for information.
+
+sections_tuple_delimiter = None
+sections_taught_by = None
+students_in = None
+instructors_in = None
+sections_for_code_and_term = None
+
--- /dev/null
+from default import *
+
+#----------------------------------------------------------------------
+# Course Codes
+
+_codes = [('ART99-100', 'Art History'),
+ ('BIOL55-350', 'Molecular Cell Biology'),
+ ('CRIM48-567', 'Current Issues in Criminology'),
+ ('ENGL26-280', 'Contemporary Literary Theory'),
+ ('ENGL26-420', 'Word and Image: The Contemporary Graphic Novel'),
+ ('SOCWK47-457', 'Advanced Social Work Research'),]
+
+_crosslists = set(['ENGL26-280', 'ENGL26-420'])
+
+
+course_code_is_valid = None
+
+course_code_example = 'BIOL55-350; SOCWK47-457'
+
+def course_code_list():
+ return [a for (a,b) in _codes]
+
+def course_code_lookup_title(course_code):
+ return dict(_codes).get(course_code)
+
+def course_code_cross_listings(course_code):
+ if course_code in _crosslists:
+ return list(_crosslists - set([course_code]))
+
+
+#----------------------------------------------------------------------
+# Course Sections
+
+sections_tuple_delimiter = '|'
+
+# For any of the students to actually appear in a course site, they
+# must also exist as Django users (or be in an authentication backend
+# that supports 'maybe_initialize_user'; see auth_evergreen.py).
+
+_db = [
+ #(instructor, (term, code, sec-code), 'student1 student2 ... studentN'),
+ ('fred', ('2009W', 'ENG203', '1'), 'jim joe jack ellen ed'),
+ ('fred', ('2009W', 'ENG327', '1'), 'ed paul bill'),
+ ('art', ('2009W', 'LIB201', '1'), 'graham bill ed'),
+ ('graham', ('2009S', 'ART108', '1'), 'alan june jack'),
+ ('graham', ('2009S', 'ART108', '2'), 'emmet'),
+ ('graham', ('2009S', 'ART108', '3'), 'freda hugo bill'),
+]
+
+def sections_taught_by(username):
+ return set([s[1] for s in _db if s[0] == username])
+
+def students_in(*sections):
+ def inner():
+ for instr, sec, studs in _db:
+ if sec in sections:
+ for s in studs.split(' '):
+ yield s
+ return set(inner())
+
+def instructors_in(*sections):
+ def inner():
+ for instr, sec, studs in _db:
+ if sec in sections:
+ yield instr
+ return set(inner())
+
+def sections_for_code_and_term(code, term):
+ return [(t, c, s) for (instr, (t, c, s), ss) in _db \
+ if c == code and t == term]
+
+
DEBUG = False
-ADMINS = (
- # ('Your Name', 'your_email@domain.com'),
-)
-
-MANAGERS = ADMINS
+ADMINS = []
DATABASE_ENGINE = '' # 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
DATABASE_NAME = '' # Or path to database file if using sqlite3.
'django.contrib.auth.backends.ModelBackend'
]
+CAMPUS_INTEGRATION_MODULE = 'conifer.integration.default'
+
#---------------------------------------------------------------------------
# local_settings.py
# Further settings that depend upon local_settings.
TEMPLATE_DEBUG = DEBUG
+MANAGERS = ADMINS
+
+#----------
if EVERGREEN_AUTHENTICATION:
- AUTHENTICATION_BACKENDS.extend(
- ['conifer.custom.auth_evergreen.EvergreenAuthBackend',
- ])
+ AUTHENTICATION_BACKENDS.append(
+ 'conifer.custom.auth_evergreen.EvergreenAuthBackend')
+
+#----------
+
+try:
+ exec 'import %s as CAMPUS_INTEGRATION' % CAMPUS_INTEGRATION_MODULE
+except:
+ raise Exception('There is an error in your campus integration module (%s)! '
+ 'Please investigate and repair.' % CAMPUS_INTEGRATION_MODULE,
+ sys.exc_value)
+
+#----------
from datetime import datetime
from genshi import Markup
from django.utils.translation import ugettext as _
-from conifer.custom import course_codes # fixme, not sure if conifer.custom is a good parent.
-from conifer.custom import course_sections # fixme, not sure if conifer.custom is a good parent.
-from conifer.custom import lib_integration
import re
import random
from django.utils import simplejson
from conifer.middleware import genshi_locals
+# 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.
+
+
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
break
def sections(self):
- delim = course_sections.sections_tuple_delimiter
+ delim = campus.sections_tuple_delimiter
if not delim:
return []
else:
def _merge_sections(secs):
- delim = course_sections.sections_tuple_delimiter
+ delim = campus.sections_tuple_delimiter
return delim.join(delim.join(sec) for sec in secs)
def section_decode_safe(secstring):
if not secstring:
return None
- return tuple(secstring.decode('base64').split(course_sections.sections_tuple_delimiter))
+ return tuple(secstring.decode('base64').split(campus.sections_tuple_delimiter))
def section_encode_safe(section):
- return course_sections.sections_tuple_delimiter.join(section).encode('base64').strip()
+ return campus.sections_tuple_delimiter.join(section).encode('base64').strip()
class Member(m.Model):
class Meta:
def clean_code(self):
v = (self.cleaned_data.get('code') or '').strip()
- is_valid_func = models.course_codes.course_code_is_valid
+ is_valid_func = models.campus.course_code_is_valid
if (not is_valid_func) or is_valid_func(v):
return v
else:
# 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)
+COURSE_CODE_LIST = bool(models.campus.course_code_list)
+COURSE_CODE_LOOKUP_TITLE = bool(models.campus.course_code_lookup_title)
if COURSE_CODE_LIST:
from django.forms import Select
- course_list = models.course_codes.course_code_list()
+ course_list = models.campus.course_code_list()
choices = [(a,a) for a in course_list]
choices.sort()
empty_label = u'---------'
if is_add:
instance = models.Course()
current_access_level = not is_add and instance.access or None
- example = models.course_codes.course_code_example
+ example = models.campus.course_code_example
if request.method != 'POST':
form = NewCourseForm(instance=instance)
return g.render('edit_course.xhtml', **locals())
# 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)
+ title = models.campus.course_code_lookup_title(course_code)
return HttpResponse(simplejson.dumps({'title':title}))
@instructors_only
(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:
+ if models.campus.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)
for name in POST \
if name.startswith('remove_section_')]
course.drop_sections(*to_remove)
- student_names = models.course_sections.students_in(*course.sections())
+ student_names = models.campus.students_in(*course.sections())
for name in student_names:
user = models.maybe_initialize_user(name)
if user:
<h3>Course section numbers</h3>
<?python
current_sections = course.sections()
- my_sections = [s for s in models.course_sections.sections_taught_by(user.username) if not s in current_sections]
- ct_sections = [s for s in models.course_sections.sections_for_code_and_term(course.code, course.term.code) \
+ my_sections = [s for s in models.campus.sections_taught_by(user.username) if not s in current_sections]
+ ct_sections = [s for s in models.campus.sections_for_code_and_term(course.code, course.term.code) \
if not s in current_sections]
encode = lambda t,c,s: models.section_encode_safe((t,c,s))
?>