Commits

Anonymous committed c047576

Added OpenID admin section.

Comments (0)

Files changed (8)

solace/_openid_auth.py

 from solace.templating import render_template
 from solace.database import get_engine, session
 from solace.schema import openid_association, openid_user_nonces
-from solace.models import _OpenIDUserMapping, User
+from solace.models import User
 from solace.forms import OpenIDLoginForm, OpenIDRegistrationForm
 from solace.auth import AuthSystemBase, LoginUnsucessful
 
         form = OpenIDRegistrationForm()
         if request.method == 'POST' and form.validate():
             user = User(form['username'], form['email'], '!')
-            _OpenIDUserMapping(user, identity_url)
+            user.openid_logins.add(identity_url)
             self.after_register(request, user)
             session.commit()
             del request.session['openid']
             raise LoginUnsucessful(_(u'OpenID authentication error'))
 
     def create_or_login(self, request, identity_url):
-        q = _OpenIDUserMapping.query.filter_by(identity_url=identity_url)
-        um = q.first()
+        user = User.query.by_openid_login(identity_url).first()
         # we don't have a user for this openid yet.  What we want to do
         # now is to remember the openid in the session until we have the
         # user.  We're using the session because it is signed.
-        if um is None:
+        if user is None:
             request.session['openid'] = identity_url
             return redirect(url_for('core.login', firstlogin='yes',
                                     next=request.next_url))
 
-        self.set_user_checked(request, um.user)
+        self.set_user_checked(request, user)
         return self.redirect_back(request)
 
     def set_user_checked(self, request, user):
             raise LoginUnsucessful(_(u'The user is not yet activated.'))
         if user.is_banned:
             raise LoginUnsucessful(_(u'The user got banned from the system.'))
-        self.set_user(request, um.user)
+        self.set_user(request, user)
 
     def perform_login(self, request, identity_url):
         try:
     def get_login_form(self):
         return OpenIDLoginForm()
 
-    def render_login_template(self, form):
+    def render_login_template(self, request, form):
         return render_template('core/login_openid.html', form=form.as_widget())
 
 from solace import settings
 from solace.i18n import lazy_gettext
-from solace.application import url_for
 from solace.utils.support import UIException
 from solace.utils.mail import send_email
 
         _auth_system = None
 
 
+def check_used_openids(identity_urls, ignored_owner=None):
+    """Returns a set of all the identity URLs from the list of identity
+    URLs that are already associated on the system.  If a owner is given,
+    items that are owned by the given user will not show up in the result
+    list.
+    """
+    query = _OpenIDUserMapping.query.filter(
+        _OpenIDUserMapping.identity_url.in_(identity_urls)
+    )
+    if ignored_owner:
+        query = query.filter(_OpenIDUserMapping.user != ignored_owner)
+    return set([x.identity_url for x in query.all()])
+
+
 class LoginUnsucessful(UIException):
     """Raised if the login failed."""
 
     password_managed_external = False
 
     #: set to True to indicate that this login system does not use
-    #: a password.  This will also affect the standard login form.
+    #: a password.  This will also affect the standard login form
+    #: and the standard profile form.
     passwordless = False
 
     #: if you don't want to see a register link in the user interface
                 request.flash(_(u'You are now logged in.'))
                 return form.redirect('kb.overview')
 
-        return self.render_login_template(form)
+        return self.render_login_template(request, form)
 
     def perform_login(self, request, **form_data):
         """If `login` is not overridden, this is called with the submitted
         """
         raise NotImplementedError()
 
-    def render_login_template(self, form):
+    def render_login_template(self, request, form):
         """Renders the login template"""
         return render_template('core/login.html', form=form.as_widget())
 
+    def get_edit_profile_form(self, user):
+        """Returns the profile form to be used by the auth system."""
+        return StandardProfileEditForm(user)
+
+    def edit_profile(self, request):
+        """Invoked like a view and does the profile handling."""
+        form = self.get_edit_profile_form(request.user)
+
+        if request.method == 'POST' and form.validate():
+            request.flash(_(u'Your profile was updated'))
+            form.apply_changes()
+            session.commit()
+            return form.redirect(form.user)
+
+        return self.render_edit_profile_template(request, form)
+
+    def render_edit_profile_template(self, request, form):
+        """Renders the template for the profile edit page."""
+        return render_template('users/edit_profile.html',
+                               form=form.as_widget())
+
     def logout(self, request):
         """This has to logout the user again.  This method must not fail.
         If the logout requires the redirect to an external resource it
 try:
     from solace._openid_auth import OpenIDAuth
 except ImportError:
+    raise
     class OpenIDAuth(AuthSystemBase):
         def __init__(self):
             raise RuntimeError('python-openid library not installed but '
 
 
 # circular dependencies
-from solace.models import User
+from solace.application import url_for
+from solace.models import User, _OpenIDUserMapping
 from solace.database import session
 from solace.i18n import _
-from solace.forms import StandardLoginForm, RegistrationForm
+from solace.forms import StandardLoginForm, RegistrationForm, \
+     StandardProfileEditForm
 from solace.templating import render_template
     :copyright: (c) 2009 by Plurk Inc., see AUTHORS for more details.
     :license: BSD, see LICENSE for more details.
 """
+import urlparse
 from solace import settings
 from solace.utils import forms
-from solace.i18n import lazy_gettext, _
+from solace.i18n import lazy_gettext, _, ngettext
 from solace.models import Topic, Post, Comment, User
 
 
                                       u'end with a dot.'))
 
 
+def is_http_url(form, value):
+    """Checks if the value is a HTTP URL."""
+    scheme, netloc = urlparse.urlparse(value)[:2]
+    if scheme not in ('http', 'https') or not netloc:
+        raise forms.ValidationError(_(u'A valid HTTP URL is required.'))
+
+
 class StandardLoginForm(forms.Form):
     """Used to log in users."""
     username = forms.TextField(lazy_gettext(u'Username'), required=True)
 
 class ProfileEditForm(forms.Form):
     """Used to change profile details."""
-    password = forms.TextField(lazy_gettext(u'Password'),
-                               widget=forms.PasswordInput)
-    password_repeat = forms.TextField(lazy_gettext(u'Password (repeat)'),
-                                      widget=forms.PasswordInput)
+    real_name = forms.TextField(lazy_gettext(u'Real name'))
     email = forms.TextField(lazy_gettext(u'E-Mail'), required=True,
                             validators=[is_valid_email])
-    real_name = forms.TextField(lazy_gettext(u'Real name'))
 
     def __init__(self, user, initial=None, action=None, request=None):
         self.user = user
         self.auth_system = get_auth_system()
         if user is not None:
             initial = forms.fill_dict(initial, real_name=user.real_name)
-            if self.auth_system.email_managed_external:
+            if not self.auth_system.email_managed_external:
                 initial['email'] = user.email
         forms.Form.__init__(self, initial, action, request)
+        if self.auth_system.email_managed_external:
+            del self.fields['email']
+
+    def apply_changes(self):
+        if 'email' in self.data:
+            self.user.email = self.data['email']
+        self.user.real_name = self.data['real_name']
+
+
+class StandardProfileEditForm(ProfileEditForm):
+    """Used to change profile details for the basic auth systems."""
+    password = forms.TextField(lazy_gettext(u'Password'),
+                               widget=forms.PasswordInput)
+    password_repeat = forms.TextField(lazy_gettext(u'Password (repeat)'),
+                                      widget=forms.PasswordInput)
+
+    def __init__(self, user, initial=None, action=None, request=None):
+        ProfileEditForm.__init__(self, user, initial, action, request)
         if self.auth_system.passwordless or \
            self.auth_system.password_managed_external:
             del self.fields['password']
             del self.fields['password_repeat']
-        if self.auth_system.email_managed_external:
-            del self.fields['email']
 
     def context_validate(self, data):
         password = data.get('password')
             raise forms.ValidationError(_(u'The two passwords do not match.'))
 
     def apply_changes(self):
-        if 'email' in self.data:
-            self.user.email = self.data['email']
+        super(StandardProfileEditForm, self).apply_changes()
         password = self.data.get('password')
         if password:
             self.user.set_password(password)
-        self.user.real_name = self.data['real_name']
 
 
 class QuestionForm(forms.Form):
     username = forms.TextField(lazy_gettext(u'Username'))
     is_admin = forms.BooleanField(lazy_gettext(u'Administrator'),
         help_text=lazy_gettext(u'Enable if this user is an admin.'))
+    openid_logins = forms.LineSeparated(forms.TextField(validators=[is_http_url]),
+                                        lazy_gettext(u'Associated OpenID Identities'))
 
     def __init__(self, user, initial=None, action=None, request=None):
         if user is not None:
             initial = forms.fill_dict(initial, username=user.username,
-                                      is_admin=user.is_admin)
+                                      is_admin=user.is_admin,
+                                      openid_logins=sorted(user.openid_logins))
         ProfileEditForm.__init__(self, user, initial, action, request)
 
     def validate_is_admin(self, value):
             raise forms.ValidationError(u'You cannot remove your own '
                                         u'admin rights.')
 
+    def validate_openid_logins(self, value):
+        ids_to_check = set(value) - set(self.user.openid_logins)
+        in_use = check_used_openids(ids_to_check, self.user)
+        if in_use:
+            count = len(in_use)
+            message = ngettext(u'The following %(count)d URL is already '
+                               u'associated to a different user: %(urls)s',
+                               u'The following %(count)d URLs are already '
+                               u'associated to different users: %(urls)s',
+                               count) % dict(count=count,
+                                             urls=u', '.join(sorted(in_use)))
+            raise forms.ValidationError(message)
+
     def apply_changes(self):
         super(EditUserForm, self).apply_changes()
         self.user.username = self.data['username']
         self.user.is_admin = self.data['is_admin']
+        self.user.bind_openid_logins(self.data['openid_logins'])
 
 
-from solace.auth import get_auth_system
+from solace.auth import get_auth_system, check_used_openids
 class UserQuery(Query):
     """Adds extra query methods for users."""
 
+    def by_openid_login(self, identity_url):
+        """Filters by open id identity URL."""
+        ss = select([openid_user_mapping.c.user_id],
+                    openid_user_mapping.c.identity_url == identity_url)
+        return self.filter(User.id.in_(ss))
+
     def active_in(self, locale):
         """Only return users that are active in the given locale."""
         ua = user_activities.c
         session.add(self)
 
     badges = association_proxy('_badges', 'badge')
+    openid_logins = association_proxy('_openid_logins', 'identity_url')
+
+    def bind_openid_logins(self, logins):
+        """Rebinds the openid logins."""
+        currently_attached = set(self.openid_logins)
+        new_logins = set(logins)
+        self.openid_logins.difference_update(
+            currently_attached.difference(new_logins))
+        self.openid_logins.update(
+            new_logins.difference(currently_attached))
 
     def _get_active(self):
         return self.activation_key is None
     """Internal helper for the openid auth system."""
     query = session.query_property()
 
-    def __init__(self, user, identity_url):
-        self.user = user
+    def __init__(self, identity_url):
         self.identity_url = identity_url
         session.add(self)
 
     post=relation(Post)
 ), primary_key=[votes.c.user_id, votes.c.post_id])
 mapper(_OpenIDUserMapping, openid_user_mapping, properties=dict(
-    user=relation(User, lazy=False)
+    user=relation(User, lazy=False, backref=backref('_openid_logins', lazy=True,
+                                                    collection_class=set))
 ))
 mapper(PostRevision, post_revisions, properties=dict(
     id=post_revisions.c.revision_id,

solace/settings.py

     """
     import re
     from pprint import pformat
+    from os.path import join, dirname
     assignment_re = re.compile(r'\s*([A-Z_][A-Z0-9_]*)\s*=')
 
     # use items() here instead of iteritems so that if a different
     items = dict((k, (pformat(v).decode('utf-8', 'replace'), u''))
                  for (k, v) in globals().items() if k.isupper())
 
-    with open(__file__.strip('c')) as f:
+    with open(join(dirname(__file__), 'default_settings.cfg')) as f:
         comment_buf = []
         for line in f:
             line = line.rstrip().decode('utf-8')

solace/utils/forms.py

             return ErrorList(chain(*(item[1] for item in items)))
         result = ErrorList()
         for key, value in items:
-            if key == self.name or key.startswith(self.name + '.'):
+            if key == self.name or (key is not None and
+                                    key.startswith(self.name + '.')):
                 result.extend(value)
         return result
 
+    @property
+    def default_display_errors(self):
+        """The errors that should be displayed."""
+        return self.errors
+
     def as_dd(self, **attrs):
         """Return a dt/dd item."""
         rv = []
 
     def __call__(self, **attrs):
         """The default display is the form + error list as ul if needed."""
-        return self.render(**attrs) + self.errors()
+        return self.render(**attrs) + self.default_display_errors()
 
 
 class Label(_Renderable):
 class Textarea(Widget):
     """Displays a textarea."""
 
+    @property
+    def default_display_errors(self):
+        """A textarea is often used with multiple, it makes sense to
+        display the errors of all childwidgets then which are not
+        renderable because they are text.
+        """
+        return self.all_errors
+
     def _attr_setdefault(self, attrs):
         Widget._attr_setdefault(self, attrs)
         attrs.setdefault('rows', 8)
             rv.append(u' ' + self.label())
         if self.help_text:
             rv.append(html.div(self.help_text, class_='explanation'))
-        rv.append(self.errors())
+        rv.append(self.default_display_errors())
         return html.li(u''.join(rv))
 
     def render(self, **attrs):
             body = '<div style="display: none">%s</div>%s' % (hidden, body)
 
         if with_errors:
-            body = self.errors() + body
+            body = self.default_display_errors() + body
         return html.form(body, action=self._field.form.action,
                          method=method, **attrs)
 
             return u''
         items = []
         for index in xrange(len(self) + attrs.pop('extra_rows', 1)):
-            items.append(html.li(self[index]()) for item in self)
-        # add an invisible item for the validator
-        if not items:
-            items.append(html.li(style='display: none'))
+            items.append(html.li(self[index]()))
         return factory(*items, **attrs)
 
     def __getitem__(self, index):
         return (self.sep + u' ').join(map(self.field.to_primitive, value))
 
 
-class LineSeparated(CommaSeparated):
+class LineSeparated(Multiple):
     r"""Works like `CommaSeparated` but uses multiple lines:
 
     >>> field = LineSeparated(IntegerField())

solace/views/admin.py

         raise NotFound()
     form = EditUserForm(user)
     if request.method == 'POST' and form.validate():
+        form.apply_changes()
         request.flash(_(u'The user details where changed.'))
         session.commit()
         return form.redirect('admin.edit_users')
     if user.is_banned:
         request.flash(_(u'The user is already banned.'))
         return redirect(next)
+    if user == request.user:
+        request.flash(_(u'You cannot ban yourself.'), error=True)
+        return redirect(next)
     admin_utils.ban_user(user)
     request.flash(_(u'The user “%s” was successfully banned and notified.') %
                   user.username)

solace/views/users.py

 
 from solace import settings
 from solace.application import url_for, require_login
+from solace.auth import get_auth_system
 from solace.database import session
 from solace.models import User, Topic, Post
 from solace.templating import render_template
 from solace.utils.pagination import Pagination
-from solace.forms import ProfileEditForm
 from solace.i18n import list_sections, _
 
 
 @require_login
 def edit_profile(request):
     """Allows the user to change profile information."""
-    form = ProfileEditForm(request.user)
-
-    if request.method == 'POST' and form.validate():
-        request.flash(_(u'Your profile was updated'))
-        form.apply_changes()
-        session.commit()
-        return form.redirect(form.user)
-
-    return render_template('users/edit_profile.html', form=form.as_widget())
+    return get_auth_system().edit_profile(request)