forms.py 49.6 KB
Newer Older
1
# Copyright (C) 2010-2016 GRNET S.A.
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
2
#
Vangelis Koukis's avatar
Vangelis Koukis committed
3 4 5 6
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
7
#
Vangelis Koukis's avatar
Vangelis Koukis committed
8 9 10 11
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
12
#
Vangelis Koukis's avatar
Vangelis Koukis committed
13 14
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
15 16 17
import re
import synnefo.util.date as date_util

18
from datetime import datetime
19 20 21

from django import forms
from django.utils.translation import ugettext as _
22 23
from django.contrib.auth.forms import UserCreationForm, AuthenticationForm, \
    PasswordResetForm, PasswordChangeForm, SetPasswordForm
24
from django.core.mail import send_mail, get_connection
25
from django.contrib.auth.tokens import default_token_generator
26
from django.core.urlresolvers import reverse
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
27
from django.utils.safestring import mark_safe
28
from astakos.im import transaction
29
from django.core import validators
30

31
from synnefo.util import units
32
from synnefo_branding.utils import render_to_string
33
from synnefo.lib import join_urls
34
from astakos.im.fields import EmailField, InfiniteChoiceField
35 36
from astakos.im.models import AstakosUser, EmailChange, Invitation, Resource, \
    PendingThirdPartyUser, get_latest_terms, ProjectApplication, Project
37
from astakos.im import presentation
38
from astakos.im.widgets import DummyWidget, RecaptchaWidget
39
from astakos.im.functions import submit_application, \
40
    accept_membership_project_checks, ProjectError
41
from astakos.im.user_utils import change_user_email
42

43
from astakos.im.util import reserved_verified_email, model_to_dict
44
from astakos.im import auth_providers
45
from astakos.im import settings
46
from astakos.im import auth
47
from astakos.im.auth_backends import LDAPBackend
48

Olga Brani's avatar
Olga Brani committed
49
import astakos.im.messages as astakos_messages
50

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
51
import logging
52
import recaptcha.client.captcha as captcha
53
import re
54

55 56
logger = logging.getLogger(__name__)

57
BASE_PROJECT_NAME_REGEX = re.compile(
58
    r'^system:[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89aAbB][a-f0-9]{3}-'
59
     '[a-f0-9]{12}$')
60
DOMAIN_VALUE_REGEX = re.compile(
61
    r'^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,6}$',
62
    re.IGNORECASE)
63

64 65
READ_ONLY_FIELD_MSG = ("This value is provided by your authentication provider"
                       " and cannot be changed.")
66

67
class LocalUserCreationForm(UserCreationForm):
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
68 69
    """
    Extends the built in UserCreationForm in several ways:
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
70

71 72
    * Adds email, first_name, last_name, recaptcha_challenge_field,
    * recaptcha_response_field field.
73
    * The username field isn't visible and it is assigned a generated id.
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
74
    * User created is not active.
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
75
    """
76
    recaptcha_challenge_field = forms.CharField(widget=DummyWidget)
77 78
    recaptcha_response_field = forms.CharField(
        widget=RecaptchaWidget, label='')
79
    email = EmailField()
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
80

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
81 82
    class Meta:
        model = AstakosUser
83 84
        fields = ("email", "first_name", "last_name",
                  "has_signed_terms", "has_signed_terms")
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
85

86 87
    def __init__(self, *args, **kwargs):
        """
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
88
        Changes the order of fields, and removes the username field.
89
        """
90
        request = kwargs.pop('request', None)
91 92 93 94 95 96
        provider = kwargs.pop('provider', 'local')

        # we only use LocalUserCreationForm for local provider
        if not provider == 'local':
            raise Exception('Invalid provider')

97
        self.ip = None
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
98 99
        if request:
            self.ip = request.META.get('REMOTE_ADDR',
100 101
                                       request.META.get('HTTP_X_REAL_IP',
                                                        None))
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
102

103
        super(LocalUserCreationForm, self).__init__(*args, **kwargs)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
104
        self.fields.keyOrder = ['email', 'first_name', 'last_name',
105
                                'password1', 'password2']
Olga Brani's avatar
Olga Brani committed
106

107
        if settings.RECAPTCHA_ENABLED:
108
            self.fields.keyOrder.extend(['recaptcha_challenge_field',
109
                                         'recaptcha_response_field', ])
Olga Brani's avatar
Olga Brani committed
110 111
        if get_latest_terms():
            self.fields.keyOrder.append('has_signed_terms')
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
112 113 114 115 116

        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>' \
117
                % (reverse('latest_terms'), _("the terms"))
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
118
            self.fields['has_signed_terms'].label = \
119
                mark_safe("I agree with %s" % terms_link_html)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
120

121
    def clean_email(self):
122
        email = self.cleaned_data['email']
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
123
        if not email:
Olga Brani's avatar
Olga Brani committed
124
            raise forms.ValidationError(_(astakos_messages.REQUIRED_FIELD))
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
125
        if reserved_verified_email(email):
Olga Brani's avatar
Olga Brani committed
126
            raise forms.ValidationError(_(astakos_messages.EMAIL_USED))
127
        return email
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
128

129 130 131
    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
132
            raise forms.ValidationError(_(astakos_messages.SIGN_TERMS))
133
        return has_signed_terms
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
134

135 136 137 138 139 140 141 142 143 144 145 146 147
    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']
148 149
        check = captcha.submit(
            rcf, rrf, settings.RECAPTCHA_PRIVATE_KEY, self.ip)
150
        if not check.is_valid:
151 152
            raise forms.ValidationError(_(
                astakos_messages.CAPTCHA_VALIDATION_ERR))
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
153

154 155 156 157 158 159 160 161 162 163 164
    def create_user(self):
        try:
            data = self.cleaned_data
        except AttributeError:
            self.is_valid()
            data = self.cleaned_data

        user = auth.make_local_user(
            email=data['email'], password=data['password1'],
            first_name=data['first_name'], last_name=data['last_name'],
            has_signed_terms=True)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
165
        return user
166

167

168
class ThirdPartyUserCreationForm(forms.ModelForm):
169
    email = EmailField(
Olga Brani's avatar
Olga Brani committed
170
        label='Contact email',
171 172
        help_text='This is needed for contact purposes. '
        'It doesn&#39;t need to be the same with the one you '
Olga Brani's avatar
Olga Brani committed
173
        'provided to login previously. '
Olga Brani's avatar
Olga Brani committed
174
    )
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
175

176 177
    ro_fields = []

178 179
    class Meta:
        model = AstakosUser
180
        fields = ['email', 'first_name', 'last_name', 'has_signed_terms']
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
181

182 183 184 185
    def __init__(self, *args, **kwargs):
        """
        Changes the order of fields, and removes the username field.
        """
186 187

        self.provider = kwargs.pop('provider', None)
188
        self.request = kwargs.pop('request', None)
189 190 191 192 193 194 195 196 197
        if not self.provider or self.provider == 'local':
            raise Exception('Invalid provider, %r' % self.provider)

        # ThirdPartyUserCreationForm should always get instantiated with
        # a third_party_token value
        self.third_party_token = kwargs.pop('third_party_token', None)
        if not self.third_party_token:
            raise Exception('ThirdPartyUserCreationForm'
                            ' requires third_party_token')
198

199
        super(ThirdPartyUserCreationForm, self).__init__(*args, **kwargs)
200

201 202 203
        if not get_latest_terms():
            del self.fields['has_signed_terms']

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
204 205 206 207
        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>' \
208
                % (reverse('latest_terms'), _("the terms"))
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
209
            self.fields['has_signed_terms'].label = \
210
                mark_safe("I agree with %s" % terms_link_html)
211

212 213 214 215 216 217 218 219
        auth_provider = auth_providers.get_provider(self.provider)
        user_attr_map = auth_provider.get_user_attr_map()
        for field in ['email', 'first_name', 'last_name']:
            if not user_attr_map[field][1]:
                self.ro_fields.append(field)
                self.fields[field].widget.attrs['readonly'] = True
                self.fields[field].help_text = _(READ_ONLY_FIELD_MSG)

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
220
    def clean_email(self):
221
        email = self.cleaned_data['email']
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
222
        if not email:
Olga Brani's avatar
Olga Brani committed
223
            raise forms.ValidationError(_(astakos_messages.REQUIRED_FIELD))
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
224
        if reserved_verified_email(email):
225
            provider_id = self.provider
226 227
            provider = auth_providers.get_provider(provider_id)
            extra_message = provider.get_add_to_existing_account_msg
228

229 230
            raise forms.ValidationError(mark_safe(
                _(astakos_messages.EMAIL_USED) + ' ' + extra_message))
231
        return email
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
232

233 234 235 236 237 238 239 240 241 242
    def clean_first_name(self):
        if 'first_name' in self.ro_fields:
            return self.initial['first_name']
        return self.cleaned_data['first_name']

    def clean_last_name(self):
        if 'last_name' in self.ro_fields:
            return self.initial['last_name']
        return self.cleaned_data['last_name']

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
243 244 245
    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
246
            raise forms.ValidationError(_(astakos_messages.SIGN_TERMS))
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
247
        return has_signed_terms
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
248

249 250 251
    def _get_pending_user(self):
        return PendingThirdPartyUser.objects.get(token=self.third_party_token)

252 253 254 255 256 257 258 259 260 261 262
    def create_user(self):
        try:
            data = self.cleaned_data
        except AttributeError:
            self.is_valid()
            data = self.cleaned_data

        user = auth.make_user(
            email=data["email"],
            first_name=data["first_name"], last_name=data["last_name"],
            has_signed_terms=True)
263
        pending = self._get_pending_user()
264 265 266
        provider = pending.get_provider(user)
        provider.add_to_user()
        pending.delete()
267 268
        return user

269
autofocus_widget = forms.TextInput(attrs={'autofocus': 'autofocus'})
270

271
class LoginForm(AuthenticationForm):
272
    username = EmailField(label=_("Email"), widget=autofocus_widget)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
273
    recaptcha_challenge_field = forms.CharField(widget=DummyWidget)
274 275
    recaptcha_response_field = forms.CharField(
        widget=RecaptchaWidget, label='')
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
276

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
277 278 279 280
    def __init__(self, *args, **kwargs):
        was_limited = kwargs.get('was_limited', False)
        request = kwargs.get('request', None)
        if request:
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
281 282 283
            self.ip = request.META.get(
                'REMOTE_ADDR',
                request.META.get('HTTP_X_REAL_IP', None))
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
284

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
285 286 287 288 289
        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
290

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
291
        self.fields.keyOrder = ['username', 'password']
292
        if was_limited and settings.RECAPTCHA_ENABLED:
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
293
            self.fields.keyOrder.extend(['recaptcha_challenge_field',
294
                                         'recaptcha_response_field', ])
Olga Brani's avatar
Olga Brani committed
295 296

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

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
299 300 301 302 303 304 305 306 307 308 309 310 311
    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']
312 313
        check = captcha.submit(
            rcf, rrf, settings.RECAPTCHA_PRIVATE_KEY, self.ip)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
314
        if not check.is_valid:
315 316
            raise forms.ValidationError(_(
                astakos_messages.CAPTCHA_VALIDATION_ERR))
317

318
    def clean(self):
319 320 321
        """
        Override default behavior in order to check user's activation later
        """
322 323
        username = self.cleaned_data.get('username')

324 325 326 327
        if username:
            try:
                user = AstakosUser.objects.get_by_identifier(username)
                if not user.has_auth_provider('local'):
328
                    provider = auth_providers.get_provider('local', user)
329 330
                    msg = provider.get_login_disabled_msg
                    raise forms.ValidationError(mark_safe(msg))
331 332
            except AstakosUser.DoesNotExist:
                pass
333

334 335
        try:
            super(LoginForm, self).clean()
336
        except forms.ValidationError:
337 338 339
            if self.user_cache is None:
                raise
            if not self.user_cache.is_active:
340 341
                msg = self.user_cache.get_inactive_message('local')
                raise forms.ValidationError(msg)
342 343 344
            if self.request:
                if not self.request.session.test_cookie_worked():
                    raise
345
        return self.cleaned_data
346

347

348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396
class LDAPLoginForm(LoginForm):
    """Login form for LDAP Authentication Provider.

    * Inherits from 'LoginForm' in order to inherit recaptcha handling.
    * Overrides username to be an arbitraty string rather than an email.
    * Overrides clean method

    """
    username = forms.CharField(label=_('Identifier'))

    def clean(self):
        username = self.cleaned_data.get('username')
        password = self.cleaned_data.get('password')

        if username:
            try:
                user = AstakosUser.objects.get_by_identifier(username)
                if not user.has_auth_provider('ldap'):
                    provider = auth_providers.get_provider('ldap', user)
                    msg = provider.get_login_disabled_msg
                    raise forms.ValidationError(mark_safe(msg))
            except AstakosUser.DoesNotExist:
                pass

        # Set user cache to None, so that methods of 'AuthenticationForm'
        # work
        self.user_cache = None

        if username and password:
            self.ldap_user_cache = LDAPBackend().authenticate(username=username,
                                                              password=password)
            if self.ldap_user_cache is None:
                if self.request:
                    if not self.request.session.test_cookie_worked():
                        raise
                raise forms.ValidationError(
                    self.error_messages['invalid_login'])
        self.check_for_test_cookie()
        return self.cleaned_data

    def get_ldap_user_id(self):
        if self.ldap_user_cache:
            return self.ldap_user_cache.id
        return None

    def get_ldap_user(self):
        return self.ldap_user_cache


Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
397 398 399
class ProfileForm(forms.ModelForm):
    """
    Subclass of ``ModelForm`` for permiting user to edit his/her profile.
Olga Brani's avatar
Olga Brani committed
400 401
    Most of the fields are readonly since the user is not allowed to change
    them.
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
402

Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
403 404
    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
405
    """
406 407
    email = EmailField(label='E-mail address',
                       help_text='E-mail address')
408
    renew = forms.BooleanField(label='Renew token', required=False)
409
    ro_fields = ['email']
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
410

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
411 412
    class Meta:
        model = AstakosUser
413
        fields = ('email', 'first_name', 'last_name')
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
414

415
    def __init__(self, *args, **kwargs):
416
        self.session_key = kwargs.pop('session_key', None)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
417 418 419
        super(ProfileForm, self).__init__(*args, **kwargs)
        instance = getattr(self, 'instance', None)
        if instance and instance.id:
420 421 422 423 424
            if not instance.can_change_first_name():
                self.ro_fields.append('first_name')
            if not instance.can_change_last_name():
                self.ro_fields.append('last_name')
            for field in self.ro_fields:
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
425
                self.fields[field].widget.attrs['readonly'] = True
426
                self.fields[field].help_text = _(READ_ONLY_FIELD_MSG)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
427

428 429 430
    def clean_email(self):
        return self.instance.email

431 432 433 434 435 436 437 438 439 440
    def clean_first_name(self):
        if 'first_name' in self.ro_fields:
            return self.initial['first_name']
        return self.cleaned_data['first_name']

    def clean_last_name(self):
        if 'last_name' in self.ro_fields:
            return self.initial['last_name']
        return self.cleaned_data['last_name']

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
441 442
    def save(self, commit=True, **kwargs):
        user = super(ProfileForm, self).save(commit=False, **kwargs)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
443
        user.is_verified = True
444
        if self.cleaned_data.get('renew'):
445 446 447 448
            user.renew_token(
                flush_sessions=True,
                current_key=self.session_key
            )
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
449
        if commit:
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
450
            user.save(**kwargs)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
451
        return user
452

453

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
454 455 456 457
class FeedbackForm(forms.Form):
    """
    Form for writing feedback.
    """
458
    feedback_msg = forms.CharField(widget=forms.Textarea, label=u'Message')
459 460
    feedback_data = forms.CharField(widget=forms.HiddenInput(), label='',
                                    required=False)
461

462

463 464 465 466
class SendInvitationForm(forms.Form):
    """
    Form for sending an invitations
    """
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
467

468 469 470
    email = EmailField(required=True, label='Email address')
    first_name = EmailField(label='First name')
    last_name = EmailField(label='Last name')
471

472 473 474

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

477 478
    save method: to pass a custom from_email in send_mail.
    clean_email: to handle local auth provider checks
479
    """
480
    def clean_email(self):
481 482 483
        # we override the default django auth clean_email to provide more
        # detailed messages in case of inactive users
        email = self.cleaned_data['email']
484
        try:
485
            user = AstakosUser.objects.get_by_identifier(email)
486
            self.users_cache = [user]
487
            if not user.is_active:
488 489 490 491 492
                if not user.has_auth_provider('local', auth_backend='astakos'):
                    provider = auth_providers.get_provider('local', user)
                    msg = mark_safe(provider.get_unusable_password_msg)
                    raise forms.ValidationError(msg)

493 494
                msg = mark_safe(user.get_inactive_message('local'))
                raise forms.ValidationError(msg)
495

496
            provider = auth_providers.get_provider('local', user)
497
            if not user.has_usable_password():
498
                msg = provider.get_unusable_password_msg
499
                raise forms.ValidationError(mark_safe(msg))
500 501

            if not user.can_change_password():
502
                msg = provider.get_cannot_change_password_msg
503
                raise forms.ValidationError(mark_safe(msg))
504 505

        except AstakosUser.DoesNotExist:
Olga Brani's avatar
Olga Brani committed
506
            raise forms.ValidationError(_(astakos_messages.EMAIL_UNKNOWN))
507
        return email
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
508

Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
509 510 511
    def save(self, domain_override=None,
             email_template_name='registration/password_reset_email.html',
             use_https=False, token_generator=default_token_generator,
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
512
             request=None, **kwargs):
513
        """
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
514 515
        Generates a one-use only link for resetting password and sends to the
        user.
516

517 518
        """
        for user in self.users_cache:
519
            url = user.astakosuser.get_password_reset_url(token_generator)
520
            url = join_urls(settings.BASE_HOST, url)
521 522
            c = {
                'email': user.email,
523
                'url': url,
524
                'user': user,
525
                'baseurl': settings.BASE_URL,
526
                'support': settings.CONTACT_EMAIL
527
            }
528
            message = render_to_string(email_template_name, c)
529
            from_email = settings.SERVER_EMAIL
530
            send_mail(_(astakos_messages.PASSWORD_RESET_EMAIL_SUBJECT),
531
                      message,
532 533
                      from_email,
                      [user.email],
534
                      connection=get_connection())
535

536

537
class EmailChangeForm(forms.ModelForm):
538

539 540
    new_email_address = EmailField()

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

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

551 552
    def save(self):
        raise NotImplementedError
553

554

555
class SignApprovalTermsForm(forms.ModelForm):
556

557 558 559
    class Meta:
        model = AstakosUser
        fields = ("has_signed_terms",)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
560

561 562
    def __init__(self, *args, **kwargs):
        super(SignApprovalTermsForm, self).__init__(*args, **kwargs)
563
        self.fields['has_signed_terms'].label = _("I agree with the terms")
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
564

565 566 567
    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
568
            raise forms.ValidationError(_(astakos_messages.SIGN_TERMS))
569
        return has_signed_terms
570

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
571 572
    def save(self, commit=True, **kwargs):
        user = super(SignApprovalTermsForm, self).save(commit=commit, **kwargs)
573 574
        user.date_signed_terms = datetime.now()
        if commit:
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
575
            user.save(**kwargs)
576 577
        return user

578

579
class InvitationForm(forms.ModelForm):
580

581
    username = EmailField(label=_("Email"))
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
582

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
583 584
    def __init__(self, *args, **kwargs):
        super(InvitationForm, self).__init__(*args, **kwargs)
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
585

586 587 588
    class Meta:
        model = Invitation
        fields = ('username', 'realname')
Kostas Papadimitriou's avatar
Kostas Papadimitriou committed
589

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

600

601 602 603 604 605
class ExtendedPasswordChangeForm(PasswordChangeForm):
    """
    Extends PasswordChangeForm by enabling user
    to optionally renew also the token.
    """
606
    if not settings.NEWPASSWD_INVALIDATE_TOKEN:
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
607 608 609 610
        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
611

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

Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
616
    def save(self, commit=True, **kwargs):
617
        try:
618 619
            if settings.NEWPASSWD_INVALIDATE_TOKEN or \
                    self.cleaned_data.get('renew'):
620 621 622 623 624
                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,
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
626
                                                            **kwargs)
627

Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
628

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

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

    @transaction.commit_on_success()
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
645
    def save(self, commit=True, **kwargs):
646 647
        try:
            self.user = AstakosUser.objects.get(id=self.user.id)
648 649
            if settings.NEWPASSWD_INVALIDATE_TOKEN or \
                    self.cleaned_data.get('renew'):
650
                self.user.renew_token()
651 652 653 654

            provider = auth_providers.get_provider('local', self.user)
            if provider.get_add_policy:
                provider.add_to_user()
655

656 657
        except BaseException, e:
            logger.exception(e)
658
        return super(ExtendedSetPasswordForm, self).save(commit=commit,
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
659
                                                         **kwargs)
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
660 661


Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
662
app_name_label = "Project name"
663
app_name_placeholder = _("myproject.mylab.ntua.gr")
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
664 665 666 667
app_name_validator = validators.RegexValidator(
    DOMAIN_VALUE_REGEX,
    _(astakos_messages.DOMAIN_VALUE_ERR),
    'invalid')
668 669 670 671
base_app_name_validator = validators.RegexValidator(
    BASE_PROJECT_NAME_REGEX,
    _(astakos_messages.BASE_PROJECT_NAME_ERR),
    'invalid')
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
672
app_name_help = _("""
673
        The project's name should be in a domain format.
674 675 676 677
        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""")
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
678 679
app_name_widget = forms.TextInput(
    attrs={'placeholder': app_name_placeholder})
680 681


Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
682 683 684
app_home_label = "Homepage URL"
app_home_placeholder = 'myinstitution.org/myproject/'
app_home_help = _("""
685
        URL pointing at your project's site.
Olga Brani's avatar
Olga Brani committed
686
        e.g.: myinstitution.org/myproject/.
687
        Leave blank if there is no website.""")
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
688 689
app_home_widget = forms.TextInput(
    attrs={'placeholder': app_home_placeholder})
690

Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
691 692
app_desc_label = _("Description")
app_desc_help = _("""
Georgios D. Tsoukalas's avatar
Georgios D. Tsoukalas committed
693
        Please provide a short but descriptive abstract of your
694 695
        project, so that anyone searching can quickly understand
        what this project is about.""")
696

Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
697 698
app_comment_label = _("Comments for review (private)")
app_comment_help = _("""
699 700 701 702
        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
Georgios D. Tsoukalas's avatar
Georgios D. Tsoukalas committed
703
        and will not be made public.""")
704

Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
705 706
app_start_date_label = _("Start date")
app_start_date_help = _("""
707 708 709 710
        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.""")

Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
711 712
app_end_date_label = _("Termination date")
app_end_date_help = _("""
713
        At this date, the project will be automatically terminated
Georgios D. Tsoukalas's avatar
Georgios D. Tsoukalas committed
714 715
        and its resource grants revoked from all members. If you are
        not certain, it is best to start with a conservative estimation.
716 717
        You can always re-apply for an extension, if you need.""")

Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
718 719
join_policy_label = _("Joining policy")
app_member_join_policy_help = _("""
Georgios D. Tsoukalas's avatar
Georgios D. Tsoukalas committed
720
        Select how new members are accepted into the project.""")
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
721 722
leave_policy_label = _("Leaving policy")
app_member_leave_policy_help = _("""
Georgios D. Tsoukalas's avatar
Georgios D. Tsoukalas committed
723
        Select how new members can leave the project.""")
724

725
max_members_label = _("Max members")
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
726
max_members_help = _("""
727 728 729
        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.
Georgios D. Tsoukalas's avatar
Georgios D. Tsoukalas committed
730 731
        If you are not certain, it is best to start with a conservative
        limit. You can always request a raise when you need it.""")
732

733 734
join_policies = presentation.PROJECT_MEMBER_JOIN_POLICIES.items()
leave_policies = presentation.PROJECT_MEMBER_LEAVE_POLICIES.items()
735

Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
736

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
737
class ProjectApplicationForm(forms.ModelForm):
738

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
739
    name = forms.CharField(
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
740 741 742 743
        label=app_name_label,
        help_text=app_name_help,
        widget=app_name_widget,
        validators=[app_name_validator])
744

745
    homepage = forms.URLField(
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
746 747 748 749
        label=app_home_label,
        help_text=app_home_help,
        widget=app_home_widget,
        required=False)
750 751

    description = forms.CharField(
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
752 753 754 755
        label=app_desc_label,
        help_text=app_desc_help,
        widget=forms.Textarea,
        required=False)
756 757

    comments = forms.CharField(
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
758 759 760 761
        label=app_comment_label,
        help_text=app_comment_help,
        widget=forms.Textarea,
        required=False)
762 763

    start_date = forms.DateTimeField(
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
764 765 766
        label=app_start_date_label,
        help_text=app_start_date_help,
        required=False)
767 768

    end_date = forms.DateTimeField(
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
769 770
        label=app_end_date_label,
        help_text=app_end_date_help)
771

Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
772 773 774 775 776 777
    member_join_policy = forms.TypedChoiceField(
        label=join_policy_label,
        help_text=app_member_join_policy_help,
        initial=2,
        coerce=int,
        choices=join_policies)
778

779
    member_leave_policy = forms.TypedChoiceField(
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
780 781 782 783
        label=leave_policy_label,
        help_text=app_member_leave_policy_help,
        coerce=int,
        choices=leave_policies)
784

785
    limit_on_members_number = InfiniteChoiceField(
786
        choices=settings.PROJECT_MEMBERS_LIMIT_CHOICES,
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
787 788
        label=max_members_label,
        help_text=max_members_help,
789 790
        initial="Unlimited",
        required=True)
791

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
792
    class Meta:
793
        model = ProjectApplication
Giorgos Korfiatis's avatar
Giorgos Korfiatis committed
794 795 796 797
        fields = ('name', 'homepage', 'description',
                  'start_date', 'end_date', 'comments',
                  'member_join_policy', 'member_leave_policy',
                  'limit_on_members_number')
798

799
    def __init__(self, *args, **kwargs):
800
        instance = kwargs.get('instance')
801

802
        super(ProjectApplicationForm, self).__init__(*args, **kwargs)
803
        # in case of new application remove closed join policy
804
        if not instance:
805 806
            policies = presentation.PROJECT_MEMBER_JOIN_POLICIES.copy()
            policies.pop(3)
807
            self.fields['member_join_policy'].choices = policies.iteritems()
808 809 810 811
        else:
            if instance.is_base:
                name_field = self.fields['name']
                name_field.validators = [base_app_name_validator]
812 813
            if self.initial['limit_on_members_number'] == \
                                                    units.PRACTICALLY_INFINITE:
814
                self.initial['limit_on_members_number'] = 'Unlimited'
815