Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (37.9 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.models import AstakosUser, EmailChange, Invitation, Resource, \
52
    PendingThirdPartyUser, get_latest_terms, ProjectApplication, Project
53
from astakos.im import presentation
54
from astakos.im.widgets import DummyWidget, RecaptchaWidget
55
from astakos.im.functions import send_change_email, submit_application, \
56
    accept_membership_project_checks, ProjectError
57

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

    
63
import astakos.im.messages as astakos_messages
64

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

    
70
logger = logging.getLogger(__name__)
71

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

    
76

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
176

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

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

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

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

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

    
206
        super(ThirdPartyUserCreationForm, self).__init__(*args, **kwargs)
207

    
208
        if not get_latest_terms():
209
            del self.fields['has_signed_terms']
210

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

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

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

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

    
238
    def _get_pending_user(self):
239
        return PendingThirdPartyUser.objects.get(token=self.third_party_token)
240

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

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

    
258

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

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

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

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

    
284
    def clean_username(self):
285
        return self.cleaned_data['username'].lower()
286

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

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

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

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

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

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

    
335

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

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

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

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

    
362
    def clean_email(self):
363
        return self.instance.email
364

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

    
377

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

    
386

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

    
392
    email = forms.EmailField(required=True, label='Email address')
393
    first_name = forms.EmailField(label='First name')
394
    last_name = forms.EmailField(label='Last name')
395

    
396

    
397
class ExtendedPasswordResetForm(PasswordResetForm):
398
    """
399
    Extends PasswordResetForm by overriding
400

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

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

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

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

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

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

    
456

    
457
class EmailChangeForm(forms.ModelForm):
458

    
459
    class Meta:
460
        model = EmailChange
461
        fields = ('new_email_address',)
462

    
463
    def clean_new_email_address(self):
464
        addr = self.cleaned_data['new_email_address']
465
        if reserved_verified_email(addr):
466
            raise forms.ValidationError(_(astakos_messages.EMAIL_USED))
467
        return addr
468

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

    
477
        activation_key = hashlib.sha1(
478
            str(random()) + smart_str(ec.new_email_address))
479
        ec.activation_key = activation_key.hexdigest()
480
        if commit:
481
            ec.save(**kwargs)
482
        send_change_email(ec, request, email_template_name=email_template_name)
483

    
484

    
485
class SignApprovalTermsForm(forms.ModelForm):
486

    
487
    class Meta:
488
        model = AstakosUser
489
        fields = ("has_signed_terms",)
490

    
491
    def __init__(self, *args, **kwargs):
492
        super(SignApprovalTermsForm, self).__init__(*args, **kwargs)
493

    
494
    def clean_has_signed_terms(self):
495
        has_signed_terms = self.cleaned_data['has_signed_terms']
496
        if not has_signed_terms:
497
            raise forms.ValidationError(_(astakos_messages.SIGN_TERMS))
498
        return has_signed_terms
499

    
500
    def save(self, commit=True, **kwargs):
501
        user = super(SignApprovalTermsForm, self).save(commit=commit, **kwargs)
502
        user.date_signed_terms = datetime.now()
503
        if commit:
504
            user.save(**kwargs)
505
        return user
506

    
507

    
508
class InvitationForm(forms.ModelForm):
509

    
510
    username = forms.EmailField(label=_("Email"))
511

    
512
    def __init__(self, *args, **kwargs):
513
        super(InvitationForm, self).__init__(*args, **kwargs)
514

    
515
    class Meta:
516
        model = Invitation
517
        fields = ('username', 'realname')
518

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

    
529

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

    
541
    def __init__(self, user, *args, **kwargs):
542
        self.session_key = kwargs.pop('session_key', None)
543
        super(ExtendedPasswordChangeForm, self).__init__(user, *args, **kwargs)
544

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

    
557

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

    
570
    def __init__(self, user, *args, **kwargs):
571
        super(ExtendedSetPasswordForm, self).__init__(user, *args, **kwargs)
572

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

    
581
            provider = auth_providers.get_provider('local', self.user)
582
            if provider.get_add_policy:
583
                provider.add_to_user()
584

    
585
        except BaseException, e:
586
            logger.exception(e)
587
        return super(ExtendedSetPasswordForm, self).save(commit=commit,
588
                                                         **kwargs)
589

    
590

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

    
606

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

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

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

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

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

    
643
join_policy_label = _("Joining policy")
644
app_member_join_policy_help = _("""
645
        Select how new members are accepted into the project.""")
646
leave_policy_label = _("Leaving policy")
647
app_member_leave_policy_help = _("""
648
        Select how new members can leave the project.""")
649

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

    
658
join_policies = presentation.PROJECT_MEMBER_JOIN_POLICIES.items()
659
leave_policies = presentation.PROJECT_MEMBER_LEAVE_POLICIES.items()
660

    
661

    
662
class ProjectApplicationForm(forms.ModelForm):
663

    
664
    name = forms.CharField(
665
        label=app_name_label,
666
        help_text=app_name_help,
667
        widget=app_name_widget,
668
        validators=[app_name_validator])
669

    
670
    homepage = forms.URLField(
671
        label=app_home_label,
672
        help_text=app_home_help,
673
        widget=app_home_widget,
674
        required=False)
675

    
676
    description = forms.CharField(
677
        label=app_desc_label,
678
        help_text=app_desc_help,
679
        widget=forms.Textarea,
680
        required=False)
681

    
682
    comments = forms.CharField(
683
        label=app_comment_label,
684
        help_text=app_comment_help,
685
        widget=forms.Textarea,
686
        required=False)
687

    
688
    start_date = forms.DateTimeField(
689
        label=app_start_date_label,
690
        help_text=app_start_date_help,
691
        required=False)
692

    
693
    end_date = forms.DateTimeField(
694
        label=app_end_date_label,
695
        help_text=app_end_date_help)
696

    
697
    member_join_policy = forms.TypedChoiceField(
698
        label=join_policy_label,
699
        help_text=app_member_join_policy_help,
700
        initial=2,
701
        coerce=int,
702
        choices=join_policies)
703

    
704
    member_leave_policy = forms.TypedChoiceField(
705
        label=leave_policy_label,
706
        help_text=app_member_leave_policy_help,
707
        coerce=int,
708
        choices=leave_policies)
709

    
710
    limit_on_members_number = forms.IntegerField(
711
        label=max_members_label,
712
        help_text=max_members_help,
713
        min_value=0,
714
        required=True)
715

    
716
    class Meta:
717
        model = ProjectApplication
718
        fields = ('name', 'homepage', 'description',
719
                  'start_date', 'end_date', 'comments',
720
                  'member_join_policy', 'member_leave_policy',
721
                  'limit_on_members_number')
722

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

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

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

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

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

    
803
        ordered_keys = presentation.RESOURCES['resources_order']
804

    
805
        def resource_order(r):
806
            if r['str_repr'] in ordered_keys:
807
                return ordered_keys.index(r['str_repr'])
808
            else:
809
                return -1
810

    
811
        policies = sorted(policies, key=resource_order)
812
        return policies
813

    
814
    def cleaned_resource_policies(self):
815
        policies = {}
816
        for d in self.resource_policies:
817
            policies[d["name"]] = {
818
                "project_capacity": None,
819
                "member_capacity": d["uplimit"]
820
            }
821

    
822
        return policies
823

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

    
833

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

    
861

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

    
872
    def __init__(self, *args, **kwargs):
873
        chain_id = kwargs.pop('chain_id', None)
874
        if chain_id:
875
            self.project = Project.objects.get(id=chain_id)
876
        self.request_user = kwargs.pop('request_user', None)
877
        super(AddProjectMembersForm, self).__init__(*args, **kwargs)
878

    
879
    def clean(self):
880
        try:
881
            accept_membership_project_checks(self.project, self.request_user)
882
        except ProjectError as e:
883
            raise forms.ValidationError(e)
884

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

    
896
    def get_valid_users(self):
897
        """Should be called after form cleaning"""
898
        try:
899
            return self.valid_users
900
        except:
901
            return ()
902

    
903

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

    
914

    
915
class ProjectSearchForm(forms.Form):
916
    q = forms.CharField(max_length=200, label='Search project', required=False)
917

    
918

    
919
class ExtendedProfileForm(ProfileForm):
920
    """
921
    Profile form that combines `email change` and `password change` user
922
    actions by propagating submited data to internal EmailChangeForm
923
    and ExtendedPasswordChangeForm objects.
924
    """
925

    
926
    password_change_form = None
927
    email_change_form = None
928

    
929
    password_change = False
930
    email_change = False
931

    
932
    extra_forms_fields = {
933
        'email': ['new_email_address'],
934
        'password': ['old_password', 'new_password1', 'new_password2']
935
    }
936

    
937
    fields = ('email')
938
    change_password = forms.BooleanField(initial=False, required=False)
939
    change_email = forms.BooleanField(initial=False, required=False)
940

    
941
    email_changed = False
942
    password_changed = False
943

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

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

    
969
        if settings.EMAILCHANGE_ENABLED and self.instance.can_change_email():
970
            self.email_change = True
971
        else:
972
            self.fields_list.remove('new_email_address')
973
            self.fields_list.remove('change_email')
974
            del self.fields['change_email']
975

    
976
        self._init_extra_forms()
977
        self.save_extra_forms = []
978
        self.success_messages = []
979
        self.fields.keyOrder = self.fields_list
980

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

    
991
        if self.password_change:
992
            self.fields.update(self.password_change_form.fields)
993
            self.fields['old_password'].required = False
994
            self.fields['old_password'].label = _('Password')
995
            self.fields['old_password'].help_text = _('Change your password.')
996
            self.fields['old_password'].initial = 'password'
997
            self.fields['new_password1'].required = False
998
            self.fields['new_password2'].required = False
999

    
1000
    def _update_extra_form_errors(self):
1001
        if self.cleaned_data.get('change_password'):
1002
            self.errors.update(self.password_change_form.errors)
1003
        if self.cleaned_data.get('change_email'):
1004
            self.errors.update(self.email_change_form.errors)
1005

    
1006
    def _init_extra_forms(self):
1007
        self.email_change_form = EmailChangeForm(self.data)
1008
        self.password_change_form = ExtendedPasswordChangeForm(
1009
            user=self.instance,
1010
            data=self.data, session_key=self.session_key)
1011
        self._init_extra_form_fields()
1012

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

    
1026
        if not password or not email:
1027
            self._update_extra_form_errors()
1028

    
1029
        return all([profile, password, email])
1030

    
1031
    def save(self, request, *args, **kwargs):
1032
        if 'email' in self.save_extra_forms:
1033
            self.email_change_form.save(request, *args, **kwargs)
1034
            self.email_changed = True
1035
        if 'password' in self.save_extra_forms:
1036
            self.password_change_form.save(*args, **kwargs)
1037
            self.password_changed = True
1038
        return super(ExtendedProfileForm, self).save(*args, **kwargs)