diff --git a/base/accounts.py b/base/accounts.py index e6d96fcb76..a6910cf958 100644 --- a/base/accounts.py +++ b/base/accounts.py @@ -1,6 +1,21 @@ from django.conf import settings from django.contrib.auth.models import User +from django.contrib.auth import authenticate, login + +from django.contrib.sites.models import RequestSite +from django.contrib.sites.models import Site + +from registration import signals +from registration.forms import RegistrationForm +from registration.models import RegistrationProfile + +from django import forms + +from django.utils.translation import ugettext_lazy as _ + +from seahub.profile.models import UserProfile + class EmailOrUsernameModelBackend(object): def authenticate(self, username=None, password=None): if '@' in username: @@ -19,3 +34,197 @@ class EmailOrUsernameModelBackend(object): return User.objects.get(pk=user_id) except User.DoesNotExist: return None + + +class RegistrationBackend(object): + """ + A registration backend which follows a simple workflow: + + 1. User signs up, inactive account is created. + + 2. Email is sent to user with activation link. + + 3. User clicks activation link, account is now active. + + Using this backend requires that + + * ``registration`` be listed in the ``INSTALLED_APPS`` setting + (since this backend makes use of models defined in this + application). + + * The setting ``ACCOUNT_ACTIVATION_DAYS`` be supplied, specifying + (as an integer) the number of days from registration during + which a user may activate their account (after that period + expires, activation will be disallowed). + + * The creation of the templates + ``registration/activation_email_subject.txt`` and + ``registration/activation_email.txt``, which will be used for + the activation email. See the notes for this backends + ``register`` method for details regarding these templates. + + Additionally, registration can be temporarily closed by adding the + setting ``REGISTRATION_OPEN`` and setting it to + ``False``. Omitting this setting, or setting it to ``True``, will + be interpreted as meaning that registration is currently open and + permitted. + + Internally, this is accomplished via storing an activation key in + an instance of ``registration.models.RegistrationProfile``. See + that model and its custom manager for full documentation of its + fields and supported operations. + + """ + def register(self, request, **kwargs): + """ + Given a username, email address and password, register a new + user account, which will initially be inactive. + + Along with the new ``User`` object, a new + ``registration.models.RegistrationProfile`` will be created, + tied to that ``User``, containing the activation key which + will be used for this account. + + An email will be sent to the supplied email address; this + email should contain an activation link. The email will be + rendered using two templates. See the documentation for + ``RegistrationProfile.send_activation_email()`` for + information about these templates and the contexts provided to + them. + + After the ``User`` and ``RegistrationProfile`` are created and + the activation email is sent, the signal + ``registration.signals.user_registered`` will be sent, with + the new ``User`` as the keyword argument ``user`` and the + class of this backend as the sender. + + """ + email, password = kwargs['email'], kwargs['password1'] + username = email + if Site._meta.installed: + site = Site.objects.get_current() + else: + site = RequestSite(request) + new_user = RegistrationProfile.objects.create_inactive_user(username, email, + password, site) + + userid = kwargs['userid'] + profile = UserProfile(user=new_user, ccnet_user_id=userid) + profile.save() + + signals.user_registered.send(sender=self.__class__, + user=new_user, + request=request) + return new_user + + def activate(self, request, activation_key): + """ + Given an an activation key, look up and activate the user + account corresponding to that key (if possible). + + After successful activation, the signal + ``registration.signals.user_activated`` will be sent, with the + newly activated ``User`` as the keyword argument ``user`` and + the class of this backend as the sender. + + """ + activated = RegistrationProfile.objects.activate_user(activation_key) + if activated: + signals.user_activated.send(sender=self.__class__, + user=activated, + request=request) + # login the user + activated.backend='django.contrib.auth.backends.ModelBackend' + login(request, activated) + + return activated + + def registration_allowed(self, request): + """ + Indicate whether account registration is currently permitted, + based on the value of the setting ``REGISTRATION_OPEN``. This + is determined as follows: + + * If ``REGISTRATION_OPEN`` is not specified in settings, or is + set to ``True``, registration is permitted. + + * If ``REGISTRATION_OPEN`` is both specified and set to + ``False``, registration is not permitted. + + """ + return getattr(settings, 'REGISTRATION_OPEN', True) + + def get_form_class(self, request): + """ + Return the default form class used for user registration. + + """ + return RegistrationForm + + def post_registration_redirect(self, request, user): + """ + Return the name of the URL to redirect to after successful + user registration. + + """ + return ('registration_complete', (), {}) + + def post_activation_redirect(self, request, user): + """ + Return the name of the URL to redirect to after successful + account activation. + + """ + return ('myhome', (), {}) + + +class RegistrationForm(forms.Form): + """ + Form for registering a new user account. + + Validates that the requested email is not already in use, and + requires the password to be entered twice to catch typos. + """ + attrs_dict = { 'class': 'required' } + + email = forms.EmailField(widget=forms.TextInput(attrs=dict(attrs_dict, + maxlength=75)), + label=_("Email address")) + userid = forms.RegexField(regex=r'^\w+$', + max_length=40, + widget=forms.TextInput(attrs=attrs_dict), + label=_("Username"), + error_messages={ 'invalid': _("This value must be of length 40") }) + + password1 = forms.CharField(widget=forms.PasswordInput(attrs=attrs_dict, render_value=False), + label=_("Password")) + password2 = forms.CharField(widget=forms.PasswordInput(attrs=attrs_dict, render_value=False), + label=_("Password (again)")) + + def clean_email(self): + try: + user = User.objects.get(email__iexact=self.cleaned_data['email']) + except User.DoesNotExist: + return self.cleaned_data['email'] + + raise forms.ValidationError(_("A user with this email alread")) + + + def clean_userid(self): + if len(self.cleaned_data['userid']) != 40: + raise forms.ValidationError(_("Invalid user id.")) + + return self.cleaned_data['userid'] + + def clean(self): + """ + Verifiy that the values entered into the two password fields + match. Note that an error here will end up in + ``non_field_errors()`` because it doesn't apply to a single + field. + + """ + if 'password1' in self.cleaned_data and 'password2' in self.cleaned_data: + if self.cleaned_data['password1'] != self.cleaned_data['password2']: + raise forms.ValidationError(_("The two password fields didn't match.")) + return self.cleaned_data diff --git a/base/middleware.py b/base/middleware.py new file mode 100644 index 0000000000..78415d08cb --- /dev/null +++ b/base/middleware.py @@ -0,0 +1,19 @@ + +from seahub.profile.models import UserProfile + +class UseridMiddleware(object): + + def process_request(self, request): + if not request.user.is_authenticated(): + return None + + try: + profile = request.user.get_profile() + request.user.user_id = profile.ccnet_user_id + except UserProfile.DoesNotExist: + request.user.user_id = '' + + return None + + def process_response(self, request, response): + return response diff --git a/base/registration_urls.py b/base/registration_urls.py new file mode 100644 index 0000000000..1107a13cc2 --- /dev/null +++ b/base/registration_urls.py @@ -0,0 +1,37 @@ +from django.conf.urls.defaults import * +from django.views.generic.simple import direct_to_template + +from registration.views import activate +from registration.views import register + +from seahub.base.accounts import RegistrationForm + + +urlpatterns = patterns('', + url(r'^activate/complete/$', + direct_to_template, + { 'template': 'registration/activation_complete.html' }, + name='registration_activation_complete'), + # Activation keys get matched by \w+ instead of the more specific + # [a-fA-F0-9]{40} because a bad activation key should still get to the view; + # that way it can return a sensible "invalid key" message instead of a + # confusing 404. + url(r'^activate/(?P\w+)/$', + activate, + { 'backend': 'seahub.base.accounts.RegistrationBackend', }, + name='registration_activate'), + url(r'^register/$', + register, + { 'backend': 'seahub.base.accounts.RegistrationBackend', + 'form_class': RegistrationForm }, + name='registration_register'), + url(r'^register/complete/$', + direct_to_template, + { 'template': 'registration/registration_complete.html' }, + name='registration_complete'), + url(r'^register/closed/$', + direct_to_template, + { 'template': 'registration/registration_closed.html' }, + name='registration_disallowed'), + (r'', include('registration.auth_urls')), + ) diff --git a/media/css/seahub.css b/media/css/seahub.css index 6656f1b89b..a5f12c9456 100644 --- a/media/css/seahub.css +++ b/media/css/seahub.css @@ -39,6 +39,8 @@ table.default tr.first { background-color: #00FF00; } #header .top_operation a { font-weight:normal; font-size:13px; margin-left:2px; text-decoration: none; } #header .top_operation a:hover { background:#0A0; color:#FFF; } #logo { margin-top: 4px; } +#user-info { float: right; font-size: 12px; color: #808; font-style:normal; margin-bottom:10px; } +#user-info span { color: #888; display:inline-block; width:60px; text-align:right; margin-right:5px; } #header #ident { margin-top: 8px; float: right; font-size: 12px; } #header #ident p { color: #808; } #header #ident label { color: #888; } diff --git a/run-seahub.sh.template b/run-seahub.sh.template new file mode 100755 index 0000000000..ee0d89ecbb --- /dev/null +++ b/run-seahub.sh.template @@ -0,0 +1,6 @@ +#!/bin/bash + +export CCNET_CONF_DIR=/home/plt/dev/ccnet/seafile/tests/basic/conf2 +export PYTHONPATH=/opt/lib/python2.6/site-packages:thirdpart + +./manage.py runserver diff --git a/settings.py b/settings.py index 1fbbb8ab4d..a22fc29e78 100644 --- a/settings.py +++ b/settings.py @@ -66,6 +66,7 @@ MIDDLEWARE_CLASSES = ( 'django.middleware.csrf.CsrfResponseMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'seahub.base.middleware.UseridMiddleware', ) SITE_ROOT_URLCONF = 'seahub.urls' diff --git a/templates/base.html b/templates/base.html index c6370ef9c8..c740b25945 100644 --- a/templates/base.html +++ b/templates/base.html @@ -38,6 +38,11 @@