From 55ee94a71a5ccfc89bcb1b34cc019df06455cf03 Mon Sep 17 00:00:00 2001 From: gfawcett Date: Fri, 13 Mar 2009 01:57:31 +0000 Subject: [PATCH] Feeds! Atom feeds for course-site items. 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 | 4 +-- conifer/static/main.css | 2 ++ conifer/syrup/models.py | 6 ++-- conifer/syrup/urls.py | 2 +- conifer/syrup/views.py | 48 +++++++++++++++++++++++++ conifer/templates/components/course.xhtml | 1 + conifer/templates/feeds/course_atom.xml | 24 +++++++++++++ conifer/templates/feeds/course_feed_index.xhtml | 22 ++++++++++++ 8 files changed, 103 insertions(+), 6 deletions(-) create mode 100644 conifer/templates/feeds/course_atom.xml create mode 100644 conifer/templates/feeds/course_feed_index.xhtml diff --git a/conifer/genshi_support.py b/conifer/genshi_support.py index 8b36996..76d2fbb 100644 --- a/conifer/genshi_support.py +++ b/conifer/genshi_support.py @@ -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)) diff --git a/conifer/static/main.css b/conifer/static/main.css index 2f48e42..aade963 100644 --- a/conifer/static/main.css +++ b/conifer/static/main.css @@ -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; } diff --git a/conifer/syrup/models.py b/conifer/syrup/models.py index 0b55722..f8fc6f0 100644 --- a/conifer/syrup/models.py +++ b/conifer/syrup/models.py @@ -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' % ( diff --git a/conifer/syrup/urls.py b/conifer/syrup/urls.py index b9c24a8..720f02e 100644 --- a/conifer/syrup/urls.py +++ b/conifer/syrup/urls.py @@ -26,6 +26,7 @@ urlpatterns = patterns('conifer.syrup.views', (r'^course/(?P\d+)/edit/$', 'edit_course'), (r'^course/(?P\d+)/edit/delete/$', 'delete_course'), (r'^course/(?P\d+)/edit/permission/$', 'edit_course_permissions'), + (r'^course/(?P\d+)/feeds/(?P.*)$', 'course_feeds'), (ITEM_PREFIX + r'$', 'item_detail'), (ITEM_PREFIX + r'dl/(?P.*)$', '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\d+)/$', 'admin_term_edit'), # (r'^admin/terms/(?P\d+)/delete$', 'admin_term_delete'), # (r'^admin/terms/$', 'admin_term'), diff --git a/conifer/syrup/views.py b/conifer/syrup/views.py index b61936d..7d0f3fd 100644 --- a/conifer/syrup/views.py +++ b/conifer/syrup/views.py @@ -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') diff --git a/conifer/templates/components/course.xhtml b/conifer/templates/components/course.xhtml index 43375c9..611f846 100644 --- a/conifer/templates/components/course.xhtml +++ b/conifer/templates/components/course.xhtml @@ -22,6 +22,7 @@ searchtext = _('search this course...') ${course_search(course)}

${course.code}: ${course.title}

+ diff --git a/conifer/templates/feeds/course_atom.xml b/conifer/templates/feeds/course_atom.xml new file mode 100644 index 0000000..562f6f5 --- /dev/null +++ b/conifer/templates/feeds/course_atom.xml @@ -0,0 +1,24 @@ + + + Reserves (${feed_type}) for ${course.list_display()} + + + ${root}${course.course_url('feeds/') + feed_type} + ${lastmod.strftime("%Y-%m-%dT%H:%M:%SZ")} + + ${render_title(item)} + + ${item.last_modified.strftime("%Y-%m-%dT%H:%M:%SZ")} + ${root}${item.item_url('')} + ${item.get_item_type_display()}. +
+ Contains ${len(models.Item.objects.filter(parent_heading=item))} items. +
+
+
+
diff --git a/conifer/templates/feeds/course_feed_index.xhtml b/conifer/templates/feeds/course_feed_index.xhtml new file mode 100644 index 0000000..8d1b8f0 --- /dev/null +++ b/conifer/templates/feeds/course_feed_index.xhtml @@ -0,0 +1,22 @@ + + + + + + ${title} +