# HG changeset patch # User sylvain.thenault@logilab.fr # Date 1240988611 -7200 # Node ID 849fd3d64f11c6a71d4d1826d03629c79bc28f1b # Parent cc2e2cbd701999f04a17a06a7ec8443ae5551e65# Parent 7d1794175e403e74f5c9f3f472a8bd7433033526 merge diff -r 7d1794175e40 -r 849fd3d64f11 appobject.py --- a/appobject.py Tue Apr 28 20:08:46 2009 +0200 +++ b/appobject.py Wed Apr 29 09:03:31 2009 +0200 @@ -23,21 +23,21 @@ ONESECOND = timedelta(0, 1, 0) -class Cache(dict): +class Cache(dict): def __init__(self): super(Cache, self).__init__() self.cache_creation_date = None self.latest_cache_lookup = datetime.now() - + CACHE_REGISTRY = {} class AppRsetObject(VObject): """This is the base class for CubicWeb application objects which are selected according to a request and result set. - + Classes are kept in the vregistry and instantiation is done at selection time. - + At registration time, the following attributes are set on the class: :vreg: the application's registry @@ -62,11 +62,11 @@ cls.config = vreg.config cls.register_properties() return cls - + @classmethod def vreg_initialization_completed(cls): pass - + @classmethod def selected(cls, *args, **kwargs): """by default web app objects are usually instantiated on @@ -85,9 +85,9 @@ # notice that when it exists multiple objects with the same id (adaptation, # overriding) only the first encountered definition is considered, so those # objects can't try to have different default values for instance. - + property_defs = {} - + @classmethod def register_properties(cls): for propid, pdef in cls.property_defs.items(): @@ -95,7 +95,7 @@ pdef['default'] = getattr(cls, propid, pdef['default']) pdef['sitewide'] = getattr(cls, 'site_wide', pdef.get('sitewide')) cls.vreg.register_property(cls.propkey(propid), **pdef) - + @classmethod def propkey(cls, propid): return '%s.%s.%s' % (cls.__registry__, cls.id, propid) @@ -109,7 +109,7 @@ if not isinstance(selector, tuple): selector = (selector,) return selector - + def __init__(self, req=None, rset=None, row=None, col=None, **extra): super(AppRsetObject, self).__init__() self.req = req @@ -117,12 +117,12 @@ self.row = row self.col = col self.extra_kwargs = extra - + def get_cache(self, cachename): """ NOTE: cachename should be dotted names as in : - cubicweb.mycache - - cubes.blog.mycache + - cubes.blog.mycache - etc. """ if cachename in CACHE_REGISTRY: @@ -132,7 +132,7 @@ CACHE_REGISTRY[cachename] = cache _now = datetime.now() if _now > cache.latest_cache_lookup + ONESECOND: - ecache = self.req.execute('Any C,T WHERE C is CWCache, C name %(name)s, C timestamp T', + ecache = self.req.execute('Any C,T WHERE C is CWCache, C name %(name)s, C timestamp T', {'name':cachename}).get_entity(0,0) cache.latest_cache_lookup = _now if not ecache.valid(cache.cache_creation_date): @@ -143,7 +143,7 @@ def propval(self, propid): assert self.req return self.req.property_value(self.propkey(propid)) - + def limited_rql(self): """return a printable rql for the result set associated to the object, with limit/offset correctly set according to maximum page size and @@ -165,7 +165,7 @@ else: rql = self.rset.printable_rql() return rql - + def _limit_offset_rql(self, limit, offset): rqlst = self.rset.syntax_tree() if len(rqlst.children) == 1: @@ -187,7 +187,7 @@ rql = rqlst.as_string(kwargs=self.rset.args) rqlst.parent = None return rql - + def view(self, __vid, rset=None, __fallback_vid=None, **kwargs): """shortcut to self.vreg.render method avoiding to pass self.req""" try: @@ -197,11 +197,11 @@ raise view = self.vreg.select_view(__fallback_vid, self.req, rset, **kwargs) return view.dispatch(**kwargs) - + # url generation methods ################################################## - + controller = 'view' - + def build_url(self, method=None, **kwargs): """return an absolute URL using params dictionary key/values as URL parameters. Values are automatically URL quoted, and the @@ -217,13 +217,13 @@ return self.req.build_url(method, **kwargs) # various resources accessors ############################################# - + def entity(self, row, col=0): """short cut to get an entity instance for a particular row/column (col default to 0) """ return self.rset.get_entity(row, col) - + def complete_entity(self, row, col=0, skip_bytes=True): """short cut to get an completed entity instance for a particular row (all instance's attributes have been fetched) @@ -239,14 +239,14 @@ def rqlexec(req, rql, args=None, key=None): req.execute(rql, args, key) return self.user_callback(rqlexec, args, msg) - + def user_callback(self, cb, args, msg=None, nonify=False): """register the given user callback and return an url to call it ready to be inserted in html """ self.req.add_js('cubicweb.ajax.js') cbname = self.req.register_onetime_callback(cb, *args) - msg = dumps(msg or '') + msg = dumps(msg or '') return "javascript:userCallbackThenReloadPage('%s', %s)" % ( cbname, msg) @@ -293,23 +293,23 @@ if num: return self.req.property_value('ui.float-format') % num return u'' - + # security related methods ################################################ - + def ensure_ro_rql(self, rql): """raise an exception if the given rql is not a select query""" first = rql.split(' ', 1)[0].lower() if first in ('insert', 'set', 'delete'): raise Unauthorized(self.req._('only select queries are authorized')) - + class AppObject(AppRsetObject): """base class for application objects which are not selected according to a result set, only by their identifier. - + Those objects may not have req, rset and cursor set. """ - + @classmethod def selected(cls, *args, **kwargs): """by default web app objects are usually instantiated on diff -r 7d1794175e40 -r 849fd3d64f11 dbapi.py --- a/dbapi.py Tue Apr 28 20:08:46 2009 +0200 +++ b/dbapi.py Wed Apr 29 09:03:31 2009 +0200 @@ -16,7 +16,7 @@ from cubicweb import ConnectionError, RequestSessionMixIn, set_log_methods from cubicweb.cwvreg import CubicWebRegistry, MulCnxCubicWebRegistry from cubicweb.cwconfig import CubicWebNoAppConfiguration - + _MARKER = object() class ConnectionProperties(object): @@ -29,7 +29,7 @@ def get_repository(method, database=None, config=None, vreg=None): """get a proxy object to the CubicWeb repository, using a specific RPC method. - + Only 'in-memory' and 'pyro' are supported for now. Either vreg or config argument should be given """ @@ -60,10 +60,10 @@ 'you may have to restart your server-side ' 'application' % nsid) return core.getProxyForURI(uri) - + def repo_connect(repo, user, password, cnxprops=None): """Constructor to create a new connection to the CubicWeb repository. - + Returns a Connection instance. """ cnxprops = cnxprops or ConnectionProperties('inmemory') @@ -72,7 +72,7 @@ if cnxprops.cnxtype == 'inmemory': cnx.vreg = repo.vreg return cnx - + def connect(database=None, user=None, password=None, host=None, group=None, cnxprops=None, port=None, setvreg=True, mulcnx=True, initlog=True): @@ -110,7 +110,7 @@ def in_memory_cnx(config, user, password): """usefull method for testing and scripting to get a dbapi.Connection - object connected to an in-memory repository instance + object connected to an in-memory repository instance """ if isinstance(config, CubicWebRegistry): vreg = config @@ -126,7 +126,7 @@ class DBAPIRequest(RequestSessionMixIn): - + def __init__(self, vreg, cnx=None): super(DBAPIRequest, self).__init__(vreg) try: @@ -146,10 +146,10 @@ def base_url(self): return self.vreg.config['base-url'] - + def from_controller(self): return 'view' - + def set_connection(self, cnx, user=None): """method called by the session handler when the user is authenticated or an anonymous connection is open @@ -157,7 +157,7 @@ self.cnx = cnx self.cursor = cnx.cursor(self) self.set_user(user) - + def set_default_language(self, vreg): try: self.lang = vreg.property_value('ui.language') @@ -175,26 +175,26 @@ rset.vreg = self.vreg rset.req = self return rset - + def describe(self, eid): """return a tuple (type, sourceuri, extid) for the entity with id """ return self.cnx.describe(eid) - + def source_defs(self): """return the definition of sources used by the repository.""" return self.cnx.source_defs() - + # entities cache management ############################################### - + def entity_cache(self, eid): return self._eid_cache[eid] - + def set_entity_cache(self, entity): self._eid_cache[entity.eid] = entity def cached_entities(self): return self._eid_cache.values() - + def drop_entity_cache(self, eid=None): if eid is None: self._eid_cache = {} @@ -210,11 +210,11 @@ def get_session_data(self, key, default=None, pop=False): """return value associated to `key` in session data""" return self.cnx.get_session_data(key, default, pop) - + def set_session_data(self, key, value): """set value associated to `key` in session data""" return self.cnx.set_session_data(key, value) - + def del_session_data(self, key): """remove value associated to `key` in session data""" return self.cnx.del_session_data(key) @@ -222,7 +222,7 @@ def get_shared_data(self, key, default=None, pop=False): """return value associated to `key` in shared data""" return self.cnx.get_shared_data(key, default, pop) - + def set_shared_data(self, key, value, querydata=False): """set value associated to `key` in shared data @@ -245,14 +245,14 @@ self._user = user if user: self.set_entity_cache(user) - + def execute(self, *args, **kwargs): """Session interface compatibility""" return self.cursor.execute(*args, **kwargs) set_log_methods(DBAPIRequest, getLogger('cubicweb.dbapi')) - - + + # exceptions ################################################################## class ProgrammingError(Exception): #DatabaseError): @@ -288,15 +288,15 @@ """String constant stating the type of parameter marker formatting expected by the interface. Possible values are : - 'qmark' Question mark style, + 'qmark' Question mark style, e.g. '...WHERE name=?' - 'numeric' Numeric, positional style, + 'numeric' Numeric, positional style, e.g. '...WHERE name=:1' - 'named' Named style, + 'named' Named style, e.g. '...WHERE name=:name' - 'format' ANSI C printf format codes, + 'format' ANSI C printf format codes, e.g. '...WHERE name=%s' - 'pyformat' Python extended format codes, + 'pyformat' Python extended format codes, e.g. '...WHERE name=%(name)s' """ paramstyle = 'pyformat' @@ -333,28 +333,28 @@ def request(self): return DBAPIRequest(self.vreg, self) - + def session_data(self): """return a dictionnary containing session data""" return self.data - + def get_session_data(self, key, default=None, pop=False): """return value associated to `key` in session data""" if pop: return self.data.pop(key, default) else: return self.data.get(key, default) - + def set_session_data(self, key, value): """set value associated to `key` in session data""" self.data[key] = value - + def del_session_data(self, key): """remove value associated to `key` in session data""" try: del self.data[key] except KeyError: - pass + pass def check(self): """raise `BadSessionId` if the connection is no more valid""" @@ -363,7 +363,7 @@ def get_shared_data(self, key, default=None, pop=False): """return value associated to `key` in shared data""" return self._repo.get_shared_data(self.sessionid, key, default, pop) - + def set_shared_data(self, key, value, querydata=False): """set value associated to `key` in shared data @@ -373,10 +373,10 @@ repository side. """ return self._repo.set_shared_data(self.sessionid, key, value, querydata) - + def get_schema(self): """Return the schema currently used by the repository. - + This is NOT part of the DB-API. """ if self._closed is not None: @@ -414,10 +414,10 @@ # application specific hooks if self._repo.config.application_hooks: hm.register_hooks(config.load_hooks(self.vreg)) - + def source_defs(self): """Return the definition of sources used by the repository. - + This is NOT part of the DB-API. """ if self._closed is not None: @@ -443,13 +443,13 @@ self.close() except: pass - + def describe(self, eid): return self._repo.describe(self.sessionid, eid) - + def close(self): """Close the connection now (rather than whenever __del__ is called). - + The connection will be unusable from this point forward; an Error (or subclass) exception will be raised if any operation is attempted with the connection. The same applies to all cursor objects trying to use the @@ -465,7 +465,7 @@ """Commit any pending transaction to the database. Note that if the database supports an auto-commit feature, this must be initially off. An interface method may be provided to turn it back on. - + Database modules that do not support transactions should implement this method with void functionality. """ @@ -476,7 +476,7 @@ def rollback(self): """This method is optional since not all databases provide transaction support. - + In case a database does provide transactions this method causes the the database to roll back to the start of any pending transaction. Closing a connection without committing the changes first will cause an implicit @@ -510,7 +510,7 @@ support is implemented (see also the connection's rollback() and commit() methods.) """ - + def __init__(self, connection, repo, req=None): """This read-only attribute return a reference to the Connection object on which the cursor was created. @@ -522,7 +522,7 @@ """This read/write attribute specifies the number of rows to fetch at a time with fetchmany(). It defaults to 1 meaning to fetch a single row at a time. - + Implementations must observe this value with respect to the fetchmany() method, but are free to interact with the database a single row at a time. It may also be used in the implementation of executemany(). @@ -535,7 +535,7 @@ self._closed = None self._index = 0 - + def close(self): """Close the cursor now (rather than whenever __del__ is called). The cursor will be unusable from this point forward; an Error (or subclass) @@ -543,30 +543,30 @@ """ self._closed = True - + def execute(self, operation, parameters=None, eid_key=None, build_descr=True): """Prepare and execute a database operation (query or command). Parameters may be provided as sequence or mapping and will be bound to variables in the operation. Variables are specified in a database-specific notation (see the module's paramstyle attribute for details). - + A reference to the operation will be retained by the cursor. If the same operation object is passed in again, then the cursor can optimize its behavior. This is most effective for algorithms where the same operation is used, but different parameters are bound to it (many times). - + For maximum efficiency when reusing an operation, it is best to use the setinputsizes() method to specify the parameter types and sizes ahead of time. It is legal for a parameter to not match the predefined information; the implementation should compensate, possibly with a loss of efficiency. - + The parameters may also be specified as list of tuples to e.g. insert multiple rows in a single operation, but this kind of usage is depreciated: executemany() should be used instead. - + Return values are not defined by the DB-API, but this here it returns a ResultSet object. """ @@ -575,25 +575,25 @@ self.req.decorate_rset(res) self._index = 0 return res - + def executemany(self, operation, seq_of_parameters): """Prepare a database operation (query or command) and then execute it against all parameter sequences or mappings found in the sequence seq_of_parameters. - + Modules are free to implement this method using multiple calls to the execute() method or by using array operations to have the database process the sequence as a whole in one call. - + Use of this method for an operation which produces one or more result sets constitutes undefined behavior, and the implementation is permitted (but not required) to raise an exception when it detects that a result set has been created by an invocation of the operation. - + The same comments as for execute() also apply accordingly to this method. - + Return values are not defined. """ for parameters in seq_of_parameters: @@ -606,7 +606,7 @@ def fetchone(self): """Fetch the next row of a query result set, returning a single sequence, or None when no more data is available. - + An Error (or subclass) exception is raised if the previous call to execute*() did not produce any result set or no call was issued yet. """ @@ -616,21 +616,21 @@ self._index += 1 return row - + def fetchmany(self, size=None): """Fetch the next set of rows of a query result, returning a sequence of sequences (e.g. a list of tuples). An empty sequence is returned when no more rows are available. - + The number of rows to fetch per call is specified by the parameter. If it is not given, the cursor's arraysize determines the number of rows to be fetched. The method should try to fetch as many rows as indicated by the size parameter. If this is not possible due to the specified number of rows not being available, fewer rows may be returned. - + An Error (or subclass) exception is raised if the previous call to execute*() did not produce any result set or no call was issued yet. - + Note there are performance considerations involved with the size parameter. For optimal performance, it is usually best to use the arraysize attribute. If the size parameter is used, then it is best @@ -644,12 +644,12 @@ self._index += size return rows - + def fetchall(self): """Fetch all (remaining) rows of a query result, returning them as a sequence of sequences (e.g. a list of tuples). Note that the cursor's arraysize attribute can affect the performance of this operation. - + An Error (or subclass) exception is raised if the previous call to execute*() did not produce any result set or no call was issued yet. """ @@ -665,39 +665,39 @@ def setinputsizes(self, sizes): """This can be used before a call to execute*() to predefine memory areas for the operation's parameters. - + sizes is specified as a sequence -- one item for each input parameter. The item should be a Type Object that corresponds to the input that will be used, or it should be an integer specifying the maximum length of a string parameter. If the item is None, then no predefined memory area will be reserved for that column (this is useful to avoid predefined areas for large inputs). - + This method would be used before the execute*() method is invoked. - + Implementations are free to have this method do nothing and users are free to not use it. """ pass - + def setoutputsize(self, size, column=None): """Set a column buffer size for fetches of large columns (e.g. LONGs, BLOBs, etc.). The column is specified as an index into the result sequence. Not specifying the column will set the default size for all large columns in the cursor. - + This method would be used before the execute*() method is invoked. - + Implementations are free to have this method do nothing and users are free to not use it. - """ + """ pass - + class LogCursor(Cursor): """override the standard cursor to log executed queries""" - + def execute(self, operation, parameters=None, eid_key=None, build_descr=True): """override the standard cursor to log executed queries""" tstart, cstart = time(), clock() diff -r 7d1794175e40 -r 849fd3d64f11 web/form.py --- a/web/form.py Tue Apr 28 20:08:46 2009 +0200 +++ b/web/form.py Wed Apr 29 09:03:31 2009 +0200 @@ -58,13 +58,22 @@ self.req.set_page_data('rql_varmaker', varmaker) self.varmaker = varmaker + def session_key(self): + """return the key that may be used to store / retreive data about a + previous post which failed because of a validation error + """ + return '%s#%s' % (self.req.url(), self.domid) + def __init__(self, req, rset, **kwargs): super(FormMixIn, self).__init__(req, rset, **kwargs) + self.restore_previous_post(self.session_key()) + + def restore_previous_post(self, sessionkey): # get validation session data which may have been previously set. # deleting validation errors here breaks form reloading (errors are # no more available), they have to be deleted by application's publish # method on successful commit - forminfo = req.get_session_data(req.url()) + forminfo = self.req.get_session_data(sessionkey, pop=True) if forminfo: req.data['formvalues'] = forminfo['values'] req.data['formerrors'] = errex = forminfo['errors'] @@ -240,7 +249,7 @@ assert hasattr(self.__class__, key) and not key[0] == '_', key setattr(self, key, val) if self.set_error_url: - self.form_add_hidden('__errorurl', req.url()) + self.form_add_hidden('__errorurl', self.session_key()) if self.copy_nav_params: for param in NAV_FORM_PARAMETERS: if not param in kwargs: @@ -250,6 +259,8 @@ if submitmsg is not None: self.form_add_hidden('__message', submitmsg) self.context = None + if 'domid' in kwargs:# session key changed + self.restore_previous_post(self.session_key()) @iclassmethod def field_by_name(cls_or_self, name, role='subject'): diff -r 7d1794175e40 -r 849fd3d64f11 web/views/management.py --- a/web/views/management.py Tue Apr 28 20:08:46 2009 +0200 +++ b/web/views/management.py Wed Apr 29 09:03:31 2009 +0200 @@ -62,6 +62,7 @@ return True return False + class SecurityManagementView(EntityView, SecurityViewMixIn): """display security information for a given entity""" id = 'security' @@ -100,6 +101,7 @@ msg = self.req._('ownerships have been changed') form = EntityFieldsForm(self.req, None, entity=entity, submitmsg=msg, form_buttons=[formwidgets.SubmitButton()], + domid='ownership%s' % entity.eid, __redirectvid='security', __redirectpath=entity.rest_path()) field = guess_field(entity.e_schema, self.schema.rschema('owned_by')) @@ -156,6 +158,7 @@ w(u'

%s

' % _('add a new permission')) form = EntityFieldsForm(self.req, None, entity=newperm, form_buttons=[formwidgets.SubmitButton()], + domid='reqperm%s' % entity.eid, __redirectvid='security', __redirectpath=entity.rest_path()) form.form_add_hidden('require_permission', entity.eid, role='object', eidparam=True) diff -r 7d1794175e40 -r 849fd3d64f11 web/views/startup.py --- a/web/views/startup.py Tue Apr 28 20:08:46 2009 +0200 +++ b/web/views/startup.py Wed Apr 29 09:03:31 2009 +0200 @@ -10,7 +10,7 @@ from logilab.common.textutils import unormalize from logilab.mtconverter import html_escape -from cubicweb.common.uilib import ureport_as_html, unormalize, ajax_replace_url +from cubicweb.common.uilib import ureport_as_html, ajax_replace_url from cubicweb.common.view import StartupView from cubicweb.common.selectors import match_user_group from cubicweb.web.httpcache import EtagHTTPCacheManager