Feeds! Atom feeds for course-site items.
authorgfawcett <gfawcett@6d9bc8c9-1ec2-4278-b937-99fde70a366f>
Fri, 13 Mar 2009 01:57:31 +0000 (01:57 +0000)
committergfawcett <gfawcett@6d9bc8c9-1ec2-4278-b937-99fde70a366f>
Fri, 13 Mar 2009 01:57:31 +0000 (01:57 +0000)
Some notes:

* all feeds are Atom; comments on my Atom details are welcome.

* several different feeds per course site. E.g., just top-level items;
  recently-changed items; a walk of all items in the site directory
  tree; many others possible.

* by design, the feeds themselves are anonymous-access. I don't see a
  real security risk here, but if exposing titles and modification
  dates violates some policy, we can change it.

* all links in the feeds refer back to the Reserves system, so they
  can be authenticated if necessary. This is also true for "URL items"
  -- the Atom link is back to the canonical item-URL in Reserves,
  which redirects to the target URL (if you're allowed to know it).

* Django has its own feed system. I tried it, and then chose not to
  use it. Genshi does a fine job, and IMHO Django makes it harder to
  offer multiple feed-variants on individual items like Courses. It
  looks good for simpler feed-needs though, and has the benefit of
  supporting both Atom and RSS. (Not that we couldn't do that with
  Genshi too.)

There's room for more feed types: "My Courses", "Things in My
Courses", "canned search", etc. Ideas are most welcome.

git-svn-id: svn://svn.open-ils.org/ILS-Contrib/servres/trunk@168 6d9bc8c9-1ec2-4278-b937-99fde70a366f

conifer/genshi_support.py
conifer/static/main.css
conifer/syrup/models.py
conifer/syrup/urls.py
conifer/syrup/views.py
conifer/templates/components/course.xhtml
conifer/templates/feeds/course_atom.xml [new file with mode: 0644]
conifer/templates/feeds/course_feed_index.xhtml [new file with mode: 0644]

index 8b36996..76d2fbb 100644 (file)
@@ -43,8 +43,8 @@ def _inject_django_things_into_namespace(request, ns):
 #------------------------------------------------------------
 # main API
 
-def render(tname, _django_type=HttpResponse, **kwargs):
+def render(tname, _django_type=HttpResponse, _serialization='xhtml', **kwargs):
     request = get_request()
     _inject_django_things_into_namespace(request, kwargs)
-    return _django_type(template(tname).generate(**kwargs).render('xhtml'))
+    return _django_type(template(tname).generate(**kwargs).render(_serialization))
 
index 2f48e42..aade963 100644 (file)
@@ -199,6 +199,8 @@ p.todo, div.todo { background-color: #fdd; padding: 6; margin: 12; border-left:
 
 #edit_course_link { margin: 8 0 8 0; font-size: 95%; }
 
+#feeds_panel { float: right; font-size: 95%; margin: 8 0; }
+
 .breadcrumbs { margin: 8 8 8 0; }
 
 .errorlist { float: right; }
index 0b55722..f8fc6f0 100644 (file)
@@ -422,7 +422,7 @@ class Item(m.Model):
 
 
     date_created = m.DateTimeField(auto_now_add=True)
-    last_modified = m.DateTimeField()
+    last_modified = m.DateTimeField(auto_now=True)
 
     def title_hl(self, terms):
         hl_title = self.title
@@ -458,12 +458,12 @@ class Item(m.Model):
 
         return self.item_type in ('ELEC', 'URL')
 
-    def item_url(self, suffix=''):
+    def item_url(self, suffix='', force_local_url=False):
         if self.item_type == 'ELEC' and suffix == '':
             return '/syrup/course/%d/item/%d/dl/%s' % (
                 self.course_id, self.id, 
                 self.fileobj.name.split('/')[-1])
-        if self.item_type == 'URL' and suffix == '':
+        if self.item_type == 'URL' and suffix == '' and not force_local_url:
             return self.url
         else:
             return '/syrup/course/%d/item/%d/%s' % (
index b9c24a8..720f02e 100644 (file)
@@ -26,6 +26,7 @@ urlpatterns = patterns('conifer.syrup.views',
     (r'^course/(?P<course_id>\d+)/edit/$', 'edit_course'),
     (r'^course/(?P<course_id>\d+)/edit/delete/$', 'delete_course'),
     (r'^course/(?P<course_id>\d+)/edit/permission/$', 'edit_course_permissions'),
+    (r'^course/(?P<course_id>\d+)/feeds/(?P<feed_type>.*)$', 'course_feeds'),
     (ITEM_PREFIX + r'$', 'item_detail'),
     (ITEM_PREFIX + r'dl/(?P<filename>.*)$', 'item_download'),
     (ITEM_PREFIX + r'meta$', 'item_metadata'),
@@ -35,7 +36,6 @@ urlpatterns = patterns('conifer.syrup.views',
     (r'^admin/terms/' + GENERIC_REGEX, 'admin_terms'),
     (r'^admin/depts/' + GENERIC_REGEX, 'admin_depts'),
     (r'^admin/news/' + GENERIC_REGEX, 'admin_news'),
-
 #     (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'),
index b61936d..7d0f3fd 100644 (file)
@@ -156,6 +156,11 @@ def admin_only(handler):
             return _access_denied(_('Only administrators are allowed here.'))
     return hdlr
 
+#decorator
+def public(handler):
+    # A no-op! Just here to be used to explicitly decorate methods
+    # that are supposed to be public.
+    return handler
 #-----------------------------------------------------------------------------
 
 def welcome(request):
@@ -838,3 +843,46 @@ class NewsForm(ModelForm):
     clean_body = strip_and_nonblank('body')
 
 admin_news = generic_handler(NewsForm, decorator=admin_only)
+
+
+
+#-----------------------------------------------------------------------------
+# Course feeds
+
+@public                         # and proud of it!
+def course_feeds(request, course_id, feed_type):
+    course = get_object_or_404(models.Course, pk=course_id)
+    if feed_type == '':
+        return g.render('feeds/course_feed_index.xhtml', 
+                        course=course)
+    else:
+        items = course.items()
+        def render_title(item):
+            return item.title
+        if feed_type == 'top-level':
+            items = items.filter(parent_heading=None).order_by('-sort_order')
+        elif feed_type == 'recent-changes':
+            items = items.order_by('-last_modified')
+        elif feed_type == 'tree':
+            def flatten(nodes, acc):
+                for node in nodes:
+                    item, kids = node
+                    acc.append(item)
+                    flatten(kids, acc)
+                return acc
+            items = flatten(course.item_tree(), [])
+            def render_title(item):
+                if item.parent_heading:
+                    return '%s :: %s' % (item.parent_heading.title, item.title)
+                else:
+                    return item.title
+
+        lastmod = max(i.last_modified for i in items)
+        return g.render('feeds/course_atom.xml',
+                        course=course,
+                        feed_type=feed_type,
+                        lastmod=lastmod,
+                        render_title=render_title,
+                        items=items,
+                        root='http://localhost:8000',
+                        _serialization='xml')
index 43375c9..611f846 100644 (file)
@@ -22,6 +22,7 @@ searchtext = _('search this course...')
       ${course_search(course)}
       <h1><a href="${course.course_url()}"><span py:if="course.code">${course.code}: </span>${course.title}</a></h1>
     </div>
+      <div id="feeds_panel"><a href="${course.course_url('feeds/')}">Feeds</a></div>
   </div>
   
   <!-- !show_tree: display a tree of items in a hierarchical style. -->
diff --git a/conifer/templates/feeds/course_atom.xml b/conifer/templates/feeds/course_atom.xml
new file mode 100644 (file)
index 0000000..562f6f5
--- /dev/null
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<feed xmlns="http://www.w3.org/2005/Atom"
+      xmlns:xi="http://www.w3.org/2001/XInclude"
+      xmlns:py="http://genshi.edgewall.org/">
+  <title>Reserves (${feed_type}) for ${course.list_display()}</title>
+  <link href="${root}${course.course_url()}"
+  rel="alternate"/>
+  <link href="${root}${course.course_url('feeds/' + feed_type)}"
+  rel="self"/>
+  <id>${root}${course.course_url('feeds/') + feed_type}</id>
+  <updated>${lastmod.strftime("%Y-%m-%dT%H:%M:%SZ")}</updated>
+  <entry py:for="item in items">
+    <title>${render_title(item)}</title>
+    <link href="${root}${item.item_url(force_local_url=True)}"
+    rel="alternate"></link>
+    <updated>${item.last_modified.strftime("%Y-%m-%dT%H:%M:%SZ")}</updated>
+    <id>${root}${item.item_url('')}</id>
+    <summary type="html">${item.get_item_type_display()}.
+    <div py:if="item.item_type=='HEADING'" py:strip="True">
+      Contains ${len(models.Item.objects.filter(parent_heading=item))} items.
+    </div>
+  </summary>
+  </entry>
+</feed>
diff --git a/conifer/templates/feeds/course_feed_index.xhtml b/conifer/templates/feeds/course_feed_index.xhtml
new file mode 100644 (file)
index 0000000..8d1b8f0
--- /dev/null
@@ -0,0 +1,22 @@
+<?python
+title = _('Available Feeds')
+?>
+<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"/>
+  <xi:include href="../components/course.xhtml"/>
+  <head>
+    <title>${title}</title>
+    <script type="text/javascript" src="/static/menublocks.js"/>
+  </head>
+  <body>
+    ${course_banner(course)}
+    <h2>Available Feeds</h2>
+    <ul>
+      <li><a href="recent-changes">Recent changes</a></li>
+      <li><a href="top-level">Top level items in this site</a></li>
+      <li><a href="tree">Tree-walk of all items in this site</a></li>
+    </ul>
+  </body>
+</html>