refactor login box & form to enable easy pluggability
authorAurelien Campeas <aurelien.campeas@logilab.fr>
Mon, 04 Oct 2010 18:56:05 +0200
changeset 6389 72ba82a26e05
parent 6388 34317f395619
child 6390 3766853656d7
refactor login box & form to enable easy pluggability * vregistry.selectable: get all selectable object of fixed oid with given context * template headeri, logbox, logform: reorganize a bit the structure
selectors.py
vregistry.py
web/data/cubicweb.htmlhelpers.js
web/data/cubicweb.login.css
web/views/authentication.py
web/views/basecomponents.py
web/views/basetemplates.py
--- a/selectors.py	Mon Oct 04 18:55:57 2010 +0200
+++ b/selectors.py	Mon Oct 04 18:56:05 2010 +0200
@@ -1182,7 +1182,6 @@
     """
     return ~ authenticated_user()
 
-
 class match_user_groups(ExpectedValueSelector):
     """Return a non-zero score if request's user is in at least one of the
     groups given as initializer argument. Returned score is the number of groups
@@ -1213,6 +1212,24 @@
         return score
 
 
+class configuration_values(Selector):
+    """Return 1 if the instance is configured according to
+    the given value(s)"""
+
+    def __init__(self, key, values):
+        self._key = key
+        if isinstance(values, basestring):
+            values = (values,)
+        self._values = frozenset(values)
+
+    @lltrace
+    def __call__(self, cls, req, **kwargs):
+        try:
+            return self._score
+        except AttributeError:
+            self._score = req.vreg.config[self._key] in self._values
+        return self._score
+
 # Web request selectors ########################################################
 
 # XXX deprecate
--- a/vregistry.py	Mon Oct 04 18:55:57 2010 +0200
+++ b/vregistry.py	Mon Oct 04 18:56:05 2010 +0200
@@ -195,6 +195,18 @@
     select_object = deprecated('[3.6] use select_or_none instead of select_object'
                                )(select_or_none)
 
+    def selectable(self, oid, *args, **kwargs):
+        """return all appobjects having the given oid that are
+        selectable against the given context, in score order
+        """
+        objects = []
+        for appobject in self[oid]:
+            score = appobject.__select__(appobject, *args, **kwargs)
+            if score > 0:
+                objects.append((score, appobject))
+        return [obj(*args, **kwargs)
+                for _score, obj in sorted(objects)]
+
     def possible_objects(self, *args, **kwargs):
         """return an iterator on possible objects in this registry for the given
         context
--- a/web/data/cubicweb.htmlhelpers.js	Mon Oct 04 18:55:57 2010 +0200
+++ b/web/data/cubicweb.htmlhelpers.js	Mon Oct 04 18:56:05 2010 +0200
@@ -1,3 +1,14 @@
+/* in CW 3.10, we should move these functions in this namespace */
+cw.htmlhelpers = new Namespace('cw.htmlhelpers');
+
+jQuery.extend(cw.htmlhelpers, {
+    popupLoginBox: function(loginboxid, focusid) {
+        $('#'+loginboxid).toggleClass('hidden');
+        jQuery('#' + focusid +':visible').focus();
+    }
+});
+
+
 /**
  * .. function:: baseuri()
  *
@@ -97,10 +108,11 @@
  * toggles visibility of login popup div
  */
 // XXX used exactly ONCE in basecomponents
-function popupLoginBox() {
-    $('#popupLoginBox').toggleClass('hidden');
-    jQuery('#__login:visible').focus();
-}
+popupLoginBox = cw.utils.deprecatedFunction(
+    function() {
+        $('#popupLoginBox').toggleClass('hidden');
+        jQuery('#__login:visible').focus();
+});
 
 /**
  * .. function getElementsMatching(tagName, properties, \/* optional \*\/ parent)
--- a/web/data/cubicweb.login.css	Mon Oct 04 18:55:57 2010 +0200
+++ b/web/data/cubicweb.login.css	Mon Oct 04 18:56:05 2010 +0200
@@ -5,25 +5,25 @@
  *  :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
  */
 
-div#popupLoginBox {
+div.popupLoginBox {
   position: absolute;
   z-index: 400;
   right: 0px;
   width: 26em;
   padding: 0px 1px 1px;
-  background: %(listingBorderColor)s; 
+  background: %(listingBorderColor)s;
 }
 
-div#popupLoginBox label{
+div.popupLoginBox label{
   font-weight: bold;
 }
 
-div#popupLoginBox div#loginContent {
+div.popupLoginBox div.loginContent {
   background: #e6e4ce;
   padding: 5px 3px 4px;
 }
 
-div#loginBox {
+div.loginBox {
   position : absolute;
   top: 15%;
   left : 50%;
@@ -40,7 +40,7 @@
   font-size: 140%;
 }
 
-div#loginTitle {
+div.loginTitle {
   color: #fff;
   font-weight: bold;
   font-size: 140%;
@@ -49,32 +49,32 @@
   background: %(headerBgColor)s url("banner.png") repeat-x top left;
 }
 
-div#loginBox div#loginContent form {
+div.loginBox div.loginContent form {
   padding-top: 1em;
   width: 90%;
   margin: auto;
 }
 
-#popupLoginBox table td {
+.popupLoginBox table td {
   padding: 0px 3px;
   white-space: nowrap;
 }
 
-#loginContent table {
+.loginContent table {
   padding: 0px 0.5em;
   margin: auto;
 }
 
-#loginBox table td {
+.loginBox table td {
   padding: 0px 3px 0.6em;
   white-space: nowrap;
 }
 
-#loginBox .loginButton {
+.loginBox .loginButton {
   margin-top: 0.6em;
  }
 
-#loginContent input.data {
+.loginContent input.data {
   width: 12em;
 }
 
@@ -85,6 +85,6 @@
   background: #f0eff0 url("gradient-grey-up.png") left top repeat-x;
 }
 
-#loginContent .formButtonBar {
+.loginContent .formButtonBar {
   float: right;
 }
--- a/web/views/authentication.py	Mon Oct 04 18:55:57 2010 +0200
+++ b/web/views/authentication.py	Mon Oct 04 18:56:05 2010 +0200
@@ -37,6 +37,7 @@
 class WebAuthInfoRetreiver(Component):
     __registry__ = 'webauth'
     order = None
+    __abstract__ = True
 
     def authentication_information(self, req):
         """retreive authentication information from the given request, raise
@@ -51,6 +52,18 @@
         """
         pass
 
+    def request_has_auth_info(self, req):
+        """tells from the request if it has enough information
+        to proceed to authentication, would the current session
+        be invalidated
+        """
+        raise NotImplementedError()
+
+    def revalidate_login(self, req):
+        """returns a login string or None, for repository session
+        validation purposes
+        """
+        return None
 
 class LoginPasswordRetreiver(WebAuthInfoRetreiver):
     __regid__ = 'loginpwdauth'
@@ -65,6 +78,11 @@
             raise NoAuthInfo()
         return login, {'password': password}
 
+    def request_has_auth_info(self, req):
+        return '__login' in req.form
+
+    def revalidate_login(self, req):
+        return req.get_authorization()[0]
 
 class RepositoryAuthenticationManager(AbstractAuthenticationManager):
     """authenticate user associated to a request and check session validity"""
@@ -73,7 +91,7 @@
         super(RepositoryAuthenticationManager, self).__init__(vreg)
         self.repo = vreg.config.repository(vreg)
         self.log_queries = vreg.config['query-log-file']
-        self.authinforetreivers = sorted(vreg['webauth'].possible_objects(vreg),
+        self.authinforetrievers = sorted(vreg['webauth'].possible_objects(vreg),
                                          key=lambda x: x.order)
         # 2-uple login / password, login is None when no anonymous access
         # configured
@@ -88,10 +106,19 @@
 
         raise :exc:`InvalidSession` if session is corrupted for a reason or
         another and should be closed
+
+        also invoked while going from anonymous to logged in
         """
         # with this authentication manager, session is actually a dbapi
         # connection
-        login = req.get_authorization()[0]
+        for retriever in self.authinforetrievers:
+            if retriever.request_has_auth_info(req):
+                login = retriever.revalidate_login(req)
+                return self._validate_session(req, session, login)
+        # let's try with the current session
+        return self._validate_session(req, session, None)
+
+    def _validate_session(self, req, session, login):
         # check session.login and not user.login, since in case of login by
         # email, login and cnx.login are the email while user.login is the
         # actual user login
@@ -114,18 +141,19 @@
         raise :exc:`cubicweb.AuthenticationError` if authentication failed
         (no authentication info found or wrong user/password)
         """
-        for retreiver in self.authinforetreivers:
+        for retriever in self.authinforetrievers:
             try:
-                login, authinfo = retreiver.authentication_information(req)
+                login, authinfo = retriever.authentication_information(req)
             except NoAuthInfo:
                 continue
             try:
                 cnx = self._authenticate(login, authinfo)
             except AuthenticationError:
                 continue # the next one may succeed
-            for retreiver_ in self.authinforetreivers:
-                retreiver_.authenticated(retreiver, req, cnx, login, authinfo)
+            for retriever_ in self.authinforetrievers:
+                retriever_.authenticated(retriever, req, cnx, login, authinfo)
             return cnx, login, authinfo
+
         # false if no authentication info found, eg this is not an
         # authentication failure
         if 'login' in locals():
--- a/web/views/basecomponents.py	Mon Oct 04 18:55:57 2010 +0200
+++ b/web/views/basecomponents.py	Mon Oct 04 18:56:05 2010 +0200
@@ -28,8 +28,8 @@
 from logilab.mtconverter import xml_escape
 from rql import parse
 
-from cubicweb.selectors import (yes, multi_etypes_rset,
-                                match_form_params, match_context,
+from cubicweb.selectors import (yes, multi_etypes_rset, match_form_params,
+                                match_context, configuration_values,
                                 anonymous_user, authenticated_user)
 from cubicweb.schema import display_name
 from cubicweb.utils import wrap_on_write
@@ -97,24 +97,36 @@
     """if the user is the anonymous user, build a link to login else display a menu
     with user'action (preference, logout, etc...)
     """
+    __abstract__ = True
     cw_property_defs = VISIBLE_PROP_DEF
     # don't want user to hide this component using an cwproperty
     site_wide = True
     __regid__ = 'loggeduserlink'
 
 
-class AnonUserLink(_UserLink):
-    __select__ = _UserLink.__select__ & anonymous_user()
+class CookieAnonUserLink(_UserLink):
+    __select__ = _UserLink.__select__ & configuration_values('auth-mode', 'cookie') & anonymous_user()
+    loginboxid = 'popupLoginBox'
+
     def call(self):
-        if self._cw.vreg.config['auth-mode'] == 'cookie':
-            self.w(self._cw._('anonymous'))
-            self.w(u'''&#160;[<a class="logout" href="javascript: popupLoginBox();">%s</a>]'''
-                   % (self._cw._('i18n_login_popup')))
-        else:
-            self.w(self._cw._('anonymous'))
-            self.w(u'&#160;[<a class="logout" href="%s">%s</a>]'
-                   % (self._cw.build_url('login'), self._cw._('login')))
+        w = self.w
+        w(self._cw._('anonymous'))
+        w(u"""[<a class="logout" href="javascript: cw.htmlhelpers.popupLoginBox('%s', '__login');">%s</a>]"""
+          % (self.loginboxid, self._cw._('i18n_login_popup')))
+        self.wview('logform', rset=self.cw_rset, id=self.loginboxid,
+                   klass='hidden', title=False, showmessage=False)
 
+class HTTPAnonUserLink(_UserLink):
+    __select__ = _UserLink.__select__ & configuration_values('auth-mode', 'http') & anonymous_user()
+    loginboxid = 'popupLoginBox'
+
+    def call(self):
+        w = self.w
+        w(self._cw._('anonymous'))
+        # this redirects to the 'login' controller which in turn
+        # will raise a 401/Unauthorized
+        w(u'&#160;[<a class="logout" href="%s">%s</a>]'
+          % (self._cw.build_url('login'), self._cw._('login')))
 
 class UserLink(_UserLink):
     __select__ = _UserLink.__select__ & authenticated_user()
--- a/web/views/basetemplates.py	Mon Oct 04 18:55:57 2010 +0200
+++ b/web/views/basetemplates.py	Mon Oct 04 18:56:05 2010 +0200
@@ -276,11 +276,12 @@
         logo = self._cw.vreg['components'].select_or_none('logo', self._cw,
                                                           rset=self.cw_rset)
         if logo and logo.cw_propval('visible'):
-            self.w(u'<table id="header"><tr>\n')
-            self.w(u'<td>')
-            logo.render(w=self.w)
-            self.w(u'</td>\n')
-            self.w(u'</tr></table>\n')
+            w = self.w
+            w(u'<table id="header"><tr>\n')
+            w(u'<td>')
+            logo.render(w=w)
+            w(u'</td>\n')
+            w(u'</tr></table>\n')
 
 
 # page parts templates ########################################################
@@ -335,35 +336,31 @@
 
     def main_header(self, view):
         """build the top menu with authentification info and the rql box"""
-        self.w(u'<table id="header"><tr>\n')
-        self.w(u'<td id="firstcolumn">')
+        w = self.w
+        w(u'<table id="header"><tr>\n')
+        w(u'<td id="firstcolumn">')
         logo = self._cw.vreg['components'].select_or_none(
             'logo', self._cw, rset=self.cw_rset)
         if logo and logo.cw_propval('visible'):
-            logo.render(w=self.w)
-        self.w(u'</td>\n')
+            logo.render(w=w)
+        w(u'</td>\n')
         # appliname and breadcrumbs
-        self.w(u'<td id="headtext">')
+        w(u'<td id="headtext">')
         for cid in self.main_cell_components:
             comp = self._cw.vreg['components'].select_or_none(
                 cid, self._cw, rset=self.cw_rset)
             if comp and comp.cw_propval('visible'):
                 comp.render(w=self.w)
-        self.w(u'</td>')
+        w(u'</td>')
         # logged user and help
-        self.w(u'<td>\n')
-        comp = self._cw.vreg['components'].select_or_none(
+        login_components = self._cw.vreg['components'].selectable(
             'loggeduserlink', self._cw, rset=self.cw_rset)
-        if comp and comp.cw_propval('visible'):
-            comp.render(w=self.w)
-        self.w(u'</td>')
-        # lastcolumn
-        self.w(u'<td id="lastcolumn">')
-        self.w(u'</td>\n')
-        self.w(u'</tr></table>\n')
-        if self._cw.session.anonymous_session:
-            self.wview('logform', rset=self.cw_rset, id='popupLoginBox',
-                       klass='hidden', title=False, showmessage=False)
+        for comp in login_components:
+            w(u'<td>\n')
+            if comp.cw_propval('visible'):
+                comp.render(w=w)
+            w(u'</td>')
+        w(u'</tr></table>\n')
 
     def state_header(self):
         state = self._cw.search_state
@@ -380,7 +377,6 @@
         return self.w(u'<div class="stateMessage">%s</div>' % msg)
 
 
-
 class HTMLPageFooter(View):
     """default html page footer: include footer actions
     """
@@ -438,12 +434,16 @@
     __regid__ = 'logform'
     domid = 'loginForm'
     needs_css = ('cubicweb.login.css',)
+    onclick = "javascript: cw.htmlhelpers.popupLoginBox('%s', '%s');"
     # XXX have to recall fields name since python is mangling __login/__password
     __login = ff.StringField('__login', widget=fw.TextInput({'class': 'data'}))
     __password = ff.StringField('__password', label=_('password'),
                                 widget=fw.PasswordSingleInput({'class': 'data'}))
     form_buttons = [fw.SubmitButton(label=_('log in'),
-                                    attrs={'class': 'loginButton'})]
+                                    attrs={'class': 'loginButton'}),
+                    fw.ResetButton(label=_('cancel'),
+                                   attrs={'class': 'loginButton',
+                                          'onclick': onclick % ('popupLoginBox', '__login')}),]
 
     def form_action(self):
         if self.action is None:
@@ -452,32 +452,35 @@
 
 
 class LogFormView(View):
+    # XXX an awfull lot of hardcoded assumptions there
+    #     makes it unobvious to reuse/specialize
     __regid__ = 'logform'
     __select__ = match_kwargs('id', 'klass')
 
     title = 'log in'
 
     def call(self, id, klass, title=True, showmessage=True):
-        self.w(u'<div id="%s" class="%s">' % (id, klass))
+        w = self.w
+        w(u'<div id="%s" class="popupLoginBox %s">' % (id, klass))
         if title:
             stitle = self._cw.property_value('ui.site-title')
             if stitle:
                 stitle = xml_escape(stitle)
             else:
                 stitle = u'&#160;'
-            self.w(u'<div id="loginTitle">%s</div>' % stitle)
-        self.w(u'<div id="loginContent">\n')
+            w(u'<div class="loginTitle">%s</div>' % stitle)
+        w(u'<div class="loginContent">\n')
         if showmessage and self._cw.message:
-            self.w(u'<div class="loginMessage">%s</div>\n' % self._cw.message)
+            w(u'<div class="loginMessage">%s</div>\n' % self._cw.message)
         config = self._cw.vreg.config
         if config['auth-mode'] != 'http':
             self.login_form(id) # Cookie authentication
-        self.w(u'</div>')
+        w(u'</div>')
         if self._cw.https and config.anonymous_user()[0]:
             path = xml_escape(config['base-url'] + self._cw.relative_path())
-            self.w(u'<div class="loginMessage"><a href="%s">%s</a></div>\n'
-                   % (path, self._cw._('No account? Try public access at %s') % path))
-        self.w(u'</div>\n')
+            w(u'<div class="loginMessage"><a href="%s">%s</a></div>\n'
+              % (path, self._cw._('No account? Try public access at %s') % path))
+        w(u'</div>\n')
 
     def login_form(self, id):
         cw = self._cw