Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / forms.py @ 69a6ca1a

History | View | Annotate | Download (38 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
                msg = mark_safe(user.get_inactive_message('local'))
415
                raise forms.ValidationError(msg)
416

    
417
            provider = auth_providers.get_provider('local', user)
418
            if not user.has_usable_password():
419
                msg = provider.get_unusable_password_msg
420
                raise forms.ValidationError(mark_safe(msg))
421

    
422
            if not user.can_change_password():
423
                msg = provider.get_cannot_change_password_msg
424
                raise forms.ValidationError(mark_safe(msg))
425

    
426
        except AstakosUser.DoesNotExist:
427
            raise forms.ValidationError(_(astakos_messages.EMAIL_UNKNOWN))
428
        return email
429

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

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

    
458

    
459
class EmailChangeForm(forms.ModelForm):
460

    
461
    new_email_address = EmailField()
462

    
463
    class Meta:
464
        model = EmailChange
465
        fields = ('new_email_address',)
466

    
467
    def clean_new_email_address(self):
468
        addr = self.cleaned_data['new_email_address']
469
        if reserved_verified_email(addr):
470
            raise forms.ValidationError(_(astakos_messages.EMAIL_USED))
471
        return addr
472

    
473
    def save(self, request,
474
             email_template_name='registration/email_change_email.txt',
475
             commit=True, **kwargs):
476
        ec = super(EmailChangeForm, self).save(commit=False, **kwargs)
477
        ec.user = request.user
478
        # delete pending email changes
479
        request.user.emailchanges.all().delete()
480

    
481
        activation_key = hashlib.sha1(
482
            str(random()) + smart_str(ec.new_email_address))
483
        ec.activation_key = activation_key.hexdigest()
484
        if commit:
485
            ec.save(**kwargs)
486
        send_change_email(ec, request, email_template_name=email_template_name)
487

    
488

    
489
class SignApprovalTermsForm(forms.ModelForm):
490

    
491
    class Meta:
492
        model = AstakosUser
493
        fields = ("has_signed_terms",)
494

    
495
    def __init__(self, *args, **kwargs):
496
        super(SignApprovalTermsForm, self).__init__(*args, **kwargs)
497

    
498
    def clean_has_signed_terms(self):
499
        has_signed_terms = self.cleaned_data['has_signed_terms']
500
        if not has_signed_terms:
501
            raise forms.ValidationError(_(astakos_messages.SIGN_TERMS))
502
        return has_signed_terms
503

    
504
    def save(self, commit=True, **kwargs):
505
        user = super(SignApprovalTermsForm, self).save(commit=commit, **kwargs)
506
        user.date_signed_terms = datetime.now()
507
        if commit:
508
            user.save(**kwargs)
509
        return user
510

    
511

    
512
class InvitationForm(forms.ModelForm):
513

    
514
    username = EmailField(label=_("Email"))
515

    
516
    def __init__(self, *args, **kwargs):
517
        super(InvitationForm, self).__init__(*args, **kwargs)
518

    
519
    class Meta:
520
        model = Invitation
521
        fields = ('username', 'realname')
522

    
523
    def clean_username(self):
524
        username = self.cleaned_data['username']
525
        try:
526
            Invitation.objects.get(username=username)
527
            raise forms.ValidationError(
528
                _(astakos_messages.INVITATION_EMAIL_EXISTS))
529
        except Invitation.DoesNotExist:
530
            pass
531
        return username
532

    
533

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

    
545
    def __init__(self, user, *args, **kwargs):
546
        self.session_key = kwargs.pop('session_key', None)
547
        super(ExtendedPasswordChangeForm, self).__init__(user, *args, **kwargs)
548

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

    
561

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

    
574
    def __init__(self, user, *args, **kwargs):
575
        super(ExtendedSetPasswordForm, self).__init__(user, *args, **kwargs)
576

    
577
    @transaction.commit_on_success()
578
    def save(self, commit=True, **kwargs):
579
        try:
580
            self.user = AstakosUser.objects.get(id=self.user.id)
581
            if settings.NEWPASSWD_INVALIDATE_TOKEN or \
582
                    self.cleaned_data.get('renew'):
583
                self.user.renew_token()
584

    
585
            provider = auth_providers.get_provider('local', self.user)
586
            if provider.get_add_policy:
587
                provider.add_to_user()
588

    
589
        except BaseException, e:
590
            logger.exception(e)
591
        return super(ExtendedSetPasswordForm, self).save(commit=commit,
592
                                                         **kwargs)
593

    
594

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

    
610

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

    
620
app_desc_label = _("Description")
621
app_desc_help = _("""
622
        Please provide a short but descriptive abstract of your
623
        project, so that anyone searching can quickly understand
624
        what this project is about.""")
625

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

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

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

    
647
join_policy_label = _("Joining policy")
648
app_member_join_policy_help = _("""
649
        Select how new members are accepted into the project.""")
650
leave_policy_label = _("Leaving policy")
651
app_member_leave_policy_help = _("""
652
        Select how new members can leave the project.""")
653

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

    
662
join_policies = presentation.PROJECT_MEMBER_JOIN_POLICIES.items()
663
leave_policies = presentation.PROJECT_MEMBER_LEAVE_POLICIES.items()
664

    
665

    
666
class ProjectApplicationForm(forms.ModelForm):
667

    
668
    name = forms.CharField(
669
        label=app_name_label,
670
        help_text=app_name_help,
671
        widget=app_name_widget,
672
        validators=[app_name_validator])
673

    
674
    homepage = forms.URLField(
675
        label=app_home_label,
676
        help_text=app_home_help,
677
        widget=app_home_widget,
678
        required=False)
679

    
680
    description = forms.CharField(
681
        label=app_desc_label,
682
        help_text=app_desc_help,
683
        widget=forms.Textarea,
684
        required=False)
685

    
686
    comments = forms.CharField(
687
        label=app_comment_label,
688
        help_text=app_comment_help,
689
        widget=forms.Textarea,
690
        required=False)
691

    
692
    start_date = forms.DateTimeField(
693
        label=app_start_date_label,
694
        help_text=app_start_date_help,
695
        required=False)
696

    
697
    end_date = forms.DateTimeField(
698
        label=app_end_date_label,
699
        help_text=app_end_date_help)
700

    
701
    member_join_policy = forms.TypedChoiceField(
702
        label=join_policy_label,
703
        help_text=app_member_join_policy_help,
704
        initial=2,
705
        coerce=int,
706
        choices=join_policies)
707

    
708
    member_leave_policy = forms.TypedChoiceField(
709
        label=leave_policy_label,
710
        help_text=app_member_leave_policy_help,
711
        coerce=int,
712
        choices=leave_policies)
713

    
714
    limit_on_members_number = forms.IntegerField(
715
        label=max_members_label,
716
        help_text=max_members_help,
717
        min_value=0,
718
        required=True)
719

    
720
    class Meta:
721
        model = ProjectApplication
722
        fields = ('name', 'homepage', 'description',
723
                  'start_date', 'end_date', 'comments',
724
                  'member_join_policy', 'member_leave_policy',
725
                  'limit_on_members_number')
726

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

    
737
    def clean_start_date(self):
738
        start_date = self.cleaned_data.get('start_date')
739
        if not self.precursor_application:
740
            today = datetime.now()
741
            today = datetime(today.year, today.month, today.day)
742
            if start_date and (start_date - today).days < 0:
743
                raise forms.ValidationError(
744
                    _(astakos_messages.INVALID_PROJECT_START_DATE))
745
        return start_date
746

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

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

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

    
807
        ordered_keys = presentation.RESOURCES['resources_order']
808

    
809
        def resource_order(r):
810
            if r['str_repr'] in ordered_keys:
811
                return ordered_keys.index(r['str_repr'])
812
            else:
813
                return -1
814

    
815
        policies = sorted(policies, key=resource_order)
816
        return policies
817

    
818
    def cleaned_resource_policies(self):
819
        policies = {}
820
        for d in self.resource_policies:
821
            policies[d["name"]] = {
822
                "project_capacity": None,
823
                "member_capacity": d["uplimit"]
824
            }
825

    
826
        return policies
827

    
828
    def save(self, commit=True, **kwargs):
829
        data = dict(self.cleaned_data)
830
        is_new = self.instance.id is None
831
        data['project_id'] = self.instance.chain.id if not is_new else None
832
        data['owner'] = self.user if is_new else self.instance.owner
833
        data['resources'] = self.cleaned_resource_policies()
834
        data['request_user'] = self.user
835
        submit_application(**data)
836

    
837

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

    
865

    
866
class AddProjectMembersForm(forms.Form):
867
    q = forms.CharField(
868
        widget=forms.Textarea(
869
            attrs={
870
                'placeholder':
871
                astakos_messages.ADD_PROJECT_MEMBERS_Q_PLACEHOLDER}),
872
        label=_('Add members'),
873
        help_text=_(astakos_messages.ADD_PROJECT_MEMBERS_Q_HELP),
874
        required=True,)
875

    
876
    def __init__(self, *args, **kwargs):
877
        chain_id = kwargs.pop('chain_id', None)
878
        if chain_id:
879
            self.project = Project.objects.get(id=chain_id)
880
        self.request_user = kwargs.pop('request_user', None)
881
        super(AddProjectMembersForm, self).__init__(*args, **kwargs)
882

    
883
    def clean(self):
884
        try:
885
            accept_membership_project_checks(self.project, self.request_user)
886
        except ProjectError as e:
887
            raise forms.ValidationError(e)
888

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

    
900
    def get_valid_users(self):
901
        """Should be called after form cleaning"""
902
        try:
903
            return self.valid_users
904
        except:
905
            return ()
906

    
907

    
908
class ProjectMembersSortForm(forms.Form):
909
    sorting = forms.ChoiceField(
910
        label='Sort by',
911
        choices=(('person__email', 'User Id'),
912
                 ('person__first_name', 'Name'),
913
                 ('acceptance_date', 'Acceptance date')
914
                 ),
915
        required=True
916
    )
917

    
918

    
919
class ProjectSearchForm(forms.Form):
920
    q = forms.CharField(max_length=200, label='Search project', required=False)
921

    
922

    
923
class ExtendedProfileForm(ProfileForm):
924
    """
925
    Profile form that combines `email change` and `password change` user
926
    actions by propagating submited data to internal EmailChangeForm
927
    and ExtendedPasswordChangeForm objects.
928
    """
929

    
930
    password_change_form = None
931
    email_change_form = None
932

    
933
    password_change = False
934
    email_change = False
935

    
936
    extra_forms_fields = {
937
        'email': ['new_email_address'],
938
        'password': ['old_password', 'new_password1', 'new_password2']
939
    }
940

    
941
    fields = ('email')
942
    change_password = forms.BooleanField(initial=False, required=False)
943
    change_email = forms.BooleanField(initial=False, required=False)
944

    
945
    email_changed = False
946
    password_changed = False
947

    
948
    def __init__(self, *args, **kwargs):
949
        session_key = kwargs.get('session_key', None)
950
        self.fields_list = [
951
            'email',
952
            'new_email_address',
953
            'first_name',
954
            'last_name',
955
            'old_password',
956
            'new_password1',
957
            'new_password2',
958
            'change_email',
959
            'change_password',
960
        ]
961

    
962
        super(ExtendedProfileForm, self).__init__(*args, **kwargs)
963
        self.session_key = session_key
964
        if self.instance.can_change_password():
965
            self.password_change = True
966
        else:
967
            self.fields_list.remove('old_password')
968
            self.fields_list.remove('new_password1')
969
            self.fields_list.remove('new_password2')
970
            self.fields_list.remove('change_password')
971
            del self.fields['change_password']
972

    
973
        if settings.EMAILCHANGE_ENABLED and self.instance.can_change_email():
974
            self.email_change = True
975
        else:
976
            self.fields_list.remove('new_email_address')
977
            self.fields_list.remove('change_email')
978
            del self.fields['change_email']
979

    
980
        self._init_extra_forms()
981
        self.save_extra_forms = []
982
        self.success_messages = []
983
        self.fields.keyOrder = self.fields_list
984

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

    
995
        if self.password_change:
996
            self.fields.update(self.password_change_form.fields)
997
            self.fields['old_password'].required = False
998
            self.fields['old_password'].label = _('Password')
999
            self.fields['old_password'].help_text = _('Change your password.')
1000
            self.fields['old_password'].initial = 'password'
1001
            self.fields['old_password'].widget.render_value = True
1002
            self.fields['new_password1'].required = False
1003
            self.fields['new_password2'].required = False
1004

    
1005
    def _update_extra_form_errors(self):
1006
        if self.cleaned_data.get('change_password'):
1007
            self.errors.update(self.password_change_form.errors)
1008
        if self.cleaned_data.get('change_email'):
1009
            self.errors.update(self.email_change_form.errors)
1010

    
1011
    def _init_extra_forms(self):
1012
        self.email_change_form = EmailChangeForm(self.data)
1013
        self.password_change_form = ExtendedPasswordChangeForm(
1014
            user=self.instance,
1015
            data=self.data, session_key=self.session_key)
1016
        self._init_extra_form_fields()
1017

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

    
1031
        if not password or not email:
1032
            self._update_extra_form_errors()
1033

    
1034
        return all([profile, password, email])
1035

    
1036
    def save(self, request, *args, **kwargs):
1037
        if 'email' in self.save_extra_forms:
1038
            self.email_change_form.save(request, *args, **kwargs)
1039
            self.email_changed = True
1040
        if 'password' in self.save_extra_forms:
1041
            self.password_change_form.save(*args, **kwargs)
1042
            self.password_changed = True
1043
        return super(ExtendedProfileForm, self).save(*args, **kwargs)