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}
+
+ + + +
Invitation Code:
Go back
+
+
+ + 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

-- 2.11.0