default is now stable stable
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Mon, 15 Feb 2010 16:48:59 +0100
branchstable
changeset 168 901959b5b867
parent 167 b269ce395932 (current diff)
parent 166 d481b1bd5dac (diff)
child 169 8ddb43387dda
child 170 daa9767c1dad
default is now stable
.hgtags
views.py
--- a/.hgtags	Mon Feb 15 16:48:35 2010 +0100
+++ b/.hgtags	Mon Feb 15 16:48:59 2010 +0100
@@ -8,3 +8,5 @@
 a340f645ae0ff25bf1a9049b27630ba8f8da0d4c cubicweb-blog-debian-version-1_6_1-1
 91859c819233793231c60f1122d06bead1ea5617 1.6.2
 1bcc5b5693c9f58ba0ffd3c9fc08b245e4dae5e1 oldstable
+4e7ee3b4a15b2d53d87424850c562f1da8412f8b cubicweb-blog-version-1.7.0
+9d3f2e2e5c49939339e091284af252f2d4f7ebf1 cubicweb-blog-debian-version-1.7.0-1
--- a/__pkginfo__.py	Mon Feb 15 16:48:35 2010 +0100
+++ b/__pkginfo__.py	Mon Feb 15 16:48:59 2010 +0100
@@ -4,46 +4,91 @@
 modname = 'blog'
 distname = "cubicweb-%s" % modname
 
-numversion = (1, 6, 2)
+numversion = (1, 7, 0)
 version = '.'.join(str(num) for num in numversion)
 
 license = 'LGPL'
-copyright = '''Copyright (c) 2003-2009 LOGILAB S.A. (Paris, FRANCE).
-http://www.logilab.fr/ -- mailto:contact@logilab.fr'''
 
 author = "Logilab"
 author_email = "contact@logilab.fr"
 web = 'http://www.cubicweb.org/project/%s' % distname
 
 short_desc = "blog component for the CubicWeb framework"
-long_desc = """This CubicWeb component provides blogging functionnalities.
+long_desc = """\
+Summary
+-------
+The `blog` cube provides blogging functionnalities. It creates two entity types,
+`Blog` and `BlogEntry`. There are related to each other by the relation
+`BlogEntry entry_of Blog`.
+
+Usage
+-----
+
+When a user submits a blog entry, it goes in a `draft` state until the blog
+entry is published by an application managers. The blog entry will not be
+visible until it reaches the `published` state.
+
+When a blog entry is submitted, an email notification is automatically sent
+to all the users belonging to the `managers` group of the application.
+
+Specific boxes provided by this cube:
+
+- `BlogEntryArchiveBox`, displays a box with the total number of blog entries
+submitted by month for the last twelve months.
 
-CubicWeb is a semantic web application framework, see http://www.cubicweb.org
+- `BlogEntryListBox`, displays a box with the latest five blog entries
+published in your application as well as link to subscribe to a RSS feed.
+
+- `BlogEntrySummary`, displays a box with the list of users who submitted
+blog entries and the total number of blog entries they submitted.
+
+This cube also provides some web services such as:
+
+- http://xx:xxxx/blogentries/YYYY to retrieve the blog entries submitted
+  during the year YYYY through a RSS feed
+
+- http://xx:xxxx/blogentries/YYYY/MM to retrieve the blog entries submitted
+  during the month MM of the year YYYY through a RSS feed
+
+- http://xx:xxxx/blog/[eid]/blogentries/YYYY to retrieve the blog entries
+  submitted in the blog of identifier [eid], during the year YYYY through
+  a RSS feed
+
+- http://xx:xxxx/blog/[eid]/blogentries/YYYY/MM to retrieve the blog entries
+  submitted in the blog of identifier [eid], during the month MM of the
+  year YYYY through a RSS feed
 """
 
+classifiers = [
+    'Environment :: Web Environment'
+    'Framework :: CubicWeb',
+    'Programming Language :: Python',
+    'Programming Language :: JavaScript',
+    ]
+
 __depends_cubes__ = {}
-__depends__ = {'cubicweb': '>= 3.2.0'}
+__depends__ = {'cubicweb': '>= 3.6.0'}
 __use__ = tuple(__depends_cubes__)
 
-from os import listdir
-from os.path import join
+# package ###
+
+from os import listdir as _listdir
+from os.path import join, isdir, exists
+from glob import glob
+
+THIS_CUBE_DIR = join('share', 'cubicweb', 'cubes', modname)
 
-CUBES_DIR = join('share', 'cubicweb', 'cubes')
-try:
-    data_files = [
-        [join(CUBES_DIR, 'blog'),
-         [fname for fname in listdir('.')
-          if fname.endswith('.py') and fname != 'setup.py']],
-        [join(CUBES_DIR, 'blog', 'data'),
-         [join('data', fname) for fname in listdir('data')]],
-        [join(CUBES_DIR, 'blog', 'i18n'),
-         [join('i18n', fname) for fname in listdir('i18n')]],
-        [join(CUBES_DIR, 'blog', 'migration'),
-         [join('migration', fname) for fname in listdir('migration')]],
-        ]
-except OSError:
-    # we are in an installed directory
-    pass
+def listdir(dirpath):
+    return [join(dirpath, fname) for fname in _listdir(dirpath)
+            if fname[0] != '.' and not fname.endswith('.pyc')
+            and not fname.endswith('~')
+            and not isdir(join(dirpath, fname))]
 
-
-cube_eid = 20300
+data_files = [
+    # common files
+    [THIS_CUBE_DIR, [fname for fname in glob('*.py') if fname != 'setup.py']],
+    ]
+# check for possible extended cube layout
+for dname in ('entities', 'views', 'sobjects', 'hooks', 'schema', 'data', 'i18n', 'migration'):
+    if isdir(dname):
+        data_files.append([join(THIS_CUBE_DIR, dname), listdir(dname)])
--- a/data/cubes.blog.css	Mon Feb 15 16:48:35 2010 +0100
+++ b/data/cubes.blog.css	Mon Feb 15 16:48:59 2010 +0100
@@ -9,7 +9,7 @@
   border-bottom: none;
 }
 
-div.post p.postmetadata {
+div.post div.postmetadata {
   background:transparent url(postdatabg.jpg) no-repeat scroll left bottom;
   font-size: 0.92em;
   padding-bottom: 12px;
@@ -43,3 +43,12 @@
 ul.simple li {
   background: transparent url(bullet_orange.png) no-repeat scroll 0 6px;
 }
+
+span.previousmonth {
+ float:left;
+}
+
+span.nextmonth {
+ float:right;
+}
+
--- a/debian/changelog	Mon Feb 15 16:48:35 2010 +0100
+++ b/debian/changelog	Mon Feb 15 16:48:59 2010 +0100
@@ -1,3 +1,9 @@
+cubicweb-blog (1.7.0-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Mon, 08 Feb 2010 21:32:37 +0100
+
 cubicweb-blog (1.6.2-1) unstable; urgency=low
 
   * new upstream release
--- a/debian/control	Mon Feb 15 16:48:35 2010 +0100
+++ b/debian/control	Mon Feb 15 16:48:59 2010 +0100
@@ -11,7 +11,7 @@
 Architecture: all
 Conflicts: erudi-blog, erudi-blog-server, erudi-blog-client, erudi-blog-comp
 Replaces: erudi-blog, erudi-blog-server, erudi-blog-client, erudi-blog-comp
-Depends: cubicweb-common (>= 3.2.0)
+Depends: cubicweb-common (>= 3.6.0)
 Description: blog component for the CubicWeb framework
  This CubicWeb component provides blogging functionnalities.
  .
--- a/entities.py	Mon Feb 15 16:48:35 2010 +0100
+++ b/entities.py	Mon Feb 15 16:48:59 2010 +0100
@@ -1,21 +1,49 @@
 """entity classes for Blog entities
 
 :organization: Logilab
-:copyright: 2003-2009 LOGILAB S.A. (Paris, FRANCE)
+:copyright: 2003-2010 LOGILAB S.A. (Paris, FRANCE)
 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
 :license: Lesser General Public License version 2 or above - http://www.gnu.org/
 """
 __docformat__ = "restructuredtext en"
 
-from cubicweb.utils import todate
+from logilab.common.date import todate
+
 from cubicweb.entities import AnyEntity, fetch_config
-from cubicweb.interfaces import ICalendarViews, ICalendarable, ISiocItem, ISiocContainer
+from cubicweb.interfaces import (ICalendarViews, ICalendarable,
+                                 ISiocItem, ISiocContainer, IPrevNext)
+
+
+class Blog(AnyEntity):
+    """customized class for Blog entities"""
+
+    __regid__ = 'Blog'
+    __implements__ = AnyEntity.__implements__ + (ISiocContainer,)
+
+    def rss_feed_url(self):
+        if self.rss_url:
+            return self.rss_url
+        rql = ('Any E ORDERBY D DESC '
+               'WHERE E is BlogEntry, E entry_of X, X eid %s, E creation_date D'
+               )
+        return self._cw.build_url(rql=rql % self.eid, vid='rss',
+                              vtitle=self.dc_title())
+
+    # isioc interface ##########################################################
+
+    def isioc_type(self):
+        return 'Weblog'
+
+    def isioc_items(self):
+        return self.reverse_entry_of
+
 
 class BlogEntry(AnyEntity):
     """customized class for BlogEntry entities"""
-    id = 'BlogEntry'
+    __regid__ = 'BlogEntry'
     fetch_attrs, fetch_order = fetch_config(['creation_date', 'title'], order='DESC')
-    __implements__ = (ICalendarViews, ICalendarable, ISiocItem)
+    __implements__ = AnyEntity.__implements__ + (
+        ICalendarViews, ICalendarable, ISiocItem, IPrevNext)
 
     def dc_title(self):
         return self.title
@@ -23,7 +51,15 @@
     def dc_description(self, format='text/plain'):
         return self.printable_value('content', format=format)
 
-    ## calendar interfaces ####################################################
+    def dc_date(self, date_format=None):# XXX default to ISO 8601 ?
+        """return latest modification date of this entity"""
+        return self._cw.format_date(self.creation_date, date_format=date_format)
+
+    def parent(self):
+        return self.entry_of and self.entry_of[0] or None
+
+    # calendar interfaces ######################################################
+
     @property
     def start(self):
         return self.creation_date
@@ -43,46 +79,46 @@
         return []
 
     def postinfo_description(self):
-        _ = self.req._
-        descr = u'%s %s' % (_('posted on'), self.format_date(self.creation_date))
+        _ = self._cw._
+        descr = u'%s %s' % (_('posted on'), self._cw.format_date(self.creation_date))
         return descr
 
-    # isioc interface
+    # isioc interface ##########################################################
+
     def isioc_content(self):
         return self.content
 
     def isioc_container(self):
-        rset = self.related('entry_of')
-        entity = rset.get_entity(0, 0)
-        return entity
+        return self.parent()
 
     def isioc_type(self):
         return 'BlogPost'
 
     def isioc_replies(self):
+        # XXX link to comments
         return []
 
     def isioc_topics(self):
+        # XXX link to tags, folders?
         return []
 
-
-class Blog(AnyEntity):
-    """customized class for Blog entities"""
-
-    id = 'Blog'
-    __implements__ = AnyEntity.__implements__ + (ISiocContainer,)
+    # IPrevNext interface #####################################################
+    def _sibling_entry(self, order, operator):
+        if self.entry_of:
+            rql = ('Any B ORDERBY B %s LIMIT 1 '
+                   'WHERE B is BlogEntry, B entry_of BL, BL eid %%(blog)s, '
+                   'B eid %s %%(eid)s')
+            rset = self._cw.execute(rql % (order, operator),
+                                    {'blog': self.entry_of[0].eid, 'eid': self.eid})
+        else:
+            rql = ('Any B ORDERBY B %s LIMIT 1 '
+                   'WHERE B is BlogEntry, B eid %s %%(eid)s')
+            rset = self._cw.execute(rql % (order, operator), {'eid': self.eid})
+        if rset:
+            return rset.get_entity(0,0)
 
-    def rss_feed_url(self):
-        if self.rss_url:
-            return self.rss_url
-        rql = 'Any E ORDERBY D DESC WHERE E is BlogEntry, E entry_of X, X eid %s, E creation_date D' % self.eid
-        return self.build_url(rql=rql, vid='rss', vtitle=self.dc_title())
+    def next_entity(self):
+        return self._sibling_entry('ASC', '>')
 
-    # isioc interface
-    def isioc_type(self):
-        return 'Weblog'
-
-    def isioc_items(self):
-        return self.reverse_entry_of
-
-
+    def previous_entity(self):
+        return self._sibling_entry('DESC', '<')
--- a/hooks.py	Mon Feb 15 16:48:35 2010 +0100
+++ b/hooks.py	Mon Feb 15 16:48:59 2010 +0100
@@ -7,6 +7,5 @@
     content_attr = 'content'
 
     def subject(self):
-        entity = self.entity(0)
-        return '[%s] %s' % (self.config.appid, entity.dc_title())
-                                     
+        entity = self.cw_rset.get_entity(0, 0)
+        return '[%s] %s' % (self._cw.vreg.config.appid, entity.dc_title())
--- a/i18n/en.po	Mon Feb 15 16:48:35 2010 +0100
+++ b/i18n/en.po	Mon Feb 15 16:48:59 2010 +0100
@@ -45,14 +45,8 @@
 msgid "add BlogEntry entry_of Blog object"
 msgstr "blog entry"
 
-msgid "add a Blog"
-msgstr "add a blog"
-
-msgid "add a BlogEntry"
-msgstr "add a blog entry"
-
 #, python-format
-msgid "blog entries created by %s %s"
+msgid "blog entries created by %s"
 msgstr ""
 
 msgid "blog's rss url (useful for when using external site such as feedburner)"
@@ -80,6 +74,12 @@
 msgid "boxes_blog_latest_box_description"
 msgstr "this box contains the latest posts"
 
+msgid "boxes_blog_summary_box"
+msgstr "Posts by author"
+
+msgid "boxes_blog_summary_box_description"
+msgstr "this box contains a list of authors with the number of posts"
+
 msgid "by"
 msgstr "by"
 
@@ -88,28 +88,64 @@
 msgid "content"
 msgstr ""
 
+msgctxt "BlogEntry"
+msgid "content"
+msgstr ""
+
 msgid "content_format"
 msgstr "format"
 
+msgctxt "BlogEntry"
+msgid "content_format"
+msgstr ""
+
 msgid "creating BlogEntry (BlogEntry entry_of Blog %(linkto)s)"
 msgstr "creating blogentry in blog %(linkto)s)"
 
+msgid "default BlogEntry workflow"
+msgstr ""
+
+msgctxt "Blog"
+msgid "description"
+msgstr ""
+
+msgctxt "Blog"
+msgid "description_format"
+msgstr ""
+
+msgid "draft"
+msgstr ""
+
 # subject and object forms for each relation type
 # (no object form for final relation types)
 msgid "entry_of"
 msgstr "blog entry of"
 
+msgctxt "BlogEntry"
+msgid "entry_of"
+msgstr ""
+
+msgctxt "Blog"
+msgid "entry_of_object"
+msgstr ""
+
 msgid "entry_of_object"
 msgstr "contains"
 
+msgid "next month"
+msgstr ""
+
 msgid "posted on"
 msgstr "posted on"
 
-msgid "remove this Blog"
-msgstr "remove this blog"
+msgid "previous month"
+msgstr ""
 
-msgid "remove this BlogEntry"
-msgstr "remove this blog entry"
+msgid "publish"
+msgstr ""
+
+msgid "published"
+msgstr ""
 
 msgid "rss icon"
 msgstr ""
@@ -117,6 +153,10 @@
 msgid "rss_url"
 msgstr "rss feed url"
 
+msgctxt "Blog"
+msgid "rss_url"
+msgstr ""
+
 msgid "see more"
 msgstr "see more"
 
@@ -126,5 +166,25 @@
 msgid "tags"
 msgstr "tags"
 
+msgctxt "Blog"
+msgid "title"
+msgstr ""
+
+msgctxt "BlogEntry"
+msgid "title"
+msgstr ""
+
+#~ msgid "add a Blog"
+#~ msgstr "add a blog"
+
+#~ msgid "add a BlogEntry"
+#~ msgstr "add a blog entry"
+
 #~ msgid "more"
 #~ msgstr "more"
+
+#~ msgid "remove this Blog"
+#~ msgstr "remove this blog"
+
+#~ msgid "remove this BlogEntry"
+#~ msgstr "remove this blog entry"
--- a/i18n/es.po	Mon Feb 15 16:48:35 2010 +0100
+++ b/i18n/es.po	Mon Feb 15 16:48:59 2010 +0100
@@ -44,15 +44,9 @@
 msgid "add BlogEntry entry_of Blog object"
 msgstr "entrada de blog"
 
-msgid "add a Blog"
-msgstr "Agregar un Blog"
-
-msgid "add a BlogEntry"
-msgstr "Agregar una Entrada de Blog"
-
 #, python-format
-msgid "blog entries created by %s %s"
-msgstr ""
+msgid "blog entries created by %s"
+msgstr "Entradas de blog creadas por %s"
 
 msgid "blog's rss url (useful for when using external site such as feedburner)"
 msgstr ""
@@ -75,6 +69,12 @@
 msgid "boxes_blog_latest_box_description"
 msgstr "espacio que contiene la lista de los últimos blogs"
 
+msgid "boxes_blog_summary_box"
+msgstr ""
+
+msgid "boxes_blog_summary_box_description"
+msgstr ""
+
 msgid "by"
 msgstr "por"
 
@@ -83,28 +83,64 @@
 msgid "content"
 msgstr ""
 
+msgctxt "BlogEntry"
+msgid "content"
+msgstr ""
+
+msgid "content_format"
+msgstr ""
+
+msgctxt "BlogEntry"
 msgid "content_format"
 msgstr ""
 
 msgid "creating BlogEntry (BlogEntry entry_of Blog %(linkto)s)"
 msgstr "creación de una entrada de blog %(linkto)s"
 
+msgid "default BlogEntry workflow"
+msgstr ""
+
+msgctxt "Blog"
+msgid "description"
+msgstr ""
+
+msgctxt "Blog"
+msgid "description_format"
+msgstr ""
+
+msgid "draft"
+msgstr ""
+
 # subject and object forms for each relation type
 # (no object form for final relation types)
 msgid "entry_of"
 msgstr "entrada de blog"
 
+msgctxt "BlogEntry"
+msgid "entry_of"
+msgstr ""
+
+msgctxt "Blog"
+msgid "entry_of_object"
+msgstr ""
+
 msgid "entry_of_object"
 msgstr "contiene las entradas"
 
+msgid "next month"
+msgstr ""
+
 msgid "posted on"
 msgstr "publicado el"
 
-msgid "remove this Blog"
-msgstr "eliminar este blog"
+msgid "previous month"
+msgstr ""
 
-msgid "remove this BlogEntry"
-msgstr "eliminar esta entrada de blog"
+msgid "publish"
+msgstr ""
+
+msgid "published"
+msgstr ""
 
 msgid "rss icon"
 msgstr "ícono rss"
@@ -112,6 +148,10 @@
 msgid "rss_url"
 msgstr ""
 
+msgctxt "Blog"
+msgid "rss_url"
+msgstr ""
+
 msgid "see more"
 msgstr "ver más"
 
@@ -121,8 +161,25 @@
 msgid "tags"
 msgstr "palabras clave"
 
-#~ msgid "blog entries created by %s"
-#~ msgstr "Entradas de blog creadas por %s"
+msgctxt "Blog"
+msgid "title"
+msgstr ""
+
+msgctxt "BlogEntry"
+msgid "title"
+msgstr ""
+
+#~ msgid "add a Blog"
+#~ msgstr "Agregar un Blog"
+
+#~ msgid "add a BlogEntry"
+#~ msgstr "Agregar una Entrada de Blog"
 
 #~ msgid "more"
 #~ msgstr "más"
+
+#~ msgid "remove this Blog"
+#~ msgstr "eliminar este blog"
+
+#~ msgid "remove this BlogEntry"
+#~ msgstr "eliminar esta entrada de blog"
--- a/i18n/fr.po	Mon Feb 15 16:48:35 2010 +0100
+++ b/i18n/fr.po	Mon Feb 15 16:48:59 2010 +0100
@@ -18,10 +18,10 @@
 msgstr "Blog"
 
 msgid "BlogEntry"
-msgstr "Entrée de blog"
+msgstr "Billet"
 
 msgid "BlogEntry_plural"
-msgstr "Entrées de blog"
+msgstr "Billets"
 
 msgid "Blog_plural"
 msgstr "Blogs"
@@ -33,27 +33,21 @@
 msgstr "Nouveau blog"
 
 msgid "New BlogEntry"
-msgstr "Nouvelle entrée de blog"
+msgstr "Nouveau  billet"
 
 msgid "This Blog"
 msgstr "Ce blog"
 
 msgid "This BlogEntry"
-msgstr "Cette entrée de blog"
+msgstr "Ce billet"
 
 # add related box generated message
 msgid "add BlogEntry entry_of Blog object"
-msgstr "entrée de blog"
-
-msgid "add a Blog"
-msgstr "ajouter un blog"
-
-msgid "add a BlogEntry"
-msgstr "ajouter une entrée de blog"
+msgstr "billet"
 
 #, python-format
-msgid "blog entries created by %s %s"
-msgstr "entrées de blog créées par %s %s"
+msgid "blog entries created by %s"
+msgstr ""
 
 msgid "blog's rss url (useful for when using external site such as feedburner)"
 msgstr ""
@@ -82,6 +76,12 @@
 msgid "boxes_blog_latest_box_description"
 msgstr "boîte contentant la liste des derniers blogs"
 
+msgid "boxes_blog_summary_box"
+msgstr "blogs par auteur"
+
+msgid "boxes_blog_summary_box_description"
+msgstr "boîte contenant la liste des auteurs et le nombre de blogs"
+
 msgid "by"
 msgstr "par"
 
@@ -90,28 +90,64 @@
 msgid "content"
 msgstr ""
 
+msgctxt "BlogEntry"
+msgid "content"
+msgstr ""
+
+msgid "content_format"
+msgstr ""
+
+msgctxt "BlogEntry"
 msgid "content_format"
 msgstr ""
 
 msgid "creating BlogEntry (BlogEntry entry_of Blog %(linkto)s)"
-msgstr "création d'une entrée du blog %(linkto)s"
+msgstr "création d'un billet dans le blog %(linkto)s"
+
+msgid "default BlogEntry workflow"
+msgstr ""
+
+msgctxt "Blog"
+msgid "description"
+msgstr ""
+
+msgctxt "Blog"
+msgid "description_format"
+msgstr ""
+
+msgid "draft"
+msgstr "brouillon"
 
 # subject and object forms for each relation type
 # (no object form for final relation types)
 msgid "entry_of"
-msgstr "entrée du blog"
+msgstr "dans le blog"
+
+msgctxt "BlogEntry"
+msgid "entry_of"
+msgstr ""
+
+msgctxt "Blog"
+msgid "entry_of_object"
+msgstr ""
 
 msgid "entry_of_object"
 msgstr "contient les entrées"
 
+msgid "next month"
+msgstr "mois suivant"
+
 msgid "posted on"
 msgstr "posté le"
 
-msgid "remove this Blog"
-msgstr "supprimer ce blog"
+msgid "previous month"
+msgstr "mois précédent"
 
-msgid "remove this BlogEntry"
-msgstr "supprimer cette entrée de blog"
+msgid "publish"
+msgstr "publier"
+
+msgid "published"
+msgstr "publié"
 
 msgid "rss icon"
 msgstr "icône rss"
@@ -119,6 +155,10 @@
 msgid "rss_url"
 msgstr "url du fil rss"
 
+msgctxt "Blog"
+msgid "rss_url"
+msgstr ""
+
 msgid "see more"
 msgstr "voir plus"
 
@@ -128,11 +168,25 @@
 msgid "tags"
 msgstr "étiquettes"
 
-#~ msgid "blog entries created by %s"
-#~ msgstr "entrées de blog créées par %s"
+msgctxt "Blog"
+msgid "title"
+msgstr ""
+
+msgctxt "BlogEntry"
+msgid "title"
+msgstr ""
+
+#~ msgid "add a Blog"
+#~ msgstr "ajouter un blog"
 
-#~ msgid "more"
-#~ msgstr "plus"
+#~ msgid "add a BlogEntry"
+#~ msgstr "ajouter un billet"
+
+#~ msgid "blog entries created by %s %s"
+#~ msgstr "billets créés par %s %s"
 
-#~ msgid "sioc"
-#~ msgstr "sioc"
+#~ msgid "remove this Blog"
+#~ msgstr "supprimer ce blog"
+
+#~ msgid "remove this BlogEntry"
+#~ msgstr "supprimer cette entrée de blog"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/migration/1.7.0_Any.py	Mon Feb 15 16:48:59 2010 +0100
@@ -0,0 +1,26 @@
+add_relation_definition('BlogEntry',  'in_state', 'State')
+add_relation_definition('TrInfo',  'wf_info_for', 'BlogEntry')
+add_relation_definition('BlogEntry',  'custom_workflow', 'Workflow')
+
+
+# add BlogEntry workflow
+if confirm('add blog entry workflow'):
+    bwf = add_workflow(_('default BlogEntry workflow'), 'BlogEntry')
+
+    draft = bwf.add_state(_('draft'), initial=True)
+    published = bwf.add_state(_('published'))
+
+    publish = bwf.add_transition(_('publish'), draft, published,
+                                 ('managers',))
+    checkpoint()
+
+# set state to published for already existing blog entries
+blogentries = rql('Any B WHERE B is BlogEntry')
+
+for eid, in blogentries:
+    session.unsafe_execute('SET B in_state S WHERE S name "published", '
+                           'S state_of WF, WF name "default BlogEntry workflow", '
+                           'B eid %(b)s',
+                           {'b': eid}, 'b')
+
+checkpoint()
--- a/migration/postcreate.py	Mon Feb 15 16:48:35 2010 +0100
+++ b/migration/postcreate.py	Mon Feb 15 16:48:59 2010 +0100
@@ -1,2 +1,11 @@
 # postcreate script. You could setup a workflow here for example
 
+# BlogEntry workflow
+bwf = add_workflow(_('default BlogEntry workflow'), 'BlogEntry')
+
+draft = bwf.add_state(_('draft'), initial=True)
+published = bwf.add_state(_('published'))
+
+publish = bwf.add_transition(_('publish'), draft, published,
+                             ('managers',))
+
--- a/schema.py	Mon Feb 15 16:48:35 2010 +0100
+++ b/schema.py	Mon Feb 15 16:48:59 2010 +0100
@@ -1,18 +1,19 @@
-from yams.buildobjs import EntityType, String, SubjectRelation
-
-from cubicweb.schema import format_constraint
+from yams.buildobjs import EntityType, String, RichString, SubjectRelation
+from cubicweb.schema import WorkflowableEntityType, ERQLExpression
 
 class Blog(EntityType):
     title = String(maxsize=50, required=True)
-    description_format = String(meta=True, internationalizable=True, maxsize=50,
-                                default='text/rest', constraints=[format_constraint])
-    description = String()
+    description = RichString()
     rss_url = String(maxsize=128, description=_('blog\'s rss url (useful for when using external site such as feedburner)'))
 
 
-class BlogEntry(EntityType):
+class BlogEntry(WorkflowableEntityType):
+    __permissions__ = {
+        'read': ('managers', 'users', ERQLExpression('X in_state S, S name "published"'),),
+        'add': ('managers', 'users'),
+        'update': ('managers', 'owners'),
+        'delete': ('managers', 'owners')
+        }
     title = String(required=True, fulltextindexed=True, maxsize=256)
-    content_format = String(meta=True, internationalizable=True, maxsize=50,
-                            default='text/rest', constraints=[format_constraint])
-    content = String(required=True, fulltextindexed=True)
+    content = RichString(required=True, fulltextindexed=True)
     entry_of = SubjectRelation('Blog', cardinality='**')
--- a/test/unittest_blog.py	Mon Feb 15 16:48:35 2010 +0100
+++ b/test/unittest_blog.py	Mon Feb 15 16:48:59 2010 +0100
@@ -2,33 +2,30 @@
 import re
 
 from logilab.common.testlib import unittest_main, mock_object
-from cubicweb.devtools.apptest import ControllerTC
-from cubicweb.devtools.testlib import WebTest
+from cubicweb.devtools.testlib import CubicWebTC, MAILBOX
 
 from email.Header import decode_header
 from cubicweb.sobjects.notification import RenderAndSendNotificationView
 from cubicweb.server.hookhelper import SendMailOp
 
 
-class BlogTests(ControllerTC):
+class BlogTestsCubicWebTC(CubicWebTC):
     """test blog specific behaviours"""
 
     def test_notifications(self):
-        cubicweb_blog = self.add_entity('Blog', title=u'cubicweb', description=u"cubicweb c'est beau")
-        blog_entry_1 = self.add_entity('BlogEntry', title=u"hop", content=u"cubicweb hop")
+        req = self.request()
+        cubicweb_blog = req.create_entity('Blog', title=u'cubicweb', description=u"cubicweb c'est beau")
+        blog_entry_1 = req.create_entity('BlogEntry', title=u"hop", content=u"cubicweb hop")
         self.execute('SET E entry_of B WHERE B eid %(beid)s, E eid %(eeid)s' % {'beid' :cubicweb_blog.eid, 'eeid' : blog_entry_1.eid})
-        blog_entry_2 = self.add_entity('BlogEntry', title=u"yes",  content=u"cubicweb yes")
+        blog_entry_2 = req.create_entity('BlogEntry', title=u"yes",  content=u"cubicweb yes")
         self.execute('SET E entry_of B WHERE B eid %(beid)s, E eid %(eeid)s' % {'beid' :cubicweb_blog.eid, 'eeid' : blog_entry_2.eid})
-        session = self.session()
-        for op in session.pending_operations:
-            if isinstance(op, RenderAndSendNotificationView):
-                op.precommit_event()
-        sendmailops = [op for op in session.pending_operations if isinstance(op, SendMailOp)]
-        self.assertEquals(len(sendmailops), 1)
-        sendmailop = sendmailops[0]
-        sent = [re.sub('#\d+', '#EID', decode_header(msg['subject'].encode())[0][0])
-                for msg, recipients in op.to_send]
-        self.assertListEquals(sent, ['[data] hop', '[data] yes'])
+        self.assertEquals(len(MAILBOX), 0)
+        self.commit()
+        self.assertEquals(len(MAILBOX), 2)
+        mail = MAILBOX[0]
+        self.assertEquals(mail.subject, '[data] hop')
+        mail = MAILBOX[1]
+        self.assertEquals(mail.subject, '[data] yes')
 
 
 if __name__ == '__main__':
--- a/views.py	Mon Feb 15 16:48:35 2010 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,246 +0,0 @@
-"""Specific views for blogs
-
-:organization: Logilab
-:copyright: 2003-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
-"""
-__docformat__ = "restructuredtext en"
-
-from calendar import monthrange
-from datetime import datetime
-
-from logilab.mtconverter import html_escape
-
-from cubicweb.view import EntityView, StartupView
-from cubicweb.utils import UStringIO
-from cubicweb.selectors import paginated_rset, sorted_rset, implements
-from cubicweb.web import uicfg
-from cubicweb.web.htmlwidgets import BoxLink, BoxWidget
-from cubicweb.web.views import baseviews, primary, boxes, calendar, navigation
-
-uicfg.primaryview_section.tag_attribute(('Blog', 'title'), 'hidden')
-uicfg.primaryview_section.tag_attribute(('Blog', 'rss_url'), 'hidden')
-uicfg.primaryview_section.tag_attribute(('BlogEntry', 'title'), 'hidden')
-uicfg.primaryview_section.tag_object_of(('*', 'entry_of', 'Blog'), 'hidden')
-uicfg.primaryview_section.tag_subject_of(('BlogEntry', 'entry_of', '*'),
-                                         'relations')
-
-uicfg.actionbox_appearsin_addmenu.tag_object_of(('*', 'entry_of', 'Blog'), True)
-
-class BlogPrimaryView(primary.PrimaryView):
-    __select__ = implements('Blog')
-
-    def render_entity_attributes(self, entity):
-        super(BlogPrimaryView, self).render_entity_attributes(entity)
-        self.w('<a class="right" href="%s">%s <img src="%s" alt="%s"/></a>' % (
-            html_escape(entity.rss_feed_url()), self.req._(u'subscribe'),
-            self.req.external_resource('RSS_LOGO_16'), self.req._('rss icon')))
-
-    def render_entity_relations(self, entity):
-        super(BlogPrimaryView, self).render_entity_relations(entity)
-        rset = entity.related('entry_of', 'object')
-        strio = UStringIO()
-        self.pagination(self.req, rset, strio.write, page_size=10)
-        self.wview('full_list', rset, 'null')
-        self.w(strio.getvalue())
-
-
-class BlogEntryPrimaryView(primary.PrimaryView):
-    __select__ = implements('BlogEntry')
-    show_attr_label = False
-
-    def render_entity_relations(self, entity):
-        rset = entity.related('entry_of', 'subject')
-        if rset:
-            _ = self.req._
-            self.w(_('blogged in '))
-            self.wview('secondary', rset, 'null')
-
-
-class BlogEntryArchiveView(StartupView):
-    """control the view of a blog archive"""
-    id = 'blog_archive'
-    countrql = 'Any COUNT(B) WHERE B is BlogEntry, B creation_date >=  %(firstday)s, B creation_date <= %(lastday)s'
-
-    def represent(self, items, year, month):
-        """represent a single month entry"""
-        firstday = datetime(year, month, 1)
-        lastday = datetime(year, month, monthrange(year, month)[1])
-        rql = ('Any B, BD ORDERBY BD DESC '
-               'WHERE B is BlogEntry, B creation_date BD, '
-               'B creation_date >=  "%s", B creation_date <= "%s"' %
-                (firstday.strftime('%Y-%m-%d'), lastday.strftime('%Y-%m-%d')))
-        args = {'firstday':firstday, 'lastday':lastday}
-        nmb_entries = self.req.execute(self.countrql, args)[0][0]
-        label = u'%s %s [%s]' % (self.req._(calendar.MONTHNAMES[month-1]), year,
-                                 nmb_entries)
-        vtitle = '%s %s' % (self.req._('BlogEntry_plural'), label)
-        url = html_escape(self.build_url('view', rql=rql, vtitle=vtitle,
-                                         vid='full_list'))
-        link = u'<a href="%s" title="">%s</a>' % (url, label)
-        items.append( u'<li class="">%s</li>\n' % link )
-
-    def call(self, maxentries=None, **kwargs):
-        """display a list of entities by calling their <item_vid> view
-        """
-        rset = self.req.execute('Any CD ORDERBY CD DESC WHERE B is BlogEntry, B creation_date CD')
-
-        blogmonths = []
-        items = []
-        for (blogdate,) in rset:
-            year, month = blogdate.year, blogdate.month
-            if (year, month) not in blogmonths:
-                blogmonths.append( (year, month) )
-        if maxentries is None:
-            displayed_months = blogmonths
-            needmore = False
-        else:
-            needmore = len(blogmonths) > maxentries
-            displayed_months = blogmonths[:maxentries]
-        for year, month in displayed_months:
-            self.represent(items, year, month)
-        if needmore:
-            url = self.build_url('view', vid='blog_archive')
-            link = u'<a href="%s" title="">[see more archives]</a>' % url
-            items.append( u'<li class="">%s</li>\n' % link )
-        self.w(u'<div class="boxFrame">')
-        if items:
-            self.w(u'<div class="boxContent">\n')
-            self.w(u'<ul class="boxListing">')
-            self.w(''.join(items))
-            self.w(u'</ul>\n</div>\n')
-        self.w(u'</div>')
-
-
-class BlogEntryArchiveBox(boxes.BoxTemplate):
-    """blog side box displaying a Blog Archive"""
-    id = 'blog_archives_box'
-    title = _('boxes_blog_archives_box')
-    order = 35
-
-    def call(self, **kwargs):
-        """display blogs archive"""
-        # XXX turn into a selector
-        count_blogentry = self.req.execute('Any COUNT(B) WHERE B is BlogEntry')
-        if count_blogentry[0][0] > 0:
-            box = BoxWidget(self.req._(self.title), id=self.id, islist=False)
-            box.append(boxes.BoxHtml(self.view('blog_archive', None, maxentries=12)))
-            box.render(self.w)
-
-
-class BlogEntryListBox(boxes.BoxTemplate):
-    """display a box with latest blogs and rss"""
-    id = 'blog_latest_box'
-    title = _('blog_latest_box')
-    visible = True # enabled by default
-    order = 34
-
-    def call(self, view=None, **kwargs):
-        # XXX turn into a selector
-        rset = self.req.execute('Any X,T,CD ORDERBY CD DESC LIMIT 5 '
-                                'WHERE X is BlogEntry, X title T, '
-                                'X creation_date CD')
-        if not rset:
-            return
-        box = BoxWidget(self.req._(self.title), self.id, islist=True)
-        # TODO - get the date between brakets after link
-        # empty string for title argument to deactivate auto-title
-        for i in xrange(rset.rowcount):
-            entity = rset.get_entity(i, 0)
-            box.append(BoxLink(entity.absolute_url(), html_escape(entity.dc_title())))
-        rqlst = rset.syntax_tree()
-        rqlst.set_limit(None)
-        rql = rqlst.as_string(kwargs=rset.args)
-        url = self.build_url('view', vid='full_list', rql=rql, page_size=10)
-        box.append(BoxLink(url,  u'[%s]' % self.req._(u'see more')))
-        rss_icon = self.req.external_resource('RSS_LOGO_16')
-        # FIXME - could use rss_url defined as a property if available
-        rss_label = u'%s <img src="%s" alt="%s"/>' % (
-            self.req._(u'subscribe'), rss_icon, self.req._('rss icon'))
-        rss_url = self.build_url('view', vid='rss', rql=rql)
-        box.append(BoxLink(rss_url, rss_label))
-        box.render(self.w)
-
-
-## list views #################################################################
-
-class BlogEntryListItemView(baseviews.ListItemView):
-    id = 'full_list'
-    __select__ = implements('BlogEntry')
-    redirect_vid = 'blog'
-
-    def call(self, **kwargs):
-        """display a list of entities by calling their <item_vid> view
-        """
-        entity = self.entity(0,0)
-        self.req.add_css('cubes.blog.css')
-        if not 'vtitle' in self.req.form:
-            self.w(u'<h1>%s</h1>' % display_name(self.req, 'BlogEntry', form='plural'))
-        super(BlogEntryListItemView, self).call(**kwargs)
-
-
-class BlogEntryBlogView(baseviews.ListItemView):
-    id = 'blog'
-    __select__ = implements('BlogEntry')
-
-    def cell_call(self, row, col):
-        entity = self.entity(row, col)
-        w = self.w
-        _ = self.req._
-        w(u'<div class="post">')
-        w(u'<h1>%s</h1>' % entity.view('incontext'))
-        w(u'%s ' % entity.postinfo_description())
-        creator = entity.creator
-        if creator:
-            vtitle = _('blog entries created by %s %s') % (creator.firstname,
-                                                           creator.surname)
-            rql = 'Any X ORDERBY D DESC WHERE X is BlogEntry, X created_by Y, '\
-                  'Y eid %s, X creation_date D' % creator.eid
-            url = self.build_url('view', vid="full_list", rql=rql, vtitle=vtitle,
-                                 page_size = 10)
-            w(u'%s <a href="%s">%s %s</a>' % (_('by'), html_escape(url),
-                                              creator.firstname,
-                                              creator.surname))
-
-        w(u'<div class="entry">')
-        body = entity.printable_value('content')
-        w(body)
-        w(u'</div>')
-        w(u'<div class="postmetadata">%s</div>' % entity.view('post-reldata'))
-        w(u'</div>')
-
-
-class BlogEntryPostMetaData(EntityView):
-    id = 'post-reldata'
-    __select__ = implements('BlogEntry')
-
-    def cell_call(self, row, col):
-        entity = self.entity(row, col)
-        _ = lambda ertype, form='': display_name(self.req, ertype, form)
-        reldata = []
-        w = reldata.append
-        if 'comments' in self.schema and 'BlogEntry' in self.schema.rschema('comments').objects():
-            count = self.req.execute('Any COUNT(C) WHERE C comments B, B eid %(x)s',
-                                     {'x': entity.eid}, 'x')[0][0]
-            if count:
-                url = html_escape(entity.absolute_url())
-                w(u'<a href="%s">%s %s</a>' % (url, count, _('Comment', 'plural')))
-            else:
-                w(u'%s %s' % (count, _('Comment')))
-        if 'tags' in self.schema and 'BlogEntry' in self.schema.rschema('tags').objects():
-            tag_rset = entity.related('tags', 'object')
-            if tag_rset:
-                w(u'%s %s' % (_('tags', 'object'), self.view('csv', tag_rset)))
-        rset = entity.related('entry_of', 'subject')
-        if rset:
-            w(u'%s %s' % (self.req._('blogged in '),
-                          self.view('secondary', rset, 'null')))
-        self.w(u' | '.join(reldata))
-
-
-class BlogNavigation(navigation.PageNavigation):
-    __select__ = paginated_rset() & sorted_rset() & implements('BlogEntry')
-
-    def index_display(self, start, stop):
-        return u'%s' % (int(start / self.page_size)+1)
-
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/views/primary.py	Mon Feb 15 16:48:59 2010 +0100
@@ -0,0 +1,60 @@
+"""Primary views for blogs
+
+:organization: Logilab
+:copyright: 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+"""
+__docformat__ = "restructuredtext en"
+
+
+from logilab.mtconverter import xml_escape
+
+from cubicweb.utils import UStringIO
+from cubicweb.selectors import implements
+from cubicweb.web import uicfg
+from cubicweb.web.views import primary
+
+uicfg.primaryview_section.tag_attribute(('Blog', 'title'), 'hidden')
+uicfg.primaryview_section.tag_attribute(('Blog', 'rss_url'), 'hidden')
+uicfg.primaryview_section.tag_attribute(('BlogEntry', 'title'), 'hidden')
+uicfg.primaryview_section.tag_object_of(('*', 'entry_of', 'Blog'), 'hidden')
+uicfg.primaryview_section.tag_subject_of(('BlogEntry', 'entry_of', '*'),
+                                         'relations')
+
+uicfg.actionbox_appearsin_addmenu.tag_object_of(('*', 'entry_of', 'Blog'), True)
+
+
+class BlogPrimaryView(primary.PrimaryView):
+    __select__ = implements('Blog')
+
+    def render_entity_attributes(self, entity):
+        super(BlogPrimaryView, self).render_entity_attributes(entity)
+        self.w('<a class="right" href="%s">%s <img src="%s" alt="%s"/></a>' % (
+            xml_escape(entity.rss_feed_url()), self._cw._(u'subscribe'),
+            self._cw.external_resource('RSS_LOGO_16'), self._cw._('rss icon')))
+
+    def render_entity_relations(self, entity):
+        super(BlogPrimaryView, self).render_entity_relations(entity)
+        rset = entity.related('entry_of', 'object')
+        if rset:
+            strio = UStringIO()
+            self.paginate(self._cw, w=strio.write, page_size=10, rset=rset)
+            self.wview('sameetypelist', rset)
+            self.w(strio.getvalue())
+
+    def render_entity_title(self, entity):
+        self.w(u'<h1>%s</h1>' % xml_escape(entity.dc_title()))
+
+
+class BlogEntryPrimaryView(primary.PrimaryView):
+    __select__ = implements('BlogEntry')
+    show_attr_label = False
+
+    def render_entity_title(self, entity):
+        self.w(u'<h1>%s</h1>' % xml_escape(entity.dc_title()))
+
+    def render_entity_relations(self, entity):
+        rset = entity.related('entry_of', 'subject')
+        if rset:
+            self.w(self._cw._('blogged in '))
+            self.wview('csv', rset, 'null')
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/views/secondary.py	Mon Feb 15 16:48:59 2010 +0100
@@ -0,0 +1,256 @@
+"""Secondary views for blogs
+
+:organization: Logilab
+:copyright: 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+"""
+__docformat__ = "restructuredtext en"
+
+from calendar import monthrange
+from datetime import datetime
+
+from logilab.mtconverter import xml_escape
+
+from cubicweb.schema import display_name
+from cubicweb.view import EntityView, StartupView
+from cubicweb.selectors import paginated_rset, sorted_rset, implements, \
+                               authenticated_user
+from cubicweb.web.htmlwidgets import BoxLink, BoxWidget
+from cubicweb.web.views import baseviews, boxes, calendar, navigation
+
+class BlogEntryArchiveView(StartupView):
+    """control the view of a blog archive"""
+    __regid__ = 'blog_archive'
+    countrql = 'Any COUNT(B) WHERE B is BlogEntry, B creation_date >=  %(firstday)s, B creation_date <= %(lastday)s'
+
+    def represent(self, items, year, month):
+        """represent a single month entry"""
+        firstday = datetime(year, month, 1)
+        lastday = datetime(year, month, monthrange(year, month)[1])
+        rql = ('Any B, BD ORDERBY BD DESC '
+               'WHERE B is BlogEntry, B creation_date BD, '
+               'B creation_date >=  "%s", B creation_date <= "%s"' %
+                (firstday.strftime('%Y-%m-%d'), lastday.strftime('%Y-%m-%d')))
+        args = {'firstday':firstday, 'lastday':lastday}
+        nmb_entries = self._cw.execute(self.countrql, args)[0][0]
+        label = u'%s %s [%s]' % (self._cw._(calendar.MONTHNAMES[month-1]), year,
+                                 nmb_entries)
+        vtitle = '%s %s' % (display_name(self._cw, 'BlogEntry', 'plural'), label)
+        url = xml_escape(self._cw.build_url('view', rql=rql, month=month, year=year, vtitle=vtitle))
+        link = u'<a href="%s" title="">%s</a>' % (url, label)
+        items.append( u'<li class="">%s</li>\n' % link )
+
+    def call(self, maxentries=None, **kwargs):
+        """display a list of entities by calling their <item_vid> view
+        """
+        rset = self._cw.execute('Any CD ORDERBY CD DESC WHERE B is BlogEntry, B creation_date CD')
+
+        blogmonths = []
+        items = []
+        for (blogdate,) in rset:
+            year, month = blogdate.year, blogdate.month
+            if (year, month) not in blogmonths:
+                blogmonths.append( (year, month) )
+        if maxentries is None:
+            displayed_months = blogmonths
+            needmore = False
+        else:
+            needmore = len(blogmonths) > maxentries
+            displayed_months = blogmonths[:maxentries]
+        for year, month in displayed_months:
+            self.represent(items, year, month)
+        if needmore:
+            url = self._cw.build_url('view', vid='blog_archive')
+            link = u'<a href="%s" title="">[see more archives]</a>' % url
+            items.append( u'<li class="">%s</li>\n' % link )
+        self.w(u'<div class="boxFrame">')
+        if items:
+            self.w(u'<div class="boxContent">\n')
+            self.w(u'<ul class="boxListing">')
+            self.w(''.join(items))
+            self.w(u'</ul>\n</div>\n')
+        self.w(u'</div>')
+
+
+class BlogEntryArchiveBox(boxes.BoxTemplate):
+    """blog side box displaying a Blog Archive"""
+    __regid__ = 'blog_archives_box'
+    title = _('boxes_blog_archives_box')
+    order = 35
+
+    def call(self, **kwargs):
+        """display blogs archive"""
+        # XXX turn into a selector
+        count_blogentry = self._cw.execute('Any COUNT(B) WHERE B is BlogEntry')
+        if count_blogentry[0][0] > 0:
+            box = BoxWidget(self._cw._(self.title), id=self.__regid__, islist=False)
+            box.append(boxes.BoxHtml(self._cw.view('blog_archive', None, maxentries=12)))
+            box.render(self.w)
+
+
+class BlogEntryListBox(boxes.BoxTemplate):
+    """display a box with latest blogs and rss"""
+    __regid__ = 'blog_latest_box'
+    title = _('blog_latest_box')
+    visible = True # enabled by default
+    order = 34
+
+    def call(self, view=None, **kwargs):
+        # XXX turn into a selector
+        rset = self._cw.execute('Any X,T,CD ORDERBY CD DESC LIMIT 5 '
+                                'WHERE X is BlogEntry, X title T, '
+                                'X creation_date CD')
+        if not rset:
+            return
+        box = BoxWidget(self._cw._(self.title), self.__regid__, islist=True)
+        # TODO - get the date between brakets after link
+        # empty string for title argument to deactivate auto-title
+        for i in xrange(rset.rowcount):
+            entity = rset.get_entity(i, 0)
+            box.append(BoxLink(entity.absolute_url(), xml_escape(entity.dc_title())))
+        rqlst = rset.syntax_tree()
+        rqlst.set_limit(None)
+        rql = rqlst.as_string(kwargs=rset.args)
+        url = self._cw.build_url('view', rql=rql, page_size=10)
+        box.append(BoxLink(url,  u'[%s]' % self._cw._(u'see more')))
+        rss_icon = self._cw.external_resource('RSS_LOGO_16')
+        # FIXME - could use rss_url defined as a property if available
+        rss_label = u'%s <img src="%s" alt="%s"/>' % (
+            self._cw._(u'subscribe'), rss_icon, self._cw._('rss icon'))
+        rss_url = self._cw.build_url('view', vid='rss', rql=rql)
+        box.append(BoxLink(rss_url, rss_label))
+        box.render(self.w)
+
+
+class BlogEntrySummary(boxes.BoxTemplate):
+    __regid__ = 'blog_summary_box'
+    title = _('boxes_blog_summary_box')
+    order = 36
+    __select__ = boxes.BoxTemplate.__select__
+
+    def call(self, view=None, **kwargs):
+        box = BoxWidget(self._cw._(self.title), self.__regid__, islist=True)
+        rql = 'Any U, COUNT(B) GROUPBY U WHERE U is CWUser, ' \
+              'B is BlogEntry, B created_by U'
+        rset = self._cw.execute(rql)
+        for user in rset:
+            euser = self._cw.entity_from_eid(user[0])
+            box.append(BoxLink(self._cw.build_url('blogentry/%s' % euser.login),
+                                              u'%s [%s]' % (euser.name(),
+                                                            user[1])))
+        box.render(self.w)
+
+## list views ##################################################################
+
+class BlogEntrySameETypeListView(baseviews.SameETypeListView):
+    __select__ = baseviews.SameETypeListView.__select__ & implements('BlogEntry')
+    countrql = 'Any COUNT(B) WHERE B is BlogEntry, B creation_date >=  %(firstday)s, B creation_date <= %(lastday)s'
+    item_vid = 'blog'
+
+    def call(self, **kwargs):
+        self._cw.add_css('cubes.blog.css')
+        super(BlogEntrySameETypeListView, self).call(**kwargs)
+        if 'year' in self._cw.form and 'month' in self._cw.form:
+            self.render_next_previous(int(self._cw.form['year']), int(self._cw.form['month']))
+
+    def render_next_previous(self, year, month):
+        if month == 12:
+            nextmonth = 1
+            year = year + 1
+        else:
+            nextmonth = month + 1
+        if month == 1:
+            previousmonth = 12
+            year = year - 1
+        else:
+            previousmonth = month -1
+
+        self.w(u'<div class="prevnext">')
+        self.w(u'<span class="previousmonth">%s</span>' \
+               % self.render_link(year, previousmonth,
+                                  xml_escape(u'<< ' + self._cw._(u'previous month'))))
+        self.w(u'<span class="nextmonth">%s</span>' \
+               % self.render_link(year, nextmonth,
+                                  xml_escape(self._cw._(u'next month') + u' >>')))
+        self.w(u'</div>')
+
+    def render_link(self, year, month, atitle):
+        firstday = datetime(year, month, 1)
+        lastday = datetime(year, month, monthrange(year, month)[1])
+        rql = ('Any B, BD ORDERBY BD DESC '
+               'WHERE B is BlogEntry, B creation_date BD, '
+               'B creation_date >=  "%s", B creation_date <= "%s"' %
+                (firstday.strftime('%Y-%m-%d'), lastday.strftime('%Y-%m-%d')))
+        args = {'firstday':firstday, 'lastday':lastday}
+        nmb_entries = self._cw.execute(self.countrql, args)[0][0]
+        label = u'%s %s [%s]' % (self._cw._(calendar.MONTHNAMES[month-1]), year,
+                                 nmb_entries)
+        vtitle = '%s %s' % (display_name(self._cw, 'BlogEntry', 'plural'), label)
+        url = xml_escape(self._cw.build_url('view', rql=rql, vtitle=vtitle,
+                                        month=month, year=year))
+        if self._cw.execute(rql):
+            return u'<a href="%s" title="">%s</a>' % (url, atitle)
+        return u''
+
+
+class BlogEntryBlogView(EntityView):
+    __regid__ = 'blog'
+    __select__ = implements('BlogEntry')
+
+    def cell_call(self, row, col):
+        entity = self.cw_rset.get_entity(row, col)
+        w = self.w
+        _ = self._cw._
+        w(u'<div class="post">')
+        w(u'<h1>%s</h1>' % entity.view('incontext'))
+        w(u'%s ' % entity.postinfo_description())
+        creator = entity.creator
+        if creator:
+            vtitle = _('blog entries created by %s') % creator.name()
+            rql = 'Any X ORDERBY D DESC WHERE X is BlogEntry, X created_by Y, '\
+                  'Y eid %s, X creation_date D' % creator.eid
+            url = self._cw.build_url('view', rql=rql, vtitle=vtitle, page_size=10)
+            w(u'%s <a title="%s" href="%s">%s</a>' % (
+                _('by'), xml_escape(vtitle), xml_escape(url), creator.name()))
+        w(u'<div class="entry">')
+        body = entity.printable_value('content')
+        w(body)
+        w(u'</div>')
+        w(u'<div class="postmetadata">%s</div>' % entity.view('post-reldata'))
+        w(u'</div>')
+
+
+class BlogEntryPostMetaData(EntityView):
+    __regid__ = 'post-reldata'
+    __select__ = implements('BlogEntry')
+
+    def cell_call(self, row, col):
+        entity = self.cw_rset.get_entity(row, col)
+        _ = lambda ertype, form='': display_name(self._cw, ertype, form)
+        reldata = []
+        w = reldata.append
+        if 'comments' in self._cw.vreg.schema and 'BlogEntry' in self._cw.vreg.schema.rschema('comments').objects():
+            count = self._cw.execute('Any COUNT(C) WHERE C comments B, B eid %(x)s',
+                                     {'x': entity.eid}, 'x')[0][0]
+            if count:
+                url = xml_escape(entity.absolute_url())
+                w(u'<a href="%s">%s %s</a>' % (url, count, _('Comment', 'plural')))
+            else:
+                w(u'%s %s' % (count, _('Comment')))
+        if 'tags' in self._cw.vreg.schema and 'BlogEntry' in self._cw.vreg.schema.rschema('tags').objects():
+            tag_rset = entity.related('tags', 'object')
+            if tag_rset:
+                w(u'%s %s' % (_('tags', 'object'), self._cw.view('csv', tag_rset)))
+        rset = entity.related('entry_of', 'subject')
+        if rset:
+            w(u'%s %s' % (self._cw._('blogged in '),
+                          self._cw.view('csv', rset, 'null')))
+        self.w(u' | '.join(reldata))
+
+
+class BlogNavigation(navigation.PageNavigation):
+    __select__ = paginated_rset() & sorted_rset() & implements('BlogEntry')
+
+    def index_display(self, start, stop):
+        return u'%s' % (int(start / self.page_size)+1)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/views/urlpublishing.py	Mon Feb 15 16:48:59 2010 +0100
@@ -0,0 +1,38 @@
+from calendar import monthrange
+from datetime import datetime
+
+from cubicweb.web.views.urlrewrite import SimpleReqRewriter, rgx
+
+class BlogReqRewriter(SimpleReqRewriter):
+    rules = [
+        (rgx('/blogentry/([a-z_]+)'),
+         dict(rql='Any X ORDERBY CD DESC WHERE X is BlogEntry, X creation_date CD, X created_by U, U login "%(user)s"' % {'user': r'\1'},
+              user = r'\1')),
+        (rgx('/blogentry/([a-z_]+)\.rss'),
+         dict(rql='Any X ORDERBY CD DESC LIMIT 20 WHERE X is BlogEntry, X creation_date CD, X created_by U, U login "%(user)s"' % {'user': r'\1'}, vid='rss')),
+        (rgx('/blogentries/([0-9]{4})'),
+         dict(rql='Any B ORDERBY CD DESC WHERE B is BlogEntry, B creation_date CD, '
+                  'B creation_date >= "%(param)s-01-01", '
+                  'B creation_date <= "%(param)s-12-31"' % {'param': r'\1'},
+              )),
+        (rgx('/blogentries/([0-9]{4})/([0-9]{2})'),
+         dict(rql='Any B, BD ORDERBY BD DESC '
+                  'WHERE B is BlogEntry, B creation_date BD, '
+                  'B creation_date >=  "%(year)s/%(month)s/01", B creation_date <= "%(year)s/%(month)s/30"' % {'year': r'\1', 'month': r'\2'},
+              )),
+        (rgx('/blog/([0-9]+)/blogentries'),
+         dict(rql='Any B ORDERBY CD DESC WHERE B is BlogEntry, B creation_date CD, '
+                  'B entry_of BL, BL eid %(eid)s' % {'eid': r'\1'},
+              )),
+        (rgx('/blog/([0-9]+)/blogentries/([0-9]{4})'),
+         dict(rql='Any B ORDERBY CD DESC WHERE B is BlogEntry, B creation_date CD, '
+                  'B creation_date >= "%(param)s-01-01", '
+                  'B creation_date <= "%(param)s-12-31", B entry_of BL, BL eid %(eid)s' % {'eid': r'\1', 'param': r'\2'},
+              )),
+        (rgx('/blog/([0-9]+)/blogentries/([0-9]{4})/([0-9]{2})'),
+         dict(rql='Any B, BD ORDERBY BD DESC '
+                  'WHERE B is BlogEntry, B creation_date BD, B entry_of BL, BL eid %(eid)s, '
+                  'B creation_date >=  "%(year)s/%(month)s/01", B creation_date <= "%(year)s/%(month)s/30"' % {'eid': r'\1', 'year': r'\2', 'month': r'\3'},
+              )),
+
+        ]