backport stable
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Mon, 23 Jan 2012 12:39:21 +0100
changeset 8179 e52a084e955c
parent 8172 77aa753dcd6b (current diff)
parent 8178 2d62b6017700 (diff)
child 8180 1f6ba9afb925
backport stable
web/request.py
--- a/devtools/__init__.py	Tue Jan 17 11:55:32 2012 +0100
+++ b/devtools/__init__.py	Mon Jan 23 12:39:21 2012 +0100
@@ -620,10 +620,12 @@
             backup_name = self._backup_name(db_id)
             self._drop(backup_name)
             self.system_source['db-name'] = backup_name
-            self._repo.turn_repo_off()
+            # during postgres database initialization, there is no repo set here.
+            assert self._repo is None
+            #self._repo.turn_repo_off()
             createdb(self.helper, self.system_source, self.dbcnx, self.cursor, template=orig_name)
             self.dbcnx.commit()
-            self._repo.turn_repo_on()
+            #self._repo.turn_repo_on()
             return backup_name
         finally:
             self.system_source['db-name'] = orig_name
--- a/doc/book/en/devrepo/testing.rst	Tue Jan 17 11:55:32 2012 +0100
+++ b/doc/book/en/devrepo/testing.rst	Mon Jan 23 12:39:21 2012 +0100
@@ -460,3 +460,127 @@
 ``````````````
 .. autoclass:: cubicweb.devtools.testlib.CubicWebTC
    :members:
+
+
+What you need to know about request and session
+-----------------------------------------------
+
+
+.. image:: ../images/request_session.png
+
+First, remember to think that some code run on a client side, some
+other on the repository side. More precisely:
+
+* client side: web interface, raw db-api connection (cubicweb-ctl shell for
+  instance);
+
+* repository side: RQL query execution, that may trigger hooks and operation.
+
+The client interact with the repository through a db-api connection.
+
+
+A db-api connection is tied to a session in the repository. The connection and
+request objects are unaccessible from repository code / the session object is
+unaccessible from client code (theorically at least).
+
+The :mod:`cubicweb.dbapi` module provides a base request class. The web interface
+provides an extended request class.
+
+
+The `request` object provides access to all cubicweb resources, eg:
+
+* the registry (which itself provides access to the schema and the
+  configuration);
+
+* an underlying db-api connection (when using req.execute, you actually call the
+  db-api);
+
+* other specific resources depending on the client type (url generation according
+  to base url, form parameters, etc.).
+
+
+A `session` provides an api similar to a request regarding RQL execution and
+access to global resources (registry and all), but also have the following
+responsibilities:
+
+* handle transaction data, that will live during the time of a single
+  transaction. This includes the database connections that will be used to
+  execute RQL queries.
+
+* handle persistent data that may be used across different (web) requests
+
+* security and hooks control (not possible through a request)
+
+
+The `_cw` attribute
+```````````````````
+The `_cw` attribute available on every application object provides access to all
+cubicweb resources, eg:
+
+For code running on the client side (eg web interface view), `_cw` is a request
+instance.
+
+For code running on the repository side (hooks and operation), `_cw` is a session
+instance.
+
+
+Beware some views may be called with a session (eg notifications) or with a
+DB-API request. In the later case, see :meth:`use_web_compatible_requests` on
+:class:`Connection` instances.
+
+
+Request, session and transaction
+````````````````````````````````
+
+In the web interface, an HTTP request is handle by a single request, which will
+be thrown way once the response send.
+
+The web publisher handle the transaction:
+
+* commit / rollback is done automatically
+* you should not commit / rollback explicitly
+
+When using a raw db-api, you're on your own regarding transaction.
+
+On the other hand, db-api connection and session live from a user login to its logout.
+
+Because session lives for a long time, and database connections is a limited
+resource, we can't bound a session to its own database connection for all its
+lifetime. The repository handles a pool of connections (4 by default), and it's
+responsible to attribute them as needed.
+
+Let's detail the process:
+
+1. an incoming RQL query comes from a client to the repository
+
+2. the repository attributes a database connection to the session
+
+3. the repository's querier execute the query
+
+4. this query may trigger hooks. Hooks and operation may execute some rql queries
+   through `_cw.execute`. Those queries go directly to the querier, hence don't
+   touch the database connection, they use the one attributed in 2.
+
+5. the repository's get the result of the query in 1. If it was a RQL read query,
+   the database connection is released. If it was a write query, the connection
+   is then tied to the session until the transaction is commited or rollbacked.
+
+6. results are sent back to the client
+
+This implies several things:
+
+* when using a request, or code executed in hooks, this database connection
+  handling is totally transparent
+
+* however, take care when writing test: you are usually faking / testing both the
+  server and the client side, so you have to decide when to use self.request() /
+  self.session. Ask yourself "where the code I want to test will be running,
+  client or repository side ?". The response is usually : use a request :)
+  However, if you really need using a session:
+
+  - commit / rollback will free the database connection (unless explicitly told
+    not to do so).
+
+  - if you issue a query after that without asking for a database connection
+    (`session.get_cnxset()`), you will end up with a 'None type has no attribute
+    source()' error
Binary file doc/book/en/images/request_session.png has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/en/images/request_session.svg	Mon Jan 23 12:39:21 2012 +0100
@@ -0,0 +1,206 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="85.960938"
+   height="12.382812"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.48.1 r9760"
+   sodipodi:docname="request_session.svg">
+  <defs
+     id="defs4">
+    <marker
+       inkscape:stockid="Arrow1Lend"
+       orient="auto"
+       refY="0.0"
+       refX="0.0"
+       id="Arrow1Lend"
+       style="overflow:visible;">
+      <path
+         id="path3822"
+         d="M 0.0,0.0 L 5.0,-5.0 L -12.5,0.0 L 5.0,5.0 L 0.0,0.0 z "
+         style="fill-rule:evenodd;stroke:#000000;stroke-width:1.0pt;marker-start:none;"
+         transform="scale(0.8) rotate(180) translate(12.5,0)" />
+    </marker>
+  </defs>
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="0.98994949"
+     inkscape:cx="25.928992"
+     inkscape:cy="-185.87004"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     fit-margin-top="0"
+     fit-margin-left="0"
+     fit-margin-right="0"
+     fit-margin-bottom="0"
+     inkscape:window-width="766"
+     inkscape:window-height="1151"
+     inkscape:window-x="1152"
+     inkscape:window-y="24"
+     inkscape:window-maximized="0"
+     inkscape:snap-global="true" />
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(-263.52249,-495.73373)">
+    <rect
+       style="fill:#ffffff;stroke:#000000;stroke-width:0.92460138;stroke-opacity:1"
+       id="rect3773"
+       width="214.15233"
+       height="184.80336"
+       x="57.578697"
+       y="366.01306" />
+    <rect
+       id="rect2985"
+       width="216.86372"
+       height="183.54575"
+       x="348.50262"
+       y="367.78079"
+       style="fill:#ffffff;stroke:#000000;stroke-width:0.55298227;stroke-opacity:1" />
+    <text
+       xml:space="preserve"
+       style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
+       x="376.7869"
+       y="399.80365"
+       id="text3755"
+       sodipodi:linespacing="125%"><tspan
+         sodipodi:role="line"
+         id="tspan3757"
+         x="376.7869"
+         y="399.80365">Repository</tspan></text>
+    <rect
+       style="fill:#ffffff;stroke:#000000;stroke-opacity:1"
+       id="rect3759"
+       width="144.45181"
+       height="104.04572"
+       x="237.38585"
+       y="423.03714" />
+    <text
+       xml:space="preserve"
+       style="font-size:24px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
+       x="262.63968"
+       y="470.51431"
+       id="text3761"
+       sodipodi:linespacing="125%"><tspan
+         sodipodi:role="line"
+         id="tspan3763"
+         x="262.63968"
+         y="470.51431">DB API</tspan></text>
+    <text
+       xml:space="preserve"
+       style="font-size:16px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
+       x="262.63968"
+       y="507.88998"
+       id="text3765"
+       sodipodi:linespacing="125%"><tspan
+         sodipodi:role="line"
+         id="tspan3767"
+         x="262.63968"
+         y="507.88998">connection</tspan></text>
+    <text
+       xml:space="preserve"
+       style="font-size:16px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
+       x="419.21332"
+       y="509.91025"
+       id="text3769"
+       sodipodi:linespacing="125%"><tspan
+         sodipodi:role="line"
+         id="tspan3771"
+         x="419.21332"
+         y="509.91025">session</tspan></text>
+    <text
+       xml:space="preserve"
+       style="font-size:32px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
+       x="102.02541"
+       y="397.78333"
+       id="text3775"
+       sodipodi:linespacing="125%"><tspan
+         sodipodi:role="line"
+         id="tspan3777"
+         x="102.02541"
+         y="397.78333">Client</tspan></text>
+    <text
+       xml:space="preserve"
+       style="font-size:16px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
+       x="116.16754"
+       y="507.88995"
+       id="text3779"
+       sodipodi:linespacing="125%"><tspan
+         sodipodi:role="line"
+         id="tspan3781"
+         x="116.16754"
+         y="507.88995">request</tspan></text>
+    <text
+       xml:space="preserve"
+       style="font-size:16px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
+       x="361.50729"
+       y="585.89832"
+       id="text3802"
+       sodipodi:linespacing="125%"><tspan
+         sodipodi:role="line"
+         id="tspan3804"
+         x="361.50729"
+         y="585.89832">database </tspan><tspan
+         sodipodi:role="line"
+         x="361.50729"
+         y="605.89832"
+         id="tspan3806">connection</tspan></text>
+    <rect
+       style="fill:#ffffff;stroke:#000000;stroke-width:1.48014534;stroke-opacity:1"
+       id="rect3808"
+       width="192.09367"
+       height="58.095726"
+       x="365.79443"
+       y="621.50018" />
+    <text
+       xml:space="preserve"
+       style="font-size:36px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans"
+       x="369.5885"
+       y="662.66992"
+       id="text3810"
+       sodipodi:linespacing="125%"><tspan
+         sodipodi:role="line"
+         id="tspan3812"
+         x="369.5885"
+         y="662.66992">Database</tspan></text>
+    <path
+       style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;marker-end:none"
+       d="M 197.57252,125.76645 195.76971,55.592808"
+       id="path4260"
+       inkscape:connector-type="polyline"
+       inkscape:connector-curvature="3"
+       inkscape:connection-start="#rect3808"
+       inkscape:connection-start-point="d4"
+       inkscape:connection-end="#rect2985"
+       inkscape:connection-end-point="d4"
+       transform="translate(263.52249,495.73373)" />
+  </g>
+</svg>
--- a/hooks/syncschema.py	Tue Jan 17 11:55:32 2012 +0100
+++ b/hooks/syncschema.py	Mon Jan 23 12:39:21 2012 +0100
@@ -487,13 +487,17 @@
         # set default value, using sql for performance and to avoid
         # modification_date update
         if default:
-            if  rdefdef.object in ('Date', 'Datetime'):
+            if rdefdef.object in ('Date', 'Datetime', 'TZDatetime'):
+                # XXX may may want to use creation_date
                 if default == 'TODAY':
                     default = syssource.dbhelper.sql_current_date()
                 elif default == 'NOW':
                     default = syssource.dbhelper.sql_current_timestamp()
-            session.system_sql('UPDATE %s SET %s=%%(default)s' % (table, column),
-                               {'default': default})
+                session.system_sql('UPDATE %s SET %s=%(default)s'
+                                   % (table, column, default))
+            else:
+                session.system_sql('UPDATE %s SET %s=%%(default)s' % (table, column),
+                                   {'default': default})
 
     def revertprecommit_event(self):
         # revert changes on in memory schema
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/data_schemareader/schema.py	Mon Jan 23 12:39:21 2012 +0100
@@ -0,0 +1,11 @@
+from cubicweb.schemas.base import in_group, CWSourceSchemaConfig
+# copy __permissions__ to avoid modifying a shared dictionary
+in_group.__permissions__ = in_group.__permissions__.copy()
+in_group.__permissions__['read'] = ('managers',)
+
+cw_for_source = CWSourceSchemaConfig.get_relation('cw_for_source')
+cw_for_source.__permissions__ = {'read': ('managers', 'users'),
+                                 'add': ('managers',),
+                                 'delete': ('managers',)}
+
+
--- a/test/unittest_schema.py	Tue Jan 17 11:55:32 2012 +0100
+++ b/test/unittest_schema.py	Mon Jan 23 12:39:21 2012 +0100
@@ -265,6 +265,20 @@
         schema = CubicWebSchema('Test Schema')
         schema.add_entity_type(EntityType('NaN'))
 
+    def test_relation_perm_overriding(self):
+        loader = CubicWebSchemaLoader()
+        config = TestConfiguration('data', apphome=join(dirname(__file__), 'data_schemareader'))
+        config.bootstrap_cubes()
+        schema = loader.load(config)
+        self.assertEqual(schema['in_group'].rdefs.values()[0].permissions,
+                         {'read': ('managers',),
+                          'add': ('managers',),
+                          'delete': ('managers',)})
+        self.assertEqual(schema['cw_for_source'].rdefs.values()[0].permissions,
+                         {'read': ('managers', 'users'),
+                          'add': ('managers',),
+                          'delete': ('managers',)})
+
 
 class BadSchemaTC(TestCase):
     def setUp(self):
--- a/view.py	Tue Jan 17 11:55:32 2012 +0100
+++ b/view.py	Mon Jan 23 12:39:21 2012 +0100
@@ -417,6 +417,10 @@
         """return some rql to be executed if the result set is None"""
         return self.default_rql
 
+    def no_entities(self, **kwargs):
+        """override to display something when no entities were found"""
+        pass
+
     def call(self, **kwargs):
         """override call to execute rql returned by the .startup_rql method if
         necessary
@@ -424,8 +428,11 @@
         rset = self.cw_rset
         if rset is None:
             rset = self.cw_rset = self._cw.execute(self.startup_rql())
-        for i in xrange(len(rset)):
-            self.wview(self.__regid__, rset, row=i, **kwargs)
+        if rset:
+            for i in xrange(len(rset)):
+                self.wview(self.__regid__, rset, row=i, **kwargs)
+        else:
+            self.no_entities(**kwargs)
 
 
 class AnyRsetView(View):
--- a/web/data/excanvas.js	Tue Jan 17 11:55:32 2012 +0100
+++ b/web/data/excanvas.js	Mon Jan 23 12:39:21 2012 +0100
@@ -1375,7 +1375,7 @@
       case null:
       case '':
         this.repetition_ = 'repeat';
-        break
+        break;
       case 'repeat-x':
       case 'repeat-y':
       case 'no-repeat':
--- a/web/httpcache.py	Tue Jan 17 11:55:32 2012 +0100
+++ b/web/httpcache.py	Mon Jan 23 12:39:21 2012 +0100
@@ -23,6 +23,7 @@
 from datetime import datetime
 
 # time delta usable to convert localized time to GMT time
+# XXX this become erroneous after a DST transition!!!
 GMTOFFSET = - (datetime.now() - datetime.utcnow())
 
 class NoHTTPCacheManager(object):
--- a/web/request.py	Tue Jan 17 11:55:32 2012 +0100
+++ b/web/request.py	Mon Jan 23 12:39:21 2012 +0100
@@ -550,6 +550,10 @@
             assert expires is None, 'both max age and expires cant be specified'
             expires = maxage + time.time()
         elif expires:
+            # we don't want to handle times before the EPOCH (cause bug on
+            # windows). Also use > and not >= else expires == 0 and Cookie think
+            # that means no expire...
+            assert expires + GMTOFFSET > date(1970, 1, 1)
             expires = timegm((expires + GMTOFFSET).timetuple())
         else:
             expires = None
@@ -564,11 +568,7 @@
             warn('[3.13] remove_cookie now take only a name as argument',
                  DeprecationWarning, stacklevel=2)
             name = bwcompat
-        self.set_cookie(name, '', maxage=0,
-                        # substracting GMTOFFSET because set_cookie
-                        # expects a localtime and we don't want to
-                        # handle times before the EPOCH
-                        expires=date(1970, 1, 1) - GMTOFFSET) 
+        self.set_cookie(name, '', maxage=0, expires=date(2000, 1, 1))
 
     def set_content_type(self, content_type, filename=None, encoding=None):
         """set output content type for this request. An optional filename
--- a/web/views/startup.py	Tue Jan 17 11:55:32 2012 +0100
+++ b/web/views/startup.py	Mon Jan 23 12:39:21 2012 +0100
@@ -1,4 +1,4 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -53,7 +53,8 @@
     add_etype_links = ()
     skip_startup_views = set( ('index', 'manage', 'schema', 'owl', 'changelog',
                                'systempropertiesform', 'propertiesform',
-                               'cw.user-management', 'cw.source-management',
+                               'cw.users-and-groups-management', 'cw.groups-management', 
+                               'cw.users-management', 'cw.sources-management',
                                'siteinfo', 'info', 'registry', 'gc',
                                'tree') )