--- a/dbapi.py Mon May 23 11:36:43 2011 +0200
+++ b/dbapi.py Wed May 25 10:59:26 2011 +0200
@@ -30,6 +30,7 @@
from itertools import count
from warnings import warn
from os.path import join
+from uuid import uuid4
from logilab.common.logging_ext import set_log_methods
from logilab.common.decorators import monkeypatch
@@ -246,7 +247,7 @@
if cnx is not None:
self.sessionid = cnx.sessionid
else:
- self.sessionid = None
+ self.sessionid = uuid4().hex
@property
def anonymous_session(self):
--- a/entity.py Mon May 23 11:36:43 2011 +0200
+++ b/entity.py Wed May 25 10:59:26 2011 +0200
@@ -62,6 +62,23 @@
return True
+def remove_ambiguous_rels(attr_set, subjtypes, schema):
+ '''remove from `attr_set` the relations of entity types `subjtypes` that have
+ different entity type sets as target'''
+ for attr in attr_set.copy():
+ rschema = schema.rschema(attr)
+ if rschema.final:
+ continue
+ ttypes = None
+ for subjtype in subjtypes:
+ cur_ttypes = rschema.objects(subjtype)
+ if ttypes is None:
+ ttypes = cur_ttypes
+ elif cur_ttypes != ttypes:
+ attr_set.remove(attr)
+ break
+
+
class Entity(AppObject):
"""an entity instance has e_schema automagically set on
the class and instances has access to their issuing cursor.
@@ -222,6 +239,7 @@
if len(targettypes) > 1:
# find fetch_attrs common to all destination types
fetchattrs = user._cw.vreg['etypes'].fetch_attrs(targettypes)
+ remove_ambiguous_rels(fetchattrs, targettypes, user._cw.vreg.schema)
else:
fetchattrs = etypecls.fetch_attrs
etypecls._fetch_restrictions(var, varmaker, fetchattrs,
@@ -739,6 +757,10 @@
etypecls = self._cw.vreg['etypes'].etype_class(targettypes[0])
if len(targettypes) > 1:
fetchattrs = self._cw.vreg['etypes'].fetch_attrs(targettypes)
+ # XXX we should fetch ambiguous relation objects too but not
+ # recurse on them in _fetch_restrictions; it is easier to remove
+ # them completely for now, as it would require an deeper api rewrite
+ remove_ambiguous_rels(fetchattrs, targettypes, self._cw.vreg.schema)
else:
fetchattrs = etypecls.fetch_attrs
rql = etypecls.fetch_rql(self._cw.user, [restriction], fetchattrs,
--- a/i18n/de.po Mon May 23 11:36:43 2011 +0200
+++ b/i18n/de.po Wed May 25 10:59:26 2011 +0200
@@ -4040,7 +4040,7 @@
msgid "tr_count"
msgstr ""
-msgid "transaction undoed"
+msgid "transaction undone"
msgstr "Transaktion rückgängig gemacht"
#, python-format
--- a/i18n/en.po Mon May 23 11:36:43 2011 +0200
+++ b/i18n/en.po Wed May 25 10:59:26 2011 +0200
@@ -3935,7 +3935,7 @@
msgid "tr_count"
msgstr "transition number"
-msgid "transaction undoed"
+msgid "transaction undone"
msgstr ""
#, python-format
--- a/i18n/es.po Mon May 23 11:36:43 2011 +0200
+++ b/i18n/es.po Wed May 25 10:59:26 2011 +0200
@@ -4090,7 +4090,7 @@
msgid "tr_count"
msgstr "n° de transición"
-msgid "transaction undoed"
+msgid "transaction undone"
msgstr "Transacciones Anuladas"
#, python-format
--- a/i18n/fr.po Mon May 23 11:36:43 2011 +0200
+++ b/i18n/fr.po Wed May 25 10:59:26 2011 +0200
@@ -4091,7 +4091,7 @@
msgid "tr_count"
msgstr "n° de transition"
-msgid "transaction undoed"
+msgid "transaction undone"
msgstr "transaction annulées"
#, python-format
--- a/selectors.py Mon May 23 11:36:43 2011 +0200
+++ b/selectors.py Wed May 25 10:59:26 2011 +0200
@@ -1342,6 +1342,8 @@
@lltrace
def __call__(self, cls, req, rset=None, row=None, col=0, **kwargs):
+ if not req.cnx:
+ return 0
user = req.user
if user is None:
return int('guests' in self.expected)
--- a/server/sources/rql2sql.py Mon May 23 11:36:43 2011 +0200
+++ b/server/sources/rql2sql.py Wed May 25 10:59:26 2011 +0200
@@ -72,7 +72,9 @@
stack.append(self.source_execute)
FunctionDescr.update_cb_stack = default_update_cb_stack
-LENGTH = SQL_FUNCTIONS_REGISTRY.get_function('LENGTH')
+get_func_descr = SQL_FUNCTIONS_REGISTRY.get_function
+
+LENGTH = get_func_descr('LENGTH')
def length_source_execute(source, session, value):
return len(value.getvalue())
LENGTH.source_execute = length_source_execute
@@ -271,7 +273,9 @@
# when a query is grouped, ensure sort terms are grouped as well
for sortterm in sorts:
term = sortterm.term
- if not isinstance(term, Constant):
+ if not (isinstance(term, Constant) or \
+ (isinstance(term, Function) and
+ get_func_descr(term.name).aggregat)):
for vref in term.iget_nodes(VariableRef):
if not vref in groups:
groups.append(vref)
@@ -308,12 +312,12 @@
break
if not isinstance(node, Function):
raise QueryError()
- func = SQL_FUNCTIONS_REGISTRY.get_function(node.name)
- if func.source_execute is None:
+ funcd = get_func_descr(node.name)
+ if funcd.source_execute is None:
raise QueryError('%s can not be called on mapped attribute'
% node.name)
state.source_cb_funcs.add(node)
- func.update_cb_stack(stack)
+ funcd.update_cb_stack(stack)
# IGenerator implementation for RQL->SQL #######################################
--- a/server/test/unittest_rql2sql.py Mon May 23 11:36:43 2011 +0200
+++ b/server/test/unittest_rql2sql.py Wed May 25 10:59:26 2011 +0200
@@ -536,6 +536,14 @@
'''SELECT _X.cw_eid
FROM cw_CWEType AS _X
WHERE _X.cw_description!=parent AND _X.cw_description!=_X.cw_name'''),
+
+ ('DISTINCT Any X, SUM(C) GROUPBY X ORDERBY SUM(C) DESC WHERE H todo_by X, H duration C',
+ '''SELECT DISTINCT rel_todo_by0.eid_to, SUM(_H.cw_duration)
+FROM cw_Affaire AS _H, todo_by_relation AS rel_todo_by0
+WHERE rel_todo_by0.eid_from=_H.cw_eid
+GROUP BY rel_todo_by0.eid_to
+ORDER BY 2 DESC'''),
+
]
ADVANCED_WITH_GROUP_CONCAT = [
--- a/server/test/unittest_undo.py Mon May 23 11:36:43 2011 +0200
+++ b/server/test/unittest_undo.py Wed May 25 10:59:26 2011 +0200
@@ -169,7 +169,7 @@
['CWUser'])
self.assertEqual([et.name for et in toto.is_instance_of],
['CWUser'])
- # undoing shouldn't be visble in undoable transaction, and the undoed
+ # undoing shouldn't be visble in undoable transaction, and the undone
# transaction should be removed
txs = self.cnx.undoable_transactions()
self.assertEqual(len(txs), 2)
--- a/test/data/schema.py Mon May 23 11:36:43 2011 +0200
+++ b/test/data/schema.py Wed May 25 10:59:26 2011 +0200
@@ -18,9 +18,11 @@
from yams.buildobjs import (EntityType, String, SubjectRelation,
RelationDefinition)
+
from cubicweb.schema import (WorkflowableEntityType,
RQLConstraint, RQLVocabularyConstraint)
+
class Personne(EntityType):
nom = String(required=True)
prenom = String()
@@ -36,22 +38,40 @@
RQLVocabularyConstraint('NOT (S connait P, P nom "toto")'),
RQLVocabularyConstraint('S travaille P, P nom "tutu"')])
+
class Societe(EntityType):
nom = String()
evaluee = SubjectRelation('Note')
+ fournit = SubjectRelation(('Service', 'Produit'), cardinality='1*')
+
+
+class Service(EntityType):
+ fabrique_par = SubjectRelation('Personne', cardinality='1*')
+
+
+class Produit(EntityType):
+ fabrique_par = SubjectRelation('Usine', cardinality='1*')
+
+
+class Usine(EntityType):
+ lieu = String(required=True)
+
class Note(EntityType):
type = String()
ecrit_par = SubjectRelation('Personne')
+
class SubNote(Note):
__specializes_schema__ = True
description = String()
+
class tags(RelationDefinition):
subject = 'Tag'
object = ('Personne', 'Note')
+
class evaluee(RelationDefinition):
subject = 'CWUser'
object = 'Note'
@@ -59,5 +79,3 @@
class StateFull(WorkflowableEntityType):
name = String()
-
-
--- a/test/unittest_entity.py Mon May 23 11:36:43 2011 +0200
+++ b/test/unittest_entity.py Wed May 25 10:59:26 2011 +0200
@@ -250,7 +250,7 @@
'WHERE E eid %(x)s, E tags X, X is IN (Personne), X nom AA, '
'X modification_date AB')
- def test_related_rql_ambigous_cant_use_fetch_order(self):
+ def test_related_rql_ambiguous_cant_use_fetch_order(self):
tag = self.vreg['etypes'].etype_class('Tag')(self.request())
for ttype in self.schema['tags'].objects():
self.vreg['etypes'].etype_class(ttype).fetch_attrs = ('modification_date',)
@@ -258,6 +258,18 @@
'Any X,AA ORDERBY AA DESC '
'WHERE E eid %(x)s, E tags X, X modification_date AA')
+ def test_related_rql_cant_fetch_ambiguous_rtype(self):
+ soc_etype = self.vreg['etypes'].etype_class('Societe')
+ soc = soc_etype(self.request())
+ soc_etype.fetch_attrs = ('fournit',)
+ self.vreg['etypes'].etype_class('Service').fetch_attrs = ('fabrique_par',)
+ self.vreg['etypes'].etype_class('Produit').fetch_attrs = ('fabrique_par',)
+ self.vreg['etypes'].etype_class('Usine').fetch_attrs = ('lieu',)
+ self.vreg['etypes'].etype_class('Personne').fetch_attrs = ('nom',)
+ # XXX should be improved: we could fetch fabrique_par object too
+ self.assertEqual(soc.cw_related_rql('fournit', 'subject'),
+ 'Any X WHERE E eid %(x)s, E fournit X')
+
def test_unrelated_rql_security_1_manager(self):
user = self.request().user
rql = user.cw_unrelated_rql('use_email', 'EmailAddress', 'subject')[0]
--- a/test/unittest_schema.py Mon May 23 11:36:43 2011 +0200
+++ b/test/unittest_schema.py Wed May 25 10:59:26 2011 +0200
@@ -166,10 +166,11 @@
'CWSource', 'CWSourceHostConfig', 'CWSourceSchemaConfig',
'CWUniqueTogetherConstraint', 'CWUser',
'ExternalUri', 'File', 'Float', 'Int', 'Interval', 'Note',
- 'Password', 'Personne',
+ 'Password', 'Personne', 'Produit',
'RQLExpression',
- 'Societe', 'State', 'StateFull', 'String', 'SubNote', 'SubWorkflowExitPoint',
+ 'Service', 'Societe', 'State', 'StateFull', 'String', 'SubNote', 'SubWorkflowExitPoint',
'Tag', 'TZDatetime', 'TZTime', 'Time', 'Transition', 'TrInfo',
+ 'Usine',
'Workflow', 'WorkflowTransition']
self.assertListEqual(sorted(expected_entities), entities)
relations = sorted([str(r) for r in schema.relations()])
@@ -188,14 +189,14 @@
'ecrit_par', 'eid', 'evaluee', 'expression', 'exprtype',
- 'final', 'firstname', 'for_user',
+ 'fabrique_par', 'final', 'firstname', 'for_user', 'fournit',
'from_entity', 'from_state', 'fulltext_container', 'fulltextindexed',
'has_text',
'identity', 'in_group', 'in_state', 'indexed',
'initial_state', 'inlined', 'internationalizable', 'is', 'is_instance_of',
- 'label', 'last_login_time', 'latest_retrieval', 'login',
+ 'label', 'last_login_time', 'latest_retrieval', 'lieu', 'login',
'mainvars', 'match_host', 'modification_date',
--- a/web/application.py Mon May 23 11:36:43 2011 +0200
+++ b/web/application.py Wed May 25 10:59:26 2011 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -204,17 +204,34 @@
except InvalidSession:
# try to open a new session, so we get an anonymous session if
# allowed
- try:
- session = self.open_session(req)
- except AuthenticationError:
- req.remove_cookie(cookie, sessioncookie)
- raise
+ session = self.open_session(req)
+ else:
+ if not session.cnx:
+ # session exists but is not bound to a connection. We should
+ # try to authenticate
+ loginsucceed = False
+ try:
+ if self.open_session(req, allow_no_cnx=False):
+ loginsucceed = True
+ except Redirect:
+ # may be raised in open_session (by postlogin mechanism)
+ # on successful connection
+ loginsucceed = True
+ raise
+ except AuthenticationError:
+ # authentication failed, continue to use this session
+ req.set_session(session)
+ finally:
+ if loginsucceed:
+ # session should be replaced by new session created
+ # in open_session
+ self.session_manager.close_session(session)
def get_session(self, req, sessionid):
return self.session_manager.get_session(req, sessionid)
- def open_session(self, req):
- session = self.session_manager.open_session(req)
+ def open_session(self, req, allow_no_cnx=True):
+ session = self.session_manager.open_session(req, allow_no_cnx=allow_no_cnx)
cookie = req.get_cookie()
sessioncookie = self.session_cookie(req)
cookie[sessioncookie] = session.sessionid
@@ -279,10 +296,7 @@
sessions (i.e. a new connection may be created or an already existing
one may be reused
"""
- try:
- self.session_handler.set_session(req)
- except AuthenticationError:
- req.set_session(DBAPISession(None))
+ self.session_handler.set_session(req)
# publish methods #########################################################
@@ -365,11 +379,12 @@
# redirect is raised by edit controller when everything went fine,
# so try to commit
try:
- txuuid = req.cnx.commit()
- if txuuid is not None:
- msg = u'<span class="undo">[<a href="%s">%s</a>]</span>' %(
- req.build_url('undo', txuuid=txuuid), req._('undo'))
- req.append_to_redirect_message(msg)
+ if req.cnx:
+ txuuid = req.cnx.commit()
+ if txuuid is not None:
+ msg = u'<span class="undo">[<a href="%s">%s</a>]</span>' %(
+ req.build_url('undo', txuuid=txuuid), req._('undo'))
+ req.append_to_redirect_message(msg)
except ValidationError, ex:
self.validation_error_handler(req, ex)
except Unauthorized, ex:
--- a/web/data/cubicweb.ajax.js Mon May 23 11:36:43 2011 +0200
+++ b/web/data/cubicweb.ajax.js Wed May 25 10:59:26 2011 +0200
@@ -738,43 +738,37 @@
}
);
-remoteExec = cw.utils.deprecatedFunction(
- '[3.9] remoteExec() is deprecated, use loadRemote instead',
- function(fname /* ... */) {
- setProgressCursor();
- var props = {
- fname: fname,
- pageid: pageid,
- arg: $.map(cw.utils.sliceList(arguments, 1), jQuery.toJSON)
- };
- var result = jQuery.ajax({
- url: JSON_BASE_URL,
- data: props,
- async: false,
- traditional: true
- }).responseText;
- if (result) {
- result = cw.evalJSON(result);
- }
- resetCursor();
- return result;
+function remoteExec(fname /* ... */) {
+ setProgressCursor();
+ var props = {
+ fname: fname,
+ pageid: pageid,
+ arg: $.map(cw.utils.sliceList(arguments, 1), jQuery.toJSON)
+ };
+ var result = jQuery.ajax({
+ url: JSON_BASE_URL,
+ data: props,
+ async: false,
+ traditional: true
+ }).responseText;
+ if (result) {
+ result = cw.evalJSON(result);
}
-);
+ resetCursor();
+ return result;
+}
-asyncRemoteExec = cw.utils.deprecatedFunction(
- '[3.9] asyncRemoteExec() is deprecated, use loadRemote instead',
- function(fname /* ... */) {
- setProgressCursor();
- var props = {
- fname: fname,
- pageid: pageid,
- arg: $.map(cw.utils.sliceList(arguments, 1), jQuery.toJSON)
- };
- // XXX we should inline the content of loadRemote here
- var deferred = loadRemote(JSON_BASE_URL, props, 'POST');
- deferred = deferred.addErrback(remoteCallFailed);
- deferred = deferred.addErrback(resetCursor);
- deferred = deferred.addCallback(resetCursor);
- return deferred;
- }
-);
+function asyncRemoteExec(fname /* ... */) {
+ setProgressCursor();
+ var props = {
+ fname: fname,
+ pageid: pageid,
+ arg: $.map(cw.utils.sliceList(arguments, 1), jQuery.toJSON)
+ };
+ // XXX we should inline the content of loadRemote here
+ var deferred = loadRemote(JSON_BASE_URL, props, 'POST');
+ deferred = deferred.addErrback(remoteCallFailed);
+ deferred = deferred.addErrback(resetCursor);
+ deferred = deferred.addCallback(resetCursor);
+ return deferred;
+}
--- a/web/views/basecomponents.py Mon May 23 11:36:43 2011 +0200
+++ b/web/views/basecomponents.py Wed May 25 10:59:26 2011 +0200
@@ -29,8 +29,8 @@
from logilab.common.deprecation import class_renamed
from rql import parse
-from cubicweb.selectors import (yes, multi_etypes_rset, match_form_params,
- match_context, configuration_values,
+from cubicweb.selectors import (yes, no_cnx, match_form_params, match_context,
+ multi_etypes_rset, configuration_values,
anonymous_user, authenticated_user)
from cubicweb.schema import display_name
from cubicweb.utils import wrap_on_write
@@ -88,6 +88,7 @@
class ApplLogo(HeaderComponent):
"""build the instance logo, usually displayed in the header"""
__regid__ = 'logo'
+ __select__ = yes() # no need for a cnx
order = -1
def render(self, w):
@@ -150,7 +151,7 @@
class AnonUserStatusLink(HeaderComponent):
__regid__ = 'userstatus'
- __select__ = HeaderComponent.__select__ & anonymous_user()
+ __select__ = anonymous_user()
context = _('header-right')
order = HeaderComponent.order - 10
@@ -159,7 +160,7 @@
class AuthenticatedUserStatus(AnonUserStatusLink):
- __select__ = HeaderComponent.__select__ & authenticated_user()
+ __select__ = authenticated_user()
def render(self, w):
# display useractions and siteactions
@@ -180,7 +181,7 @@
"""display messages given using the __message parameter into a special div
section
"""
- __select__ = yes()
+ __select__ = ~no_cnx()
__regid__ = 'applmessages'
# don't want user to hide this component using an cwproperty
cw_property_defs = {}
--- a/web/views/basetemplates.py Mon May 23 11:36:43 2011 +0200
+++ b/web/views/basetemplates.py Wed May 25 10:59:26 2011 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
--- a/web/views/debug.py Mon May 23 11:36:43 2011 +0200
+++ b/web/views/debug.py Wed May 25 10:59:26 2011 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -133,6 +133,9 @@
if sessions:
w(u'<ul>')
for session in sessions:
+ if not session.cnx:
+ w(u'<li>%s (NO CNX)</li>' % session.sessionid)
+ continue
try:
last_usage_time = session.cnx.check()
except BadConnectionId:
--- a/web/views/sessions.py Mon May 23 11:36:43 2011 +0200
+++ b/web/views/sessions.py Wed May 25 10:59:26 2011 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
@@ -21,7 +21,7 @@
__docformat__ = "restructuredtext en"
-from cubicweb import RepositoryError, Unauthorized
+from cubicweb import RepositoryError, Unauthorized, AuthenticationError
from cubicweb.web import InvalidSession, Redirect
from cubicweb.web.application import AbstractSessionManager
from cubicweb.dbapi import DBAPISession
@@ -49,28 +49,36 @@
def get_session(self, req, sessionid):
"""return existing session for the given session identifier"""
- if not sessionid in self._sessions:
+ if sessionid not in self._sessions:
raise InvalidSession()
session = self._sessions[sessionid]
- try:
- user = self.authmanager.validate_session(req, session)
- except InvalidSession:
- # invalid session
- self.close_session(session)
- raise
- # associate the connection to the current request
- req.set_session(session, user)
+ if session.cnx:
+ try:
+ user = self.authmanager.validate_session(req, session)
+ except InvalidSession:
+ # invalid session
+ self.close_session(session)
+ raise
+ # associate the connection to the current request
+ req.set_session(session, user)
return session
- def open_session(self, req):
+ def open_session(self, req, allow_no_cnx=True):
"""open and return a new session for the given request. The session is
also bound to the request.
raise :exc:`cubicweb.AuthenticationError` if authentication failed
(no authentication info found or wrong user/password)
"""
- cnx, login = self.authmanager.authenticate(req)
- session = DBAPISession(cnx, login)
+ try:
+ cnx, login = self.authmanager.authenticate(req)
+ except AuthenticationError:
+ if allow_no_cnx:
+ session = DBAPISession(None)
+ else:
+ raise
+ else:
+ session = DBAPISession(cnx, login)
self._sessions[session.sessionid] = session
# associate the connection to the current request
req.set_session(session)
@@ -89,15 +97,16 @@
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))
+ 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']
+ raise Redirect(req.build_url(path, **args))
+ req.set_message(req._('welcome %s !') % req.user.login)
def _update_last_login_time(self, req):
# XXX should properly detect missing permission / non writeable source
@@ -120,10 +129,11 @@
"""
self.info('closing http session %s' % session.sessionid)
del self._sessions[session.sessionid]
- try:
- session.cnx.close()
- except:
- # already closed, may occurs if the repository session expired but
- # not the web session
- pass
- session.cnx = None
+ if session.cnx:
+ try:
+ session.cnx.close()
+ except:
+ # already closed, may occur if the repository session expired
+ # but not the web session
+ pass
+ session.cnx = None