From: gfawcett Date: Tue, 4 May 2010 01:48:22 +0000 (+0000) Subject: Merged 2010-02-campus-integration-reorg branch changes r797:849 into trunk X-Git-Url: https://old-git.evergreen-ils.org/?a=commitdiff_plain;h=dcd41d7916da854561959ca06e71ca8743dec250;p=Syrup.git Merged 2010-02-campus-integration-reorg branch changes r797:849 into trunk git-svn-id: svn://svn.open-ils.org/ILS-Contrib/servres/trunk@878 6d9bc8c9-1ec2-4278-b937-99fde70a366f --- diff --git a/.gitignore b/.gitignore index 7b80a14..b634ce8 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ PyZ3950_parsetab.py xsip TAGS private_local_settings.py +/conifer/.dired diff --git a/conifer/BRANCH-TODO.org b/conifer/BRANCH-TODO.org new file mode 100644 index 0000000..51f47f9 --- /dev/null +++ b/conifer/BRANCH-TODO.org @@ -0,0 +1,38 @@ +* 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? diff --git a/conifer/custom/README b/conifer/custom/README new file mode 100644 index 0000000..a80b2b0 --- /dev/null +++ b/conifer/custom/README @@ -0,0 +1,4 @@ +This directory is going away. + +Default integrations are being moved to 'conifer.integration'. The +active integration modules are to be specified in local_settings. diff --git a/conifer/custom/course_codes.py b/conifer/custom/course_codes.py deleted file mode 100644 index 7482ad0..0000000 --- a/conifer/custom/course_codes.py +++ /dev/null @@ -1,140 +0,0 @@ -# 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])) - diff --git a/conifer/custom/course_sections.py b/conifer/custom/course_sections.py deleted file mode 100644 index 4003e0b..0000000 --- a/conifer/custom/course_sections.py +++ /dev/null @@ -1,153 +0,0 @@ -# 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] diff --git a/conifer/integration/.header.tex b/conifer/integration/.header.tex new file mode 100644 index 0000000..804e56c --- /dev/null +++ b/conifer/integration/.header.tex @@ -0,0 +1,37 @@ +%% 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} diff --git a/conifer/integration/COURSE_CODES.txt b/conifer/integration/COURSE_CODES.txt new file mode 100644 index 0000000..7482ad0 --- /dev/null +++ b/conifer/integration/COURSE_CODES.txt @@ -0,0 +1,140 @@ +# 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])) + diff --git a/conifer/integration/COURSE_SECTIONS.txt b/conifer/integration/COURSE_SECTIONS.txt new file mode 100644 index 0000000..4003e0b --- /dev/null +++ b/conifer/integration/COURSE_SECTIONS.txt @@ -0,0 +1,153 @@ +# 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] diff --git a/conifer/integration/__init__.py b/conifer/integration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/conifer/integration/campus-interface.md b/conifer/integration/campus-interface.md new file mode 100644 index 0000000..e444e8e --- /dev/null +++ b/conifer/integration/campus-interface.md @@ -0,0 +1,338 @@ +% 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 + + diff --git a/conifer/integration/default.py b/conifer/integration/default.py new file mode 100644 index 0000000..b553b28 --- /dev/null +++ b/conifer/integration/default.py @@ -0,0 +1,18 @@ +# 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 + diff --git a/conifer/integration/example.py b/conifer/integration/example.py new file mode 100644 index 0000000..2426949 --- /dev/null +++ b/conifer/integration/example.py @@ -0,0 +1,72 @@ +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] + + diff --git a/conifer/settings.py b/conifer/settings.py index 51cf117..39f68ea 100644 --- a/conifer/settings.py +++ b/conifer/settings.py @@ -8,11 +8,7 @@ sys.path.append(HERE('..')) 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. @@ -97,6 +93,8 @@ AUTHENTICATION_BACKENDS = [ 'django.contrib.auth.backends.ModelBackend' ] +CAMPUS_INTEGRATION_MODULE = 'conifer.integration.default' + #--------------------------------------------------------------------------- # local_settings.py @@ -115,9 +113,22 @@ except: # 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) + +#---------- diff --git a/conifer/syrup/models.py b/conifer/syrup/models.py index 03a3ea1..5bfd74c 100644 --- a/conifer/syrup/models.py +++ b/conifer/syrup/models.py @@ -5,14 +5,17 @@ from django.contrib.auth import get_backends 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='\\1'): ''' This may be a lame way to do this, but will want to highlight matches somehow @@ -262,7 +265,7 @@ class Course(m.Model): break def sections(self): - delim = course_sections.sections_tuple_delimiter + delim = campus.sections_tuple_delimiter if not delim: return [] else: @@ -302,16 +305,16 @@ class Course(m.Model): 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: diff --git a/conifer/syrup/views/courses.py b/conifer/syrup/views/courses.py index 8a778ce..e0bc9a7 100644 --- a/conifer/syrup/views/courses.py +++ b/conifer/syrup/views/courses.py @@ -12,7 +12,7 @@ class NewCourseForm(ModelForm): 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: @@ -20,12 +20,12 @@ class NewCourseForm(ModelForm): # 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'---------' @@ -52,7 +52,7 @@ def _add_or_edit_course(request, instance=None): if is_add: instance = models.Course() current_access_level = not is_add and instance.access or None - example = models.course_codes.course_code_example + example = models.campus.course_code_example if request.method != 'POST': form = NewCourseForm(instance=instance) return g.render('edit_course.xhtml', **locals()) @@ -81,7 +81,7 @@ def _add_or_edit_course(request, instance=None): # 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 @@ -96,7 +96,7 @@ def edit_course_permissions(request, course_id): (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) @@ -172,7 +172,7 @@ def edit_course_permissions(request, course_id): 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: diff --git a/conifer/templates/edit_course_permissions.xhtml b/conifer/templates/edit_course_permissions.xhtml index e8adbe2..98510a8 100644 --- a/conifer/templates/edit_course_permissions.xhtml +++ b/conifer/templates/edit_course_permissions.xhtml @@ -66,8 +66,8 @@ instructors = [m for m in models.Member.objects.filter(course=course) if m.role

Course section numbers