Commit 5ed6816e authored by Sofia Papagiannaki's avatar Sofia Papagiannaki
Browse files

- use email to login & authenticate users

- fix user profile editing bug
- fix user activation issue
- consume invitation upon invited user creation
- fix invited user invitations
- add token info in test user fixture
- minor bug fixes

Refs: #1917
Refs: #1911
Refs: #1919
parent 0321ca9b
......@@ -2,7 +2,7 @@
Αγαπητέ/η {{ user.realname }},
Ο λογαρισμός σας για την υπηρεσία {{ site_name }} της ΕΔΕΤκατά την Alpha (πιλοτική)
Ο λογαρισμός σας για την υπηρεσία {{ site_name }} της ΕΔΕΤ κατά την Alpha (πιλοτική)
φάση λειτουργίας της έχει ενεργοποιηθεί.
Για να συνδεθείτε, χρησιμοποιήστε τον παρακάτω σύνδεσμο:
......
......@@ -223,7 +223,8 @@ def users_modify(request, user_id, template_name='users_info.html', extra_contex
If the ``request.user`` is not a superuser redirects to login page.
"""
form = AdminProfileForm(request.POST)
user = AstakosUser.objects.get(id = user_id)
form = AdminProfileForm(request.POST, instance=user)
if form.is_valid():
form.save()
return redirect(users_info, user.id, template_name, extra_context)
......@@ -307,13 +308,12 @@ def pending_users(request, template_name='pending_users.html', extra_context={})
context_instance = get_context(request, extra_context,**kwargs))
def _send_greeting(request, user, template_name):
url = reverse('astakos.im.views.index')
subject = _('Welcome to %s' %settings.SERVICE_NAME)
site = Site.objects.get_current()
baseurl = request.build_absolute_uri('/').rstrip('/')
message = render_to_string(template_name, {
'user': user,
'url': url,
'url': site.domain,
'baseurl': baseurl,
'site_name': site.name,
'support': settings.DEFAULT_CONTACT_EMAIL})
......
......@@ -83,6 +83,7 @@ def authenticate(request):
response = HttpResponse()
response.status=204
user_info = {'uniq':user.username,
'email':user.email,
'auth_token':user.auth_token,
'auth_token_created':user.auth_token_created.isoformat(),
'auth_token_expires':user.auth_token_expires.isoformat()}
......
from django.conf import settings
from django.contrib.auth.backends import ModelBackend
#from django.core.exceptions import ImproperlyConfigured
#from django.db.models import get_model
from django.core.validators import email_re
from astakos.im.models import AstakosUser
class AstakosUserModelCredentialsBackend(ModelBackend):
class TokenBackend(ModelBackend):
"""
AuthenticationBackend used to authenticate user creadentials
AuthenticationBackend used to authenticate using token instead
"""
def authenticate(self, username=None, password=None):
def authenticate(self, email=None, auth_token=None):
try:
user = AstakosUser.objects.get(username=username)
if user.check_password(password):
user = AstakosUser.objects.get(email=email)
if user.auth_token == auth_token:
return user
except AstakosUser.DoesNotExist:
return None
......@@ -23,27 +22,29 @@ class AstakosUserModelCredentialsBackend(ModelBackend):
except AstakosUser.DoesNotExist:
return None
#@property
#def user_class(self):
# if not hasattr(self, '_user_class'):
# #self._user_class = get_model(*settings.CUSTOM_USER_MODEL.split('.', 2))
# self._user_class = get_model('astakos.im', 'astakosuser')
# print '#', self._user_class
# if not self._user_class:
# raise ImproperlyConfigured('Could not get custom user model')
# return self._user_class
class AstakosUserModelTokenBackend(ModelBackend):
class EmailBackend(ModelBackend):
"""
AuthenticationBackend used to authenticate using token instead
If the ``username`` parameter is actually an email uses email to authenticate
the user else tries the username.
Used from ``astakos.im.forms.LoginForm`` to authenticate.
"""
def authenticate(self, username=None, auth_token=None):
def authenticate(self, username=None, password=None):
#If username is an email address, then try to pull it up
if email_re.search(username):
try:
user = AstakosUser.objects.get(email=username)
except AstakosUser.DoesNotExist:
return None
else:
#We have a non-email address username we
#should try username
try:
user = AstakosUser.objects.get(username=username)
if user.auth_token == auth_token:
return user
except AstakosUser.DoesNotExist:
return None
if user.check_password(password):
return user
def get_user(self, user_id):
try:
......
......@@ -40,6 +40,7 @@ from django.utils.translation import ugettext as _
from django.contrib.auth.forms import UserCreationForm
from django.contrib.sites.models import Site
from django.contrib import messages
from django.shortcuts import redirect
from smtplib import SMTPException
from urllib import quote
......@@ -48,7 +49,10 @@ from astakos.im.util import get_or_create_user
from astakos.im.models import AstakosUser, Invitation
from astakos.im.forms import ExtendedUserCreationForm, InvitedExtendedUserCreationForm
def get_backend():
import socket
import logging
def get_backend(request):
"""
Return an instance of a registration backend,
according to the INVITATIONS_ENABLED setting
......@@ -69,7 +73,7 @@ def get_backend():
backend_class = getattr(mod, backend_class_name)
except AttributeError:
raise ImproperlyConfigured('Module "%s" does not define a registration backend named "%s"' % (module, attr))
return backend_class()
return backend_class(request)
class InvitationsBackend(object):
"""
......@@ -79,26 +83,38 @@ class InvitationsBackend(object):
account is created and the user is going to receive an email as soon as an
administrator activates his/her account.
"""
def get_signup_form(self, request):
def __init__(self, request):
self.request = request
self.invitation = None
self.set_invitation()
def set_invitation(self):
code = self.request.GET.get('code', '')
if not code:
code = self.request.POST.get('code', '')
if code:
self.invitation = Invitation.objects.get(code=code)
if self.invitation.is_consumed:
raise Exception('Invitation has beeen used')
def get_signup_form(self):
"""
Returns the necassary registration form depending the user is invited or not
Throws Invitation.DoesNotExist in case ``code`` is not valid.
"""
code = request.GET.get('code', '')
request = self.request
formclass = 'ExtendedUserCreationForm'
initial_data = None
if request.method == 'GET':
if code:
if self.invitation:
formclass = 'Invited%s' %formclass
self.invitation = Invitation.objects.get(code=code)
if self.invitation.is_consumed:
raise Exception('Invitation has beeen used')
initial_data = {'username':self.invitation.username,
'email':self.invitation.username,
'realname':self.invitation.realname}
inviter = AstakosUser.objects.get(username=self.invitation.inviter)
initial_data['inviter'] = inviter.realname
initial_data['level'] = inviter.level + 1
else:
initial_data = request.POST
return globals()[formclass](initial_data)
......@@ -110,32 +126,28 @@ class InvitationsBackend(object):
It should be called after ``get_signup_form`` which sets invitation if exists.
"""
invitation = getattr(self, 'invitation') if hasattr(self, 'invitation') else None
invitation = self.invitation
if not invitation:
return False
if invitation.username == user.username and not invitation.is_consumed:
if invitation.username == user.email and not invitation.is_consumed:
invitation.consume()
return True
return False
def signup(self, request):
def signup(self, form):
"""
Creates a incative user account. If the user is preaccepted (has a valid
invitation code) the user is activated and if the request param ``next``
is present redirects to it.
Initially creates an inactive user account. If the user is preaccepted
(has a valid invitation code) the user is activated and if the request
param ``next`` is present redirects to it.
In any other case the method returns the action status and a message.
"""
kwargs = {}
form = self.get_signup_form(request)
user = form.save()
try:
if self._is_preaccepted(user):
user.is_active = True
user.save()
message = _('Registration completed. You can now login.')
next = request.POST.get('next')
if next:
return redirect(next)
else:
message = _('Registration completed. You will receive an email upon your account\'s activation')
status = messages.SUCCESS
......@@ -150,14 +162,18 @@ class SimpleBackend(object):
supplies the necessary registation information, an incative user account is
created and receives an email in order to activate his/her account.
"""
def get_signup_form(self, request):
def __init__(self, request):
self.request = request
def get_signup_form(self):
"""
Returns the UserCreationForm
"""
request = self.request
initial_data = request.POST if request.method == 'POST' else None
return UserCreationForm(initial_data)
return ExtendedUserCreationForm(initial_data)
def signup(self, request, email_template_name='activation_email.txt'):
def signup(self, form, email_template_name='activation_email.txt'):
"""
Creates an inactive user account and sends a verification email.
......@@ -178,12 +194,11 @@ class SimpleBackend(object):
* DEFAULT_FROM_EMAIL: from email
"""
kwargs = {}
form = self.get_signup_form(request)
user = form.save()
status = messages.SUCCESS
try:
_send_verification(request, user, email_template_name)
_send_verification(self.request, user, email_template_name)
message = _('Verification sent to %s' % user.email)
except (SMTPException, socket.error) as e:
status = messages.ERROR
......@@ -191,7 +206,7 @@ class SimpleBackend(object):
message = getattr(e, name) if hasattr(e, name) else e
return status, message
def _send_verification(request, user, template_name):
def _send_verification(request, user, template_name):
site = Site.objects.get_current()
baseurl = request.build_absolute_uri('/').rstrip('/')
url = settings.ACTIVATION_LOGIN_TARGET % (baseurl,
......
......@@ -14,7 +14,10 @@
"fields": {
"level": 1,
"invitations": 2,
"updated": "2012-01-24"
"auth_token": "0000",
"auth_token_created": "2011-04-07 09:17:14",
"auth_token_expires": "2015-04-07 09:17:14",
"updated": "2011-02-06"
}
}
]
......@@ -33,14 +33,17 @@
from django import forms
from django.utils.translation import ugettext as _
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
from django.conf import settings
from django.core.validators import email_re
from hashlib import new as newhasher
from astakos.im.models import AstakosUser
from astakos.im.util import get_or_create_user
import logging
import uuid
class UniqueUserEmailField(forms.EmailField):
"""
......@@ -61,18 +64,14 @@ class ExtendedUserCreationForm(UserCreationForm):
Extends the built in UserCreationForm in several ways:
* Adds an email field, which uses the custom UniqueUserEmailField.
* The username field isn't visible and it is assigned the email value.
* The username field isn't visible and it is assigned a generated id.
* first_name and last_name fields are added.
* User is created not active.
"""
username = forms.CharField(required = False, max_length = 30)
email = UniqueUserEmailField(required = True, label = 'Email address')
first_name = forms.CharField(required = False, max_length = 30)
last_name = forms.CharField(required = False, max_length = 30)
class Meta:
model = AstakosUser
fields = ("username",)
fields = ("email", "first_name", "last_name")
def __init__(self, *args, **kwargs):
"""
......@@ -82,76 +81,46 @@ class ExtendedUserCreationForm(UserCreationForm):
self.fields.keyOrder = ['email', 'first_name', 'last_name',
'password1', 'password2']
def clean(self, *args, **kwargs):
"""
Normal cleanup + username generation.
"""
cleaned_data = super(ExtendedUserCreationForm, self).clean(*args, **kwargs)
if cleaned_data.has_key('email'):
cleaned_data['username'] = cleaned_data['email']
return cleaned_data
def save(self, commit=True):
"""
Saves the email, first_name and last_name properties, after the normal
save behavior is complete.
"""
user = super(ExtendedUserCreationForm, self).save(commit=False)
user.username = uuid.uuid4().hex[:30]
user.is_active = False
user.renew_token()
user.save()
logging.info('Created user %s', user)
return user
class InvitedExtendedUserCreationForm(UserCreationForm):
class InvitedExtendedUserCreationForm(ExtendedUserCreationForm):
"""
Extends the built in UserCreationForm in several ways:
* Adds an email field, which uses the custom UniqueUserEmailField.
* The username field isn't visible and it is assigned the email value.
* first_name and last_name fields are added.
Extends the ExtendedUserCreationForm: adds an inviter readonly field.
"""
username = forms.CharField(required = False, max_length = 30)
email = UniqueUserEmailField(required = True, label = 'Email address')
first_name = forms.CharField(required = False, max_length = 30)
last_name = forms.CharField(required = False, max_length = 30)
inviter = forms.CharField(widget=forms.TextInput(), label=_('Inviter Real Name'))
level = forms.IntegerField(widget=forms.TextInput(), label=_('Level'))
class Meta:
model = AstakosUser
fields = ("username",)
fields = ("email", "first_name", "last_name")
def __init__(self, *args, **kwargs):
"""
Changes the order of fields, and removes the username field.
"""
super(InvitedExtendedUserCreationForm, self).__init__(*args, **kwargs)
self.fields.keyOrder = ['email', 'inviter', 'first_name', 'last_name',
'password1', 'password2']
self.fields.keyOrder = ['email', 'inviter', 'level', 'first_name',
'last_name', 'password1', 'password2']
#set readonly form fields
self.fields['inviter'].widget.attrs['readonly'] = True
self.fields['level'].widget.attrs['readonly'] = True
self.fields['email'].widget.attrs['readonly'] = True
self.fields['username'].widget.attrs['readonly'] = True
def clean(self, *args, **kwargs):
"""
Normal cleanup + username generation.
"""
cleaned_data = super(UserCreationForm, self).clean(*args, **kwargs)
if cleaned_data.has_key('email'):
cleaned_data['username'] = cleaned_data['email']
return cleaned_data
def save(self, commit=True):
"""
Saves the email, first_name and last_name properties, after the normal
save behavior is complete.
"""
user = super(InvitedExtendedUserCreationForm, self).save(commit=False)
user.renew_token()
user.save()
logging.info('Created user %s', user)
return user
class LoginForm(AuthenticationForm):
username = forms.EmailField(label=_("Email"))
class ProfileForm(forms.ModelForm):
"""
......@@ -163,15 +132,14 @@ class ProfileForm(forms.ModelForm):
"""
class Meta:
model = AstakosUser
exclude = ('groups', 'user_permissions')
exclude = ('is_active', 'is_superuser', 'is_staff', 'is_verified', 'groups', 'user_permissions')
def __init__(self, *args, **kwargs):
super(ProfileForm, self).__init__(*args, **kwargs)
instance = getattr(self, 'instance', None)
ro_fields = ('username','date_joined', 'updated', 'auth_token',
'auth_token_created', 'auth_token_expires', 'invitations',
'level', 'last_login', 'email', 'is_active', 'is_superuser',
'is_staff')
'level', 'last_login', 'email', )
if instance and instance.id:
for field in ro_fields:
if isinstance(self.fields[field].widget, forms.CheckboxInput):
......@@ -195,3 +163,12 @@ class FeedbackForm(forms.Form):
feedback_data = forms.CharField(widget=forms.Textarea(),
label=u'Data', required=False)
class SendInvitationForm(forms.Form):
"""
Form for sending an invitations
"""
email = forms.EmailField(required = True, label = 'Email address')
first_name = forms.EmailField(label = 'First name')
last_name = forms.EmailField(label = 'Last name')
......@@ -41,6 +41,7 @@ from base64 import b64encode
from django.conf import settings
from django.db import models
from django.contrib.auth.models import User, UserManager
from django.utils.translation import ugettext_lazy as _
from astakos.im.interface import get_quota, set_quota
......@@ -51,8 +52,8 @@ class AstakosUser(User):
# Use UserManager to get the create_user method, etc.
objects = UserManager()
affiliation = models.CharField('Affiliation', max_length=255, default='')
provider = models.CharField('Provider', max_length=255, default='')
affiliation = models.CharField('Affiliation', max_length=255, blank=True)
provider = models.CharField('Provider', max_length=255, blank=True)
#for invitations
level = models.IntegerField('Inviter level', default=4)
......
......@@ -62,7 +62,7 @@ def login(request):
invitation.inviter.level + 1)
# in order to login the user we must call authenticate first
authenticate(username=user.username, auth_token=user.auth_token)
authenticate(email=user.email, auth_token=user.auth_token)
next = request.GET.get('next')
return prepare_response(request, user, next, 'renew' in request.GET)
......@@ -37,13 +37,12 @@ from django.template.loader import render_to_string
from django.shortcuts import render_to_response
from django.template import RequestContext
from django.contrib.auth import authenticate
from django.contrib.auth.forms import AuthenticationForm
from django.contrib import messages
from django.utils.translation import ugettext as _
from astakos.im.target.util import prepare_response
from astakos.im.models import AstakosUser
from astakos.im.forms import LoginForm
from urllib import unquote
from hashlib import new as newhasher
......@@ -52,7 +51,7 @@ def login(request, on_failure='index.html'):
"""
on_failure: whatever redirect accepts as to
"""
form = AuthenticationForm(data=request.POST)
form = LoginForm(data=request.POST)
if not form.is_valid():
return render_to_response(on_failure,
{'form':form},
......@@ -84,4 +83,4 @@ def activate(request):
user.is_active = True
user.save()
return prepare_response(request, user, next, renew=True)
return prepare_response(request, user, next, renew=True, skip_login=True)
......@@ -12,22 +12,7 @@
<div class="span4">
<h4>Local account</h4>
<form action="{% url astakos.im.target.local.login %}" method="post" class="form-stacked">{% csrf_token %}
{{ form.non_field_errors }}
{% for field in form %}
<div class="field-wrapper">
<div class="label-wrapper">
{% if field.name == "username" %}
Email address or Username
{% else %}
{{ field.label_tag }}
{% endif %}
</div>
<div class="value-wrapper">
{{ field }}
{{ field.errors }}
</div>
</div>
{% endfor %}
{{ form.as_p }}
<div>
<a href="{% url django.contrib.auth.views.password_reset %}">Forgot your password?</a>
</div>
......
......@@ -6,13 +6,6 @@
<div class="container">
<h3>You have {{ user.invitations }} invitation{{ user.invitations|pluralize }} left.</h3>
<!--{% if message %}
<br />
<div class="alert-message {{ status }}">
<p>{{ message }}</p>
</div>
{% endif %}-->
{% if user.invitations %}
<br />
<h4>Invite someone else:</h4>
......
......@@ -54,7 +54,6 @@ from django.shortcuts import render_to_response
from django.utils.http import urlencode
from django.utils.translation import ugettext as _
from django.core.urlresolvers import reverse
from django.contrib.auth.forms import AuthenticationForm
from django.contrib.auth.models import AnonymousUser
from django.contrib.auth.decorators import login_required
from django.contrib.sites.models import Site
......@@ -66,7 +65,7 @@ from django.contrib.auth.forms import UserCreationForm
from astakos.im.models import AstakosUser, Invitation
from astakos.im.util import isoformat, get_or_create_user, get_context
from astakos.im.backends import get_backend
from astakos.im.forms import ProfileForm, FeedbackForm
from astakos.im.forms import ProfileForm, FeedbackForm, LoginForm
def render_response(template, tab=None, status=200, context_instance=None, **kwargs):
"""
......@@ -99,7 +98,7 @@ def index(request, template_name='index.html', extra_context={}):
"""
return render_response(template_name,
form = AuthenticationForm(),
form = LoginForm(),
context_instance = get_context(request, extra_context))
def _generate_invitation_code():
......@@ -164,7 +163,7 @@ def invite(request, template_name='invitations.html', extra_context={}):
"""
status = None
message = None
inviter = request.user
inviter = AstakosUser.objects.get(username = request.user.username)
if request.method == 'POST':
username = request.POST.get('uniq')
......@@ -287,12 +286,12 @@ def signup(request, template_name='signup.html', extra_context={}, backend=None)
signup.html or ``template_name`` keyword argument.
"""
if not backend:
backend = get_backend()
backend = get_backend(request)
try:
form = backend.get_signup_form(request)
form = backend.get_signup_form()
if request.method == 'POST':
if form.is_valid():
status, message = backend.signup(request)
status, message = backend.signup(form)
# rollback incase of error
if status == messages.ERROR:
transaction.rollback()
......@@ -302,7 +301,7 @@ def signup(request, template_name='signup.html', extra_context={}, backend=None)
if next:
return redirect(next)
messages.add_message(request, status, message)
except (Invitation.DoesNotExist, Exception), e:
except Invitation.DoesNotExist, e:
messages.add_message(request, messages.ERROR, e)
return render_response(template_name,
form = form if 'form' in locals() else UserCreationForm(),
......
......@@ -41,10 +41,8 @@ TEMPLATE_CONTEXT_PROCESSORS = ('django.contrib.messages.context_processors.messa
'astakos.im.context_processors.code',
'astakos.im.context_processors.invitations')
AUTHENTICATION_BACKENDS = (
'astakos.im.auth_backends.AstakosUserModelCredentialsBackend',
'astakos.im.auth_backends.AstakosUserModelTokenBackend',
)
AUTHENTICATION_BACKENDS = ('astakos.im.auth_backends.EmailBackend',
'astakos.im.auth_backends.TokenBackend')
CUSTOM_USER_MODEL = 'astakos.im.AstakosUser'
......