# HG changeset patch # User Nicolas Chauvat # Date 1242859824 -7200 # Node ID 874a055c373b5f40d433d2919ce465b232a6d5fa # Parent f36d43f00f32ea118977d13f735515816ad3b5be# Parent c2011d238e9848a0f082b833188b2303ad8a7555 merge stable branch back into default branch diff -r f36d43f00f32 -r 874a055c373b dbapi.py --- a/dbapi.py Thu May 21 00:44:57 2009 +0200 +++ b/dbapi.py Thu May 21 00:50:24 2009 +0200 @@ -50,7 +50,7 @@ # resolve the Pyro object try: nshost, nsport = config['pyro-ns-host'], config['pyro-ns-port'] - uri = locator.getNS(nshost, nsport) .resolve(nsid) + uri = locator.getNS(nshost, nsport).resolve(nsid) except ProtocolError: raise ConnectionError('Could not connect to the Pyro name server ' '(host: %s:%i)' % (nshost, nsport)) @@ -325,6 +325,11 @@ self.vreg = None # session's data self.data = {} + # XXX < 3.2 bw compat + if 'EUser' in self._repo.get_schema(): + self._user_etype = 'EUser' + else: + self._user_etype = 'CWUser' def __repr__(self): if self.anonymous_connection: @@ -430,9 +435,9 @@ eid, login, groups, properties = self._repo.user_info(self.sessionid, props) if req is None: req = self.request() - rset = req.eid_rset(eid, 'CWUser') - user = self.vreg.etype_class('CWUser')(req, rset, row=0, groups=groups, - properties=properties) + rset = req.eid_rset(eid, self._user_etype) + user = self.vreg.etype_class(self._user_etype)(req, rset, row=0, groups=groups, + properties=properties) user['login'] = login # cache login return user diff -r f36d43f00f32 -r 874a055c373b debian/cubicweb-common.install.in --- a/debian/cubicweb-common.install.in Thu May 21 00:44:57 2009 +0200 +++ b/debian/cubicweb-common.install.in Thu May 21 00:50:24 2009 +0200 @@ -1,4 +1,5 @@ debian/tmp/usr/lib/PY_VERSION/site-packages/cubicweb/common/ usr/lib/PY_VERSION/site-packages/cubicweb debian/tmp/usr/lib/PY_VERSION/site-packages/cubicweb/entities/ usr/lib/PY_VERSION/site-packages/cubicweb +debian/tmp/usr/lib/PY_VERSION/site-packages/cubicweb/ext/ usr/lib/PY_VERSION/site-packages/cubicweb debian/tmp/usr/share/cubicweb/cubes/shared/i18n usr/share/cubicweb/cubes/shared/ debian/tmp/usr/lib/PY_VERSION/site-packages/cubicweb/*.py usr/share/pyshared/cubicweb diff -r f36d43f00f32 -r 874a055c373b devtools/devctl.py --- a/devtools/devctl.py Thu May 21 00:44:57 2009 +0200 +++ b/devtools/devctl.py Thu May 21 00:50:24 2009 +0200 @@ -77,11 +77,11 @@ if mod.__file__.startswith(path): del sys.modules[name] break - # fresh rtags - from cubicweb import rtags - from cubicweb.web import uicfg - rtags.RTAGS[:] = [] - reload(uicfg) + # fresh rtags + from cubicweb import rtags + from cubicweb.web import uicfg + rtags.RTAGS[:] = [] + reload(uicfg) def generate_schema_pot(w, cubedir=None): """generate a pot file with schema specific i18n messages diff -r f36d43f00f32 -r 874a055c373b devtools/fake.py diff -r f36d43f00f32 -r 874a055c373b entities/__init__.py --- a/entities/__init__.py Thu May 21 00:44:57 2009 +0200 +++ b/entities/__init__.py Thu May 21 00:50:24 2009 +0200 @@ -57,16 +57,6 @@ return '%s DESC' % var return None - @classmethod - def __initialize__(cls): - super(ANYENTITY, cls).__initialize__() # XXX - # set a default_ATTR method for rich text format fields - # XXX move this away once the old widgets have been dropped! - eschema = cls.e_schema - for metaattr, (metadata, attr) in eschema.meta_attributes().iteritems(): - if metadata == 'format' and not hasattr(cls, 'default_%s' % metaattr): - setattr(cls, 'default_%s' % metaattr, cls._default_format) - # meta data api ########################################################### def dc_title(self): @@ -269,9 +259,6 @@ from cubicweb.web.views.autoform import AutomaticEntityForm return AutomaticEntityForm.esrelations_by_category(self, categories, permission) - def _default_format(self): - return self.req.property_value('ui.default-text-format') - def attribute_values(self, attrname): if self.has_eid() or attrname in self: try: diff -r f36d43f00f32 -r 874a055c373b entity.py --- a/entity.py Thu May 21 00:44:57 2009 +0200 +++ b/entity.py Thu May 21 00:50:24 2009 +0200 @@ -284,6 +284,23 @@ parents.append(cls.vreg.etype_class('Any')) return parents + @classmethod + @cached + def _rest_attr_info(cls): + mainattr, needcheck = 'eid', True + if cls.rest_attr: + mainattr = cls.rest_attr + needcheck = not cls.e_schema.has_unique_values(mainattr) + else: + for rschema in cls.e_schema.subject_relations(): + if rschema.is_final() and rschema != 'eid' and cls.e_schema.has_unique_values(rschema): + mainattr = str(rschema) + needcheck = False + break + if mainattr == 'eid': + needcheck = False + return mainattr, needcheck + def __init__(self, req, rset=None, row=None, col=0): AppRsetObject.__init__(self, req, rset, row, col) dict.__init__(self) @@ -363,46 +380,41 @@ if getattr(self.req, 'search_state', ('normal',))[0] == 'normal': kwargs['base_url'] = self.metainformation()['source'].get('base-url') if method is None or method == 'view': - kwargs['_restpath'] = self.rest_path() + try: + kwargs['_restpath'] = self.rest_path(kwargs.get('base_url')) + except TypeError: + warn('%s: rest_path() now take use_ext_eid argument, ' + 'please update' % self.id, DeprecationWarning) + kwargs['_restpath'] = self.rest_path() else: kwargs['rql'] = 'Any X WHERE X eid %s' % self.eid return self.build_url(method, **kwargs) - def rest_path(self): + def rest_path(self, use_ext_eid=False): """returns a REST-like (relative) path for this entity""" mainattr, needcheck = self._rest_attr_info() etype = str(self.e_schema) - if mainattr == 'eid': - value = self.eid - else: + path = etype.lower() + if mainattr != 'eid': value = getattr(self, mainattr) if value is None: - return '%s/eid/%s' % (etype.lower(), self.eid) - if needcheck: - # make sure url is not ambiguous - rql = 'Any COUNT(X) WHERE X is %s, X %s %%(value)s' % (etype, mainattr) - if value is not None: - nbresults = self.req.execute(rql, {'value' : value})[0][0] - # may an assertion that nbresults is not 0 would be a good idea - if nbresults != 1: # no ambiguity - return '%s/eid/%s' % (etype.lower(), self.eid) - return '%s/%s' % (etype.lower(), self.req.url_quote(value)) - - @classmethod - def _rest_attr_info(cls): - mainattr, needcheck = 'eid', True - if cls.rest_attr: - mainattr = cls.rest_attr - needcheck = not cls.e_schema.has_unique_values(mainattr) - else: - for rschema in cls.e_schema.subject_relations(): - if rschema.is_final() and rschema != 'eid' and cls.e_schema.has_unique_values(rschema): - mainattr = str(rschema) - needcheck = False - break + mainattr = 'eid' + path += '/eid' + elif needcheck: + # make sure url is not ambiguous + rql = 'Any COUNT(X) WHERE X is %s, X %s %%(value)s' % ( + etype, mainattr) + if value is not None: + nbresults = self.req.execute(rql, {'value' : value})[0][0] + if nbresults != 1: # ambiguity? + mainattr = 'eid' + path += '/eid' if mainattr == 'eid': - needcheck = False - return mainattr, needcheck + if use_ext_eid: + value = self.metainformation()['extid'] + else: + value = self.eid + return '%s/%s' % (path, self.req.url_quote(value)) def attr_metadata(self, attr, metadata): """return a metadata for an attribute (None if unspecified)""" diff -r f36d43f00f32 -r 874a055c373b rtags.py --- a/rtags.py Thu May 21 00:44:57 2009 +0200 +++ b/rtags.py Thu May 21 00:50:24 2009 +0200 @@ -24,7 +24,8 @@ This class associates a single tag to each key. """ _allowed_values = None - def __init__(self, initfunc=None, allowed_values=None): + def __init__(self, name=None, initfunc=None, allowed_values=None): + self._name = name or '' self._tagdefs = {} if allowed_values is not None: self._allowed_values = allowed_values @@ -32,7 +33,7 @@ register_rtag(self) def __repr__(self): - return repr(self._tagdefs) + return '%s: %s' % (self._name, repr(self._tagdefs)) # dict compat def __getitem__(self, key): @@ -95,7 +96,9 @@ #else: stype, rtype, otype, tagged = [str(k) for k in key] if self._allowed_values is not None: - assert tag in self._allowed_values, '%r is not an allowed tag' % tag + assert tag in self._allowed_values, \ + '%r is not an allowed tag (should be in %s)' % ( + tag, self._allowed_values) self._tagdefs[(rtype, tagged, stype, otype)] = tag # rtag runtime api ######################################################## diff -r f36d43f00f32 -r 874a055c373b server/repository.py --- a/server/repository.py Thu May 21 00:44:57 2009 +0200 +++ b/server/repository.py Thu May 21 00:50:24 2009 +0200 @@ -217,6 +217,7 @@ self._get_pool().close(True) for i in xrange(config['connections-pool-size']): self._available_pools.put_nowait(ConnectionsPool(self.sources)) + self._shutting_down = False # internals ############################################################### @@ -349,6 +350,7 @@ """called on server stop event to properly close opened sessions and connections """ + self._shutting_down = True if isinstance(self._looping_tasks, tuple): # if tasks have been started for looptask in self._looping_tasks: self.info('canceling task %s...', looptask.name) @@ -618,7 +620,7 @@ """commit transaction for the session with the given id""" self.debug('begin commit for session %s', sessionid) try: - self._get_session(sessionid, setpool=True).commit() + self._get_session(sessionid).commit() except (ValidationError, Unauthorized): raise except: @@ -629,7 +631,7 @@ """commit transaction for the session with the given id""" self.debug('begin rollback for session %s', sessionid) try: - self._get_session(sessionid, setpool=True).rollback() + self._get_session(sessionid).rollback() except: self.exception('unexpected error') raise @@ -720,6 +722,8 @@ def _get_session(self, sessionid, setpool=False): """return the user associated to the given session identifier""" + if self._shutting_down: + raise Exception('Repository is shutting down') try: session = self._sessions[sessionid] except KeyError: diff -r f36d43f00f32 -r 874a055c373b server/schemaserial.py --- a/server/schemaserial.py Thu May 21 00:44:57 2009 +0200 +++ b/server/schemaserial.py Thu May 21 00:50:24 2009 +0200 @@ -14,6 +14,7 @@ from yams import schema as schemamod, buildobjs as ybo from cubicweb.schema import CONSTRAINTS, ETYPE_NAME_MAP +from cubicweb.server import sqlutils def group_mapping(cursor, interactive=True): """create a group mapping from an rql cursor @@ -124,7 +125,8 @@ if etype in ETYPE_NAME_MAP: # XXX <2.45 bw compat print 'fixing etype name from %s to %s' % (etype, ETYPE_NAME_MAP[etype]) # can't use write rql queries at this point, use raw sql - session.system_sql('UPDATE CWEType SET name=%(n)s WHERE eid=%(x)s', + session.system_sql('UPDATE %(p)sCWEType SET %(p)sname=%%(n)s WHERE %(p)seid=%%(x)s' + % {'p': sqlutils.SQL_PREFIX}, {'x': eid, 'n': ETYPE_NAME_MAP[etype]}) session.system_sql('UPDATE entities SET type=%(n)s WHERE type=%(x)s', {'x': etype, 'n': ETYPE_NAME_MAP[etype]}) diff -r f36d43f00f32 -r 874a055c373b server/session.py --- a/server/session.py Thu May 21 00:44:57 2009 +0200 +++ b/server/session.py Thu May 21 00:50:24 2009 +0200 @@ -65,6 +65,8 @@ # i18n initialization self.set_language(cnxprops.lang) self._threaddata = threading.local() + self._threads_in_transaction = set() + self._closed = False def get_mode(self): return getattr(self._threaddata, 'mode', 'read') @@ -150,6 +152,8 @@ def set_pool(self): """the session need a pool to execute some queries""" + if self._closed: + raise Exception('try to set pool on a closed session') if self.pool is None: self._threaddata.pool = self.repo._get_pool() try: @@ -158,6 +162,7 @@ self.repo._free_pool(self.pool) self._threaddata.pool = None raise + self._threads_in_transaction.add(threading.currentThread()) return self._threaddata.pool def reset_pool(self): @@ -167,6 +172,7 @@ # or rollback if self.pool is not None and self.mode == 'read': # even in read mode, we must release the current transaction + self._threads_in_transaction.remove(threading.currentThread()) self.repo._free_pool(self.pool) self.pool.pool_reset(self) self._threaddata.pool = None @@ -343,6 +349,23 @@ def close(self): """do not close pool on session close, since they are shared now""" + self._closed = True + # copy since _threads_in_transaction maybe modified while waiting + for thread in self._threads_in_transaction.copy(): + if thread is threading.currentThread(): + continue + self.info('waiting for thread %s', thread) + # do this loop/break instead of a simple join(10) in case thread is + # the main thread (in which case it will be removed from + # self._threads_in_transaction but still be alive...) + for i in xrange(10): + thread.join(1) + if not (thread.isAlive() and + thread in self._threads_in_transaction): + break + else: + self.error('thread %s still alive after 10 seconds, will close ' + 'session anyway', thread) self.rollback() # transaction data/operations management ################################## diff -r f36d43f00f32 -r 874a055c373b server/sources/rql2sql.py --- a/server/sources/rql2sql.py Thu May 21 00:44:57 2009 +0200 +++ b/server/sources/rql2sql.py Thu May 21 00:50:24 2009 +0200 @@ -901,6 +901,7 @@ def visit_comparison(self, cmp, contextrels=None): """generate SQL for a comparaison""" if len(cmp.children) == 2: + # XXX occurs ? lhs, rhs = cmp.children else: lhs = None @@ -911,6 +912,11 @@ operator = ' LIKE ' else: operator = ' %s ' % operator + elif (operator == '=' and isinstance(rhs, Constant) + and rhs.eval(self._args) is None): + if lhs is None: + return ' IS NULL' + return '%s IS NULL' % lhs.accept(self, contextrels) elif isinstance(rhs, Function) and rhs.name == 'IN': assert operator == '=' operator = ' ' diff -r f36d43f00f32 -r 874a055c373b server/test/data/sources_multi --- a/server/test/data/sources_multi Thu May 21 00:44:57 2009 +0200 +++ b/server/test/data/sources_multi Thu May 21 00:50:24 2009 +0200 @@ -14,6 +14,7 @@ cubicweb-user = admin cubicweb-password = gingkow mapping-file = extern_mapping.py +base-url=http://extern.org/ [extern-multi] adapter = pyrorql diff -r f36d43f00f32 -r 874a055c373b server/test/unittest_multisources.py --- a/server/test/unittest_multisources.py Thu May 21 00:44:57 2009 +0200 +++ b/server/test/unittest_multisources.py Thu May 21 00:50:24 2009 +0200 @@ -235,9 +235,21 @@ states.remove((aff1stateeid, aff1statename)) notstates = set(tuple(x) for x in self.execute('Any S,SN WHERE S is State, S name SN, NOT X in_state S, X eid %(x)s', {'x': aff1}, 'x')) - self.set_debug(False) self.assertSetEquals(notstates, states) + def test_absolute_url_base_url(self): + ceid = cu.execute('INSERT Card X: X title "without wikiid to get eid based url"')[0][0] + cnx2.commit() + lc = self.execute('Card X WHERE X title "without wikiid to get eid based url"').get_entity(0, 0) + self.assertEquals(lc.absolute_url(), 'http://extern.org/card/eid/%s' % ceid) + + def test_absolute_url_no_base_url(self): + cu = cnx3.cursor() + ceid = cu.execute('INSERT Card X: X title "without wikiid to get eid based url"')[0][0] + cnx3.commit() + lc = self.execute('Card X WHERE X title "without wikiid to get eid based url"').get_entity(0, 0) + self.assertEquals(lc.absolute_url(), 'http://testing.fr/cubicweb/card/eid/%s' % lc.eid) + def test_nonregr1(self): ueid = self.session.user.eid affaire = self.execute('Affaire X WHERE X ref "AFFREF"').get_entity(0, 0) @@ -251,7 +263,6 @@ self.assertEquals(len(rset), 1) self.assertEquals(rset.rows[0], [self.session.user.eid]) - def test_nonregr3(self): self.execute('DELETE Card X WHERE X eid %(x)s, NOT X multisource_inlined_rel Y', {'x': self.ic1}) diff -r f36d43f00f32 -r 874a055c373b server/test/unittest_repository.py --- a/server/test/unittest_repository.py Thu May 21 00:44:57 2009 +0200 +++ b/server/test/unittest_repository.py Thu May 21 00:50:24 2009 +0200 @@ -199,6 +199,26 @@ def test_transaction_interleaved(self): self.skip('implement me') + def test_close_wait_processing_request(self): + repo = self.repo + cnxid = repo.connect(*self.default_user_password()) + repo.execute(cnxid, 'INSERT CWUser X: X login "toto", X upassword "tutu", X in_group G WHERE G name "users"') + repo.commit(cnxid) + # close has to be in the thread due to sqlite limitations + def close_in_a_few_moment(): + time.sleep(0.1) + repo.close(cnxid) + t = threading.Thread(target=close_in_a_few_moment) + t.start() + try: + print 'execute' + repo.execute(cnxid, 'DELETE CWUser X WHERE X login "toto"') + print 'commit' + repo.commit(cnxid) + print 'commited' + finally: + t.join() + def test_initial_schema(self): schema = self.repo.schema # check order of attributes is respected @@ -238,31 +258,29 @@ def test_pyro(self): import Pyro Pyro.config.PYRO_MULTITHREADED = 0 - lock = threading.Lock() + done = [] # the client part has to be in the thread due to sqlite limitations - t = threading.Thread(target=self._pyro_client, args=(lock,)) + t = threading.Thread(target=self._pyro_client, args=(done,)) try: daemon = self.repo.pyro_register() t.start() - # connection - daemon.handleRequests(1.0) - daemon.handleRequests(1.0) - daemon.handleRequests(1.0) - # get schema - daemon.handleRequests(1.0) - # execute - daemon.handleRequests(1.0) - t.join() + while not done: + daemon.handleRequests(1.0) + t.join(1) + if t.isAlive(): + self.fail('something went wrong, thread still alive') finally: repository.pyro_unregister(self.repo.config) - def _pyro_client(self, lock): + def _pyro_client(self, done): cnx = connect(self.repo.config.appid, u'admin', 'gingkow') # check we can get the schema schema = cnx.get_schema() self.assertEquals(schema.__hashmode__, None) - rset = cnx.cursor().execute('Any U,G WHERE U in_group G') - + cu = cnx.cursor() + rset = cu.execute('Any U,G WHERE U in_group G') + cnx.close() + done.append(True) def test_internal_api(self): repo = self.repo diff -r f36d43f00f32 -r 874a055c373b server/test/unittest_rql2sql.py --- a/server/test/unittest_rql2sql.py Thu May 21 00:44:57 2009 +0200 +++ b/server/test/unittest_rql2sql.py Thu May 21 00:50:24 2009 +0200 @@ -1,3 +1,4 @@ + """unit tests for module cubicweb.server.sources.rql2sql""" import sys @@ -1211,6 +1212,14 @@ WHERE rel_in_group0.eid_from=T00.x AND rel_in_group0.eid_to=G.cw_eid''', varmap={'X': 'T00.x', 'X.login': 'T00.l'}) + def test_is_null_transform(self): + union = self._prepare('Any X WHERE X login %(login)s') + r, args = self.o.generate(union, {'login': None}) + self.assertLinesEquals((r % args).strip(), + '''SELECT X.cw_eid +FROM cw_CWUser AS X +WHERE X.cw_login IS NULL''') + def test_parser_parse(self): for t in self._parse(PARSER): yield t diff -r f36d43f00f32 -r 874a055c373b web/box.py --- a/web/box.py Thu May 21 00:44:57 2009 +0200 +++ b/web/box.py Thu May 21 00:50:24 2009 +0200 @@ -152,7 +152,7 @@ entity = self.entity(row, col) limit = self.req.property_value('navigation.related-limit') + 1 role = get_role(self) - self.w(u'
') + self.w(u'') diff -r f36d43f00f32 -r 874a055c373b web/data/cubicweb.css --- a/web/data/cubicweb.css Thu May 21 00:44:57 2009 +0200 +++ b/web/data/cubicweb.css Thu May 21 00:50:24 2009 +0200 @@ -455,6 +455,8 @@ padding: 0.2em 0px; margin-bottom: 0.5em; background: #eeedd9; + min-width: 21em; + max-width: 50em; } ul.sideBox li{ @@ -572,24 +574,11 @@ padding: 0.5em 0.2em 0.2em; } -div.sideRelated h4, -div.sideRelated h5 { - margin-top: 0px; - margin-bottom: 0px; -} - div.primaryRight{ float:right; } -div.sideRelated { - margin-right: 1em; - padding: 12px 0px 12px 12px; - min-width: 21em; - max-width: 50em; -} - div.metadata { font-size: 90%; margin: 5px 0px 3px; diff -r f36d43f00f32 -r 874a055c373b web/data/cubicweb.edition.js --- a/web/data/cubicweb.edition.js Thu May 21 00:44:57 2009 +0200 +++ b/web/data/cubicweb.edition.js Thu May 21 00:50:24 2009 +0200 @@ -342,11 +342,11 @@ } -function handleFormValidationResponse(formid, onsuccess, result) { +function handleFormValidationResponse(formid, onsuccess, onfailure, result) { // Success if (result[0]) { if (onsuccess) { - return onsuccess(result[1]); + return onsuccess(result[1], formid); } else { document.location.href = result[1]; return ; @@ -365,6 +365,9 @@ _displayValidationerrors(formid, descr[0], descr[1]); updateMessage(_("please correct errors below")); document.location.hash = '#header'; + if (onfailure){ + onfailure(formid); + } return false; } @@ -426,7 +429,7 @@ * to the appropriate URL. Otherwise, the validation errors are displayed * around the corresponding input fields. */ -function validateForm(formid, action, onsuccess) { +function validateForm(formid, action, onsuccess, onfailure) { try { var zipped = formContents(formid); var d = asyncRemoteExec('validate_form', action, zipped[0], zipped[1]); @@ -435,7 +438,7 @@ return false; } function _callback(result, req) { - handleFormValidationResponse(formid, onsuccess, result); + handleFormValidationResponse(formid, onsuccess, onfailure, result); } d.addCallback(_callback); return false; @@ -468,7 +471,7 @@ return false; } d.addCallback(function (result, req) { - handleFormValidationResponse(formid, noop, result); + handleFormValidationResponse(formid, noop, noop, result); if (reload) { document.location.href = result[1]; } else { @@ -502,7 +505,7 @@ return false; } d.addCallback(function (result, req) { - handleFormValidationResponse(formid, noop, result); + handleFormValidationResponse(formid, noop, noop, result); var fieldview = getNode(divid); fieldview.innerHTML = result[2]; // switch inline form off only if no error diff -r f36d43f00f32 -r 874a055c373b web/data/cubicweb.preferences.css --- a/web/data/cubicweb.preferences.css Thu May 21 00:44:57 2009 +0200 +++ b/web/data/cubicweb.preferences.css Thu May 21 00:50:24 2009 +0200 @@ -6,98 +6,105 @@ */ -table.preferences td{ - padding: 0 0.5em 1em; +.preferences .validateButton{ + margin-top:0px; } -fieldset.preferences{ +fieldset.preferences{ border : 1px solid #CFCEB7; - margin:1em 0; - padding:0 1em 1em; + margin:7px 1em 0; + padding:2px 6px 6px; } -div.preffield { - margin-bottom: 0.8em ; +div.component { + margin-left: 1em; } -/* -div.preffield label{ - font-size:110% - } -*/ - -div.prefinput{ - margin:.3em 0; -} - -div.componentLink{ +div.componentLink{ margin-top:0.3em; } a.componentTitle{ font-weight:bold; - color: #000; + color: #000/*#0083AB;*/ } a.componentTitle:visited{ color: #000; } - - h2.propertiesform a{ display:block; margin: 10px 0px 6px 0px; font-weight: bold; - color: #222211; + color: #000; padding: 0.2em 0.2em 0.2em 16px; background:#eeedd9 url("puce_down.png") 3px center no-repeat; - font-size:76%; + font-size:89%; } h2.propertiesform a:hover{ - color:#000; background-color:#cfceb7; +} + +h2.propertiesform a:hover, +h2.propertiesform a:visited{ text-decoration:none; + color: #000; } +div.preffield { + margin-bottom: 5px; + padding:2px 5px; + background:#eeedd9; +} + +div.prefinput{ + margin:.3em; +} + + div.prefinput select.changed, -div.prefinput input.changed{ - background:#eeedd9; - border: 1px solid #eeedd9; +div.prefinput input.changed{ + border: 1px solid #000; + font-weight:bold; + } div.prefinput select, -div.prefinput input{ +div.prefinput input{ background:#fff; border: 1px solid #CFCEB7; } .prefinput input.error { - background:transparent url(error.png) no-repeat scroll 100% 50% !important; + /* background:#fff url(error.png) no-repeat scroll 100% 50% !important; */ + border:1px solid red !important; + color:red; + padding-right:1em; } -div.formsg{ +div.formsg{ font-weight:bold; margin:0.5em 0px; - } +} -div.formsg .critical{ +div.critical{ color:red; padding-left:20px; background:#fff url(critical.png) no-repeat; } -div.formsg .message{ +div.formsg .msg{ color : green; } .helper{ font-size: 96%; color: #555544; - padding:0; + padding:0; } div.prefinput .helper:hover { @@ -105,11 +112,6 @@ cursor: default; } -.error{ - color:red; - padding-right:1em; +div.openlink{ + display:inline; } - -div.openlink{ - display:inline; - } \ No newline at end of file diff -r f36d43f00f32 -r 874a055c373b web/data/cubicweb.preferences.js --- a/web/data/cubicweb.preferences.js Thu May 21 00:44:57 2009 +0200 +++ b/web/data/cubicweb.preferences.js Thu May 21 00:50:24 2009 +0200 @@ -4,23 +4,21 @@ * move me in a more appropriate place */ -function toggleVisibility(elemId, cookiename) { - _clearPreviousMessages(); - jqNode(elemId).toggleClass('hidden'); - asyncRemoteExec('set_cookie', cookiename, - jQuery('#' + elemId).attr('class')); +function togglePrefVisibility(elemId) { + clearPreviousMessages(); + jQuery('#' + elemId).toggleClass('hidden'); } function closeFieldset(fieldsetid){ var linklabel = _('open all'); - var linkhref = 'javascript:openFieldset("' +fieldsetid + '")' - _toggleFieldset(fieldsetid, 1, linklabel, linkhref) + var linkhref = 'javascript:openFieldset("' +fieldsetid + '")'; + _toggleFieldset(fieldsetid, 1, linklabel, linkhref); } function openFieldset(fieldsetid){ var linklabel = _('close all'); - var linkhref = 'javascript:closeFieldset("'+ fieldsetid + '")' - _toggleFieldset(fieldsetid, 0, linklabel, linkhref) + var linkhref = 'javascript:closeFieldset("'+ fieldsetid + '")'; + _toggleFieldset(fieldsetid, 0, linklabel, linkhref); } @@ -28,13 +26,13 @@ jQuery('#'+fieldsetid).find('div.openlink').each(function(){ var link = A({'href' : "javascript:noop();", 'onclick' : linkhref}, - linklabel) + linklabel); jQuery(this).empty().append(link); }); jQuery('#'+fieldsetid).find('fieldset[id]').each(function(){ var fieldset = jQuery(this); if(closeaction){ - fieldset.addClass('hidden') + fieldset.addClass('hidden'); }else{ fieldset.removeClass('hidden'); linkLabel = (_('open all')); @@ -43,44 +41,24 @@ } function validatePrefsForm(formid){ - var form = getNode(formid); - freezeFormButtons(formid); - try { - var d = _sendForm(formid, null); - } catch (ex) { - log('got exception', ex); - return false; - } - function _callback(result, req) { - _clearPreviousMessages(); - _clearPreviousErrors(formid); - // success - if(result[0]){ - return submitSucces(formid) - } - // Failures - unfreezeFormButtons(formid); - var descr = result[1]; - if (!isArrayLike(descr) || descr.length != 2) { - log('got strange error :', descr); - updateMessage(descr); - return ; - } - _displayValidationerrors(formid, descr[0], descr[1]); - var dom = DIV({'class':'critical'}, - _("please correct errors below")); - jQuery(form).find('div.formsg').empty().append(dom); - updateMessage(_("")); - return false; - } - d.addCallback(_callback); - return false; + clearPreviousMessages(); + clearPreviousErrors(formid); + return validateForm(formid, null, submitSucces, submitFailure); } -function submitSucces(formid){ +function submitFailure(formid){ + var form = jQuery('#'+formid); + var dom = DIV({'class':'critical'}, + _("please correct errors below")); + jQuery(form).find('div.formsg').empty().append(dom); + // clearPreviousMessages() + jQuery(form).find('span.error').next().focus(); +} + +function submitSucces(url, formid){ var form = jQuery('#'+formid); setCurrentValues(form); - var dom = DIV({'class':'message'}, + var dom = DIV({'class':'msg'}, _("changes applied")); jQuery(form).find('div.formsg').empty().append(dom); jQuery(form).find('input').removeClass('changed'); @@ -88,64 +66,63 @@ return; } -function _clearPreviousMessages() { +function clearPreviousMessages() { jQuery('div#appMsg').addClass('hidden'); jQuery('div.formsg').empty(); } -function _clearPreviousErrors(formid) { - jQuery('#' + formid + ' span.error').remove(); +function clearPreviousErrors(formid) { + jQuery('#err-value:' + formid).remove(); } function checkValues(form, success){ var unfreezeButtons = false; - jQuery(form).find('select').each(function () { + jQuery(form).find('select').each(function () { unfreezeButtons = _checkValue(jQuery(this), unfreezeButtons); }); jQuery(form).find('[type=text]').each(function () { unfreezeButtons = _checkValue(jQuery(this), unfreezeButtons); }); - jQuery(form).find('input[type=radio]').each(function () { + jQuery(form).find('input[type=radio]').each(function () { if (jQuery(this).attr('checked')){ unfreezeButtons = _checkValue(jQuery(this), unfreezeButtons); } - }); - + }); + if (unfreezeButtons){ unfreezeFormButtons(form.attr('id')); }else{ if (!success){ - _clearPreviousMessages(); + clearPreviousMessages(); } - _clearPreviousErrors(form.attr('id')); + clearPreviousErrors(form.attr('id')); freezeFormButtons(form.attr('id')); } } function _checkValue(input, unfreezeButtons){ - var currentValueInput = jQuery("input[id=current-" + input.attr('name') + "]"); + var currentValueInput = jQuery("input[name=current-" + input.attr('name') + "]"); if (currentValueInput.attr('value') != input.attr('value')){ input.addClass('changed'); unfreezeButtons = true; }else{ input.removeClass('changed'); jQuery("span[id=err-" + input.attr('id') + "]").remove(); - } + } input.removeClass('error'); - return unfreezeButtons + return unfreezeButtons; } function setCurrentValues(form){ - jQuery(form).find('input[id^=current-value]').each(function () { + jQuery(form).find('input[name^=current-value]').each(function () { var currentValueInput = jQuery(this); - var name = currentValueInput.attr('id').split('-')[1]; + var name = currentValueInput.attr('name').split('-')[1]; jQuery(form).find("[name=" + name + "]").each(function (){ var input = jQuery(this); if(input.attr('type')=='radio'){ if(input.attr('checked')){ - log(input.attr('value')); currentValueInput.attr('value', input.attr('value')); } }else{ @@ -155,20 +132,20 @@ }); } - function initEvents(){ - jQuery('form').each(function() { + jQuery('form').each(function() { var form = jQuery(this); freezeFormButtons(form.attr('id')); - form.find('input[type=text]').keyup(function(){ - checkValues(form); + form.find('input[type=text]').keyup(function(){ + checkValues(form); }); - form.find('input[type=radio]').change(function(){ - checkValues(form); + form.find('input[type=radio]').change(function(){ + checkValues(form); }); - form.find('select').change(function(){ - checkValues(form); + form.find('select').change(function(){ + checkValues(form); }); + setCurrentValues(form); }); } diff -r f36d43f00f32 -r 874a055c373b web/form.py --- a/web/form.py Thu May 21 00:44:57 2009 +0200 +++ b/web/form.py Thu May 21 00:50:24 2009 +0200 @@ -372,10 +372,15 @@ return self.form_previous_values[qname] if qname in self.req.form: return self.req.form[qname] + if field.name in self.req.form: + return self.req.form[field.name] return None def form_field_value(self, field, load_bytes=False): """return field's *typed* value""" + myattr = '%s_%s_default' % (field.role, field.name) + if hasattr(self, myattr): + return getattr(self, myattr)() value = field.initial if callable(value): value = value(self) diff -r f36d43f00f32 -r 874a055c373b web/formfields.py --- a/web/formfields.py Thu May 21 00:44:57 2009 +0200 +++ b/web/formfields.py Thu May 21 00:50:24 2009 +0200 @@ -213,20 +213,21 @@ try: return req.data[self] except KeyError: + fkwargs = {} if self.use_fckeditor(form): # if fckeditor is used and format field isn't explicitly # deactivated, we want an hidden field for the format - widget = HiddenInput() - choices = None + fkwargs['widget'] = HiddenInput() + fkwargs['initial'] = 'text/html' else: # else we want a format selector - # XXX compute vocabulary - widget = Select() + fkwargs['widget'] = Select() + fkwargs['widget'].attrs['size'] = 1 fcstr = FormatConstraint() - choices = [(req._(fmt), fmt) for fmt in fcstr.vocabulary(req=req)] - widget.attrs['size'] = 1 - field = StringField(name=self.name + '_format', widget=widget, - choices=choices) + fkwargs['choices'] = fcstr.vocabulary(req=req) + fkwargs['internationalizable'] = True + fkwargs['initial'] = lambda f: f.form_field_format(self) + field = StringField(name=self.name + '_format', **fkwargs) req.data[self] = field return field @@ -471,7 +472,10 @@ kwargs.setdefault('widget', Select()) kwargs.setdefault('choices', cstr.vocabulary) if card in '?1': + if isinstance(kwargs['widget'], type): + kwargs['widget'] = kwargs['widget']() kwargs['widget'].attrs.setdefault('size', 1) + for cstr in constraints: if isinstance(cstr, SizeConstraint) and cstr.max is not None: if cstr.max < 257: kwargs.setdefault('widget', TextInput()) diff -r f36d43f00f32 -r 874a055c373b web/formrenderers.py --- a/web/formrenderers.py Thu May 21 00:44:57 2009 +0200 +++ b/web/formrenderers.py Thu May 21 00:50:24 2009 +0200 @@ -83,13 +83,13 @@ return tags.label(label, **attrs) def render_help(self, form, field): - help = [ u'
' ] + help = [] descr = field.help if descr: - help.append('%s' % form.req._(descr)) + help.append('
%s
' % form.req._(descr)) example = field.example_format(form.req) if example: - help.append('(%s: %s)' + help.append('
(%s: %s)
' % (form.req._('sample format'), example)) return u' '.join(help) diff -r f36d43f00f32 -r 874a055c373b web/formwidgets.py --- a/web/formwidgets.py Thu May 21 00:44:57 2009 +0200 +++ b/web/formwidgets.py Thu May 21 00:50:24 2009 +0200 @@ -199,27 +199,8 @@ def render(self, form, field): name, curvalues, attrs = self._render_attrs(form, field) - options = [] - for label, value in field.vocabulary(form): - if value in curvalues: - tag = tags.input(name=name, value=value, type=self.type, - checked='checked', **attrs) - else: - tag = tags.input(name=name, value=value, type=self.type, - **attrs) - options.append(tag + label) - return '
\n'.join(options) - - -class Radio(Input): - """, for field having a specific vocabulary. One - input will be generated for each possible value. - """ - type = 'radio' - - def render(self, form, field): - name, curvalues, attrs = self._render_attrs(form, field) domid = attrs.pop('id', None) + sep = attrs.pop('separator', u'
') options = [] for i, (label, value) in enumerate(field.vocabulary(form)): iattrs = attrs.copy() @@ -228,10 +209,16 @@ if value in curvalues: iattrs['checked'] = u'checked' tag = tags.input(name=name, type=self.type, value=value, **iattrs) - options.append(tag + label + '
') + options.append(tag + label + sep) return '\n'.join(options) +class Radio(CheckBox): + """, for field having a specific vocabulary. One + input will be generated for each possible value. + """ + type = 'radio' + # javascript widgets ########################################################### class DateTimePicker(TextInput): @@ -318,20 +305,21 @@ values = (INTERNAL_FIELD_VALUE,) init_ajax_attributes(attrs, self.wdgtype, self.loadtype) # XXX entity form specific - attrs['cubicweb:dataurl'] = self._get_url(form.edited_entity) + attrs['cubicweb:dataurl'] = self._get_url(form.edited_entity, field) return name, values, attrs - def _get_url(self, entity): - return entity.req.build_url('json', fname=entity.autocomplete_initfuncs[self.rschema], - pageid=entity.req.pageid, mode='remote') + def _get_url(self, entity, field): + fname = entity.autocomplete_initfuncs[field.name] + return entity.req.build_url('json', fname=fname, mode='remote', + pageid=entity.req.pageid) class StaticFileAutoCompletionWidget(AutoCompletionWidget): """XXX describe me""" wdgtype = 'StaticFileSuggestField' - def _get_url(self, entity): - return entity.req.datadir_url + entity.autocomplete_initfuncs[self.rschema] + def _get_url(self, entity, field): + return entity.req.datadir_url + entity.autocomplete_initfuncs[field.name] class RestrictedAutoCompletionWidget(AutoCompletionWidget): diff -r f36d43f00f32 -r 874a055c373b web/htmlwidgets.py --- a/web/htmlwidgets.py Thu May 21 00:44:57 2009 +0200 +++ b/web/htmlwidgets.py Thu May 21 00:50:24 2009 +0200 @@ -57,6 +57,7 @@ def append(self, item): self.items.append(item) + title_class = 'boxTitle' main_div_class = 'boxContent' listing_class = 'boxListing' @@ -82,7 +83,7 @@ title = '%s' % html_escape(self.title) else: title = '%s' % self.title - self.w(u'
%s
' % title) + self.w(u'
%s
' % (self.title_class, title)) if self.items: self.box_begin_content() for item in self.items: @@ -93,6 +94,7 @@ class SideBoxWidget(BoxWidget): """default CubicWeb's sidebox widget""" + title_class = u'sideBoxTitle' main_div_class = u'sideBoxBody' listing_class = '' diff -r f36d43f00f32 -r 874a055c373b web/test/data/schema/testschema.py --- a/web/test/data/schema/testschema.py Thu May 21 00:44:57 2009 +0200 +++ b/web/test/data/schema/testschema.py Thu May 21 00:50:24 2009 +0200 @@ -1,6 +1,7 @@ class Salesterm(EntityType): described_by_test = SubjectRelation('File', cardinality='1*', composite='subject') amount = Int(constraints=[IntervalBoundConstraint(0, 100)]) + reason = String(maxsize=20, vocabulary=[u'canceled', u'sold']) class tags(RelationDefinition): subject = 'Tag' diff -r f36d43f00f32 -r 874a055c373b web/test/unittest_form.py --- a/web/test/unittest_form.py Thu May 21 00:44:57 2009 +0200 +++ b/web/test/unittest_form.py Thu May 21 00:50:24 2009 +0200 @@ -64,7 +64,14 @@ self.assertEquals(len(states), 1) self.assertEquals(states[0][0], u'deactivated') # list of (combobox view, state eid) - + def test_consider_req_form_params(self): + e = self.etype_instance('CWUser') + e.eid = 'A' + form = EntityFieldsForm(self.request(login=u'toto'), None, entity=e) + field = StringField(name='login', eidparam=True) + form.append_field(field) + form.form_build_context({}) + self.assertEquals(form.form_field_display_value(field, {}), 'toto') # form view tests ######################################################### diff -r f36d43f00f32 -r 874a055c373b web/test/unittest_formfields.py --- a/web/test/unittest_formfields.py Thu May 21 00:44:57 2009 +0200 +++ b/web/test/unittest_formfields.py Thu May 21 00:50:24 2009 +0200 @@ -1,11 +1,15 @@ """unittests for cw.web.formfields""" from logilab.common.testlib import TestCase, unittest_main + +from yams.constraints import StaticVocabularyConstraint, SizeConstraint + from cubicweb.devtools import TestServerConfiguration -from cubicweb.web.formwidgets import PasswordInput, TextArea +from cubicweb.devtools.testlib import EnvBasedTC +from cubicweb.web.form import EntityFieldsForm +from cubicweb.web.formwidgets import PasswordInput, TextArea, Select from cubicweb.web.formfields import * -from cubicweb.entities.wfobjs import State -from cubicweb.entities.authobjs import CWUser + from cubes.file.entities import File config = TestServerConfiguration('data') @@ -14,6 +18,7 @@ state_schema = schema['State'] cwuser_schema = schema['CWUser'] file_schema = schema['File'] +salesterm_schema = schema['Salesterm'] class GuessFieldTC(TestCase): @@ -41,12 +46,13 @@ self.assertEquals(description_format_field.sort, True) self.assertEquals(description_format_field.initial(None), 'text/rest') + # wikiid_field = guess_field(state_schema, schema['wikiid']) # self.assertIsInstance(wikiid_field, StringField) # self.assertEquals(wikiid_field.required, False) - def test_euser_fields(self): + def test_cwuser_fields(self): upassword_field = guess_field(cwuser_schema, schema['upassword']) self.assertIsInstance(upassword_field, StringField) self.assertIsInstance(upassword_field.widget, PasswordInput) @@ -80,5 +86,28 @@ self.assertIsInstance(data_field.format_field, StringField) self.assertIsInstance(data_field.encoding_field, StringField) + def test_constraints_priority(self): + salesterm_field = guess_field(salesterm_schema, schema['reason']) + constraints = schema['reason'].rproperty('Salesterm', 'String', 'constraints') + self.assertEquals([c.__class__ for c in constraints], + [SizeConstraint, StaticVocabularyConstraint]) + self.assertIsInstance(salesterm_field.widget, Select) + +class MoreFieldsTC(EnvBasedTC): + def test_rtf_format_field(self): + req = self.request() + req.use_fckeditor = lambda: False + e = self.etype_instance('State') + form = EntityFieldsForm(req, entity=e) + description_field = guess_field(state_schema, schema['description']) + description_format_field = description_field.get_format_field(form) + self.assertEquals(description_format_field.internationalizable, True) + self.assertEquals(description_format_field.sort, True) + # unlike below, initial is bound to form.form_field_format + self.assertEquals(description_format_field.initial(form), 'text/html') + self.execute('INSERT CWProperty X: X pkey "ui.default-text-format", X value "text/rest", X for_user U WHERE U login "admin"') + self.commit() + self.assertEquals(description_format_field.initial(form), 'text/rest') + if __name__ == '__main__': unittest_main() diff -r f36d43f00f32 -r 874a055c373b web/uicfg.py --- a/web/uicfg.py Thu May 21 00:44:57 2009 +0200 +++ b/web/uicfg.py Thu May 21 00:50:24 2009 +0200 @@ -97,8 +97,9 @@ section = 'sideboxes' rtag.tag_relation((sschema, rschema, oschema, role), section) -primaryview_section = RelationTags(init_primaryview_section, - frozenset(('attributes', 'relations', +primaryview_section = RelationTags('primaryview_section', + init_primaryview_section, + frozenset(('attributes', 'relations', 'sideboxes', 'hidden'))) for rtype in ('eid', 'creation_date', 'modification_date', 'is', 'is_instance_of', 'identity', @@ -108,6 +109,8 @@ 'see_also'): primaryview_section.tag_subject_of(('*', rtype, '*'), 'hidden') primaryview_section.tag_object_of(('*', rtype, '*'), 'hidden') +primaryview_section.tag_subject_of(('*', 'use_email', '*'), 'attributes') +primaryview_section.tag_subject_of(('*', 'primary_email', '*'), 'hidden') for attr in ('name', 'meta', 'final'): primaryview_section.tag_attribute(('CWEType', attr), 'hidden') @@ -140,7 +143,8 @@ rtag.tag_relation((sschema, rschema, oschema, role), displayinfo) displayinfo.setdefault('label', label) -primaryview_display_ctrl = DisplayCtrlRelationTags(init_primaryview_display_ctrl) +primaryview_display_ctrl = DisplayCtrlRelationTags('primaryview_display_ctrl', + init_primaryview_display_ctrl) # index view configuration #################################################### @@ -178,7 +182,7 @@ section = 'generic' rtag.tag_relation((sschema, rschema, oschema, role), section) -autoform_section = RelationTags(init_autoform_section, +autoform_section = RelationTags('autoform_section', init_autoform_section, set(('primary', 'secondary', 'generic', 'metadata', 'generated'))) # use primary and not generated for eid since it has to be an hidden @@ -217,7 +221,7 @@ # relations'field class -autoform_field = RelationTags() +autoform_field = RelationTags('autoform_field') # relations'field explicit kwargs (given to field's __init__) autoform_field_kwargs = RelationTags() @@ -231,7 +235,7 @@ # inlined view flag for non final relations: when True for an entry, the # entity(ies) at the other end of the relation will be editable from the # form of the edited entity -autoform_is_inlined = RelationTagsBool() +autoform_is_inlined = RelationTagsBool('autoform_is_inlined') autoform_is_inlined.tag_subject_of(('*', 'use_email', '*'), True) autoform_is_inlined.tag_subject_of(('CWRelation', 'relation_type', '*'), True) autoform_is_inlined.tag_subject_of(('CWRelation', 'from_entity', '*'), True) @@ -241,7 +245,7 @@ # set of tags of the form _on_new on relations. is a # schema action (add/update/delete/read), and when such a tag is found # permissions checking is by-passed and supposed to be ok -autoform_permissions_overrides = RelationTagsSet() +autoform_permissions_overrides = RelationTagsSet('autoform_permissions_overrides') # boxes.EditBox configuration ################################################# @@ -254,7 +258,8 @@ rschema.rproperty(sschema, oschema, 'composite') == role: rtag.tag_relation((sschema, rschema, oschema, role), True) -actionbox_appearsin_addmenu = RelationTagsBool(init_actionbox_appearsin_addmenu) +actionbox_appearsin_addmenu = RelationTagsBool('actionbox_appearsin_addmenu', + init_actionbox_appearsin_addmenu) actionbox_appearsin_addmenu.tag_subject_of(('*', 'is', '*'), False) actionbox_appearsin_addmenu.tag_object_of(('*', 'is', '*'), False) actionbox_appearsin_addmenu.tag_subject_of(('*', 'is_instance_of', '*'), False) @@ -279,3 +284,4 @@ actionbox_appearsin_addmenu.tag_object_of(('*', 'allowed_transition', 'Transition'), True) actionbox_appearsin_addmenu.tag_object_of(('*', 'destination_state', 'State'), True) actionbox_appearsin_addmenu.tag_subject_of(('State', 'allowed_transition', '*'), True) + diff -r f36d43f00f32 -r 874a055c373b web/views/actions.py --- a/web/views/actions.py Thu May 21 00:44:57 2009 +0200 +++ b/web/views/actions.py Thu May 21 00:50:24 2009 +0200 @@ -9,7 +9,7 @@ from cubicweb.vregistry import objectify_selector from cubicweb.selectors import (EntitySelector, one_line_rset, two_lines_rset, one_etype_rset, relation_possible, - non_final_entity, + nonempty_rset, non_final_entity, authenticated_user, match_user_groups, match_search_state, has_permission, has_add_permission, ) @@ -74,7 +74,7 @@ if accept match. """ id = 'select' - __select__ = match_search_state('linksearch') & match_searched_etype() + __select__ = match_search_state('linksearch') & nonempty_rset() & match_searched_etype() title = _('select') category = 'mainactions' diff -r f36d43f00f32 -r 874a055c373b web/views/basecontrollers.py --- a/web/views/basecontrollers.py Thu May 21 00:44:57 2009 +0200 +++ b/web/views/basecontrollers.py Thu May 21 00:50:24 2009 +0200 @@ -18,7 +18,7 @@ from cubicweb import NoSelectableObject, ValidationError, ObjectNotFound, typed_eid from cubicweb.utils import strptime from cubicweb.selectors import yes, match_user_groups -from cubicweb.view import STRICT_DOCTYPE +from cubicweb.view import STRICT_DOCTYPE, STRICT_DOCTYPE_NOEXT from cubicweb.common.mail import format_mail from cubicweb.web import ExplicitLogin, Redirect, RemoteCallFailed, json_dumps from cubicweb.web.formrenderers import FormRenderer @@ -32,8 +32,13 @@ HAS_SEARCH_RESTRICTION = False -def xhtml_wrap(source): - head = u'\n' + STRICT_DOCTYPE +def xhtml_wrap(self, source): + # XXX factor out, watch view.py ~ Maintemplate.doctype + if self.req.xhtml_browser(): + dt = STRICT_DOCTYPE + else: + dt = STRICT_DOCTYPE_NOEXT + head = u'\n' + dt return head + u'
%s
' % source.strip() def jsonize(func): @@ -51,7 +56,7 @@ def wrapper(self, *args, **kwargs): self.req.set_content_type(self.req.html_content_type()) result = func(self, *args, **kwargs) - return xhtml_wrap(result) + return xhtml_wrap(self, result) wrapper.__name__ = func.__name__ return wrapper @@ -205,7 +210,7 @@ self.req.set_content_type('text/html') jsarg = simplejson.dumps( (status, args) ) return """""" % simplejson.dumps( (status, args) ) def validation_error(self, err): @@ -379,11 +384,7 @@ ctrl.publish(None, fromjson=True) except ValidationError, err: self.req.cnx.rollback() - if not err.entity or isinstance(err.entity, (long, int)): - eid = err.entity - else: - eid = err.entity.eid - return (False, (eid, err.errors)) + return (False, (err.entity, err.errors)) except Redirect, redir: return (True, redir.location) except Exception, err: @@ -484,6 +485,24 @@ rql = 'DELETE B bookmarked_by U WHERE B eid %(b)s, U eid %(u)s' self.req.execute(rql, {'b': typed_eid(beid), 'u' : self.req.user.eid}) + def js_node_clicked(self, treeid, nodeeid): + """add/remove eid in treestate cookie""" + from cubicweb.web.views.treeview import treecookiename + cookies = self.req.get_cookie() + statename = treecookiename(treeid) + treestate = cookies.get(statename) + if treestate is None: + cookies[statename] = nodeeid + self.req.set_cookie(cookies, statename) + else: + marked = set(filter(None, treestate.value.split(';'))) + if nodeeid in marked: + marked.remove(nodeeid) + else: + marked.add(nodeeid) + cookies[statename] = ';'.join(marked) + self.req.set_cookie(cookies, statename) + def js_set_cookie(self, cookiename, cookievalue): # XXX we should consider jQuery.Cookie cookiename, cookievalue = str(cookiename), str(cookievalue) diff -r f36d43f00f32 -r 874a055c373b web/views/basetemplates.py --- a/web/views/basetemplates.py Thu May 21 00:44:57 2009 +0200 +++ b/web/views/basetemplates.py Thu May 21 00:50:24 2009 +0200 @@ -7,7 +7,6 @@ """ __docformat__ = "restructuredtext en" - from logilab.mtconverter import html_escape from cubicweb.vregistry import objectify_selector diff -r f36d43f00f32 -r 874a055c373b web/views/baseviews.py --- a/web/views/baseviews.py Thu May 21 00:44:57 2009 +0200 +++ b/web/views/baseviews.py Thu May 21 00:50:24 2009 +0200 @@ -11,8 +11,8 @@ :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr """ #from __future__ import with_statement - __docformat__ = "restructuredtext en" +_ = unicode from rql import nodes @@ -23,7 +23,6 @@ from cubicweb.view import EntityView, AnyRsetView, View from cubicweb.common.uilib import cut, printable_value -_ = unicode class NullView(AnyRsetView): """default view when no result has been found""" @@ -204,9 +203,10 @@ def cell_call(self, row, col): entity = self.entity(row, col) desc = cut(entity.dc_description(), 50) - self.w(u'' % (html_escape(entity.absolute_url()), - html_escape(desc))) - self.w(html_escape(self.view('textincontext', self.rset, row=row, col=col))) + self.w(u'' % ( + html_escape(entity.absolute_url()), html_escape(desc))) + self.w(html_escape(self.view('textincontext', self.rset, + row=row, col=col))) self.w(u'') @@ -214,8 +214,12 @@ id = 'outofcontext' def cell_call(self, row, col): - self.w(u'' % self.entity(row, col).absolute_url()) - self.w(html_escape(self.view('textoutofcontext', self.rset, row=row, col=col))) + entity = self.entity(row, col) + desc = cut(entity.dc_description(), 50) + self.w(u'' % ( + html_escape(entity.absolute_url()), html_escape(desc))) + self.w(html_escape(self.view('textoutofcontext', self.rset, + row=row, col=col))) self.w(u'') diff -r f36d43f00f32 -r 874a055c373b web/views/boxes.py --- a/web/views/boxes.py Thu May 21 00:44:57 2009 +0200 +++ b/web/views/boxes.py Thu May 21 00:50:24 2009 +0200 @@ -13,6 +13,7 @@ :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr """ __docformat__ = "restructuredtext en" +_ = unicode from logilab.mtconverter import html_escape @@ -23,7 +24,6 @@ from cubicweb.web import uicfg from cubicweb.web.box import BoxTemplate -_ = unicode class EditBox(BoxTemplate): """ @@ -99,10 +99,10 @@ eschema = entity.e_schema for rschema, teschema, x in self.add_related_schemas(entity): if x == 'subject': - label = '%s %s %s %s' % (eschema, rschema, teschema, x) + label = 'add %s %s %s %s' % (eschema, rschema, teschema, x) url = self.linkto_url(entity, rschema, teschema, 'object') else: - label = '%s %s %s %s' % (teschema, rschema, eschema, x) + label = 'add %s %s %s %s' % (teschema, rschema, eschema, x) url = self.linkto_url(entity, rschema, teschema, 'subject') actions.append(self.mk_action(_(label), url)) return actions diff -r f36d43f00f32 -r 874a055c373b web/views/calendar.py --- a/web/views/calendar.py Thu May 21 00:44:57 2009 +0200 +++ b/web/views/calendar.py Thu May 21 00:50:24 2009 +0200 @@ -4,6 +4,8 @@ :copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved. :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr """ +__docformat__ = "restructuredtext en" +_ = unicode from datetime import datetime, date, timedelta @@ -14,7 +16,6 @@ from cubicweb.utils import strptime, date_range, todate, todatetime from cubicweb.view import EntityView -_ = unicode # useful constants & functions ################################################ @@ -81,7 +82,8 @@ task = self.complete_entity(i) self.w(u'
') self.w(u'

%s

' % html_escape(task.dc_title())) - self.w(u'
%s
' % html_escape(task.dc_description())) + self.w(u'
%s
' + % task.dc_description(format='text/html')) if task.start: self.w(u'%s' % (task.start.isoformat(), self.format_date(task.start))) if task.stop: diff -r f36d43f00f32 -r 874a055c373b web/views/cwproperties.py --- a/web/views/cwproperties.py Thu May 21 00:44:57 2009 +0200 +++ b/web/views/cwproperties.py Thu May 21 00:50:24 2009 +0200 @@ -17,12 +17,13 @@ from cubicweb.view import StartupView from cubicweb.web import uicfg, stdmsgs from cubicweb.web.form import CompositeForm, EntityFieldsForm, FormViewMixIn +from cubicweb.web.formrenderers import FormRenderer from cubicweb.web.formfields import FIELDS, StringField from cubicweb.web.formwidgets import Select, Button, SubmitButton from cubicweb.web.views import primary -# some string we want to be internationalizable for nicer display of eproperty +# some string we want to be internationalizable for nicer display of property # groups _('navigation') _('ui') @@ -48,10 +49,9 @@ _('category') -def make_togglable_link(nodeid, label, cookiename): +def make_togglable_link(nodeid, label): """builds a HTML link that switches the visibility & remembers it""" - action = u"javascript: toggleVisibility('%s', '%s')" % \ - (nodeid, cookiename) + action = u"javascript: togglePrefVisibility('%s')" % nodeid return u'%s' % (action, label) def css_class(someclass): @@ -63,8 +63,8 @@ skip_none = False -class SystemEPropertiesForm(FormViewMixIn, StartupView): - id = 'systemepropertiesform' +class SystemCWPropertiesForm(FormViewMixIn, StartupView): + id = 'systempropertiesform' __select__ = none_rset() & match_user_groups('managers') title = _('site configuration') @@ -95,15 +95,15 @@ def call(self, **kwargs): """The default view representing the application's index""" - self.req.add_js('cubicweb.preferences.js') + self.req.add_js(('cubicweb.edition.js', 'cubicweb.preferences.js', 'cubicweb.ajax.js')) self.req.add_css('cubicweb.preferences.css') vreg = self.vreg values = self.defined_keys groupedopts = {} mainopts = {} - # "self.id=='systemepropertiesform'" to skip site wide properties on + # "self.id=='systempropertiesform'" to skip site wide properties on # user's preference but not site's configuration - for key in vreg.user_property_keys(self.id=='systemepropertiesform'): + for key in vreg.user_property_keys(self.id=='systempropertiesform'): parts = key.split('.') if parts[0] in vreg: # appobject configuration @@ -113,61 +113,73 @@ mainopts.setdefault(parts[0], []).append(key) # precompute form to consume error message for group, keys in mainopts.items(): - mainopts[group] = self.form(keys, True) + mainopts[group] = self.form(group, keys, False) + for group, objects in groupedopts.items(): for oid, keys in objects.items(): - groupedopts[group][oid] = self.form(keys, True) + groupedopts[group][oid] = self.form(group + '-' + oid, keys, True) + w = self.w req = self.req _ = req._ w(u'

%s

\n' % _(self.title)) - # we don't want this in each sub-forms - w(u'
%s
' % self.req._('validating...')) for label, group, form in sorted((_(g), g, f) for g, f in mainopts.iteritems()): status = css_class(self._group_status(group)) w(u'

%s

\n' % - (make_togglable_link('fieldset_' + group, label, - self._cookie_name(group)))) + (make_togglable_link('fieldset_' + group, label.capitalize()))) w(u'
' % (group, status)) + w(u'
') w(form) - w(u'
') + w(u'
') + for label, group, objects in sorted((_(g), g, o) for g, o in groupedopts.iteritems()): status = css_class(self._group_status(group)) w(u'

%s

\n' % - (make_togglable_link('fieldset_' + group, label, - self._cookie_name(group)))) + (make_togglable_link('fieldset_' + group, label.capitalize()))) w(u'
' % (group, status)) - for label, oid, form in sorted((self.req.__('%s_%s' % (group, o)), o, f) - for o, f in objects.iteritems()): - w(u'
') - w(u'%s\n' % label) + + # create selection + sorted_objects = sorted((self.req.__('%s_%s' % (group, o)), o, f) + for o, f in objects.iteritems()) + for label, oid, form in sorted_objects: + w(u'
') + w(u'''') docmsgid = '%s_%s_description' % (group, oid) doc = _(docmsgid) if doc != docmsgid: - w(u'

%s

' % html_escape(doc)) + w(u'
%s
' % html_escape(doc).capitalize()) + w(u'
') + w(u'') w(u'
') @property @cached - def eprops_rset(self): + def cwprops_rset(self): return self.req.execute('Any P,K,V WHERE P is CWProperty, P pkey K, ' 'P value V, NOT P for_user U') @property def defined_keys(self): values = {} - for i, entity in enumerate(self.eprops_rset.entities()): + for i, entity in enumerate(self.cwprops_rset.entities()): values[entity.pkey] = i return values def entity_for_key(self, key): values = self.defined_keys if key in values: - entity = self.eprops_rset.get_entity(values[key], 0) + entity = self.cwprops_rset.get_entity(values[key], 0) else: entity = self.vreg.etype_class('CWProperty')(self.req, None, None) entity.eid = self.req.varmaker.next() @@ -175,11 +187,11 @@ entity['value'] = self.vreg.property_value(key) return entity - def form(self, keys, splitlabel=False): - buttons = [SubmitButton(), - Button(stdmsgs.BUTTON_CANCEL, cwaction='cancel')] - form = CompositeForm(self.req, domid=None, action=self.build_url(), + def form(self, formid, keys, splitlabel=False): + buttons = [SubmitButton()] + form = CompositeForm(self.req, domid=formid, action=self.build_url(), form_buttons=buttons, + onsubmit="return validatePrefsForm('%s')" % formid, submitmsg=self.req._('changes applied')) path = self.req.relative_path() if '?' in path: @@ -188,7 +200,9 @@ form.form_add_hidden('__redirectpath', path) for key in keys: self.form_row(form, key, splitlabel) - return form.form_render(display_progress_div=False) + renderer = CWPropertiesFormRenderer() + return form.form_render(display_progress_div=False, + renderer=renderer) def form_row(self, form, key, splitlabel): entity = self.entity_for_key(key) @@ -197,20 +211,21 @@ else: label = key subform = EntityFieldsForm(self.req, entity=entity, set_error_url=False) + subform.append_field(PropertyValueField(name='value', label=label, eidparam=True)) subform.vreg = self.vreg subform.form_add_hidden('pkey', key, eidparam=True) + subform.form_add_hidden("current-value:%s" % entity.eid,) form.form_add_subform(subform) return subform - def is_user_prefs(cls, req, rset, row=None, col=0, **kwargs): return req.user.eid == rset[row or 0][col] -class EPropertiesForm(SystemEPropertiesForm): - id = 'epropertiesform' +class CWPropertiesForm(SystemCWPropertiesForm): + id = 'propertiesform' __select__ = ( # we don't want guests to be able to come here match_user_groups('users', 'managers') & @@ -228,19 +243,19 @@ @property @cached - def eprops_rset(self): + def cwprops_rset(self): return self.req.execute('Any P,K,V WHERE P is CWProperty, P pkey K, P value V,' 'P for_user U, U eid %(x)s', {'x': self.user.eid}) def form_row(self, form, key, splitlabel): - subform = super(EPropertiesForm, self).form_row(form, key, splitlabel) + subform = super(CWPropertiesForm, self).form_row(form, key, splitlabel) # if user is in the managers group and the property is being created, # we have to set for_user explicitly if not subform.edited_entity.has_eid() and self.user.matching_groups('managers'): subform.form_add_hidden('for_user', self.user.eid, eidparam=True) -# eproperty form objects ###################################################### +# cwproperty form objects ###################################################### class PlaceHolderWidget(object): @@ -341,3 +356,29 @@ uicfg.autoform_field.tag_attribute(('CWProperty', 'pkey'), PropertyKeyField) uicfg.autoform_field.tag_attribute(('CWProperty', 'value'), PropertyValueField) + + +class CWPropertiesFormRenderer(FormRenderer): + """specific renderer for properties""" + + def open_form(self, form, values): + err = '
' + return super(CWPropertiesFormRenderer, self).open_form(form, values) + err + + def _render_fields(self, fields, w, form): + for field in fields: + w(u'
\n') + if self.display_label: + w(u'%s' % self.render_label(form, field)) + error = form.form_field_error(field) + w(u'%s' % self.render_help(form, field)) + w(u'
') + w(field.render(form, self)) + w(u'
') + w(u'
') + + def render_buttons(self, w, form): + w(u'
\n') + for button in form.form_buttons: + w(u'%s\n' % button.render(form)) + w(u'
') diff -r f36d43f00f32 -r 874a055c373b web/views/editforms.py --- a/web/views/editforms.py Thu May 21 00:44:57 2009 +0200 +++ b/web/views/editforms.py Thu May 21 00:50:24 2009 +0200 @@ -52,7 +52,7 @@ # else we will only delete the displayed page need_navigation = False - def call(self): + def call(self, onsubmit=None): """ask for confirmation before real deletion""" req, w = self.req, self.w _ = req._ @@ -61,7 +61,7 @@ # XXX above message should have style of a warning w(u'

%s

\n' % _('Do you want to delete the following element(s) ?')) form = CompositeForm(req, domid='deleteconf', copy_nav_params=True, - action=self.build_url('edit'), onsubmit=None, + action=self.build_url('edit'), onsubmit=onsubmit, form_buttons=[Button(stdmsgs.YES, cwaction='delete'), Button(stdmsgs.NO, cwaction='cancel')]) done = set() @@ -350,7 +350,8 @@ def render_form(self, entity, peid, rtype, role, **kwargs): """fetch and render the form""" form = self.vreg.select_object('forms', 'edition', self.req, None, - entity=entity, set_error_url=False) + entity=entity, set_error_url=False, + copy_nav_params=False) self.add_hiddens(form, entity, peid, rtype, role) divid = '%s-%s-%s' % (peid, rtype, entity.eid) title = self.schema.rschema(rtype).display_name(self.req, role) diff -r f36d43f00f32 -r 874a055c373b web/views/ibreadcrumbs.py --- a/web/views/ibreadcrumbs.py Thu May 21 00:44:57 2009 +0200 +++ b/web/views/ibreadcrumbs.py Thu May 21 00:50:24 2009 +0200 @@ -5,6 +5,7 @@ :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr """ __docformat__ = "restructuredtext en" +_ = unicode from logilab.mtconverter import html_escape @@ -16,7 +17,6 @@ from cubicweb.common.uilib import cut from cubicweb.web.component import EntityVComponent -_ = unicode def bc_title(entity): textsize = entity.req.property_value('navigation.short-line-size') @@ -80,7 +80,6 @@ def cell_call(self, row, col): entity = self.entity(row, col) - desc = cut(entity.dc_description(), 50) - self.w(u'%s' % (html_escape(entity.absolute_url()), - html_escape(desc), - bc_title(entity))) + desc = html_escape(cut(entity.dc_description(), 50)) + self.w(u'%s' % ( + html_escape(entity.absolute_url()), desc, bc_title(entity))) diff -r f36d43f00f32 -r 874a055c373b web/views/idownloadable.py --- a/web/views/idownloadable.py Thu May 21 00:44:57 2009 +0200 +++ b/web/views/idownloadable.py Thu May 21 00:50:24 2009 +0200 @@ -26,7 +26,7 @@ def download_box(w, entity, title=None, label=None): req = entity.req - w(u'
') + w(u'