Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / forms.py @ 5ae8216a

History | View | Annotate | Download (39 kB)

1
# Copyright 2011-2012 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

    
62
import astakos.im.messages as astakos_messages
63

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

    
69
logger = logging.getLogger(__name__)
70

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

    
75

    
76
class StoreUserMixin(object):
77

    
78
    def store_user(self, user, request=None):
79
        """
80
        WARNING: this should be wrapped inside a transactional view/method.
81
        """
82
        user.save()
83
        self.post_store_user(user, request)
84
        return user
85

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

    
93

    
94
class LocalUserCreationForm(UserCreationForm, StoreUserMixin):
95
    """
96
    Extends the built in UserCreationForm in several ways:
97

98
    * Adds email, first_name, last_name, recaptcha_challenge_field,
99
    * recaptcha_response_field field.
100
    * The username field isn't visible and it is assigned a generated id.
101
    * User created is not active.
102
    """
103
    recaptcha_challenge_field = forms.CharField(widget=DummyWidget)
104
    recaptcha_response_field = forms.CharField(
105
        widget=RecaptchaWidget, label='')
106

    
107
    class Meta:
108
        model = AstakosUser
109
        fields = ("email", "first_name", "last_name",
110
                  "has_signed_terms", "has_signed_terms")
111

    
112
    def __init__(self, *args, **kwargs):
113
        """
114
        Changes the order of fields, and removes the username field.
115
        """
116
        request = kwargs.pop('request', None)
117
        provider = kwargs.pop('provider', 'local')
118

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

    
123
        self.ip = None
124
        if request:
125
            self.ip = request.META.get('REMOTE_ADDR',
126
                                       request.META.get('HTTP_X_REAL_IP',
127
                                                        None))
128

    
129
        super(LocalUserCreationForm, self).__init__(*args, **kwargs)
130
        self.fields.keyOrder = ['email', 'first_name', 'last_name',
131
                                'password1', 'password2']
132

    
133
        if settings.RECAPTCHA_ENABLED:
134
            self.fields.keyOrder.extend(['recaptcha_challenge_field',
135
                                         'recaptcha_response_field', ])
136
        if get_latest_terms():
137
            self.fields.keyOrder.append('has_signed_terms')
138

    
139
        if 'has_signed_terms' in self.fields:
140
            # Overriding field label since we need to apply a link
141
            # to the terms within the label
142
            terms_link_html = '<a href="%s" target="_blank">%s</a>' \
143
                % (reverse('latest_terms'), _("the terms"))
144
            self.fields['has_signed_terms'].label = \
145
                mark_safe("I agree with %s" % terms_link_html)
146

    
147
    def clean_email(self):
148
        email = self.cleaned_data['email']
149
        if not email:
150
            raise forms.ValidationError(_(astakos_messages.REQUIRED_FIELD))
151
        if reserved_verified_email(email):
152
            raise forms.ValidationError(_(astakos_messages.EMAIL_USED))
153
        return email
154

    
155
    def clean_has_signed_terms(self):
156
        has_signed_terms = self.cleaned_data['has_signed_terms']
157
        if not has_signed_terms:
158
            raise forms.ValidationError(_(astakos_messages.SIGN_TERMS))
159
        return has_signed_terms
160

    
161
    def clean_recaptcha_response_field(self):
162
        if 'recaptcha_challenge_field' in self.cleaned_data:
163
            self.validate_captcha()
164
        return self.cleaned_data['recaptcha_response_field']
165

    
166
    def clean_recaptcha_challenge_field(self):
167
        if 'recaptcha_response_field' in self.cleaned_data:
168
            self.validate_captcha()
169
        return self.cleaned_data['recaptcha_challenge_field']
170

    
171
    def validate_captcha(self):
172
        rcf = self.cleaned_data['recaptcha_challenge_field']
173
        rrf = self.cleaned_data['recaptcha_response_field']
174
        check = captcha.submit(
175
            rcf, rrf, settings.RECAPTCHA_PRIVATE_KEY, self.ip)
176
        if not check.is_valid:
177
            raise forms.ValidationError(_(
178
                astakos_messages.CAPTCHA_VALIDATION_ERR))
179

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

    
188
    def save(self, commit=True, **kwargs):
189
        """
190
        Saves the email, first_name and last_name properties, after the normal
191
        save behavior is complete.
192
        """
193
        user = super(LocalUserCreationForm, self).save(commit=False, **kwargs)
194
        user.has_signed_terms = True
195
        user.date_signed_terms = datetime.now()
196
        user.renew_token()
197
        if commit:
198
            user.save(**kwargs)
199
            logger.info('Created user %s', user.log_display)
200
        return user
201

    
202

    
203
class ThirdPartyUserCreationForm(forms.ModelForm, StoreUserMixin):
204
    email = forms.EmailField(
205
        label='Contact email',
206
        help_text='This is needed for contact purposes. '
207
        'It doesn&#39;t need to be the same with the one you '
208
        'provided to login previously. '
209
    )
210

    
211
    class Meta:
212
        model = AstakosUser
213
        fields = ['email', 'first_name', 'last_name', 'has_signed_terms']
214

    
215
    def __init__(self, *args, **kwargs):
216
        """
217
        Changes the order of fields, and removes the username field.
218
        """
219

    
220
        self.provider = kwargs.pop('provider', None)
221
        self.request = kwargs.pop('request', None)
222
        if not self.provider or self.provider == 'local':
223
            raise Exception('Invalid provider, %r' % self.provider)
224

    
225
        # ThirdPartyUserCreationForm should always get instantiated with
226
        # a third_party_token value
227
        self.third_party_token = kwargs.pop('third_party_token', None)
228
        if not self.third_party_token:
229
            raise Exception('ThirdPartyUserCreationForm'
230
                            ' requires third_party_token')
231

    
232
        super(ThirdPartyUserCreationForm, self).__init__(*args, **kwargs)
233

    
234
        if not get_latest_terms():
235
            del self.fields['has_signed_terms']
236

    
237
        if 'has_signed_terms' in self.fields:
238
            # Overriding field label since we need to apply a link
239
            # to the terms within the label
240
            terms_link_html = '<a href="%s" target="_blank">%s</a>' \
241
                % (reverse('latest_terms'), _("the terms"))
242
            self.fields['has_signed_terms'].label = \
243
                mark_safe("I agree with %s" % terms_link_html)
244

    
245
    def clean_email(self):
246
        email = self.cleaned_data['email']
247
        if not email:
248
            raise forms.ValidationError(_(astakos_messages.REQUIRED_FIELD))
249
        if reserved_verified_email(email):
250
            provider_id = self.provider
251
            provider = auth_providers.get_provider(provider_id)
252
            extra_message = provider.get_add_to_existing_account_msg
253

    
254
            raise forms.ValidationError(mark_safe(
255
                _(astakos_messages.EMAIL_USED) + ' ' + extra_message))
256
        return email
257

    
258
    def clean_has_signed_terms(self):
259
        has_signed_terms = self.cleaned_data['has_signed_terms']
260
        if not has_signed_terms:
261
            raise forms.ValidationError(_(astakos_messages.SIGN_TERMS))
262
        return has_signed_terms
263

    
264
    def _get_pending_user(self):
265
        return PendingThirdPartyUser.objects.get(token=self.third_party_token)
266

    
267
    def post_store_user(self, user, request=None):
268
        pending = self._get_pending_user()
269
        provider = pending.get_provider(user)
270
        provider.add_to_user()
271
        pending.delete()
272

    
273
    def save(self, commit=True, **kwargs):
274
        user = super(ThirdPartyUserCreationForm, self).save(commit=False,
275
                                                            **kwargs)
276
        user.set_unusable_password()
277
        user.renew_token()
278
        user.has_signed_terms = True
279
        user.date_signed_terms = datetime.now()
280
        if commit:
281
            user.save(**kwargs)
282
            logger.info('Created user %s' % user.log_display)
283
        return user
284

    
285

    
286
class LoginForm(AuthenticationForm):
287
    username = forms.EmailField(label=_("Email"))
288
    recaptcha_challenge_field = forms.CharField(widget=DummyWidget)
289
    recaptcha_response_field = forms.CharField(
290
        widget=RecaptchaWidget, label='')
291

    
292
    def __init__(self, *args, **kwargs):
293
        was_limited = kwargs.get('was_limited', False)
294
        request = kwargs.get('request', None)
295
        if request:
296
            self.ip = request.META.get(
297
                'REMOTE_ADDR',
298
                request.META.get('HTTP_X_REAL_IP', None))
299

    
300
        t = ('request', 'was_limited')
301
        for elem in t:
302
            if elem in kwargs.keys():
303
                kwargs.pop(elem)
304
        super(LoginForm, self).__init__(*args, **kwargs)
305

    
306
        self.fields.keyOrder = ['username', 'password']
307
        if was_limited and settings.RECAPTCHA_ENABLED:
308
            self.fields.keyOrder.extend(['recaptcha_challenge_field',
309
                                         'recaptcha_response_field', ])
310

    
311
    def clean_username(self):
312
        return self.cleaned_data['username'].lower()
313

    
314
    def clean_recaptcha_response_field(self):
315
        if 'recaptcha_challenge_field' in self.cleaned_data:
316
            self.validate_captcha()
317
        return self.cleaned_data['recaptcha_response_field']
318

    
319
    def clean_recaptcha_challenge_field(self):
320
        if 'recaptcha_response_field' in self.cleaned_data:
321
            self.validate_captcha()
322
        return self.cleaned_data['recaptcha_challenge_field']
323

    
324
    def validate_captcha(self):
325
        rcf = self.cleaned_data['recaptcha_challenge_field']
326
        rrf = self.cleaned_data['recaptcha_response_field']
327
        check = captcha.submit(
328
            rcf, rrf, settings.RECAPTCHA_PRIVATE_KEY, self.ip)
329
        if not check.is_valid:
330
            raise forms.ValidationError(_(
331
                astakos_messages.CAPTCHA_VALIDATION_ERR))
332

    
333
    def clean(self):
334
        """
335
        Override default behavior in order to check user's activation later
336
        """
337
        username = self.cleaned_data.get('username')
338

    
339
        if username:
340
            try:
341
                user = AstakosUser.objects.get_by_identifier(username)
342
                if not user.has_auth_provider('local'):
343
                    provider = auth_providers.get_provider('local', user)
344
                    msg = provider.get_login_disabled_msg
345
                    raise forms.ValidationError(mark_safe(msg))
346
            except AstakosUser.DoesNotExist:
347
                pass
348

    
349
        try:
350
            super(LoginForm, self).clean()
351
        except forms.ValidationError:
352
            if self.user_cache is None:
353
                raise
354
            if not self.user_cache.is_active:
355
                msg = self.user_cache.get_inactive_message('local')
356
                raise forms.ValidationError(msg)
357
            if self.request:
358
                if not self.request.session.test_cookie_worked():
359
                    raise
360
        return self.cleaned_data
361

    
362

    
363
class ProfileForm(forms.ModelForm):
364
    """
365
    Subclass of ``ModelForm`` for permiting user to edit his/her profile.
366
    Most of the fields are readonly since the user is not allowed to change
367
    them.
368

369
    The class defines a save method which sets ``is_verified`` to True so as
370
    the user during the next login will not to be redirected to profile page.
371
    """
372
    email = forms.EmailField(label='E-mail address',
373
                             help_text='E-mail address')
374
    renew = forms.BooleanField(label='Renew token', required=False)
375

    
376
    class Meta:
377
        model = AstakosUser
378
        fields = ('email', 'first_name', 'last_name')
379

    
380
    def __init__(self, *args, **kwargs):
381
        self.session_key = kwargs.pop('session_key', None)
382
        super(ProfileForm, self).__init__(*args, **kwargs)
383
        instance = getattr(self, 'instance', None)
384
        ro_fields = ('email',)
385
        if instance and instance.id:
386
            for field in ro_fields:
387
                self.fields[field].widget.attrs['readonly'] = True
388

    
389
    def clean_email(self):
390
        return self.instance.email
391

    
392
    def save(self, commit=True, **kwargs):
393
        user = super(ProfileForm, self).save(commit=False, **kwargs)
394
        user.is_verified = True
395
        if self.cleaned_data.get('renew'):
396
            user.renew_token(
397
                flush_sessions=True,
398
                current_key=self.session_key
399
            )
400
        if commit:
401
            user.save(**kwargs)
402
        return user
403

    
404

    
405
class FeedbackForm(forms.Form):
406
    """
407
    Form for writing feedback.
408
    """
409
    feedback_msg = forms.CharField(widget=forms.Textarea, label=u'Message')
410
    feedback_data = forms.CharField(widget=forms.HiddenInput(), label='',
411
                                    required=False)
412

    
413

    
414
class SendInvitationForm(forms.Form):
415
    """
416
    Form for sending an invitations
417
    """
418

    
419
    email = forms.EmailField(required=True, label='Email address')
420
    first_name = forms.EmailField(label='First name')
421
    last_name = forms.EmailField(label='Last name')
422

    
423

    
424
class ExtendedPasswordResetForm(PasswordResetForm):
425
    """
426
    Extends PasswordResetForm by overriding
427

428
    save method: to pass a custom from_email in send_mail.
429
    clean_email: to handle local auth provider checks
430
    """
431
    def clean_email(self):
432
        # we override the default django auth clean_email to provide more
433
        # detailed messages in case of inactive users
434
        email = self.cleaned_data['email']
435
        try:
436
            user = AstakosUser.objects.get_by_identifier(email)
437
            self.users_cache = [user]
438
            if not user.is_active:
439
                msg = mark_safe(user.get_inactive_message('local'))
440
                raise forms.ValidationError(msg)
441

    
442
            provider = auth_providers.get_provider('local', user)
443
            if not user.has_usable_password():
444
                msg = provider.get_unusable_password_msg
445
                raise forms.ValidationError(mark_safe(msg))
446

    
447
            if not user.can_change_password():
448
                msg = provider.get_cannot_change_password_msg
449
                raise forms.ValidationError(mark_safe(msg))
450

    
451
        except AstakosUser.DoesNotExist:
452
            raise forms.ValidationError(_(astakos_messages.EMAIL_UNKNOWN))
453
        return email
454

    
455
    def save(self, domain_override=None,
456
             email_template_name='registration/password_reset_email.html',
457
             use_https=False, token_generator=default_token_generator,
458
             request=None, **kwargs):
459
        """
460
        Generates a one-use only link for resetting password and sends to the
461
        user.
462

463
        """
464
        for user in self.users_cache:
465
            url = user.astakosuser.get_password_reset_url(token_generator)
466
            url = join_urls(settings.BASE_HOST, url)
467
            c = {
468
                'email': user.email,
469
                'url': url,
470
                'site_name': settings.SITENAME,
471
                'user': user,
472
                'baseurl': settings.BASE_URL,
473
                'support': settings.CONTACT_EMAIL
474
            }
475
            message = render_to_string(email_template_name, c)
476
            from_email = settings.SERVER_EMAIL
477
            send_mail(_(astakos_messages.PASSWORD_RESET_EMAIL_SUBJECT),
478
                      message,
479
                      from_email,
480
                      [user.email],
481
                      connection=get_connection())
482

    
483

    
484
class EmailChangeForm(forms.ModelForm):
485

    
486
    class Meta:
487
        model = EmailChange
488
        fields = ('new_email_address',)
489

    
490
    def clean_new_email_address(self):
491
        addr = self.cleaned_data['new_email_address']
492
        if reserved_verified_email(addr):
493
            raise forms.ValidationError(_(astakos_messages.EMAIL_USED))
494
        return addr
495

    
496
    def save(self, request,
497
             email_template_name='registration/email_change_email.txt',
498
             commit=True, **kwargs):
499
        ec = super(EmailChangeForm, self).save(commit=False, **kwargs)
500
        ec.user = request.user
501
        # delete pending email changes
502
        request.user.emailchanges.all().delete()
503

    
504
        activation_key = hashlib.sha1(
505
            str(random()) + smart_str(ec.new_email_address))
506
        ec.activation_key = activation_key.hexdigest()
507
        if commit:
508
            ec.save(**kwargs)
509
        send_change_email(ec, request, email_template_name=email_template_name)
510

    
511

    
512
class SignApprovalTermsForm(forms.ModelForm):
513

    
514
    class Meta:
515
        model = AstakosUser
516
        fields = ("has_signed_terms",)
517

    
518
    def __init__(self, *args, **kwargs):
519
        super(SignApprovalTermsForm, self).__init__(*args, **kwargs)
520

    
521
    def clean_has_signed_terms(self):
522
        has_signed_terms = self.cleaned_data['has_signed_terms']
523
        if not has_signed_terms:
524
            raise forms.ValidationError(_(astakos_messages.SIGN_TERMS))
525
        return has_signed_terms
526

    
527
    def save(self, commit=True, **kwargs):
528
        user = super(SignApprovalTermsForm, self).save(commit=commit, **kwargs)
529
        user.date_signed_terms = datetime.now()
530
        if commit:
531
            user.save(**kwargs)
532
        return user
533

    
534

    
535
class InvitationForm(forms.ModelForm):
536

    
537
    username = forms.EmailField(label=_("Email"))
538

    
539
    def __init__(self, *args, **kwargs):
540
        super(InvitationForm, self).__init__(*args, **kwargs)
541

    
542
    class Meta:
543
        model = Invitation
544
        fields = ('username', 'realname')
545

    
546
    def clean_username(self):
547
        username = self.cleaned_data['username']
548
        try:
549
            Invitation.objects.get(username=username)
550
            raise forms.ValidationError(
551
                _(astakos_messages.INVITATION_EMAIL_EXISTS))
552
        except Invitation.DoesNotExist:
553
            pass
554
        return username
555

    
556

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

    
568
    def __init__(self, user, *args, **kwargs):
569
        self.session_key = kwargs.pop('session_key', None)
570
        super(ExtendedPasswordChangeForm, self).__init__(user, *args, **kwargs)
571

    
572
    def save(self, commit=True, **kwargs):
573
        try:
574
            if settings.NEWPASSWD_INVALIDATE_TOKEN or \
575
                    self.cleaned_data.get('renew'):
576
                self.user.renew_token()
577
            self.user.flush_sessions(current_key=self.session_key)
578
        except AttributeError:
579
            # if user model does has not such methods
580
            pass
581
        return super(ExtendedPasswordChangeForm, self).save(commit=commit,
582
                                                            **kwargs)
583

    
584

    
585
class ExtendedSetPasswordForm(SetPasswordForm):
586
    """
587
    Extends SetPasswordForm by enabling user
588
    to optionally renew also the token.
589
    """
590
    if not settings.NEWPASSWD_INVALIDATE_TOKEN:
591
        renew = forms.BooleanField(
592
            label='Renew token',
593
            required=False,
594
            initial=True,
595
            help_text='Unsetting this may result in security risk.')
596

    
597
    def __init__(self, user, *args, **kwargs):
598
        super(ExtendedSetPasswordForm, self).__init__(user, *args, **kwargs)
599

    
600
    @transaction.commit_on_success()
601
    def save(self, commit=True, **kwargs):
602
        try:
603
            self.user = AstakosUser.objects.get(id=self.user.id)
604
            if settings.NEWPASSWD_INVALIDATE_TOKEN or \
605
                    self.cleaned_data.get('renew'):
606
                self.user.renew_token()
607

    
608
            provider = auth_providers.get_provider('local', self.user)
609
            if provider.get_add_policy:
610
                provider.add_to_user()
611

    
612
        except BaseException, e:
613
            logger.exception(e)
614
        return super(ExtendedSetPasswordForm, self).save(commit=commit,
615
                                                         **kwargs)
616

    
617

    
618
app_name_label = "Project name"
619
app_name_placeholder = _("myproject.mylab.ntua.gr")
620
app_name_validator = validators.RegexValidator(
621
    DOMAIN_VALUE_REGEX,
622
    _(astakos_messages.DOMAIN_VALUE_ERR),
623
    'invalid')
624
app_name_help = _("""
625
        The project's name should be in a domain format.
626
        The domain shouldn't neccessarily exist in the real
627
        world but is helpful to imply a structure.
628
        e.g.: myproject.mylab.ntua.gr or
629
        myservice.myteam.myorganization""")
630
app_name_widget = forms.TextInput(
631
    attrs={'placeholder': app_name_placeholder})
632

    
633

    
634
app_home_label = "Homepage URL"
635
app_home_placeholder = 'myinstitution.org/myproject/'
636
app_home_help = _("""
637
        URL pointing at your project's site.
638
        e.g.: myinstitution.org/myproject/.
639
        Leave blank if there is no website.""")
640
app_home_widget = forms.TextInput(
641
    attrs={'placeholder': app_home_placeholder})
642

    
643
app_desc_label = _("Description")
644
app_desc_help = _("""
645
        Please provide a short but descriptive abstract of your
646
        project, so that anyone searching can quickly understand
647
        what this project is about.""")
648

    
649
app_comment_label = _("Comments for review (private)")
650
app_comment_help = _("""
651
        Write down any comments you may have for the reviewer
652
        of this application (e.g. background and rationale to
653
        support your request).
654
        The comments are strictly for the review process
655
        and will not be made public.""")
656

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

    
663
app_end_date_label = _("Termination date")
664
app_end_date_help = _("""
665
        At this date, the project will be automatically terminated
666
        and its resource grants revoked from all members. If you are
667
        not certain, it is best to start with a conservative estimation.
668
        You can always re-apply for an extension, if you need.""")
669

    
670
join_policy_label = _("Joining policy")
671
app_member_join_policy_help = _("""
672
        Select how new members are accepted into the project.""")
673
leave_policy_label = _("Leaving policy")
674
app_member_leave_policy_help = _("""
675
        Select how new members can leave the project.""")
676

    
677
max_members_label = _("Maximum member count")
678
max_members_help = _("""
679
        Specify the maximum number of members this project may have,
680
        including the owner. Beyond this number, no new members
681
        may join the project and be granted the project resources.
682
        If you are not certain, it is best to start with a conservative
683
        limit. You can always request a raise when you need it.""")
684

    
685
join_policies = presentation.PROJECT_MEMBER_JOIN_POLICIES.items()
686
leave_policies = presentation.PROJECT_MEMBER_LEAVE_POLICIES.items()
687

    
688

    
689
class ProjectApplicationForm(forms.ModelForm):
690

    
691
    name = forms.CharField(
692
        label=app_name_label,
693
        help_text=app_name_help,
694
        widget=app_name_widget,
695
        validators=[app_name_validator])
696

    
697
    homepage = forms.URLField(
698
        label=app_home_label,
699
        help_text=app_home_help,
700
        widget=app_home_widget,
701
        required=False)
702

    
703
    description = forms.CharField(
704
        label=app_desc_label,
705
        help_text=app_desc_help,
706
        widget=forms.Textarea,
707
        required=False)
708

    
709
    comments = forms.CharField(
710
        label=app_comment_label,
711
        help_text=app_comment_help,
712
        widget=forms.Textarea,
713
        required=False)
714

    
715
    start_date = forms.DateTimeField(
716
        label=app_start_date_label,
717
        help_text=app_start_date_help,
718
        required=False)
719

    
720
    end_date = forms.DateTimeField(
721
        label=app_end_date_label,
722
        help_text=app_end_date_help)
723

    
724
    member_join_policy = forms.TypedChoiceField(
725
        label=join_policy_label,
726
        help_text=app_member_join_policy_help,
727
        initial=2,
728
        coerce=int,
729
        choices=join_policies)
730

    
731
    member_leave_policy = forms.TypedChoiceField(
732
        label=leave_policy_label,
733
        help_text=app_member_leave_policy_help,
734
        coerce=int,
735
        choices=leave_policies)
736

    
737
    limit_on_members_number = forms.IntegerField(
738
        label=max_members_label,
739
        help_text=max_members_help,
740
        min_value=0,
741
        required=True)
742

    
743
    class Meta:
744
        model = ProjectApplication
745
        fields = ('name', 'homepage', 'description',
746
                  'start_date', 'end_date', 'comments',
747
                  'member_join_policy', 'member_leave_policy',
748
                  'limit_on_members_number')
749

    
750
    def __init__(self, *args, **kwargs):
751
        instance = kwargs.get('instance')
752
        self.precursor_application = instance
753
        super(ProjectApplicationForm, self).__init__(*args, **kwargs)
754
        # in case of new application remove closed join policy
755
        if not instance:
756
            policies = presentation.PROJECT_MEMBER_JOIN_POLICIES.copy()
757
            policies.pop(3)
758
            self.fields['member_join_policy'].choices = policies.iteritems()
759

    
760
    def clean_start_date(self):
761
        start_date = self.cleaned_data.get('start_date')
762
        if not self.precursor_application:
763
            today = datetime.now()
764
            today = datetime(today.year, today.month, today.day)
765
            if start_date and (start_date - today).days < 0:
766
                raise forms.ValidationError(
767
                    _(astakos_messages.INVALID_PROJECT_START_DATE))
768
        return start_date
769

    
770
    def clean_end_date(self):
771
        start_date = self.cleaned_data.get('start_date')
772
        end_date = self.cleaned_data.get('end_date')
773
        today = datetime.now()
774
        today = datetime(today.year, today.month, today.day)
775
        if end_date and (end_date - today).days < 0:
776
            raise forms.ValidationError(
777
                _(astakos_messages.INVALID_PROJECT_END_DATE))
778
        if start_date and (end_date - start_date).days <= 0:
779
            raise forms.ValidationError(
780
                _(astakos_messages.INCONSISTENT_PROJECT_DATES))
781
        return end_date
782

    
783
    def clean(self):
784
        userid = self.data.get('user', None)
785
        self.resource_policies
786
        self.user = None
787
        if userid:
788
            try:
789
                self.user = AstakosUser.objects.get(id=userid)
790
            except AstakosUser.DoesNotExist:
791
                pass
792
        if not self.user:
793
            raise forms.ValidationError(_(astakos_messages.NO_APPLICANT))
794
        super(ProjectApplicationForm, self).clean()
795
        return self.cleaned_data
796

    
797
    @property
798
    def resource_policies(self):
799
        policies = []
800
        append = policies.append
801
        for name, value in self.data.iteritems():
802
            if not value:
803
                continue
804
            uplimit = value
805
            if name.endswith('_uplimit'):
806
                subs = name.split('_uplimit')
807
                prefix, suffix = subs
808
                try:
809
                    resource = Resource.objects.get(name=prefix)
810
                except Resource.DoesNotExist:
811
                    raise forms.ValidationError("Resource %s does not exist" %
812
                                                resource.name)
813
                # keep only resource limits for selected resource groups
814
                if self.data.get('is_selected_%s' %
815
                                 resource.group, "0") == "1":
816
                    if not resource.ui_visible:
817
                        raise forms.ValidationError("Invalid resource %s" %
818
                                                    resource.name)
819
                    d = model_to_dict(resource)
820
                    try:
821
                        uplimit = long(uplimit)
822
                    except ValueError:
823
                        m = "Limit should be an integer"
824
                        raise forms.ValidationError(m)
825
                    display = units.show(uplimit, resource.unit)
826
                    d.update(dict(resource=prefix, uplimit=uplimit,
827
                                  display_uplimit=display))
828
                    append(d)
829

    
830
        ordered_keys = presentation.RESOURCES['resources_order']
831

    
832
        def resource_order(r):
833
            if r['str_repr'] in ordered_keys:
834
                return ordered_keys.index(r['str_repr'])
835
            else:
836
                return -1
837

    
838
        policies = sorted(policies, key=resource_order)
839
        return policies
840

    
841
    def cleaned_resource_policies(self):
842
        policies = {}
843
        for d in self.resource_policies:
844
            policies[d["name"]] = {
845
                "project_capacity": None,
846
                "member_capacity": d["uplimit"]
847
            }
848

    
849
        return policies
850

    
851
    def save(self, commit=True, **kwargs):
852
        data = dict(self.cleaned_data)
853
        is_new = self.instance.id is None
854
        data['project_id'] = self.instance.chain.id if not is_new else None
855
        data['owner'] = self.user if is_new else self.instance.owner
856
        data['resources'] = self.cleaned_resource_policies()
857
        data['request_user'] = self.user
858
        submit_application(**data)
859

    
860

    
861
class ProjectSortForm(forms.Form):
862
    sorting = forms.ChoiceField(
863
        label='Sort by',
864
        choices=(('name', 'Sort by Name'),
865
                 ('issue_date', 'Sort by Issue date'),
866
                 ('start_date', 'Sort by Start Date'),
867
                 ('end_date', 'Sort by End Date'),
868
                 # ('approved_members_num', 'Sort by Participants'),
869
                 ('state', 'Sort by Status'),
870
                 ('member_join_policy__description',
871
                  'Sort by Member Join Policy'),
872
                 ('member_leave_policy__description',
873
                  'Sort by Member Leave Policy'),
874
                 ('-name', 'Sort by Name'),
875
                 ('-issue_date', 'Sort by Issue date'),
876
                 ('-start_date', 'Sort by Start Date'),
877
                 ('-end_date', 'Sort by End Date'),
878
                 # ('-approved_members_num', 'Sort by Participants'),
879
                 ('-state', 'Sort by Status'),
880
                 ('-member_join_policy__description',
881
                  'Sort by Member Join Policy'),
882
                 ('-member_leave_policy__description',
883
                  'Sort by Member Leave Policy')
884
                 ),
885
        required=True
886
    )
887

    
888

    
889
class AddProjectMembersForm(forms.Form):
890
    q = forms.CharField(
891
        widget=forms.Textarea(
892
            attrs={
893
                'placeholder':
894
                astakos_messages.ADD_PROJECT_MEMBERS_Q_PLACEHOLDER}),
895
        label=_('Add members'),
896
        help_text=_(astakos_messages.ADD_PROJECT_MEMBERS_Q_HELP),
897
        required=True,)
898

    
899
    def __init__(self, *args, **kwargs):
900
        chain_id = kwargs.pop('chain_id', None)
901
        if chain_id:
902
            self.project = Project.objects.get(id=chain_id)
903
        self.request_user = kwargs.pop('request_user', None)
904
        super(AddProjectMembersForm, self).__init__(*args, **kwargs)
905

    
906
    def clean(self):
907
        try:
908
            accept_membership_project_checks(self.project, self.request_user)
909
        except ProjectError as e:
910
            raise forms.ValidationError(e)
911

    
912
        q = self.cleaned_data.get('q') or ''
913
        users = q.split(',')
914
        users = list(u.strip() for u in users if u)
915
        db_entries = AstakosUser.objects.verified().filter(email__in=users)
916
        unknown = list(set(users) - set(u.email for u in db_entries))
917
        if unknown:
918
            raise forms.ValidationError(
919
                _(astakos_messages.UNKNOWN_USERS) % ','.join(unknown))
920
        self.valid_users = db_entries
921
        return self.cleaned_data
922

    
923
    def get_valid_users(self):
924
        """Should be called after form cleaning"""
925
        try:
926
            return self.valid_users
927
        except:
928
            return ()
929

    
930

    
931
class ProjectMembersSortForm(forms.Form):
932
    sorting = forms.ChoiceField(
933
        label='Sort by',
934
        choices=(('person__email', 'User Id'),
935
                 ('person__first_name', 'Name'),
936
                 ('acceptance_date', 'Acceptance date')
937
                 ),
938
        required=True
939
    )
940

    
941

    
942
class ProjectSearchForm(forms.Form):
943
    q = forms.CharField(max_length=200, label='Search project', required=False)
944

    
945

    
946
class ExtendedProfileForm(ProfileForm):
947
    """
948
    Profile form that combines `email change` and `password change` user
949
    actions by propagating submited data to internal EmailChangeForm
950
    and ExtendedPasswordChangeForm objects.
951
    """
952

    
953
    password_change_form = None
954
    email_change_form = None
955

    
956
    password_change = False
957
    email_change = False
958

    
959
    extra_forms_fields = {
960
        'email': ['new_email_address'],
961
        'password': ['old_password', 'new_password1', 'new_password2']
962
    }
963

    
964
    fields = ('email')
965
    change_password = forms.BooleanField(initial=False, required=False)
966
    change_email = forms.BooleanField(initial=False, required=False)
967

    
968
    email_changed = False
969
    password_changed = False
970

    
971
    def __init__(self, *args, **kwargs):
972
        session_key = kwargs.get('session_key', None)
973
        self.fields_list = [
974
            'email',
975
            'new_email_address',
976
            'first_name',
977
            'last_name',
978
            'old_password',
979
            'new_password1',
980
            'new_password2',
981
            'change_email',
982
            'change_password',
983
        ]
984

    
985
        super(ExtendedProfileForm, self).__init__(*args, **kwargs)
986
        self.session_key = session_key
987
        if self.instance.can_change_password():
988
            self.password_change = True
989
        else:
990
            self.fields_list.remove('old_password')
991
            self.fields_list.remove('new_password1')
992
            self.fields_list.remove('new_password2')
993
            self.fields_list.remove('change_password')
994
            del self.fields['change_password']
995

    
996
        if settings.EMAILCHANGE_ENABLED and self.instance.can_change_email():
997
            self.email_change = True
998
        else:
999
            self.fields_list.remove('new_email_address')
1000
            self.fields_list.remove('change_email')
1001
            del self.fields['change_email']
1002

    
1003
        self._init_extra_forms()
1004
        self.save_extra_forms = []
1005
        self.success_messages = []
1006
        self.fields.keyOrder = self.fields_list
1007

    
1008
    def _init_extra_form_fields(self):
1009
        if self.email_change:
1010
            self.fields.update(self.email_change_form.fields)
1011
            self.fields['new_email_address'].required = False
1012
            self.fields['email'].help_text = _(
1013
                'Change the email associated with '
1014
                'your account. This email will '
1015
                'remain active until you verify '
1016
                'your new one.')
1017

    
1018
        if self.password_change:
1019
            self.fields.update(self.password_change_form.fields)
1020
            self.fields['old_password'].required = False
1021
            self.fields['old_password'].label = _('Password')
1022
            self.fields['old_password'].help_text = _('Change your password.')
1023
            self.fields['old_password'].initial = 'password'
1024
            self.fields['new_password1'].required = False
1025
            self.fields['new_password2'].required = False
1026

    
1027
    def _update_extra_form_errors(self):
1028
        if self.cleaned_data.get('change_password'):
1029
            self.errors.update(self.password_change_form.errors)
1030
        if self.cleaned_data.get('change_email'):
1031
            self.errors.update(self.email_change_form.errors)
1032

    
1033
    def _init_extra_forms(self):
1034
        self.email_change_form = EmailChangeForm(self.data)
1035
        self.password_change_form = ExtendedPasswordChangeForm(
1036
            user=self.instance,
1037
            data=self.data, session_key=self.session_key)
1038
        self._init_extra_form_fields()
1039

    
1040
    def is_valid(self):
1041
        password, email = True, True
1042
        profile = super(ExtendedProfileForm, self).is_valid()
1043
        if profile and self.cleaned_data.get('change_password', None):
1044
            self.password_change_form.fields['new_password1'].required = True
1045
            self.password_change_form.fields['new_password2'].required = True
1046
            password = self.password_change_form.is_valid()
1047
            self.save_extra_forms.append('password')
1048
        if profile and self.cleaned_data.get('change_email'):
1049
            self.fields['new_email_address'].required = True
1050
            email = self.email_change_form.is_valid()
1051
            self.save_extra_forms.append('email')
1052

    
1053
        if not password or not email:
1054
            self._update_extra_form_errors()
1055

    
1056
        return all([profile, password, email])
1057

    
1058
    def save(self, request, *args, **kwargs):
1059
        if 'email' in self.save_extra_forms:
1060
            self.email_change_form.save(request, *args, **kwargs)
1061
            self.email_changed = True
1062
        if 'password' in self.save_extra_forms:
1063
            self.password_change_form.save(*args, **kwargs)
1064
            self.password_changed = True
1065
        return super(ExtendedProfileForm, self).save(*args, **kwargs)