--- a/.hgtags Wed Jan 05 18:42:21 2011 +0100
+++ b/.hgtags Thu Jan 13 19:24:21 2011 +0100
@@ -177,3 +177,5 @@
e2e7410e994777589aec218d31eef9ff8d893f92 cubicweb-debian-version-3.10.5-1
3c81dbb58ac4d4a6f61b74eef4b943a8316c2f42 cubicweb-version-3.10.6
1484257fe9aeb29d0210e635c12ae5b3d6118cfb cubicweb-debian-version-3.10.6-1
+1959d97ebf2e6a0f7cd05d4cc48bb955c4351da5 cubicweb-version-3.10.7
+bf5d9a1415e3c9abe6b68ba3b24a8ad741f9de3c cubicweb-debian-version-3.10.7-1
--- a/__pkginfo__.py Wed Jan 05 18:42:21 2011 +0100
+++ b/__pkginfo__.py Thu Jan 13 19:24:21 2011 +0100
@@ -42,7 +42,7 @@
__depends__ = {
'logilab-common': '>= 0.54.0',
'logilab-mtconverter': '>= 0.8.0',
- 'rql': '>= 0.27.0',
+ 'rql': '>= 0.28.0',
'yams': '>= 0.30.1',
'docutils': '>= 0.6',
#gettext # for xgettext, msgcat, etc...
--- a/cwconfig.py Wed Jan 05 18:42:21 2011 +0100
+++ b/cwconfig.py Thu Jan 13 19:24:21 2011 +0100
@@ -450,14 +450,15 @@
@classmethod
def cube_dir(cls, cube):
- """return the cube directory for the given cube id,
- raise `ConfigurationError` if it doesn't exists
+ """return the cube directory for the given cube id, raise
+ `ConfigurationError` if it doesn't exist
"""
for directory in cls.cubes_search_path():
cubedir = join(directory, cube)
if exists(cubedir):
return cubedir
- raise ConfigurationError('no cube %s in %s' % (cube, cls.cubes_search_path()))
+ raise ConfigurationError('no cube %r in %s' % (
+ cube, cls.cubes_search_path()))
@classmethod
def cube_migration_scripts_dir(cls, cube):
@@ -1278,7 +1279,9 @@
stack[0] = self.source_execute
def as_sql(self, backend, args):
- raise NotImplementedError('source only callback')
+ raise NotImplementedError(
+ 'This callback is only available for BytesFileSystemStorage '
+ 'managed attribute. Is FSPATH() argument BFSS managed?')
def source_execute(self, source, session, value):
fpath = source.binary_to_str(value)
--- a/cwvreg.py Wed Jan 05 18:42:21 2011 +0100
+++ b/cwvreg.py Thu Jan 13 19:24:21 2011 +0100
@@ -290,13 +290,18 @@
class ETypeRegistry(CWRegistry):
+ def clear_caches(self):
+ clear_cache(self, 'etype_class')
+ clear_cache(self, 'parent_classes')
+ from cubicweb import selectors
+ selectors._reset_is_instance_cache(self.vreg)
+
def initialization_completed(self):
"""on registration completed, clear etype_class internal cache
"""
super(ETypeRegistry, self).initialization_completed()
# clear etype cache if you don't want to run into deep weirdness
- clear_cache(self, 'etype_class')
- clear_cache(self, 'parent_classes')
+ self.clear_caches()
def register(self, obj, **kwargs):
oid = kwargs.get('oid') or class_regid(obj)
--- a/debian/changelog Wed Jan 05 18:42:21 2011 +0100
+++ b/debian/changelog Thu Jan 13 19:24:21 2011 +0100
@@ -1,3 +1,9 @@
+cubicweb (3.10.7-1) unstable; urgency=low
+
+ * new upstream release
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr> Wed, 12 Jan 2011 08:50:29 +0100
+
cubicweb (3.10.6-1) unstable; urgency=low
* new upstream release
--- a/debian/control Wed Jan 05 18:42:21 2011 +0100
+++ b/debian/control Thu Jan 13 19:24:21 2011 +0100
@@ -97,7 +97,7 @@
Package: cubicweb-common
Architecture: all
XB-Python-Version: ${python:Versions}
-Depends: ${python:Depends}, graphviz, gettext, python-logilab-mtconverter (>= 0.8.0), python-logilab-common (>= 0.54.0), python-yams (>= 0.30.1), python-rql (>= 0.27.0), python-lxml
+Depends: ${python:Depends}, graphviz, gettext, python-logilab-mtconverter (>= 0.8.0), python-logilab-common (>= 0.54.0), python-yams (>= 0.30.1), python-rql (>= 0.28.0), python-lxml
Recommends: python-simpletal (>= 4.0), python-crypto
Conflicts: cubicweb-core
Replaces: cubicweb-core
--- a/debian/cubicweb-ctl.cubicweb.init Wed Jan 05 18:42:21 2011 +0100
+++ b/debian/cubicweb-ctl.cubicweb.init Thu Jan 13 19:24:21 2011 +0100
@@ -24,12 +24,12 @@
case $1 in
force-reload)
- /usr/bin/cubicweb-ctl reload --force
+ python -W ignore /usr/bin/cubicweb-ctl reload --force
;;
status)
- /usr/bin/cubicweb-ctl status
+ python -W ignore /usr/bin/cubicweb-ctl status
;;
*)
- /usr/bin/cubicweb-ctl $1 --force
+ python -W ignore /usr/bin/cubicweb-ctl $1 --force
;;
esac
--- a/devtools/repotest.py Wed Jan 05 18:42:21 2011 +0100
+++ b/devtools/repotest.py Thu Jan 13 19:24:21 2011 +0100
@@ -26,6 +26,7 @@
from pprint import pprint
from logilab.common.decorators import clear_cache
+from logilab.common.testlib import SkipTest
def tuplify(list):
for i in range(len(list)):
@@ -149,6 +150,15 @@
class RQLGeneratorTC(TestCase):
schema = backend = None # set this in concret test
+
+ @classmethod
+ def setUpClass(cls):
+ if cls.backend is not None:
+ try:
+ cls.dbhelper = get_db_helper(cls.backend)
+ except ImportError, ex:
+ raise SkipTest(str(ex))
+
def setUp(self):
self.repo = FakeRepo(self.schema)
self.repo.system_source = mock_object(dbdriver=self.backend)
@@ -159,11 +169,7 @@
ExecutionPlan._check_permissions = _dummy_check_permissions
rqlannotation._select_principal = _select_principal
if self.backend is not None:
- try:
- dbhelper = get_db_helper(self.backend)
- except ImportError, ex:
- self.skipTest(str(ex))
- self.o = SQLGenerator(self.schema, dbhelper)
+ self.o = SQLGenerator(self.schema, self.dbhelper)
def tearDown(self):
ExecutionPlan._check_permissions = _orig_check_permissions
@@ -378,7 +384,7 @@
def _merge_input_maps(*args, **kwargs):
return sorted(_orig_merge_input_maps(*args, **kwargs))
-def _choose_term(self, sourceterms):
+def _choose_term(self, source, sourceterms):
# predictable order for test purpose
def get_key(x):
try:
@@ -391,7 +397,7 @@
except AttributeError:
# const
return x.value
- return _orig_choose_term(self, DumbOrderedDict2(sourceterms, get_key))
+ return _orig_choose_term(self, source, DumbOrderedDict2(sourceterms, get_key))
from cubicweb.server.sources.pyrorql import PyroRQLSource
_orig_syntax_tree_search = PyroRQLSource.syntax_tree_search
--- a/devtools/testlib.py Wed Jan 05 18:42:21 2011 +0100
+++ b/devtools/testlib.py Thu Jan 13 19:24:21 2011 +0100
@@ -480,9 +480,7 @@
def items(self):
return self
class fake_box(object):
- def mk_action(self, label, url, **kwargs):
- return (label, url)
- def box_action(self, action, **kwargs):
+ def action_link(self, action, **kwargs):
return (action.title, action.url())
submenu = fake_menu()
action.fill_menu(fake_box(), submenu)
@@ -741,10 +739,8 @@
:returns: an instance of `cubicweb.devtools.htmlparser.PageInfo`
encapsulation the generated HTML
"""
- output = None
try:
output = viewfunc(**kwargs)
- return self._check_html(output, view, template)
except (SystemExit, KeyboardInterrupt):
raise
except:
@@ -755,22 +751,8 @@
msg = '[%s in %s] %s' % (klass, view.__regid__, exc)
except:
msg = '[%s in %s] undisplayable exception' % (klass, view.__regid__)
- msg = str(msg) # ensure no unicode
- if output is not None:
- position = getattr(exc, "position", (0,))[0]
- if position:
- # define filter
- output = output.splitlines()
- width = int(log(len(output), 10)) + 1
- line_template = " %" + ("%i" % width) + "i: %s"
- # XXX no need to iterate the whole file except to get
- # the line number
- output = '\n'.join(line_template % (idx + 1, line)
- for idx, line in enumerate(output)
- if line_context_filter(idx+1, position))
- msg += '\nfor output:\n%s' % output
raise AssertionError, msg, tcbk
-
+ return self._check_html(output, view, template)
@nocoverage
def _check_html(self, output, view, template='main-template'):
@@ -794,7 +776,44 @@
if isinstance(validator, htmlparser.DTDValidator):
# XXX remove <canvas> used in progress widget, unknown in html dtd
output = re.sub('<canvas.*?></canvas>', '', output)
- return validator.parse_string(output.strip())
+ return self.assertWellFormed(validator, output.strip(), context= view.__regid__)
+
+ def assertWellFormed(self, validator, content, context=None):
+ try:
+ return validator.parse_string(content)
+ except (SystemExit, KeyboardInterrupt):
+ raise
+ except:
+ # hijack exception: generative tests stop when the exception
+ # is not an AssertionError
+ klass, exc, tcbk = sys.exc_info()
+ if context is None:
+ msg = u'[%s]' % (klass,)
+ else:
+ msg = u'[%s in %s]' % (klass, context)
+ msg = msg.encode(sys.getdefaultencoding(), 'replace')
+
+ try:
+ str_exc = str(exc)
+ except:
+ str_exc = 'undisplayable exception'
+ msg += str_exc
+ if content is not None:
+ position = getattr(exc, "position", (0,))[0]
+ if position:
+ # define filter
+ if isinstance(content, str):
+ content = unicode(content, sys.getdefaultencoding(), 'replace')
+ content = content.splitlines()
+ width = int(log(len(content), 10)) + 1
+ line_template = " %" + ("%i" % width) + "i: %s"
+ # XXX no need to iterate the whole file except to get
+ # the line number
+ content = u'\n'.join(line_template % (idx + 1, line)
+ for idx, line in enumerate(content)
+ if line_context_filter(idx+1, position))
+ msg += u'\nfor content:\n%s' % content
+ raise AssertionError, msg, tcbk
# deprecated ###############################################################
--- a/doc/book/en/makefile Wed Jan 05 18:42:21 2011 +0100
+++ b/doc/book/en/makefile Thu Jan 13 19:24:21 2011 +0100
@@ -1,10 +1,5 @@
-MKHTML=mkdoc
-MKHTMLOPTS=--doctype article --target html --stylesheet standard
SRC=.
-TXTFILES:= $(wildcard *.txt)
-TARGET := $(TXTFILES:.txt=.html)
-
# You can set these sphinx variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
@@ -41,10 +36,7 @@
-rm -rf ${BUILDDIR}/*
-rm -rf ${BUILDJS}
-all: ${TARGET} html
-
-%.html: %.txt
- ${MKHTML} ${MKHTMLOPTS} $<
+all: html
# run sphinx ###
html: js
--- a/entities/test/unittest_wfobjs.py Wed Jan 05 18:42:21 2011 +0100
+++ b/entities/test/unittest_wfobjs.py Thu Jan 13 19:24:21 2011 +0100
@@ -15,7 +15,9 @@
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
+
from __future__ import with_statement
+
from cubicweb.devtools.testlib import CubicWebTC
from cubicweb import ValidationError
from cubicweb.server.session import security_enabled
@@ -55,8 +57,9 @@
wf.add_state(u'foo', initial=True)
self.commit()
wf.add_state(u'foo')
- ex = self.assertRaises(ValidationError, self.commit)
- self.assertEqual(ex.errors, {'name-subject': 'workflow already have a state of that name'})
+ with self.assertRaises(ValidationError) as cm:
+ self.commit()
+ self.assertEqual(cm.exception.errors, {'name-subject': 'workflow already have a state of that name'})
# no pb if not in the same workflow
wf2 = add_wf(self, 'Company')
foo = wf2.add_state(u'foo', initial=True)
@@ -65,8 +68,9 @@
bar = wf.add_state(u'bar')
self.commit()
bar.set_attributes(name=u'foo')
- ex = self.assertRaises(ValidationError, self.commit)
- self.assertEqual(ex.errors, {'name-subject': 'workflow already have a state of that name'})
+ with self.assertRaises(ValidationError) as cm:
+ self.commit()
+ self.assertEqual(cm.exception.errors, {'name-subject': 'workflow already have a state of that name'})
def test_duplicated_transition(self):
wf = add_wf(self, 'Company')
@@ -74,8 +78,9 @@
bar = wf.add_state(u'bar')
wf.add_transition(u'baz', (foo,), bar, ('managers',))
wf.add_transition(u'baz', (bar,), foo)
- ex = self.assertRaises(ValidationError, self.commit)
- self.assertEqual(ex.errors, {'name-subject': 'workflow already have a transition of that name'})
+ with self.assertRaises(ValidationError) as cm:
+ self.commit()
+ self.assertEqual(cm.exception.errors, {'name-subject': 'workflow already have a transition of that name'})
# no pb if not in the same workflow
wf2 = add_wf(self, 'Company')
foo = wf.add_state(u'foo', initial=True)
@@ -86,8 +91,9 @@
biz = wf.add_transition(u'biz', (bar,), foo)
self.commit()
biz.set_attributes(name=u'baz')
- ex = self.assertRaises(ValidationError, self.commit)
- self.assertEqual(ex.errors, {'name-subject': 'workflow already have a transition of that name'})
+ with self.assertRaises(ValidationError) as cm:
+ self.commit()
+ self.assertEqual(cm.exception.errors, {'name-subject': 'workflow already have a transition of that name'})
class WorkflowTC(CubicWebTC):
@@ -150,10 +156,10 @@
s = wf.add_state(u'foo', initial=True)
self.commit()
with security_enabled(self.session, write=False):
- ex = self.assertRaises(ValidationError, self.session.execute,
- 'SET X in_state S WHERE X eid %(x)s, S eid %(s)s',
- {'x': self.user().eid, 's': s.eid})
- self.assertEqual(ex.errors, {'in_state-subject': "state doesn't belong to entity's workflow. "
+ with self.assertRaises(ValidationError) as cm:
+ self.session.execute('SET X in_state S WHERE X eid %(x)s, S eid %(s)s',
+ {'x': self.user().eid, 's': s.eid})
+ self.assertEqual(cm.exception.errors, {'in_state-subject': "state doesn't belong to entity's workflow. "
"You may want to set a custom workflow for this entity first."})
def test_fire_transition(self):
@@ -197,18 +203,18 @@
cnx = self.login('tutu')
req = self.request()
iworkflowable = req.entity_from_eid(self.member.eid).cw_adapt_to('IWorkflowable')
- ex = self.assertRaises(ValidationError,
- iworkflowable.fire_transition, 'deactivate')
- self.assertEqual(ex.errors, {'by_transition-subject': "transition may not be fired"})
+ with self.assertRaises(ValidationError) as cm:
+ iworkflowable.fire_transition('deactivate')
+ self.assertEqual(cm.exception.errors, {'by_transition-subject': "transition may not be fired"})
cnx.close()
cnx = self.login('member')
req = self.request()
iworkflowable = req.entity_from_eid(self.member.eid).cw_adapt_to('IWorkflowable')
iworkflowable.fire_transition('deactivate')
cnx.commit()
- ex = self.assertRaises(ValidationError,
- iworkflowable.fire_transition, 'activate')
- self.assertEqual(ex.errors, {'by_transition-subject': "transition may not be fired"})
+ with self.assertRaises(ValidationError) as cm:
+ iworkflowable.fire_transition('activate')
+ self.assertEqual(cm.exception.errors, {'by_transition-subject': "transition may not be fired"})
def test_fire_transition_owned_by(self):
self.execute('INSERT RQLExpression X: X exprtype "ERQLExpression", '
@@ -280,9 +286,9 @@
self.assertEqual(iworkflowable.subworkflow_input_transition(), None)
# force back to swfstate1 is impossible since we can't any more find
# subworkflow input transition
- ex = self.assertRaises(ValidationError,
- iworkflowable.change_state, swfstate1, u'gadget')
- self.assertEqual(ex.errors, {'to_state-subject': "state doesn't belong to entity's workflow"})
+ with self.assertRaises(ValidationError) as cm:
+ iworkflowable.change_state(swfstate1, u'gadget')
+ self.assertEqual(cm.exception.errors, {'to_state-subject': "state doesn't belong to entity's workflow"})
self.rollback()
# force back to state1
iworkflowable.change_state('state1', u'gadget')
@@ -317,8 +323,9 @@
state3 = mwf.add_state(u'state3')
mwf.add_wftransition(u'swftr1', swf, state1,
[(swfstate2, state2), (swfstate2, state3)])
- ex = self.assertRaises(ValidationError, self.commit)
- self.assertEqual(ex.errors, {'subworkflow_exit-subject': u"can't have multiple exits on the same state"})
+ with self.assertRaises(ValidationError) as cm:
+ self.commit()
+ self.assertEqual(cm.exception.errors, {'subworkflow_exit-subject': u"can't have multiple exits on the same state"})
def test_swf_fire_in_a_row(self):
# sub-workflow
@@ -435,8 +442,9 @@
wf.add_state('asleep')
self.execute('SET X custom_workflow WF WHERE X eid %(x)s, WF eid %(wf)s',
{'wf': wf.eid, 'x': self.member.eid})
- ex = self.assertRaises(ValidationError, self.commit)
- self.assertEqual(ex.errors, {'custom_workflow-subject': u'workflow has no initial state'})
+ with self.assertRaises(ValidationError) as cm:
+ self.commit()
+ self.assertEqual(cm.exception.errors, {'custom_workflow-subject': u'workflow has no initial state'})
def test_custom_wf_bad_etype(self):
"""try to set a custom workflow which doesn't apply to entity type"""
@@ -444,8 +452,9 @@
wf.add_state('asleep', initial=True)
self.execute('SET X custom_workflow WF WHERE X eid %(x)s, WF eid %(wf)s',
{'wf': wf.eid, 'x': self.member.eid})
- ex = self.assertRaises(ValidationError, self.commit)
- self.assertEqual(ex.errors, {'custom_workflow-subject': u"workflow isn't a workflow for this type"})
+ with self.assertRaises(ValidationError) as cm:
+ self.commit()
+ self.assertEqual(cm.exception.errors, {'custom_workflow-subject': u"workflow isn't a workflow for this type"})
def test_del_custom_wf(self):
"""member in some state shared by the new workflow, nothing has to be
@@ -590,9 +599,9 @@
cnx = self.login('stduser')
user = cnx.user(self.session)
iworkflowable = user.cw_adapt_to('IWorkflowable')
- ex = self.assertRaises(ValidationError,
- iworkflowable.fire_transition, 'activate')
- self.assertEqual(self._cleanup_msg(ex.errors['by_transition-subject']),
+ with self.assertRaises(ValidationError) as cm:
+ iworkflowable.fire_transition('activate')
+ self.assertEqual(self._cleanup_msg(cm.exception.errors['by_transition-subject']),
u"transition isn't allowed from")
cnx.close()
@@ -600,9 +609,9 @@
cnx = self.login('stduser')
user = cnx.user(self.session)
iworkflowable = user.cw_adapt_to('IWorkflowable')
- ex = self.assertRaises(ValidationError,
- iworkflowable.fire_transition, 'dummy')
- self.assertEqual(self._cleanup_msg(ex.errors['by_transition-subject']),
+ with self.assertRaises(ValidationError) as cm:
+ iworkflowable.fire_transition('dummy')
+ self.assertEqual(self._cleanup_msg(cm.exception.errors['by_transition-subject']),
u"transition isn't allowed from")
cnx.close()
@@ -614,9 +623,9 @@
iworkflowable.fire_transition('deactivate')
cnx.commit()
session.set_pool()
- ex = self.assertRaises(ValidationError,
- iworkflowable.fire_transition, 'deactivate')
- self.assertEqual(self._cleanup_msg(ex.errors['by_transition-subject']),
+ with self.assertRaises(ValidationError) as cm:
+ iworkflowable.fire_transition('deactivate')
+ self.assertEqual(self._cleanup_msg(cm.exception.errors['by_transition-subject']),
u"transition isn't allowed from")
cnx.rollback()
session.set_pool()
--- a/entity.py Wed Jan 05 18:42:21 2011 +0100
+++ b/entity.py Thu Jan 13 19:24:21 2011 +0100
@@ -697,7 +697,7 @@
if not self.has_eid():
if entities:
return []
- return self.empty_rset()
+ return self._cw.empty_rset()
rql = self.cw_related_rql(rtype, role)
rset = self._cw.execute(rql, {'x': self.eid})
self.cw_set_relation_cache(rtype, role, rset)
@@ -972,7 +972,7 @@
def set_related_cache(self, rtype, role, rset):
self.cw_set_relation_cache(rtype, role, rset)
- @deprecated('[3.9] use entity.cw_clear_relation_cache(rtype, role, rset)')
+ @deprecated('[3.9] use entity.cw_clear_relation_cache(rtype, role)')
def clear_related_cache(self, rtype=None, role=None):
self.cw_clear_relation_cache(rtype, role)
@@ -996,7 +996,7 @@
return self.cw_edited.skip_security
@property
- @deprecated('[3.10] use entity.cw_edited.skip_security')
+ @deprecated('[3.10] use entity.cw_edited.querier_pending_relations')
def querier_pending_relations(self):
return self.cw_edited.querier_pending_relations
--- a/etwist/server.py Wed Jan 05 18:42:21 2011 +0100
+++ b/etwist/server.py Thu Jan 13 19:24:21 2011 +0100
@@ -408,7 +408,8 @@
website = server.Site(root_resource)
# serve it via standard HTTP on port set in the configuration
port = config['port'] or 8080
- reactor.listenTCP(port, website)
+ interface = config['interface']
+ reactor.listenTCP(port, website, interface=interface)
if not config.debugmode:
if sys.platform == 'win32':
raise ConfigurationError("Under windows, you must use the service management "
--- a/etwist/twconfig.py Wed Jan 05 18:42:21 2011 +0100
+++ b/etwist/twconfig.py Thu Jan 13 19:24:21 2011 +0100
@@ -45,6 +45,12 @@
'help': 'http server port number (default to 8080)',
'group': 'web', 'level': 0,
}),
+ ('interface',
+ {'type' : 'string',
+ 'default': "",
+ 'help': 'http server address on which to listen (default to everywhere)',
+ 'group': 'web', 'level': 0,
+ }),
('max-post-length',
{'type' : 'bytes',
'default': '100MB',
--- a/hooks/syncschema.py Wed Jan 05 18:42:21 2011 +0100
+++ b/hooks/syncschema.py Thu Jan 13 19:24:21 2011 +0100
@@ -30,7 +30,6 @@
from yams import buildobjs as ybo, schema2sql as y2sql
from logilab.common.decorators import clear_cache
-from logilab.common.testlib import mock_object
from cubicweb import ValidationError
from cubicweb.selectors import is_instance
@@ -131,6 +130,11 @@
raise ValidationError(entity.eid, errors)
+class _MockEntity(object): # XXX use a named tuple with python 2.6
+ def __init__(self, eid):
+ self.eid = eid
+
+
class SyncSchemaHook(hook.Hook):
"""abstract class for schema synchronization hooks (in the `syncschema`
category)
@@ -266,8 +270,8 @@
sampletype = rschema.subjects()[0]
desttype = rschema.objects()[0]
rdef = copy(rschema.rdef(sampletype, desttype))
- rdef.subject = mock_object(eid=entity.eid)
- mock = mock_object(eid=None)
+ rdef.subject = _MockEntity(eid=entity.eid)
+ mock = _MockEntity(eid=None)
ss.execschemarql(session.execute, mock, ss.rdef2rql(rdef, cmap, gmap))
def revertprecommit_event(self):
@@ -701,14 +705,14 @@
syssource.update_rdef_unique(session, rdef)
self.unique_changed = True
+
class CWUniqueTogetherConstraintAddOp(MemSchemaOperation):
entity = None # make pylint happy
def precommit_event(self):
session = self.session
prefix = SQL_PREFIX
table = '%s%s' % (prefix, self.entity.constraint_of[0].name)
- cols = ['%s%s' % (prefix, r.rtype.name)
- for r in self.entity.relations]
+ cols = ['%s%s' % (prefix, r.name) for r in self.entity.relations]
dbhelper= session.pool.source('system').dbhelper
sqls = dbhelper.sqls_create_multicol_unique_index(table, cols)
for sql in sqls:
@@ -718,9 +722,10 @@
def postcommit_event(self):
eschema = self.session.vreg.schema.schema_by_eid(self.entity.constraint_of[0].eid)
- attrs = [r.rtype.name for r in self.entity.relations]
+ attrs = [r.name for r in self.entity.relations]
eschema._unique_together.append(attrs)
+
class CWUniqueTogetherConstraintDelOp(MemSchemaOperation):
entity = oldcstr = None # for pylint
cols = [] # for pylint
@@ -743,6 +748,7 @@
if set(ut) != cols]
eschema._unique_together = unique_together
+
# operations for in-memory schema synchronization #############################
class MemSchemaCWETypeDel(MemSchemaOperation):
@@ -1138,9 +1144,9 @@
schema = self._cw.vreg.schema
cstr = self._cw.entity_from_eid(self.eidfrom)
entity = schema.schema_by_eid(self.eidto)
- cols = [r.rtype.name
- for r in cstr.relations]
- CWUniqueTogetherConstraintDelOp(self._cw, entity=entity, oldcstr=cstr, cols=cols)
+ cols = [r.name for r in cstr.relations]
+ CWUniqueTogetherConstraintDelOp(self._cw, entity=entity,
+ oldcstr=cstr, cols=cols)
# permissions synchronization hooks ############################################
--- a/hooks/test/unittest_hooks.py Wed Jan 05 18:42:21 2011 +0100
+++ b/hooks/test/unittest_hooks.py Thu Jan 13 19:24:21 2011 +0100
@@ -20,6 +20,7 @@
note: most schemahooks.py hooks are actually tested in unittest_migrations.py
"""
+from __future__ import with_statement
from logilab.common.testlib import TestCase, unittest_main
@@ -115,8 +116,9 @@
def test_unsatisfied_constraints(self):
releid = self.execute('SET U in_group G WHERE G name "owners", U login "admin"')[0][0]
- ex = self.assertRaises(ValidationError, self.commit)
- self.assertEqual(ex.errors,
+ with self.assertRaises(ValidationError) as cm:
+ self.commit()
+ self.assertEqual(cm.exception.errors,
{'in_group-object': u'RQLConstraint NOT O name "owners" failed'})
def test_html_tidy_hook(self):
@@ -227,25 +229,25 @@
class CWPropertyHooksTC(CubicWebTC):
def test_unexistant_eproperty(self):
- ex = self.assertRaises(ValidationError,
- self.execute, 'INSERT CWProperty X: X pkey "bla.bla", X value "hop", X for_user U')
- self.assertEqual(ex.errors, {'pkey-subject': 'unknown property key bla.bla'})
- ex = self.assertRaises(ValidationError,
- self.execute, 'INSERT CWProperty X: X pkey "bla.bla", X value "hop"')
- self.assertEqual(ex.errors, {'pkey-subject': 'unknown property key bla.bla'})
+ with self.assertRaises(ValidationError) as cm:
+ self.execute('INSERT CWProperty X: X pkey "bla.bla", X value "hop", X for_user U')
+ self.assertEqual(cm.exception.errors, {'pkey-subject': 'unknown property key bla.bla'})
+ with self.assertRaises(ValidationError) as cm:
+ self.execute('INSERT CWProperty X: X pkey "bla.bla", X value "hop"')
+ self.assertEqual(cm.exception.errors, {'pkey-subject': 'unknown property key bla.bla'})
def test_site_wide_eproperty(self):
- ex = self.assertRaises(ValidationError,
- self.execute, 'INSERT CWProperty X: X pkey "ui.site-title", X value "hop", X for_user U')
- self.assertEqual(ex.errors, {'for_user-subject': "site-wide property can't be set for user"})
+ with self.assertRaises(ValidationError) as cm:
+ self.execute('INSERT CWProperty X: X pkey "ui.site-title", X value "hop", X for_user U')
+ self.assertEqual(cm.exception.errors, {'for_user-subject': "site-wide property can't be set for user"})
def test_bad_type_eproperty(self):
- ex = self.assertRaises(ValidationError,
- self.execute, 'INSERT CWProperty X: X pkey "ui.language", X value "hop", X for_user U')
- self.assertEqual(ex.errors, {'value-subject': u'unauthorized value'})
- ex = self.assertRaises(ValidationError,
- self.execute, 'INSERT CWProperty X: X pkey "ui.language", X value "hop"')
- self.assertEqual(ex.errors, {'value-subject': u'unauthorized value'})
+ with self.assertRaises(ValidationError) as cm:
+ self.execute('INSERT CWProperty X: X pkey "ui.language", X value "hop", X for_user U')
+ self.assertEqual(cm.exception.errors, {'value-subject': u'unauthorized value'})
+ with self.assertRaises(ValidationError) as cm:
+ self.execute('INSERT CWProperty X: X pkey "ui.language", X value "hop"')
+ self.assertEqual(cm.exception.errors, {'value-subject': u'unauthorized value'})
class SchemaHooksTC(CubicWebTC):
--- a/i18n/de.po Wed Jan 05 18:42:21 2011 +0100
+++ b/i18n/de.po Thu Jan 13 19:24:21 2011 +0100
@@ -2566,7 +2566,7 @@
msgid "has_text"
msgstr "enthält Text"
-msgid "header-center"
+msgid "header-left"
msgstr ""
msgid "header-right"
@@ -3918,6 +3918,13 @@
msgid "toggle check boxes"
msgstr "Kontrollkästchen umkehren"
+msgid "tr_count"
+msgstr ""
+
+msgctxt "TrInfo"
+msgid "tr_count"
+msgstr ""
+
msgid "transaction undoed"
msgstr "Transaktion rückgängig gemacht"
--- a/i18n/en.po Wed Jan 05 18:42:21 2011 +0100
+++ b/i18n/en.po Thu Jan 13 19:24:21 2011 +0100
@@ -2499,8 +2499,8 @@
msgid "has_text"
msgstr "has text"
-msgid "header-center"
-msgstr "header (center)"
+msgid "header-left"
+msgstr "header (left)"
msgid "header-right"
msgstr "header (right)"
@@ -3811,6 +3811,13 @@
msgid "toggle check boxes"
msgstr ""
+msgid "tr_count"
+msgstr "transition number"
+
+msgctxt "TrInfo"
+msgid "tr_count"
+msgstr "transition number"
+
msgid "transaction undoed"
msgstr ""
--- a/i18n/es.po Wed Jan 05 18:42:21 2011 +0100
+++ b/i18n/es.po Thu Jan 13 19:24:21 2011 +0100
@@ -2594,7 +2594,7 @@
msgid "has_text"
msgstr "Contiene el texto"
-msgid "header-center"
+msgid "header-left"
msgstr ""
msgid "header-right"
@@ -3939,6 +3939,13 @@
msgid "toggle check boxes"
msgstr "Cambiar valor"
+msgid "tr_count"
+msgstr ""
+
+msgctxt "TrInfo"
+msgid "tr_count"
+msgstr ""
+
msgid "transaction undoed"
msgstr "Transacciones Anuladas"
--- a/i18n/fr.po Wed Jan 05 18:42:21 2011 +0100
+++ b/i18n/fr.po Thu Jan 13 19:24:21 2011 +0100
@@ -2596,8 +2596,8 @@
msgid "has_text"
msgstr "contient le texte"
-msgid "header-center"
-msgstr "en-tête (centre)"
+msgid "header-left"
+msgstr "en-tête (gauche)"
msgid "header-right"
msgstr "en-tête (droite)"
@@ -3948,6 +3948,13 @@
msgid "toggle check boxes"
msgstr "inverser les cases à cocher"
+msgid "tr_count"
+msgstr "n° de transition"
+
+msgctxt "TrInfo"
+msgid "tr_count"
+msgstr "n° de transition"
+
msgid "transaction undoed"
msgstr "transaction annulées"
--- a/misc/migration/3.10.7_Any.py Wed Jan 05 18:42:21 2011 +0100
+++ b/misc/migration/3.10.7_Any.py Thu Jan 13 19:24:21 2011 +0100
@@ -1,2 +1,8 @@
+add_relation_definition('CWUniqueTogetherConstraint', 'relations', 'CWRType')
+rql('SET C relations RT WHERE C relations RDEF, RDEF relation_type RT')
+commit()
+drop_relation_definition('CWUniqueTogetherConstraint', 'relations', 'CWAttribute')
+drop_relation_definition('CWUniqueTogetherConstraint', 'relations', 'CWRelation')
+
add_attribute('TrInfo', 'tr_count')
sync_schema_props_perms('TrInfo')
--- a/schemas/bootstrap.py Wed Jan 05 18:42:21 2011 +0100
+++ b/schemas/bootstrap.py Thu Jan 13 19:24:21 2011 +0100
@@ -159,10 +159,10 @@
__permissions__ = PUB_SYSTEM_ENTITY_PERMS
constraint_of = SubjectRelation('CWEType', cardinality='1*', composite='object',
inlined=True)
- relations = SubjectRelation(('CWAttribute', 'CWRelation'), cardinality='+*',
- constraints=[RQLConstraint(
- 'O from_entity X, S constraint_of X, O relation_type T, '
- 'T final TRUE OR (T final FALSE AND T inlined TRUE)')])
+ relations = SubjectRelation('CWRType', cardinality='+*',
+ constraints=[RQLConstraint(
+ 'S constraint_of ET, RDEF relation_type O, RDEF from_entity ET, '
+ 'O final TRUE OR (O final FALSE AND O inlined TRUE)')])
class CWConstraintType(EntityType):
--- a/server/hook.py Wed Jan 05 18:42:21 2011 +0100
+++ b/server/hook.py Thu Jan 13 19:24:21 2011 +0100
@@ -239,8 +239,8 @@
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. autoclass:: cubicweb.server.hook.Hook
.. autoclass:: cubicweb.server.hook.Operation
-.. autoclass:: cubicweb.server.hook.DataOperation
.. autoclass:: cubicweb.server.hook.LateOperation
+.. autoclass:: cubicweb.server.hook.DataOperationMixIn
"""
from __future__ import with_statement
--- a/server/migractions.py Wed Jan 05 18:42:21 2011 +0100
+++ b/server/migractions.py Thu Jan 13 19:24:21 2011 +0100
@@ -536,38 +536,21 @@
unique_together = set([frozenset(ut)
for ut in eschema._unique_together])
for ut in repo_unique_together - unique_together:
- restrictions = ', '.join(['C relations R%(i)d, '
- 'R%(i)d relation_type T%(i)d, '
- 'R%(i)d from_entity X, '
- 'T%(i)d name %%(T%(i)d)s' % {'i': i,
- 'col':col}
- for (i, col) in enumerate(ut)])
- substs = {'etype': etype}
+ restrictions = []
+ substs = {'x': repoeschema.eid}
for i, col in enumerate(ut):
+ restrictions.append('C relations T%(i)d, '
+ 'T%(i)d name %%(T%(i)d)s' % {'i': i})
substs['T%d'%i] = col
self.rqlexec('DELETE CWUniqueTogetherConstraint C '
'WHERE C constraint_of E, '
- ' E name %%(etype)s,'
- ' %s' % restrictions,
+ ' E eid %%(x)s,'
+ ' %s' % ', '.join( restrictions),
substs)
for ut in unique_together - repo_unique_together:
- relations = ', '.join(['C relations R%d' % i
- for (i, col) in enumerate(ut)])
- restrictions = ', '.join(['R%(i)d relation_type T%(i)d, '
- 'R%(i)d from_entity E, '
- 'T%(i)d name %%(T%(i)d)s' % {'i': i,
- 'col':col}
- for (i, col) in enumerate(ut)])
- substs = {'etype': etype}
- for i, col in enumerate(ut):
- substs['T%d'%i] = col
- self.rqlexec('INSERT CWUniqueTogetherConstraint C:'
- ' C constraint_of E, '
- ' %s '
- 'WHERE '
- ' E name %%(etype)s,'
- ' %s' % (relations, restrictions),
- substs)
+ rql, substs = ss.uniquetogether2rql(eschema, ut)
+ substs['x'] = repoeschema.eid
+ self.rqlexec(rql, substs)
def _synchronize_rdef_schema(self, subjtype, rtype, objtype,
syncperms=True, syncprops=True):
@@ -1218,8 +1201,14 @@
# Workflows handling ######################################################
+ def cmd_make_workflowable(self, etype):
+ """add workflow relations to an entity type to make it workflowable"""
+ self.cmd_add_relation_definition(etype, 'in_state', 'State')
+ self.cmd_add_relation_definition(etype, 'custom_workflow', 'Workflow')
+ self.cmd_add_relation_definition('TrInfo', 'wf_info_for', etype)
+
def cmd_add_workflow(self, name, wfof, default=True, commit=False,
- **kwargs):
+ ensure_workflowable=True, **kwargs):
"""
create a new workflow and links it to entity types
:type name: unicode
@@ -1239,7 +1228,14 @@
**kwargs)
if not isinstance(wfof, (list, tuple)):
wfof = (wfof,)
+ def _missing_wf_rel(etype):
+ return 'missing workflow relations, see make_workflowable(%s)' % etype
for etype in wfof:
+ eschema = self.repo.schema[etype]
+ if ensure_workflowable:
+ assert 'in_state' in eschema.subjrels, _missing_wf_rel(etype)
+ assert 'custom_workflow' in eschema.subjrels, _missing_wf_rel(etype)
+ assert 'wf_info_for' in eschema.objrels, _missing_wf_rel(etype)
rset = self.rqlexec(
'SET X workflow_of ET WHERE X eid %(x)s, ET name %(et)s',
{'x': wf.eid, 'et': etype}, ask_confirm=False)
--- a/server/msplanner.py Wed Jan 05 18:42:21 2011 +0100
+++ b/server/msplanner.py Thu Jan 13 19:24:21 2011 +0100
@@ -824,7 +824,7 @@
while sourceterms:
# take a term randomly, and all terms supporting the
# same solutions
- term, solindices = self._choose_term(sourceterms)
+ term, solindices = self._choose_term(source, sourceterms)
if source.uri == 'system':
# ensure all variables are available for the latest step
# (missing one will be available from temporary tables
@@ -854,7 +854,7 @@
# set of terms which should be additionaly selected when
# possible
needsel = set()
- if not self._sourcesterms:
+ if not self._sourcesterms and scope is select:
terms += scope.defined_vars.values() + scope.aliases.values()
if isinstance(term, Relation) and len(sources) > 1:
variants = set()
@@ -867,13 +867,10 @@
# before a join with prefetched inputs
# (see test_crossed_relation_noeid_needattr in
# unittest_msplanner / unittest_multisources)
- needsel2 = needsel.copy()
- needsel2.update(variants)
lhs, rhs = term.get_variable_parts()
steps.append( (sources, [term, getattr(lhs, 'variable', lhs),
getattr(rhs, 'variable', rhs)],
- solindices, scope,
- needsel2, False) )
+ solindices, scope, variants, False) )
sources = [self.system_source]
final = True
else:
@@ -906,7 +903,7 @@
break
else:
if not scope is select:
- self._exists_relation(rel, terms, needsel)
+ self._exists_relation(rel, terms, needsel, source)
# if relation is supported by all sources and some of
# its lhs/rhs variable isn't in "terms", and the
# other end *is* in "terms", mark it have to be
@@ -950,9 +947,14 @@
self._cleanup_sourcesterms(sources, solindices)
steps.append((sources, terms, solindices, scope, needsel, final)
)
+ if not steps[-1][-1]:
+ # add a final step
+ terms = select.defined_vars.values() + select.aliases.values()
+ steps.append( ([self.system_source], terms, set(self._solindices),
+ select, set(), True) )
return steps
- def _exists_relation(self, rel, terms, needsel):
+ def _exists_relation(self, rel, terms, needsel, source):
rschema = self._schema.rschema(rel.r_type)
lhs, rhs = rel.get_variable_parts()
try:
@@ -965,13 +967,24 @@
# variable is refed by an outer scope and should be substituted
# using an 'identity' relation (else we'll get a conflict of
# temporary tables)
- if rhsvar in terms and not lhsvar in terms and ms_scope(lhsvar) is lhsvar.stmt:
- self._identity_substitute(rel, lhsvar, terms, needsel)
- elif lhsvar in terms and not rhsvar in terms and ms_scope(rhsvar) is rhsvar.stmt:
- self._identity_substitute(rel, rhsvar, terms, needsel)
+ relscope = ms_scope(rel)
+ lhsscope = ms_scope(lhsvar)
+ rhsscope = ms_scope(rhsvar)
+ if rhsvar in terms and not lhsvar in terms and lhsscope is lhsvar.stmt:
+ self._identity_substitute(rel, lhsvar, terms, needsel, relscope)
+ elif lhsvar in terms and not rhsvar in terms and rhsscope is rhsvar.stmt:
+ self._identity_substitute(rel, rhsvar, terms, needsel, relscope)
+ elif self.crossed_relation(source, rel):
+ if lhsscope is not relscope:
+ self._identity_substitute(rel, lhsvar, terms, needsel,
+ relscope, lhsscope)
+ if rhsscope is not relscope:
+ self._identity_substitute(rel, rhsvar, terms, needsel,
+ relscope, rhsscope)
- def _identity_substitute(self, relation, var, terms, needsel):
- newvar = self._insert_identity_variable(ms_scope(relation), var)
+ def _identity_substitute(self, relation, var, terms, needsel, exist,
+ idrelscope=None):
+ newvar = self._insert_identity_variable(exist, var, idrelscope)
# ensure relation is using '=' operator, else we rely on a
# sqlgenerator side effect (it won't insert an inequality operator
# in this case)
@@ -979,12 +992,28 @@
terms.append(newvar)
needsel.add(newvar.name)
- def _choose_term(self, sourceterms):
+ def _choose_term(self, source, sourceterms):
"""pick one term among terms supported by a source, which will be used
as a base to generate an execution step
"""
secondchoice = None
if len(self._sourcesterms) > 1:
+ # first, return non invariant variable of crossed relation, then the
+ # crossed relation itself
+ for term in sourceterms:
+ if (isinstance(term, Relation)
+ and self.crossed_relation(source, term)
+ and not ms_scope(term) is self.rqlst):
+ for vref in term.get_variable_parts():
+ try:
+ var = vref.variable
+ except AttributeError:
+ # Constant
+ continue
+ if ((len(var.stinfo['relations']) > 1 or var.stinfo['selected'])
+ and var in sourceterms):
+ return var, sourceterms.pop(var)
+ return term, sourceterms.pop(term)
# priority to variable from subscopes
for term in sourceterms:
if not ms_scope(term) is self.rqlst:
--- a/server/repository.py Wed Jan 05 18:42:21 2011 +0100
+++ b/server/repository.py Thu Jan 13 19:24:21 2011 +0100
@@ -1353,8 +1353,9 @@
# pyro handling ###########################################################
- def pyro_register(self, host=''):
- """register the repository as a pyro object"""
+ @property
+ @cached
+ def pyro_appid(self):
from logilab.common import pyro_ext as pyro
config = self.config
appid = '%s.%s' % pyro.ns_group_and_id(
@@ -1362,13 +1363,27 @@
config['pyro-ns-group'])
# ensure config['pyro-instance-id'] is a full qualified pyro name
config['pyro-instance-id'] = appid
- daemon = pyro.register_object(self, appid,
- daemonhost=config['pyro-host'],
- nshost=config['pyro-ns-host'])
- self.info('repository registered as a pyro object %s', appid)
+ return appid
+
+ def pyro_register(self, host=''):
+ """register the repository as a pyro object"""
+ from logilab.common import pyro_ext as pyro
+ daemon = pyro.register_object(self, self.pyro_appid,
+ daemonhost=self.config['pyro-host'],
+ nshost=self.config['pyro-ns-host'])
+ self.info('repository registered as a pyro object %s', self.pyro_appid)
self.pyro_registered = True
+ # register a looping task to regularly ensure we're still registered
+ # into the pyro name server
+ self.looping_task(60*10, self._ensure_pyro_ns)
return daemon
+ def _ensure_pyro_ns(self):
+ from logilab.common import pyro_ext as pyro
+ pyro.ns_reregister(self.pyro_appid, nshost=self.config['pyro-ns-host'])
+ self.info('repository re-registered as a pyro object %s',
+ self.pyro_appid)
+
# multi-sources planner helpers ###########################################
@cached
--- a/server/schemaserial.py Wed Jan 05 18:42:21 2011 +0100
+++ b/server/schemaserial.py Thu Jan 13 19:24:21 2011 +0100
@@ -235,7 +235,7 @@
uniquecstreid, eeid, releid = values
eschema = schema.schema_by_eid(eeid)
relations = unique_togethers.setdefault(uniquecstreid, (eschema, []))
- relations[1].append(ertidx[releid].rtype.type)
+ relations[1].append(ertidx[releid])
for eschema, unique_together in unique_togethers.itervalues():
eschema._unique_together.append(tuple(sorted(unique_together)))
schema.infer_specialization_rules()
@@ -355,6 +355,7 @@
for eschema in eschemas:
for unique_together in eschema._unique_together:
execschemarql(execute, eschema, [uniquetogether2rql(eschema, unique_together)])
+ # serialize yams inheritance relationships
for rql, kwargs in specialize2rql(schema):
execute(rql, kwargs, build_descr=False)
if pb is not None:
@@ -417,23 +418,17 @@
restrictions = []
substs = {}
for i, name in enumerate(unique_together):
- rschema = eschema.rdef(name)
- var = 'R%d' % i
+ rschema = eschema.schema.rschema(name)
rtype = 'T%d' % i
- substs[rtype] = rschema.rtype.type
- relations.append('C relations %s' % var)
- restrictions.append('%(var)s from_entity X, '
- '%(var)s relation_type %(rtype)s, '
- '%(rtype)s name %%(%(rtype)s)s' \
- % {'var': var,
- 'rtype':rtype})
+ substs[rtype] = rschema.type
+ relations.append('C relations %s' % rtype)
+ restrictions.append('%(rtype)s name %%(%(rtype)s)s' % {'rtype': rtype})
relations = ', '.join(relations)
restrictions = ', '.join(restrictions)
rql = ('INSERT CWUniqueTogetherConstraint C: '
' C constraint_of X, %s '
'WHERE '
- ' X eid %%(x)s, %s' )
-
+ ' X eid %%(x)s, %s')
return rql % (relations, restrictions), substs
--- a/server/test/data/migratedapp/schema.py Wed Jan 05 18:42:21 2011 +0100
+++ b/server/test/data/migratedapp/schema.py Thu Jan 13 19:24:21 2011 +0100
@@ -15,9 +15,7 @@
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
-"""
-
-"""
+"""cw.server.migraction test"""
from yams.buildobjs import (EntityType, RelationType, RelationDefinition,
SubjectRelation,
RichString, String, Int, Boolean, Datetime, Date)
@@ -68,7 +66,7 @@
type = String(maxsize=1)
unique_id = String(maxsize=1, required=True, unique=True)
mydate = Date(default='TODAY')
- shortpara = String(maxsize=64)
+ shortpara = String(maxsize=64, default='hop')
ecrit_par = SubjectRelation('Personne', constraints=[RQLConstraint('S concerne A, O concerne A')])
attachment = SubjectRelation('File')
--- a/server/test/unittest_fti.py Wed Jan 05 18:42:21 2011 +0100
+++ b/server/test/unittest_fti.py Thu Jan 13 19:24:21 2011 +0100
@@ -2,6 +2,8 @@
import socket
+from logilab.common.testlib import SkipTest
+
from cubicweb.devtools import ApptestConfiguration
from cubicweb.devtools.testlib import CubicWebTC
from cubicweb.selectors import is_instance
@@ -9,8 +11,6 @@
AT_LOGILAB = socket.gethostname().endswith('.logilab.fr')
-from logilab.common.testlib import SkipTest
-
class PostgresFTITC(CubicWebTC):
config = ApptestConfiguration('data', sourcefile='sources_fti')
--- a/server/test/unittest_hook.py Wed Jan 05 18:42:21 2011 +0100
+++ b/server/test/unittest_hook.py Thu Jan 13 19:24:21 2011 +0100
@@ -18,6 +18,8 @@
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
"""unit/functional tests for cubicweb.server.hook"""
+from __future__ import with_statement
+
from logilab.common.testlib import TestCase, unittest_main, mock_object
@@ -101,20 +103,23 @@
def test_register_bad_hook1(self):
class _Hook(hook.Hook):
events = ('before_add_entiti',)
- ex = self.assertRaises(Exception, self.o.register, _Hook)
- self.assertEqual(str(ex), 'bad event before_add_entiti on %s._Hook' % __name__)
+ with self.assertRaises(Exception) as cm:
+ self.o.register(_Hook)
+ self.assertEqual(str(cm.exception), 'bad event before_add_entiti on %s._Hook' % __name__)
def test_register_bad_hook2(self):
class _Hook(hook.Hook):
events = None
- ex = self.assertRaises(Exception, self.o.register, _Hook)
- self.assertEqual(str(ex), 'bad .events attribute None on %s._Hook' % __name__)
+ with self.assertRaises(Exception) as cm:
+ self.o.register(_Hook)
+ self.assertEqual(str(cm.exception), 'bad .events attribute None on %s._Hook' % __name__)
def test_register_bad_hook3(self):
class _Hook(hook.Hook):
events = 'before_add_entity'
- ex = self.assertRaises(Exception, self.o.register, _Hook)
- self.assertEqual(str(ex), 'bad event b on %s._Hook' % __name__)
+ with self.assertRaises(Exception) as cm:
+ self.o.register(_Hook)
+ self.assertEqual(str(cm.exception), 'bad event b on %s._Hook' % __name__)
def test_call_hook(self):
self.o.register(AddAnyHook)
--- a/server/test/unittest_migractions.py Wed Jan 05 18:42:21 2011 +0100
+++ b/server/test/unittest_migractions.py Thu Jan 13 19:24:21 2011 +0100
@@ -77,6 +77,10 @@
assert self.cnx is self.mh._cnx
assert self.session is self.mh.session, (self.session.id, self.mh.session.id)
+ def tearDown(self):
+ CubicWebTC.tearDown(self)
+ self.repo.vreg['etypes'].clear_caches()
+
def test_add_attribute_int(self):
self.failIf('whatever' in self.schema)
self.request().create_entity('Note')
@@ -88,8 +92,12 @@
self.assertEqual(self.schema['whatever'].subjects(), ('Note',))
self.assertEqual(self.schema['whatever'].objects(), ('Int',))
self.assertEqual(self.schema['Note'].default('whatever'), 2)
+ # test default value set on existing entities
note = self.execute('Note X').get_entity(0, 0)
self.assertEqual(note.whatever, 2)
+ # test default value set for next entities
+ self.assertEqual(self.request().create_entity('Note').whatever, 2)
+ # test attribute order
orderdict2 = dict(self.mh.rqlexec('Any RTN, O WHERE X name "Note", RDEF from_entity X, '
'RDEF relation_type RT, RDEF ordernum O, RT name RTN'))
whateverorder = migrschema['whatever'].rdef('Note', 'Int').order
@@ -108,6 +116,9 @@
self.mh.rollback()
def test_add_attribute_varchar(self):
+ self.failIf('whatever' in self.schema)
+ self.request().create_entity('Note')
+ self.commit()
self.failIf('shortpara' in self.schema)
self.mh.cmd_add_attribute('Note', 'shortpara')
self.failUnless('shortpara' in self.schema)
@@ -117,6 +128,11 @@
notesql = self.mh.sqlexec("SELECT sql FROM sqlite_master WHERE type='table' and name='%sNote'" % SQL_PREFIX)[0][0]
fields = dict(x.strip().split()[:2] for x in notesql.split('(', 1)[1].rsplit(')', 1)[0].split(','))
self.assertEqual(fields['%sshortpara' % SQL_PREFIX], 'varchar(64)')
+ req = self.request()
+ # test default value set on existing entities
+ self.assertEqual(req.execute('Note X').get_entity(0, 0).shortpara, 'hop')
+ # test default value set for next entities
+ self.assertEqual(req.create_entity('Note').shortpara, 'hop')
self.mh.rollback()
def test_add_datetime_with_default_value_attribute(self):
@@ -173,7 +189,8 @@
def test_workflow_actions(self):
- wf = self.mh.cmd_add_workflow(u'foo', ('Personne', 'Email'))
+ wf = self.mh.cmd_add_workflow(u'foo', ('Personne', 'Email'),
+ ensure_workflowable=False)
for etype in ('Personne', 'Email'):
s1 = self.mh.rqlexec('Any N WHERE WF workflow_of ET, ET name "%s", WF name N' %
etype)[0][0]
@@ -209,7 +226,8 @@
def test_add_drop_entity_type(self):
self.mh.cmd_add_entity_type('Folder2')
- wf = self.mh.cmd_add_workflow(u'folder2 wf', 'Folder2')
+ wf = self.mh.cmd_add_workflow(u'folder2 wf', 'Folder2',
+ ensure_workflowable=False)
todo = wf.add_state(u'todo', initial=True)
done = wf.add_state(u'done')
wf.add_transition(u'redoit', done, todo)
@@ -416,7 +434,7 @@
('nom', 'prenom', 'datenaiss'))
rset = cursor.execute('Any C WHERE C is CWUniqueTogetherConstraint, C constraint_of ET, ET name "Personne"')
self.assertEqual(len(rset), 1)
- relations = [r.rtype.name for r in rset.get_entity(0, 0).relations]
+ relations = [r.name for r in rset.get_entity(0, 0).relations]
self.assertItemsEqual(relations, ('nom', 'prenom', 'datenaiss'))
def _erqlexpr_rset(self, action, ertype):
@@ -536,8 +554,9 @@
self.commit()
def test_remove_dep_cube(self):
- ex = self.assertRaises(ConfigurationError, self.mh.cmd_remove_cube, 'file')
- self.assertEqual(str(ex), "can't remove cube file, used as a dependency")
+ with self.assertRaises(ConfigurationError) as cm:
+ self.mh.cmd_remove_cube('file')
+ self.assertEqual(str(cm.exception), "can't remove cube file, used as a dependency")
def test_introduce_base_class(self):
self.mh.cmd_add_entity_type('Para')
--- a/server/test/unittest_msplanner.py Wed Jan 05 18:42:21 2011 +0100
+++ b/server/test/unittest_msplanner.py Thu Jan 13 19:24:21 2011 +0100
@@ -15,6 +15,9 @@
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
+"""unit tests for module cubicweb.server.msplanner"""
+
+from __future__ import with_statement
from logilab.common.decorators import clear_cache
@@ -2013,15 +2016,15 @@
def test_source_conflict_1(self):
self.repo._type_source_cache[999999] = ('Note', 'cards', 999999)
- ex = self.assertRaises(BadRQLQuery,
- self._test, 'Any X WHERE X cw_source S, S name "system", X eid %(x)s',
- [], {'x': 999999})
- self.assertEqual(str(ex), 'source conflict for term %(x)s')
+ with self.assertRaises(BadRQLQuery) as cm:
+ self._test('Any X WHERE X cw_source S, S name "system", X eid %(x)s',
+ [], {'x': 999999})
+ self.assertEqual(str(cm.exception), 'source conflict for term %(x)s')
def test_source_conflict_2(self):
- ex = self.assertRaises(BadRQLQuery,
- self._test, 'Card X WHERE X cw_source S, S name "systeme"', [])
- self.assertEqual(str(ex), 'source conflict for term X')
+ with self.assertRaises(BadRQLQuery) as cm:
+ self._test('Card X WHERE X cw_source S, S name "systeme"', [])
+ self.assertEqual(str(cm.exception), 'source conflict for term X')
def test_source_conflict_3(self):
self.skipTest('oops')
@@ -2473,6 +2476,37 @@
)]
)
+ def test_version_crossed_depends_on_4(self):
+ self._test('Any X,AD,AE WHERE EXISTS(E multisource_crossed_rel X), X in_state AD, AD name AE, E is Note',
+ [('FetchStep',
+ [('Any X,AD,AE WHERE X in_state AD, AD name AE, AD is State, X is Note',
+ [{'X': 'Note', 'AD': 'State', 'AE': 'String'}])],
+ [self.cards, self.cards2, self.system], None,
+ {'X': 'table0.C0',
+ 'AD': 'table0.C1',
+ 'AD.name': 'table0.C2',
+ 'AE': 'table0.C2'},
+ []),
+ ('FetchStep',
+ [('Any A WHERE E multisource_crossed_rel A, A is Note, E is Note',
+ [{'A': 'Note', 'E': 'Note'}])],
+ [self.cards, self.cards2, self.system], None,
+ {'A': 'table1.C0'},
+ []),
+ ('OneFetchStep',
+ [('Any X,AD,AE WHERE EXISTS(X identity A), AD name AE, A is Note, AD is State, X is Note',
+ [{'A': 'Note', 'AD': 'State', 'AE': 'String', 'X': 'Note'}])],
+ None, None,
+ [self.system],
+ {'A': 'table1.C0',
+ 'AD': 'table0.C1',
+ 'AD.name': 'table0.C2',
+ 'AE': 'table0.C2',
+ 'X': 'table0.C0'},
+ []
+ )]
+ )
+
def test_nonregr_dont_cross_rel_source_filtering_1(self):
self.repo._type_source_cache[999999] = ('Note', 'cards', 999999)
self._test('Any S WHERE E eid %(x)s, E in_state S, NOT S name "moved"',
--- a/server/test/unittest_repository.py Wed Jan 05 18:42:21 2011 +0100
+++ b/server/test/unittest_repository.py Thu Jan 13 19:24:21 2011 +0100
@@ -154,8 +154,9 @@
self.assertRaises(ValidationError,
self.execute, 'SET X name "toto" WHERE X is CWGroup, X name "guests"')
self.failUnless(self.execute('Any X WHERE X is CWGroup, X name "toto"'))
- ex = self.assertRaises(QueryError, self.commit)
- self.assertEqual(str(ex), 'transaction must be rollbacked')
+ with self.assertRaises(QueryError) as cm:
+ self.commit()
+ self.assertEqual(str(cm.exception), 'transaction must be rollbacked')
self.rollback()
self.failIf(self.execute('Any X WHERE X is CWGroup, X name "toto"'))
@@ -170,8 +171,9 @@
self.assertRaises(Unauthorized,
self.execute, 'SET X name "toto" WHERE X is CWGroup, X name "guests"')
self.failUnless(self.execute('Any X WHERE X is CWGroup, X name "toto"'))
- ex = self.assertRaises(QueryError, self.commit)
- self.assertEqual(str(ex), 'transaction must be rollbacked')
+ with self.assertRaises(QueryError) as cm:
+ self.commit()
+ self.assertEqual(str(cm.exception), 'transaction must be rollbacked')
self.rollback()
self.failIf(self.execute('Any X WHERE X is CWGroup, X name "toto"'))
@@ -276,8 +278,9 @@
repo.execute(cnxid, 'DELETE CWUser X WHERE X login "toto"')
repo.commit(cnxid)
try:
- ex = self.assertRaises(Exception, run_transaction)
- self.assertEqual(str(ex), 'try to access pool on a closed session')
+ with self.assertRaises(Exception) as cm:
+ run_transaction()
+ self.assertEqual(str(cm.exception), 'try to access pool on a closed session')
finally:
t.join()
@@ -668,8 +671,9 @@
req.cnx.commit()
req = self.request()
req.create_entity('Note', type=u'todo', inline1=a01)
- ex = self.assertRaises(ValidationError, req.cnx.commit)
- self.assertEqual(ex.errors, {'inline1-subject': u'RQLUniqueConstraint S type T, S inline1 A1, A1 todo_by C, Y type T, Y inline1 A2, A2 todo_by C failed'})
+ with self.assertRaises(ValidationError) as cm:
+ req.cnx.commit()
+ self.assertEqual(cm.exception.errors, {'inline1-subject': u'RQLUniqueConstraint S type T, S inline1 A1, A1 todo_by C, Y type T, Y inline1 A2, A2 todo_by C failed'})
if __name__ == '__main__':
unittest_main()
--- a/server/test/unittest_storage.py Wed Jan 05 18:42:21 2011 +0100
+++ b/server/test/unittest_storage.py Thu Jan 13 19:24:21 2011 +0100
@@ -75,6 +75,15 @@
{'f': entity.eid})[0][0]
return fspath.getvalue()
+ def test_bfss_wrong_fspath_usage(self):
+ f1 = self.create_file()
+ self.execute('Any fspath(D) WHERE F eid %(f)s, F data D', {'f': f1.eid})
+ with self.assertRaises(NotImplementedError) as cm:
+ self.execute('Any fspath(F) WHERE F eid %(f)s', {'f': f1.eid})
+ self.assertEqual(str(cm.exception),
+ 'This callback is only available for BytesFileSystemStorage '
+ 'managed attribute. Is FSPATH() argument BFSS managed?')
+
def test_bfss_storage(self):
f1 = self.create_file()
expected_filepath = osp.join(self.tempdir, '%s_data_%s' %
@@ -114,34 +123,34 @@
self.create_file()
def test_source_mapped_attribute_error_cases(self):
- ex = self.assertRaises(QueryError, self.execute,
- 'Any X WHERE X data ~= "hop", X is File')
- self.assertEqual(str(ex), 'can\'t use File.data (X data ILIKE "hop") in restriction')
- ex = self.assertRaises(QueryError, self.execute,
- 'Any X, Y WHERE X data D, Y data D, '
- 'NOT X identity Y, X is File, Y is File')
- self.assertEqual(str(ex), "can't use D as a restriction variable")
+ with self.assertRaises(QueryError) as cm:
+ self.execute('Any X WHERE X data ~= "hop", X is File')
+ self.assertEqual(str(cm.exception), 'can\'t use File.data (X data ILIKE "hop") in restriction')
+ with self.assertRaises(QueryError) as cm:
+ self.execute('Any X, Y WHERE X data D, Y data D, '
+ 'NOT X identity Y, X is File, Y is File')
+ self.assertEqual(str(cm.exception), "can't use D as a restriction variable")
# query returning mix of mapped / regular attributes (only file.data
# mapped, not image.data for instance)
- ex = self.assertRaises(QueryError, self.execute,
- 'Any X WITH X BEING ('
- ' (Any NULL)'
- ' UNION '
- ' (Any D WHERE X data D, X is File)'
- ')')
- self.assertEqual(str(ex), 'query fetch some source mapped attribute, some not')
- ex = self.assertRaises(QueryError, self.execute,
- '(Any D WHERE X data D, X is File)'
- ' UNION '
- '(Any D WHERE X title D, X is Bookmark)')
- self.assertEqual(str(ex), 'query fetch some source mapped attribute, some not')
+ with self.assertRaises(QueryError) as cm:
+ self.execute('Any X WITH X BEING ('
+ ' (Any NULL)'
+ ' UNION '
+ ' (Any D WHERE X data D, X is File)'
+ ')')
+ self.assertEqual(str(cm.exception), 'query fetch some source mapped attribute, some not')
+ with self.assertRaises(QueryError) as cm:
+ self.execute('(Any D WHERE X data D, X is File)'
+ ' UNION '
+ '(Any D WHERE X title D, X is Bookmark)')
+ self.assertEqual(str(cm.exception), 'query fetch some source mapped attribute, some not')
storages.set_attribute_storage(self.repo, 'State', 'name',
storages.BytesFileSystemStorage(self.tempdir))
try:
- ex = self.assertRaises(QueryError,
- self.execute, 'Any D WHERE X name D, X is IN (State, Transition)')
- self.assertEqual(str(ex), 'query fetch some source mapped attribute, some not')
+ with self.assertRaises(QueryError) as cm:
+ self.execute('Any D WHERE X name D, X is IN (State, Transition)')
+ self.assertEqual(str(cm.exception), 'query fetch some source mapped attribute, some not')
finally:
storages.unset_attribute_storage(self.repo, 'State', 'name')
@@ -172,10 +181,10 @@
self.assertEqual(rset[1][0], f1.eid)
self.assertEqual(rset[0][1], len('the-data'))
self.assertEqual(rset[1][1], len('the-data'))
- ex = self.assertRaises(QueryError, self.execute,
- 'Any X,UPPER(D) WHERE X eid %(x)s, X data D',
- {'x': f1.eid})
- self.assertEqual(str(ex), 'UPPER can not be called on mapped attribute')
+ with self.assertRaises(QueryError) as cm:
+ self.execute('Any X,UPPER(D) WHERE X eid %(x)s, X data D',
+ {'x': f1.eid})
+ self.assertEqual(str(cm.exception), 'UPPER can not be called on mapped attribute')
def test_bfss_fs_importing_transparency(self):
--- a/server/test/unittest_undo.py Wed Jan 05 18:42:21 2011 +0100
+++ b/server/test/unittest_undo.py Thu Jan 13 19:24:21 2011 +0100
@@ -212,9 +212,10 @@
self.assertEqual(errors,
[u"Can't restore relation in_group, object entity "
"%s doesn't exist anymore." % g.eid])
- ex = self.assertRaises(ValidationError, self.commit)
- self.assertEqual(ex.entity, self.toto.eid)
- self.assertEqual(ex.errors,
+ with self.assertRaises(ValidationError) as cm:
+ self.commit()
+ self.assertEqual(cm.exception.entity, self.toto.eid)
+ self.assertEqual(cm.exception.errors,
{'in_group-subject': u'at least one relation in_group is '
'required on CWUser (%s)' % self.toto.eid})
@@ -252,10 +253,10 @@
value=u'text/html')
tutu.set_relations(use_email=email, reverse_for_user=prop)
self.commit()
- ex = self.assertRaises(ValidationError,
- self.cnx.undo_transaction, txuuid)
- self.assertEqual(ex.entity, tutu.eid)
- self.assertEqual(ex.errors,
+ with self.assertRaises(ValidationError) as cm:
+ self.cnx.undo_transaction(txuuid)
+ self.assertEqual(cm.exception.entity, tutu.eid)
+ self.assertEqual(cm.exception.errors,
{None: 'some later transaction(s) touch entity, undo them first'})
def test_undo_creation_integrity_2(self):
@@ -265,17 +266,17 @@
session.execute('DELETE U in_group G WHERE U eid %(x)s', {'x': self.toto.eid})
self.toto.set_relations(in_group=g)
self.commit()
- ex = self.assertRaises(ValidationError,
- self.cnx.undo_transaction, txuuid)
- self.assertEqual(ex.entity, g.eid)
- self.assertEqual(ex.errors,
+ with self.assertRaises(ValidationError) as cm:
+ self.cnx.undo_transaction(txuuid)
+ self.assertEqual(cm.exception.entity, g.eid)
+ self.assertEqual(cm.exception.errors,
{None: 'some later transaction(s) touch entity, undo them first'})
# self.assertEqual(errors,
# [u"Can't restore relation in_group, object entity "
# "%s doesn't exist anymore." % g.eid])
- # ex = self.assertRaises(ValidationError, self.commit)
- # self.assertEqual(ex.entity, self.toto.eid)
- # self.assertEqual(ex.errors,
+ # with self.assertRaises(ValidationError) as cm: self.commit()
+ # self.assertEqual(cm.exception.entity, self.toto.eid)
+ # self.assertEqual(cm.exception.errors,
# {'in_group-subject': u'at least one relation in_group is '
# 'required on CWUser (%s)' % self.toto.eid})
--- a/sobjects/notification.py Wed Jan 05 18:42:21 2011 +0100
+++ b/sobjects/notification.py Thu Jan 13 19:24:21 2011 +0100
@@ -27,7 +27,7 @@
from cubicweb.selectors import yes
from cubicweb.view import Component
-from cubicweb.mail import NotificationView, SkipEmail
+from cubicweb.mail import NotificationView as BaseNotificationView, SkipEmail
from cubicweb.server.hook import SendMailOp
@@ -59,7 +59,7 @@
# abstract or deactivated notification views and mixin ########################
-class NotificationView(NotificationView):
+class NotificationView(BaseNotificationView):
"""overriden to delay actual sending of mails to a commit operation by
default
"""
--- a/test/unittest_schema.py Wed Jan 05 18:42:21 2011 +0100
+++ b/test/unittest_schema.py Thu Jan 13 19:24:21 2011 +0100
@@ -17,6 +17,8 @@
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
"""unit tests for module cubicweb.schema"""
+from __future__ import with_statement
+
import sys
from os.path import join, isabs, basename, dirname
@@ -279,9 +281,9 @@
def _test(self, schemafile, msg):
self.loader.handle_file(join(DATADIR, schemafile))
- ex = self.assertRaises(BadSchemaDefinition,
- self.loader._build_schema, 'toto', False)
- self.assertEqual(str(ex), msg)
+ with self.assertRaises(BadSchemaDefinition) as cm:
+ self.loader._build_schema('toto', False)
+ self.assertEqual(str(cm.exception), msg)
def test_rrqlexpr_on_etype(self):
self._test('rrqlexpr_on_eetype.py',
--- a/web/_exceptions.py Wed Jan 05 18:42:21 2011 +0100
+++ b/web/_exceptions.py Thu Jan 13 19:24:21 2011 +0100
@@ -62,7 +62,7 @@
"""raised when a json remote call fails
"""
def __init__(self, reason=''):
- super(RequestError, self).__init__()
+ super(RemoteCallFailed, self).__init__()
self.reason = reason
def dumps(self):
--- a/web/action.py Wed Jan 05 18:42:21 2011 +0100
+++ b/web/action.py Thu Jan 13 19:24:21 2011 +0100
@@ -43,7 +43,7 @@
def fill_menu(self, box, menu):
"""add action(s) to the given submenu of the given box"""
for action in self.actual_actions():
- menu.append(box.box_action(action))
+ menu.append(box.action_link(action))
def html_class(self):
if self._cw.selected(self.url()):
--- a/web/application.py Wed Jan 05 18:42:21 2011 +0100
+++ b/web/application.py Thu Jan 13 19:24:21 2011 +0100
@@ -31,7 +31,7 @@
from cubicweb import set_log_methods, cwvreg
from cubicweb import (
ValidationError, Unauthorized, AuthenticationError, NoSelectableObject,
- RepositoryError, BadConnectionId, CW_EVENT_MANAGER)
+ BadConnectionId, CW_EVENT_MANAGER)
from cubicweb.dbapi import DBAPISession
from cubicweb.web import LOGGER, component
from cubicweb.web import (
@@ -148,8 +148,6 @@
vreg=self.vreg)
global SESSION_MANAGER
SESSION_MANAGER = self.session_manager
- if not 'last_login_time' in self.vreg.schema:
- self._update_last_login_time = lambda x: None
if self.vreg.config.mode != 'test':
# don't try to reset session manager during test, this leads to
# weird failures when running multiple tests
@@ -224,46 +222,9 @@
cookie[sessioncookie]['secure'] = True
req.set_cookie(cookie, sessioncookie, maxage=None)
if not session.anonymous_session:
- self._postlogin(req)
+ self.session_manager.postlogin(req)
return session
- def _update_last_login_time(self, req):
- # XXX should properly detect missing permission / non writeable source
- # and avoid "except (RepositoryError, Unauthorized)" below
- if req.user.cw_metainformation()['source']['type'] == 'ldapuser':
- return
- try:
- req.execute('SET X last_login_time NOW WHERE X eid %(x)s',
- {'x' : req.user.eid})
- req.cnx.commit()
- except (RepositoryError, Unauthorized):
- req.cnx.rollback()
- except:
- req.cnx.rollback()
- raise
-
- def _postlogin(self, req):
- """postlogin: the user has been authenticated, redirect to the original
- page (index by default) with a welcome message
- """
- # Update last connection date
- # XXX: this should be in a post login hook in the repository, but there
- # we can't differentiate actual login of automatic session
- # reopening. Is it actually a problem?
- self._update_last_login_time(req)
- args = req.form
- for forminternal_key in ('__form_id', '__domid', '__errorurl'):
- args.pop(forminternal_key, None)
- args['__message'] = req._('welcome %s !') % req.user.login
- if 'vid' in req.form:
- args['vid'] = req.form['vid']
- if 'rql' in req.form:
- args['rql'] = req.form['rql']
- path = req.relative_path(False)
- if path == 'login':
- path = 'view'
- raise Redirect(req.build_url(path, **args))
-
def logout(self, req, goto_url):
"""logout from the instance by cleaning the session and raising
`AuthenticationError`
--- a/web/component.py Wed Jan 05 18:42:21 2011 +0100
+++ b/web/component.py Thu Jan 13 19:24:21 2011 +0100
@@ -24,7 +24,7 @@
from warnings import warn
-from logilab.common.deprecation import class_deprecated, class_renamed
+from logilab.common.deprecation import class_deprecated, class_renamed, deprecated
from logilab.mtconverter import xml_escape
from cubicweb import Unauthorized, role, target, tags
@@ -36,7 +36,7 @@
non_final_entity, partial_relation_possible,
partial_has_related_entities)
from cubicweb.appobject import AppObject
-from cubicweb.web import INTERNAL_FIELD_VALUE, htmlwidgets, stdmsgs
+from cubicweb.web import INTERNAL_FIELD_VALUE, stdmsgs
# abstract base class for navigation components ################################
@@ -163,6 +163,57 @@
rendered
"""
+
+class Link(object):
+ """a link to a view or action in the ui.
+
+ Use this rather than `cw.web.htmlwidgets.BoxLink`.
+
+ Note this class could probably be avoided with a proper DOM on the server
+ side.
+ """
+ newstyle = True
+
+ def __init__(self, href, label, **attrs):
+ self.href = href
+ self.label = label
+ self.attrs = attrs
+
+ def __unicode__(self):
+ return tags.a(self.label, href=self.href, **self.attrs)
+
+ def render(self, w):
+ w(tags.a(self.label, href=self.href, **self.attrs))
+
+
+class Separator(object):
+ """a menu separator.
+
+ Use this rather than `cw.web.htmlwidgets.BoxSeparator`.
+ """
+ newstyle = True
+
+ def render(self, w):
+ w(u'<hr class="boxSeparator"/>')
+
+
+def _bwcompatible_render_item(w, item):
+ if hasattr(item, 'render'):
+ if getattr(item, 'newstyle', False):
+ if isinstance(item, Separator):
+ w(u'</ul>')
+ item.render(w)
+ w(u'<ul>')
+ else:
+ w(u'<li>')
+ item.render(w)
+ w(u'</li>')
+ else:
+ item.render(w) # XXX displays <li> by itself
+ else:
+ w(u'<li>%s</li>' % item)
+
+
class Layout(Component):
__regid__ = 'layout'
__abstract__ = True
@@ -289,20 +340,31 @@
assert items
w(u'<ul class="%s">' % klass)
for item in items:
- if hasattr(item, 'render'):
- item.render(w) # XXX displays <li> by itself
- else:
- w(u'<li>')
- w(item)
- w(u'</li>')
+ _bwcompatible_render_item(w, item)
w(u'</ul>')
def append(self, item):
self.items.append(item)
+ def action_link(self, action):
+ return self.link(self._cw._(action.title), action.url())
+
+ def link(self, title, url, **kwargs):
+ if self._cw.selected(url):
+ try:
+ kwargs['klass'] += ' selected'
+ except KeyError:
+ kwargs['klass'] = 'selected'
+ return Link(url, title, **kwargs)
+
+ def separator(self):
+ return Separator()
+
+ @deprecated('[3.10] use action_link() / link()')
def box_action(self, action): # XXX action_link
return self.build_link(self._cw._(action.title), action.url())
+ @deprecated('[3.10] use action_link() / link()')
def build_link(self, title, url, **kwargs):
if self._cw.selected(url):
try:
@@ -362,9 +424,9 @@
items = []
for i, (eid, label) in enumerate(rset):
entity = rset.get_entity(i, 0)
- items.append(self.build_link(label, entity.absolute_url()))
+ items.append(self.link(label, entity.absolute_url()))
else:
- items = [self.build_link(e.dc_title(), e.absolute_url())
+ items = [self.link(e.dc_title(), e.absolute_url())
for e in rset.entities()]
self.render_items(w, items)
--- a/web/htmlwidgets.py Wed Jan 05 18:42:21 2011 +0100
+++ b/web/htmlwidgets.py Thu Jan 13 19:24:21 2011 +0100
@@ -25,10 +25,12 @@
from math import floor
from logilab.mtconverter import xml_escape
+from logilab.common.deprecation import class_deprecated
from cubicweb.utils import UStringIO
from cubicweb.uilib import toggle_action, htmlescape
from cubicweb.web import jsonize
+from cubicweb.web.component import _bwcompatible_render_item
# XXX HTMLWidgets should have access to req (for datadir / static urls,
# i18n strings, etc.)
@@ -54,7 +56,8 @@
return False
-class BoxWidget(HTMLWidget):
+class BoxWidget(HTMLWidget): # XXX Deprecated
+
def __init__(self, title, id, items=None, _class="boxFrame",
islist=True, shadow=True, escape=True):
self.title = title
@@ -107,16 +110,16 @@
if self.items:
self.box_begin_content()
for item in self.items:
- if hasattr(item, 'render'):
- item.render(self.w)
- else:
- self.w(u'<li>%s</li>' % item)
+ _bwcompatible_render_item(self.w, item)
self.box_end_content()
self.w(u'</div>')
class SideBoxWidget(BoxWidget):
"""default CubicWeb's sidebox widget"""
+ __metaclass__ = class_deprecated
+ __deprecation_warning__ = '[3.10] class %(cls)s is deprecated'
+
title_class = u'sideBoxTitle'
main_div_class = u'sideBoxBody'
listing_class = ''
@@ -127,6 +130,7 @@
class MenuWidget(BoxWidget):
+
main_div_class = 'menuContent'
listing_class = 'menuListing'
@@ -136,8 +140,9 @@
self.w(u'</div>\n')
-class RawBoxItem(HTMLWidget):
+class RawBoxItem(HTMLWidget): # XXX deprecated
"""a simpe box item displaying raw data"""
+
def __init__(self, label, liclass=None):
self.label = label
self.liclass = liclass
@@ -156,6 +161,7 @@
class BoxMenu(RawBoxItem):
"""a menu in a box"""
+
link_class = 'boxMenu'
def __init__(self, label, items=None, isitem=True, liclass=None, ident=None,
@@ -184,10 +190,7 @@
toggle_action(ident), self.link_class, self.label))
self._begin_menu(ident)
for item in self.items:
- if hasattr(item, 'render'):
- item.render(self.w)
- else:
- self.w(u'<li>%s</li>' % item)
+ _bwcompatible_render_item(self.w, item)
self._end_menu()
if self.isitem:
self.w(u'</li>')
@@ -208,6 +211,8 @@
class BoxField(HTMLWidget):
"""couples label / value meant to be displayed in a box"""
+ __metaclass__ = class_deprecated
+ __deprecation_warning__ = '[3.10] class %(cls)s is deprecated'
def __init__(self, label, value):
self.label = label
self.value = value
@@ -219,6 +224,8 @@
class BoxSeparator(HTMLWidget):
"""a menu separator"""
+ __metaclass__ = class_deprecated
+ __deprecation_warning__ = '[3.10] class %(cls)s is deprecated'
def _render(self):
self.w(u'</ul><hr class="boxSeparator"/><ul>')
@@ -226,6 +233,8 @@
class BoxLink(HTMLWidget):
"""a link in a box"""
+ __metaclass__ = class_deprecated
+ __deprecation_warning__ = '[3.10] class %(cls)s is deprecated'
def __init__(self, href, label, _class='', title='', ident='', escape=False):
self.href = href
if escape:
@@ -247,6 +256,8 @@
class BoxHtml(HTMLWidget):
"""a form in a box"""
+ __metaclass__ = class_deprecated
+ __deprecation_warning__ = '[3.10] class %(cls)s is deprecated'
def __init__(self, rawhtml):
self.rawhtml = rawhtml
@@ -272,6 +283,7 @@
def add_attr(self, attr, value):
self.cell_attrs[attr] = value
+
class SimpleTableModel(object):
"""
uses a list of lists as a storage backend
@@ -283,7 +295,6 @@
def __init__(self, rows):
self._rows = rows
-
def get_rows(self):
return self._rows
--- a/web/test/unittest_application.py Wed Jan 05 18:42:21 2011 +0100
+++ b/web/test/unittest_application.py Thu Jan 13 19:24:21 2011 +0100
@@ -298,8 +298,9 @@
def test_login_not_available_to_authenticated(self):
req = self.request()
- ex = self.assertRaises(Unauthorized, self.app_publish, req, 'login')
- self.assertEqual(str(ex), 'log out first')
+ with self.assertRaises(Unauthorized) as cm:
+ self.app_publish(req, 'login')
+ self.assertEqual(str(cm.exception), 'log out first')
def test_fb_login_concept(self):
"""see data/views.py"""
@@ -367,8 +368,9 @@
# preparing the suite of the test
# set session id in cookie
cookie = Cookie.SimpleCookie()
- cookie['__session'] = req.session.sessionid
- req._headers['Cookie'] = cookie['__session'].OutputString()
+ sessioncookie = self.app.session_handler.session_cookie(req)
+ cookie[sessioncookie] = req.session.sessionid
+ req._headers['Cookie'] = cookie[sessioncookie].OutputString()
clear_cache(req, 'get_authorization')
# reset session as if it was a new incoming request
req.session = req.cnx = None
--- a/web/test/unittest_views_basecontrollers.py Wed Jan 05 18:42:21 2011 +0100
+++ b/web/test/unittest_views_basecontrollers.py Thu Jan 13 19:24:21 2011 +0100
@@ -47,8 +47,9 @@
def test_noparam_edit(self):
"""check behaviour of this controller without any form parameter
"""
- ex = self.assertRaises(ValidationError, self.ctrl_publish, self.request())
- self.assertEqual(ex.errors, {None: u'no selected entities'})
+ with self.assertRaises(ValidationError) as cm:
+ self.ctrl_publish(self.request())
+ self.assertEqual(cm.exception.errors, {None: u'no selected entities'})
def test_validation_unique(self):
"""test creation of two linked entities
@@ -61,8 +62,9 @@
'upassword-subject:X': u'toto',
'upassword-subject-confirm:X': u'toto',
}
- ex = self.assertRaises(ValidationError, self.ctrl_publish, req)
- self.assertEqual(ex.errors, {'login-subject': 'the value "admin" is already used, use another one'})
+ with self.assertRaises(ValidationError) as cm:
+ self.ctrl_publish(req)
+ self.assertEqual(cm.exception.errors, {'login-subject': 'the value "admin" is already used, use another one'})
def test_user_editing_itself(self):
"""checking that a manager user can edit itself
@@ -205,8 +207,9 @@
'login-subject:X': u'toto',
'upassword-subject:X': u'toto',
}
- ex = self.assertRaises(ValidationError, self.ctrl_publish, req)
- self.assertEqual(ex.errors, {'upassword-subject': u'password and confirmation don\'t match'})
+ with self.assertRaises(ValidationError) as cm:
+ self.ctrl_publish(req)
+ self.assertEqual(cm.exception.errors, {'upassword-subject': u'password and confirmation don\'t match'})
req = self.request()
req.form = {'__cloned_eid:X': u(user.eid),
'eid': 'X', '__type:X': 'CWUser',
@@ -215,8 +218,9 @@
'upassword-subject:X': u'toto',
'upassword-subject-confirm:X': u'tutu',
}
- ex = self.assertRaises(ValidationError, self.ctrl_publish, req)
- self.assertEqual(ex.errors, {'upassword-subject': u'password and confirmation don\'t match'})
+ with self.assertRaises(ValidationError) as cm:
+ self.ctrl_publish(req)
+ self.assertEqual(cm.exception.errors, {'upassword-subject': u'password and confirmation don\'t match'})
def test_interval_bound_constraint_success(self):
@@ -230,8 +234,9 @@
'amount-subject:X': u'-10',
'described_by_test-subject:X': u(feid),
}
- ex = self.assertRaises(ValidationError, self.ctrl_publish, req)
- self.assertEqual(ex.errors, {'amount-subject': 'value must be >= 0'})
+ with self.assertRaises(ValidationError) as cm:
+ self.ctrl_publish(req)
+ self.assertEqual(cm.exception.errors, {'amount-subject': 'value must be >= 0'})
req = self.request(rollbackfirst=True)
req.form = {'eid': ['X'],
'__type:X': 'Salesterm',
@@ -239,8 +244,9 @@
'amount-subject:X': u'110',
'described_by_test-subject:X': u(feid),
}
- ex = self.assertRaises(ValidationError, self.ctrl_publish, req)
- self.assertEqual(ex.errors, {'amount-subject': 'value must be <= 100'})
+ with self.assertRaises(ValidationError) as cm:
+ self.ctrl_publish(req)
+ self.assertEqual(cm.exception.errors, {'amount-subject': 'value must be <= 100'})
req = self.request(rollbackfirst=True)
req.form = {'eid': ['X'],
'__type:X': 'Salesterm',
@@ -421,8 +427,9 @@
'alias-subject:Y': u'',
'use_email-object:Y': 'X',
}
- ex = self.assertRaises(ValidationError, self.ctrl_publish, req)
- self.assertEqual(ex.errors, {'address-subject': u'required field'})
+ with self.assertRaises(ValidationError) as cm:
+ self.ctrl_publish(req)
+ self.assertEqual(cm.exception.errors, {'address-subject': u'required field'})
def test_nonregr_copy(self):
user = self.user()
--- a/web/views/basecomponents.py Wed Jan 05 18:42:21 2011 +0100
+++ b/web/views/basecomponents.py Thu Jan 13 19:24:21 2011 +0100
@@ -36,8 +36,7 @@
from cubicweb.utils import wrap_on_write
from cubicweb.uilib import toggle_action
from cubicweb.web import component, uicfg
-from cubicweb.web.htmlwidgets import (MenuWidget, PopupBoxMenu, BoxSeparator,
- BoxLink)
+from cubicweb.web.htmlwidgets import MenuWidget, PopupBoxMenu
VISIBLE_PROP_DEF = {
_('visible'): dict(type='Boolean', default=True,
@@ -167,13 +166,11 @@
menu = PopupBoxMenu(self._cw.user.login, isitem=False)
box.append(menu)
for action in actions.get('useractions', ()):
- menu.append(BoxLink(action.url(), self._cw._(action.title),
- action.html_class()))
+ menu.append(self.action_link(action))
if actions.get('useractions') and actions.get('siteactions'):
- menu.append(BoxSeparator())
+ menu.append(self.separator())
for action in actions.get('siteactions', ()):
- menu.append(BoxLink(action.url(), self._cw._(action.title),
- action.html_class()))
+ menu.append(self.action_link(action))
box.render(w=w)
--- a/web/views/basecontrollers.py Wed Jan 05 18:42:21 2011 +0100
+++ b/web/views/basecontrollers.py Thu Jan 13 19:24:21 2011 +0100
@@ -419,7 +419,7 @@
**optional_kwargs(extraargs))
#except NoSelectableObject:
# raise RemoteCallFailed('unselectable')
- return self._call_view(comp, **extraargs)
+ return self._call_view(comp, **optional_kwargs(extraargs))
@xhtmlize
def js_render(self, registry, oid, eid=None,
--- a/web/views/basetemplates.py Wed Jan 05 18:42:21 2011 +0100
+++ b/web/views/basetemplates.py Thu Jan 13 19:24:21 2011 +0100
@@ -367,13 +367,15 @@
class HTMLPageFooter(View):
- """default html page footer: include footer actions
- """
+ """default html page footer: include footer actions"""
__regid__ = 'footer'
def call(self, **kwargs):
- req = self._cw
self.w(u'<div id="footer">')
+ self.footer_content()
+ self.w(u'</div>')
+
+ def footer_content(self):
actions = self._cw.vreg['actions'].possible_actions(self._cw,
rset=self.cw_rset)
footeractions = actions.get('footer', ())
@@ -382,8 +384,6 @@
self._cw._(action.title)))
if i < (len(footeractions) - 1):
self.w(u' | ')
- self.w(u'</div>')
-
class HTMLContentHeader(View):
"""default html page content header:
--- a/web/views/bookmark.py Wed Jan 05 18:42:21 2011 +0100
+++ b/web/views/bookmark.py Thu Jan 13 19:24:21 2011 +0100
@@ -98,7 +98,7 @@
if self.can_delete:
req.add_js('cubicweb.ajax.js')
for bookmark in self.bookmarks_rset.entities():
- label = self.build_link(bookmark.title, bookmark.action_url())
+ label = self.link(bookmark.title, bookmark.action_url())
if self.can_delete:
dlink = u'[<a class="action" href="javascript:removeBookmark(%s)" title="%s">-</a>]' % (
bookmark.eid, req._('delete this bookmark'))
@@ -114,7 +114,7 @@
# default value for bookmark's title
url = req.vreg['etypes'].etype_class('Bookmark').cw_create_url(
req, __linkto=linkto, path=path)
- menu.append(self.build_link(req._('bookmark this page'), url))
+ menu.append(self.link(req._('bookmark this page'), url))
if self.bookmarks_rset:
if req.user.is_in_group('managers'):
bookmarksrql = 'Bookmark B WHERE B bookmarked_by U, U eid %s' % ueid
@@ -127,9 +127,9 @@
bookmarksrql %= {'x': ueid}
if erset:
url = req.build_url(vid='muledit', rql=bookmarksrql)
- menu.append(self.build_link(req._('edit bookmarks'), url))
+ menu.append(self.link(req._('edit bookmarks'), url))
url = req.user.absolute_url(vid='xaddrelation', rtype='bookmarked_by',
target='subject')
- menu.append(self.build_link(req._('pick existing bookmarks'), url))
+ menu.append(self.link(req._('pick existing bookmarks'), url))
self.append(menu)
self.render_items(w)
--- a/web/views/boxes.py Wed Jan 05 18:42:21 2011 +0100
+++ b/web/views/boxes.py Thu Jan 13 19:24:21 2011 +0100
@@ -51,7 +51,7 @@
class EditBox(component.CtxComponent): # XXX rename to ActionsBox
"""
box with all actions impacting the entity displayed: edit, copy, delete
- change state, add related entities
+ change state, add related entities...
"""
__regid__ = 'edit_box'
__select__ = component.CtxComponent.__select__ & non_final_entity()
@@ -127,7 +127,7 @@
if hasattr(boxlink, 'label'):
boxlink.label = u'%s %s' % (submenu.label_prefix, boxlink.label)
else:
- submenu.items[0] = u'%s %s' % (submenu.label_prefix, boxlink)
+ boxlink = u'%s %s' % (submenu.label_prefix, boxlink)
box.append(boxlink)
elif submenu.items:
box.append(submenu)
@@ -187,7 +187,7 @@
for category, views in box.sort_by_category(self.views):
menu = htmlwidgets.BoxMenu(category)
for view in views:
- menu.append(self.box_action(view))
+ menu.append(self.action_link(view))
self.append(menu)
self.render_items(w)
@@ -218,7 +218,7 @@
@property
def domid(self):
- return super(RsetBox, self).domid + unicode(abs(id(self)))
+ return super(RsetBox, self).domid + unicode(abs(id(self))) + unicode(abs(id(self.cw_rset)))
def render_title(self, w):
w(self.cw_extra_kwargs['title'])
--- a/web/views/idownloadable.py Wed Jan 05 18:42:21 2011 +0100
+++ b/web/views/idownloadable.py Thu Jan 13 19:24:21 2011 +0100
@@ -21,6 +21,7 @@
_ = unicode
from logilab.mtconverter import BINARY_ENCODINGS, TransformError, xml_escape
+from logilab.common.deprecation import class_renamed, deprecated
from cubicweb import tags
from cubicweb.view import EntityView
@@ -31,7 +32,7 @@
from cubicweb.web.views import primary, baseviews
-# XXX deprecated
+@deprecated('[3.10] use a custom IDownloadable adapter instead')
def download_box(w, entity, title=None, label=None, footer=u''):
req = entity._cw
w(u'<div class="sideBox">')
@@ -62,10 +63,12 @@
def render_body(self, w):
for item in self.items:
+ idownloadable = item.cw_adapt_to('IDownloadable')
w(u'<a href="%s"><img src="%s" alt="%s"/> %s</a>'
- % (xml_escape(item.cw_adapt_to('IDownloadable').download_url()),
+ % (xml_escape(idownloadable.download_url()),
self._cw.uiprops['DOWNLOAD_ICON'],
- self._cw._('download icon'), xml_escape(item.dc_title())))
+ self._cw._('download icon'),
+ xml_escape(idownloadable.download_file_name())))
class DownloadView(EntityView):
@@ -154,7 +157,7 @@
return False
-class IDownloadableLineView(baseviews.OneLineView):
+class IDownloadableOneLineView(baseviews.OneLineView):
__select__ = adaptable('IDownloadable')
def cell_call(self, row, col, title=None, **kwargs):
@@ -162,11 +165,15 @@
entity = self.cw_rset.get_entity(row, col)
url = xml_escape(entity.absolute_url())
adapter = entity.cw_adapt_to('IDownloadable')
- name = xml_escape(title or adapter.download_file_name())
+ name = xml_escape(title or entity.dc_title())
durl = xml_escape(adapter.download_url())
self.w(u'<a href="%s">%s</a> [<a href="%s">%s</a>]' %
(url, name, durl, self._cw._('download')))
+IDownloadableLineView = class_renamed(
+ 'IDownloadableLineView', IDownloadableOneLineView,
+ '[3.10] IDownloadableLineView is deprecated, use %IDownloadableOneLineView')
+
class AbstractEmbeddedView(EntityView):
__abstract__ = True
--- a/web/views/pyviews.py Wed Jan 05 18:42:21 2011 +0100
+++ b/web/views/pyviews.py Thu Jan 13 19:24:21 2011 +0100
@@ -24,6 +24,10 @@
class PyValTableView(View):
+ """display a list of list of values into an html table.
+
+ Take care, content is NOT xml-escaped.
+ """
__regid__ = 'pyvaltable'
__select__ = match_kwargs('pyvalue')
@@ -50,6 +54,10 @@
class PyValListView(View):
+ """display a list of values into an html list.
+
+ Take care, content is NOT xml-escaped.
+ """
__regid__ = 'pyvallist'
__select__ = match_kwargs('pyvalue')
--- a/web/views/sessions.py Wed Jan 05 18:42:21 2011 +0100
+++ b/web/views/sessions.py Thu Jan 13 19:24:21 2011 +0100
@@ -21,7 +21,8 @@
__docformat__ = "restructuredtext en"
-from cubicweb.web import InvalidSession
+from cubicweb import RepositoryError, Unauthorized
+from cubicweb.web import InvalidSession, Redirect
from cubicweb.web.application import AbstractSessionManager
from cubicweb.dbapi import DBAPISession
@@ -75,6 +76,44 @@
req.set_session(session)
return session
+ def postlogin(self, req):
+ """postlogin: the user has been authenticated, redirect to the original
+ page (index by default) with a welcome message
+ """
+ # Update last connection date
+ # XXX: this should be in a post login hook in the repository, but there
+ # we can't differentiate actual login of automatic session
+ # reopening. Is it actually a problem?
+ if 'last_login_time' in req.vreg.schema:
+ self._update_last_login_time(req)
+ args = req.form
+ for forminternal_key in ('__form_id', '__domid', '__errorurl'):
+ args.pop(forminternal_key, None)
+ args['__message'] = req._('welcome %s !') % req.user.login
+ if 'vid' in req.form:
+ args['vid'] = req.form['vid']
+ if 'rql' in req.form:
+ args['rql'] = req.form['rql']
+ path = req.relative_path(False)
+ if path == 'login':
+ path = 'view'
+ raise Redirect(req.build_url(path, **args))
+
+ def _update_last_login_time(self, req):
+ # XXX should properly detect missing permission / non writeable source
+ # and avoid "except (RepositoryError, Unauthorized)" below
+ if req.user.cw_metainformation()['source']['type'] == 'ldapuser':
+ return
+ try:
+ req.execute('SET X last_login_time NOW WHERE X eid %(x)s',
+ {'x' : req.user.eid})
+ req.cnx.commit()
+ except (RepositoryError, Unauthorized):
+ req.cnx.rollback()
+ except:
+ req.cnx.rollback()
+ raise
+
def close_session(self, session):
"""close session on logout or on invalid session detected (expired out,
corrupted...)
--- a/web/views/tableview.py Wed Jan 05 18:42:21 2011 +0100
+++ b/web/views/tableview.py Thu Jan 13 19:24:21 2011 +0100
@@ -28,8 +28,9 @@
from cubicweb import tags
from cubicweb.uilib import toggle_action, limitsize, htmlescape
from cubicweb.web import jsonize
+from cubicweb.web.component import Link
from cubicweb.web.htmlwidgets import (TableWidget, TableColumn, MenuWidget,
- PopupBoxMenu, BoxLink)
+ PopupBoxMenu)
from cubicweb.web.facet import prepare_facets_rqlst, filter_hiddens
class TableView(AnyRsetView):
@@ -212,7 +213,7 @@
ident='%sActions' % divid)
box.append(menu)
for url, label, klass, ident in actions:
- menu.append(BoxLink(url, label, klass, ident=ident, escape=True))
+ menu.append(Link(url, label, klass=klass, id=ident))
box.render(w=self.w)
self.w(u'<div class="clear"/>')