X-Git-Url: https://code.grnet.gr/git/astakos/blobdiff_plain/18ffbee19899c48650ce9fb3e9730f4c24fc5097..53161dd822f7265b05d1c8341278fc0cd8cd82a9:/snf-astakos-app/astakos/im/forms.py diff --git a/snf-astakos-app/astakos/im/forms.py b/snf-astakos-app/astakos/im/forms.py index 9e29a7a..a73926e 100644 --- a/snf-astakos-app/astakos/im/forms.py +++ b/snf-astakos-app/astakos/im/forms.py @@ -35,7 +35,8 @@ from datetime import datetime from django import forms from django.utils.translation import ugettext as _ -from django.contrib.auth.forms import UserCreationForm, AuthenticationForm, PasswordResetForm +from django.contrib.auth.forms import UserCreationForm, AuthenticationForm, \ + PasswordResetForm, PasswordChangeForm, SetPasswordForm from django.core.mail import send_mail from django.contrib.auth.tokens import default_token_generator from django.template import Context, loader @@ -43,16 +44,24 @@ from django.utils.http import int_to_base36 from django.core.urlresolvers import reverse from django.utils.functional import lazy from django.utils.safestring import mark_safe +from django.contrib import messages +from django.utils.encoding import smart_str -from astakos.im.models import AstakosUser, Invitation -from astakos.im.settings import INVITATIONS_PER_LEVEL, DEFAULT_FROM_EMAIL, BASEURL, SITENAME, RECAPTCHA_PRIVATE_KEY, DEFAULT_CONTACT_EMAIL, RECAPTCHA_ENABLED -from astakos.im.widgets import DummyWidget, RecaptchaWidget, ApprovalTermsWidget +from astakos.im.models import AstakosUser, Invitation, get_latest_terms, EmailChange +from astakos.im.settings import INVITATIONS_PER_LEVEL, DEFAULT_FROM_EMAIL, \ + BASEURL, SITENAME, RECAPTCHA_PRIVATE_KEY, DEFAULT_CONTACT_EMAIL, \ + RECAPTCHA_ENABLED, LOGGING_LEVEL, PASSWORD_RESET_EMAIL_SUBJECT, \ + NEWPASSWD_INVALIDATE_TOKEN +from astakos.im.widgets import DummyWidget, RecaptchaWidget +from astakos.im.functions import send_change_email # since Django 1.4 use django.core.urlresolvers.reverse_lazy instead -from astakos.im.util import reverse_lazy, get_latest_terms +from astakos.im.util import reverse_lazy, reserved_email, get_query import logging +import hashlib import recaptcha.client.captcha as captcha +from random import random logger = logging.getLogger(__name__) @@ -69,24 +78,27 @@ class LocalUserCreationForm(UserCreationForm): class Meta: model = AstakosUser - fields = ("email", "first_name", "last_name", "has_signed_terms") - widgets = {"has_signed_terms":ApprovalTermsWidget(terms_uri=reverse_lazy('latest_terms'))} + fields = ("email", "first_name", "last_name", "has_signed_terms", "has_signed_terms") def __init__(self, *args, **kwargs): """ Changes the order of fields, and removes the username field. """ - if 'ip' in kwargs: - self.ip = kwargs['ip'] - kwargs.pop('ip') + request = kwargs.get('request', None) + if request: + kwargs.pop('request') + self.ip = request.META.get('REMOTE_ADDR', + request.META.get('HTTP_X_REAL_IP', None)) + super(LocalUserCreationForm, self).__init__(*args, **kwargs) self.fields.keyOrder = ['email', 'first_name', 'last_name', 'password1', 'password2'] - if get_latest_terms(): - self.fields.keyOrder.append('has_signed_terms') + if RECAPTCHA_ENABLED: self.fields.keyOrder.extend(['recaptcha_challenge_field', 'recaptcha_response_field',]) + if get_latest_terms(): + self.fields.keyOrder.append('has_signed_terms') if 'has_signed_terms' in self.fields: # Overriding field label since we need to apply a link @@ -100,11 +112,9 @@ class LocalUserCreationForm(UserCreationForm): email = self.cleaned_data['email'] if not email: raise forms.ValidationError(_("This field is required")) - try: - AstakosUser.objects.get(email = email) + if reserved_email(email): raise forms.ValidationError(_("This email is already used")) - except AstakosUser.DoesNotExist: - return email + return email def clean_has_signed_terms(self): has_signed_terms = self.cleaned_data['has_signed_terms'] @@ -138,20 +148,16 @@ class LocalUserCreationForm(UserCreationForm): user.renew_token() if commit: user.save() - logger.info('Created user %s', user) + logger._log(LOGGING_LEVEL, 'Created user %s' % user.email, []) return user class InvitedLocalUserCreationForm(LocalUserCreationForm): """ - Extends the LocalUserCreationForm: adds an inviter readonly field. + Extends the LocalUserCreationForm: email is readonly. """ - - inviter = forms.CharField(widget=forms.TextInput(), label=_('Inviter Real Name')) - class Meta: model = AstakosUser fields = ("email", "first_name", "last_name", "has_signed_terms") - widgets = {"has_signed_terms":ApprovalTermsWidget(terms_uri=reverse_lazy('latest_terms'))} def __init__(self, *args, **kwargs): """ @@ -160,9 +166,10 @@ class InvitedLocalUserCreationForm(LocalUserCreationForm): super(InvitedLocalUserCreationForm, self).__init__(*args, **kwargs) #set readonly form fields - self.fields['inviter'].widget.attrs['readonly'] = True - self.fields['email'].widget.attrs['readonly'] = True - self.fields['username'].widget.attrs['readonly'] = True + ro = ('email', 'username',) + for f in ro: + self.fields[f].widget.attrs['readonly'] = True + def save(self, commit=True): user = super(InvitedLocalUserCreationForm, self).save(commit=False) @@ -177,26 +184,24 @@ class InvitedLocalUserCreationForm(LocalUserCreationForm): class ThirdPartyUserCreationForm(forms.ModelForm): class Meta: model = AstakosUser - fields = ("email", "first_name", "last_name", "third_party_identifier", - "has_signed_terms", "provider") - widgets = {"has_signed_terms":ApprovalTermsWidget(terms_uri=reverse_lazy('latest_terms'))} - + fields = ("email", "first_name", "last_name", "third_party_identifier", "has_signed_terms") + def __init__(self, *args, **kwargs): """ Changes the order of fields, and removes the username field. """ - if 'ip' in kwargs: - kwargs.pop('ip') + self.request = kwargs.get('request', None) + if self.request: + kwargs.pop('request') super(ThirdPartyUserCreationForm, self).__init__(*args, **kwargs) - self.fields.keyOrder = ['email', 'first_name', 'last_name', - 'provider', 'third_party_identifier'] + self.fields.keyOrder = ['email', 'first_name', 'last_name', 'third_party_identifier'] if get_latest_terms(): self.fields.keyOrder.append('has_signed_terms') #set readonly form fields - ro = ["provider", "third_party_identifier", "first_name", "last_name"] + ro = ["third_party_identifier"] for f in ro: self.fields[f].widget.attrs['readonly'] = True - + if 'has_signed_terms' in self.fields: # Overriding field label since we need to apply a link # to the terms within the label @@ -204,55 +209,124 @@ class ThirdPartyUserCreationForm(forms.ModelForm): % (reverse('latest_terms'), _("the terms")) self.fields['has_signed_terms'].label = \ mark_safe("I agree with %s" % terms_link_html) - + def clean_email(self): email = self.cleaned_data['email'] if not email: raise forms.ValidationError(_("This field is required")) - try: - AstakosUser.objects.get(email = email) - raise forms.ValidationError(_("This email is already used")) - except AstakosUser.DoesNotExist: - return email - + return email + def clean_has_signed_terms(self): has_signed_terms = self.cleaned_data['has_signed_terms'] if not has_signed_terms: raise forms.ValidationError(_('You have to agree with the terms')) return has_signed_terms - + def save(self, commit=True): user = super(ThirdPartyUserCreationForm, self).save(commit=False) user.set_unusable_password() user.renew_token() + user.provider = get_query(self.request).get('provider') if commit: user.save() - logger.info('Created user %s', user) + logger._log(LOGGING_LEVEL, 'Created user %s' % user.email, []) return user class InvitedThirdPartyUserCreationForm(ThirdPartyUserCreationForm): + """ + Extends the ThirdPartyUserCreationForm: email is readonly. + """ def __init__(self, *args, **kwargs): + """ + Changes the order of fields, and removes the username field. + """ super(InvitedThirdPartyUserCreationForm, self).__init__(*args, **kwargs) + #set readonly form fields - self.fields['email'].widget.attrs['readonly'] = True + ro = ('email',) + for f in ro: + self.fields[f].widget.attrs['readonly'] = True + + def save(self, commit=True): + user = super(InvitedThirdPartyUserCreationForm, self).save(commit=False) + level = user.invitation.inviter.level + 1 + user.level = level + user.invitations = INVITATIONS_PER_LEVEL.get(level, 0) + user.email_verified = True + if commit: + user.save() + return user class ShibbolethUserCreationForm(ThirdPartyUserCreationForm): + additional_email = forms.CharField(widget=forms.HiddenInput(), label='', required = False) + + def __init__(self, *args, **kwargs): + super(ShibbolethUserCreationForm, self).__init__(*args, **kwargs) + self.fields.keyOrder.append('additional_email') + # copy email value to additional_mail in case user will change it + name = 'email' + field = self.fields[name] + self.initial['additional_email'] = self.initial.get(name, field.initial) + def clean_email(self): email = self.cleaned_data['email'] - if not email: - raise forms.ValidationError(_("This field is required")) - try: - user = AstakosUser.objects.get(email = email) - if user.provider == 'local': - self.instance = user - return email - else: + for user in AstakosUser.objects.filter(email = email): + if user.provider == 'shibboleth': raise forms.ValidationError(_("This email is already associated with another shibboleth account.")) - except AstakosUser.DoesNotExist: - return email - + elif not user.is_active: + raise forms.ValidationError(_("This email is already associated with an inactive account. \ + You need to wait to be activated before being able to switch to a shibboleth account.")) + super(ShibbolethUserCreationForm, self).clean_email() + return email + +class InvitedShibbolethUserCreationForm(ShibbolethUserCreationForm, InvitedThirdPartyUserCreationForm): + pass + class LoginForm(AuthenticationForm): username = forms.EmailField(label=_("Email")) + recaptcha_challenge_field = forms.CharField(widget=DummyWidget) + recaptcha_response_field = forms.CharField(widget=RecaptchaWidget, label='') + + def __init__(self, *args, **kwargs): + was_limited = kwargs.get('was_limited', False) + request = kwargs.get('request', None) + if request: + self.ip = request.META.get('REMOTE_ADDR', + request.META.get('HTTP_X_REAL_IP', None)) + + t = ('request', 'was_limited') + for elem in t: + if elem in kwargs.keys(): + kwargs.pop(elem) + super(LoginForm, self).__init__(*args, **kwargs) + + self.fields.keyOrder = ['username', 'password'] + if was_limited and RECAPTCHA_ENABLED: + self.fields.keyOrder.extend(['recaptcha_challenge_field', + 'recaptcha_response_field',]) + + def clean_recaptcha_response_field(self): + if 'recaptcha_challenge_field' in self.cleaned_data: + self.validate_captcha() + return self.cleaned_data['recaptcha_response_field'] + + def clean_recaptcha_challenge_field(self): + if 'recaptcha_response_field' in self.cleaned_data: + self.validate_captcha() + return self.cleaned_data['recaptcha_challenge_field'] + + def validate_captcha(self): + rcf = self.cleaned_data['recaptcha_challenge_field'] + rrf = self.cleaned_data['recaptcha_response_field'] + check = captcha.submit(rcf, rrf, RECAPTCHA_PRIVATE_KEY, self.ip) + if not check.is_valid: + raise forms.ValidationError(_('You have not entered the correct words')) + + def clean(self): + super(LoginForm, self).clean() + if self.user_cache and self.user_cache.provider not in ('local', ''): + raise forms.ValidationError(_('Local login is not the current authentication method for this account.')) + return self.cleaned_data class ProfileForm(forms.ModelForm): """ @@ -266,12 +340,12 @@ class ProfileForm(forms.ModelForm): class Meta: model = AstakosUser - fields = ('email', 'first_name', 'last_name', 'auth_token', 'auth_token_expires', 'groups') + fields = ('email', 'first_name', 'last_name', 'auth_token', 'auth_token_expires') def __init__(self, *args, **kwargs): super(ProfileForm, self).__init__(*args, **kwargs) instance = getattr(self, 'instance', None) - ro_fields = ('auth_token', 'auth_token_expires', 'groups') + ro_fields = ('email', 'auth_token', 'auth_token_expires') if instance and instance.id: for field in ro_fields: self.fields[field].widget.attrs['readonly'] = True @@ -289,7 +363,7 @@ class FeedbackForm(forms.Form): """ Form for writing feedback. """ - feedback_msg = forms.CharField(widget=forms.TextInput(), label=u'Message') + feedback_msg = forms.CharField(widget=forms.Textarea, label=u'Message') feedback_data = forms.CharField(widget=forms.HiddenInput(), label='', required=False) @@ -310,6 +384,16 @@ class ExtendedPasswordResetForm(PasswordResetForm): Since Django 1.3 this is useless since ``django.contrib.auth.views.reset_password`` accepts a from_email argument. """ + def clean_email(self): + email = super(ExtendedPasswordResetForm, self).clean_email() + try: + user = AstakosUser.objects.get(email=email, is_active=True) + if not user.has_usable_password(): + raise forms.ValidationError(_("This account has not a usable password.")) + except AstakosUser.DoesNotExist, e: + raise forms.ValidationError(_('That e-mail address doesn\'t have an associated user account. Are you sure you\'ve registered?')) + return email + def save(self, domain_override=None, email_template_name='registration/password_reset_email.html', use_https=False, token_generator=default_token_generator, request=None): """ @@ -319,7 +403,7 @@ class ExtendedPasswordResetForm(PasswordResetForm): url = reverse('django.contrib.auth.views.password_reset_confirm', kwargs={'uidb36':int_to_base36(user.id), 'token':token_generator.make_token(user)}) - url = request.build_absolute_uri(url) + url = urljoin(BASEURL, url) t = loader.get_template(email_template_name) c = { 'email': user.email, @@ -330,9 +414,29 @@ class ExtendedPasswordResetForm(PasswordResetForm): 'support': DEFAULT_CONTACT_EMAIL } from_email = DEFAULT_FROM_EMAIL - send_mail(_("Password reset on %s alpha2 testing") % SITENAME, + send_mail(_(PASSWORD_RESET_EMAIL_SUBJECT), t.render(Context(c)), from_email, [user.email]) +class EmailChangeForm(forms.ModelForm): + class Meta: + model = EmailChange + fields = ('new_email_address',) + + def clean_new_email_address(self): + addr = self.cleaned_data['new_email_address'] + if AstakosUser.objects.filter(email__iexact=addr): + raise forms.ValidationError(_(u'This email address is already in use. Please supply a different email address.')) + return addr + + def save(self, email_template_name, request, commit=True): + ec = super(EmailChangeForm, self).save(commit=False) + ec.user = request.user + activation_key = hashlib.sha1(str(random()) + smart_str(ec.new_email_address)) + ec.activation_key=activation_key.hexdigest() + if commit: + ec.save() + send_change_email(ec, request, email_template_name=email_template_name) + class SignApprovalTermsForm(forms.ModelForm): class Meta: model = AstakosUser @@ -349,14 +453,14 @@ class SignApprovalTermsForm(forms.ModelForm): class InvitationForm(forms.ModelForm): username = forms.EmailField(label=_("Email")) - + def __init__(self, *args, **kwargs): super(InvitationForm, self).__init__(*args, **kwargs) - + class Meta: model = Invitation fields = ('username', 'realname') - + def clean_username(self): username = self.cleaned_data['username'] try: @@ -365,3 +469,40 @@ class InvitationForm(forms.ModelForm): except Invitation.DoesNotExist: pass return username + +class ExtendedPasswordChangeForm(PasswordChangeForm): + """ + Extends PasswordChangeForm by enabling user + to optionally renew also the token. + """ + if not NEWPASSWD_INVALIDATE_TOKEN: + renew = forms.BooleanField(label='Renew token', required=False, + initial=True, + help_text='Unsetting this may result in security risk.') + + def __init__(self, user, *args, **kwargs): + super(ExtendedPasswordChangeForm, self).__init__(user, *args, **kwargs) + + def save(self, commit=True): + if NEWPASSWD_INVALIDATE_TOKEN or self.cleaned_data.get('renew'): + self.user.renew_token() + return super(ExtendedPasswordChangeForm, self).save(commit=commit) + +class ExtendedSetPasswordForm(SetPasswordForm): + """ + Extends SetPasswordForm by enabling user + to optionally renew also the token. + """ + if not NEWPASSWD_INVALIDATE_TOKEN: + renew = forms.BooleanField(label='Renew token', required=False, + initial=True, + help_text='Unsetting this may result in security risk.') + + def __init__(self, user, *args, **kwargs): + super(ExtendedSetPasswordForm, self).__init__(user, *args, **kwargs) + + def save(self, commit=True): + if NEWPASSWD_INVALIDATE_TOKEN or self.cleaned_data.get('renew'): + if isinstance(self.user, AstakosUser): + self.user.renew_token() + return super(ExtendedSetPasswordForm, self).save(commit=commit)