# HG changeset patch # User Sylvain Thénault # Date 1260266336 -3600 # Node ID eae23c40627adef2f256f101bdedbced3b10efaa # Parent 934e758a73ef9f89e4f35bb912de1e777e22c0d0 drop common subpackage diff -r 934e758a73ef -r eae23c40627a common/__init__.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) diff -r 934e758a73ef -r eae23c40627a common/mail.py --- 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×tamp=%.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 * diff -r 934e758a73ef -r eae23c40627a common/mixins.py --- 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'
  • %s
  • ' % 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'
  • \n' % entity.__regid__.lower()) - def close_item(self, entity): - self.w(u'
  • \n') - - -class TreePathMixIn(object): - """a recursive path view""" - __regid__ = 'path' - item_vid = 'oneline' - separator = u' > ' - - def call(self, **kwargs): - self.w(u'
    ') - super(TreePathMixIn, self).call(**kwargs) - self.w(u'
    ') - - 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'%s' % 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 * diff -r 934e758a73ef -r eae23c40627a common/mttransforms.py --- 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 * diff -r 934e758a73ef -r eae23c40627a common/tags.py --- 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'') - 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 * diff -r 934e758a73ef -r eae23c40627a common/test/data/bootstrap_cubes --- 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 @@ - diff -r 934e758a73ef -r eae23c40627a common/test/unittest_mail.py --- 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?= -Reply-to: =?utf-8?q?oim?= , =?utf-8?q?BimBam?= -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 ') - self.assertEquals(msg.get('to'), u'test@logilab.fr') - self.assertEquals(msg.get('reply-to'), u'oim , BimBam ') - 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?= -Reply-to: =?utf-8?q?o=C3=AEm?= -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 ') - self.assertEquals(msg.get('to'), u'test@logilab.fr') - self.assertEquals(msg.get('reply-to'), u'oîm ') - 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 ') - self.assertEquals(msg.get('reply-to'), u'tutu ') - 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 ') - self.assertEquals(msg.get('reply-to'), u'tutu ') - # 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 ') - self.assertEquals(msg.get('reply-to'), u'cubicweb-test ') - # 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 ') - self.assertEquals(msg.get('reply-to'), u'cubicweb-test , cubicweb-test ') - # 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 ') - self.assertEquals(msg.get('reply-to'), u'tutu ') - - - -if __name__ == '__main__': - unittest_main() - diff -r 934e758a73ef -r eae23c40627a common/test/unittest_uilib.py --- 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 = [ - ('

    Hello

    ', 'Hello'), - ('

    Hello spam

    ', 'Hello spam'), - ('
    Hello', 'Hello'), - ('

    ', ''), - ] - 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 cd', 4), u'ab c...') - self.assertEquals(uilib.fallback_safe_cut(u'ab cd', 5), u'ab cd') - self.assertEquals(uilib.fallback_safe_cut(u'ab &d', 4), u'ab &...') - self.assertEquals(uilib.fallback_safe_cut(u'ab &d ef', 5), u'ab &d...') - self.assertEquals(uilib.fallback_safe_cut(u'ab ìd', 4), u'ab ì...') - self.assertEquals(uilib.fallback_safe_cut(u'& &d ef', 4), u'& &d...') - - def test_lxml_safe_cut(self): - self.assertEquals(uilib.safe_cut(u'aaa
    aaad
    ef', 4), u'

    aaa

    a...
    ') - self.assertEquals(uilib.safe_cut(u'aaa
    aaad
    ef', 7), u'

    aaa

    aaad
    ...') - self.assertEquals(uilib.safe_cut(u'aaa
    aaad
    ', 7), u'

    aaa

    aaad
    ') - # Missing ellipsis due to space management but we don't care - self.assertEquals(uilib.safe_cut(u'ab &d', 4), u'

    ab &...

    ') - - def test_cut(self): - """tests uilib.cut() behaviour""" - data = [ - ('hello', 'hello'), - ('hello world', 'hello wo...'), - ("hellO' world", "hellO..."), - ] - 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() - diff -r 934e758a73ef -r eae23c40627a common/uilib.py --- 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('hi world') - 'hi world' - >>> - """ - return TAG_PROG.sub('', text) - - -REF_PROG = re.compile(r"([^<]*)", re.U) -def _subst_rql(view, obj): - delim, rql, descr = obj.groups() - return u'%s' % (view._cw.build_url(rql=rql), descr) - -def html_publish(view, text): - """replace links by """ - 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 based on , 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
    `data`
    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('
    %s
    ' % 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 and and decode to unicode - return body[11:-13].decode(encoding) - - if hasattr(etree.HTML('
    test
    '), 'iter'): - - def safe_cut(text, length): - """returns an html document of length based on , - 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 - 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 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 based on - (approximatively, since if text has been cut, '...' is added to the end of the string, - resulting in a string of len + 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' % (content, tag) - else: - if tag in HTML4_EMPTY_TAGS: - value += u' />' - else: - value += u'>' % tag - return value - -def tooltipize(text, tooltip, url=None): - """make an HTML tooltip""" - url = url or '#' - return u'
    %s' % (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'%s' % (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'
    ') - # FIXME - strings.append(body) - strings.append(u'
    ') - if title: - strings.append(u'

    %s

    '% xml_escape(title)) - try: - strings.append(u'

    %s

    ' % xml_escape(str(exception)).replace("\n","
    ")) - except UnicodeError: - pass - strings.append(u'
    ') - for index, stackentry in enumerate(stacktb): - strings.append(u'File %s, line ' - u'%s, function ' - u'%s:
    '%( - 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'  %s
    \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'%s=%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'
    ' + info - chars = line_length - html_info.append(info) - boxid = 'ctxlevel%d' % index - strings.append(u'[%s]' % toggle_link(boxid, '+')) - strings.append(u'' % - (boxid, ''.join(html_info))) - tcbk = tcbk.tb_next - except Exception: - pass # doesn't really matter if we have no context info - strings.append(u'
    ') - 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 * diff -r 934e758a73ef -r eae23c40627a cwconfig.py --- 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) diff -r 934e758a73ef -r eae23c40627a entities/wfobjs.py --- 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 diff -r 934e758a73ef -r eae23c40627a entity.py --- 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() diff -r 934e758a73ef -r eae23c40627a goa/__init__.py --- 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): diff -r 934e758a73ef -r eae23c40627a goa/appobjects/components.py --- 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 diff -r 934e758a73ef -r eae23c40627a goa/appobjects/dbmgmt.py --- 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 diff -r 934e758a73ef -r eae23c40627a goa/appobjects/sessions.py --- 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): diff -r 934e758a73ef -r eae23c40627a goa/goactl.py --- 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)) diff -r 934e758a73ef -r eae23c40627a goa/overrides/mttransforms.py --- 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 diff -r 934e758a73ef -r eae23c40627a goa/test/data/views.py --- 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'''

    hellô {{ user.login }}

    diff -r 934e758a73ef -r eae23c40627a goa/test/unittest_editcontroller.py --- 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 diff -r 934e758a73ef -r eae23c40627a goa/tools/laxctl.py --- 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__)), '..')) diff -r 934e758a73ef -r eae23c40627a hooks/integrity.py --- 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 diff -r 934e758a73ef -r eae23c40627a interfaces.py --- 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 """ diff -r 934e758a73ef -r eae23c40627a mail.py --- /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×tamp=%.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""" diff -r 934e758a73ef -r eae23c40627a mixins.py --- /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'
  • %s
  • ' % 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'
  • \n' % entity.__regid__.lower()) + def close_item(self, entity): + self.w(u'
  • \n') + + +class TreePathMixIn(object): + """a recursive path view""" + __regid__ = 'path' + item_vid = 'oneline' + separator = u' > ' + + def call(self, **kwargs): + self.w(u'
    ') + super(TreePathMixIn, self).call(**kwargs) + self.w(u'
    ') + + 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'%s' % 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 diff -r 934e758a73ef -r eae23c40627a mttransforms.py --- /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) diff -r 934e758a73ef -r eae23c40627a rset.py --- 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: diff -r 934e758a73ef -r eae23c40627a server/sqlutils.py --- 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 diff -r 934e758a73ef -r eae23c40627a sobjects/notification.py --- 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) diff -r 934e758a73ef -r eae23c40627a sobjects/supervising.py --- 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 diff -r 934e758a73ef -r eae23c40627a sobjects/test/unittest_notification.py --- 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): diff -r 934e758a73ef -r eae23c40627a tags.py --- /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'') + return u'\n'.join(html) + diff -r 934e758a73ef -r eae23c40627a test/unittest_entity.py --- 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'), '''
    lambda x: 1
    diff -r 934e758a73ef -r eae23c40627a test/unittest_mail.py
    --- /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?= 
    +Reply-to: =?utf-8?q?oim?= , =?utf-8?q?BimBam?= 
    +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 ')
    +        self.assertEquals(msg.get('to'), u'test@logilab.fr')
    +        self.assertEquals(msg.get('reply-to'), u'oim , BimBam ')
    +        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?= 
    +Reply-to: =?utf-8?q?o=C3=AEm?= 
    +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 ')
    +        self.assertEquals(msg.get('to'), u'test@logilab.fr')
    +        self.assertEquals(msg.get('reply-to'), u'oîm ')
    +        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 ')
    +        self.assertEquals(msg.get('reply-to'), u'tutu ')
    +        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 ')
    +        self.assertEquals(msg.get('reply-to'), u'tutu ')
    +        # 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 ')
    +        self.assertEquals(msg.get('reply-to'), u'cubicweb-test ')
    +        # 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 ')
    +        self.assertEquals(msg.get('reply-to'), u'cubicweb-test , cubicweb-test ')
    +        # 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 ')
    +        self.assertEquals(msg.get('reply-to'), u'tutu ')
    +
    +
    +
    +if __name__ == '__main__':
    +    unittest_main()
    +
    diff -r 934e758a73ef -r eae23c40627a test/unittest_migration.py
    --- 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.
    diff -r 934e758a73ef -r eae23c40627a test/unittest_rset.py
    --- 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.
    diff -r 934e758a73ef -r eae23c40627a test/unittest_uilib.py
    --- /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 = [
    +            ('

    Hello

    ', 'Hello'), + ('

    Hello spam

    ', 'Hello spam'), + ('
    Hello', 'Hello'), + ('

    ', ''), + ] + 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 cd', 4), u'ab c...') + self.assertEquals(uilib.fallback_safe_cut(u'ab cd', 5), u'ab cd') + self.assertEquals(uilib.fallback_safe_cut(u'ab &d', 4), u'ab &...') + self.assertEquals(uilib.fallback_safe_cut(u'ab &d ef', 5), u'ab &d...') + self.assertEquals(uilib.fallback_safe_cut(u'ab ìd', 4), u'ab ì...') + self.assertEquals(uilib.fallback_safe_cut(u'& &d ef', 4), u'& &d...') + + def test_lxml_safe_cut(self): + self.assertEquals(uilib.safe_cut(u'aaa
    aaad
    ef', 4), u'

    aaa

    a...
    ') + self.assertEquals(uilib.safe_cut(u'aaa
    aaad
    ef', 7), u'

    aaa

    aaad
    ...') + self.assertEquals(uilib.safe_cut(u'aaa
    aaad
    ', 7), u'

    aaa

    aaad
    ') + # Missing ellipsis due to space management but we don't care + self.assertEquals(uilib.safe_cut(u'ab &d', 4), u'

    ab &...

    ') + + def test_cut(self): + """tests uilib.cut() behaviour""" + data = [ + ('hello', 'hello'), + ('hello world', 'hello wo...'), + ("hellO' world", "hellO..."), + ] + 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() + diff -r 934e758a73ef -r eae23c40627a test/unittest_utils.py --- 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. diff -r 934e758a73ef -r eae23c40627a uilib.py --- /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('hi world') + 'hi world' + >>> + """ + return TAG_PROG.sub('', text) + + +REF_PROG = re.compile(r"([^<]*)", re.U) +def _subst_rql(view, obj): + delim, rql, descr = obj.groups() + return u'%s' % (view._cw.build_url(rql=rql), descr) + +def html_publish(view, text): + """replace links by """ + 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 based on , 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
    `data`
    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('
    %s
    ' % 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 and and decode to unicode + return body[11:-13].decode(encoding) + + if hasattr(etree.HTML('
    test
    '), 'iter'): + + def safe_cut(text, length): + """returns an html document of length based on , + 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 + 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 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 based on + (approximatively, since if text has been cut, '...' is added to the end of the string, + resulting in a string of len + 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' % (content, tag) + else: + if tag in HTML4_EMPTY_TAGS: + value += u' />' + else: + value += u'>' % tag + return value + +def tooltipize(text, tooltip, url=None): + """make an HTML tooltip""" + url = url or '#' + return u'
    %s' % (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'%s' % (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'
    ') + # FIXME + strings.append(body) + strings.append(u'
    ') + if title: + strings.append(u'

    %s

    '% xml_escape(title)) + try: + strings.append(u'

    %s

    ' % xml_escape(str(exception)).replace("\n","
    ")) + except UnicodeError: + pass + strings.append(u'
    ') + for index, stackentry in enumerate(stacktb): + strings.append(u'File %s, line ' + u'%s, function ' + u'%s:
    '%( + 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'  %s
    \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'%s=%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'
    ' + info + chars = line_length + html_info.append(info) + boxid = 'ctxlevel%d' % index + strings.append(u'[%s]' % toggle_link(boxid, '+')) + strings.append(u'' % + (boxid, ''.join(html_info))) + tcbk = tcbk.tb_next + except Exception: + pass # doesn't really matter if we have no context info + strings.append(u'
    ') + 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 diff -r 934e758a73ef -r eae23c40627a web/form.py --- 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 diff -r 934e758a73ef -r eae23c40627a web/formfields.py --- 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, diff -r 934e758a73ef -r eae23c40627a web/formwidgets.py --- 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] diff -r 934e758a73ef -r eae23c40627a web/htmlwidgets.py --- 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, diff -r 934e758a73ef -r eae23c40627a web/request.py --- 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, diff -r 934e758a73ef -r eae23c40627a web/test/unittest_views_basecontrollers.py --- 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 diff -r 934e758a73ef -r eae23c40627a web/views/basecomponents.py --- 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) diff -r 934e758a73ef -r eae23c40627a web/views/basecontrollers.py --- 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 diff -r 934e758a73ef -r eae23c40627a web/views/baseviews.py --- 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): diff -r 934e758a73ef -r eae23c40627a web/views/csvexport.py --- 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): diff -r 934e758a73ef -r eae23c40627a web/views/editforms.py --- 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 diff -r 934e758a73ef -r eae23c40627a web/views/editviews.py --- 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 diff -r 934e758a73ef -r eae23c40627a web/views/emailaddress.py --- 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): diff -r 934e758a73ef -r eae23c40627a web/views/embedding.py --- 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 diff -r 934e758a73ef -r eae23c40627a web/views/formrenderers.py --- 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 diff -r 934e758a73ef -r eae23c40627a web/views/ibreadcrumbs.py --- 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): diff -r 934e758a73ef -r eae23c40627a web/views/idownloadable.py --- 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 diff -r 934e758a73ef -r eae23c40627a web/views/management.py --- 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 diff -r 934e758a73ef -r eae23c40627a web/views/navigation.py --- 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 diff -r 934e758a73ef -r eae23c40627a web/views/schema.py --- 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 diff -r 934e758a73ef -r eae23c40627a web/views/tableview.py --- 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) diff -r 934e758a73ef -r eae23c40627a web/views/tabs.py --- 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 diff -r 934e758a73ef -r eae23c40627a web/views/urlpublishing.py --- 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 diff -r 934e758a73ef -r eae23c40627a web/views/wdoc.py --- 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 diff -r 934e758a73ef -r eae23c40627a web/views/xmlrss.py --- 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