From 84ff68670cc990bb58eb946338a163399eefd59f Mon Sep 17 00:00:00 2001
From: gfawcett
Date: Sun, 8 Mar 2009 17:11:14 +0000
Subject: [PATCH] joining course by invitation code; vestigial
edit-course-permissions screen.
I want to rework the course-permissions screen though, I don't like
that permissions are spread across two pages.
git-svn-id: svn://svn.open-ils.org/ILS-Contrib/servres/trunk@147 6d9bc8c9-1ec2-4278-b937-99fde70a366f
---
conifer/syrup/models.py | 19 +++++--
conifer/syrup/urls.py | 2 +
conifer/syrup/views.py | 71 +++++++++++++++++++++----
conifer/templates/course_invitation.xhtml | 27 ++++++++++
conifer/templates/edit_course_permissions.xhtml | 38 +++++++++++++
conifer/templates/my_courses.xhtml | 1 +
6 files changed, 145 insertions(+), 13 deletions(-)
create mode 100644 conifer/templates/course_invitation.xhtml
create mode 100644 conifer/templates/edit_course_permissions.xhtml
diff --git a/conifer/syrup/models.py b/conifer/syrup/models.py
index d9599cb..1da86d5 100644
--- a/conifer/syrup/models.py
+++ b/conifer/syrup/models.py
@@ -7,6 +7,7 @@ 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.
import re
+import random
def highlight(text, phrase,
highlighter='\\1'):
@@ -157,11 +158,11 @@ class Course(m.Model):
('ANON', _('World-accessible')),
('LOGIN', _('Accessible to all logged-in users')),
('STUDT', _('Accessible to course students (by section)')),
- ('PASWD', _('Accessible to course students (by password)')),
+ ('INVIT', _('Accessible to course students (by invitation code)')),
('CLOSE', _('Accessible only to course owners'))],
default='CLOSE')
- # For sites that use a passphrase as an invitation (PASWD access).
+ # For sites that use a passkey as an invitation (INVIT access).
passkey = m.CharField(db_index=True, blank=True, null=True, max_length=255)
# For sites that have registration-lists from an external system
@@ -177,7 +178,7 @@ class Course(m.Model):
# multiple NULL values in a unique column.
if self.passkey:
try:
- already = Course.objects.get(passkey=self.passkey)
+ already = Course.objects.exclude(pk=self.id).get(passkey=self.passkey)
except Course.DoesNotExist:
super(Course, self).save(force_insert, force_update)
else:
@@ -237,6 +238,18 @@ class Course(m.Model):
def course_url(self, suffix=''):
return '/syrup/course/%d/%s' % (self.id, suffix)
+ def generate_new_passkey(self):
+ # todo: have a pluggable passkey algorithm.
+ def algorithm():
+ # four numbers, separated by dashes, e.g. "342-58-928-21".
+ return '-'.join([str(random.randint(1,999)) for x in range(4)])
+ while True:
+ key = algorithm()
+ try:
+ crs = Course.objects.get(passkey=key)
+ except Course.DoesNotExist:
+ self.passkey = key
+ break
class Member(m.Model):
course = m.ForeignKey(Course)
diff --git a/conifer/syrup/urls.py b/conifer/syrup/urls.py
index cc11b73..5bba02a 100644
--- a/conifer/syrup/urls.py
+++ b/conifer/syrup/urls.py
@@ -11,6 +11,7 @@ urlpatterns = patterns('conifer.syrup.views',
(r'^course/$', 'my_courses'),
(r'^course/new/$', 'add_new_course'),
(r'^course/new/ajax_title$', 'add_new_course_ajax_title'),
+ (r'^course/invitation/$', 'course_invitation'),
(r'^browse/$', 'browse_courses'),
(r'^browse/(?P.*)/$', 'browse_courses'),
(r'^prefs/$', 'user_prefs'),
@@ -22,6 +23,7 @@ urlpatterns = patterns('conifer.syrup.views',
(r'^department/(?P.*)/$', 'department_detail'),
(r'^course/(?P\d+)/search/$', 'course_search'),
(r'^course/(?P\d+)/edit/$', 'edit_course'),
+ (r'^course/(?P\d+)/edit/permission/$', 'edit_course_permissions'),
(ITEM_PREFIX + r'$', 'item_detail'),
(ITEM_PREFIX + r'dl/(?P.*)$', 'item_download'),
(ITEM_PREFIX + r'meta$', 'item_metadata'),
diff --git a/conifer/syrup/views.py b/conifer/syrup/views.py
index 8dc951e..cb9693e 100644
--- a/conifer/syrup/views.py
+++ b/conifer/syrup/views.py
@@ -13,7 +13,7 @@ from datetime import datetime
from generics import *
from gettext import gettext as _ # fixme, is this the right function to import?
from django.utils import simplejson
-
+import sys
#------------------------------------------------------------
# Authentication
@@ -136,6 +136,7 @@ def course_search(request, course_id):
class NewCourseForm(ModelForm):
class Meta:
model = models.Course
+ exclude = ('passkey',)
def clean_code(self):
v = (self.cleaned_data.get('code') or '').strip()
@@ -148,10 +149,10 @@ class NewCourseForm(ModelForm):
# I want different choice labels on the access screen than are given
# in the model.
NewCourseForm.base_fields['access'].widget.choices = [
- ('CLOSE', _('For now, just myself and designated colleagues')),
- ('STUDT', _('Students in my course (I will provide section numbers)')),
- ('PASWD', _('Students in my course (I will share a password with them)')),
- ('LOGIN', _('All Reserves patrons'))]
+ (u'CLOSE', _(u'For now, just myself and designated colleagues')),
+ (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'))]
# hack the new-course form if we have course-code lookup
@@ -175,12 +176,14 @@ def add_new_course(request):
@login_required
def edit_course(request, course_id):
- instance = models.Course.objects.get(pk=course_id)
+ instance = get_object_or_404(models.Course, pk=course_id)
return add_or_edit_course(request, instance=instance)
def add_or_edit_course(request, instance=None):
- if instance is None:
+ is_add = (instance is 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
if request.method != 'POST':
form = NewCourseForm(instance=instance)
@@ -192,20 +195,68 @@ def add_or_edit_course(request, instance=None):
else:
form.save()
course = form.instance
+ if course.access == u'INVIT' and not course.passkey:
+ course.generate_new_passkey()
+ course.save()
assert course.id
user_in_course = models.Member.objects.filter(user=request.user,course=course)
if not user_in_course: # for edits, might already be!
mbr = course.member_set.create(user=request.user, role='INSTR')
mbr.save()
-
- # fixme, need to ask about PASWD and STUDT settings before redirect.
- return HttpResponseRedirect('../') # back to My Courses
+
+ if is_add or (current_access_level != course.access):
+ # we need to configure permissions.
+ return HttpResponseRedirect(course.course_url('edit/permission/'))
+ else:
+ return HttpResponseRedirect('../') # back to main view.
def add_new_course_ajax_title(request):
course_code = request.GET['course_code']
title = models.course_codes.course_code_lookup_title(course_code)
return HttpResponse(simplejson.dumps({'title':title}))
+def edit_course_permissions(request, course_id):
+ course = get_object_or_404(models.Course, pk=course_id)
+ if request.method != 'POST':
+ return g.render('edit_course_permissions.xhtml', **locals())
+ else:
+ if request.POST.get('action') == 'change_code':
+ course.generate_new_passkey()
+ course.save()
+ return HttpResponseRedirect('.')
+
+#------------------------------------------------------------
+
+@login_required # must be, to avoid/audit brute force attacks.
+def course_invitation(request):
+ if request.method != 'POST':
+ return g.render('course_invitation.xhtml', code='', error='',
+ **locals())
+ else:
+ code = request.POST.get('code', '').strip()
+ # todo, a pluggable passkey implementation would normalize the code here.
+ if not code:
+ return HttpResponseRedirect('.')
+ try:
+ # note, we only allow the passkey if access='INVIT'.
+ crs = models.Course.objects.filter(access='INVIT').get(passkey=code)
+ except models.Course.DoesNotExist:
+ # todo, do we need a formal logging system? Or a table for
+ # invitation failures? They should be captured somehow, I
+ # think. Should we temporarily disable accounts after
+ # multiple failures?
+ print >> sys.stdout, '[%s] WARN: Invitation failure, user %r gave code %r' % \
+ (datetime.now(), request.user.username, code)
+ error = _('The code you provided is not valid.')
+ return g.render('course_invitation.xhtml', **locals())
+
+ # the passkey is good; add the user if not already a member.
+ if not models.Member.objects.filter(user=request.user, course=crs):
+ mbr = models.Member.objects.create(user=request.user, course=crs,
+ role='STUDT')
+ mbr.save()
+ return HttpResponseRedirect(crs.course_url())
+
#------------------------------------------------------------
def instructor_detail(request, instructor_id):
diff --git a/conifer/templates/course_invitation.xhtml b/conifer/templates/course_invitation.xhtml
new file mode 100644
index 0000000..5b3ea41
--- /dev/null
+++ b/conifer/templates/course_invitation.xhtml
@@ -0,0 +1,27 @@
+
+
+
+
+ ${title}
+
+
+ ${title}
+ Your instructor may have provided you with an Invitation Code,
+ which will give you access to your course's reserves. Enter the
+ invitation code below to continue. Note that not all courses require
+ an invitation code; contact your instructor or the library staff for
+ more information.
+ ${error}
+
+
+
+
diff --git a/conifer/templates/edit_course_permissions.xhtml b/conifer/templates/edit_course_permissions.xhtml
new file mode 100644
index 0000000..21bc498
--- /dev/null
+++ b/conifer/templates/edit_course_permissions.xhtml
@@ -0,0 +1,38 @@
+
+
+
+
+ ${title}
+
+
+ ${title}
+ Add Co-Instructors and Proxies
+ blah blah...
+
+
+
Course Invitation Code
+
Your Course Invitation Code is: ${course.passkey}
+
+
This invitation code will enable your students to join this
+ course site. Share it only with your students: anyone who has
+ the code can join your site.
+
You may change the code at any time. This will not block
+ students who have already joined, but will prevent new students
+ from joining with the old code.
+
+
+
Course section numbers
+
+
+
+
+
diff --git a/conifer/templates/my_courses.xhtml b/conifer/templates/my_courses.xhtml
index c68e718..e0ed1a4 100644
--- a/conifer/templates/my_courses.xhtml
+++ b/conifer/templates/my_courses.xhtml
@@ -21,6 +21,7 @@ title = _('Welcome to Syrup E-Reserves!')
${course.list_display()}
Add a new course
+ Join a course using an Invitation Code