backport stable
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Mon, 26 Sep 2011 19:24:08 +0200
changeset 7868 39a54b88906d
parent 7859 15809afe093b (current diff)
parent 7867 0abed94a9859 (diff)
child 7869 2c6ef18e956a
backport stable
__pkginfo__.py
i18n/de.po
i18n/en.po
i18n/es.po
i18n/fr.po
test/unittest_rqlrewrite.py
web/request.py
web/views/cwproperties.py
--- a/.hgtags	Mon Sep 26 18:03:38 2011 +0200
+++ b/.hgtags	Mon Sep 26 19:24:08 2011 +0200
@@ -223,3 +223,7 @@
 a62f24e1497e953fbaed5894f6064a64f7ac0be3 cubicweb-version-3.10.x
 20d9c550c57eb6f9adcb0cfab1c11b6b8793afb6 cubicweb-version-3.13.5
 2e9dd7d945557c210d3b79153c65f6885e755315 cubicweb-debian-version-3.13.5-1
+074c848a3712a77737d9a1bfbb618c75f5c0cbfa cubicweb-version-3.12.10
+9dfd21fa0a8b9f121a08866ad3e2ebd1dd06790d cubicweb-debian-version-3.12.10-1
+17c007ad845abbac82e12146abab32a634657574 cubicweb-version-3.13.6
+8a8949ca5351d48c5cf795ccdff06c1d4aab2ce0 cubicweb-debian-version-3.13.6-1
--- a/__pkginfo__.py	Mon Sep 26 18:03:38 2011 +0200
+++ b/__pkginfo__.py	Mon Sep 26 19:24:08 2011 +0200
@@ -22,7 +22,11 @@
 
 modname = distname = "cubicweb"
 
+<<<<<<< /home/syt/src/cubicweb/__pkginfo__.py
 numversion = (3, 14, 0)
+=======
+numversion = (3, 13, 6)
+>>>>>>> /tmp/__pkginfo__.py~other.Prvgd6
 version = '.'.join(str(num) for num in numversion)
 
 description = "a repository of entities / relations for knowledge management"
--- a/debian/changelog	Mon Sep 26 18:03:38 2011 +0200
+++ b/debian/changelog	Mon Sep 26 19:24:08 2011 +0200
@@ -1,3 +1,9 @@
+cubicweb (3.13.6-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Mon, 26 Sep 2011 18:36:00 +0200
+
 cubicweb (3.13.5-1) unstable; urgency=low
 
   * new upstream release
@@ -28,6 +34,12 @@
 
  -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Tue, 12 Jul 2011 12:23:54 +0200
 
+cubicweb (3.12.10-1) unstable; urgency=low
+
+  * new upstream release
+
+ -- Sylvain Thénault <sylvain.thenault@logilab.fr>  Mon, 26 Sep 2011 17:22:29 +0200
+
 cubicweb (3.12.9-1) unstable; urgency=low
 
   * new upstream release
--- a/devtools/__init__.py	Mon Sep 26 18:03:38 2011 +0200
+++ b/devtools/__init__.py	Mon Sep 26 19:24:08 2011 +0200
@@ -260,8 +260,9 @@
     Example usage::
 
       class MyTests(CubicWebTC):
-          _config = RealDatabseConfiguration('myapp',
-                                             sourcefile='/path/to/sources')
+          _config = RealDatabaseConfiguration('myapp',
+                                              sourcefile='/path/to/sources')
+
           def test_something(self):
               rset = self.execute('Any X WHERE X is CWUser')
               self.view('foaf', rset)
--- a/devtools/fake.py	Mon Sep 26 18:03:38 2011 +0200
+++ b/devtools/fake.py	Mon Sep 26 19:24:08 2011 +0200
@@ -63,8 +63,8 @@
         self._session_data = {}
         self._headers_in = Headers()
 
-    def set_cookie(self, cookie, key, maxage=300, expires=None):
-        super(FakeRequest, self).set_cookie(cookie, key, maxage=300, expires=None)
+    def set_cookie(self, name, value, maxage=300, expires=None, secure=False):
+        super(FakeRequest, self).set_cookie(name, value, maxage, expires, secure)
         cookie = self.get_response_header('Set-Cookie')
         self._headers_in.setHeader('Cookie', cookie)
 
--- a/devtools/repotest.py	Mon Sep 26 18:03:38 2011 +0200
+++ b/devtools/repotest.py	Mon Sep 26 19:24:08 2011 +0200
@@ -329,9 +329,10 @@
 
 # monkey patch some methods to get predicatable results #######################
 
-from cubicweb.rqlrewrite import RQLRewriter
-_orig_insert_snippets = RQLRewriter.insert_snippets
-_orig_build_variantes = RQLRewriter.build_variantes
+from cubicweb import rqlrewrite
+_orig_iter_relations = rqlrewrite.iter_relations
+_orig_insert_snippets = rqlrewrite.RQLRewriter.insert_snippets
+_orig_build_variantes = rqlrewrite.RQLRewriter.build_variantes
 
 def _insert_snippets(self, snippets, varexistsmap=None):
     _orig_insert_snippets(self, sorted(snippets, snippet_cmp), varexistsmap)
@@ -415,9 +416,13 @@
 def _syntax_tree_search(*args, **kwargs):
     return deepcopy(_orig_syntax_tree_search(*args, **kwargs))
 
+def _ordered_iter_relations(stinfo):
+    return sorted(_orig_iter_relations(stinfo), key=lambda x:x.r_type)
+
 def do_monkey_patch():
-    RQLRewriter.insert_snippets = _insert_snippets
-    RQLRewriter.build_variantes = _build_variantes
+    rqlrewrite.iter_relations = _ordered_iter_relations
+    rqlrewrite.RQLRewriter.insert_snippets = _insert_snippets
+    rqlrewrite.RQLRewriter.build_variantes = _build_variantes
     ExecutionPlan._check_permissions = _check_permissions
     ExecutionPlan.tablesinorder = None
     ExecutionPlan.init_temp_table = _init_temp_table
@@ -426,8 +431,9 @@
     PyroRQLSource.syntax_tree_search = _syntax_tree_search
 
 def undo_monkey_patch():
-    RQLRewriter.insert_snippets = _orig_insert_snippets
-    RQLRewriter.build_variantes = _orig_build_variantes
+    rqlrewrite.iter_relations = _orig_iter_relations
+    rqlrewrite.RQLRewriter.insert_snippets = _orig_insert_snippets
+    rqlrewrite.RQLRewriter.build_variantes = _orig_build_variantes
     ExecutionPlan._check_permissions = _orig_check_permissions
     ExecutionPlan.init_temp_table = _orig_init_temp_table
     PartPlanInformation.merge_input_maps = _orig_merge_input_maps
--- a/doc/book/en/devrepo/testing.rst	Mon Sep 26 18:03:38 2011 +0200
+++ b/doc/book/en/devrepo/testing.rst	Mon Sep 26 19:24:08 2011 +0200
@@ -337,13 +337,12 @@
                                             sourcefile='/path/to/realdb_sources')
 
         def test_blog_rss(self):
-	    req = self.request()
-	    rset = req.execute('Any B ORDERBY D DESC WHERE B is BlogEntry, '
-	                       'B created_by U, U login "logilab", B creation_date D')
+            req = self.request()
+            rset = req.execute('Any B ORDERBY D DESC WHERE B is BlogEntry, '
+                'B created_by U, U login "logilab", B creation_date D')
             self.view('rss', rset)
 
 
-
 Testing with other cubes
 ------------------------
 
--- a/etwist/http.py	Mon Sep 26 18:03:38 2011 +0200
+++ b/etwist/http.py	Mon Sep 26 19:24:08 2011 +0200
@@ -1,7 +1,7 @@
 """twisted server for CubicWeb web instances
 
 :organization: Logilab
-:copyright: 2001-2010 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
+:copyright: 2001-2011 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
 :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
 """
@@ -25,25 +25,14 @@
     def _init_headers(self):
         if self._headers_out is None:
             return
-
-        # initialize cookies
-        cookies = self._headers_out.getHeader('set-cookie') or []
-        for cookie in cookies:
-            self._twreq.addCookie(cookie.name, cookie.value, cookie.expires,
-                                  cookie.domain, cookie.path, #TODO max-age
-                                  comment = cookie.comment, secure=cookie.secure)
-        self._headers_out.removeHeader('set-cookie')
-
-        # initialize other headers
-        for k, v in self._headers_out.getAllRawHeaders():
-            self._twreq.setHeader(k, v[0])
-
+        # initialize headers
+        for k, values in self._headers_out.getAllRawHeaders():
+            self._twreq.responseHeaders.setRawHeaders(k, values)
         # add content-length if not present
         if (self._headers_out.getHeader('content-length') is None
             and self._stream is not None):
            self._twreq.setHeader('content-length', len(self._stream))
 
-
     def _finalize(self):
         # we must set code before writing anything, else it's too late
         if self._code is not None:
--- a/i18n/de.po	Mon Sep 26 18:03:38 2011 +0200
+++ b/i18n/de.po	Mon Sep 26 19:24:08 2011 +0200
@@ -225,6 +225,9 @@
 "können ein <a href=\"%s\">vollständiges Schema</a> mit Meta-Daten anzeigen.</"
 "div>"
 
+msgid "<no relation>"
+msgstr ""
+
 msgid "<not specified>"
 msgstr "<nicht spezifiziert>"
 
@@ -3595,9 +3598,6 @@
 msgid "searching for"
 msgstr "Suche nach"
 
-msgid "secondary"
-msgstr "sekundär"
-
 msgid "security"
 msgstr "Sicherheit"
 
@@ -4461,9 +4461,12 @@
 #, python-format
 msgid "you should un-inline relation %s which is supported and may be crossed "
 msgstr ""
+<<<<<<< /home/syt/src/cubicweb/i18n/de.po
 
 #~ msgid "Schema of the data model"
 #~ msgstr "Schema des Datenmodells"
 
 #~ msgid "instance schema"
 #~ msgstr "Schema der Instanz"
+=======
+>>>>>>> /tmp/de.po~other.fn7YK0
--- a/i18n/en.po	Mon Sep 26 18:03:38 2011 +0200
+++ b/i18n/en.po	Mon Sep 26 19:24:08 2011 +0200
@@ -214,6 +214,9 @@
 "can also display a <a href=\"%s\">complete schema with meta-data</a>.</div>"
 msgstr ""
 
+msgid "<no relation>"
+msgstr ""
+
 msgid "<not specified>"
 msgstr ""
 
@@ -1041,9 +1044,6 @@
 msgid "add a CWRType"
 msgstr "add a relation type"
 
-msgid "add a CWSource"
-msgstr "add a source"
-
 msgctxt "inlined:CWUser.use_email.subject"
 msgid "add a EmailAddress"
 msgstr "add an email address"
@@ -3508,9 +3508,6 @@
 msgid "searching for"
 msgstr ""
 
-msgid "secondary"
-msgstr ""
-
 msgid "security"
 msgstr ""
 
@@ -4349,9 +4346,12 @@
 #, python-format
 msgid "you should un-inline relation %s which is supported and may be crossed "
 msgstr ""
+<<<<<<< /home/syt/src/cubicweb/i18n/en.po
 
 #~ msgid "add a CWSourceSchemaConfig"
 #~ msgstr "add an item to mapping "
 
 #~ msgid "siteinfo"
 #~ msgstr "site information"
+=======
+>>>>>>> /tmp/en.po~other.NvgtGM
--- a/i18n/es.po	Mon Sep 26 18:03:38 2011 +0200
+++ b/i18n/es.po	Mon Sep 26 19:24:08 2011 +0200
@@ -226,6 +226,9 @@
 "pero se puede ver a un <a href=\"%s\">modelo completo con meta-datos</a>.</"
 "div>"
 
+msgid "<no relation>"
+msgstr ""
+
 msgid "<not specified>"
 msgstr "<no especificado>"
 
@@ -1091,9 +1094,12 @@
 msgid "add a CWRType"
 msgstr "Agregar un tipo de relación"
 
+<<<<<<< /home/syt/src/cubicweb/i18n/es.po
 msgid "add a CWSource"
 msgstr "agregar una fuente"
 
+=======
+>>>>>>> /tmp/es.po~other.Yb6Y5q
 msgctxt "inlined:CWUser.use_email.subject"
 msgid "add a EmailAddress"
 msgstr "Agregar correo electrónico"
@@ -3645,9 +3651,6 @@
 msgid "searching for"
 msgstr "Buscando"
 
-msgid "secondary"
-msgstr "Secundaria"
-
 msgid "security"
 msgstr "Seguridad"
 
@@ -4512,6 +4515,7 @@
 msgstr ""
 "usted debe  quitar la puesta en línea de la relación %s que es aceptada y "
 "puede ser cruzada"
+<<<<<<< /home/syt/src/cubicweb/i18n/es.po
 
 #~ msgid "Schema of the data model"
 #~ msgstr "Esquema del modelo de datos"
@@ -4524,3 +4528,5 @@
 
 #~ msgid "siteinfo"
 #~ msgstr "información"
+=======
+>>>>>>> /tmp/es.po~other.Yb6Y5q
--- a/i18n/fr.po	Mon Sep 26 18:03:38 2011 +0200
+++ b/i18n/fr.po	Mon Sep 26 19:24:08 2011 +0200
@@ -226,6 +226,9 @@
 "<div>Ce schéma du modèle de données <em>exclue</em> les méta-données, mais "
 "vous pouvez afficher un <a href=\"%s\">schéma complet</a>.</div>"
 
+msgid "<no relation>"
+msgstr "<pas de relation>"
+
 msgid "<not specified>"
 msgstr "<non spécifié>"
 
@@ -1091,9 +1094,12 @@
 msgid "add a CWRType"
 msgstr "ajouter un type de relation"
 
+<<<<<<< /home/syt/src/cubicweb/i18n/fr.po
 msgid "add a CWSource"
 msgstr "ajouter une source"
 
+=======
+>>>>>>> /tmp/fr.po~other.KbuYNx
 msgctxt "inlined:CWUser.use_email.subject"
 msgid "add a EmailAddress"
 msgstr "ajouter une adresse électronique"
@@ -3651,9 +3657,6 @@
 msgid "searching for"
 msgstr "Recherche de"
 
-msgid "secondary"
-msgstr "secondaire"
-
 msgid "security"
 msgstr "sécurité"
 
--- a/rqlrewrite.py	Mon Sep 26 18:03:38 2011 +0200
+++ b/rqlrewrite.py	Mon Sep 26 19:24:08 2011 +0200
@@ -119,6 +119,10 @@
     return newsolutions
 
 
+def iter_relations(stinfo):
+    # this is a function so that test may return relation in a predictable order
+    return stinfo['relations'] - stinfo['rhsrelations']
+
 class Unsupported(Exception):
     """raised when an rql expression can't be inserted in some rql query
     because it create an unresolvable query (eg no solutions found)
@@ -349,7 +353,7 @@
             while todo:
                 varname, stinfo = todo.pop()
                 done.add(varname)
-                for rel in stinfo['relations'] - stinfo['rhsrelations']:
+                for rel in iter_relations(stinfo):
                     if rel in done:
                         continue
                     done.add(rel)
@@ -380,7 +384,7 @@
                         if vref.name not in done and rschema.inlined:
                             # we can use vref here define in above for loop
                             ostinfo = vref.variable.stinfo
-                            for orel in ostinfo['relations'] - ostinfo['rhsrelations']:
+                            for orel in iter_relations(ostinfo):
                                 orschema = get_rschema(orel.r_type)
                                 if orschema.final or orschema.inlined:
                                     todo.append( (vref.name, ostinfo) )
--- a/skeleton/test/realdb_test_CUBENAME.py	Mon Sep 26 18:03:38 2011 +0200
+++ b/skeleton/test/realdb_test_CUBENAME.py	Mon Sep 26 19:24:08 2011 +0200
@@ -18,8 +18,8 @@
 """
 
 """
-from cubicweb.devtools import buildconfig, loadconfig
-from cubicweb.devtools.testlib import RealDBTest
+from cubicweb.devtools.testlib import CubicWebTC
+from cubicweb.devtools.realdbtest import buildconfig, loadconfig
 
 def setUpModule(options):
     if options.source:
@@ -32,7 +32,8 @@
                                                options.epassword)
     RealDatabaseTC.configcls = configcls
 
-class RealDatabaseTC(RealDBTest):
+
+class RealDatabaseTC(CubicWebTC):
     configcls = None # set by setUpModule()
 
     def test_all_primaries(self):
--- a/test/unittest_rqlrewrite.py	Mon Sep 26 18:03:38 2011 +0200
+++ b/test/unittest_rqlrewrite.py	Mon Sep 26 19:24:08 2011 +0200
@@ -21,9 +21,8 @@
 from yams import BadSchemaDefinition
 from rql import parse, nodes, RQLHelper
 
-from cubicweb import Unauthorized
+from cubicweb import Unauthorized, rqlrewrite
 from cubicweb.schema import RRQLExpression, ERQLExpression
-from cubicweb.rqlrewrite import RQLRewriter
 from cubicweb.devtools import repotest, TestServerConfiguration
 
 
@@ -62,9 +61,10 @@
             @staticmethod
             def simplify(mainrqlst, needcopy=False):
                 rqlhelper.simplify(rqlst, needcopy)
-    rewriter = RQLRewriter(mock_object(vreg=FakeVReg, user=(mock_object(eid=1))))
+    rewriter = rqlrewrite.RQLRewriter(
+        mock_object(vreg=FakeVReg, user=(mock_object(eid=1))))
     snippets = []
-    for v, exprs in snippets_map.items():
+    for v, exprs in sorted(snippets_map.items()):
         rqlexprs = [isinstance(snippet, basestring)
                     and mock_object(snippet_rqlst=parse('Any X WHERE '+snippet).children[0],
                                     expression='Any X WHERE '+snippet)
@@ -210,11 +210,11 @@
                         }, {})
         # XXX suboptimal
         self.assertEqual(rqlst.as_string(),
-                             "Any C,A,R WITH A,R,C BEING "
-                             "(Any A,R,C WHERE A ref R, A? inlined_card C, "
-                             "(A is NULL) OR (EXISTS(A inlined_card B, B require_permission D, "
-                             "B is Card, D is CWPermission)), "
-                             "A is Affaire, C is Card, EXISTS(C require_permission E, E is CWPermission))")
+                         "Any C,A,R WITH A,C,R BEING "
+                         "(Any A,C,R WHERE A? inlined_card C, A ref R, "
+                         "(A is NULL) OR (EXISTS(A inlined_card B, B require_permission D, "
+                         "B is Card, D is CWPermission)), "
+                         "A is Affaire, C is Card, EXISTS(C require_permission E, E is CWPermission))")
 
     # def test_optional_var_inlined_has_perm(self):
     #     c1 = ('X require_permission P')
--- a/web/application.py	Mon Sep 26 18:03:38 2011 +0200
+++ b/web/application.py	Mon Sep 26 19:24:08 2011 +0200
@@ -236,12 +236,10 @@
 
     def open_session(self, req, allow_no_cnx=True):
         session = self.session_manager.open_session(req, allow_no_cnx=allow_no_cnx)
-        cookie = req.get_cookie()
         sessioncookie = self.session_cookie(req)
-        cookie[sessioncookie] = session.sessionid
-        if req.https and req.base_url().startswith('https://'):
-            cookie[sessioncookie]['secure'] = True
-        req.set_cookie(cookie, sessioncookie, maxage=None)
+        secure = req.https and req.base_url().startswith('https://')
+        req.set_cookie(sessioncookie, session.sessionid,
+                       maxage=None, secure=secure)
         if not session.anonymous_session:
             self.session_manager.postlogin(req)
         return session
@@ -251,8 +249,7 @@
         `AuthenticationError`
         """
         self.session_manager.close_session(req.session)
-        sessioncookie = self.session_cookie(req)
-        req.remove_cookie(req.get_cookie(), sessioncookie)
+        req.remove_cookie(self.session_cookie(req))
         raise LogOut(url=goto_url)
 
     # these are overridden by set_log_methods below
--- a/web/data/cubicweb.facets.css	Mon Sep 26 18:03:38 2011 +0200
+++ b/web/data/cubicweb.facets.css	Mon Sep 26 19:24:08 2011 +0200
@@ -15,7 +15,7 @@
  color: #000;
  margin-bottom: 2px;
  cursor: pointer;
- font: bold 100% Georgia;
+ font: %(facet_titleFont)s;
 }
 
 div.facetTitle a {
@@ -30,8 +30,8 @@
  color: #000 !important;
 }
 
-div.overflowed{
-  height: 12em;
+div.overflowed {
+  height: %(facet_overflowedHeight)s;
   overflow-y: auto;
 }
 
--- a/web/data/uiprops.py	Mon Sep 26 18:03:38 2011 +0200
+++ b/web/data/uiprops.py	Mon Sep 26 19:24:08 2011 +0200
@@ -165,3 +165,9 @@
 infoMsgBgImg = 'url("information.png") 5px center no-repeat'
 errorMsgBgImg = 'url("error.png") 100% 50% no-repeat'
 errorMsgColor = '#ed0d0d'
+
+# facets
+facet_titleFont = 'bold 100% Georgia'
+facet_overflowedHeight = '12em'
+
+
--- a/web/facet.py	Mon Sep 26 18:03:38 2011 +0200
+++ b/web/facet.py	Mon Sep 26 19:24:08 2011 +0200
@@ -587,7 +587,7 @@
     _select_target_entity = True
 
     title = property(rtype_facet_title)
-    no_relation_label = '<no relation>'
+    no_relation_label = _('<no relation>')
 
     def __repr__(self):
         return '<%s on (%s-%s)>' % (self.__class__.__name__, self.rtype, self.role)
@@ -991,7 +991,7 @@
       class AgencyFacet(RQLPathFacet):
           __regid__ = 'agency'
           # this facet should only be selected when visualizing offices
-          __select__ = RelationFacet.__select__ & is_instance('Office')
+          __select__ = is_instance('Office')
           # this facet is a filter on the 'Agency' entities linked to the office
           # through the 'proposed_by' relation, where the office is the subject
           # of the relation
@@ -1002,7 +1002,7 @@
       class PostalCodeFacet(RQLPathFacet):
           __regid__ = 'postalcode'
           # this facet should only be selected when visualizing offices
-          __select__ = RelationAttributeFacet.__select__ & is_instance('Office')
+          __select__ = is_instance('Office')
           # this facet is a filter on the PostalAddress entities linked to the
           # office through the 'has_address' relation, where the office is the
           # subject of the relation
@@ -1336,6 +1336,24 @@
 
 
 ## html widets ################################################################
+_DEFAULT_CONSTANT_VOCAB_WIDGET_HEIGHT = 9
+
+@cached
+def _css_height_to_line_count(vreg):
+    cssprop = vreg.config.uiprops['facet_overflowedHeight'].lower().strip()
+    # let's talk a bit ...
+    # we try to deduce a number of displayed lines from a css property
+    # there is a linear (rough empiric coefficient == 0.73) relation between
+    # css _em_ value and line qty
+    # if we get another unit we're out of luck and resort to one constant
+    # hence, it is strongly advised not to specify but ems for this css prop
+    if cssprop.endswith('em'):
+        try:
+            return int(cssprop[:-2]) * .73
+        except Exception:
+            vreg.warning('css property facet_overflowedHeight looks malformed (%r)',
+                         cssprop)
+    return _DEFAULT_CONSTANT_VOCAB_WIDGET_HEIGHT
 
 class FacetVocabularyWidget(htmlwidgets.HTMLWidget):
 
@@ -1343,8 +1361,10 @@
         self.facet = facet
         self.items = []
 
+    @cached
     def height(self):
-        return len(self.items) + 1
+        maxheight = _css_height_to_line_count(self.facet._cw.vreg)
+        return 1 + min(len(self.items), maxheight) + int(self.facet._support_and_compat())
 
     def append(self, item):
         self.items.append(item)
--- a/web/http_headers.py	Mon Sep 26 18:03:38 2011 +0200
+++ b/web/http_headers.py	Mon Sep 26 19:24:08 2011 +0200
@@ -1354,9 +1354,25 @@
         raw_header.append(value)
         self._headers[name] = _RecalcNeeded
 
+    def addHeader(self, name, value):
+        """
+        Add a parsed representatoin to a header that may or may not already exist.
+        If it exists, add it as a separate header to output; do not
+        replace anything.
+        """
+        name=name.lower()
+        header = self._headers.get(name)
+        if header is None:
+            # No header yet
+            header = []
+            self._headers[name] = header
+        elif header is _RecalcNeeded:
+            header = self._toParsed(name)
+        header.append(value)
+        self._raw_headers[name] = _RecalcNeeded
+
     def removeHeader(self, name):
         """Removes the header named."""
-
         name=name.lower()
         if self._raw_headers.has_key(name):
             del self._raw_headers[name]
--- a/web/request.py	Mon Sep 26 18:03:38 2011 +0200
+++ b/web/request.py	Mon Sep 26 19:24:08 2011 +0200
@@ -19,11 +19,12 @@
 
 __docformat__ = "restructuredtext en"
 
-import Cookie
 import hashlib
 import time
 import random
 import base64
+from Cookie import SimpleCookie
+from calendar import timegm
 from datetime import date
 from urlparse import urlsplit
 from itertools import count
@@ -42,7 +43,8 @@
 from cubicweb.view import STRICT_DOCTYPE, TRANSITIONAL_DOCTYPE_NOEXT
 from cubicweb.web import (INTERNAL_FIELD_VALUE, LOGGER, NothingToEdit,
                           RequestError, StatusResponse)
-from cubicweb.web.http_headers import Headers
+from cubicweb.web.httpcache import GMTOFFSET
+from cubicweb.web.http_headers import Headers, Cookie
 
 _MARKER = object()
 
@@ -518,30 +520,44 @@
 
     def get_cookie(self):
         """retrieve request cookies, returns an empty cookie if not found"""
+        # XXX use http_headers implementation
         try:
-            return Cookie.SimpleCookie(self.get_header('Cookie'))
+            return SimpleCookie(self.get_header('Cookie'))
         except KeyError:
-            return Cookie.SimpleCookie()
+            return SimpleCookie()
 
-    def set_cookie(self, cookie, key, maxage=300, expires=None):
-        """set / update a cookie key
+    def set_cookie(self, name, value, maxage=300, expires=None, secure=False):
+        """set / update a cookie
 
         by default, cookie will be available for the next 5 minutes.
         Give maxage = None to have a "session" cookie expiring when the
         client close its browser
         """
-        morsel = cookie[key]
-        if maxage is not None:
-            morsel['Max-Age'] = maxage
-        if expires:
-            morsel['expires'] = expires.strftime('%a, %d %b %Y %H:%M:%S %z')
+        if isinstance(name, SimpleCookie):
+            warn('[3.13] set_cookie now takes name and value as two first '
+                 'argument, not anymore cookie object and name',
+                 DeprecationWarning, stacklevel=2)
+            secure = name[value]['secure']
+            name, value = value, name[value].value
+        if maxage: # don't check is None, 0 may be specified
+            assert expires is None, 'both max age and expires cant be specified'
+            expires = maxage + time.time()
+        elif expires:
+            expires = timegm((expires + GMTOFFSET).timetuple())
+        else:
+            expires = None
         # make sure cookie is set on the correct path
-        morsel['path'] = self.base_url_path()
-        self.add_header('Set-Cookie', morsel.OutputString())
+        cookie = Cookie(name, value, self.base_url_path(), expires=expires,
+                        secure=secure)
+        self.headers_out.addHeader('Set-cookie', cookie)
 
-    def remove_cookie(self, cookie, key):
+    def remove_cookie(self, name, bwcompat=None):
         """remove a cookie by expiring it"""
-        self.set_cookie(cookie, key, maxage=0, expires=date(1970, 1, 1))
+        if bwcompat is not None:
+            warn('[3.13] remove_cookie now take only a name as argument',
+                 DeprecationWarning, stacklevel=2)
+            name = bwcompat
+        self.set_cookie(key, '', maxage=0, expires=date(1970, 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/basecontrollers.py	Mon Sep 26 18:03:38 2011 +0200
+++ b/web/views/basecontrollers.py	Mon Sep 26 19:24:08 2011 +0200
@@ -535,24 +535,20 @@
         statename = treecookiename(treeid)
         treestate = cookies.get(statename)
         if treestate is None:
-            cookies[statename] = nodeeid
-            self._cw.set_cookie(cookies, statename)
+            self._cw.set_cookie(statename, nodeeid)
         else:
             marked = set(filter(None, treestate.value.split(':')))
             if nodeeid in marked:
                 marked.remove(nodeeid)
             else:
                 marked.add(nodeeid)
-            cookies[statename] = ':'.join(marked)
-            self._cw.set_cookie(cookies, statename)
+            self._cw.set_cookie(statename, ':'.join(marked))
 
     @jsonize
     @deprecated("[3.13] use jQuery.cookie(cookiename, cookievalue, {path: '/'}) in js land instead")
     def js_set_cookie(self, cookiename, cookievalue):
         cookiename, cookievalue = str(cookiename), str(cookievalue)
-        cookies = self._cw.get_cookie()
-        cookies[cookiename] = cookievalue
-        self._cw.set_cookie(cookies, cookiename)
+        self._cw.set_cookie(cookiename, cookievalue)
 
     # relations edition stuff ##################################################
 
--- a/web/views/cwproperties.py	Mon Sep 26 18:03:38 2011 +0200
+++ b/web/views/cwproperties.py	Mon Sep 26 19:24:08 2011 +0200
@@ -102,8 +102,7 @@
         cookiename = self._cookie_name(group)
         cookie = cookies.get(cookiename)
         if cookie is None:
-            cookies[cookiename] = default
-            self._cw.set_cookie(cookies, cookiename, maxage=None)
+            self._cw.set_cookie(cookiename, default, maxage=None)
             status = default
         else:
             status = cookie.value
--- a/web/views/cwsources.py	Mon Sep 26 18:03:38 2011 +0200
+++ b/web/views/cwsources.py	Mon Sep 26 19:24:08 2011 +0200
@@ -263,12 +263,8 @@
 
     def call(self, **kwargs):
         self.w('<h1>%s</h1>' % self._cw._(self.title))
-        eschema = self._cw.vreg.schema.eschema('CWSource')
-        if eschema.has_perm(self._cw, 'add'):
-            self.w(u'<a href="%s" class="addButton right">%s</a>' % (
-                self._cw.build_url('add/%s' % eschema),
-                self._cw._('add a CWSource')))
-            self.w(u'<div class="clear"></div>')
+        self.w(add_etype_button(self._cw, 'CWSource'))
+        self.w(u'<div class="clear"></div>')
         self.wview('table', self._cw.execute(self.rql), displaycols=range(4))
 
 
--- a/web/views/facets.py	Mon Sep 26 18:03:38 2011 +0200
+++ b/web/views/facets.py	Mon Sep 26 19:24:08 2011 +0200
@@ -206,7 +206,6 @@
 class FilterTable(FacetFilterMixIn, AnyRsetView):
     __regid__ = 'facet.filtertable'
     __select__ = has_facets()
-    wdg_stack_size = 8
     compact_layout_threshold = 5
 
     def call(self, vid, divid, vidargs, cssclass=''):
@@ -235,10 +234,11 @@
         w(u'<table class="filter">\n')
         widget_queue = []
         queue_height = 0
+        wdg_stack_size = max(wdgs, key=lambda wdg:wdg.height()).height()
         w(u'<tr>\n')
         for wdg in wdgs:
             height = wdg.height()
-            if queue_height + height <= self.wdg_stack_size:
+            if queue_height + height <= wdg_stack_size:
                 widget_queue.append(wdg)
                 queue_height += height
                 continue
--- a/web/views/sessions.py	Mon Sep 26 18:03:38 2011 +0200
+++ b/web/views/sessions.py	Mon Sep 26 19:24:08 2011 +0200
@@ -99,10 +99,10 @@
         for forminternal_key in ('__form_id', '__domid', '__errorurl'):
             args.pop(forminternal_key, None)
         path = req.relative_path(False)
-        if path == 'login':
+        if path in ('login', 'logout') or req.form.get('vid') == 'loggedout':
             path = 'view'
             args['__message'] = req._('welcome %s !') % req.user.login
-            if 'vid' in req.form:
+            if 'vid' in req.form and req.form['vid'] != 'loggedout':
                 args['vid'] = req.form['vid']
             if 'rql' in req.form:
                 args['rql'] = req.form['rql']
--- a/web/views/tabs.py	Mon Sep 26 18:03:38 2011 +0200
+++ b/web/views/tabs.py	Mon Sep 26 19:24:08 2011 +0200
@@ -93,8 +93,7 @@
         activetab = cookies.get(cookiename)
         if activetab is None:
             domid = uilib.domid(default)
-            cookies[cookiename] = domid
-            self._cw.set_cookie(cookies, cookiename)
+            self._cw.set_cookie(cookiename, domid)
             return domid
         return activetab.value