[open-ils-commits] r133 - in servres/trunk/conifer: . static syrup templates templates/admin templates/generic (gfawcett)

svn at svn.open-ils.org svn at svn.open-ils.org
Sun Mar 1 23:09:20 EST 2009


Author: gfawcett
Date: 2009-03-01 23:09:18 -0500 (Sun, 01 Mar 2009)
New Revision: 133

Added:
   servres/trunk/conifer/syrup/generics.py
   servres/trunk/conifer/syrup/on_courses.txt
   servres/trunk/conifer/templates/admin/
   servres/trunk/conifer/templates/admin/index.xhtml
   servres/trunk/conifer/templates/admin/term.xhtml
   servres/trunk/conifer/templates/generic/
   servres/trunk/conifer/templates/generic/delete.xhtml
   servres/trunk/conifer/templates/generic/edit.xhtml
   servres/trunk/conifer/templates/generic/index.xhtml
Modified:
   servres/trunk/conifer/genshi_namespace.py
   servres/trunk/conifer/static/main.css
   servres/trunk/conifer/syrup/models.py
   servres/trunk/conifer/syrup/urls.py
   servres/trunk/conifer/syrup/views.py
   servres/trunk/conifer/templates/tabbar.xhtml
Log:
Admin options for editing terms, deparments: generic indexes and forms.

The big change in this rev is introduction of the Django 'newforms'
system. Since we're using Genshi (not Django templating), newforms
seemed a bad fit. But I think I've got a fairly nice compromise
between Django and Genshi going on in my generics.py.

See the ModelForm subclasses in views.py: the Index attribute is
novel, and specific to the generics.py system.

There is still more to do here, notably figuring out how to handle
permissions and i18n.

Modified: servres/trunk/conifer/genshi_namespace.py
===================================================================
--- servres/trunk/conifer/genshi_namespace.py	2009-03-01 21:18:50 UTC (rev 132)
+++ servres/trunk/conifer/genshi_namespace.py	2009-03-02 04:09:18 UTC (rev 133)
@@ -9,3 +9,11 @@
 # this probably ought to be a method on User, or another model class.
 def instructor_url(instructor, suffix=''):
     return '/syrup/instructor/%d/%s' % (instructor.id, suffix)
+
+
+def call_or_value(obj, dflt=None):
+    # This is used by the generics templates.
+    if callable(obj):
+        return obj() or dflt
+    else:
+        return obj or dflt

Modified: servres/trunk/conifer/static/main.css
===================================================================
--- servres/trunk/conifer/static/main.css	2009-03-01 21:18:50 UTC (rev 132)
+++ servres/trunk/conifer/static/main.css	2009-03-02 04:09:18 UTC (rev 133)
@@ -189,3 +189,6 @@
 #coursebanner h1 { padding: 0; font-size: 110%; }
 
 .breadcrumbs { margin: 8 8 8 0; }
+
+.errorlist { float: right; }
+.errorlist li { color: red; font-size: 90%; }
\ No newline at end of file

Added: servres/trunk/conifer/syrup/generics.py
===================================================================
--- servres/trunk/conifer/syrup/generics.py	                        (rev 0)
+++ servres/trunk/conifer/syrup/generics.py	2009-03-02 04:09:18 UTC (rev 133)
@@ -0,0 +1,53 @@
+import conifer.genshi_support as g
+from django.http import HttpResponse, HttpResponseRedirect
+from django.http import HttpResponseForbidden
+from django.shortcuts import get_object_or_404
+from django.forms import ModelForm, ValidationError
+
+def generic_index(form):
+    assert hasattr(form, 'Index')
+    return g.render('generic/index.xhtml', form=form)
+
+def generic_edit(form, request, obj_id):
+    if obj_id == '0':
+        instance = None
+    else:
+        instance = get_object_or_404(form.Meta.model, pk=obj_id)
+    if request.method == 'GET':
+        form = form(instance=instance)
+        return g.render('generic/edit.xhtml', **locals())
+    else:
+        form = form(request.POST, instance=instance)
+        if not form.is_valid():
+            return g.render('generic/edit.xhtml', **locals())
+        else:
+            form.save()
+            return HttpResponseRedirect('../')
+
+def generic_delete(form, request, obj_id):
+    instance = get_object_or_404(models.Term, pk=obj_id)
+    if request.method != 'POST':
+        form = form(instance=instance)
+        return g.render('generic/delete.xhtml', **locals())
+    else:
+        instance.delete()
+        return HttpResponseRedirect('../')
+
+def generic_handler(form):
+    def handler(request, obj_id=None, action=None):
+        if obj_id is None and action is None:
+            return generic_index(form)
+        elif action is None:
+            return generic_edit(form, request, obj_id)
+        elif action == 'delete':
+            return generic_delete(form, request, obj_id)
+    return handler
+
+
+def strip_and_nonblank(fieldname):
+    def clean(self):
+        v = self.cleaned_data.get(fieldname) or ''
+        if not v.strip():
+            raise ValidationError('Cannot be blank.')
+        return v.strip()
+    return clean

Modified: servres/trunk/conifer/syrup/models.py
===================================================================
--- servres/trunk/conifer/syrup/models.py	2009-03-01 21:18:50 UTC (rev 132)
+++ servres/trunk/conifer/syrup/models.py	2009-03-02 04:09:18 UTC (rev 133)
@@ -127,7 +127,7 @@
 # TERMS, COURSES, MEMBERSHIP
 
 class Term(m.Model):
-    code   = m.CharField(max_length=16, blank=True, null=True)
+    code   = m.CharField(max_length=16, blank=True, null=True, unique=True)
     name   = m.CharField(max_length=255)
     start  = m.DateField()
     finish = m.DateField()

Added: servres/trunk/conifer/syrup/on_courses.txt
===================================================================
--- servres/trunk/conifer/syrup/on_courses.txt	                        (rev 0)
+++ servres/trunk/conifer/syrup/on_courses.txt	2009-03-02 04:09:18 UTC (rev 133)
@@ -0,0 +1,161 @@
+# -*- mode: text; mode: auto-fill; -*-
+
+
+Fleshing out the course model
+--------------------------------------------------
+
+These are just some stream-of-thought notes; don't take them too
+seriously yet.
+
+
+
+We should capitalize on external data sources for course and
+registration information.  At the same time, it must be possible to
+use Syrup without an external source, or in a mixed mode (where some
+courses are defined externally and others are ad-hoc).
+
+There will be local variations in the quality of course information.
+Possibly some can provide lists of known courses, but not registration
+information; others will know students enrolled but may not know
+instructors (or vice versa).
+
+So, in fact we have a number of granular external sources, and Syrup
+should operate with any combination of them.
+
+* list of terms (but allowing ad-hoc terms);
+
+* list of course codes and titles (not time- or term-related);
+
+* list of offerings (course-code offered in term);
+
+* list of sections (same course offered several times in one term,
+  and/or broken up into multiple subgroups. Some reserve courses may
+  aggregate some sections, excluding others). Sections are ultimately
+  the join-points between instructors and students, so we must handle
+  them well;
+
+* cross-listings (equivalent course-codes);
+
+* people (username/identifier, given, surname, email)
+
+* instructor and student relationships (fred teaches FRE233/2009W/01;
+  bill takes ESP125/2009W/03).
+
+
+Rolls-Royce Scenario
+--------------------------------------------------
+
+Shelley Smith wants to set up a reserves-site for the course she's
+teaching next term. She logs into reserves, clicks on My Courses, and
+clicks Add a New Course Site. The form asks her to pick a term (the
+current term is the default, all future terms are listed in a
+drop-down), and to pick a course. The system suggests the courses she
+is currently teaching; but this is for a future term, and she's not
+the instructor of record yet, so she picks the code and title from a
+drop-down list. 
+
+She doesn't specify any sections, since she won't know that for a few
+months yet. The site remains unavailable to students until she does.
+
+Later, she clicks on Invite Students. It asks her to pick the
+course-sections she's teaching from a list. It knows her course is
+cross-listed; the cross-list sections also appear on the list. 
+
+Once the sections are selected, and she presses Continue, the students
+are granted access.
+
+  Variation: staff-assistance
+  --------------------------------------------------
+
+  Shelley contacts Ed at the Reserves desk, and tells him she wants to
+  put some items on reserve. She's teaching MAC-100, Intro to Macrame,
+  section 2, in the upcoming term.
+
+  Ed clicks on Assist, and finds Shelley's in the people-list, and
+  selects her. He clicks on Shelley's Courses, then Add New Course,
+  and enters the term and picks MAC-100 from the list. He knows the
+  section so is able to add that right away. The system alerts him to
+  the cross-listed section, and he adds that too.
+
+  The site won't be ready for students until he (or Shelley) activates
+  the site later on.
+
+  Shelley gets an email letting her know the site is ready for
+  content.
+
+  Data sources
+  --------------------------------------------------
+
+  Shelley and Ed logged in against campus LDAP.
+
+  Shelley's current courses and 
+
+
+
+Another take
+------------------------------------------------------------
+
+Shelley can set up the course well in advance, with a trivial working
+title, e.g. "Macrame". It's one of Shelley's courses, but no one else
+has access, and it's not associated with any term, etc.
+
+  Instructor:    course-add
+  Library-Staff: course-add
+
+Later, when Shelley is ready to open her reserves to her students, she
+opens the site and hits Invite. What appears is a list of the course
+sections she is teaching:
+
+    MAC-100 section 02 -- Introduction to Macrame (58 members)
+    MAC-301 section 01 -- Macrame Advanced Studio (2 members)
+    LAB-203 section 01 -- Macrame in Labour Studies (8 members)
+
+She checkmarks the first and last section, and presses Invite.
+Automatically, her site is available to her students, but it also gets
+a proper title and course number: she is asked to pick either MAC-100
+or LAB-203 as the primary identifier for the site; the other becomes
+an alias.
+
+Later calls Ed -- she's talked with the instructor of Textiles 101,
+and they've agreed to share Shelley's reserves list with the Textiles
+class. She cannot add an arbitrary section to the course site. So Ed
+opens the site; clicks on Invite; clicks Arbitrary Section; and
+specifies the term, course code, and section of the Textiles class.
+
+  Instructor:    course-section-invite
+  Library-Staff: course-section-invite-arbitrary
+
+A key point here is that the formal course numbers may not be
+selectable until late in the game; until just before the course, a
+working title may be all that you have. That's fine. Choosing specific
+course sections can be deferred until the registration info is
+available.
+
+
+What about the case where there is no registration data? How do you
+make a site available to an unknown audience?
+
+Have controls for "publishing" a site, that is, making it available to
+unknown users. A few options:
+
+  * Anyone at all can view the site, even anonymously
+
+  * Any logged-in user can view the site
+
+  * The instructor provides an access key out-of-band. The access key
+    auto-invites you into the site. This isn't so bad really.
+
+  * Any logged-in user can request access; the instructor has to grant
+    access. This way lies madness.
+
+Scrapping the madness-path, we're left with a reasonable set of
+options: Anonymous, Authenticated, AccessKey, Restricted (to members
+of the registered list), or NoAccess (for archived sites, etc.). None
+of this applies to instructors and their proxies, just to visitors.
+
+  Instructor:    course-change-access
+  Library-Staff: course-change-access-extended
+
+  The extended permissions could let the librarian decide if certain
+  types of materials in the site should be inaccessible to Anonymous
+  users. Or maybe we should just scrap Anonymous access altogether.

Modified: servres/trunk/conifer/syrup/urls.py
===================================================================
--- servres/trunk/conifer/syrup/urls.py	2009-03-01 21:18:50 UTC (rev 132)
+++ servres/trunk/conifer/syrup/urls.py	2009-03-02 04:09:18 UTC (rev 133)
@@ -4,8 +4,8 @@
 # want to cut down on the common boilerplate in the urlpatterns below.
 
 ITEM_PREFIX = r'^course/(?P<course_id>\d+)/item/(?P<item_id>\d+)/'
+GENERIC_REGEX = r'((?P<obj_id>\d+)/)?(?P<action>.+)?$'
 
-
 urlpatterns = patterns('conifer.syrup.views',
     (r'^$', 'welcome'),                       
     (r'^course/$', 'my_courses'),
@@ -24,4 +24,11 @@
     (ITEM_PREFIX + r'meta$', 'item_metadata'),
     (ITEM_PREFIX + r'edit/$', 'item_edit'),
     (ITEM_PREFIX + r'add/$', 'item_add'), # for adding sub-things
+    (r'^admin/$', 'admin_index'),
+    (r'^admin/terms/' + GENERIC_REGEX, 'admin_terms'),
+    (r'^admin/depts/' + GENERIC_REGEX, 'admin_depts'),
+
+#     (r'^admin/terms/(?P<term_id>\d+)/$', 'admin_term_edit'),
+#     (r'^admin/terms/(?P<term_id>\d+)/delete$', 'admin_term_delete'),
+#     (r'^admin/terms/$', 'admin_term'),
 )

Modified: servres/trunk/conifer/syrup/views.py
===================================================================
--- servres/trunk/conifer/syrup/views.py	2009-03-01 21:18:50 UTC (rev 132)
+++ servres/trunk/conifer/syrup/views.py	2009-03-02 04:09:18 UTC (rev 133)
@@ -10,6 +10,7 @@
 from django.contrib.auth.models import User
 from django.db.models import Q
 from datetime import datetime
+from generics import *
 
 #------------------------------------------------------------
 # Authentication
@@ -415,3 +416,49 @@
 
     return g.render('search_results.xhtml', **locals())
 
+
+#------------------------------------------------------------
+# administrative options
+
+def admin_index(request):
+    return g.render('admin/index.xhtml')
+
+# fixme, no auth or permissions stuff yet.
+
+class TermForm(ModelForm):
+    class Meta:
+        model = models.Term
+
+    class Index:
+        title = 'Terms'
+        all   = models.Term.objects.order_by('start', 'code').all
+        cols  = ['code', 'name', 'start', 'finish']
+        links = [0,1]
+
+    clean_name = strip_and_nonblank('name')
+    clean_code = strip_and_nonblank('code')
+
+    def clean(self):
+        cd = self.cleaned_data
+        s, f = cd.get('start'), cd.get('finish')
+        if (s and f) and s >= f:
+            raise ValidationError, 'start must precede finish'
+        return cd
+
+admin_terms = generic_handler(TermForm)
+
+class DeptForm(ModelForm):
+    class Meta:
+        model = models.Department
+
+    class Index:
+        title = 'Departments'
+        all   = models.Department.objects.order_by('abbreviation').all
+        cols  = ['abbreviation', 'name']
+        links = [0,1]
+
+    clean_abbreviation = strip_and_nonblank('abbreviation')
+    clean_name = strip_and_nonblank('name')
+
+admin_depts = generic_handler(DeptForm)
+

Added: servres/trunk/conifer/templates/admin/index.xhtml
===================================================================
--- servres/trunk/conifer/templates/admin/index.xhtml	                        (rev 0)
+++ servres/trunk/conifer/templates/admin/index.xhtml	2009-03-02 04:09:18 UTC (rev 133)
@@ -0,0 +1,18 @@
+<?python
+title = 'Administrative Options'
+?>
+<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>
+  <ul>
+    <li><a href="depts/">Departments</a></li>
+    <li><a href="terms/">Terms</a></li>
+  </ul>
+</body>
+</html>

Added: servres/trunk/conifer/templates/admin/term.xhtml
===================================================================
--- servres/trunk/conifer/templates/admin/term.xhtml	                        (rev 0)
+++ servres/trunk/conifer/templates/admin/term.xhtml	2009-03-02 04:09:18 UTC (rev 133)
@@ -0,0 +1,18 @@
+<?python
+title = 'Add a new Term'
+?>
+<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>
+  <form action="." method="POST">
+  <table>${Markup(form.as_table())}</table>
+  <input type="submit" value="Save"/>
+  </form>
+</body>
+</html>

Added: servres/trunk/conifer/templates/generic/delete.xhtml
===================================================================
--- servres/trunk/conifer/templates/generic/delete.xhtml	                        (rev 0)
+++ servres/trunk/conifer/templates/generic/delete.xhtml	2009-03-02 04:09:18 UTC (rev 133)
@@ -0,0 +1,24 @@
+<?python
+title = 'Delete %s?' % form.Meta.model.__name__
+?>
+<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>
+  <form action="delete" method="POST">
+  <table>
+    <tr>
+      <th>${form.Meta.model.__name__}</th>
+      <td>${instance}</td>
+    </tr>
+  </table>
+  <p><input type="submit" value="Delete"/></p>
+  <p><a href="../">Cancel</a></p>
+  </form>
+</body>
+</html>

Added: servres/trunk/conifer/templates/generic/edit.xhtml
===================================================================
--- servres/trunk/conifer/templates/generic/edit.xhtml	                        (rev 0)
+++ servres/trunk/conifer/templates/generic/edit.xhtml	2009-03-02 04:09:18 UTC (rev 133)
@@ -0,0 +1,23 @@
+<?python
+if instance:
+    title = 'Modify %s' % form.Meta.model.__name__
+else:
+    title = 'Add %s' % form.Meta.model.__name__
+?>
+<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>
+  <form action="." method="POST">
+  <table>${Markup(form.as_table())}</table>
+  <p><input type="submit" value="Save changes"/></p>
+  <p><a href="../">Cancel changes</a></p>
+  <p><a href="delete">Delete this record</a></p>
+  </form>
+</body>
+</html>

Added: servres/trunk/conifer/templates/generic/index.xhtml
===================================================================
--- servres/trunk/conifer/templates/generic/index.xhtml	                        (rev 0)
+++ servres/trunk/conifer/templates/generic/index.xhtml	2009-03-02 04:09:18 UTC (rev 133)
@@ -0,0 +1,28 @@
+<?python
+index = form.Index
+title = index.title
+?>
+<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>
+  <table class="pagetable">
+    <thead><tr><th py:for="c in form.Index.cols">${c}</th></tr></thead>
+    <tbody>
+      <tr py:for="r in form.Index.all()">
+	<td py:for="n, c in enumerate(form.Index.cols)">
+	<a py:strip="n not in form.Index.links"
+	   href="${r.id}/">${call_or_value(getattr(r,c))}
+	</a>
+	</td>
+      </tr>
+    </tbody>
+  </table>
+  <p><a href="0/">Add ${form.Meta.model.__name__}</a></p>
+</body>
+</html>

Modified: servres/trunk/conifer/templates/tabbar.xhtml
===================================================================
--- servres/trunk/conifer/templates/tabbar.xhtml	2009-03-01 21:18:50 UTC (rev 132)
+++ servres/trunk/conifer/templates/tabbar.xhtml	2009-03-02 04:09:18 UTC (rev 133)
@@ -10,6 +10,7 @@
   <li><a href="/syrup/">Home</a></li>
   <li><a href="/syrup/browse/">Browse</a></li>
   <li class="active"><a href="/syrup/course/">My Courses</a></li>
+  <li><a href="/syrup/admin/">Admin Options</a></li>
     <!--
         RD had a concept of "joining" which i am
         suppressing for now, our staff is definitely



More information about the open-ils-commits mailing list