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='<strong class="highlight">\\1</strong>'):
('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
# 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:
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)
(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_option>.*)/$', 'browse_courses'),
(r'^prefs/$', 'user_prefs'),
(r'^department/(?P<department_id>.*)/$', 'department_detail'),
(r'^course/(?P<course_id>\d+)/search/$', 'course_search'),
(r'^course/(?P<course_id>\d+)/edit/$', 'edit_course'),
+ (r'^course/(?P<course_id>\d+)/edit/permission/$', 'edit_course_permissions'),
(ITEM_PREFIX + r'$', 'item_detail'),
(ITEM_PREFIX + r'dl/(?P<filename>.*)$', 'item_download'),
(ITEM_PREFIX + r'meta$', 'item_metadata'),
from generics import *
from gettext import gettext as _ # fixme, is this the right function to import?
from django.utils import simplejson
-
+import sys
#------------------------------------------------------------
# Authentication
class NewCourseForm(ModelForm):
class Meta:
model = models.Course
+ exclude = ('passkey',)
def clean_code(self):
v = (self.cleaned_data.get('code') or '').strip()
# 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
@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)
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):
--- /dev/null
+<?python
+title = _('Join a course using an Invitation Code')
+?>
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xi="http://www.w3.org/2001/XInclude"
+ xmlns:py="http://genshi.edgewall.org/">
+<xi:include href="master.xhtml"/>
+<head>
+ <title>${title}</title>
+</head>
+<body>
+ <h1>${title}</h1>
+ <p>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.</p>
+ <div class="errors" py:if="error">${error}</div>
+ <form action="." method="POST">
+ <table>
+ <tr><th>Invitation Code:</th><td><input type="text" name="code" size="30" value="${code}"/></td></tr>
+ <tr><th/><td><input type="submit" value="Continue"/> <a style="margin-left: 12;" href="../">Go back</a></td></tr>
+ </table>
+ </form>
+ <div class="gap"/>
+</body>
+</html>
--- /dev/null
+<?python
+title = _('Edit course permissions')
+?>
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xi="http://www.w3.org/2001/XInclude"
+ xmlns:py="http://genshi.edgewall.org/">
+<xi:include href="master.xhtml"/>
+<head>
+ <title>${title}</title>
+</head>
+<body>
+ <h1>${title}</h1>
+ <h2>Add Co-Instructors and Proxies</h2>
+ <p>blah blah...</p>
+ <div py:choose="course.access">
+ <div py:when="'INVIT'">
+ <h2>Course Invitation Code</h2>
+ <p style="font-size: larger;">Your Course Invitation Code is: <strong>${course.passkey}</strong></p>
+ <form action="." method="POST">
+ <p>
+ <input type="hidden" name="action" value="change_code"/>
+ <input type="submit" value="Select a new code (this will invalidate the old code!)"/>
+ </p>
+ </form>
+ <p>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.</p>
+ <p>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.</p>
+ </div>
+ <div py:when="'STUDT'">
+ <h2>Course section numbers</h2>
+ </div>
+ </div>
+ <div class="gap"/>
+</body>
+</html>
<a href="${course.id}/">${course.list_display()}</a>
</p>
<p><a href="new/">Add a new course</a></p>
+ <p><a href="invitation/">Join a course using an Invitation Code</a></p>
<div class="gap"/>
</body>
</html>