forms.py 39.2 KB
Newer Older
Antony Chazapis's avatar
Antony Chazapis committed
1
# Copyright 2011-2012 GRNET S.A. All rights reserved.
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
2
#
3 4 5
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# conditions are met:
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
6
#
7 8 9
#   1. Redistributions of source code must retain the above
#      copyright notice, this list of conditions and the following
#      disclaimer.
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
10
#
11 12 13 14
#   2. Redistributions in binary form must reproduce the above
#      copyright notice, this list of conditions and the following
#      disclaimer in the documentation and/or other materials
#      provided with the distribution.
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
15
#
16 17 18 19 20 21 22 23 24 25 26 27
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
28
#
29 30 31 32
# The views and conclusions contained in the software and
# documentation are those of the authors and should not be
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.
33
from urlparse import urljoin
34
from random import random
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
35
from datetime import datetime, timedelta
36 37 38

from django import forms
from django.utils.translation import ugettext as _
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
39 40 41
from django.contrib.auth.forms import (
    UserCreationForm, AuthenticationForm,
    PasswordResetForm, PasswordChangeForm,
42
    SetPasswordForm)
43
from django.core.mail import send_mail, get_connection
44 45 46
from django.contrib.auth.tokens import default_token_generator
from django.template import Context, loader
from django.utils.http import int_to_base36
47
from django.core.urlresolvers import reverse
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
48
from django.utils.safestring import mark_safe
49
from django.utils.encoding import smart_str
50
from django.conf import settings
51
from django.forms.models import fields_for_model
52
from django.db import transaction
53 54
from django.utils.encoding import smart_unicode
from django.core import validators
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
55
from django.contrib.auth.models import AnonymousUser
56
from django.core.exceptions import PermissionDenied
57

58
from astakos.im.models import (
59
    AstakosUser, EmailChange, Invitation,
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
60
    Resource, PendingThirdPartyUser, get_latest_terms, RESOURCE_SEPARATOR,
61
    ProjectApplication, Project)
62 63 64
from astakos.im.settings import (
    INVITATIONS_PER_LEVEL, BASEURL, SITENAME, RECAPTCHA_PRIVATE_KEY,
    RECAPTCHA_ENABLED, DEFAULT_CONTACT_EMAIL, LOGGING_LEVEL,
65
    PASSWORD_RESET_EMAIL_SUBJECT, NEWPASSWD_INVALIDATE_TOKEN,
66
    MODERATION_ENABLED, PROJECT_MEMBER_JOIN_POLICIES,
67 68
    PROJECT_MEMBER_LEAVE_POLICIES, EMAILCHANGE_ENABLED,
    RESOURCES_PRESENTATION_DATA)
69
from astakos.im.widgets import DummyWidget, RecaptchaWidget
70
from astakos.im.functions import (
71
    send_change_email, submit_application, accept_membership_checks)
72

Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
73 74
from astakos.im.util import reserved_email, reserved_verified_email, \
                            get_query, model_to_dict
75
from astakos.im import auth_providers
76

Olga Brani's avatar
Olga Brani committed
77
import astakos.im.messages as astakos_messages
78

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
79
import logging
80
import hashlib
81
import recaptcha.client.captcha as captcha
82
import re
83

84 85
logger = logging.getLogger(__name__)

86
DOMAIN_VALUE_REGEX = re.compile(
87
    r'^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,6}$',
88
    re.IGNORECASE)
89

90
class StoreUserMixin(object):
91

92
    def store_user(self, user, request):
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
93 94 95
        """
        WARNING: this should be wrapped inside a transactional view/method.
        """
96 97 98 99 100 101 102 103 104 105 106 107 108
        user.save()
        self.post_store_user(user, request)
        return user

    def post_store_user(self, user, request):
        """
        Interface method for descendant backends to be able to do stuff within
        the transaction enabled by store_user.
        """
        pass


class LocalUserCreationForm(UserCreationForm, StoreUserMixin):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
109 110
    """
    Extends the built in UserCreationForm in several ways:
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
111

112 113
    * Adds email, first_name, last_name, recaptcha_challenge_field,
    * recaptcha_response_field field.
114
    * The username field isn't visible and it is assigned a generated id.
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
115
    * User created is not active.
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
116
    """
117
    recaptcha_challenge_field = forms.CharField(widget=DummyWidget)
118 119
    recaptcha_response_field = forms.CharField(
        widget=RecaptchaWidget, label='')
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
120

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
121 122
    class Meta:
        model = AstakosUser
123 124
        fields = ("email", "first_name", "last_name",
                  "has_signed_terms", "has_signed_terms")
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
125

126 127
    def __init__(self, *args, **kwargs):
        """
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
128
        Changes the order of fields, and removes the username field.
129
        """
130
        request = kwargs.pop('request', None)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
131 132 133
        if request:
            self.ip = request.META.get('REMOTE_ADDR',
                                       request.META.get('HTTP_X_REAL_IP', None))
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
134

135
        super(LocalUserCreationForm, self).__init__(*args, **kwargs)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
136
        self.fields.keyOrder = ['email', 'first_name', 'last_name',
137
                                'password1', 'password2']
Olga Brani's avatar
Olga Brani committed
138

139 140
        if RECAPTCHA_ENABLED:
            self.fields.keyOrder.extend(['recaptcha_challenge_field',
141
                                         'recaptcha_response_field', ])
Olga Brani's avatar
Olga Brani committed
142 143
        if get_latest_terms():
            self.fields.keyOrder.append('has_signed_terms')
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
144 145 146 147 148

        if 'has_signed_terms' in self.fields:
            # Overriding field label since we need to apply a link
            # to the terms within the label
            terms_link_html = '<a href="%s" target="_blank">%s</a>' \
149
                % (reverse('latest_terms'), _("the terms"))
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
150
            self.fields['has_signed_terms'].label = \
151
                mark_safe("I agree with %s" % terms_link_html)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
152

153
    def clean_email(self):
154
        email = self.cleaned_data['email']
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
155
        if not email:
Olga Brani's avatar
Olga Brani committed
156
            raise forms.ValidationError(_(astakos_messages.REQUIRED_FIELD))
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
157
        if reserved_verified_email(email):
Olga Brani's avatar
Olga Brani committed
158
            raise forms.ValidationError(_(astakos_messages.EMAIL_USED))
159
        return email
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
160

161 162 163
    def clean_has_signed_terms(self):
        has_signed_terms = self.cleaned_data['has_signed_terms']
        if not has_signed_terms:
Olga Brani's avatar
Olga Brani committed
164
            raise forms.ValidationError(_(astakos_messages.SIGN_TERMS))
165
        return has_signed_terms
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
166

167 168 169 170 171 172 173 174 175 176 177 178 179 180 181
    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:
Olga Brani's avatar
Olga Brani committed
182
            raise forms.ValidationError(_(astakos_messages.CAPTCHA_VALIDATION_ERR))
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
183

184 185 186 187 188 189 190 191
    def post_store_user(self, user, request):
        """
        Interface method for descendant backends to be able to do stuff within
        the transaction enabled by store_user.
        """
        user.add_auth_provider('local', auth_backend='astakos')
        user.set_password(self.cleaned_data['password1'])

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
192
    def save(self, commit=True):
193
        """
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
194 195 196
        Saves the email, first_name and last_name properties, after the normal
        save behavior is complete.
        """
197
        user = super(LocalUserCreationForm, self).save(commit=False)
198
        user.renew_token()
199 200
        if commit:
            user.save()
201
            logger.log(LOGGING_LEVEL, 'Created user %s' % user.email)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
202
        return user
203

204

205
class InvitedLocalUserCreationForm(LocalUserCreationForm):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
206
    """
207
    Extends the LocalUserCreationForm: email is readonly.
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
208
    """
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
209 210
    class Meta:
        model = AstakosUser
211
        fields = ("email", "first_name", "last_name", "has_signed_terms")
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
212

213 214
    def __init__(self, *args, **kwargs):
        """
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
215
        Changes the order of fields, and removes the username field.
216
        """
217
        super(InvitedLocalUserCreationForm, self).__init__(*args, **kwargs)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
218

219
        #set readonly form fields
220
        ro = ('email', 'username',)
221 222
        for f in ro:
            self.fields[f].widget.attrs['readonly'] = True
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
223

224
    def save(self, commit=True):
225
        user = super(InvitedLocalUserCreationForm, self).save(commit=False)
226
        user.set_invitations_level()
227
        user.email_verified = True
228 229 230
        if commit:
            user.save()
        return user
231

232 233

class ThirdPartyUserCreationForm(forms.ModelForm, StoreUserMixin):
234 235 236 237 238
    id = forms.CharField(
        widget=forms.HiddenInput(),
        label='',
        required=False
    )
239 240 241 242
    third_party_identifier = forms.CharField(
        widget=forms.HiddenInput(),
        label=''
    )
Olga Brani's avatar
Olga Brani committed
243 244
    email = forms.EmailField(
        label='Contact email',
Olga Brani's avatar
Olga Brani committed
245
        help_text = 'This is needed for contact purposes. It doesn&#39;t need to be the same with the one you provided to login previously. '
Olga Brani's avatar
Olga Brani committed
246
    )
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
247

248 249
    class Meta:
        model = AstakosUser
250 251
        fields = ['id', 'email', 'third_party_identifier',
                  'first_name', 'last_name', 'has_signed_terms']
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
252

253 254 255 256
    def __init__(self, *args, **kwargs):
        """
        Changes the order of fields, and removes the username field.
        """
257 258 259
        self.request = kwargs.get('request', None)
        if self.request:
            kwargs.pop('request')
260

261
        super(ThirdPartyUserCreationForm, self).__init__(*args, **kwargs)
262

263 264 265
        if not get_latest_terms():
            del self.fields['has_signed_terms']

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
266 267 268 269
        if 'has_signed_terms' in self.fields:
            # Overriding field label since we need to apply a link
            # to the terms within the label
            terms_link_html = '<a href="%s" target="_blank">%s</a>' \
270
                % (reverse('latest_terms'), _("the terms"))
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
271 272
            self.fields['has_signed_terms'].label = \
                    mark_safe("I agree with %s" % terms_link_html)
273

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
274
    def clean_email(self):
275
        email = self.cleaned_data['email']
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
276
        if not email:
Olga Brani's avatar
Olga Brani committed
277
            raise forms.ValidationError(_(astakos_messages.REQUIRED_FIELD))
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
278
        if reserved_verified_email(email):
279 280 281 282 283 284
            provider = auth_providers.get_provider(self.request.REQUEST.get('provider', 'local'))
            extra_message = _(astakos_messages.EXISTING_EMAIL_THIRD_PARTY_NOTIFICATION) % \
                    (provider.get_title_display, reverse('edit_profile'))

            raise forms.ValidationError(_(astakos_messages.EMAIL_USED) + ' ' + \
                                        extra_message)
285
        return email
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
286

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
287 288 289
    def clean_has_signed_terms(self):
        has_signed_terms = self.cleaned_data['has_signed_terms']
        if not has_signed_terms:
Olga Brani's avatar
Olga Brani committed
290
            raise forms.ValidationError(_(astakos_messages.SIGN_TERMS))
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
291
        return has_signed_terms
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
292

293 294 295 296
    def post_store_user(self, user, request):
        pending = PendingThirdPartyUser.objects.get(
                                token=request.POST.get('third_party_token'),
                                third_party_identifier= \
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
297
                            self.cleaned_data.get('third_party_identifier'))
298 299
        return user.add_pending_auth_provider(pending)

300 301 302
    def save(self, commit=True):
        user = super(ThirdPartyUserCreationForm, self).save(commit=False)
        user.set_unusable_password()
303
        user.renew_token()
304 305
        if commit:
            user.save()
306
            logger.log(LOGGING_LEVEL, 'Created user %s' % user.email)
307 308
        return user

309

310
class InvitedThirdPartyUserCreationForm(ThirdPartyUserCreationForm):
311
    """
312
    Extends the ThirdPartyUserCreationForm: email is readonly.
313
    """
314
    def __init__(self, *args, **kwargs):
315 316 317
        """
        Changes the order of fields, and removes the username field.
        """
318 319
        super(
            InvitedThirdPartyUserCreationForm, self).__init__(*args, **kwargs)
320

321
        #set readonly form fields
322
        ro = ('email',)
323 324
        for f in ro:
            self.fields[f].widget.attrs['readonly'] = True
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
325

326 327
    def save(self, commit=True):
        user = super(InvitedThirdPartyUserCreationForm, self).save(commit=False)
328
        user.set_invitation_level()
329 330 331 332
        user.email_verified = True
        if commit:
            user.save()
        return user
333

334

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
335
class ShibbolethUserCreationForm(ThirdPartyUserCreationForm):
336 337
    additional_email = forms.CharField(
        widget=forms.HiddenInput(), label='', required=False)
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
338

339 340 341 342 343 344
    def __init__(self, *args, **kwargs):
        super(ShibbolethUserCreationForm, self).__init__(*args, **kwargs)
        # 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)
345
        self.initial['email'] = None
346

347

Olga Brani's avatar
Olga Brani committed
348 349
class InvitedShibbolethUserCreationForm(ShibbolethUserCreationForm,
                                        InvitedThirdPartyUserCreationForm):
350
    pass
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
351

352

353 354
class LoginForm(AuthenticationForm):
    username = forms.EmailField(label=_("Email"))
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
355
    recaptcha_challenge_field = forms.CharField(widget=DummyWidget)
356 357
    recaptcha_response_field = forms.CharField(
        widget=RecaptchaWidget, label='')
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
358

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
359 360 361 362 363 364
    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))
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
365

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
366 367 368 369 370
        t = ('request', 'was_limited')
        for elem in t:
            if elem in kwargs.keys():
                kwargs.pop(elem)
        super(LoginForm, self).__init__(*args, **kwargs)
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
371

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
372 373 374
        self.fields.keyOrder = ['username', 'password']
        if was_limited and RECAPTCHA_ENABLED:
            self.fields.keyOrder.extend(['recaptcha_challenge_field',
375
                                         'recaptcha_response_field', ])
Olga Brani's avatar
Olga Brani committed
376 377

    def clean_username(self):
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
378
        return self.cleaned_data['username'].lower()
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
379

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
380 381 382 383 384 385 386 387 388 389 390 391 392 393 394
    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:
Olga Brani's avatar
Olga Brani committed
395
            raise forms.ValidationError(_(astakos_messages.CAPTCHA_VALIDATION_ERR))
396

397
    def clean(self):
398 399 400
        """
        Override default behavior in order to check user's activation later
        """
401 402
        username = self.cleaned_data.get('username')

403 404 405 406 407 408 409 410 411
        if username:
            try:
                user = AstakosUser.objects.get_by_identifier(username)
                if not user.has_auth_provider('local'):
                    provider = auth_providers.get_provider('local')
                    raise forms.ValidationError(
                        _(provider.get_message('NOT_ACTIVE_FOR_USER')))
            except AstakosUser.DoesNotExist:
                pass
412

413 414 415
        try:
            super(LoginForm, self).clean()
        except forms.ValidationError, e:
416 417 418 419
            if self.user_cache is None:
                raise
            if not self.user_cache.is_active:
                raise forms.ValidationError(self.user_cache.get_inactive_message())
420 421 422
            if self.request:
                if not self.request.session.test_cookie_worked():
                    raise
423
        return self.cleaned_data
424

425

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
426 427 428
class ProfileForm(forms.ModelForm):
    """
    Subclass of ``ModelForm`` for permiting user to edit his/her profile.
Olga Brani's avatar
Olga Brani committed
429 430
    Most of the fields are readonly since the user is not allowed to change
    them.
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
431

Olga Brani's avatar
Olga Brani committed
432 433
    The class defines a save method which sets ``is_verified`` to True so as the
    user during the next login will not to be redirected to profile page.
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
434
    """
435
    renew = forms.BooleanField(label='Renew token', required=False)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
436

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
437 438
    class Meta:
        model = AstakosUser
439 440
        fields = ('email', 'first_name', 'last_name', 'auth_token',
                  'auth_token_expires')
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
441

442
    def __init__(self, *args, **kwargs):
443
        self.session_key = kwargs.pop('session_key', None)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
444 445
        super(ProfileForm, self).__init__(*args, **kwargs)
        instance = getattr(self, 'instance', None)
446
        ro_fields = ('email', 'auth_token', 'auth_token_expires')
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
447 448 449
        if instance and instance.id:
            for field in ro_fields:
                self.fields[field].widget.attrs['readonly'] = True
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
450

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
451 452 453
    def save(self, commit=True):
        user = super(ProfileForm, self).save(commit=False)
        user.is_verified = True
454
        if self.cleaned_data.get('renew'):
455 456 457 458
            user.renew_token(
                flush_sessions=True,
                current_key=self.session_key
            )
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
459 460 461
        if commit:
            user.save()
        return user
462

463

464

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
465 466 467 468
class FeedbackForm(forms.Form):
    """
    Form for writing feedback.
    """
469
    feedback_msg = forms.CharField(widget=forms.Textarea, label=u'Message')
470 471
    feedback_data = forms.CharField(widget=forms.HiddenInput(), label='',
                                    required=False)
472

473

474 475 476 477
class SendInvitationForm(forms.Form):
    """
    Form for sending an invitations
    """
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
478

479 480 481 482
    email = forms.EmailField(required=True, label='Email address')
    first_name = forms.EmailField(label='First name')
    last_name = forms.EmailField(label='Last name')

483 484 485

class ExtendedPasswordResetForm(PasswordResetForm):
    """
486
    Extends PasswordResetForm by overriding
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
487

488 489
    save method: to pass a custom from_email in send_mail.
    clean_email: to handle local auth provider checks
490
    """
491 492 493
    def clean_email(self):
        email = super(ExtendedPasswordResetForm, self).clean_email()
        try:
494 495 496 497 498
            user = AstakosUser.objects.get_by_identifier(email)

            if not user.is_active:
                raise forms.ValidationError(_(astakos_messages.ACCOUNT_INACTIVE))

499
            if not user.has_usable_password():
500 501 502 503 504 505 506
                provider = auth_providers.get_provider('local')
                available_providers = user.auth_providers.all()
                available_providers = ",".join(p.settings.get_title_display for p in \
                                                   available_providers)
                message = astakos_messages.UNUSABLE_PASSWORD % \
                    (provider.get_method_prompt_display, available_providers)
                raise forms.ValidationError(message)
507 508

            if not user.can_change_password():
509
                raise forms.ValidationError(_(astakos_messages.AUTH_PROVIDER_CANNOT_CHANGE_PASSWORD))
510
        except AstakosUser.DoesNotExist, e:
Olga Brani's avatar
Olga Brani committed
511
            raise forms.ValidationError(_(astakos_messages.EMAIL_UNKNOWN))
512
        return email
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
513

514 515 516
    def save(
        self, domain_override=None, email_template_name='registration/password_reset_email.html',
            use_https=False, token_generator=default_token_generator, request=None):
517 518 519 520
        """
        Generates a one-use only link for resetting password and sends to the user.
        """
        for user in self.users_cache:
521
            url = user.astakosuser.get_password_reset_url(token_generator)
522
            url = urljoin(BASEURL, url)
523 524 525
            t = loader.get_template(email_template_name)
            c = {
                'email': user.email,
526
                'url': url,
527
                'site_name': SITENAME,
528
                'user': user,
529
                'baseurl': BASEURL,
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
530
                'support': DEFAULT_CONTACT_EMAIL
531
            }
532
            from_email = settings.SERVER_EMAIL
533
            send_mail(_(PASSWORD_RESET_EMAIL_SUBJECT),
534 535 536
                      t.render(Context(c)),
                      from_email,
                      [user.email],
537
                      connection=get_connection())
538

539

540
class EmailChangeForm(forms.ModelForm):
541

542 543 544
    class Meta:
        model = EmailChange
        fields = ('new_email_address',)
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
545

546 547
    def clean_new_email_address(self):
        addr = self.cleaned_data['new_email_address']
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
548
        if reserved_verified_email(addr):
Olga Brani's avatar
Olga Brani committed
549
            raise forms.ValidationError(_(astakos_messages.EMAIL_USED))
550
        return addr
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
551

552
    def save(self, request, email_template_name='registration/email_change_email.txt', commit=True):
553 554
        ec = super(EmailChangeForm, self).save(commit=False)
        ec.user = request.user
555 556 557
        # delete pending email changes
        request.user.emailchanges.all().delete()

558 559 560
        activation_key = hashlib.sha1(
            str(random()) + smart_str(ec.new_email_address))
        ec.activation_key = activation_key.hexdigest()
561 562 563 564
        if commit:
            ec.save()
        send_change_email(ec, request, email_template_name=email_template_name)

565

566
class SignApprovalTermsForm(forms.ModelForm):
567

568 569 570
    class Meta:
        model = AstakosUser
        fields = ("has_signed_terms",)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
571

572 573
    def __init__(self, *args, **kwargs):
        super(SignApprovalTermsForm, self).__init__(*args, **kwargs)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
574

575 576 577
    def clean_has_signed_terms(self):
        has_signed_terms = self.cleaned_data['has_signed_terms']
        if not has_signed_terms:
Olga Brani's avatar
Olga Brani committed
578
            raise forms.ValidationError(_(astakos_messages.SIGN_TERMS))
579
        return has_signed_terms
580

581

582
class InvitationForm(forms.ModelForm):
583

584
    username = forms.EmailField(label=_("Email"))
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
585

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
586 587
    def __init__(self, *args, **kwargs):
        super(InvitationForm, self).__init__(*args, **kwargs)
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
588

589 590 591
    class Meta:
        model = Invitation
        fields = ('username', 'realname')
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
592

593 594 595
    def clean_username(self):
        username = self.cleaned_data['username']
        try:
596
            Invitation.objects.get(username=username)
Olga Brani's avatar
Olga Brani committed
597
            raise forms.ValidationError(_(astakos_messages.INVITATION_EMAIL_EXISTS))
598 599
        except Invitation.DoesNotExist:
            pass
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
600
        return username
601

602

603 604 605 606 607
class ExtendedPasswordChangeForm(PasswordChangeForm):
    """
    Extends PasswordChangeForm by enabling user
    to optionally renew also the token.
    """
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
608
    if not NEWPASSWD_INVALIDATE_TOKEN:
609 610 611
        renew = forms.BooleanField(label='Renew token', required=False,
                                   initial=True,
                                   help_text='Unsetting this may result in security risk.')
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
612

613
    def __init__(self, user, *args, **kwargs):
614
        self.session_key = kwargs.pop('session_key', None)
615
        super(ExtendedPasswordChangeForm, self).__init__(user, *args, **kwargs)
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
616

617
    def save(self, commit=True):
618 619 620 621 622 623 624
        try:
            if NEWPASSWD_INVALIDATE_TOKEN or self.cleaned_data.get('renew'):
                self.user.renew_token()
            self.user.flush_sessions(current_key=self.session_key)
        except AttributeError:
            # if user model does has not such methods
            pass
625
        return super(ExtendedPasswordChangeForm, self).save(commit=commit)
626 627 628 629 630 631

class ExtendedSetPasswordForm(SetPasswordForm):
    """
    Extends SetPasswordForm by enabling user
    to optionally renew also the token.
    """
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
632
    if not NEWPASSWD_INVALIDATE_TOKEN:
633 634 635 636
        renew = forms.BooleanField(
            label='Renew token',
            required=False,
            initial=True,
637
            help_text='Unsetting this may result in security risk.')
638

639 640
    def __init__(self, user, *args, **kwargs):
        super(ExtendedSetPasswordForm, self).__init__(user, *args, **kwargs)
641 642

    @transaction.commit_on_success()
643
    def save(self, commit=True):
644 645 646
        try:
            self.user = AstakosUser.objects.get(id=self.user.id)
            if NEWPASSWD_INVALIDATE_TOKEN or self.cleaned_data.get('renew'):
647
                self.user.renew_token()
648 649 650 651
            #self.user.flush_sessions()
            if not self.user.has_auth_provider('local'):
                self.user.add_auth_provider('local', auth_backend='astakos')

652 653
        except BaseException, e:
            logger.exception(e)
654
        return super(ExtendedSetPasswordForm, self).save(commit=commit)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
655 656


657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675


app_name_label       =  "Project name"
app_name_placeholder = _("myproject.mylab.ntua.gr")
app_name_validator   =  validators.RegexValidator(
                            DOMAIN_VALUE_REGEX,
                            _(astakos_messages.DOMAIN_VALUE_ERR),
                            'invalid')
app_name_help        =  _("""
        The Project's name should be in a domain format.
        The domain shouldn't neccessarily exist in the real
        world but is helpful to imply a structure.
        e.g.: myproject.mylab.ntua.gr or
        myservice.myteam.myorganization""")
app_name_widget      =  forms.TextInput(
                            attrs={'placeholder': app_name_placeholder})


app_home_label       =  "Homepage URL"
Olga Brani's avatar
Olga Brani committed
676
app_home_placeholder =  'myinstitution.org/myproject/'
677 678
app_home_help        =  _("""
        URL pointing at your project's site.
Olga Brani's avatar
Olga Brani committed
679
        e.g.: myinstitution.org/myproject/.
680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712
        Leave blank if there is no website.""")
app_home_widget      =  forms.TextInput(
                            attrs={'placeholder': app_home_placeholder})

app_desc_label       =  _("Description")
app_desc_help        =  _("""
        Please provide a short but descriptive abstract of your Project,
        so that anyone searching can quickly understand
        what this Project is about.""")

app_comment_label    =  _("Comments for review (private)")
app_comment_help     =  _("""
        Write down any comments you may have for the reviewer
        of this application (e.g. background and rationale to
        support your request).
        The comments are strictly for the review process
        and will not be published.""")

app_start_date_label =  _("Start date")
app_start_date_help  =  _("""
        Provide a date when your need your project to be created,
        and members to be able to join and get resources.
        This date is only a hint to help prioritize reviews.""")

app_end_date_label   =  _("Termination date")
app_end_date_help    =  _("""
        At this date, the project will be automatically terminated
        and its resource grants revoked from all members.
        Unless you know otherwise,
        it is best to start with a conservative estimation.
        You can always re-apply for an extension, if you need.""")

join_policy_label    =  _("Joining policy")
Olga Brani's avatar
Olga Brani committed
713 714
app_member_join_policy_help    =  _("""
        Text fo member_join_policy.""")
715
leave_policy_label   =  _("Leaving policy")
Olga Brani's avatar
Olga Brani committed
716 717
app_member_leave_policy_help    =  _("""
        Text fo member_leave_policy.""")
718 719 720 721 722 723 724 725 726 727 728 729 730

max_members_label    =  _("Maximum member count")
max_members_help     =  _("""
        Specify the maximum number of members this project may have,
        including the owner. Beyond this number, no new members
        may join the project and be granted the project resources.
        Unless you certainly for otherwise,
        it is best to start with a conservative limit.
        You can always request a raise when you need it.""")

join_policies = PROJECT_MEMBER_JOIN_POLICIES.iteritems()
leave_policies = PROJECT_MEMBER_LEAVE_POLICIES.iteritems()

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
731
class ProjectApplicationForm(forms.ModelForm):
732

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
733
    name = forms.CharField(
734 735 736 737 738
        label     = app_name_label,
        help_text = app_name_help,
        widget    = app_name_widget,
        validators = [app_name_validator])

739
    homepage = forms.URLField(
740
        label     = app_home_label,
741
        help_text = app_home_help,
742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765
        widget    = app_home_widget,
        required  = False)

    description = forms.CharField(
        label     = app_desc_label,
        help_text = app_desc_help,
        widget    = forms.Textarea,
        required  = False)

    comments = forms.CharField(
        label     = app_comment_label,
        help_text = app_comment_help,
        widget    = forms.Textarea,
        required  = False)

    start_date = forms.DateTimeField(
        label     = app_start_date_label,
        help_text = app_start_date_help,
        required  = False)

    end_date = forms.DateTimeField(
        label     = app_end_date_label,
        help_text = app_end_date_help)

766
    member_join_policy  = forms.TypedChoiceField(
767
        label     = join_policy_label,
Olga Brani's avatar
Olga Brani committed
768
        help_text = app_member_join_policy_help,
769 770
        initial   = 2,
        coerce    = int,
771
        choices   = join_policies)
772

773
    member_leave_policy = forms.TypedChoiceField(
774
        label     = leave_policy_label,
Olga Brani's avatar
Olga Brani committed
775
        help_text = app_member_leave_policy_help,
776
        coerce    = int,
777 778 779 780 781 782
        choices   = leave_policies)

    limit_on_members_number = forms.IntegerField(
        label     = max_members_label,
        help_text = max_members_help,
        required  = False)
783

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
784
    class Meta:
785
        model = ProjectApplication
786 787 788 789 790
        fields = ( 'name', 'homepage', 'description',
                    'start_date', 'end_date', 'comments',
                    'member_join_policy', 'member_leave_policy',
                    'limit_on_members_number')

791
    def __init__(self, *args, **kwargs):
792 793
        instance = kwargs.get('instance')
        self.precursor_application = instance
794
        super(ProjectApplicationForm, self).__init__(*args, **kwargs)
795
        # in case of new application remove closed join policy
796 797 798 799
        if not instance:
            policies = PROJECT_MEMBER_JOIN_POLICIES.copy()
            policies.pop('3')
            self.fields['member_join_policy'].choices = policies.iteritems()
800

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
801 802
    def clean_start_date(self):
        start_date = self.cleaned_data.get('start_date')