Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / forms.py @ fb9d96e9

History | View | Annotate | Download (38.3 kB)

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

    
36
from django import forms
37
from django.utils.translation import ugettext as _
38
from django.contrib.auth.forms import UserCreationForm, AuthenticationForm, \
39
    PasswordResetForm, PasswordChangeForm, SetPasswordForm
40
from django.core.mail import send_mail, get_connection
41
from django.contrib.auth.tokens import default_token_generator
42
from django.core.urlresolvers import reverse
43
from django.utils.safestring import mark_safe
44
from django.utils.encoding import smart_str
45
from django.db import transaction
46
from django.core import validators
47

    
48
from synnefo.util import units
49
from synnefo_branding.utils import render_to_string
50
from synnefo.lib import join_urls
51
from astakos.im.fields import EmailField
52
from astakos.im.models import AstakosUser, EmailChange, Invitation, Resource, \
53
    PendingThirdPartyUser, get_latest_terms, ProjectApplication, Project
54
from astakos.im import presentation
55
from astakos.im.widgets import DummyWidget, RecaptchaWidget
56
from astakos.im.functions import send_change_email, submit_application, \
57
    accept_membership_project_checks, ProjectError
58

    
59
from astakos.im.util import reserved_verified_email, model_to_dict
60
from astakos.im import auth_providers
61
from astakos.im import settings
62
from astakos.im import auth
63

    
64
import astakos.im.messages as astakos_messages
65

    
66
import logging
67
import hashlib
68
import recaptcha.client.captcha as captcha
69
import re
70

    
71
logger = logging.getLogger(__name__)
72

    
73
DOMAIN_VALUE_REGEX = re.compile(
74
    r'^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,6}$',
75
    re.IGNORECASE)
76

    
77

    
78
class LocalUserCreationForm(UserCreationForm):
79
    """
80
    Extends the built in UserCreationForm in several ways:
81

82
    * Adds email, first_name, last_name, recaptcha_challenge_field,
83
    * recaptcha_response_field field.
84
    * The username field isn't visible and it is assigned a generated id.
85
    * User created is not active.
86
    """
87
    recaptcha_challenge_field = forms.CharField(widget=DummyWidget)
88
    recaptcha_response_field = forms.CharField(
89
        widget=RecaptchaWidget, label='')
90
    email = EmailField()
91

    
92
    class Meta:
93
        model = AstakosUser
94
        fields = ("email", "first_name", "last_name",
95
                  "has_signed_terms", "has_signed_terms")
96

    
97
    def __init__(self, *args, **kwargs):
98
        """
99
        Changes the order of fields, and removes the username field.
100
        """
101
        request = kwargs.pop('request', None)
102
        provider = kwargs.pop('provider', 'local')
103

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

    
108
        self.ip = None
109
        if request:
110
            self.ip = request.META.get('REMOTE_ADDR',
111
                                       request.META.get('HTTP_X_REAL_IP',
112
                                                        None))
113

    
114
        super(LocalUserCreationForm, self).__init__(*args, **kwargs)
115
        self.fields.keyOrder = ['email', 'first_name', 'last_name',
116
                                'password1', 'password2']
117

    
118
        if settings.RECAPTCHA_ENABLED:
119
            self.fields.keyOrder.extend(['recaptcha_challenge_field',
120
                                         'recaptcha_response_field', ])
121
        if get_latest_terms():
122
            self.fields.keyOrder.append('has_signed_terms')
123

    
124
        if 'has_signed_terms' in self.fields:
125
            # Overriding field label since we need to apply a link
126
            # to the terms within the label
127
            terms_link_html = '<a href="%s" target="_blank">%s</a>' \
128
                % (reverse('latest_terms'), _("the terms"))
129
            self.fields['has_signed_terms'].label = \
130
                mark_safe("I agree with %s" % terms_link_html)
131

    
132
    def clean_email(self):
133
        email = self.cleaned_data['email']
134
        if not email:
135
            raise forms.ValidationError(_(astakos_messages.REQUIRED_FIELD))
136
        if reserved_verified_email(email):
137
            raise forms.ValidationError(_(astakos_messages.EMAIL_USED))
138
        return email
139

    
140
    def clean_has_signed_terms(self):
141
        has_signed_terms = self.cleaned_data['has_signed_terms']
142
        if not has_signed_terms:
143
            raise forms.ValidationError(_(astakos_messages.SIGN_TERMS))
144
        return has_signed_terms
145

    
146
    def clean_recaptcha_response_field(self):
147
        if 'recaptcha_challenge_field' in self.cleaned_data:
148
            self.validate_captcha()
149
        return self.cleaned_data['recaptcha_response_field']
150

    
151
    def clean_recaptcha_challenge_field(self):
152
        if 'recaptcha_response_field' in self.cleaned_data:
153
            self.validate_captcha()
154
        return self.cleaned_data['recaptcha_challenge_field']
155

    
156
    def validate_captcha(self):
157
        rcf = self.cleaned_data['recaptcha_challenge_field']
158
        rrf = self.cleaned_data['recaptcha_response_field']
159
        check = captcha.submit(
160
            rcf, rrf, settings.RECAPTCHA_PRIVATE_KEY, self.ip)
161
        if not check.is_valid:
162
            raise forms.ValidationError(_(
163
                astakos_messages.CAPTCHA_VALIDATION_ERR))
164

    
165
    def create_user(self):
166
        try:
167
            data = self.cleaned_data
168
        except AttributeError:
169
            self.is_valid()
170
            data = self.cleaned_data
171

    
172
        user = auth.make_local_user(
173
            email=data['email'], password=data['password1'],
174
            first_name=data['first_name'], last_name=data['last_name'],
175
            has_signed_terms=True)
176
        return user
177

    
178

    
179
class ThirdPartyUserCreationForm(forms.ModelForm):
180
    email = EmailField(
181
        label='Contact email',
182
        help_text='This is needed for contact purposes. '
183
        'It doesn&#39;t need to be the same with the one you '
184
        'provided to login previously. '
185
    )
186

    
187
    class Meta:
188
        model = AstakosUser
189
        fields = ['email', 'first_name', 'last_name', 'has_signed_terms']
190

    
191
    def __init__(self, *args, **kwargs):
192
        """
193
        Changes the order of fields, and removes the username field.
194
        """
195

    
196
        self.provider = kwargs.pop('provider', None)
197
        self.request = kwargs.pop('request', None)
198
        if not self.provider or self.provider == 'local':
199
            raise Exception('Invalid provider, %r' % self.provider)
200

    
201
        # ThirdPartyUserCreationForm should always get instantiated with
202
        # a third_party_token value
203
        self.third_party_token = kwargs.pop('third_party_token', None)
204
        if not self.third_party_token:
205
            raise Exception('ThirdPartyUserCreationForm'
206
                            ' requires third_party_token')
207

    
208
        super(ThirdPartyUserCreationForm, self).__init__(*args, **kwargs)
209

    
210
        if not get_latest_terms():
211
            del self.fields['has_signed_terms']
212

    
213
        if 'has_signed_terms' in self.fields:
214
            # Overriding field label since we need to apply a link
215
            # to the terms within the label
216
            terms_link_html = '<a href="%s" target="_blank">%s</a>' \
217
                % (reverse('latest_terms'), _("the terms"))
218
            self.fields['has_signed_terms'].label = \
219
                mark_safe("I agree with %s" % terms_link_html)
220

    
221
    def clean_email(self):
222
        email = self.cleaned_data['email']
223
        if not email:
224
            raise forms.ValidationError(_(astakos_messages.REQUIRED_FIELD))
225
        if reserved_verified_email(email):
226
            provider_id = self.provider
227
            provider = auth_providers.get_provider(provider_id)
228
            extra_message = provider.get_add_to_existing_account_msg
229

    
230
            raise forms.ValidationError(mark_safe(
231
                _(astakos_messages.EMAIL_USED) + ' ' + extra_message))
232
        return email
233

    
234
    def clean_has_signed_terms(self):
235
        has_signed_terms = self.cleaned_data['has_signed_terms']
236
        if not has_signed_terms:
237
            raise forms.ValidationError(_(astakos_messages.SIGN_TERMS))
238
        return has_signed_terms
239

    
240
    def _get_pending_user(self):
241
        return PendingThirdPartyUser.objects.get(token=self.third_party_token)
242

    
243
    def create_user(self):
244
        try:
245
            data = self.cleaned_data
246
        except AttributeError:
247
            self.is_valid()
248
            data = self.cleaned_data
249

    
250
        user = auth.make_user(
251
            email=data["email"],
252
            first_name=data["first_name"], last_name=data["last_name"],
253
            has_signed_terms=True)
254
        pending = self._get_pending_user()
255
        provider = pending.get_provider(user)
256
        provider.add_to_user()
257
        pending.delete()
258
        return user
259

    
260

    
261
class LoginForm(AuthenticationForm):
262
    username = EmailField(label=_("Email"))
263
    recaptcha_challenge_field = forms.CharField(widget=DummyWidget)
264
    recaptcha_response_field = forms.CharField(
265
        widget=RecaptchaWidget, label='')
266

    
267
    def __init__(self, *args, **kwargs):
268
        was_limited = kwargs.get('was_limited', False)
269
        request = kwargs.get('request', None)
270
        if request:
271
            self.ip = request.META.get(
272
                'REMOTE_ADDR',
273
                request.META.get('HTTP_X_REAL_IP', None))
274

    
275
        t = ('request', 'was_limited')
276
        for elem in t:
277
            if elem in kwargs.keys():
278
                kwargs.pop(elem)
279
        super(LoginForm, self).__init__(*args, **kwargs)
280

    
281
        self.fields.keyOrder = ['username', 'password']
282
        if was_limited and settings.RECAPTCHA_ENABLED:
283
            self.fields.keyOrder.extend(['recaptcha_challenge_field',
284
                                         'recaptcha_response_field', ])
285

    
286
    def clean_username(self):
287
        return self.cleaned_data['username'].lower()
288

    
289
    def clean_recaptcha_response_field(self):
290
        if 'recaptcha_challenge_field' in self.cleaned_data:
291
            self.validate_captcha()
292
        return self.cleaned_data['recaptcha_response_field']
293

    
294
    def clean_recaptcha_challenge_field(self):
295
        if 'recaptcha_response_field' in self.cleaned_data:
296
            self.validate_captcha()
297
        return self.cleaned_data['recaptcha_challenge_field']
298

    
299
    def validate_captcha(self):
300
        rcf = self.cleaned_data['recaptcha_challenge_field']
301
        rrf = self.cleaned_data['recaptcha_response_field']
302
        check = captcha.submit(
303
            rcf, rrf, settings.RECAPTCHA_PRIVATE_KEY, self.ip)
304
        if not check.is_valid:
305
            raise forms.ValidationError(_(
306
                astakos_messages.CAPTCHA_VALIDATION_ERR))
307

    
308
    def clean(self):
309
        """
310
        Override default behavior in order to check user's activation later
311
        """
312
        username = self.cleaned_data.get('username')
313

    
314
        if username:
315
            try:
316
                user = AstakosUser.objects.get_by_identifier(username)
317
                if not user.has_auth_provider('local'):
318
                    provider = auth_providers.get_provider('local', user)
319
                    msg = provider.get_login_disabled_msg
320
                    raise forms.ValidationError(mark_safe(msg))
321
            except AstakosUser.DoesNotExist:
322
                pass
323

    
324
        try:
325
            super(LoginForm, self).clean()
326
        except forms.ValidationError:
327
            if self.user_cache is None:
328
                raise
329
            if not self.user_cache.is_active:
330
                msg = self.user_cache.get_inactive_message('local')
331
                raise forms.ValidationError(msg)
332
            if self.request:
333
                if not self.request.session.test_cookie_worked():
334
                    raise
335
        return self.cleaned_data
336

    
337

    
338
class ProfileForm(forms.ModelForm):
339
    """
340
    Subclass of ``ModelForm`` for permiting user to edit his/her profile.
341
    Most of the fields are readonly since the user is not allowed to change
342
    them.
343

344
    The class defines a save method which sets ``is_verified`` to True so as
345
    the user during the next login will not to be redirected to profile page.
346
    """
347
    email = EmailField(label='E-mail address',
348
                       help_text='E-mail address')
349
    renew = forms.BooleanField(label='Renew token', required=False)
350

    
351
    class Meta:
352
        model = AstakosUser
353
        fields = ('email', 'first_name', 'last_name')
354

    
355
    def __init__(self, *args, **kwargs):
356
        self.session_key = kwargs.pop('session_key', None)
357
        super(ProfileForm, self).__init__(*args, **kwargs)
358
        instance = getattr(self, 'instance', None)
359
        ro_fields = ('email',)
360
        if instance and instance.id:
361
            for field in ro_fields:
362
                self.fields[field].widget.attrs['readonly'] = True
363

    
364
    def clean_email(self):
365
        return self.instance.email
366

    
367
    def save(self, commit=True, **kwargs):
368
        user = super(ProfileForm, self).save(commit=False, **kwargs)
369
        user.is_verified = True
370
        if self.cleaned_data.get('renew'):
371
            user.renew_token(
372
                flush_sessions=True,
373
                current_key=self.session_key
374
            )
375
        if commit:
376
            user.save(**kwargs)
377
        return user
378

    
379

    
380
class FeedbackForm(forms.Form):
381
    """
382
    Form for writing feedback.
383
    """
384
    feedback_msg = forms.CharField(widget=forms.Textarea, label=u'Message')
385
    feedback_data = forms.CharField(widget=forms.HiddenInput(), label='',
386
                                    required=False)
387

    
388

    
389
class SendInvitationForm(forms.Form):
390
    """
391
    Form for sending an invitations
392
    """
393

    
394
    email = EmailField(required=True, label='Email address')
395
    first_name = EmailField(label='First name')
396
    last_name = EmailField(label='Last name')
397

    
398

    
399
class ExtendedPasswordResetForm(PasswordResetForm):
400
    """
401
    Extends PasswordResetForm by overriding
402

403
    save method: to pass a custom from_email in send_mail.
404
    clean_email: to handle local auth provider checks
405
    """
406
    def clean_email(self):
407
        # we override the default django auth clean_email to provide more
408
        # detailed messages in case of inactive users
409
        email = self.cleaned_data['email']
410
        try:
411
            user = AstakosUser.objects.get_by_identifier(email)
412
            self.users_cache = [user]
413
            if not user.is_active:
414
                if not user.has_auth_provider('local', auth_backend='astakos'):
415
                    provider = auth_providers.get_provider('local', user)
416
                    msg = mark_safe(provider.get_unusable_password_msg)
417
                    raise forms.ValidationError(msg)
418

    
419
                msg = mark_safe(user.get_inactive_message('local'))
420
                raise forms.ValidationError(msg)
421

    
422
            provider = auth_providers.get_provider('local', user)
423
            if not user.has_usable_password():
424
                msg = provider.get_unusable_password_msg
425
                raise forms.ValidationError(mark_safe(msg))
426

    
427
            if not user.can_change_password():
428
                msg = provider.get_cannot_change_password_msg
429
                raise forms.ValidationError(mark_safe(msg))
430

    
431
        except AstakosUser.DoesNotExist:
432
            raise forms.ValidationError(_(astakos_messages.EMAIL_UNKNOWN))
433
        return email
434

    
435
    def save(self, domain_override=None,
436
             email_template_name='registration/password_reset_email.html',
437
             use_https=False, token_generator=default_token_generator,
438
             request=None, **kwargs):
439
        """
440
        Generates a one-use only link for resetting password and sends to the
441
        user.
442

443
        """
444
        for user in self.users_cache:
445
            url = user.astakosuser.get_password_reset_url(token_generator)
446
            url = join_urls(settings.BASE_HOST, url)
447
            c = {
448
                'email': user.email,
449
                'url': url,
450
                'site_name': settings.SITENAME,
451
                'user': user,
452
                'baseurl': settings.BASE_URL,
453
                'support': settings.CONTACT_EMAIL
454
            }
455
            message = render_to_string(email_template_name, c)
456
            from_email = settings.SERVER_EMAIL
457
            send_mail(_(astakos_messages.PASSWORD_RESET_EMAIL_SUBJECT),
458
                      message,
459
                      from_email,
460
                      [user.email],
461
                      connection=get_connection())
462

    
463

    
464
class EmailChangeForm(forms.ModelForm):
465

    
466
    new_email_address = EmailField()
467

    
468
    class Meta:
469
        model = EmailChange
470
        fields = ('new_email_address',)
471

    
472
    def clean_new_email_address(self):
473
        addr = self.cleaned_data['new_email_address']
474
        if reserved_verified_email(addr):
475
            raise forms.ValidationError(_(astakos_messages.EMAIL_USED))
476
        return addr
477

    
478
    def save(self, request,
479
             email_template_name='registration/email_change_email.txt',
480
             commit=True, **kwargs):
481
        ec = super(EmailChangeForm, self).save(commit=False, **kwargs)
482
        ec.user = request.user
483
        # delete pending email changes
484
        request.user.emailchanges.all().delete()
485

    
486
        activation_key = hashlib.sha1(
487
            str(random()) + smart_str(ec.new_email_address))
488
        ec.activation_key = activation_key.hexdigest()
489
        if commit:
490
            ec.save(**kwargs)
491
        send_change_email(ec, request, email_template_name=email_template_name)
492

    
493

    
494
class SignApprovalTermsForm(forms.ModelForm):
495

    
496
    class Meta:
497
        model = AstakosUser
498
        fields = ("has_signed_terms",)
499

    
500
    def __init__(self, *args, **kwargs):
501
        super(SignApprovalTermsForm, self).__init__(*args, **kwargs)
502

    
503
    def clean_has_signed_terms(self):
504
        has_signed_terms = self.cleaned_data['has_signed_terms']
505
        if not has_signed_terms:
506
            raise forms.ValidationError(_(astakos_messages.SIGN_TERMS))
507
        return has_signed_terms
508

    
509
    def save(self, commit=True, **kwargs):
510
        user = super(SignApprovalTermsForm, self).save(commit=commit, **kwargs)
511
        user.date_signed_terms = datetime.now()
512
        if commit:
513
            user.save(**kwargs)
514
        return user
515

    
516

    
517
class InvitationForm(forms.ModelForm):
518

    
519
    username = EmailField(label=_("Email"))
520

    
521
    def __init__(self, *args, **kwargs):
522
        super(InvitationForm, self).__init__(*args, **kwargs)
523

    
524
    class Meta:
525
        model = Invitation
526
        fields = ('username', 'realname')
527

    
528
    def clean_username(self):
529
        username = self.cleaned_data['username']
530
        try:
531
            Invitation.objects.get(username=username)
532
            raise forms.ValidationError(
533
                _(astakos_messages.INVITATION_EMAIL_EXISTS))
534
        except Invitation.DoesNotExist:
535
            pass
536
        return username
537

    
538

    
539
class ExtendedPasswordChangeForm(PasswordChangeForm):
540
    """
541
    Extends PasswordChangeForm by enabling user
542
    to optionally renew also the token.
543
    """
544
    if not settings.NEWPASSWD_INVALIDATE_TOKEN:
545
        renew = forms.BooleanField(
546
            label='Renew token', required=False,
547
            initial=True,
548
            help_text='Unsetting this may result in security risk.')
549

    
550
    def __init__(self, user, *args, **kwargs):
551
        self.session_key = kwargs.pop('session_key', None)
552
        super(ExtendedPasswordChangeForm, self).__init__(user, *args, **kwargs)
553

    
554
    def save(self, commit=True, **kwargs):
555
        try:
556
            if settings.NEWPASSWD_INVALIDATE_TOKEN or \
557
                    self.cleaned_data.get('renew'):
558
                self.user.renew_token()
559
            self.user.flush_sessions(current_key=self.session_key)
560
        except AttributeError:
561
            # if user model does has not such methods
562
            pass
563
        return super(ExtendedPasswordChangeForm, self).save(commit=commit,
564
                                                            **kwargs)
565

    
566

    
567
class ExtendedSetPasswordForm(SetPasswordForm):
568
    """
569
    Extends SetPasswordForm by enabling user
570
    to optionally renew also the token.
571
    """
572
    if not settings.NEWPASSWD_INVALIDATE_TOKEN:
573
        renew = forms.BooleanField(
574
            label='Renew token',
575
            required=False,
576
            initial=True,
577
            help_text='Unsetting this may result in security risk.')
578

    
579
    def __init__(self, user, *args, **kwargs):
580
        super(ExtendedSetPasswordForm, self).__init__(user, *args, **kwargs)
581

    
582
    @transaction.commit_on_success()
583
    def save(self, commit=True, **kwargs):
584
        try:
585
            self.user = AstakosUser.objects.get(id=self.user.id)
586
            if settings.NEWPASSWD_INVALIDATE_TOKEN or \
587
                    self.cleaned_data.get('renew'):
588
                self.user.renew_token()
589

    
590
            provider = auth_providers.get_provider('local', self.user)
591
            if provider.get_add_policy:
592
                provider.add_to_user()
593

    
594
        except BaseException, e:
595
            logger.exception(e)
596
        return super(ExtendedSetPasswordForm, self).save(commit=commit,
597
                                                         **kwargs)
598

    
599

    
600
app_name_label = "Project name"
601
app_name_placeholder = _("myproject.mylab.ntua.gr")
602
app_name_validator = validators.RegexValidator(
603
    DOMAIN_VALUE_REGEX,
604
    _(astakos_messages.DOMAIN_VALUE_ERR),
605
    'invalid')
606
app_name_help = _("""
607
        The project's name should be in a domain format.
608
        The domain shouldn't neccessarily exist in the real
609
        world but is helpful to imply a structure.
610
        e.g.: myproject.mylab.ntua.gr or
611
        myservice.myteam.myorganization""")
612
app_name_widget = forms.TextInput(
613
    attrs={'placeholder': app_name_placeholder})
614

    
615

    
616
app_home_label = "Homepage URL"
617
app_home_placeholder = 'myinstitution.org/myproject/'
618
app_home_help = _("""
619
        URL pointing at your project's site.
620
        e.g.: myinstitution.org/myproject/.
621
        Leave blank if there is no website.""")
622
app_home_widget = forms.TextInput(
623
    attrs={'placeholder': app_home_placeholder})
624

    
625
app_desc_label = _("Description")
626
app_desc_help = _("""
627
        Please provide a short but descriptive abstract of your
628
        project, so that anyone searching can quickly understand
629
        what this project is about.""")
630

    
631
app_comment_label = _("Comments for review (private)")
632
app_comment_help = _("""
633
        Write down any comments you may have for the reviewer
634
        of this application (e.g. background and rationale to
635
        support your request).
636
        The comments are strictly for the review process
637
        and will not be made public.""")
638

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

    
645
app_end_date_label = _("Termination date")
646
app_end_date_help = _("""
647
        At this date, the project will be automatically terminated
648
        and its resource grants revoked from all members. If you are
649
        not certain, it is best to start with a conservative estimation.
650
        You can always re-apply for an extension, if you need.""")
651

    
652
join_policy_label = _("Joining policy")
653
app_member_join_policy_help = _("""
654
        Select how new members are accepted into the project.""")
655
leave_policy_label = _("Leaving policy")
656
app_member_leave_policy_help = _("""
657
        Select how new members can leave the project.""")
658

    
659
max_members_label = _("Maximum member count")
660
max_members_help = _("""
661
        Specify the maximum number of members this project may have,
662
        including the owner. Beyond this number, no new members
663
        may join the project and be granted the project resources.
664
        If you are not certain, it is best to start with a conservative
665
        limit. You can always request a raise when you need it.""")
666

    
667
join_policies = presentation.PROJECT_MEMBER_JOIN_POLICIES.items()
668
leave_policies = presentation.PROJECT_MEMBER_LEAVE_POLICIES.items()
669

    
670

    
671
class ProjectApplicationForm(forms.ModelForm):
672

    
673
    name = forms.CharField(
674
        label=app_name_label,
675
        help_text=app_name_help,
676
        widget=app_name_widget,
677
        validators=[app_name_validator])
678

    
679
    homepage = forms.URLField(
680
        label=app_home_label,
681
        help_text=app_home_help,
682
        widget=app_home_widget,
683
        required=False)
684

    
685
    description = forms.CharField(
686
        label=app_desc_label,
687
        help_text=app_desc_help,
688
        widget=forms.Textarea,
689
        required=False)
690

    
691
    comments = forms.CharField(
692
        label=app_comment_label,
693
        help_text=app_comment_help,
694
        widget=forms.Textarea,
695
        required=False)
696

    
697
    start_date = forms.DateTimeField(
698
        label=app_start_date_label,
699
        help_text=app_start_date_help,
700
        required=False)
701

    
702
    end_date = forms.DateTimeField(
703
        label=app_end_date_label,
704
        help_text=app_end_date_help)
705

    
706
    member_join_policy = forms.TypedChoiceField(
707
        label=join_policy_label,
708
        help_text=app_member_join_policy_help,
709
        initial=2,
710
        coerce=int,
711
        choices=join_policies)
712

    
713
    member_leave_policy = forms.TypedChoiceField(
714
        label=leave_policy_label,
715
        help_text=app_member_leave_policy_help,
716
        coerce=int,
717
        choices=leave_policies)
718

    
719
    limit_on_members_number = forms.IntegerField(
720
        label=max_members_label,
721
        help_text=max_members_help,
722
        min_value=0,
723
        required=True)
724

    
725
    class Meta:
726
        model = ProjectApplication
727
        fields = ('name', 'homepage', 'description',
728
                  'start_date', 'end_date', 'comments',
729
                  'member_join_policy', 'member_leave_policy',
730
                  'limit_on_members_number')
731

    
732
    def __init__(self, *args, **kwargs):
733
        instance = kwargs.get('instance')
734
        self.precursor_application = instance
735
        super(ProjectApplicationForm, self).__init__(*args, **kwargs)
736
        # in case of new application remove closed join policy
737
        if not instance:
738
            policies = presentation.PROJECT_MEMBER_JOIN_POLICIES.copy()
739
            policies.pop(3)
740
            self.fields['member_join_policy'].choices = policies.iteritems()
741

    
742
    def clean_start_date(self):
743
        start_date = self.cleaned_data.get('start_date')
744
        if not self.precursor_application:
745
            today = datetime.now()
746
            today = datetime(today.year, today.month, today.day)
747
            if start_date and (start_date - today).days < 0:
748
                raise forms.ValidationError(
749
                    _(astakos_messages.INVALID_PROJECT_START_DATE))
750
        return start_date
751

    
752
    def clean_end_date(self):
753
        start_date = self.cleaned_data.get('start_date')
754
        end_date = self.cleaned_data.get('end_date')
755
        today = datetime.now()
756
        today = datetime(today.year, today.month, today.day)
757
        if end_date and (end_date - today).days < 0:
758
            raise forms.ValidationError(
759
                _(astakos_messages.INVALID_PROJECT_END_DATE))
760
        if start_date and (end_date - start_date).days <= 0:
761
            raise forms.ValidationError(
762
                _(astakos_messages.INCONSISTENT_PROJECT_DATES))
763
        return end_date
764

    
765
    def clean(self):
766
        userid = self.data.get('user', None)
767
        self.resource_policies
768
        self.user = None
769
        if userid:
770
            try:
771
                self.user = AstakosUser.objects.get(id=userid)
772
            except AstakosUser.DoesNotExist:
773
                pass
774
        if not self.user:
775
            raise forms.ValidationError(_(astakos_messages.NO_APPLICANT))
776
        super(ProjectApplicationForm, self).clean()
777
        return self.cleaned_data
778

    
779
    @property
780
    def resource_policies(self):
781
        policies = []
782
        append = policies.append
783
        for name, value in self.data.iteritems():
784
            if not value:
785
                continue
786
            uplimit = value
787
            if name.endswith('_uplimit'):
788
                subs = name.split('_uplimit')
789
                prefix, suffix = subs
790
                try:
791
                    resource = Resource.objects.get(name=prefix)
792
                except Resource.DoesNotExist:
793
                    raise forms.ValidationError("Resource %s does not exist" %
794
                                                resource.name)
795
                # keep only resource limits for selected resource groups
796
                if self.data.get('is_selected_%s' %
797
                                 resource.group, "0") == "1":
798
                    if not resource.ui_visible:
799
                        raise forms.ValidationError("Invalid resource %s" %
800
                                                    resource.name)
801
                    d = model_to_dict(resource)
802
                    try:
803
                        uplimit = long(uplimit)
804
                    except ValueError:
805
                        m = "Limit should be an integer"
806
                        raise forms.ValidationError(m)
807
                    display = units.show(uplimit, resource.unit)
808
                    d.update(dict(resource=prefix, uplimit=uplimit,
809
                                  display_uplimit=display))
810
                    append(d)
811

    
812
        ordered_keys = presentation.RESOURCES['resources_order']
813

    
814
        def resource_order(r):
815
            if r['str_repr'] in ordered_keys:
816
                return ordered_keys.index(r['str_repr'])
817
            else:
818
                return -1
819

    
820
        policies = sorted(policies, key=resource_order)
821
        return policies
822

    
823
    def cleaned_resource_policies(self):
824
        policies = {}
825
        for d in self.resource_policies:
826
            policies[d["name"]] = {
827
                "project_capacity": None,
828
                "member_capacity": d["uplimit"]
829
            }
830

    
831
        return policies
832

    
833
    def save(self, commit=True, **kwargs):
834
        data = dict(self.cleaned_data)
835
        is_new = self.instance.id is None
836
        data['project_id'] = self.instance.chain.id if not is_new else None
837
        data['owner'] = self.user if is_new else self.instance.owner
838
        data['resources'] = self.cleaned_resource_policies()
839
        data['request_user'] = self.user
840
        submit_application(**data)
841

    
842

    
843
class ProjectSortForm(forms.Form):
844
    sorting = forms.ChoiceField(
845
        label='Sort by',
846
        choices=(('name', 'Sort by Name'),
847
                 ('issue_date', 'Sort by Issue date'),
848
                 ('start_date', 'Sort by Start Date'),
849
                 ('end_date', 'Sort by End Date'),
850
                 # ('approved_members_num', 'Sort by Participants'),
851
                 ('state', 'Sort by Status'),
852
                 ('member_join_policy__description',
853
                  'Sort by Member Join Policy'),
854
                 ('member_leave_policy__description',
855
                  'Sort by Member Leave Policy'),
856
                 ('-name', 'Sort by Name'),
857
                 ('-issue_date', 'Sort by Issue date'),
858
                 ('-start_date', 'Sort by Start Date'),
859
                 ('-end_date', 'Sort by End Date'),
860
                 # ('-approved_members_num', 'Sort by Participants'),
861
                 ('-state', 'Sort by Status'),
862
                 ('-member_join_policy__description',
863
                  'Sort by Member Join Policy'),
864
                 ('-member_leave_policy__description',
865
                  'Sort by Member Leave Policy')
866
                 ),
867
        required=True
868
    )
869

    
870

    
871
class AddProjectMembersForm(forms.Form):
872
    q = forms.CharField(
873
        widget=forms.Textarea(
874
            attrs={
875
                'placeholder':
876
                astakos_messages.ADD_PROJECT_MEMBERS_Q_PLACEHOLDER}),
877
        label=_('Add members'),
878
        help_text=_(astakos_messages.ADD_PROJECT_MEMBERS_Q_HELP),
879
        required=True,)
880

    
881
    def __init__(self, *args, **kwargs):
882
        chain_id = kwargs.pop('chain_id', None)
883
        if chain_id:
884
            self.project = Project.objects.get(id=chain_id)
885
        self.request_user = kwargs.pop('request_user', None)
886
        super(AddProjectMembersForm, self).__init__(*args, **kwargs)
887

    
888
    def clean(self):
889
        try:
890
            accept_membership_project_checks(self.project, self.request_user)
891
        except ProjectError as e:
892
            raise forms.ValidationError(e)
893

    
894
        q = self.cleaned_data.get('q') or ''
895
        users = q.split(',')
896
        users = list(u.strip() for u in users if u)
897
        db_entries = AstakosUser.objects.accepted().filter(email__in=users)
898
        unknown = list(set(users) - set(u.email for u in db_entries))
899
        if unknown:
900
            raise forms.ValidationError(
901
                _(astakos_messages.UNKNOWN_USERS) % ','.join(unknown))
902
        self.valid_users = db_entries
903
        return self.cleaned_data
904

    
905
    def get_valid_users(self):
906
        """Should be called after form cleaning"""
907
        try:
908
            return self.valid_users
909
        except:
910
            return ()
911

    
912

    
913
class ProjectMembersSortForm(forms.Form):
914
    sorting = forms.ChoiceField(
915
        label='Sort by',
916
        choices=(('person__email', 'User Id'),
917
                 ('person__first_name', 'Name'),
918
                 ('acceptance_date', 'Acceptance date')
919
                 ),
920
        required=True
921
    )
922

    
923

    
924
class ProjectSearchForm(forms.Form):
925
    q = forms.CharField(max_length=200, label='Search project', required=False)
926

    
927

    
928
class ExtendedProfileForm(ProfileForm):
929
    """
930
    Profile form that combines `email change` and `password change` user
931
    actions by propagating submited data to internal EmailChangeForm
932
    and ExtendedPasswordChangeForm objects.
933
    """
934

    
935
    password_change_form = None
936
    email_change_form = None
937

    
938
    password_change = False
939
    email_change = False
940

    
941
    extra_forms_fields = {
942
        'email': ['new_email_address'],
943
        'password': ['old_password', 'new_password1', 'new_password2']
944
    }
945

    
946
    fields = ('email')
947
    change_password = forms.BooleanField(initial=False, required=False)
948
    change_email = forms.BooleanField(initial=False, required=False)
949

    
950
    email_changed = False
951
    password_changed = False
952

    
953
    def __init__(self, *args, **kwargs):
954
        session_key = kwargs.get('session_key', None)
955
        self.fields_list = [
956
            'email',
957
            'new_email_address',
958
            'first_name',
959
            'last_name',
960
            'old_password',
961
            'new_password1',
962
            'new_password2',
963
            'change_email',
964
            'change_password',
965
        ]
966

    
967
        super(ExtendedProfileForm, self).__init__(*args, **kwargs)
968
        self.session_key = session_key
969
        if self.instance.can_change_password():
970
            self.password_change = True
971
        else:
972
            self.fields_list.remove('old_password')
973
            self.fields_list.remove('new_password1')
974
            self.fields_list.remove('new_password2')
975
            self.fields_list.remove('change_password')
976
            del self.fields['change_password']
977

    
978
        if settings.EMAILCHANGE_ENABLED and self.instance.can_change_email():
979
            self.email_change = True
980
        else:
981
            self.fields_list.remove('new_email_address')
982
            self.fields_list.remove('change_email')
983
            del self.fields['change_email']
984

    
985
        self._init_extra_forms()
986
        self.save_extra_forms = []
987
        self.success_messages = []
988
        self.fields.keyOrder = self.fields_list
989

    
990
    def _init_extra_form_fields(self):
991
        if self.email_change:
992
            self.fields.update(self.email_change_form.fields)
993
            self.fields['new_email_address'].required = False
994
            self.fields['email'].help_text = _(
995
                'Change the email associated with '
996
                'your account. This email will '
997
                'remain active until you verify '
998
                'your new one.')
999

    
1000
        if self.password_change:
1001
            self.fields.update(self.password_change_form.fields)
1002
            self.fields['old_password'].required = False
1003
            self.fields['old_password'].label = _('Password')
1004
            self.fields['old_password'].help_text = _('Change your password.')
1005
            self.fields['old_password'].initial = 'password'
1006
            self.fields['old_password'].widget.render_value = True
1007
            self.fields['new_password1'].required = False
1008
            self.fields['new_password2'].required = False
1009

    
1010
    def _update_extra_form_errors(self):
1011
        if self.cleaned_data.get('change_password'):
1012
            self.errors.update(self.password_change_form.errors)
1013
        if self.cleaned_data.get('change_email'):
1014
            self.errors.update(self.email_change_form.errors)
1015

    
1016
    def _init_extra_forms(self):
1017
        self.email_change_form = EmailChangeForm(self.data)
1018
        self.password_change_form = ExtendedPasswordChangeForm(
1019
            user=self.instance,
1020
            data=self.data, session_key=self.session_key)
1021
        self._init_extra_form_fields()
1022

    
1023
    def is_valid(self):
1024
        password, email = True, True
1025
        profile = super(ExtendedProfileForm, self).is_valid()
1026
        if profile and self.cleaned_data.get('change_password', None):
1027
            self.password_change_form.fields['new_password1'].required = True
1028
            self.password_change_form.fields['new_password2'].required = True
1029
            password = self.password_change_form.is_valid()
1030
            self.save_extra_forms.append('password')
1031
        if profile and self.cleaned_data.get('change_email'):
1032
            self.fields['new_email_address'].required = True
1033
            email = self.email_change_form.is_valid()
1034
            self.save_extra_forms.append('email')
1035

    
1036
        if not password or not email:
1037
            self._update_extra_form_errors()
1038

    
1039
        return all([profile, password, email])
1040

    
1041
    def save(self, request, *args, **kwargs):
1042
        if 'email' in self.save_extra_forms:
1043
            self.email_change_form.save(request, *args, **kwargs)
1044
            self.email_changed = True
1045
        if 'password' in self.save_extra_forms:
1046
            self.password_change_form.save(*args, **kwargs)
1047
            self.password_changed = True
1048
        return super(ExtendedProfileForm, self).save(*args, **kwargs)