From dcd41d7916da854561959ca06e71ca8743dec250 Mon Sep 17 00:00:00 2001 From: gfawcett Date: Tue, 4 May 2010 01:48:22 +0000 Subject: [PATCH] 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 --- .gitignore | 1 + conifer/BRANCH-TODO.org | 38 +++ conifer/custom/README | 4 + conifer/integration/.header.tex | 37 +++ .../COURSE_CODES.txt} | 0 .../COURSE_SECTIONS.txt} | 0 conifer/integration/__init__.py | 0 conifer/integration/campus-interface.md | 338 +++++++++++++++++++++ conifer/integration/default.py | 18 ++ conifer/integration/example.py | 72 +++++ conifer/settings.py | 27 +- conifer/syrup/models.py | 17 +- conifer/syrup/views/courses.py | 16 +- conifer/templates/edit_course_permissions.xhtml | 4 +- 14 files changed, 547 insertions(+), 25 deletions(-) create mode 100644 conifer/BRANCH-TODO.org create mode 100644 conifer/custom/README create mode 100644 conifer/integration/.header.tex rename conifer/{custom/course_codes.py => integration/COURSE_CODES.txt} (100%) rename conifer/{custom/course_sections.py => integration/COURSE_SECTIONS.txt} (100%) create mode 100644 conifer/integration/__init__.py create mode 100644 conifer/integration/campus-interface.md create mode 100644 conifer/integration/default.py create mode 100644 conifer/integration/example.py 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/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/custom/course_codes.py b/conifer/integration/COURSE_CODES.txt similarity index 100% rename from conifer/custom/course_codes.py rename to conifer/integration/COURSE_CODES.txt diff --git a/conifer/custom/course_sections.py b/conifer/integration/COURSE_SECTIONS.txt similarity index 100% rename from conifer/custom/course_sections.py rename to conifer/integration/COURSE_SECTIONS.txt 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

-- 2.11.0