drop common subpackage
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Tue, 08 Dec 2009 10:58:56 +0100
changeset 4023 eae23c40627a
parent 4022 934e758a73ef
child 4024 6a14cff373c3
child 4034 9dfa0219f21e
drop common subpackage
common/__init__.py
common/mail.py
common/mixins.py
common/mttransforms.py
common/tags.py
common/test/data/bootstrap_cubes
common/test/unittest_mail.py
common/test/unittest_uilib.py
common/uilib.py
cwconfig.py
entities/wfobjs.py
entity.py
goa/__init__.py
goa/appobjects/components.py
goa/appobjects/dbmgmt.py
goa/appobjects/sessions.py
goa/goactl.py
goa/overrides/mttransforms.py
goa/test/data/views.py
goa/test/unittest_editcontroller.py
goa/tools/laxctl.py
hooks/integrity.py
interfaces.py
mail.py
mixins.py
mttransforms.py
rset.py
server/sqlutils.py
sobjects/notification.py
sobjects/supervising.py
sobjects/test/unittest_notification.py
tags.py
test/unittest_entity.py
test/unittest_mail.py
test/unittest_migration.py
test/unittest_rset.py
test/unittest_uilib.py
test/unittest_utils.py
uilib.py
web/form.py
web/formfields.py
web/formwidgets.py
web/htmlwidgets.py
web/request.py
web/test/unittest_views_basecontrollers.py
web/views/basecomponents.py
web/views/basecontrollers.py
web/views/baseviews.py
web/views/csvexport.py
web/views/editforms.py
web/views/editviews.py
web/views/emailaddress.py
web/views/embedding.py
web/views/formrenderers.py
web/views/ibreadcrumbs.py
web/views/idownloadable.py
web/views/management.py
web/views/navigation.py
web/views/schema.py
web/views/tableview.py
web/views/tabs.py
web/views/urlpublishing.py
web/views/wdoc.py
web/views/xmlrss.py
--- a/common/__init__.py	Tue Dec 08 10:40:20 2009 +0100
+++ b/common/__init__.py	Tue Dec 08 10:58:56 2009 +0100
@@ -7,47 +7,3 @@
 :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
 """
 
-from logilab.common.adbh import FunctionDescr
-
-from cubicweb._exceptions import * # bw compat
-
-from rql.utils import register_function, iter_funcnode_variables
-
-class COMMA_JOIN(FunctionDescr):
-    supported_backends = ('postgres', 'sqlite',)
-    rtype = 'String'
-
-    @classmethod
-    def st_description(cls, funcnode, mainindex, tr):
-        return ', '.join(sorted(term.get_description(mainindex, tr)
-                                for term in iter_funcnode_variables(funcnode)))
-
-register_function(COMMA_JOIN)  # XXX do not expose?
-
-
-class CONCAT_STRINGS(COMMA_JOIN):
-    aggregat = True
-
-register_function(CONCAT_STRINGS) # XXX bw compat
-
-class GROUP_CONCAT(CONCAT_STRINGS):
-    supported_backends = ('mysql', 'postgres', 'sqlite',)
-
-register_function(GROUP_CONCAT)
-
-
-class LIMIT_SIZE(FunctionDescr):
-    supported_backends = ('postgres', 'sqlite',)
-    rtype = 'String'
-
-    @classmethod
-    def st_description(cls, funcnode, mainindex, tr):
-        return funcnode.children[0].get_description(mainindex, tr)
-
-register_function(LIMIT_SIZE)
-
-
-class TEXT_LIMIT_SIZE(LIMIT_SIZE):
-    supported_backends = ('mysql', 'postgres', 'sqlite',)
-
-register_function(TEXT_LIMIT_SIZE)
--- a/common/mail.py	Tue Dec 08 10:40:20 2009 +0100
+++ b/common/mail.py	Tue Dec 08 10:58:56 2009 +0100
@@ -1,272 +1,5 @@
-"""Common utilies to format / semd emails.
-
-:organization: Logilab
-:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
-:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
-:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
-"""
-__docformat__ = "restructuredtext en"
-
-from base64 import b64encode, b64decode
-from itertools import repeat
-from time import time
-from email.MIMEMultipart import MIMEMultipart
-from email.MIMEText import MIMEText
-from email.MIMEImage import MIMEImage
-from email.Header import Header
-try:
-    from socket import gethostname
-except ImportError:
-    def gethostname(): # gae
-        return 'XXX'
-
-from cubicweb.view import EntityView
-from cubicweb.entity import Entity
-
-def header(ustring):
-    return Header(ustring.encode('UTF-8'), 'UTF-8')
-
-def addrheader(uaddr, uname=None):
-    # even if an email address should be ascii, encode it using utf8 since
-    # automatic tests may generate non ascii email address
-    addr = uaddr.encode('UTF-8')
-    if uname:
-        return '%s <%s>' % (header(uname).encode(), addr)
-    return addr
-
-
-def construct_message_id(appid, eid, withtimestamp=True):
-    if withtimestamp:
-        addrpart = 'eid=%s&timestamp=%.10f' % (eid, time())
-    else:
-        addrpart = 'eid=%s' % eid
-    # we don't want any equal sign nor trailing newlines
-    leftpart = b64encode(addrpart, '.-').rstrip().rstrip('=')
-    return '<%s@%s.%s>' % (leftpart, appid, gethostname())
-
-
-def parse_message_id(msgid, appid):
-    if msgid[0] == '<':
-        msgid = msgid[1:]
-    if msgid[-1] == '>':
-        msgid = msgid[:-1]
-    try:
-        values, qualif = msgid.split('@')
-        padding = len(values) % 4
-        values = b64decode(str(values + '='*padding), '.-')
-        values = dict(v.split('=') for v in values.split('&'))
-        fromappid, host = qualif.split('.', 1)
-    except:
-        return None
-    if appid != fromappid or host != gethostname():
-        return None
-    return values
-
-
-def format_mail(uinfo, to_addrs, content, subject="",
-                cc_addrs=(), msgid=None, references=(), config=None):
-    """Sends an Email to 'e_addr' with content 'content', and subject 'subject'
-
-    to_addrs and cc_addrs are expected to be a list of email address without
-    name
-    """
-    assert type(content) is unicode, repr(content)
-    msg = MIMEText(content.encode('UTF-8'), 'plain', 'UTF-8')
-    # safety: keep only the first newline
-    subject = subject.splitlines()[0]
-    msg['Subject'] = header(subject)
-    if uinfo.get('email'):
-        email = uinfo['email']
-    elif config and config['sender-addr']:
-        email = unicode(config['sender-addr'])
-    else:
-        email = u''
-    if uinfo.get('name'):
-        name = uinfo['name']
-    elif config and config['sender-addr']:
-        name = unicode(config['sender-name'])
-    else:
-        name = u''
-    msg['From'] = addrheader(email, name)
-    if config and config['sender-addr'] and config['sender-addr'] != email:
-        appaddr = addrheader(config['sender-addr'], config['sender-name'])
-        msg['Reply-to'] = '%s, %s' % (msg['From'], appaddr)
-    elif email:
-        msg['Reply-to'] = msg['From']
-    if config is not None:
-        msg['X-CW'] = config.appid
-    unique_addrs = lambda addrs: sorted(set(addr for addr in addrs if addr is not None))
-    msg['To'] = ', '.join(addrheader(addr) for addr in unique_addrs(to_addrs))
-    if cc_addrs:
-        msg['Cc'] = ', '.join(addrheader(addr) for addr in unique_addrs(cc_addrs))
-    if msgid:
-        msg['Message-id'] = msgid
-    if references:
-        msg['References'] = ', '.join(references)
-    return msg
-
-
-class HtmlEmail(MIMEMultipart):
-
-    def __init__(self, subject, textcontent, htmlcontent,
-                 sendermail=None, sendername=None, recipients=None, ccrecipients=None):
-        MIMEMultipart.__init__(self, 'related')
-        self['Subject'] = header(subject)
-        self.preamble = 'This is a multi-part message in MIME format.'
-        # Attach alternative text message
-        alternative = MIMEMultipart('alternative')
-        self.attach(alternative)
-        msgtext = MIMEText(textcontent.encode('UTF-8'), 'plain', 'UTF-8')
-        alternative.attach(msgtext)
-        # Attach html message
-        msghtml = MIMEText(htmlcontent.encode('UTF-8'), 'html', 'UTF-8')
-        alternative.attach(msghtml)
-        if sendermail or sendername:
-            self['From'] = addrheader(sendermail, sendername)
-        if recipients:
-            self['To'] = ', '.join(addrheader(addr) for addr in recipients if addr is not None)
-        if ccrecipients:
-            self['Cc'] = ', '.join(addrheader(addr) for addr in ccrecipients if addr is not None)
-
-    def attach_image(self, data, htmlId):
-        image = MIMEImage(data)
-        image.add_header('Content-ID', '<%s>' % htmlId)
-        self.attach(image)
-
-
-class NotificationView(EntityView):
-    """abstract view implementing the "email" API (eg to simplify sending
-    notification)
-    """
-    # XXX refactor this class to work with len(rset) > 1
-
-    msgid_timestamp = True
-
-    # this is usually the method to call
-    def render_and_send(self, **kwargs):
-        """generate and send an email message for this view"""
-        delayed = kwargs.pop('delay_to_commit', None)
-        for recipients, msg in self.render_emails(**kwargs):
-            if delayed is None:
-                self.send(recipients, msg)
-            elif delayed:
-                self.send_on_commit(recipients, msg)
-            else:
-                self.send_now(recipients, msg)
-
-    def cell_call(self, row, col=0, **kwargs):
-        self.w(self._cw._(self.content) % self.context(**kwargs))
-
-    def render_emails(self, **kwargs):
-        """generate and send emails for this view (one per recipient)"""
-        self._kwargs = kwargs
-        recipients = self.recipients()
-        if not recipients:
-            self.info('skipping %s notification, no recipients', self.__regid__)
-            return
-        if self.cw_rset is not None:
-            entity = self.cw_rset.get_entity(self.cw_row or 0, self.cw_col or 0)
-            # if the view is using timestamp in message ids, no way to reference
-            # previous email
-            if not self.msgid_timestamp:
-                refs = [self.construct_message_id(eid)
-                        for eid in entity.notification_references(self)]
-            else:
-                refs = ()
-            msgid = self.construct_message_id(entity.eid)
-        else:
-            refs = ()
-            msgid = None
-        req = self._cw
-        self.user_data = req.user_data()
-        origlang = req.lang
-        for something in recipients:
-            if isinstance(something, Entity):
-                # hi-jack self._cw to get a session for the returned user
-                self._cw = self._cw.hijack_user(something)
-                emailaddr = something.get_email()
-            else:
-                emailaddr, lang = something
-                self._cw.set_language(lang)
-            # since the same view (eg self) may be called multiple time and we
-            # need a fresh stream at each iteration, reset it explicitly
-            self.w = None
-            # XXX call render before subject to set .row/.col attributes on the
-            #     view
-            try:
-                content = self.render(row=0, col=0, **kwargs)
-                subject = self.subject()
-            except SkipEmail:
-                continue
-            except Exception, ex:
-                # shouldn't make the whole transaction fail because of rendering
-                # error (unauthorized or such)
-                self.exception(str(ex))
-                continue
-            msg = format_mail(self.user_data, [emailaddr], content, subject,
-                              config=self._cw.vreg.config, msgid=msgid, references=refs)
-            yield [emailaddr], msg
-        # restore language
-        req.set_language(origlang)
-
-    # recipients / email sending ###############################################
-
-    def recipients(self):
-        """return a list of either 2-uple (email, language) or user entity to
-        who this email should be sent
-        """
-        # use super_session when available, we don't want to consider security
-        # when selecting recipients_finder
-        try:
-            req = self._cw.super_session
-        except AttributeError:
-            req = self._cw
-        finder = self._cw.vreg['components'].select('recipients_finder', req,
-                                                    rset=self.cw_rset,
-                                                    row=self.cw_row or 0,
-                                                    col=self.cw_col or 0)
-        return finder.recipients()
-
-    def send_now(self, recipients, msg):
-        self._cw.vreg.config.sendmails([(msg, recipients)])
-
-    def send_on_commit(self, recipients, msg):
-        raise NotImplementedError
-
-    send = send_now
-
-    # email generation helpers #################################################
-
-    def construct_message_id(self, eid):
-        return construct_message_id(self._cw.vreg.config.appid, eid, self.msgid_timestamp)
-
-    def format_field(self, attr, value):
-        return ':%(attr)s: %(value)s' % {'attr': attr, 'value': value}
-
-    def format_section(self, attr, value):
-        return '%(attr)s\n%(ul)s\n%(value)s\n' % {
-            'attr': attr, 'ul': '-'*len(attr), 'value': value}
-
-    def subject(self):
-        entity = self.cw_rset.get_entity(self.cw_row or 0, self.cw_col or 0)
-        subject = self._cw._(self.message)
-        etype = entity.dc_type()
-        eid = entity.eid
-        login = self.user_data['login']
-        return self._cw._('%(subject)s %(etype)s #%(eid)s (%(login)s)') % locals()
-
-    def context(self, **kwargs):
-        entity = self.cw_rset.get_entity(self.cw_row or 0, self.cw_col or 0)
-        for key, val in kwargs.iteritems():
-            if val and isinstance(val, unicode) and val.strip():
-               kwargs[key] = self._cw._(val)
-        kwargs.update({'user': self.user_data['login'],
-                       'eid': entity.eid,
-                       'etype': entity.dc_type(),
-                       'url': entity.absolute_url(),
-                       'title': entity.dc_long_title(),})
-        return kwargs
-
-
-class SkipEmail(Exception):
-    """raise this if you decide to skip an email during its generation"""
+"""pre 3.6 bw compat"""
+# pylint: disable-msg=W0614,W0401
+from warnings import warn
+warn('moved to cubicweb.mail', DeprecationWarning, stacklevel=2)
+from cubicweb.mail import *
--- a/common/mixins.py	Tue Dec 08 10:40:20 2009 +0100
+++ b/common/mixins.py	Tue Dec 08 10:58:56 2009 +0100
@@ -1,315 +1,5 @@
-"""mixins of entity/views organized somewhat in a graph or tree structure
-
-
-:organization: Logilab
-:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
-:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
-:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
-"""
-__docformat__ = "restructuredtext en"
-
-from logilab.common.deprecation import deprecated
-from logilab.common.decorators import cached
-
-from cubicweb import typed_eid
-from cubicweb.selectors import implements
-from cubicweb.interfaces import IEmailable, ITree
-
-
-class TreeMixIn(object):
-    """base tree-mixin providing the tree interface
-
-    This mixin has to be inherited explicitly and configured using the
-    tree_attribute, parent_target and children_target class attribute to
-    benefit from this default implementation
-    """
-    tree_attribute = None
-    # XXX misnamed
-    parent_target = 'subject'
-    children_target = 'object'
-
-    def different_type_children(self, entities=True):
-        """return children entities of different type as this entity.
-
-        according to the `entities` parameter, return entity objects or the
-        equivalent result set
-        """
-        res = self.related(self.tree_attribute, self.children_target,
-                           entities=entities)
-        if entities:
-            return [e for e in res if e.e_schema != self.e_schema]
-        return res.filtered_rset(lambda x: x.e_schema != self.e_schema, self.cw_col)
-
-    def same_type_children(self, entities=True):
-        """return children entities of the same type as this entity.
-
-        according to the `entities` parameter, return entity objects or the
-        equivalent result set
-        """
-        res = self.related(self.tree_attribute, self.children_target,
-                           entities=entities)
-        if entities:
-            return [e for e in res if e.e_schema == self.e_schema]
-        return res.filtered_rset(lambda x: x.e_schema == self.e_schema, self.cw_col)
-
-    def iterchildren(self, _done=None):
-        if _done is None:
-            _done = set()
-        for child in self.children():
-            if child.eid in _done:
-                self.error('loop in %s tree', self.__regid__.lower())
-                continue
-            yield child
-            _done.add(child.eid)
-
-    def prefixiter(self, _done=None):
-        if _done is None:
-            _done = set()
-        if self.eid in _done:
-            return
-        yield self
-        _done.add(self.eid)
-        for child in self.iterchildren(_done):
-            try:
-                for entity in child.prefixiter(_done):
-                    yield entity
-            except AttributeError:
-                pass
-
-    @cached
-    def path(self):
-        """returns the list of eids from the root object to this object"""
-        path = []
-        parent = self
-        while parent:
-            if parent.eid in path:
-                self.error('loop in %s tree', self.__regid__.lower())
-                break
-            path.append(parent.eid)
-            try:
-                # check we are not leaving the tree
-                if (parent.tree_attribute != self.tree_attribute or
-                    parent.parent_target != self.parent_target):
-                    break
-                parent = parent.parent()
-            except AttributeError:
-                break
-
-        path.reverse()
-        return path
-
-    def iterparents(self):
-        def _uptoroot(self):
-            curr = self
-            while True:
-                curr = curr.parent()
-                if curr is None:
-                    break
-                yield curr
-        return _uptoroot(self)
-
-    def notification_references(self, view):
-        """used to control References field of email send on notification
-        for this entity. `view` is the notification view.
-
-        Should return a list of eids which can be used to generate message ids
-        of previously sent email
-        """
-        return self.path()[:-1]
-
-
-    ## ITree interface ########################################################
-    def parent(self):
-        """return the parent entity if any, else None (e.g. if we are on the
-        root
-        """
-        try:
-            return self.related(self.tree_attribute, self.parent_target,
-                                entities=True)[0]
-        except (KeyError, IndexError):
-            return None
-
-    def children(self, entities=True, sametype=False):
-        """return children entities
-
-        according to the `entities` parameter, return entity objects or the
-        equivalent result set
-        """
-        if sametype:
-            return self.same_type_children(entities)
-        else:
-            return self.related(self.tree_attribute, self.children_target,
-                                entities=entities)
-
-    def children_rql(self):
-        return self.related_rql(self.tree_attribute, self.children_target)
-
-    def is_leaf(self):
-        return len(self.children()) == 0
-
-    def is_root(self):
-        return self.parent() is None
-
-    def root(self):
-        """return the root object"""
-        return self._cw.entity_from_eid(self.path()[0])
-
-
-class EmailableMixIn(object):
-    """base mixin providing the default get_email() method used by
-    the massmailing view
-
-    NOTE: The default implementation is based on the
-    primary_email / use_email scheme
-    """
-    __implements__ = (IEmailable,)
-
-    def get_email(self):
-        if getattr(self, 'primary_email', None):
-            return self.primary_email[0].address
-        if getattr(self, 'use_email', None):
-            return self.use_email[0].address
-        return None
-
-    @classmethod
-    def allowed_massmail_keys(cls):
-        """returns a set of allowed email substitution keys
-
-        The default is to return the entity's attribute list but an
-        entity class might override this method to allow extra keys.
-        For instance, the Person class might want to return a `companyname`
-        key.
-        """
-        return set(rschema.type
-                   for rschema, attrtype in cls.e_schema.attribute_definitions()
-                   if attrtype.type not in ('Password', 'Bytes'))
-
-    def as_email_context(self):
-        """returns the dictionary as used by the sendmail controller to
-        build email bodies.
-
-        NOTE: the dictionary keys should match the list returned by the
-        `allowed_massmail_keys` method.
-        """
-        return dict( (attr, getattr(self, attr)) for attr in self.allowed_massmail_keys() )
-
-
-"""pluggable mixins system: plug classes registered in MI_REL_TRIGGERS on entity
-classes which have the relation described by the dict's key.
-
-NOTE: pluggable mixins can't override any method of the 'explicit' user classes tree
-(eg without plugged classes). This includes bases Entity and AnyEntity classes.
-"""
-MI_REL_TRIGGERS = {
-    ('primary_email',   'subject'): EmailableMixIn,
-    ('use_email',   'subject'): EmailableMixIn,
-    }
-
-
-
-def _done_init(done, view, row, col):
-    """handle an infinite recursion safety belt"""
-    if done is None:
-        done = set()
-    entity = view.cw_rset.get_entity(row, col)
-    if entity.eid in done:
-        msg = entity._cw._('loop in %(rel)s relation (%(eid)s)') % {
-            'rel': entity.tree_attribute,
-            'eid': entity.eid
-            }
-        return None, msg
-    done.add(entity.eid)
-    return done, entity
-
-
-class TreeViewMixIn(object):
-    """a recursive tree view"""
-    __regid__ = 'tree'
-    item_vid = 'treeitem'
-    __select__ = implements(ITree)
-
-    def call(self, done=None, **kwargs):
-        if done is None:
-            done = set()
-        super(TreeViewMixIn, self).call(done=done, **kwargs)
-
-    def cell_call(self, row, col=0, vid=None, done=None, **kwargs):
-        done, entity = _done_init(done, self, row, col)
-        if done is None:
-            # entity is actually an error message
-            self.w(u'<li class="badcontent">%s</li>' % entity)
-            return
-        self.open_item(entity)
-        entity.view(vid or self.item_vid, w=self.w, **kwargs)
-        relatedrset = entity.children(entities=False)
-        self.wview(self.__regid__, relatedrset, 'null', done=done, **kwargs)
-        self.close_item(entity)
-
-    def open_item(self, entity):
-        self.w(u'<li class="%s">\n' % entity.__regid__.lower())
-    def close_item(self, entity):
-        self.w(u'</li>\n')
-
-
-class TreePathMixIn(object):
-    """a recursive path view"""
-    __regid__ = 'path'
-    item_vid = 'oneline'
-    separator = u'&#160;&gt;&#160;'
-
-    def call(self, **kwargs):
-        self.w(u'<div class="pathbar">')
-        super(TreePathMixIn, self).call(**kwargs)
-        self.w(u'</div>')
-
-    def cell_call(self, row, col=0, vid=None, done=None, **kwargs):
-        done, entity = _done_init(done, self, row, col)
-        if done is None:
-            # entity is actually an error message
-            self.w(u'<span class="badcontent">%s</span>' % entity)
-            return
-        parent = entity.parent()
-        if parent:
-            parent.view(self.__regid__, w=self.w, done=done)
-            self.w(self.separator)
-        entity.view(vid or self.item_vid, w=self.w)
-
-
-class ProgressMixIn(object):
-    """provide default implementations for IProgress interface methods"""
-    # This is an adapter isn't it ?
-
-    @property
-    def cost(self):
-        return self.progress_info()['estimated']
-
-    @property
-    def revised_cost(self):
-        return self.progress_info().get('estimatedcorrected', self.cost)
-
-    @property
-    def done(self):
-        return self.progress_info()['done']
-
-    @property
-    def todo(self):
-        return self.progress_info()['todo']
-
-    @cached
-    def progress_info(self):
-        raise NotImplementedError()
-
-    def finished(self):
-        return not self.in_progress()
-
-    def in_progress(self):
-        raise NotImplementedError()
-
-    def progress(self):
-        try:
-            return 100. * self.done / self.revised_cost
-        except ZeroDivisionError:
-            # total cost is 0 : if everything was estimated, task is completed
-            if self.progress_info().get('notestimated'):
-                return 0.
-            return 100
+"""pre 3.6 bw compat"""
+# pylint: disable-msg=W0614,W0401
+from warnings import warn
+warn('moved to cubicweb.mixins', DeprecationWarning, stacklevel=2)
+from cubicweb.mixins import *
--- a/common/mttransforms.py	Tue Dec 08 10:40:20 2009 +0100
+++ b/common/mttransforms.py	Tue Dec 08 10:58:56 2009 +0100
@@ -1,102 +1,5 @@
-"""mime type transformation engine for cubicweb, based on mtconverter
-
-:organization: Logilab
-:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
-:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
-:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
-"""
-__docformat__ = "restructuredtext en"
-
-from logilab import mtconverter
-
-from logilab.mtconverter.engine import TransformEngine
-from logilab.mtconverter.transform import Transform
-from logilab.mtconverter import (register_base_transforms,
-                                 register_pil_transforms,
-                                 register_pygments_transforms)
-
-from cubicweb.utils import UStringIO
-from cubicweb.common.uilib import rest_publish, html_publish
-
-HTML_MIMETYPES = ('text/html', 'text/xhtml', 'application/xhtml+xml')
-
-# CubicWeb specific transformations
-
-class rest_to_html(Transform):
-    inputs = ('text/rest', 'text/x-rst')
-    output = 'text/html'
-    def _convert(self, trdata):
-        return rest_publish(trdata.appobject, trdata.decode())
-
-class html_to_html(Transform):
-    inputs = HTML_MIMETYPES
-    output = 'text/html'
-    def _convert(self, trdata):
-        return html_publish(trdata.appobject, trdata.data)
-
-
-# Instantiate and configure the transformation engine
-
-mtconverter.UNICODE_POLICY = 'replace'
-
-ENGINE = TransformEngine()
-ENGINE.add_transform(rest_to_html())
-ENGINE.add_transform(html_to_html())
-
-try:
-    from cubicweb.ext.tal import CubicWebContext, compile_template
-except ImportError:
-    HAS_TAL = False
-    from cubicweb import schema
-    schema.NEED_PERM_FORMATS.remove('text/cubicweb-page-template')
-
-else:
-    HAS_TAL = True
-
-    class ept_to_html(Transform):
-        inputs = ('text/cubicweb-page-template',)
-        output = 'text/html'
-        output_encoding = 'utf-8'
-        def _convert(self, trdata):
-            context = CubicWebContext()
-            appobject = trdata.appobject
-            context.update({'self': appobject, 'rset': appobject.cw_rset,
-                            'req': appobject._cw,
-                            '_' : appobject._cw._,
-                            'user': appobject._cw.user})
-            output = UStringIO()
-            template = compile_template(trdata.encode(self.output_encoding))
-            template.expand(context, output)
-            return output.getvalue()
-
-    ENGINE.add_transform(ept_to_html())
-
-if register_pil_transforms(ENGINE, verb=False):
-    HAS_PIL_TRANSFORMS = True
-else:
-    HAS_PIL_TRANSFORMS = False
-
-try:
-    from logilab.mtconverter.transforms import pygmentstransforms
-    for mt in ('text/plain',) + HTML_MIMETYPES:
-        try:
-            pygmentstransforms.mimetypes.remove(mt)
-        except ValueError:
-            continue
-    register_pygments_transforms(ENGINE, verb=False)
-
-    def patch_convert(cls):
-        def _convert(self, trdata, origconvert=cls._convert):
-            try:
-                trdata.appobject._cw.add_css('pygments.css')
-            except AttributeError: # session has no add_css, only http request
-                pass
-            return origconvert(self, trdata)
-        cls._convert = _convert
-    patch_convert(pygmentstransforms.PygmentsHTMLTransform)
-
-    HAS_PYGMENTS_TRANSFORMS = True
-except ImportError:
-    HAS_PYGMENTS_TRANSFORMS = False
-
-register_base_transforms(ENGINE, verb=False)
+"""pre 3.6 bw compat"""
+# pylint: disable-msg=W0614,W0401
+from warnings import warn
+warn('moved to cubicweb.mttransforms', DeprecationWarning, stacklevel=2)
+from cubicweb.mttransforms import *
--- a/common/tags.py	Tue Dec 08 10:40:20 2009 +0100
+++ b/common/tags.py	Tue Dec 08 10:58:56 2009 +0100
@@ -1,49 +1,5 @@
-"""helper classes to generate simple (X)HTML tags
-
-:organization: Logilab
-:copyright: 2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
-:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
-:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
-"""
-__docformat__ = "restructuredtext en"
-
-from cubicweb.common.uilib import simple_sgml_tag, sgml_attributes
-
-class tag(object):
-    def __init__(self, name, escapecontent=True):
-        self.name = name
-        self.escapecontent = escapecontent
-
-    def __call__(self, __content=None, **attrs):
-        attrs.setdefault('escapecontent', self.escapecontent)
-        return simple_sgml_tag(self.name, __content, **attrs)
-
-button = tag('button')
-input = tag('input')
-textarea = tag('textarea')
-a = tag('a')
-span = tag('span')
-div = tag('div', False)
-img = tag('img')
-label = tag('label')
-option = tag('option')
-h1 = tag('h1')
-h2 = tag('h2')
-h3 = tag('h3')
-h4 = tag('h4')
-h5 = tag('h5')
-tr = tag('tr')
-th = tag('th')
-td = tag('td')
-
-def select(name, id=None, multiple=False, options=[], **attrs):
-    if multiple:
-        attrs['multiple'] = 'multiple'
-    if id:
-        attrs['id'] = id
-    attrs['name'] = name
-    html = [u'<select %s>' % sgml_attributes(attrs)]
-    html += options
-    html.append(u'</select>')
-    return u'\n'.join(html)
-
+"""pre 3.6 bw compat"""
+# pylint: disable-msg=W0614,W0401
+from warnings import warn
+warn('moved to cubicweb.tags', DeprecationWarning, stacklevel=2)
+from cubicweb.tags import *
--- a/common/test/data/bootstrap_cubes	Tue Dec 08 10:40:20 2009 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-
--- a/common/test/unittest_mail.py	Tue Dec 08 10:40:20 2009 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,132 +0,0 @@
-# -*- coding: utf-8 -*-
-"""unit tests for module cubicweb.common.mail
-
-:organization: Logilab
-:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
-:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
-:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
-"""
-
-import os
-import sys
-
-from logilab.common.testlib import unittest_main
-from logilab.common.umessage import message_from_string
-
-from cubicweb.devtools.testlib import CubicWebTC
-from cubicweb.common.mail import format_mail
-
-
-def getlogin():
-    """avoid usinng os.getlogin() because of strange tty / stdin problems
-    (man 3 getlogin)
-    Another solution would be to use $LOGNAME, $USER or $USERNAME
-    """
-    if sys.platform != 'win32':
-        import pwd
-        return pwd.getpwuid(os.getuid())[0]
-    else:
-        return os.environ.get('USERNAME')
-
-
-class EmailTC(CubicWebTC):
-
-    def test_format_mail(self):
-        self.set_option('sender-addr', 'bim@boum.fr')
-        self.set_option('sender-name', 'BimBam')
-
-        mail = format_mail({'name': 'oim', 'email': 'oim@logilab.fr'},
-                           ['test@logilab.fr'], u'un petit cöucou', u'bïjour',
-                           config=self.config)
-        self.assertLinesEquals(mail.as_string(), """\
-MIME-Version: 1.0
-Content-Type: text/plain; charset="utf-8"
-Content-Transfer-Encoding: base64
-Subject: =?utf-8?q?b=C3=AFjour?=
-From: =?utf-8?q?oim?= <oim@logilab.fr>
-Reply-to: =?utf-8?q?oim?= <oim@logilab.fr>, =?utf-8?q?BimBam?= <bim@boum.fr>
-X-CW: data
-To: test@logilab.fr
-
-dW4gcGV0aXQgY8O2dWNvdQ==
-""")
-        msg = message_from_string(mail.as_string())
-        self.assertEquals(msg.get('subject'), u'bïjour')
-        self.assertEquals(msg.get('from'), u'oim <oim@logilab.fr>')
-        self.assertEquals(msg.get('to'), u'test@logilab.fr')
-        self.assertEquals(msg.get('reply-to'), u'oim <oim@logilab.fr>, BimBam <bim@boum.fr>')
-        self.assertEquals(msg.get_payload(decode=True), u'un petit cöucou')
-
-
-    def test_format_mail_euro(self):
-        mail = format_mail({'name': u'oîm', 'email': u'oim@logilab.fr'},
-                           ['test@logilab.fr'], u'un petit cöucou €', u'bïjour €')
-        self.assertLinesEquals(mail.as_string(), """\
-MIME-Version: 1.0
-Content-Type: text/plain; charset="utf-8"
-Content-Transfer-Encoding: base64
-Subject: =?utf-8?b?YsOvam91ciDigqw=?=
-From: =?utf-8?q?o=C3=AEm?= <oim@logilab.fr>
-Reply-to: =?utf-8?q?o=C3=AEm?= <oim@logilab.fr>
-To: test@logilab.fr
-
-dW4gcGV0aXQgY8O2dWNvdSDigqw=
-""")
-        msg = message_from_string(mail.as_string())
-        self.assertEquals(msg.get('subject'), u'bïjour €')
-        self.assertEquals(msg.get('from'), u'oîm <oim@logilab.fr>')
-        self.assertEquals(msg.get('to'), u'test@logilab.fr')
-        self.assertEquals(msg.get('reply-to'), u'oîm <oim@logilab.fr>')
-        self.assertEquals(msg.get_payload(decode=True), u'un petit cöucou €')
-
-
-    def test_format_mail_from_reply_to(self):
-        # no sender-name, sender-addr in the configuration
-        self.set_option('sender-name', '')
-        self.set_option('sender-addr', '')
-        msg = format_mail({'name': u'', 'email': u''},
-                          ['test@logilab.fr'], u'un petit cöucou €', u'bïjour €',
-                          config=self.config)
-        self.assertEquals(msg.get('from'), u'')
-        self.assertEquals(msg.get('reply-to'), None)
-        msg = format_mail({'name': u'tutu', 'email': u'tutu@logilab.fr'},
-                          ['test@logilab.fr'], u'un petit cöucou €', u'bïjour €',
-                          config=self.config)
-        msg = message_from_string(msg.as_string())
-        self.assertEquals(msg.get('from'), u'tutu <tutu@logilab.fr>')
-        self.assertEquals(msg.get('reply-to'), u'tutu <tutu@logilab.fr>')
-        msg = format_mail({'name': u'tutu', 'email': u'tutu@logilab.fr'},
-                          ['test@logilab.fr'], u'un petit cöucou €', u'bïjour €')
-        msg = message_from_string(msg.as_string())
-        self.assertEquals(msg.get('from'), u'tutu <tutu@logilab.fr>')
-        self.assertEquals(msg.get('reply-to'), u'tutu <tutu@logilab.fr>')
-        # set sender name and address as expected
-        self.set_option('sender-name', 'cubicweb-test')
-        self.set_option('sender-addr', 'cubicweb-test@logilab.fr')
-        # anonymous notification: no name and no email specified
-        msg = format_mail({'name': u'', 'email': u''},
-                           ['test@logilab.fr'], u'un petit cöucou €', u'bïjour €',
-                           config=self.config)
-        msg = message_from_string(msg.as_string())
-        self.assertEquals(msg.get('from'), u'cubicweb-test <cubicweb-test@logilab.fr>')
-        self.assertEquals(msg.get('reply-to'), u'cubicweb-test <cubicweb-test@logilab.fr>')
-        # anonymous notification: only email specified
-        msg = format_mail({'email': u'tutu@logilab.fr'},
-                           ['test@logilab.fr'], u'un petit cöucou €', u'bïjour €',
-                           config=self.config)
-        msg = message_from_string(msg.as_string())
-        self.assertEquals(msg.get('from'), u'cubicweb-test <tutu@logilab.fr>')
-        self.assertEquals(msg.get('reply-to'), u'cubicweb-test <tutu@logilab.fr>, cubicweb-test <cubicweb-test@logilab.fr>')
-        # anonymous notification: only name specified
-        msg = format_mail({'name': u'tutu'},
-                          ['test@logilab.fr'], u'un petit cöucou €', u'bïjour €',
-                          config=self.config)
-        msg = message_from_string(msg.as_string())
-        self.assertEquals(msg.get('from'), u'tutu <cubicweb-test@logilab.fr>')
-        self.assertEquals(msg.get('reply-to'), u'tutu <cubicweb-test@logilab.fr>')
-
-
-
-if __name__ == '__main__':
-    unittest_main()
-
--- a/common/test/unittest_uilib.py	Tue Dec 08 10:40:20 2009 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,86 +0,0 @@
-# -*- coding: utf-8 -*-
-"""unittests for cubicweb.common.uilib
-
-:organization: Logilab
-:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
-:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
-:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
-"""
-
-__docformat__ = "restructuredtext en"
-
-from logilab.common.testlib import TestCase, unittest_main
-from logilab.common.tree import Node
-
-from cubicweb.common import uilib
-
-class UILIBTC(TestCase):
-
-    def test_remove_tags(self):
-        """make sure remove_tags remove all tags"""
-        data = [
-            ('<h1>Hello</h1>', 'Hello'),
-            ('<h1>Hello <a href="foo/bar"><b>s</b>pam</a></h1>', 'Hello spam'),
-            ('<br>Hello<img src="doh.png"/>', 'Hello'),
-            ('<p></p>', ''),
-            ]
-        for text, expected in data:
-            got = uilib.remove_html_tags(text)
-            self.assertEquals(got, expected)
-
-    def test_fallback_safe_cut(self):
-        self.assertEquals(uilib.fallback_safe_cut(u'ab <a href="hello">cd</a>', 4), u'ab c...')
-        self.assertEquals(uilib.fallback_safe_cut(u'ab <a href="hello">cd</a>', 5), u'ab <a href="hello">cd</a>')
-        self.assertEquals(uilib.fallback_safe_cut(u'ab <a href="hello">&amp;d</a>', 4), u'ab &amp;...')
-        self.assertEquals(uilib.fallback_safe_cut(u'ab <a href="hello">&amp;d</a> ef', 5), u'ab &amp;d...')
-        self.assertEquals(uilib.fallback_safe_cut(u'ab <a href="hello">&igrave;d</a>', 4), u'ab ì...')
-        self.assertEquals(uilib.fallback_safe_cut(u'&amp; <a href="hello">&amp;d</a> ef', 4), u'&amp; &amp;d...')
-
-    def test_lxml_safe_cut(self):
-        self.assertEquals(uilib.safe_cut(u'aaa<div>aaad</div> ef', 4), u'<p>aaa</p><div>a...</div>')
-        self.assertEquals(uilib.safe_cut(u'aaa<div>aaad</div> ef', 7), u'<p>aaa</p><div>aaad</div>...')
-        self.assertEquals(uilib.safe_cut(u'aaa<div>aaad</div>', 7), u'<p>aaa</p><div>aaad</div>')
-        # Missing ellipsis due to space management but we don't care
-        self.assertEquals(uilib.safe_cut(u'ab <a href="hello">&amp;d</a>', 4), u'<p>ab <a href="hello">&amp;...</a></p>')
-
-    def test_cut(self):
-        """tests uilib.cut() behaviour"""
-        data = [
-            ('hello', 'hello'),
-            ('hello world', 'hello wo...'),
-            ("hell<b>O'</b> world", "hell<b>O..."),
-            ]
-        for text, expected in data:
-            got = uilib.cut(text, 8)
-            self.assertEquals(got, expected)
-
-    def test_text_cut(self):
-        """tests uilib.text_cut() behaviour with no text"""
-        data = [('',''),
-                ("""Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
-tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
-quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
-consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
-cillum dolore eu fugiat nulla pariatur.""",
-                 "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod \
-tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, \
-quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo \
-consequat."),
-                ("""Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
-tempor incididunt ut labore et dolore magna aliqua Ut enim ad minim veniam,
-quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
-consequat Duis aute irure dolor in reprehenderit in voluptate velit esse
-cillum dolore eu fugiat nulla pariatur Excepteur sint occaecat cupidatat non
-proident, sunt in culpa qui officia deserunt mollit anim id est laborum
-""",
-                 "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod \
-tempor incididunt ut labore et dolore magna aliqua Ut enim ad minim veniam, \
-quis nostrud exercitation ullamco laboris nisi"),
-                ]
-        for text, expected in data:
-            got = uilib.text_cut(text, 30)
-            self.assertEquals(got, expected)
-
-if __name__ == '__main__':
-    unittest_main()
-
--- a/common/uilib.py	Tue Dec 08 10:40:20 2009 +0100
+++ b/common/uilib.py	Tue Dec 08 10:58:56 2009 +0100
@@ -1,384 +1,5 @@
-# -*- coding: utf-8 -*-
-"""user interface libraries
-
-contains some functions designed to help implementation of cubicweb user interface
-
-:organization: Logilab
-:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
-:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
-:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
-"""
-__docformat__ = "restructuredtext en"
-
-import csv
-import re
-from StringIO import StringIO
-
-from logilab.mtconverter import xml_escape, html_unescape
-
-from cubicweb.utils import ustrftime
-
-def rql_for_eid(eid):
-    """return the rql query necessary to fetch entity with the given eid.  This
-    function should only be used to generate link with rql inside, not to give
-    to cursor.execute (in which case you won't benefit from rql cache).
-
-    :Parameters:
-      - `eid`: the eid of the entity we should search
-    :rtype: str
-    :return: the rql query
-    """
-    return 'Any X WHERE X eid %s' % eid
-
-
-def printable_value(req, attrtype, value, props=None, displaytime=True):
-    """return a displayable value (i.e. unicode string)"""
-    if value is None or attrtype == 'Bytes':
-        return u''
-    if attrtype == 'String':
-        # don't translate empty value if you don't want strange results
-        if props is not None and value and props.get('internationalizable'):
-            return req._(value)
-
-        return value
-    if attrtype == 'Date':
-        return ustrftime(value, req.property_value('ui.date-format'))
-    if attrtype == 'Time':
-        return ustrftime(value, req.property_value('ui.time-format'))
-    if attrtype == 'Datetime':
-        if displaytime:
-            return ustrftime(value, req.property_value('ui.datetime-format'))
-        return ustrftime(value, req.property_value('ui.date-format'))
-    if attrtype == 'Boolean':
-        if value:
-            return req._('yes')
-        return req._('no')
-    if attrtype == 'Float':
-        value = req.property_value('ui.float-format') % value
-    return unicode(value)
-
-
-# text publishing #############################################################
-
-try:
-    from cubicweb.ext.rest import rest_publish # pylint: disable-msg=W0611
-except ImportError:
-    def rest_publish(entity, data):
-        """default behaviour if docutils was not found"""
-        return xml_escape(data)
-
-TAG_PROG = re.compile(r'</?.*?>', re.U)
-def remove_html_tags(text):
-    """Removes HTML tags from text
-
-    >>> remove_html_tags('<td>hi <a href="http://www.google.fr">world</a></td>')
-    'hi world'
-    >>>
-    """
-    return TAG_PROG.sub('', text)
-
-
-REF_PROG = re.compile(r"<ref\s+rql=([\'\"])([^\1]*?)\1\s*>([^<]*)</ref>", re.U)
-def _subst_rql(view, obj):
-    delim, rql, descr = obj.groups()
-    return u'<a href="%s">%s</a>' % (view._cw.build_url(rql=rql), descr)
-
-def html_publish(view, text):
-    """replace <ref rql=''> links by <a href="...">"""
-    if not text:
-        return u''
-    return REF_PROG.sub(lambda obj, view=view:_subst_rql(view, obj), text)
-
-# fallback implementation, nicer one defined below if lxml is available
-def soup2xhtml(data, encoding):
-    # normalize line break
-    # see http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1
-    return u'\n'.join(data.splitlines())
-
-# fallback implementation, nicer one defined below if lxml> 2.0 is available
-def safe_cut(text, length):
-    """returns a string of length <length> based on <text>, removing any html
-    tags from given text if cut is necessary."""
-    if text is None:
-        return u''
-    noenttext = html_unescape(text)
-    text_nohtml = remove_html_tags(noenttext)
-    # try to keep html tags if text is short enough
-    if len(text_nohtml) <= length:
-        return text
-    # else if un-tagged text is too long, cut it
-    return xml_escape(text_nohtml[:length] + u'...')
-
-fallback_safe_cut = safe_cut
-
-
-try:
-    from lxml import etree
-except (ImportError, AttributeError):
-    # gae environment: lxml not available
-    pass
-else:
-
-    def soup2xhtml(data, encoding):
-        """tidy (at least try) html soup and return the result
-        Note: the function considers a string with no surrounding tag as valid
-              if <div>`data`</div> can be parsed by an XML parser
-        """
-        # normalize line break
-        # see http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1
-        data = u'\n'.join(data.splitlines())
-        # XXX lxml 1.1 support still needed ?
-        xmltree = etree.HTML('<div>%s</div>' % data)
-        # NOTE: lxml 1.1 (etch platforms) doesn't recognize
-        #       the encoding=unicode parameter (lxml 2.0 does), this is
-        #       why we specify an encoding and re-decode to unicode later
-        body = etree.tostring(xmltree[0], encoding=encoding)
-        # remove <body> and </body> and decode to unicode
-        return body[11:-13].decode(encoding)
-
-    if hasattr(etree.HTML('<div>test</div>'), 'iter'):
-
-        def safe_cut(text, length):
-            """returns an html document of length <length> based on <text>,
-            and cut is necessary.
-            """
-            if text is None:
-                return u''
-            dom = etree.HTML(text)
-            curlength = 0
-            add_ellipsis = False
-            for element in dom.iter():
-                if curlength >= length:
-                    parent = element.getparent()
-                    parent.remove(element)
-                    if curlength == length and (element.text or element.tail):
-                        add_ellipsis = True
-                else:
-                    if element.text is not None:
-                        element.text = cut(element.text, length - curlength)
-                        curlength += len(element.text)
-                    if element.tail is not None:
-                        if curlength < length:
-                            element.tail = cut(element.tail, length - curlength)
-                            curlength += len(element.tail)
-                        elif curlength == length:
-                            element.tail = '...'
-                        else:
-                            element.tail = ''
-            text = etree.tounicode(dom[0])[6:-7] # remove wrapping <body></body>
-            if add_ellipsis:
-                return text + u'...'
-            return text
-
-def text_cut(text, nbwords=30, gotoperiod=True):
-    """from the given plain text, return a text with at least <nbwords> words,
-    trying to go to the end of the current sentence.
-
-    :param nbwords: the minimum number of words required
-    :param gotoperiod: specifies if the function should try to go to
-                       the first period after the cut (i.e. finish
-                       the sentence if possible)
-
-    Note that spaces are normalized.
-    """
-    if text is None:
-        return u''
-    words = text.split()
-    text = u' '.join(words) # normalize spaces
-    textlength = minlength = len(' '.join(words[:nbwords]))
-    if gotoperiod:
-        textlength = text.find('.', minlength) + 1
-        if textlength == 0: # no period found
-            textlength = minlength
-    return text[:textlength]
-
-def cut(text, length):
-    """returns a string of a maximum length <length> based on <text>
-    (approximatively, since if text has been  cut, '...' is added to the end of the string,
-    resulting in a string of len <length> + 3)
-    """
-    if text is None:
-        return u''
-    if len(text) <= length:
-        return text
-    # else if un-tagged text is too long, cut it
-    return text[:length] + u'...'
-
-
-
-# HTML generation helper functions ############################################
-
-HTML4_EMPTY_TAGS = frozenset(('base', 'meta', 'link', 'hr', 'br', 'param',
-                              'img', 'area', 'input', 'col'))
-
-def sgml_attributes(attrs):
-    return u' '.join(u'%s="%s"' % (attr, xml_escape(unicode(value)))
-                     for attr, value in sorted(attrs.items())
-                     if value is not None)
-
-def simple_sgml_tag(tag, content=None, escapecontent=True, **attrs):
-    """generation of a simple sgml tag (eg without children tags) easier
-
-    content and attri butes will be escaped
-    """
-    value = u'<%s' % tag
-    if attrs:
-        try:
-            attrs['class'] = attrs.pop('klass')
-        except KeyError:
-            pass
-        value += u' ' + sgml_attributes(attrs)
-    if content:
-        if escapecontent:
-            content = xml_escape(unicode(content))
-        value += u'>%s</%s>' % (content, tag)
-    else:
-        if tag in HTML4_EMPTY_TAGS:
-            value += u' />'
-        else:
-            value += u'></%s>' % tag
-    return value
-
-def tooltipize(text, tooltip, url=None):
-    """make an HTML tooltip"""
-    url = url or '#'
-    return u'<a href="%s" title="%s">%s</a>' % (url, tooltip, text)
-
-def toggle_action(nodeid):
-    """builds a HTML link that uses the js toggleVisibility function"""
-    return u"javascript: toggleVisibility('%s')" % nodeid
-
-def toggle_link(nodeid, label):
-    """builds a HTML link that uses the js toggleVisibility function"""
-    return u'<a href="%s">%s</a>' % (toggle_action(nodeid), label)
-
-
-def ureport_as_html(layout):
-    from logilab.common.ureports import HTMLWriter
-    formater = HTMLWriter(True)
-    stream = StringIO() #UStringIO() don't want unicode assertion
-    formater.format(layout, stream)
-    res = stream.getvalue()
-    if isinstance(res, str):
-        res = unicode(res, 'UTF8')
-    return res
-
-# traceback formatting ########################################################
-
-import traceback
-
-def rest_traceback(info, exception):
-    """return a ReST formated traceback"""
-    res = [u'Traceback\n---------\n::\n']
-    for stackentry in traceback.extract_tb(info[2]):
-        res.append(u'\tFile %s, line %s, function %s' % tuple(stackentry[:3]))
-        if stackentry[3]:
-            res.append(u'\t  %s' % stackentry[3].decode('utf-8', 'replace'))
-    res.append(u'\n')
-    try:
-        res.append(u'\t Error: %s\n' % exception)
-    except:
-        pass
-    return u'\n'.join(res)
-
-
-def html_traceback(info, exception, title='',
-                   encoding='ISO-8859-1', body=''):
-    """ return an html formatted traceback from python exception infos.
-    """
-    tcbk = info[2]
-    stacktb = traceback.extract_tb(tcbk)
-    strings = []
-    if body:
-        strings.append(u'<div class="error_body">')
-        # FIXME
-        strings.append(body)
-        strings.append(u'</div>')
-    if title:
-        strings.append(u'<h1 class="error">%s</h1>'% xml_escape(title))
-    try:
-        strings.append(u'<p class="error">%s</p>' % xml_escape(str(exception)).replace("\n","<br />"))
-    except UnicodeError:
-        pass
-    strings.append(u'<div class="error_traceback">')
-    for index, stackentry in enumerate(stacktb):
-        strings.append(u'<b>File</b> <b class="file">%s</b>, <b>line</b> '
-                       u'<b class="line">%s</b>, <b>function</b> '
-                       u'<b class="function">%s</b>:<br/>'%(
-            xml_escape(stackentry[0]), stackentry[1], xml_escape(stackentry[2])))
-        if stackentry[3]:
-            string = xml_escape(stackentry[3]).decode('utf-8', 'replace')
-            strings.append(u'&#160;&#160;%s<br/>\n' % (string))
-        # add locals info for each entry
-        try:
-            local_context = tcbk.tb_frame.f_locals
-            html_info = []
-            chars = 0
-            for name, value in local_context.iteritems():
-                value = xml_escape(repr(value))
-                info = u'<span class="name">%s</span>=%s, ' % (name, value)
-                line_length = len(name) + len(value)
-                chars += line_length
-                # 150 is the result of *years* of research ;-) (CSS might be helpful here)
-                if chars > 150:
-                    info = u'<br/>' + info
-                    chars = line_length
-                html_info.append(info)
-            boxid = 'ctxlevel%d' % index
-            strings.append(u'[%s]' % toggle_link(boxid, '+'))
-            strings.append(u'<div id="%s" class="pycontext hidden">%s</div>' %
-                           (boxid, ''.join(html_info)))
-            tcbk = tcbk.tb_next
-        except Exception:
-            pass # doesn't really matter if we have no context info
-    strings.append(u'</div>')
-    return '\n'.join(strings)
-
-# csv files / unicode support #################################################
-
-class UnicodeCSVWriter:
-    """proxies calls to csv.writer.writerow to be able to deal with unicode"""
-
-    def __init__(self, wfunc, encoding, **kwargs):
-        self.writer = csv.writer(self, **kwargs)
-        self.wfunc = wfunc
-        self.encoding = encoding
-
-    def write(self, data):
-        self.wfunc(data)
-
-    def writerow(self, row):
-        csvrow = []
-        for elt in row:
-            if isinstance(elt, unicode):
-                csvrow.append(elt.encode(self.encoding))
-            else:
-                csvrow.append(str(elt))
-        self.writer.writerow(csvrow)
-
-    def writerows(self, rows):
-        for row in rows:
-            self.writerow(row)
-
-
-# some decorators #############################################################
-
-class limitsize(object):
-    def __init__(self, maxsize):
-        self.maxsize = maxsize
-
-    def __call__(self, function):
-        def newfunc(*args, **kwargs):
-            ret = function(*args, **kwargs)
-            if isinstance(ret, basestring):
-                return ret[:self.maxsize]
-            return ret
-        return newfunc
-
-
-def htmlescape(function):
-    def newfunc(*args, **kwargs):
-        ret = function(*args, **kwargs)
-        assert isinstance(ret, basestring)
-        return xml_escape(ret)
-    return newfunc
+"""pre 3.6 bw compat"""
+# pylint: disable-msg=W0614,W0401
+from warnings import warn
+warn('moved to cubicweb.uilib', DeprecationWarning, stacklevel=2)
+from cubicweb.uilib import *
--- a/cwconfig.py	Tue Dec 08 10:40:20 2009 +0100
+++ b/cwconfig.py	Tue Dec 08 10:58:56 2009 +0100
@@ -556,6 +556,7 @@
         return vregpath
 
     def __init__(self):
+        register_stored_procedures()
         ConfigurationMixIn.__init__(self)
         self.adjust_sys_path()
         self.load_defaults()
@@ -967,3 +968,53 @@
 # alias to get a configuration instance from an instance id
 instance_configuration = CubicWebConfiguration.config_for
 application_configuration = deprecated('use instance_configuration')(instance_configuration)
+
+
+_EXT_REGISTERED = False
+def register_stored_procedures():
+    from logilab.common.adbh import FunctionDescr
+    from rql.utils import register_function, iter_funcnode_variables
+
+    global _EXT_REGISTERED
+    if _EXT_REGISTERED:
+        return
+    _EXT_REGISTERED = True
+
+    class COMMA_JOIN(FunctionDescr):
+        supported_backends = ('postgres', 'sqlite',)
+        rtype = 'String'
+
+        @classmethod
+        def st_description(cls, funcnode, mainindex, tr):
+            return ', '.join(sorted(term.get_description(mainindex, tr)
+                                    for term in iter_funcnode_variables(funcnode)))
+
+    register_function(COMMA_JOIN)  # XXX do not expose?
+
+
+    class CONCAT_STRINGS(COMMA_JOIN):
+        aggregat = True
+
+    register_function(CONCAT_STRINGS) # XXX bw compat
+
+    class GROUP_CONCAT(CONCAT_STRINGS):
+        supported_backends = ('mysql', 'postgres', 'sqlite',)
+
+    register_function(GROUP_CONCAT)
+
+
+    class LIMIT_SIZE(FunctionDescr):
+        supported_backends = ('postgres', 'sqlite',)
+        rtype = 'String'
+
+        @classmethod
+        def st_description(cls, funcnode, mainindex, tr):
+            return funcnode.children[0].get_description(mainindex, tr)
+
+    register_function(LIMIT_SIZE)
+
+
+    class TEXT_LIMIT_SIZE(LIMIT_SIZE):
+        supported_backends = ('mysql', 'postgres', 'sqlite',)
+
+    register_function(TEXT_LIMIT_SIZE)
--- a/entities/wfobjs.py	Tue Dec 08 10:40:20 2009 +0100
+++ b/entities/wfobjs.py	Tue Dec 08 10:58:56 2009 +0100
@@ -15,7 +15,7 @@
 
 from cubicweb.entities import AnyEntity, fetch_config
 from cubicweb.interfaces import IWorkflowable
-from cubicweb.common.mixins import MI_REL_TRIGGERS
+from cubicweb.mixins import MI_REL_TRIGGERS
 
 class WorkflowException(Exception): pass
 
--- a/entity.py	Tue Dec 08 10:40:20 2009 +0100
+++ b/entity.py	Tue Dec 08 10:58:56 2009 +0100
@@ -25,9 +25,9 @@
 from cubicweb.schema import RQLVocabularyConstraint, RQLConstraint
 from cubicweb.rqlrewrite import RQLRewriter
 
-from cubicweb.common.uilib import printable_value, soup2xhtml
-from cubicweb.common.mixins import MI_REL_TRIGGERS
-from cubicweb.common.mttransforms import ENGINE
+from cubicweb.uilib import printable_value, soup2xhtml
+from cubicweb.mixins import MI_REL_TRIGGERS
+from cubicweb.mttransforms import ENGINE
 
 _marker = object()
 
--- a/goa/__init__.py	Tue Dec 08 10:40:20 2009 +0100
+++ b/goa/__init__.py	Tue Dec 08 10:58:56 2009 +0100
@@ -44,7 +44,7 @@
 
         def rql_for_eid(eid):
             return 'Any X WHERE X eid "%s"' % eid
-        from cubicweb.common import uilib
+        from cubicweb import uilib
         uilib.rql_for_eid = rql_for_eid
 
         def typed_eid(eid):
--- a/goa/appobjects/components.py	Tue Dec 08 10:40:20 2009 +0100
+++ b/goa/appobjects/components.py	Tue Dec 08 10:58:56 2009 +0100
@@ -12,7 +12,7 @@
 from cubicweb import typed_eid
 from cubicweb.selectors import one_line_rset, match_search_state, accept
 from cubicweb.schema import display_name
-from cubicweb.common.view import StartupView, EntityView
+from cubicweb.view import StartupView, EntityView
 from cubicweb.web import Redirect
 from cubicweb.web.views import vid_from_rset
 
--- a/goa/appobjects/dbmgmt.py	Tue Dec 08 10:40:20 2009 +0100
+++ b/goa/appobjects/dbmgmt.py	Tue Dec 08 10:58:56 2009 +0100
@@ -15,7 +15,7 @@
 from logilab.mtconverter import xml_escape
 
 from cubicweb.selectors import none_rset, match_user_groups
-from cubicweb.common.view import StartupView
+from cubicweb.view import StartupView
 from cubicweb.web import Redirect
 from cubicweb.goa.dbinit import fix_entities, init_persistent_schema, insert_versions
 
--- a/goa/appobjects/sessions.py	Tue Dec 08 10:40:20 2009 +0100
+++ b/goa/appobjects/sessions.py	Tue Dec 08 10:58:56 2009 +0100
@@ -251,7 +251,7 @@
 set_log_methods(ConnectionProxy, logging.getLogger('cubicweb.web.goa.session'))
 
 
-from cubicweb.common.view import StartupView
+from cubicweb.view import StartupView
 from cubicweb.web import application
 
 class SessionsCleaner(StartupView):
--- a/goa/goactl.py	Tue Dec 08 10:40:20 2009 +0100
+++ b/goa/goactl.py	Tue Dec 08 10:58:56 2009 +0100
@@ -60,21 +60,20 @@
     'entity.py',
     'interfaces.py',
     'i18n.py',
+    'mail.py',
     'migration.py',
+    'mixins.py',
+    'mttransforms.py',
     'rqlrewrite.py',
     'rset.py',
     'schema.py',
     'schemaviewer.py',
     'selectors.py',
+    'uilib.py',
     'utils.py',
     'vregistry.py',
     'view.py',
 
-    'common/mail.py',
-    'common/mixins.py',
-    'common/mttransforms.py',
-    'common/uilib.py',
-
     'ext/html4zope.py',
     'ext/rest.py',
 
@@ -166,7 +165,7 @@
 
 OVERRIDEN_FILES = (
     ('toolsutils.py', 'toolsutils.py'),
-    ('mttransforms.py', 'common/mttransforms.py'),
+    ('mttransforms.py', 'mttransforms.py'),
     ('server__init__.py', 'server/__init__.py'),
     ('rqlannotation.py', 'server/rqlannotation.py'),
     )
@@ -210,7 +209,6 @@
                 create_dir(split(target)[0])
             create_symlink(join(CW_SOFTWARE_ROOT, fpath), target)
         # overriden files
-        create_init_file(join(appldir, 'cubicweb/common'), 'cubicweb.common')
         for fpath, subfpath in OVERRIDEN_FILES:
             create_symlink(join(CW_SOFTWARE_ROOT, 'goa', 'overrides', fpath),
                            join(appldir, 'cubicweb', subfpath))
--- a/goa/overrides/mttransforms.py	Tue Dec 08 10:40:20 2009 +0100
+++ b/goa/overrides/mttransforms.py	Tue Dec 08 10:58:56 2009 +0100
@@ -11,7 +11,7 @@
 
 from logilab.mtconverter.engine import TransformEngine
 from logilab.mtconverter.transform import Transform
-from cubicweb.common.uilib import rest_publish, html_publish, remove_html_tags
+from cubicweb.uilib import rest_publish, html_publish, remove_html_tags
 
 HTML_MIMETYPES = ('text/html', 'text/xhtml', 'application/xhtml+xml')
 # CubicWeb specific transformations
--- a/goa/test/data/views.py	Tue Dec 08 10:40:20 2009 +0100
+++ b/goa/test/data/views.py	Tue Dec 08 10:58:56 2009 +0100
@@ -20,7 +20,7 @@
 
 template.VariableNode.encode_output = encode_output
 
-from cubicweb.common.view import StartupView
+from cubicweb.view import StartupView
 
 INDEX_TEMPLATE = template.Template(u'''
  <h1>hellô {{ user.login }}</h1>
--- a/goa/test/unittest_editcontroller.py	Tue Dec 08 10:40:20 2009 +0100
+++ b/goa/test/unittest_editcontroller.py	Tue Dec 08 10:58:56 2009 +0100
@@ -9,8 +9,8 @@
 
 from urllib import unquote
 
-from cubicweb.common import ValidationError
-from cubicweb.common.uilib import rql_for_eid
+from cubicweb import ValidationError
+from cubicweb.uilib import rql_for_eid
 
 from cubicweb.web import INTERNAL_FIELD_VALUE, Redirect
 
--- a/goa/tools/laxctl.py	Tue Dec 08 10:40:20 2009 +0100
+++ b/goa/tools/laxctl.py	Tue Dec 08 10:58:56 2009 +0100
@@ -18,7 +18,7 @@
 
 from logilab.common.clcommands import Command, register_commands, main_run
 
-from cubicweb.common.uilib import remove_html_tags
+from cubicweb.uilib import remove_html_tags
 from cubicweb.web.views.schema import SKIP_TYPES
 
 APPLROOT = osp.abspath(osp.join(osp.dirname(osp.abspath(__file__)), '..'))
--- a/hooks/integrity.py	Tue Dec 08 10:40:20 2009 +0100
+++ b/hooks/integrity.py	Tue Dec 08 10:58:56 2009 +0100
@@ -11,7 +11,7 @@
 from cubicweb import ValidationError
 from cubicweb.schema import RQLConstraint, RQLUniqueConstraint
 from cubicweb.selectors import entity_implements
-from cubicweb.common.uilib import soup2xhtml
+from cubicweb.uilib import soup2xhtml
 from cubicweb.server import hook
 
 # special relations that don't have to be checked for integrity, usually
--- a/interfaces.py	Tue Dec 08 10:40:20 2009 +0100
+++ b/interfaces.py	Tue Dec 08 10:58:56 2009 +0100
@@ -56,7 +56,7 @@
 class IProgress(Interface):
     """something that has a cost, a state and a progression
 
-    Take a look at cubicweb.common.mixins.ProgressMixIn for some
+    Take a look at cubicweb.mixins.ProgressMixIn for some
     default implementations
     """
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mail.py	Tue Dec 08 10:58:56 2009 +0100
@@ -0,0 +1,272 @@
+"""Common utilies to format / semd emails.
+
+:organization: Logilab
+:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
+:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
+"""
+__docformat__ = "restructuredtext en"
+
+from base64 import b64encode, b64decode
+from itertools import repeat
+from time import time
+from email.MIMEMultipart import MIMEMultipart
+from email.MIMEText import MIMEText
+from email.MIMEImage import MIMEImage
+from email.Header import Header
+try:
+    from socket import gethostname
+except ImportError:
+    def gethostname(): # gae
+        return 'XXX'
+
+from cubicweb.view import EntityView
+from cubicweb.entity import Entity
+
+def header(ustring):
+    return Header(ustring.encode('UTF-8'), 'UTF-8')
+
+def addrheader(uaddr, uname=None):
+    # even if an email address should be ascii, encode it using utf8 since
+    # automatic tests may generate non ascii email address
+    addr = uaddr.encode('UTF-8')
+    if uname:
+        return '%s <%s>' % (header(uname).encode(), addr)
+    return addr
+
+
+def construct_message_id(appid, eid, withtimestamp=True):
+    if withtimestamp:
+        addrpart = 'eid=%s&timestamp=%.10f' % (eid, time())
+    else:
+        addrpart = 'eid=%s' % eid
+    # we don't want any equal sign nor trailing newlines
+    leftpart = b64encode(addrpart, '.-').rstrip().rstrip('=')
+    return '<%s@%s.%s>' % (leftpart, appid, gethostname())
+
+
+def parse_message_id(msgid, appid):
+    if msgid[0] == '<':
+        msgid = msgid[1:]
+    if msgid[-1] == '>':
+        msgid = msgid[:-1]
+    try:
+        values, qualif = msgid.split('@')
+        padding = len(values) % 4
+        values = b64decode(str(values + '='*padding), '.-')
+        values = dict(v.split('=') for v in values.split('&'))
+        fromappid, host = qualif.split('.', 1)
+    except:
+        return None
+    if appid != fromappid or host != gethostname():
+        return None
+    return values
+
+
+def format_mail(uinfo, to_addrs, content, subject="",
+                cc_addrs=(), msgid=None, references=(), config=None):
+    """Sends an Email to 'e_addr' with content 'content', and subject 'subject'
+
+    to_addrs and cc_addrs are expected to be a list of email address without
+    name
+    """
+    assert type(content) is unicode, repr(content)
+    msg = MIMEText(content.encode('UTF-8'), 'plain', 'UTF-8')
+    # safety: keep only the first newline
+    subject = subject.splitlines()[0]
+    msg['Subject'] = header(subject)
+    if uinfo.get('email'):
+        email = uinfo['email']
+    elif config and config['sender-addr']:
+        email = unicode(config['sender-addr'])
+    else:
+        email = u''
+    if uinfo.get('name'):
+        name = uinfo['name']
+    elif config and config['sender-addr']:
+        name = unicode(config['sender-name'])
+    else:
+        name = u''
+    msg['From'] = addrheader(email, name)
+    if config and config['sender-addr'] and config['sender-addr'] != email:
+        appaddr = addrheader(config['sender-addr'], config['sender-name'])
+        msg['Reply-to'] = '%s, %s' % (msg['From'], appaddr)
+    elif email:
+        msg['Reply-to'] = msg['From']
+    if config is not None:
+        msg['X-CW'] = config.appid
+    unique_addrs = lambda addrs: sorted(set(addr for addr in addrs if addr is not None))
+    msg['To'] = ', '.join(addrheader(addr) for addr in unique_addrs(to_addrs))
+    if cc_addrs:
+        msg['Cc'] = ', '.join(addrheader(addr) for addr in unique_addrs(cc_addrs))
+    if msgid:
+        msg['Message-id'] = msgid
+    if references:
+        msg['References'] = ', '.join(references)
+    return msg
+
+
+class HtmlEmail(MIMEMultipart):
+
+    def __init__(self, subject, textcontent, htmlcontent,
+                 sendermail=None, sendername=None, recipients=None, ccrecipients=None):
+        MIMEMultipart.__init__(self, 'related')
+        self['Subject'] = header(subject)
+        self.preamble = 'This is a multi-part message in MIME format.'
+        # Attach alternative text message
+        alternative = MIMEMultipart('alternative')
+        self.attach(alternative)
+        msgtext = MIMEText(textcontent.encode('UTF-8'), 'plain', 'UTF-8')
+        alternative.attach(msgtext)
+        # Attach html message
+        msghtml = MIMEText(htmlcontent.encode('UTF-8'), 'html', 'UTF-8')
+        alternative.attach(msghtml)
+        if sendermail or sendername:
+            self['From'] = addrheader(sendermail, sendername)
+        if recipients:
+            self['To'] = ', '.join(addrheader(addr) for addr in recipients if addr is not None)
+        if ccrecipients:
+            self['Cc'] = ', '.join(addrheader(addr) for addr in ccrecipients if addr is not None)
+
+    def attach_image(self, data, htmlId):
+        image = MIMEImage(data)
+        image.add_header('Content-ID', '<%s>' % htmlId)
+        self.attach(image)
+
+
+class NotificationView(EntityView):
+    """abstract view implementing the "email" API (eg to simplify sending
+    notification)
+    """
+    # XXX refactor this class to work with len(rset) > 1
+
+    msgid_timestamp = True
+
+    # this is usually the method to call
+    def render_and_send(self, **kwargs):
+        """generate and send an email message for this view"""
+        delayed = kwargs.pop('delay_to_commit', None)
+        for recipients, msg in self.render_emails(**kwargs):
+            if delayed is None:
+                self.send(recipients, msg)
+            elif delayed:
+                self.send_on_commit(recipients, msg)
+            else:
+                self.send_now(recipients, msg)
+
+    def cell_call(self, row, col=0, **kwargs):
+        self.w(self._cw._(self.content) % self.context(**kwargs))
+
+    def render_emails(self, **kwargs):
+        """generate and send emails for this view (one per recipient)"""
+        self._kwargs = kwargs
+        recipients = self.recipients()
+        if not recipients:
+            self.info('skipping %s notification, no recipients', self.__regid__)
+            return
+        if self.cw_rset is not None:
+            entity = self.cw_rset.get_entity(self.cw_row or 0, self.cw_col or 0)
+            # if the view is using timestamp in message ids, no way to reference
+            # previous email
+            if not self.msgid_timestamp:
+                refs = [self.construct_message_id(eid)
+                        for eid in entity.notification_references(self)]
+            else:
+                refs = ()
+            msgid = self.construct_message_id(entity.eid)
+        else:
+            refs = ()
+            msgid = None
+        req = self._cw
+        self.user_data = req.user_data()
+        origlang = req.lang
+        for something in recipients:
+            if isinstance(something, Entity):
+                # hi-jack self._cw to get a session for the returned user
+                self._cw = self._cw.hijack_user(something)
+                emailaddr = something.get_email()
+            else:
+                emailaddr, lang = something
+                self._cw.set_language(lang)
+            # since the same view (eg self) may be called multiple time and we
+            # need a fresh stream at each iteration, reset it explicitly
+            self.w = None
+            # XXX call render before subject to set .row/.col attributes on the
+            #     view
+            try:
+                content = self.render(row=0, col=0, **kwargs)
+                subject = self.subject()
+            except SkipEmail:
+                continue
+            except Exception, ex:
+                # shouldn't make the whole transaction fail because of rendering
+                # error (unauthorized or such)
+                self.exception(str(ex))
+                continue
+            msg = format_mail(self.user_data, [emailaddr], content, subject,
+                              config=self._cw.vreg.config, msgid=msgid, references=refs)
+            yield [emailaddr], msg
+        # restore language
+        req.set_language(origlang)
+
+    # recipients / email sending ###############################################
+
+    def recipients(self):
+        """return a list of either 2-uple (email, language) or user entity to
+        who this email should be sent
+        """
+        # use super_session when available, we don't want to consider security
+        # when selecting recipients_finder
+        try:
+            req = self._cw.super_session
+        except AttributeError:
+            req = self._cw
+        finder = self._cw.vreg['components'].select('recipients_finder', req,
+                                                    rset=self.cw_rset,
+                                                    row=self.cw_row or 0,
+                                                    col=self.cw_col or 0)
+        return finder.recipients()
+
+    def send_now(self, recipients, msg):
+        self._cw.vreg.config.sendmails([(msg, recipients)])
+
+    def send_on_commit(self, recipients, msg):
+        raise NotImplementedError
+
+    send = send_now
+
+    # email generation helpers #################################################
+
+    def construct_message_id(self, eid):
+        return construct_message_id(self._cw.vreg.config.appid, eid, self.msgid_timestamp)
+
+    def format_field(self, attr, value):
+        return ':%(attr)s: %(value)s' % {'attr': attr, 'value': value}
+
+    def format_section(self, attr, value):
+        return '%(attr)s\n%(ul)s\n%(value)s\n' % {
+            'attr': attr, 'ul': '-'*len(attr), 'value': value}
+
+    def subject(self):
+        entity = self.cw_rset.get_entity(self.cw_row or 0, self.cw_col or 0)
+        subject = self._cw._(self.message)
+        etype = entity.dc_type()
+        eid = entity.eid
+        login = self.user_data['login']
+        return self._cw._('%(subject)s %(etype)s #%(eid)s (%(login)s)') % locals()
+
+    def context(self, **kwargs):
+        entity = self.cw_rset.get_entity(self.cw_row or 0, self.cw_col or 0)
+        for key, val in kwargs.iteritems():
+            if val and isinstance(val, unicode) and val.strip():
+               kwargs[key] = self._cw._(val)
+        kwargs.update({'user': self.user_data['login'],
+                       'eid': entity.eid,
+                       'etype': entity.dc_type(),
+                       'url': entity.absolute_url(),
+                       'title': entity.dc_long_title(),})
+        return kwargs
+
+
+class SkipEmail(Exception):
+    """raise this if you decide to skip an email during its generation"""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mixins.py	Tue Dec 08 10:58:56 2009 +0100
@@ -0,0 +1,315 @@
+"""mixins of entity/views organized somewhat in a graph or tree structure
+
+
+:organization: Logilab
+:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
+:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
+"""
+__docformat__ = "restructuredtext en"
+
+from logilab.common.deprecation import deprecated
+from logilab.common.decorators import cached
+
+from cubicweb import typed_eid
+from cubicweb.selectors import implements
+from cubicweb.interfaces import IEmailable, ITree
+
+
+class TreeMixIn(object):
+    """base tree-mixin providing the tree interface
+
+    This mixin has to be inherited explicitly and configured using the
+    tree_attribute, parent_target and children_target class attribute to
+    benefit from this default implementation
+    """
+    tree_attribute = None
+    # XXX misnamed
+    parent_target = 'subject'
+    children_target = 'object'
+
+    def different_type_children(self, entities=True):
+        """return children entities of different type as this entity.
+
+        according to the `entities` parameter, return entity objects or the
+        equivalent result set
+        """
+        res = self.related(self.tree_attribute, self.children_target,
+                           entities=entities)
+        if entities:
+            return [e for e in res if e.e_schema != self.e_schema]
+        return res.filtered_rset(lambda x: x.e_schema != self.e_schema, self.cw_col)
+
+    def same_type_children(self, entities=True):
+        """return children entities of the same type as this entity.
+
+        according to the `entities` parameter, return entity objects or the
+        equivalent result set
+        """
+        res = self.related(self.tree_attribute, self.children_target,
+                           entities=entities)
+        if entities:
+            return [e for e in res if e.e_schema == self.e_schema]
+        return res.filtered_rset(lambda x: x.e_schema == self.e_schema, self.cw_col)
+
+    def iterchildren(self, _done=None):
+        if _done is None:
+            _done = set()
+        for child in self.children():
+            if child.eid in _done:
+                self.error('loop in %s tree', self.__regid__.lower())
+                continue
+            yield child
+            _done.add(child.eid)
+
+    def prefixiter(self, _done=None):
+        if _done is None:
+            _done = set()
+        if self.eid in _done:
+            return
+        yield self
+        _done.add(self.eid)
+        for child in self.iterchildren(_done):
+            try:
+                for entity in child.prefixiter(_done):
+                    yield entity
+            except AttributeError:
+                pass
+
+    @cached
+    def path(self):
+        """returns the list of eids from the root object to this object"""
+        path = []
+        parent = self
+        while parent:
+            if parent.eid in path:
+                self.error('loop in %s tree', self.__regid__.lower())
+                break
+            path.append(parent.eid)
+            try:
+                # check we are not leaving the tree
+                if (parent.tree_attribute != self.tree_attribute or
+                    parent.parent_target != self.parent_target):
+                    break
+                parent = parent.parent()
+            except AttributeError:
+                break
+
+        path.reverse()
+        return path
+
+    def iterparents(self):
+        def _uptoroot(self):
+            curr = self
+            while True:
+                curr = curr.parent()
+                if curr is None:
+                    break
+                yield curr
+        return _uptoroot(self)
+
+    def notification_references(self, view):
+        """used to control References field of email send on notification
+        for this entity. `view` is the notification view.
+
+        Should return a list of eids which can be used to generate message ids
+        of previously sent email
+        """
+        return self.path()[:-1]
+
+
+    ## ITree interface ########################################################
+    def parent(self):
+        """return the parent entity if any, else None (e.g. if we are on the
+        root
+        """
+        try:
+            return self.related(self.tree_attribute, self.parent_target,
+                                entities=True)[0]
+        except (KeyError, IndexError):
+            return None
+
+    def children(self, entities=True, sametype=False):
+        """return children entities
+
+        according to the `entities` parameter, return entity objects or the
+        equivalent result set
+        """
+        if sametype:
+            return self.same_type_children(entities)
+        else:
+            return self.related(self.tree_attribute, self.children_target,
+                                entities=entities)
+
+    def children_rql(self):
+        return self.related_rql(self.tree_attribute, self.children_target)
+
+    def is_leaf(self):
+        return len(self.children()) == 0
+
+    def is_root(self):
+        return self.parent() is None
+
+    def root(self):
+        """return the root object"""
+        return self._cw.entity_from_eid(self.path()[0])
+
+
+class EmailableMixIn(object):
+    """base mixin providing the default get_email() method used by
+    the massmailing view
+
+    NOTE: The default implementation is based on the
+    primary_email / use_email scheme
+    """
+    __implements__ = (IEmailable,)
+
+    def get_email(self):
+        if getattr(self, 'primary_email', None):
+            return self.primary_email[0].address
+        if getattr(self, 'use_email', None):
+            return self.use_email[0].address
+        return None
+
+    @classmethod
+    def allowed_massmail_keys(cls):
+        """returns a set of allowed email substitution keys
+
+        The default is to return the entity's attribute list but an
+        entity class might override this method to allow extra keys.
+        For instance, the Person class might want to return a `companyname`
+        key.
+        """
+        return set(rschema.type
+                   for rschema, attrtype in cls.e_schema.attribute_definitions()
+                   if attrtype.type not in ('Password', 'Bytes'))
+
+    def as_email_context(self):
+        """returns the dictionary as used by the sendmail controller to
+        build email bodies.
+
+        NOTE: the dictionary keys should match the list returned by the
+        `allowed_massmail_keys` method.
+        """
+        return dict( (attr, getattr(self, attr)) for attr in self.allowed_massmail_keys() )
+
+
+"""pluggable mixins system: plug classes registered in MI_REL_TRIGGERS on entity
+classes which have the relation described by the dict's key.
+
+NOTE: pluggable mixins can't override any method of the 'explicit' user classes tree
+(eg without plugged classes). This includes bases Entity and AnyEntity classes.
+"""
+MI_REL_TRIGGERS = {
+    ('primary_email',   'subject'): EmailableMixIn,
+    ('use_email',   'subject'): EmailableMixIn,
+    }
+
+
+
+def _done_init(done, view, row, col):
+    """handle an infinite recursion safety belt"""
+    if done is None:
+        done = set()
+    entity = view.cw_rset.get_entity(row, col)
+    if entity.eid in done:
+        msg = entity._cw._('loop in %(rel)s relation (%(eid)s)') % {
+            'rel': entity.tree_attribute,
+            'eid': entity.eid
+            }
+        return None, msg
+    done.add(entity.eid)
+    return done, entity
+
+
+class TreeViewMixIn(object):
+    """a recursive tree view"""
+    __regid__ = 'tree'
+    item_vid = 'treeitem'
+    __select__ = implements(ITree)
+
+    def call(self, done=None, **kwargs):
+        if done is None:
+            done = set()
+        super(TreeViewMixIn, self).call(done=done, **kwargs)
+
+    def cell_call(self, row, col=0, vid=None, done=None, **kwargs):
+        done, entity = _done_init(done, self, row, col)
+        if done is None:
+            # entity is actually an error message
+            self.w(u'<li class="badcontent">%s</li>' % entity)
+            return
+        self.open_item(entity)
+        entity.view(vid or self.item_vid, w=self.w, **kwargs)
+        relatedrset = entity.children(entities=False)
+        self.wview(self.__regid__, relatedrset, 'null', done=done, **kwargs)
+        self.close_item(entity)
+
+    def open_item(self, entity):
+        self.w(u'<li class="%s">\n' % entity.__regid__.lower())
+    def close_item(self, entity):
+        self.w(u'</li>\n')
+
+
+class TreePathMixIn(object):
+    """a recursive path view"""
+    __regid__ = 'path'
+    item_vid = 'oneline'
+    separator = u'&#160;&gt;&#160;'
+
+    def call(self, **kwargs):
+        self.w(u'<div class="pathbar">')
+        super(TreePathMixIn, self).call(**kwargs)
+        self.w(u'</div>')
+
+    def cell_call(self, row, col=0, vid=None, done=None, **kwargs):
+        done, entity = _done_init(done, self, row, col)
+        if done is None:
+            # entity is actually an error message
+            self.w(u'<span class="badcontent">%s</span>' % entity)
+            return
+        parent = entity.parent()
+        if parent:
+            parent.view(self.__regid__, w=self.w, done=done)
+            self.w(self.separator)
+        entity.view(vid or self.item_vid, w=self.w)
+
+
+class ProgressMixIn(object):
+    """provide default implementations for IProgress interface methods"""
+    # This is an adapter isn't it ?
+
+    @property
+    def cost(self):
+        return self.progress_info()['estimated']
+
+    @property
+    def revised_cost(self):
+        return self.progress_info().get('estimatedcorrected', self.cost)
+
+    @property
+    def done(self):
+        return self.progress_info()['done']
+
+    @property
+    def todo(self):
+        return self.progress_info()['todo']
+
+    @cached
+    def progress_info(self):
+        raise NotImplementedError()
+
+    def finished(self):
+        return not self.in_progress()
+
+    def in_progress(self):
+        raise NotImplementedError()
+
+    def progress(self):
+        try:
+            return 100. * self.done / self.revised_cost
+        except ZeroDivisionError:
+            # total cost is 0 : if everything was estimated, task is completed
+            if self.progress_info().get('notestimated'):
+                return 0.
+            return 100
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mttransforms.py	Tue Dec 08 10:58:56 2009 +0100
@@ -0,0 +1,102 @@
+"""mime type transformation engine for cubicweb, based on mtconverter
+
+:organization: Logilab
+:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
+:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
+"""
+__docformat__ = "restructuredtext en"
+
+from logilab import mtconverter
+
+from logilab.mtconverter.engine import TransformEngine
+from logilab.mtconverter.transform import Transform
+from logilab.mtconverter import (register_base_transforms,
+                                 register_pil_transforms,
+                                 register_pygments_transforms)
+
+from cubicweb.utils import UStringIO
+from cubicweb.uilib import rest_publish, html_publish
+
+HTML_MIMETYPES = ('text/html', 'text/xhtml', 'application/xhtml+xml')
+
+# CubicWeb specific transformations
+
+class rest_to_html(Transform):
+    inputs = ('text/rest', 'text/x-rst')
+    output = 'text/html'
+    def _convert(self, trdata):
+        return rest_publish(trdata.appobject, trdata.decode())
+
+class html_to_html(Transform):
+    inputs = HTML_MIMETYPES
+    output = 'text/html'
+    def _convert(self, trdata):
+        return html_publish(trdata.appobject, trdata.data)
+
+
+# Instantiate and configure the transformation engine
+
+mtconverter.UNICODE_POLICY = 'replace'
+
+ENGINE = TransformEngine()
+ENGINE.add_transform(rest_to_html())
+ENGINE.add_transform(html_to_html())
+
+try:
+    from cubicweb.ext.tal import CubicWebContext, compile_template
+except ImportError:
+    HAS_TAL = False
+    from cubicweb import schema
+    schema.NEED_PERM_FORMATS.remove('text/cubicweb-page-template')
+
+else:
+    HAS_TAL = True
+
+    class ept_to_html(Transform):
+        inputs = ('text/cubicweb-page-template',)
+        output = 'text/html'
+        output_encoding = 'utf-8'
+        def _convert(self, trdata):
+            context = CubicWebContext()
+            appobject = trdata.appobject
+            context.update({'self': appobject, 'rset': appobject.cw_rset,
+                            'req': appobject._cw,
+                            '_' : appobject._cw._,
+                            'user': appobject._cw.user})
+            output = UStringIO()
+            template = compile_template(trdata.encode(self.output_encoding))
+            template.expand(context, output)
+            return output.getvalue()
+
+    ENGINE.add_transform(ept_to_html())
+
+if register_pil_transforms(ENGINE, verb=False):
+    HAS_PIL_TRANSFORMS = True
+else:
+    HAS_PIL_TRANSFORMS = False
+
+try:
+    from logilab.mtconverter.transforms import pygmentstransforms
+    for mt in ('text/plain',) + HTML_MIMETYPES:
+        try:
+            pygmentstransforms.mimetypes.remove(mt)
+        except ValueError:
+            continue
+    register_pygments_transforms(ENGINE, verb=False)
+
+    def patch_convert(cls):
+        def _convert(self, trdata, origconvert=cls._convert):
+            try:
+                trdata.appobject._cw.add_css('pygments.css')
+            except AttributeError: # session has no add_css, only http request
+                pass
+            return origconvert(self, trdata)
+        cls._convert = _convert
+    patch_convert(pygmentstransforms.PygmentsHTMLTransform)
+
+    HAS_PYGMENTS_TRANSFORMS = True
+except ImportError:
+    HAS_PYGMENTS_TRANSFORMS = False
+
+register_base_transforms(ENGINE, verb=False)
--- a/rset.py	Tue Dec 08 10:40:20 2009 +0100
+++ b/rset.py	Tue Dec 08 10:58:56 2009 +0100
@@ -257,7 +257,7 @@
         """
         # try to get page boundaries from the navigation component
         # XXX we should probably not have a ref to this component here (eg in
-        #     cubicweb.common)
+        #     cubicweb)
         nav = self.vreg['components'].select_or_none('navigation', self.req,
                                                      rset=self)
         if nav:
--- a/server/sqlutils.py	Tue Dec 08 10:40:20 2009 +0100
+++ b/server/sqlutils.py	Tue Dec 08 10:58:56 2009 +0100
@@ -22,7 +22,7 @@
 
 from cubicweb import Binary, ConfigurationError
 from cubicweb.utils import todate, todatetime
-from cubicweb.common.uilib import remove_html_tags
+from cubicweb.uilib import remove_html_tags
 from cubicweb.toolsutils import restrict_perms_to_user
 from cubicweb.schema import PURE_VIRTUAL_RTYPES
 from cubicweb.server import SQL_CONNECT_HOOKS
--- a/sobjects/notification.py	Tue Dec 08 10:40:20 2009 +0100
+++ b/sobjects/notification.py	Tue Dec 08 10:58:56 2009 +0100
@@ -15,7 +15,7 @@
 
 from cubicweb.selectors import yes
 from cubicweb.view import Component
-from cubicweb.common.mail import NotificationView, SkipEmail
+from cubicweb.mail import NotificationView, SkipEmail
 from cubicweb.server.hook import SendMailOp
 
 
@@ -185,9 +185,9 @@
 
 from logilab.common.deprecation import class_renamed, class_moved, deprecated
 from cubicweb.hooks.notification import RenderAndSendNotificationView
-from cubicweb.common.mail import parse_message_id
+from cubicweb.mail import parse_message_id
 
 NormalizedTextView = class_renamed('NormalizedTextView', ContentAddedView)
 RenderAndSendNotificationView = class_moved(RenderAndSendNotificationView)
-parse_message_id = deprecated('parse_message_id is now defined in cubicweb.common.mail')(parse_message_id)
+parse_message_id = deprecated('parse_message_id is now defined in cubicweb.mail')(parse_message_id)
 
--- a/sobjects/supervising.py	Tue Dec 08 10:40:20 2009 +0100
+++ b/sobjects/supervising.py	Tue Dec 08 10:58:56 2009 +0100
@@ -12,7 +12,7 @@
 from cubicweb.selectors import none_rset
 from cubicweb.schema import display_name
 from cubicweb.view import Component
-from cubicweb.common.mail import format_mail
+from cubicweb.mail import format_mail
 from cubicweb.server.hook import SendMailOp
 
 
--- a/sobjects/test/unittest_notification.py	Tue Dec 08 10:40:20 2009 +0100
+++ b/sobjects/test/unittest_notification.py	Tue Dec 08 10:58:56 2009 +0100
@@ -11,7 +11,7 @@
 from logilab.common.testlib import unittest_main, TestCase
 from cubicweb.devtools.testlib import CubicWebTC, MAILBOX
 
-from cubicweb.common.mail import construct_message_id, parse_message_id
+from cubicweb.mail import construct_message_id, parse_message_id
 
 class MessageIdTC(TestCase):
     def test_base(self):
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tags.py	Tue Dec 08 10:58:56 2009 +0100
@@ -0,0 +1,49 @@
+"""helper classes to generate simple (X)HTML tags
+
+:organization: Logilab
+:copyright: 2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
+:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
+"""
+__docformat__ = "restructuredtext en"
+
+from cubicweb.uilib import simple_sgml_tag, sgml_attributes
+
+class tag(object):
+    def __init__(self, name, escapecontent=True):
+        self.name = name
+        self.escapecontent = escapecontent
+
+    def __call__(self, __content=None, **attrs):
+        attrs.setdefault('escapecontent', self.escapecontent)
+        return simple_sgml_tag(self.name, __content, **attrs)
+
+button = tag('button')
+input = tag('input')
+textarea = tag('textarea')
+a = tag('a')
+span = tag('span')
+div = tag('div', False)
+img = tag('img')
+label = tag('label')
+option = tag('option')
+h1 = tag('h1')
+h2 = tag('h2')
+h3 = tag('h3')
+h4 = tag('h4')
+h5 = tag('h5')
+tr = tag('tr')
+th = tag('th')
+td = tag('td')
+
+def select(name, id=None, multiple=False, options=[], **attrs):
+    if multiple:
+        attrs['multiple'] = 'multiple'
+    if id:
+        attrs['id'] = id
+    attrs['name'] = name
+    html = [u'<select %s>' % sgml_attributes(attrs)]
+    html += options
+    html.append(u'</select>')
+    return u'\n'.join(html)
+
--- a/test/unittest_entity.py	Tue Dec 08 10:40:20 2009 +0100
+++ b/test/unittest_entity.py	Tue Dec 08 10:58:56 2009 +0100
@@ -11,7 +11,7 @@
 
 from cubicweb import Binary, Unauthorized
 from cubicweb.devtools.testlib import CubicWebTC
-from cubicweb.common.mttransforms import HAS_TAL
+from cubicweb.mttransforms import HAS_TAL
 from cubicweb.entities import fetch_config
 
 class EntityTC(CubicWebTC):
@@ -327,7 +327,7 @@
     def test_printable_value_bytes(self):
         e = self.add_entity('File', data=Binary('lambda x: 1'), data_format=u'text/x-python',
                             data_encoding=u'ascii', data_name=u'toto.py')
-        from cubicweb.common import mttransforms
+        from cubicweb import mttransforms
         if mttransforms.HAS_PYGMENTS_TRANSFORMS:
             self.assertEquals(e.printable_value('data'),
                               '''<div class="highlight"><pre><span class="k">lambda</span> <span class="n">x</span><span class="p">:</span> <span class="mi">1</span>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/unittest_mail.py	Tue Dec 08 10:58:56 2009 +0100
@@ -0,0 +1,132 @@
+# -*- coding: utf-8 -*-
+"""unit tests for module cubicweb.mail
+
+:organization: Logilab
+:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
+:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
+"""
+
+import os
+import sys
+
+from logilab.common.testlib import unittest_main
+from logilab.common.umessage import message_from_string
+
+from cubicweb.devtools.testlib import CubicWebTC
+from cubicweb.mail import format_mail
+
+
+def getlogin():
+    """avoid usinng os.getlogin() because of strange tty / stdin problems
+    (man 3 getlogin)
+    Another solution would be to use $LOGNAME, $USER or $USERNAME
+    """
+    if sys.platform != 'win32':
+        import pwd
+        return pwd.getpwuid(os.getuid())[0]
+    else:
+        return os.environ.get('USERNAME')
+
+
+class EmailTC(CubicWebTC):
+
+    def test_format_mail(self):
+        self.set_option('sender-addr', 'bim@boum.fr')
+        self.set_option('sender-name', 'BimBam')
+
+        mail = format_mail({'name': 'oim', 'email': 'oim@logilab.fr'},
+                           ['test@logilab.fr'], u'un petit cöucou', u'bïjour',
+                           config=self.config)
+        self.assertLinesEquals(mail.as_string(), """\
+MIME-Version: 1.0
+Content-Type: text/plain; charset="utf-8"
+Content-Transfer-Encoding: base64
+Subject: =?utf-8?q?b=C3=AFjour?=
+From: =?utf-8?q?oim?= <oim@logilab.fr>
+Reply-to: =?utf-8?q?oim?= <oim@logilab.fr>, =?utf-8?q?BimBam?= <bim@boum.fr>
+X-CW: data
+To: test@logilab.fr
+
+dW4gcGV0aXQgY8O2dWNvdQ==
+""")
+        msg = message_from_string(mail.as_string())
+        self.assertEquals(msg.get('subject'), u'bïjour')
+        self.assertEquals(msg.get('from'), u'oim <oim@logilab.fr>')
+        self.assertEquals(msg.get('to'), u'test@logilab.fr')
+        self.assertEquals(msg.get('reply-to'), u'oim <oim@logilab.fr>, BimBam <bim@boum.fr>')
+        self.assertEquals(msg.get_payload(decode=True), u'un petit cöucou')
+
+
+    def test_format_mail_euro(self):
+        mail = format_mail({'name': u'oîm', 'email': u'oim@logilab.fr'},
+                           ['test@logilab.fr'], u'un petit cöucou €', u'bïjour €')
+        self.assertLinesEquals(mail.as_string(), """\
+MIME-Version: 1.0
+Content-Type: text/plain; charset="utf-8"
+Content-Transfer-Encoding: base64
+Subject: =?utf-8?b?YsOvam91ciDigqw=?=
+From: =?utf-8?q?o=C3=AEm?= <oim@logilab.fr>
+Reply-to: =?utf-8?q?o=C3=AEm?= <oim@logilab.fr>
+To: test@logilab.fr
+
+dW4gcGV0aXQgY8O2dWNvdSDigqw=
+""")
+        msg = message_from_string(mail.as_string())
+        self.assertEquals(msg.get('subject'), u'bïjour €')
+        self.assertEquals(msg.get('from'), u'oîm <oim@logilab.fr>')
+        self.assertEquals(msg.get('to'), u'test@logilab.fr')
+        self.assertEquals(msg.get('reply-to'), u'oîm <oim@logilab.fr>')
+        self.assertEquals(msg.get_payload(decode=True), u'un petit cöucou €')
+
+
+    def test_format_mail_from_reply_to(self):
+        # no sender-name, sender-addr in the configuration
+        self.set_option('sender-name', '')
+        self.set_option('sender-addr', '')
+        msg = format_mail({'name': u'', 'email': u''},
+                          ['test@logilab.fr'], u'un petit cöucou €', u'bïjour €',
+                          config=self.config)
+        self.assertEquals(msg.get('from'), u'')
+        self.assertEquals(msg.get('reply-to'), None)
+        msg = format_mail({'name': u'tutu', 'email': u'tutu@logilab.fr'},
+                          ['test@logilab.fr'], u'un petit cöucou €', u'bïjour €',
+                          config=self.config)
+        msg = message_from_string(msg.as_string())
+        self.assertEquals(msg.get('from'), u'tutu <tutu@logilab.fr>')
+        self.assertEquals(msg.get('reply-to'), u'tutu <tutu@logilab.fr>')
+        msg = format_mail({'name': u'tutu', 'email': u'tutu@logilab.fr'},
+                          ['test@logilab.fr'], u'un petit cöucou €', u'bïjour €')
+        msg = message_from_string(msg.as_string())
+        self.assertEquals(msg.get('from'), u'tutu <tutu@logilab.fr>')
+        self.assertEquals(msg.get('reply-to'), u'tutu <tutu@logilab.fr>')
+        # set sender name and address as expected
+        self.set_option('sender-name', 'cubicweb-test')
+        self.set_option('sender-addr', 'cubicweb-test@logilab.fr')
+        # anonymous notification: no name and no email specified
+        msg = format_mail({'name': u'', 'email': u''},
+                           ['test@logilab.fr'], u'un petit cöucou €', u'bïjour €',
+                           config=self.config)
+        msg = message_from_string(msg.as_string())
+        self.assertEquals(msg.get('from'), u'cubicweb-test <cubicweb-test@logilab.fr>')
+        self.assertEquals(msg.get('reply-to'), u'cubicweb-test <cubicweb-test@logilab.fr>')
+        # anonymous notification: only email specified
+        msg = format_mail({'email': u'tutu@logilab.fr'},
+                           ['test@logilab.fr'], u'un petit cöucou €', u'bïjour €',
+                           config=self.config)
+        msg = message_from_string(msg.as_string())
+        self.assertEquals(msg.get('from'), u'cubicweb-test <tutu@logilab.fr>')
+        self.assertEquals(msg.get('reply-to'), u'cubicweb-test <tutu@logilab.fr>, cubicweb-test <cubicweb-test@logilab.fr>')
+        # anonymous notification: only name specified
+        msg = format_mail({'name': u'tutu'},
+                          ['test@logilab.fr'], u'un petit cöucou €', u'bïjour €',
+                          config=self.config)
+        msg = message_from_string(msg.as_string())
+        self.assertEquals(msg.get('from'), u'tutu <cubicweb-test@logilab.fr>')
+        self.assertEquals(msg.get('reply-to'), u'tutu <cubicweb-test@logilab.fr>')
+
+
+
+if __name__ == '__main__':
+    unittest_main()
+
--- a/test/unittest_migration.py	Tue Dec 08 10:40:20 2009 +0100
+++ b/test/unittest_migration.py	Tue Dec 08 10:58:56 2009 +0100
@@ -1,4 +1,4 @@
-"""cubicweb.common.migration unit tests
+"""cubicweb.migration unit tests
 
 :organization: Logilab
 :copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
--- a/test/unittest_rset.py	Tue Dec 08 10:40:20 2009 +0100
+++ b/test/unittest_rset.py	Tue Dec 08 10:58:56 2009 +0100
@@ -1,5 +1,5 @@
 # coding: utf-8
-"""unit tests for module cubicweb.common.utils
+"""unit tests for module cubicweb.utils
 
 :organization: Logilab
 :copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/unittest_uilib.py	Tue Dec 08 10:58:56 2009 +0100
@@ -0,0 +1,86 @@
+# -*- coding: utf-8 -*-
+"""unittests for cubicweb.uilib
+
+:organization: Logilab
+:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
+:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
+"""
+
+__docformat__ = "restructuredtext en"
+
+from logilab.common.testlib import TestCase, unittest_main
+from logilab.common.tree import Node
+
+from cubicweb import uilib
+
+class UILIBTC(TestCase):
+
+    def test_remove_tags(self):
+        """make sure remove_tags remove all tags"""
+        data = [
+            ('<h1>Hello</h1>', 'Hello'),
+            ('<h1>Hello <a href="foo/bar"><b>s</b>pam</a></h1>', 'Hello spam'),
+            ('<br>Hello<img src="doh.png"/>', 'Hello'),
+            ('<p></p>', ''),
+            ]
+        for text, expected in data:
+            got = uilib.remove_html_tags(text)
+            self.assertEquals(got, expected)
+
+    def test_fallback_safe_cut(self):
+        self.assertEquals(uilib.fallback_safe_cut(u'ab <a href="hello">cd</a>', 4), u'ab c...')
+        self.assertEquals(uilib.fallback_safe_cut(u'ab <a href="hello">cd</a>', 5), u'ab <a href="hello">cd</a>')
+        self.assertEquals(uilib.fallback_safe_cut(u'ab <a href="hello">&amp;d</a>', 4), u'ab &amp;...')
+        self.assertEquals(uilib.fallback_safe_cut(u'ab <a href="hello">&amp;d</a> ef', 5), u'ab &amp;d...')
+        self.assertEquals(uilib.fallback_safe_cut(u'ab <a href="hello">&igrave;d</a>', 4), u'ab ì...')
+        self.assertEquals(uilib.fallback_safe_cut(u'&amp; <a href="hello">&amp;d</a> ef', 4), u'&amp; &amp;d...')
+
+    def test_lxml_safe_cut(self):
+        self.assertEquals(uilib.safe_cut(u'aaa<div>aaad</div> ef', 4), u'<p>aaa</p><div>a...</div>')
+        self.assertEquals(uilib.safe_cut(u'aaa<div>aaad</div> ef', 7), u'<p>aaa</p><div>aaad</div>...')
+        self.assertEquals(uilib.safe_cut(u'aaa<div>aaad</div>', 7), u'<p>aaa</p><div>aaad</div>')
+        # Missing ellipsis due to space management but we don't care
+        self.assertEquals(uilib.safe_cut(u'ab <a href="hello">&amp;d</a>', 4), u'<p>ab <a href="hello">&amp;...</a></p>')
+
+    def test_cut(self):
+        """tests uilib.cut() behaviour"""
+        data = [
+            ('hello', 'hello'),
+            ('hello world', 'hello wo...'),
+            ("hell<b>O'</b> world", "hell<b>O..."),
+            ]
+        for text, expected in data:
+            got = uilib.cut(text, 8)
+            self.assertEquals(got, expected)
+
+    def test_text_cut(self):
+        """tests uilib.text_cut() behaviour with no text"""
+        data = [('',''),
+                ("""Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
+tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
+quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
+consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
+cillum dolore eu fugiat nulla pariatur.""",
+                 "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod \
+tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, \
+quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo \
+consequat."),
+                ("""Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
+tempor incididunt ut labore et dolore magna aliqua Ut enim ad minim veniam,
+quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
+consequat Duis aute irure dolor in reprehenderit in voluptate velit esse
+cillum dolore eu fugiat nulla pariatur Excepteur sint occaecat cupidatat non
+proident, sunt in culpa qui officia deserunt mollit anim id est laborum
+""",
+                 "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod \
+tempor incididunt ut labore et dolore magna aliqua Ut enim ad minim veniam, \
+quis nostrud exercitation ullamco laboris nisi"),
+                ]
+        for text, expected in data:
+            got = uilib.text_cut(text, 30)
+            self.assertEquals(got, expected)
+
+if __name__ == '__main__':
+    unittest_main()
+
--- a/test/unittest_utils.py	Tue Dec 08 10:40:20 2009 +0100
+++ b/test/unittest_utils.py	Tue Dec 08 10:58:56 2009 +0100
@@ -1,4 +1,4 @@
-"""unit tests for module cubicweb.common.utils
+"""unit tests for module cubicweb.utils
 
 :organization: Logilab
 :copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/uilib.py	Tue Dec 08 10:58:56 2009 +0100
@@ -0,0 +1,384 @@
+# -*- coding: utf-8 -*-
+"""user interface libraries
+
+contains some functions designed to help implementation of cubicweb user interface
+
+:organization: Logilab
+:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
+:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
+"""
+__docformat__ = "restructuredtext en"
+
+import csv
+import re
+from StringIO import StringIO
+
+from logilab.mtconverter import xml_escape, html_unescape
+
+from cubicweb.utils import ustrftime
+
+def rql_for_eid(eid):
+    """return the rql query necessary to fetch entity with the given eid.  This
+    function should only be used to generate link with rql inside, not to give
+    to cursor.execute (in which case you won't benefit from rql cache).
+
+    :Parameters:
+      - `eid`: the eid of the entity we should search
+    :rtype: str
+    :return: the rql query
+    """
+    return 'Any X WHERE X eid %s' % eid
+
+
+def printable_value(req, attrtype, value, props=None, displaytime=True):
+    """return a displayable value (i.e. unicode string)"""
+    if value is None or attrtype == 'Bytes':
+        return u''
+    if attrtype == 'String':
+        # don't translate empty value if you don't want strange results
+        if props is not None and value and props.get('internationalizable'):
+            return req._(value)
+
+        return value
+    if attrtype == 'Date':
+        return ustrftime(value, req.property_value('ui.date-format'))
+    if attrtype == 'Time':
+        return ustrftime(value, req.property_value('ui.time-format'))
+    if attrtype == 'Datetime':
+        if displaytime:
+            return ustrftime(value, req.property_value('ui.datetime-format'))
+        return ustrftime(value, req.property_value('ui.date-format'))
+    if attrtype == 'Boolean':
+        if value:
+            return req._('yes')
+        return req._('no')
+    if attrtype == 'Float':
+        value = req.property_value('ui.float-format') % value
+    return unicode(value)
+
+
+# text publishing #############################################################
+
+try:
+    from cubicweb.ext.rest import rest_publish # pylint: disable-msg=W0611
+except ImportError:
+    def rest_publish(entity, data):
+        """default behaviour if docutils was not found"""
+        return xml_escape(data)
+
+TAG_PROG = re.compile(r'</?.*?>', re.U)
+def remove_html_tags(text):
+    """Removes HTML tags from text
+
+    >>> remove_html_tags('<td>hi <a href="http://www.google.fr">world</a></td>')
+    'hi world'
+    >>>
+    """
+    return TAG_PROG.sub('', text)
+
+
+REF_PROG = re.compile(r"<ref\s+rql=([\'\"])([^\1]*?)\1\s*>([^<]*)</ref>", re.U)
+def _subst_rql(view, obj):
+    delim, rql, descr = obj.groups()
+    return u'<a href="%s">%s</a>' % (view._cw.build_url(rql=rql), descr)
+
+def html_publish(view, text):
+    """replace <ref rql=''> links by <a href="...">"""
+    if not text:
+        return u''
+    return REF_PROG.sub(lambda obj, view=view:_subst_rql(view, obj), text)
+
+# fallback implementation, nicer one defined below if lxml is available
+def soup2xhtml(data, encoding):
+    # normalize line break
+    # see http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1
+    return u'\n'.join(data.splitlines())
+
+# fallback implementation, nicer one defined below if lxml> 2.0 is available
+def safe_cut(text, length):
+    """returns a string of length <length> based on <text>, removing any html
+    tags from given text if cut is necessary."""
+    if text is None:
+        return u''
+    noenttext = html_unescape(text)
+    text_nohtml = remove_html_tags(noenttext)
+    # try to keep html tags if text is short enough
+    if len(text_nohtml) <= length:
+        return text
+    # else if un-tagged text is too long, cut it
+    return xml_escape(text_nohtml[:length] + u'...')
+
+fallback_safe_cut = safe_cut
+
+
+try:
+    from lxml import etree
+except (ImportError, AttributeError):
+    # gae environment: lxml not available
+    pass
+else:
+
+    def soup2xhtml(data, encoding):
+        """tidy (at least try) html soup and return the result
+        Note: the function considers a string with no surrounding tag as valid
+              if <div>`data`</div> can be parsed by an XML parser
+        """
+        # normalize line break
+        # see http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1
+        data = u'\n'.join(data.splitlines())
+        # XXX lxml 1.1 support still needed ?
+        xmltree = etree.HTML('<div>%s</div>' % data)
+        # NOTE: lxml 1.1 (etch platforms) doesn't recognize
+        #       the encoding=unicode parameter (lxml 2.0 does), this is
+        #       why we specify an encoding and re-decode to unicode later
+        body = etree.tostring(xmltree[0], encoding=encoding)
+        # remove <body> and </body> and decode to unicode
+        return body[11:-13].decode(encoding)
+
+    if hasattr(etree.HTML('<div>test</div>'), 'iter'):
+
+        def safe_cut(text, length):
+            """returns an html document of length <length> based on <text>,
+            and cut is necessary.
+            """
+            if text is None:
+                return u''
+            dom = etree.HTML(text)
+            curlength = 0
+            add_ellipsis = False
+            for element in dom.iter():
+                if curlength >= length:
+                    parent = element.getparent()
+                    parent.remove(element)
+                    if curlength == length and (element.text or element.tail):
+                        add_ellipsis = True
+                else:
+                    if element.text is not None:
+                        element.text = cut(element.text, length - curlength)
+                        curlength += len(element.text)
+                    if element.tail is not None:
+                        if curlength < length:
+                            element.tail = cut(element.tail, length - curlength)
+                            curlength += len(element.tail)
+                        elif curlength == length:
+                            element.tail = '...'
+                        else:
+                            element.tail = ''
+            text = etree.tounicode(dom[0])[6:-7] # remove wrapping <body></body>
+            if add_ellipsis:
+                return text + u'...'
+            return text
+
+def text_cut(text, nbwords=30, gotoperiod=True):
+    """from the given plain text, return a text with at least <nbwords> words,
+    trying to go to the end of the current sentence.
+
+    :param nbwords: the minimum number of words required
+    :param gotoperiod: specifies if the function should try to go to
+                       the first period after the cut (i.e. finish
+                       the sentence if possible)
+
+    Note that spaces are normalized.
+    """
+    if text is None:
+        return u''
+    words = text.split()
+    text = u' '.join(words) # normalize spaces
+    textlength = minlength = len(' '.join(words[:nbwords]))
+    if gotoperiod:
+        textlength = text.find('.', minlength) + 1
+        if textlength == 0: # no period found
+            textlength = minlength
+    return text[:textlength]
+
+def cut(text, length):
+    """returns a string of a maximum length <length> based on <text>
+    (approximatively, since if text has been  cut, '...' is added to the end of the string,
+    resulting in a string of len <length> + 3)
+    """
+    if text is None:
+        return u''
+    if len(text) <= length:
+        return text
+    # else if un-tagged text is too long, cut it
+    return text[:length] + u'...'
+
+
+
+# HTML generation helper functions ############################################
+
+HTML4_EMPTY_TAGS = frozenset(('base', 'meta', 'link', 'hr', 'br', 'param',
+                              'img', 'area', 'input', 'col'))
+
+def sgml_attributes(attrs):
+    return u' '.join(u'%s="%s"' % (attr, xml_escape(unicode(value)))
+                     for attr, value in sorted(attrs.items())
+                     if value is not None)
+
+def simple_sgml_tag(tag, content=None, escapecontent=True, **attrs):
+    """generation of a simple sgml tag (eg without children tags) easier
+
+    content and attri butes will be escaped
+    """
+    value = u'<%s' % tag
+    if attrs:
+        try:
+            attrs['class'] = attrs.pop('klass')
+        except KeyError:
+            pass
+        value += u' ' + sgml_attributes(attrs)
+    if content:
+        if escapecontent:
+            content = xml_escape(unicode(content))
+        value += u'>%s</%s>' % (content, tag)
+    else:
+        if tag in HTML4_EMPTY_TAGS:
+            value += u' />'
+        else:
+            value += u'></%s>' % tag
+    return value
+
+def tooltipize(text, tooltip, url=None):
+    """make an HTML tooltip"""
+    url = url or '#'
+    return u'<a href="%s" title="%s">%s</a>' % (url, tooltip, text)
+
+def toggle_action(nodeid):
+    """builds a HTML link that uses the js toggleVisibility function"""
+    return u"javascript: toggleVisibility('%s')" % nodeid
+
+def toggle_link(nodeid, label):
+    """builds a HTML link that uses the js toggleVisibility function"""
+    return u'<a href="%s">%s</a>' % (toggle_action(nodeid), label)
+
+
+def ureport_as_html(layout):
+    from logilab.common.ureports import HTMLWriter
+    formater = HTMLWriter(True)
+    stream = StringIO() #UStringIO() don't want unicode assertion
+    formater.format(layout, stream)
+    res = stream.getvalue()
+    if isinstance(res, str):
+        res = unicode(res, 'UTF8')
+    return res
+
+# traceback formatting ########################################################
+
+import traceback
+
+def rest_traceback(info, exception):
+    """return a ReST formated traceback"""
+    res = [u'Traceback\n---------\n::\n']
+    for stackentry in traceback.extract_tb(info[2]):
+        res.append(u'\tFile %s, line %s, function %s' % tuple(stackentry[:3]))
+        if stackentry[3]:
+            res.append(u'\t  %s' % stackentry[3].decode('utf-8', 'replace'))
+    res.append(u'\n')
+    try:
+        res.append(u'\t Error: %s\n' % exception)
+    except:
+        pass
+    return u'\n'.join(res)
+
+
+def html_traceback(info, exception, title='',
+                   encoding='ISO-8859-1', body=''):
+    """ return an html formatted traceback from python exception infos.
+    """
+    tcbk = info[2]
+    stacktb = traceback.extract_tb(tcbk)
+    strings = []
+    if body:
+        strings.append(u'<div class="error_body">')
+        # FIXME
+        strings.append(body)
+        strings.append(u'</div>')
+    if title:
+        strings.append(u'<h1 class="error">%s</h1>'% xml_escape(title))
+    try:
+        strings.append(u'<p class="error">%s</p>' % xml_escape(str(exception)).replace("\n","<br />"))
+    except UnicodeError:
+        pass
+    strings.append(u'<div class="error_traceback">')
+    for index, stackentry in enumerate(stacktb):
+        strings.append(u'<b>File</b> <b class="file">%s</b>, <b>line</b> '
+                       u'<b class="line">%s</b>, <b>function</b> '
+                       u'<b class="function">%s</b>:<br/>'%(
+            xml_escape(stackentry[0]), stackentry[1], xml_escape(stackentry[2])))
+        if stackentry[3]:
+            string = xml_escape(stackentry[3]).decode('utf-8', 'replace')
+            strings.append(u'&#160;&#160;%s<br/>\n' % (string))
+        # add locals info for each entry
+        try:
+            local_context = tcbk.tb_frame.f_locals
+            html_info = []
+            chars = 0
+            for name, value in local_context.iteritems():
+                value = xml_escape(repr(value))
+                info = u'<span class="name">%s</span>=%s, ' % (name, value)
+                line_length = len(name) + len(value)
+                chars += line_length
+                # 150 is the result of *years* of research ;-) (CSS might be helpful here)
+                if chars > 150:
+                    info = u'<br/>' + info
+                    chars = line_length
+                html_info.append(info)
+            boxid = 'ctxlevel%d' % index
+            strings.append(u'[%s]' % toggle_link(boxid, '+'))
+            strings.append(u'<div id="%s" class="pycontext hidden">%s</div>' %
+                           (boxid, ''.join(html_info)))
+            tcbk = tcbk.tb_next
+        except Exception:
+            pass # doesn't really matter if we have no context info
+    strings.append(u'</div>')
+    return '\n'.join(strings)
+
+# csv files / unicode support #################################################
+
+class UnicodeCSVWriter:
+    """proxies calls to csv.writer.writerow to be able to deal with unicode"""
+
+    def __init__(self, wfunc, encoding, **kwargs):
+        self.writer = csv.writer(self, **kwargs)
+        self.wfunc = wfunc
+        self.encoding = encoding
+
+    def write(self, data):
+        self.wfunc(data)
+
+    def writerow(self, row):
+        csvrow = []
+        for elt in row:
+            if isinstance(elt, unicode):
+                csvrow.append(elt.encode(self.encoding))
+            else:
+                csvrow.append(str(elt))
+        self.writer.writerow(csvrow)
+
+    def writerows(self, rows):
+        for row in rows:
+            self.writerow(row)
+
+
+# some decorators #############################################################
+
+class limitsize(object):
+    def __init__(self, maxsize):
+        self.maxsize = maxsize
+
+    def __call__(self, function):
+        def newfunc(*args, **kwargs):
+            ret = function(*args, **kwargs)
+            if isinstance(ret, basestring):
+                return ret[:self.maxsize]
+            return ret
+        return newfunc
+
+
+def htmlescape(function):
+    def newfunc(*args, **kwargs):
+        ret = function(*args, **kwargs)
+        assert isinstance(ret, basestring)
+        return xml_escape(ret)
+    return newfunc
--- a/web/form.py	Tue Dec 08 10:40:20 2009 +0100
+++ b/web/form.py	Tue Dec 08 10:58:56 2009 +0100
@@ -11,7 +11,7 @@
 
 from cubicweb.appobject import AppObject
 from cubicweb.view import NOINDEX, NOFOLLOW
-from cubicweb.common import tags
+from cubicweb import tags
 from cubicweb.web import stdmsgs, httpcache, formfields
 
 
--- a/web/formfields.py	Tue Dec 08 10:40:20 2009 +0100
+++ b/web/formfields.py	Tue Dec 08 10:58:56 2009 +0100
@@ -15,7 +15,7 @@
                               FormatConstraint)
 
 from cubicweb.utils import ustrftime
-from cubicweb.common import tags, uilib
+from cubicweb import tags, uilib
 from cubicweb.web import INTERNAL_FIELD_VALUE
 from cubicweb.web.formwidgets import (
     HiddenInput, TextInput, FileInput, PasswordInput, TextArea, FCKEditor,
--- a/web/formwidgets.py	Tue Dec 08 10:40:20 2009 +0100
+++ b/web/formwidgets.py	Tue Dec 08 10:58:56 2009 +0100
@@ -10,7 +10,7 @@
 from datetime import date
 from warnings import warn
 
-from cubicweb.common import tags, uilib
+from cubicweb import tags, uilib
 from cubicweb.web import stdmsgs, INTERNAL_FIELD_VALUE, ProcessFormError
 
 from logilab.mtconverter import xml_escape
@@ -334,7 +334,7 @@
     @classmethod
     def add_localized_infos(cls, req):
         """inserts JS variables defining localized months and days"""
-        # import here to avoid dependancy from cubicweb-common to simplejson
+        # import here to avoid dependancy from cubicweb to simplejson
         _ = req._
         monthnames = [_(mname) for mname in cls.monthnames]
         daynames = [_(dname) for dname in cls.daynames]
--- a/web/htmlwidgets.py	Tue Dec 08 10:40:20 2009 +0100
+++ b/web/htmlwidgets.py	Tue Dec 08 10:58:56 2009 +0100
@@ -1,6 +1,6 @@
 """html widgets
 
-those are in cubicweb.common since we need to know available widgets at schema
+those are in cubicweb since we need to know available widgets at schema
 serialization time
 
 :organization: Logilab
@@ -12,7 +12,7 @@
 from logilab.mtconverter import xml_escape
 
 from cubicweb.utils import UStringIO
-from cubicweb.common.uilib import toggle_action, limitsize, htmlescape
+from cubicweb.uilib import toggle_action, limitsize, htmlescape
 from cubicweb.web import jsonize
 
 # XXX HTMLWidgets should have access to req (for datadir / static urls,
--- a/web/request.py	Tue Dec 08 10:40:20 2009 +0100
+++ b/web/request.py	Tue Dec 08 10:58:56 2009 +0100
@@ -25,8 +25,8 @@
 from logilab.mtconverter import xml_escape
 
 from cubicweb.dbapi import DBAPIRequest
-from cubicweb.common.mail import header
-from cubicweb.common.uilib import remove_html_tags
+from cubicweb.mail import header
+from cubicweb.uilib import remove_html_tags
 from cubicweb.utils import SizeConstrainedList, HTMLHead, make_uid
 from cubicweb.view import STRICT_DOCTYPE, TRANSITIONAL_DOCTYPE_NOEXT
 from cubicweb.web import (INTERNAL_FIELD_VALUE, LOGGER, NothingToEdit,
--- a/web/test/unittest_views_basecontrollers.py	Tue Dec 08 10:40:20 2009 +0100
+++ b/web/test/unittest_views_basecontrollers.py	Tue Dec 08 10:58:56 2009 +0100
@@ -12,7 +12,7 @@
 from cubicweb import Binary, NoSelectableObject, ValidationError
 from cubicweb.view import STRICT_DOCTYPE
 from cubicweb.devtools.testlib import CubicWebTC
-from cubicweb.common.uilib import rql_for_eid
+from cubicweb.uilib import rql_for_eid
 from cubicweb.web import INTERNAL_FIELD_VALUE, Redirect, RequestError
 from cubicweb.entities.authobjs import CWUser
 
--- a/web/views/basecomponents.py	Tue Dec 08 10:40:20 2009 +0100
+++ b/web/views/basecomponents.py	Tue Dec 08 10:58:56 2009 +0100
@@ -17,7 +17,7 @@
 
 from cubicweb.selectors import yes, two_etypes_rset, match_form_params
 from cubicweb.schema import display_name
-from cubicweb.common.uilib import toggle_action
+from cubicweb.uilib import toggle_action
 from cubicweb.web import component
 from cubicweb.web.htmlwidgets import (MenuWidget, PopupBoxMenu, BoxSeparator,
                                       BoxLink)
--- a/web/views/basecontrollers.py	Tue Dec 08 10:40:20 2009 +0100
+++ b/web/views/basecontrollers.py	Tue Dec 08 10:58:56 2009 +0100
@@ -19,7 +19,7 @@
 from cubicweb import NoSelectableObject, ValidationError, ObjectNotFound, typed_eid
 from cubicweb.utils import strptime, CubicWebJsonEncoder
 from cubicweb.selectors import yes, match_user_groups
-from cubicweb.common.mail import format_mail
+from cubicweb.mail import format_mail
 from cubicweb.web import ExplicitLogin, Redirect, RemoteCallFailed, json_dumps
 from cubicweb.web.controller import Controller
 from cubicweb.web.views import vid_from_rset
--- a/web/views/baseviews.py	Tue Dec 08 10:40:20 2009 +0100
+++ b/web/views/baseviews.py	Tue Dec 08 10:58:56 2009 +0100
@@ -25,7 +25,7 @@
 from cubicweb.selectors import yes, empty_rset, one_etype_rset
 from cubicweb.schema import display_name
 from cubicweb.view import EntityView, AnyRsetView, View
-from cubicweb.common.uilib import cut, printable_value
+from cubicweb.uilib import cut, printable_value
 
 
 class NullView(AnyRsetView):
--- a/web/views/csvexport.py	Tue Dec 08 10:40:20 2009 +0100
+++ b/web/views/csvexport.py	Tue Dec 08 10:58:56 2009 +0100
@@ -8,7 +8,7 @@
 __docformat__ = "restructuredtext en"
 
 from cubicweb.schema import display_name
-from cubicweb.common.uilib import UnicodeCSVWriter
+from cubicweb.uilib import UnicodeCSVWriter
 from cubicweb.view import EntityView, AnyRsetView
 
 class CSVMixIn(object):
--- a/web/views/editforms.py	Tue Dec 08 10:40:20 2009 +0100
+++ b/web/views/editforms.py	Tue Dec 08 10:58:56 2009 +0100
@@ -20,7 +20,7 @@
 from cubicweb.selectors import (match_kwargs, one_line_rset, non_final_entity,
                                 specified_etype_implements, yes)
 from cubicweb.view import EntityView
-from cubicweb.common import tags
+from cubicweb import tags
 from cubicweb.web import stdmsgs, eid_param
 from cubicweb.web import uicfg
 from cubicweb.web.form import FormViewMixIn, FieldNotFound
--- a/web/views/editviews.py	Tue Dec 08 10:40:20 2009 +0100
+++ b/web/views/editviews.py	Tue Dec 08 10:58:56 2009 +0100
@@ -17,7 +17,7 @@
 from cubicweb.view import EntityView
 from cubicweb.selectors import (one_line_rset, non_final_entity,
                                 match_search_state, match_form_params)
-from cubicweb.common.uilib import cut
+from cubicweb.uilib import cut
 from cubicweb.web.views import linksearch_select_url
 from cubicweb.web.views.editforms import relation_id
 from cubicweb.web.views.baseviews import FinalView
--- a/web/views/emailaddress.py	Tue Dec 08 10:40:20 2009 +0100
+++ b/web/views/emailaddress.py	Tue Dec 08 10:58:56 2009 +0100
@@ -11,7 +11,7 @@
 
 from cubicweb.schema import display_name
 from cubicweb.selectors import implements
-from cubicweb.common import Unauthorized
+from cubicweb import Unauthorized
 from cubicweb.web.views import baseviews, primary
 
 class EmailAddressPrimaryView(primary.PrimaryView):
--- a/web/views/embedding.py	Tue Dec 08 10:40:20 2009 +0100
+++ b/web/views/embedding.py	Tue Dec 08 10:58:56 2009 +0100
@@ -20,7 +20,7 @@
                                 match_search_state, implements)
 from cubicweb.interfaces import IEmbedable
 from cubicweb.view import NOINDEX, NOFOLLOW
-from cubicweb.common.uilib import soup2xhtml
+from cubicweb.uilib import soup2xhtml
 from cubicweb.web.controller import Controller
 from cubicweb.web.action import Action
 from cubicweb.web.views import basetemplates
--- a/web/views/formrenderers.py	Tue Dec 08 10:40:20 2009 +0100
+++ b/web/views/formrenderers.py	Tue Dec 08 10:58:56 2009 +0100
@@ -14,7 +14,7 @@
 
 from simplejson import dumps
 
-from cubicweb.common import tags
+from cubicweb import tags
 from cubicweb.appobject import AppObject
 from cubicweb.selectors import entity_implements, yes
 from cubicweb.web import eid_param, formwidgets as fwdgs
--- a/web/views/ibreadcrumbs.py	Tue Dec 08 10:40:20 2009 +0100
+++ b/web/views/ibreadcrumbs.py	Tue Dec 08 10:58:56 2009 +0100
@@ -16,7 +16,7 @@
 from cubicweb.view import EntityView, Component
 # don't use AnyEntity since this may cause bug with isinstance() due to reloading
 from cubicweb.entity import Entity
-from cubicweb.common import tags, uilib
+from cubicweb import tags, uilib
 
 
 class BreadCrumbEntityVComponent(Component):
--- a/web/views/idownloadable.py	Tue Dec 08 10:40:20 2009 +0100
+++ b/web/views/idownloadable.py	Tue Dec 08 10:58:56 2009 +0100
@@ -14,7 +14,7 @@
 from cubicweb.selectors import (one_line_rset, score_entity,
                                 implements, match_context_prop)
 from cubicweb.interfaces import IDownloadable
-from cubicweb.common.mttransforms import ENGINE
+from cubicweb.mttransforms import ENGINE
 from cubicweb.web.box import EntityBoxTemplate
 from cubicweb.web.views import primary, baseviews
 
--- a/web/views/management.py	Tue Dec 08 10:40:20 2009 +0100
+++ b/web/views/management.py	Tue Dec 08 10:58:56 2009 +0100
@@ -13,7 +13,7 @@
 
 from cubicweb.selectors import yes, none_rset, match_user_groups, authenticated_user
 from cubicweb.view import AnyRsetView, StartupView, EntityView
-from cubicweb.common.uilib import html_traceback, rest_traceback
+from cubicweb.uilib import html_traceback, rest_traceback
 from cubicweb.web import formwidgets as wdgs
 from cubicweb.web.formfields import guess_field
 
--- a/web/views/navigation.py	Tue Dec 08 10:40:20 2009 +0100
+++ b/web/views/navigation.py	Tue Dec 08 10:58:56 2009 +0100
@@ -17,7 +17,7 @@
 from cubicweb.selectors import (paginated_rset, sorted_rset,
                                 primary_view, match_context_prop,
                                 one_line_rset, implements)
-from cubicweb.common.uilib import cut
+from cubicweb.uilib import cut
 from cubicweb.web.component import EntityVComponent, NavigationComponent
 
 
--- a/web/views/schema.py	Tue Dec 08 10:40:20 2009 +0100
+++ b/web/views/schema.py	Tue Dec 08 10:58:56 2009 +0100
@@ -17,7 +17,7 @@
 from cubicweb.schema import META_RTYPES, SCHEMA_TYPES, SYSTEM_RTYPES
 from cubicweb.schemaviewer import SchemaViewer
 from cubicweb.view import EntityView, StartupView
-from cubicweb.common import tags, uilib
+from cubicweb import tags, uilib
 from cubicweb.web import action, facet, uicfg
 from cubicweb.web.views import TmpFileViewMixin
 from cubicweb.web.views import primary, baseviews, tabs, management
--- a/web/views/tableview.py	Tue Dec 08 10:40:20 2009 +0100
+++ b/web/views/tableview.py	Tue Dec 08 10:58:56 2009 +0100
@@ -15,8 +15,8 @@
 from cubicweb.selectors import nonempty_rset, match_form_params
 from cubicweb.utils import make_uid
 from cubicweb.view import EntityView, AnyRsetView
-from cubicweb.common import tags
-from cubicweb.common.uilib import toggle_action, limitsize, htmlescape
+from cubicweb import tags
+from cubicweb.uilib import toggle_action, limitsize, htmlescape
 from cubicweb.web import jsonize
 from cubicweb.web.htmlwidgets import (TableWidget, TableColumn, MenuWidget,
                                       PopupBoxMenu, BoxLink)
--- a/web/views/tabs.py	Tue Dec 08 10:40:20 2009 +0100
+++ b/web/views/tabs.py	Tue Dec 08 10:58:56 2009 +0100
@@ -13,7 +13,7 @@
 from cubicweb import NoSelectableObject, role
 from cubicweb.selectors import partial_has_related_entities
 from cubicweb.view import EntityView
-from cubicweb.common import tags, uilib
+from cubicweb import tags, uilib
 from cubicweb.utils import make_uid
 from cubicweb.web.views import primary
 
--- a/web/views/urlpublishing.py	Tue Dec 08 10:40:20 2009 +0100
+++ b/web/views/urlpublishing.py	Tue Dec 08 10:58:56 2009 +0100
@@ -75,7 +75,7 @@
         :type path: str
         :param path: the path of the resource to publish
 
-        :rtype: tuple(str, `cubicweb.common.utils.ResultSet` or None)
+        :rtype: tuple(str, `cubicweb.utils.ResultSet` or None)
         :return: the publishing method identifier and an optional result set
 
         :raise NotFound: if no handler is able to decode the given path
--- a/web/views/wdoc.py	Tue Dec 08 10:40:20 2009 +0100
+++ b/web/views/wdoc.py	Tue Dec 08 10:58:56 2009 +0100
@@ -18,7 +18,7 @@
 from cubicweb.selectors import match_form_params, yes
 from cubicweb.view import StartupView
 from cubicweb.utils import strptime, todate
-from cubicweb.common.uilib import rest_publish
+from cubicweb.uilib import rest_publish
 from cubicweb.web import NotFound, action
 _ = unicode
 
--- a/web/views/xmlrss.py	Tue Dec 08 10:40:20 2009 +0100
+++ b/web/views/xmlrss.py	Tue Dec 08 10:58:56 2009 +0100
@@ -14,7 +14,7 @@
 
 from cubicweb.selectors import non_final_entity, one_line_rset, appobject_selectable
 from cubicweb.view import EntityView, AnyRsetView, Component
-from cubicweb.common.uilib import simple_sgml_tag
+from cubicweb.uilib import simple_sgml_tag
 from cubicweb.web import httpcache, box