[open-ils-commits] r147 - in servres/trunk/conifer: syrup templates (gfawcett)

svn at svn.open-ils.org svn at svn.open-ils.org
Sun Mar 8 13:11:16 EDT 2009


Author: gfawcett
Date: 2009-03-08 13:11:14 -0400 (Sun, 08 Mar 2009)
New Revision: 147

Added:
   servres/trunk/conifer/templates/course_invitation.xhtml
   servres/trunk/conifer/templates/edit_course_permissions.xhtml
Modified:
   servres/trunk/conifer/syrup/models.py
   servres/trunk/conifer/syrup/urls.py
   servres/trunk/conifer/syrup/views.py
   servres/trunk/conifer/templates/my_courses.xhtml
Log:
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.


Modified: servres/trunk/conifer/syrup/models.py
===================================================================
--- servres/trunk/conifer/syrup/models.py	2009-03-08 01:57:06 UTC (rev 146)
+++ servres/trunk/conifer/syrup/models.py	2009-03-08 17:11:14 UTC (rev 147)
@@ -7,6 +7,7 @@
 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>'):
@@ -157,11 +158,11 @@
                                  ('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 @@
         # 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 @@
     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)

Modified: servres/trunk/conifer/syrup/urls.py
===================================================================
--- servres/trunk/conifer/syrup/urls.py	2009-03-08 01:57:06 UTC (rev 146)
+++ servres/trunk/conifer/syrup/urls.py	2009-03-08 17:11:14 UTC (rev 147)
@@ -11,6 +11,7 @@
     (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'),
@@ -22,6 +23,7 @@
     (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'),

Modified: servres/trunk/conifer/syrup/views.py
===================================================================
--- servres/trunk/conifer/syrup/views.py	2009-03-08 01:57:06 UTC (rev 146)
+++ servres/trunk/conifer/syrup/views.py	2009-03-08 17:11:14 UTC (rev 147)
@@ -13,8 +13,8 @@
 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 @@
 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 @@
 # 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 @@
 
 @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,22 +195,70 @@
         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('.')
+
 #------------------------------------------------------------
 
+ at 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):
     page_num = int(request.GET.get('page', 1))
     count = int(request.GET.get('count', 5))

Added: servres/trunk/conifer/templates/course_invitation.xhtml
===================================================================
--- servres/trunk/conifer/templates/course_invitation.xhtml	                        (rev 0)
+++ servres/trunk/conifer/templates/course_invitation.xhtml	2009-03-08 17:11:14 UTC (rev 147)
@@ -0,0 +1,27 @@
+<?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>

Added: servres/trunk/conifer/templates/edit_course_permissions.xhtml
===================================================================
--- servres/trunk/conifer/templates/edit_course_permissions.xhtml	                        (rev 0)
+++ servres/trunk/conifer/templates/edit_course_permissions.xhtml	2009-03-08 17:11:14 UTC (rev 147)
@@ -0,0 +1,38 @@
+<?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>

Modified: servres/trunk/conifer/templates/my_courses.xhtml
===================================================================
--- servres/trunk/conifer/templates/my_courses.xhtml	2009-03-08 01:57:06 UTC (rev 146)
+++ servres/trunk/conifer/templates/my_courses.xhtml	2009-03-08 17:11:14 UTC (rev 147)
@@ -21,6 +21,7 @@
     <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>



More information about the open-ils-commits mailing list