Commit e3e49f1d authored by Nicolas Chauvat's avatar Nicolas Chauvat
Browse files

add microblogging

parent 4c9a9b321087
......@@ -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 @@ author_email = "contact@logilab.fr"
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 @@ classifiers = [
'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__ = {}
......
......@@ -49,25 +49,25 @@ ul.simple li {
}
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.author_date {
}*/
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
add_entity_type('MicroBlogEntry')
add_entity_type('UserAccount')
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 @@ class BlogEntry(WorkflowableEntityType):
}
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'
# -*- 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 @@ def parse_blogpost_sioc(url):
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 = {}
item['uri'] = entry.id
if 'id' in entry:
item['uri'] = entry.id
else:
item['uri'] = entry.link
item['title'] = entry.title
item['content'] = entry.description
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['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 @@ class BlogPostRSSParser(BlogPostParser):
__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
......
......@@ -16,6 +16,7 @@ from cubicweb.web.views import boxes
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 BlogArchivesBox(boxes.BoxTemplate):
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 BlogsByAuthorBox(boxes.BoxTemplate):
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
......
......@@ -11,7 +11,7 @@ from logilab.mtconverter import xml_escape
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(('Blog', 'rss_url'), 'hidden')
_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 @@ class BlogPrimaryView(primary.PrimaryView):
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 @@ class BlogEntryWFHistoryVComponent(workflow.WFHistoryVComponent):
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))
......@@ -128,11 +128,20 @@ class BlogEntryBlogView(EntityView):
__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')
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment