add microblogging
authorNicolas Chauvat <nicolas.chauvat@logilab.fr>
Sun, 01 Aug 2010 22:20:35 +0200
changeset 216 e8340fe485c9
parent 215 4c9a9b321087
child 217 fe739ada925c
add microblogging
__pkginfo__.py
data/cubes.blog.css
migration/1.9.0_Any.py
schema.py
sobjects.py
views/boxes.py
views/primary.py
views/secondary.py
--- a/__pkginfo__.py	Sun Jul 25 17:03:20 2010 +0200
+++ b/__pkginfo__.py	Sun Aug 01 22:20:35 2010 +0200
@@ -4,7 +4,7 @@
 modname = 'blog'
 distname = "cubicweb-%s" % modname
 
-numversion = (1, 8, 0)
+numversion = (1, 9, 0)
 version = '.'.join(str(num) for num in numversion)
 
 license = 'LGPL'
@@ -14,7 +14,6 @@
 web = 'http://www.cubicweb.org/project/%s' % distname
 
 description = "blogging component for the CubicWeb framework"
-short_desc = description # XXX cw < 3.8 bw compat
 
 classifiers = [
     'Environment :: Web Environment'
@@ -23,7 +22,11 @@
     'Programming Language :: JavaScript',
     ]
 
+__depends_cubes__ = {'datafeed': None,
+                     }
 __depends__ = {'cubicweb': '>= 3.9.0'}
+for key, value in __depends_cubes__.items():
+    __depends__['cubicweb-'+key] = value
 __recommends_cubes__ = {'tag': None,
                         'comment': '>= 1.6.3'}
 __recommends__ = {}
--- a/data/cubes.blog.css	Sun Jul 25 17:03:20 2010 +0200
+++ b/data/cubes.blog.css	Sun Aug 01 22:20:35 2010 +0200
@@ -49,25 +49,25 @@
 }
 
 span.previousmonth {
- float:left;
+  float:left;
 }
 
 span.nextmonth {
- float:right;
+  float:right;
 }
 
 div.author_date div{
- float:right;
- padding-top: 3px;
- color: #999999;
+  float:right;
+  padding-top: 3px;
+  color: #999999;
 }
 
 div.author_date {
- font-size:1.2em;
- font-style:italic;
- border-top-style: ridge;
- border-top-color: #CCC;
- border-top-width: thin;
+  font-size:1.2em;
+  font-style:italic;
+  border-top-style: ridge;
+  border-top-color: #CCC;
+  border-top-width: thin;
 }
 
 /*div.blogentry_title h1{
@@ -78,5 +78,23 @@
 }*/
 
 div.blogentry_title {
- padding: 0px 0px 15px 0px;
+  padding: 0px 0px 15px 0px;
+}
+
+div.microblog {
+  clear: both;
 }
+
+div.microblog span.author {
+  margin: 0 10px 5px 0;
+  float: left;
+}
+
+div.microblog span.msgtxt {
+  margin: 0 0 10px 0;
+}
+
+div.microblog span.meta {
+  color: #999;
+  display: block;
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/migration/1.9.0_Any.py	Sun Aug 01 22:20:35 2010 +0200
@@ -0,0 +1,2 @@
+add_entity_type('MicroBlogEntry')
+add_entity_type('UserAccount')
--- a/schema.py	Sun Jul 25 17:03:20 2010 +0200
+++ b/schema.py	Sun Aug 01 22:20:35 2010 +0200
@@ -1,4 +1,4 @@
-from yams.buildobjs import EntityType, String, RichString, SubjectRelation
+from yams.buildobjs import EntityType, String, RichString, SubjectRelation, RelationDefinition
 from cubicweb.schema import WorkflowableEntityType, ERQLExpression
 
 class Blog(EntityType):
@@ -16,4 +16,29 @@
         }
     title = String(required=True, fulltextindexed=True, maxsize=256)
     content = RichString(required=True, fulltextindexed=True)
-    entry_of = SubjectRelation('Blog', cardinality='**')
+    entry_of = SubjectRelation('Blog')
+    same_as = SubjectRelation('ExternalUri')
+
+
+class MicroBlogEntry(EntityType):
+    __permissions__ = {
+        'read': ('managers', 'users'),
+        'add': ('managers', 'users'),
+        'update': ('managers', 'owners'),
+        'delete': ('managers', 'owners')
+        }
+    content = RichString(required=True, fulltextindexed=True)
+    entry_of = SubjectRelation('Blog')
+    same_as = SubjectRelation('ExternalUri')
+
+class UserAccount(EntityType):
+    name = String(required=True) # see foaf:accountName
+
+class has_creator(RelationDefinition):
+    subject = ('BlogEntry', 'MicroBlogEntry')
+    object = 'UserAccount'
+
+class has_avatar(RelationDefinition):
+    subject = 'UserAccount'
+    object = 'ExternalUri'
+
--- a/sobjects.py	Sun Jul 25 17:03:20 2010 +0200
+++ b/sobjects.py	Sun Aug 01 22:20:35 2010 +0200
@@ -1,8 +1,9 @@
 # -*- coding: utf-8 -*-
 
 import sys
+import re
 from datetime import datetime
-from lxml import etree
+from lxml.html import fromstring, tostring
 import feedparser
 import rdflib
 
@@ -35,38 +36,102 @@
         item['content'] = unicode(get_object(g, post, sioc_content))
         yield item
 
+format_map = {'application/xhtml+xml':u'text/html',
+              'text/html':u'text/html',
+              'text/plain':u'text/plain',
+              }
+
+def remove_content_spies(content):
+    root = fromstring(content)
+    for img in root.findall('.//img'):
+        if img.get('src').startswith('http://feeds.feedburner.com'):
+            img.drop_tree()
+    for anchor in root.findall('.//a'):
+        href = anchor.get('href')
+        if href and href.startswith('http://api.tweetmeme.com/share'):
+            anchor.drop_tree()
+    return unicode(tostring(root))
+
 def parse_blogpost_rss(url):
     feed = feedparser.parse(url)
     for entry in feed.entries:
         item = {}
+        if 'id' in entry:
+            item['uri'] = entry.id
+        else:
+            item['uri'] = entry.link
+        item['title'] = entry.title
+        if hasattr(entry, 'content'):
+            content = entry.content[0].value
+            mimetype = entry.content[0].type
+        elif hasattr(entry, 'summary_detail'):
+            content = entry.summary_detail.value
+            mimetype = entry.summary_detail.type
+        else:
+            content = u''#XXX entry.description?
+            mimetype = u'text/plain'
+        if mimetype == u'text/html':
+            content = remove_content_spies(content)
+        item['content'] = content
+        item['content_format'] = format_map.get(mimetype, u'text/plain')
+        if hasattr(entry, 'date_parsed'):
+            item['creation_date'] = datetime(*entry.date_parsed[:6])
+        yield item
+
+def parse_microblogpost_rss(url):
+    feed = feedparser.parse(url)
+    for entry in feed.entries:
+        item = {}
         item['uri'] = entry.id
-        item['title'] = entry.title
-        item['content'] = entry.description
+        item['content'] = entry.description.split(':',1)[1][:140]
         item['creation_date'] = datetime(*entry.date_parsed[:6])
+        item['modification_date'] = datetime(*entry.date_parsed[:6])
+        item['author'] = feed.channel.link # true for twitter
+        screen_name = feed.channel.link.split('/')[-1]
+        item['avatar'] = get_twitter_avatar(screen_name)
         yield item
 
+AVATAR_CACHE = {}
+
+def get_twitter_avatar(screen_name):
+    if screen_name not in AVATAR_CACHE:
+        from urllib2 import urlopen
+        import simplejson
+        data = urlopen('http://api.twitter.com/1/users/show.json?screen_name=%s' % screen_name).read()
+        user = simplejson.loads(data)
+        AVATAR_CACHE[screen_name] = user['profile_image_url']
+    return AVATAR_CACHE[screen_name]
+
 class BlogPostParser(DataFeedParser):
     __abstract__ = True
+    entity_type = 'BlogEntry'
 
     def process(self, url):
         for item in self.parse(url):
-            euri = self.sget_externaluri(item.pop('uri'))
+            author = item.pop('author', None)
+            avatar = item.pop('avatar', None)
+            euri = self.sget_entity('ExternalUri', uri=item.pop('uri'))
             if euri.same_as:
                 sys.stdout.write('.')
-                self.update_blogpost(euri.same_as[0], item)
+                post = self.update_blogpost(euri.same_as[0], item)
             else:
                 sys.stdout.write('+')
-                self.create_blogpost(item, euri)
+                post = self.create_blogpost(item, euri)
+            if author:
+                account = self.sget_entity('UserAccount', name=author)
+                self.sget_relation(post.eid, 'has_creator', account.eid)
+                if avatar:
+                    auri = self.sget_entity('ExternalUri', uri=avatar)
+                    self.sget_relation(account.eid, 'has_avatar', auri.eid)
             sys.stdout.flush()
 
     def create_blogpost(self, item, uri):
-        entity = self._cw.create_entity('BlogEntry', **item)
+        entity = self._cw.create_entity(self.entity_type, **item)
         entity.set_relations(same_as=uri)
-        return self.update_blogpost(entity, None)
+        return entity
 
     def update_blogpost(self, entity, item):
-        if item:
-            entity.set_attributes(**item)
+        entity.set_attributes(**item)
         return entity
 
 class BlogPostSiocParser(BlogPostParser):
@@ -77,6 +142,11 @@
     __regid__ = 'blogpost-rss'
     parse = staticmethod(parse_blogpost_rss)
 
+class MicroBlogPostRSSParser(BlogPostParser):
+    __regid__ = 'microblogpost-rss'
+    entity_type = 'MicroBlogEntry'
+    parse = staticmethod(parse_microblogpost_rss)
+
 if __name__ == '__main__':
     import sys
     from pprint import pprint
--- a/views/boxes.py	Sun Jul 25 17:03:20 2010 +0200
+++ b/views/boxes.py	Sun Aug 01 22:20:35 2010 +0200
@@ -16,6 +16,7 @@
 class BlogArchivesBox(boxes.BoxTemplate):
     """blog side box displaying a Blog Archive"""
     __regid__ = 'blog_archives_box'
+    __select__ = boxes.BoxTemplate.__select__ & is_instance('Blog','BlogEntry','MicroBlogEntry')
     title = _('boxes_blog_archives_box')
     order = 35
 
@@ -31,6 +32,7 @@
 
 class BlogsByAuthorBox(boxes.BoxTemplate):
     __regid__ = 'blog_summary_box'
+    __select__ = boxes.BoxTemplate.__select__ & is_instance('Blog','BlogEntry','MicroBlogEntry')
     title = _('boxes_blog_summary_box')
     order = 36
 
@@ -50,6 +52,7 @@
 class LatestBlogsBox(boxes.BoxTemplate):
     """display a box with latest blogs and rss"""
     __regid__ = 'latest_blogs_box'
+    __select__ = boxes.BoxTemplate.__select__ & is_instance('Blog','BlogEntry','MicroBlogEntry')
     title = _('latest_blogs_box')
     visible = True # enabled by default
     order = 34
--- a/views/primary.py	Sun Jul 25 17:03:20 2010 +0200
+++ b/views/primary.py	Sun Aug 01 22:20:35 2010 +0200
@@ -11,7 +11,7 @@
 from cubicweb.utils import UStringIO
 from cubicweb.selectors import is_instance
 from cubicweb.web import uicfg, component
-from cubicweb.web.views import primary, workflow
+from cubicweb.web.views import primary, workflow, baseviews
 
 _pvs = uicfg.primaryview_section
 _pvs.tag_attribute(('Blog', 'title'), 'hidden')
@@ -19,6 +19,8 @@
 _pvs.tag_attribute(('BlogEntry', 'title'), 'hidden')
 _pvs.tag_object_of(('*', 'entry_of', 'Blog'), 'hidden')
 _pvs.tag_subject_of(('BlogEntry', 'entry_of', '*'), 'relations')
+_pvs.tag_object_of(('*', 'has_creator', 'UserAccount'), 'relations')
+_pvs.tag_attribute(('UserAccount', 'name'), 'hidden')
 
 _pvdc = uicfg.primaryview_display_ctrl
 _pvdc.tag_attribute(('Blog', 'description'), {'showlabel': False})
@@ -43,9 +45,6 @@
             self.wview('sameetypelist', rset, showtitle=False)
             self.w(strio.getvalue())
 
-
-
-
 class SubscribeToBlogComponent(component.EntityVComponent):
     __regid__ = 'blogsubscribe'
     __select__ = component.EntityVComponent.__select__ & is_instance('Blog')
@@ -96,3 +95,42 @@
 
     def cell_call(self, row, col, view=None):
         pass
+
+def format_microblog(entity):
+    author = entity.has_creator[0]
+    if author.has_avatar:
+        imgurl = author.has_avatar[0].uri
+        ablock = u'<a href="%s"><img src="%s" /></a>' % (author.absolute_url(),
+                                                         xml_escape(imgurl))
+    else:
+        ablock = entity.has_creator[0].view('outofcontext')
+    words = []
+    for word in entity.content.split():
+        if word.startswith('http://'):
+            word = u'<a href="%s">%s</a>' % (word, word)
+        else:
+            word = xml_escape(word)
+        words.append(word)
+    content = u' '.join(words)
+    return (u'<div class="microblog">'
+            u'<span class="author">%s</span>'
+            u'<span class="msgtxt">%s</span>'
+            u'<span class="meta">%s</span>'
+            u'</div>' % (ablock, content, entity.creation_date))
+
+class MicroBlogEntryPrimaryView(primary.PrimaryView):
+    __select__ = primary.PrimaryView.__select__ & is_instance('MicroBlogEntry')
+
+    def cell_call(self, row, col):
+        self._cw.add_css('cubes.blog.css')
+        entity = self.cw_rset.get_entity(row, col)
+        self.w(format_microblog(entity))
+
+class MicroBlogEntrySameETypeListView(baseviews.SameETypeListView):
+    __select__ = baseviews.SameETypeListView.__select__ & is_instance('MicroBlogEntry')
+
+    def cell_call(self, row, col):
+        self._cw.add_css('cubes.blog.css')
+        entity = self.cw_rset.get_entity(row, col)
+        self.w(format_microblog(entity))
+
--- a/views/secondary.py	Sun Jul 25 17:03:20 2010 +0200
+++ b/views/secondary.py	Sun Aug 01 22:20:35 2010 +0200
@@ -128,11 +128,20 @@
     __regid__ = 'blog'
     __select__ = is_instance('BlogEntry')
 
+    def toolbar_components(self, context):
+        # copy from PrimaryView
+        self.w(u'<div class="%s">' % context)
+        for comp in self._cw.vreg['toolbar'].poss_visible_objects(
+            self._cw, rset=self.cw_rset, row=self.cw_row, view=self, context=context):
+            comp.render(w=self.w, row=self.cw_row, view=self)
+        self.w(u'</div>')
+
     def cell_call(self, row, col, **kwargs):
         entity = self.cw_rset.get_entity(row, col)
         w = self.w
         _ = self._cw._
         w(u'<div class="post">')
+        self.toolbar_components('ctxtoolbar')
         render_blogentry_title(self._cw, w, entity)
         w(u'<div class="entry">')
         body = entity.printable_value('content')