From 4f695bf43d2b80f140b496e362e1ab305d06c1fb Mon Sep 17 00:00:00 2001 From: gfawcett Date: Mon, 9 Mar 2009 02:05:26 +0000 Subject: [PATCH] preliminary support for course-sections (from an external data source). See conifer/custom/course_sections.py for the course-section interface. It's primarily used in the edit-course-permissions handler; it needs more testing, but appears pretty decent so far. git-svn-id: svn://svn.open-ils.org/ILS-Contrib/servres/trunk@157 6d9bc8c9-1ec2-4278-b937-99fde70a366f --- conifer/custom/course_sections.py | 147 ++++++++++++++++++++++++ conifer/syrup/models.py | 39 +++++++ conifer/syrup/views.py | 35 +++++- conifer/templates/edit_course_permissions.xhtml | 39 ++++++- 4 files changed, 252 insertions(+), 8 deletions(-) create mode 100644 conifer/custom/course_sections.py diff --git a/conifer/custom/course_sections.py b/conifer/custom/course_sections.py new file mode 100644 index 0000000..02bce4d --- /dev/null +++ b/conifer/custom/course_sections.py @@ -0,0 +1,147 @@ +# 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 = '|' + +_db = [ + ('fred', ('2009W', 'ENG203', '1'), 'jim joe jack ellen ed'), + ('fred', ('2009W', 'ENG327', '1'), 'ed paul bill'), + ('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/syrup/models.py b/conifer/syrup/models.py index 1da86d5..673d642 100644 --- a/conifer/syrup/models.py +++ b/conifer/syrup/models.py @@ -6,6 +6,7 @@ from datetime import datetime from genshi import Markup from gettext import gettext as _ # fixme, is this the right function to import? 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. import re import random @@ -251,6 +252,44 @@ class Course(m.Model): self.passkey = key break + def sections(self): + delim = course_sections.sections_tuple_delimiter + if not delim: + return [] + else: + def inner(): + parts = self.enrol_codes.split(delim) + while len(parts) > 2: + yield tuple(parts[:3]) + del parts[:3] + return set(inner()) + + def add_sections(self, *sections): + current = self.sections() + sections = set(sections).union(current) + self.enrol_codes = _merge_sections(sections) + + def drop_sections(self, *sections): + current = self.sections() + sections = current - set(sections) + self.enrol_codes = _merge_sections(sections) + + def get_students(self): + return User.objects.filter(member__course__exact=self, member__role__exact='STUDT') \ + .order_by('last_name', 'first_name') + +def _merge_sections(secs): + delim = course_sections.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)) + +def section_encode_safe(section): + return course_sections.sections_tuple_delimiter.join(section).encode('base64').strip() + class Member(m.Model): course = m.ForeignKey(Course) user = m.ForeignKey(User) diff --git a/conifer/syrup/views.py b/conifer/syrup/views.py index 12065e5..ecd2f9b 100644 --- a/conifer/syrup/views.py +++ b/conifer/syrup/views.py @@ -226,11 +226,15 @@ def add_new_course_ajax_title(request): def edit_course_permissions(request, course_id): course = get_object_or_404(models.Course, pk=course_id) - choose_access = django.forms.Select(choices=[ + choices = [ (u'CLOSE', _(u'No students: this site is closed.')), (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'))]) + (u'LOGIN', _(u'All Reserves patrons'))] + if models.course_sections.sections_tuple_delimiter is None: + del choices[1] # no STUDT support. + choose_access = django.forms.Select(choices=choices) + if request.method != 'POST': return g.render('edit_course_permissions.xhtml', **locals()) else: @@ -288,10 +292,31 @@ def edit_course_permissions(request, course_id): # update student details ------------------------------------ access = POST.get('access') course.access = access - course.save() + # drop all provided users. fixme, this could be optimized to do add/drops. + models.Member.objects.filter(course=course, provided=True).delete() if course.access == u'STUDT': - raise NotImplementedError, 'No course sections yet! Coming soon.' - return HttpResponseRedirect('.') + initial_sections = course.sections() + # add the 'new section' if any + new_sec = request.POST.get('add_section') + new_sec = models.section_decode_safe(new_sec) + if new_sec: + course.add_sections(new_sec) + # remove the sections to be dropped + to_remove = [models.section_decode_safe(name.rsplit('_',1)[1]) \ + for name in POST \ + if name.startswith('remove_section_')] + course.drop_sections(*to_remove) + student_names = models.course_sections.students_in(*course.sections()) + for name in student_names: + user = models.maybe_initialize_user(name) + if user: + mbr = models.Member.objects.create(course=course, user=user, + role='STUDT', provided=True) + mbr.save() + else: + pass + course.save() + return HttpResponseRedirect('.#student_access') @instructors_only def delete_course(request, course_id): diff --git a/conifer/templates/edit_course_permissions.xhtml b/conifer/templates/edit_course_permissions.xhtml index b4ab4f7..ab8f15c 100644 --- a/conifer/templates/edit_course_permissions.xhtml +++ b/conifer/templates/edit_course_permissions.xhtml @@ -64,16 +64,49 @@ instructors = [m for m in models.Member.objects.filter(course=course) if m.role

Course section numbers

- - + +
+ + + + + + + + + + +
Associated sectionRemove?
${code}, section ${sec}, in term ${term}
+

Add section: + +

Class List

The following users have student-level access in this course site.

- +
    +
  1. + ${student.get_full_name()} (${student.email}) +
  2. +
-- 2.11.0