Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / forms.py @ 73fbaec4

History | View | Annotate | Download (37.7 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 urlparse import urljoin
34
from random import random
35

    
36
from django import forms
37
from django.utils.translation import ugettext as _
38
from django.contrib.auth.forms import (
39
    UserCreationForm, AuthenticationForm,
40
    PasswordResetForm, PasswordChangeForm,
41
    SetPasswordForm)
42
from django.core.mail import send_mail
43
from django.contrib.auth.tokens import default_token_generator
44
from django.template import Context, loader
45
from django.utils.http import int_to_base36
46
from django.core.urlresolvers import reverse
47
from django.utils.safestring import mark_safe
48
from django.utils.encoding import smart_str
49
from django.conf import settings
50
from django.forms.models import fields_for_model
51
from django.db import transaction
52
from django.utils.encoding import smart_unicode
53
from django.core import validators
54
from django.contrib.auth.models import AnonymousUser
55

    
56
from astakos.im.models import (
57
    AstakosUser, EmailChange, Invitation,
58
#     AstakosGroup, GroupKind,
59
    Resource, PendingThirdPartyUser, get_latest_terms, RESOURCE_SEPARATOR,
60
    ProjectApplication)
61
from astakos.im.settings import (
62
    INVITATIONS_PER_LEVEL, BASEURL, SITENAME, RECAPTCHA_PRIVATE_KEY,
63
    RECAPTCHA_ENABLED, DEFAULT_CONTACT_EMAIL, LOGGING_LEVEL,
64
    PASSWORD_RESET_EMAIL_SUBJECT, NEWPASSWD_INVALIDATE_TOKEN,
65
    MODERATION_ENABLED)
66
from astakos.im.widgets import DummyWidget, RecaptchaWidget
67
from astakos.im.functions import send_change_email, submit_application
68

    
69
from astakos.im.util import reserved_email, get_query
70

    
71
import astakos.im.messages as astakos_messages
72

    
73
import logging
74
import hashlib
75
import recaptcha.client.captcha as captcha
76
import re
77

    
78
logger = logging.getLogger(__name__)
79

    
80
DOMAIN_VALUE_REGEX = re.compile(
81
    r'^(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.){0,126}(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?))$',
82
    re.IGNORECASE)
83

    
84
class StoreUserMixin(object):
85
    @transaction.commit_on_success
86
    def store_user(self, user, request):
87
        user.save()
88
        self.post_store_user(user, request)
89
        return user
90

    
91
    def post_store_user(self, user, request):
92
        """
93
        Interface method for descendant backends to be able to do stuff within
94
        the transaction enabled by store_user.
95
        """
96
        pass
97

    
98

    
99
class LocalUserCreationForm(UserCreationForm, StoreUserMixin):
100
    """
101
    Extends the built in UserCreationForm in several ways:
102

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

    
111
    class Meta:
112
        model = AstakosUser
113
        fields = ("email", "first_name", "last_name",
114
                  "has_signed_terms", "has_signed_terms")
115

    
116
    def __init__(self, *args, **kwargs):
117
        """
118
        Changes the order of fields, and removes the username field.
119
        """
120
        request = kwargs.pop('request', None)
121
        if request:
122
            self.ip = request.META.get('REMOTE_ADDR',
123
                                       request.META.get('HTTP_X_REAL_IP', None))
124

    
125
        super(LocalUserCreationForm, self).__init__(*args, **kwargs)
126
        self.fields.keyOrder = ['email', 'first_name', 'last_name',
127
                                'password1', 'password2']
128

    
129
        if RECAPTCHA_ENABLED:
130
            self.fields.keyOrder.extend(['recaptcha_challenge_field',
131
                                         'recaptcha_response_field', ])
132
        if get_latest_terms():
133
            self.fields.keyOrder.append('has_signed_terms')
134

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

    
143
    def clean_email(self):
144
        email = self.cleaned_data['email'].lower()
145
        if not email:
146
            raise forms.ValidationError(_(astakos_messages.REQUIRED_FIELD))
147
        if reserved_email(email):
148
            raise forms.ValidationError(_(astakos_messages.EMAIL_USED))
149
        return email
150

    
151
    def clean_has_signed_terms(self):
152
        has_signed_terms = self.cleaned_data['has_signed_terms']
153
        if not has_signed_terms:
154
            raise forms.ValidationError(_(astakos_messages.SIGN_TERMS))
155
        return has_signed_terms
156

    
157
    def clean_recaptcha_response_field(self):
158
        if 'recaptcha_challenge_field' in self.cleaned_data:
159
            self.validate_captcha()
160
        return self.cleaned_data['recaptcha_response_field']
161

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

    
167
    def validate_captcha(self):
168
        rcf = self.cleaned_data['recaptcha_challenge_field']
169
        rrf = self.cleaned_data['recaptcha_response_field']
170
        check = captcha.submit(rcf, rrf, RECAPTCHA_PRIVATE_KEY, self.ip)
171
        if not check.is_valid:
172
            raise forms.ValidationError(_(astakos_messages.CAPTCHA_VALIDATION_ERR))
173

    
174
    def post_store_user(self, user, request):
175
        """
176
        Interface method for descendant backends to be able to do stuff within
177
        the transaction enabled by store_user.
178
        """
179
        user.add_auth_provider('local', auth_backend='astakos')
180
        user.set_password(self.cleaned_data['password1'])
181

    
182
    def save(self, commit=True):
183
        """
184
        Saves the email, first_name and last_name properties, after the normal
185
        save behavior is complete.
186
        """
187
        user = super(LocalUserCreationForm, self).save(commit=False)
188
        user.renew_token()
189
        if commit:
190
            user.save()
191
            logger.log(LOGGING_LEVEL, 'Created user %s' % user.email)
192
        return user
193

    
194

    
195
class InvitedLocalUserCreationForm(LocalUserCreationForm):
196
    """
197
    Extends the LocalUserCreationForm: email is readonly.
198
    """
199
    class Meta:
200
        model = AstakosUser
201
        fields = ("email", "first_name", "last_name", "has_signed_terms")
202

    
203
    def __init__(self, *args, **kwargs):
204
        """
205
        Changes the order of fields, and removes the username field.
206
        """
207
        super(InvitedLocalUserCreationForm, self).__init__(*args, **kwargs)
208

    
209
        #set readonly form fields
210
        ro = ('email', 'username',)
211
        for f in ro:
212
            self.fields[f].widget.attrs['readonly'] = True
213

    
214
    def save(self, commit=True):
215
        user = super(InvitedLocalUserCreationForm, self).save(commit=False)
216
        user.set_invitations_level()
217
        user.email_verified = True
218
        if commit:
219
            user.save()
220
        return user
221

    
222

    
223
class ThirdPartyUserCreationForm(forms.ModelForm, StoreUserMixin):
224
    id = forms.CharField(
225
        widget=forms.HiddenInput(),
226
        label='',
227
        required=False
228
    )
229
    third_party_identifier = forms.CharField(
230
        widget=forms.HiddenInput(),
231
        label=''
232
    )
233
    class Meta:
234
        model = AstakosUser
235
        fields = ['id', 'email', 'third_party_identifier', 'first_name', 'last_name']
236

    
237
    def __init__(self, *args, **kwargs):
238
        """
239
        Changes the order of fields, and removes the username field.
240
        """
241
        self.request = kwargs.get('request', None)
242
        if self.request:
243
            kwargs.pop('request')
244

    
245
        latest_terms = get_latest_terms()
246
        if latest_terms:
247
            self._meta.fields.append('has_signed_terms')
248

    
249
        super(ThirdPartyUserCreationForm, self).__init__(*args, **kwargs)
250

    
251
        if latest_terms:
252
            self.fields.keyOrder.append('has_signed_terms')
253

    
254
        if 'has_signed_terms' in self.fields:
255
            # Overriding field label since we need to apply a link
256
            # to the terms within the label
257
            terms_link_html = '<a href="%s" target="_blank">%s</a>' \
258
                % (reverse('latest_terms'), _("the terms"))
259
            self.fields['has_signed_terms'].label = \
260
                    mark_safe("I agree with %s" % terms_link_html)
261

    
262
    def clean_email(self):
263
        email = self.cleaned_data['email'].lower()
264
        if not email:
265
            raise forms.ValidationError(_(astakos_messages.REQUIRED_FIELD))
266
        return email
267

    
268
    def clean_has_signed_terms(self):
269
        has_signed_terms = self.cleaned_data['has_signed_terms']
270
        if not has_signed_terms:
271
            raise forms.ValidationError(_(astakos_messages.SIGN_TERMS))
272
        return has_signed_terms
273

    
274
    def post_store_user(self, user, request):
275
        pending = PendingThirdPartyUser.objects.get(
276
                                token=request.POST.get('third_party_token'),
277
                                third_party_identifier= \
278
            self.cleaned_data.get('third_party_identifier'))
279
        return user.add_pending_auth_provider(pending)
280

    
281

    
282
    def save(self, commit=True):
283
        user = super(ThirdPartyUserCreationForm, self).save(commit=False)
284
        user.set_unusable_password()
285
        user.renew_token()
286
        if commit:
287
            user.save()
288
            logger.log(LOGGING_LEVEL, 'Created user %s' % user.email)
289
        return user
290

    
291

    
292
class InvitedThirdPartyUserCreationForm(ThirdPartyUserCreationForm):
293
    """
294
    Extends the ThirdPartyUserCreationForm: email is readonly.
295
    """
296
    def __init__(self, *args, **kwargs):
297
        """
298
        Changes the order of fields, and removes the username field.
299
        """
300
        super(
301
            InvitedThirdPartyUserCreationForm, self).__init__(*args, **kwargs)
302

    
303
        #set readonly form fields
304
        ro = ('email',)
305
        for f in ro:
306
            self.fields[f].widget.attrs['readonly'] = True
307

    
308
    def save(self, commit=True):
309
        user = super(InvitedThirdPartyUserCreationForm, self).save(commit=False)
310
        user.set_invitation_level()
311
        user.email_verified = True
312
        if commit:
313
            user.save()
314
        return user
315

    
316

    
317
class ShibbolethUserCreationForm(ThirdPartyUserCreationForm):
318
    additional_email = forms.CharField(
319
        widget=forms.HiddenInput(), label='', required=False)
320

    
321
    def __init__(self, *args, **kwargs):
322
        super(ShibbolethUserCreationForm, self).__init__(*args, **kwargs)
323
        # copy email value to additional_mail in case user will change it
324
        name = 'email'
325
        field = self.fields[name]
326
        self.initial['additional_email'] = self.initial.get(name, field.initial)
327
        self.initial['email'] = None
328

    
329
    def clean_email(self):
330
        email = self.cleaned_data['email'].lower()
331
        if self.instance:
332
            if self.instance.email == email:
333
                raise forms.ValidationError(_("This is your current email."))
334
        for user in AstakosUser.objects.filter(email__iexact=email):
335
            if user.provider == 'shibboleth':
336
                raise forms.ValidationError(_(
337
                        "This email is already associated with another \
338
                         shibboleth account."
339
                    )
340
                )
341
            else:
342
                raise forms.ValidationError(_("This email is already used"))
343
        super(ShibbolethUserCreationForm, self).clean_email()
344
        return email
345

    
346

    
347
class InvitedShibbolethUserCreationForm(ShibbolethUserCreationForm,
348
                                        InvitedThirdPartyUserCreationForm):
349
    pass
350

    
351

    
352
class LoginForm(AuthenticationForm):
353
    username = forms.EmailField(label=_("Email"))
354
    recaptcha_challenge_field = forms.CharField(widget=DummyWidget)
355
    recaptcha_response_field = forms.CharField(
356
        widget=RecaptchaWidget, label='')
357

    
358
    def __init__(self, *args, **kwargs):
359
        was_limited = kwargs.get('was_limited', False)
360
        request = kwargs.get('request', None)
361
        if request:
362
            self.ip = request.META.get('REMOTE_ADDR',
363
                                       request.META.get('HTTP_X_REAL_IP', None))
364

    
365
        t = ('request', 'was_limited')
366
        for elem in t:
367
            if elem in kwargs.keys():
368
                kwargs.pop(elem)
369
        super(LoginForm, self).__init__(*args, **kwargs)
370

    
371
        self.fields.keyOrder = ['username', 'password']
372
        if was_limited and RECAPTCHA_ENABLED:
373
            self.fields.keyOrder.extend(['recaptcha_challenge_field',
374
                                         'recaptcha_response_field', ])
375

    
376
    def clean_username(self):
377
        if 'username' in self.cleaned_data:
378
            return self.cleaned_data['username'].lower()
379

    
380
    def clean_recaptcha_response_field(self):
381
        if 'recaptcha_challenge_field' in self.cleaned_data:
382
            self.validate_captcha()
383
        return self.cleaned_data['recaptcha_response_field']
384

    
385
    def clean_recaptcha_challenge_field(self):
386
        if 'recaptcha_response_field' in self.cleaned_data:
387
            self.validate_captcha()
388
        return self.cleaned_data['recaptcha_challenge_field']
389

    
390
    def validate_captcha(self):
391
        rcf = self.cleaned_data['recaptcha_challenge_field']
392
        rrf = self.cleaned_data['recaptcha_response_field']
393
        check = captcha.submit(rcf, rrf, RECAPTCHA_PRIVATE_KEY, self.ip)
394
        if not check.is_valid:
395
            raise forms.ValidationError(_(astakos_messages.CAPTCHA_VALIDATION_ERR))
396

    
397
    def clean(self):
398
        """
399
        Override default behavior in order to check user's activation later
400
        """
401
        try:
402
            super(LoginForm, self).clean()
403
        except forms.ValidationError, e:
404
#            if self.user_cache is None:
405
#                raise
406
            if self.request:
407
                if not self.request.session.test_cookie_worked():
408
                    raise
409
        return self.cleaned_data
410

    
411

    
412
class ProfileForm(forms.ModelForm):
413
    """
414
    Subclass of ``ModelForm`` for permiting user to edit his/her profile.
415
    Most of the fields are readonly since the user is not allowed to change
416
    them.
417

418
    The class defines a save method which sets ``is_verified`` to True so as the
419
    user during the next login will not to be redirected to profile page.
420
    """
421
    renew = forms.BooleanField(label='Renew token', required=False)
422

    
423
    class Meta:
424
        model = AstakosUser
425
        fields = ('email', 'first_name', 'last_name', 'auth_token',
426
                  'auth_token_expires')
427

    
428
    def __init__(self, *args, **kwargs):
429
        self.session_key = kwargs.pop('session_key', None)
430
        super(ProfileForm, self).__init__(*args, **kwargs)
431
        instance = getattr(self, 'instance', None)
432
        ro_fields = ('email', 'auth_token', 'auth_token_expires')
433
        if instance and instance.id:
434
            for field in ro_fields:
435
                self.fields[field].widget.attrs['readonly'] = True
436

    
437
    def save(self, commit=True):
438
        user = super(ProfileForm, self).save(commit=False)
439
        user.is_verified = True
440
        if self.cleaned_data.get('renew'):
441
            user.renew_token(
442
                flush_sessions=True,
443
                current_key=self.session_key
444
            )
445
        if commit:
446
            user.save()
447
        return user
448

    
449

    
450
class FeedbackForm(forms.Form):
451
    """
452
    Form for writing feedback.
453
    """
454
    feedback_msg = forms.CharField(widget=forms.Textarea, label=u'Message')
455
    feedback_data = forms.CharField(widget=forms.HiddenInput(), label='',
456
                                    required=False)
457

    
458

    
459
class SendInvitationForm(forms.Form):
460
    """
461
    Form for sending an invitations
462
    """
463

    
464
    email = forms.EmailField(required=True, label='Email address')
465
    first_name = forms.EmailField(label='First name')
466
    last_name = forms.EmailField(label='Last name')
467

    
468

    
469
class ExtendedPasswordResetForm(PasswordResetForm):
470
    """
471
    Extends PasswordResetForm by overriding save method:
472
    passes a custom from_email in send_mail.
473

474
    Since Django 1.3 this is useless since ``django.contrib.auth.views.reset_password``
475
    accepts a from_email argument.
476
    """
477
    def clean_email(self):
478
        email = super(ExtendedPasswordResetForm, self).clean_email()
479
        try:
480
            user = AstakosUser.objects.get(email__iexact=email, is_active=True)
481
            if not user.has_usable_password():
482
                raise forms.ValidationError(_(astakos_messages.UNUSABLE_PASSWORD))
483

    
484
            if not user.can_change_password():
485
                raise forms.ValidationError(_('Password change for this account'
486
                                              ' is not supported.'))
487

    
488
        except AstakosUser.DoesNotExist, e:
489
            raise forms.ValidationError(_(astakos_messages.EMAIL_UNKNOWN))
490
        return email
491

    
492
    def save(
493
        self, domain_override=None, email_template_name='registration/password_reset_email.html',
494
            use_https=False, token_generator=default_token_generator, request=None):
495
        """
496
        Generates a one-use only link for resetting password and sends to the user.
497
        """
498
        for user in self.users_cache:
499
            url = user.astakosuser.get_password_reset_url(token_generator)
500
            url = urljoin(BASEURL, url)
501
            t = loader.get_template(email_template_name)
502
            c = {
503
                'email': user.email,
504
                'url': url,
505
                'site_name': SITENAME,
506
                'user': user,
507
                'baseurl': BASEURL,
508
                'support': DEFAULT_CONTACT_EMAIL
509
            }
510
            from_email = settings.SERVER_EMAIL
511
            send_mail(_(PASSWORD_RESET_EMAIL_SUBJECT),
512
                      t.render(Context(c)), from_email, [user.email])
513

    
514

    
515
class EmailChangeForm(forms.ModelForm):
516
    class Meta:
517
        model = EmailChange
518
        fields = ('new_email_address',)
519

    
520
    def clean_new_email_address(self):
521
        addr = self.cleaned_data['new_email_address']
522
        if AstakosUser.objects.filter(email__iexact=addr):
523
            raise forms.ValidationError(_(astakos_messages.EMAIL_USED))
524
        return addr
525

    
526
    def save(self, email_template_name, request, commit=True):
527
        ec = super(EmailChangeForm, self).save(commit=False)
528
        ec.user = request.user
529
        activation_key = hashlib.sha1(
530
            str(random()) + smart_str(ec.new_email_address))
531
        ec.activation_key = activation_key.hexdigest()
532
        if commit:
533
            ec.save()
534
        send_change_email(ec, request, email_template_name=email_template_name)
535

    
536

    
537
class SignApprovalTermsForm(forms.ModelForm):
538
    class Meta:
539
        model = AstakosUser
540
        fields = ("has_signed_terms",)
541

    
542
    def __init__(self, *args, **kwargs):
543
        super(SignApprovalTermsForm, self).__init__(*args, **kwargs)
544

    
545
    def clean_has_signed_terms(self):
546
        has_signed_terms = self.cleaned_data['has_signed_terms']
547
        if not has_signed_terms:
548
            raise forms.ValidationError(_(astakos_messages.SIGN_TERMS))
549
        return has_signed_terms
550

    
551

    
552
class InvitationForm(forms.ModelForm):
553
    username = forms.EmailField(label=_("Email"))
554

    
555
    def __init__(self, *args, **kwargs):
556
        super(InvitationForm, self).__init__(*args, **kwargs)
557

    
558
    class Meta:
559
        model = Invitation
560
        fields = ('username', 'realname')
561

    
562
    def clean_username(self):
563
        username = self.cleaned_data['username']
564
        try:
565
            Invitation.objects.get(username=username)
566
            raise forms.ValidationError(_(astakos_messages.INVITATION_EMAIL_EXISTS))
567
        except Invitation.DoesNotExist:
568
            pass
569
        return username
570

    
571

    
572
class ExtendedPasswordChangeForm(PasswordChangeForm):
573
    """
574
    Extends PasswordChangeForm by enabling user
575
    to optionally renew also the token.
576
    """
577
    if not NEWPASSWD_INVALIDATE_TOKEN:
578
        renew = forms.BooleanField(label='Renew token', required=False,
579
                                   initial=True,
580
                                   help_text='Unsetting this may result in security risk.')
581

    
582
    def __init__(self, user, *args, **kwargs):
583
        self.session_key = kwargs.pop('session_key', None)
584
        super(ExtendedPasswordChangeForm, self).__init__(user, *args, **kwargs)
585

    
586
    def save(self, commit=True):
587
        try:
588
            if NEWPASSWD_INVALIDATE_TOKEN or self.cleaned_data.get('renew'):
589
                self.user.renew_token()
590
            self.user.flush_sessions(current_key=self.session_key)
591
        except AttributeError:
592
            # if user model does has not such methods
593
            pass
594
        return super(ExtendedPasswordChangeForm, self).save(commit=commit)
595

    
596

    
597
# class AstakosGroupCreationForm(forms.ModelForm):
598
#     kind = forms.ModelChoiceField(
599
#         queryset=GroupKind.objects.all(),
600
#         label="",
601
#         widget=forms.HiddenInput()
602
#     )
603
#     name = forms.CharField(
604
#         validators=[validators.RegexValidator(
605
#             DOMAIN_VALUE_REGEX,
606
#             _(astakos_messages.DOMAIN_VALUE_ERR), 'invalid'
607
#         )],
608
#         widget=forms.TextInput(attrs={'placeholder': 'myproject.mylab.ntua.gr'}),
609
#         help_text=" The Project's name should be in a domain format. The domain shouldn't neccessarily exist in the real world but is helpful to imply a structure. e.g.: myproject.mylab.ntua.gr or myservice.myteam.myorganization "
610
#     )
611
#     homepage = forms.URLField(
612
#         label= 'Homepage Url',
613
#         widget=forms.TextInput(attrs={'placeholder': 'http://myproject.com'}),
614
#         help_text="This should be a URL pointing at your project's site. e.g.: http://myproject.com ",
615
#         required=False
616
#     )
617
#     desc = forms.CharField(
618
#         label= 'Description',
619
#         widget=forms.Textarea, 
620
#         help_text= "Please provide a short but descriptive abstract of your Project, so that anyone searching can quickly understand what this Project is about. "
621
#     )
622
#     issue_date = forms.DateTimeField(
623
#         label= 'Start date',
624
#         help_text= "Here you specify the date you want your Project to start granting its resources. Its members will get the resources coming from this Project on this exact date."
625
#     )
626
#     expiration_date = forms.DateTimeField(
627
#         label= 'End date',
628
#         help_text= "Here you specify the date you want your Project to cease. This means that after this date all members will no longer be able to allocate resources from this Project.  "
629
#     )
630
#     moderation_enabled = forms.BooleanField(
631
#         label= 'Moderated',
632
#         help_text="Select this to approve each member manually, before they become a part of your Project (default). Be sure you know what you are doing, if you uncheck this option. ",
633
#         required=False,
634
#         initial=True
635
#     )
636
#     max_participants = forms.IntegerField(
637
#         label='Total number of members',
638
#         required=True, min_value=1,
639
#         help_text="Here you specify the number of members this Project is going to have. This means that this number of people will be granted the resources you will specify in the next step. This can be '1' if you are the only one wanting to get resources. "
640
#     )
641
# 
642
#     class Meta:
643
#         model = AstakosGroup
644
# 
645
#     def __init__(self, *args, **kwargs):
646
#         #update QueryDict
647
#         args = list(args)
648
#         qd = args.pop(0).copy()
649
#         members_unlimited = qd.pop('members_unlimited', False)
650
#         members_uplimit = qd.pop('members_uplimit', None)
651
# 
652
#         #substitue QueryDict
653
#         args.insert(0, qd)
654
# 
655
#         super(AstakosGroupCreationForm, self).__init__(*args, **kwargs)
656
#         
657
#         self.fields.keyOrder = ['kind', 'name', 'homepage', 'desc',
658
#                                 'issue_date', 'expiration_date',
659
#                                 'moderation_enabled', 'max_participants']
660
#         def add_fields((k, v)):
661
#             k = k.partition('_proxy')[0]
662
#             self.fields[k] = forms.IntegerField(
663
#                 required=False,
664
#                 widget=forms.HiddenInput(),
665
#                 min_value=1
666
#             )
667
#         map(add_fields,
668
#             ((k, v) for k,v in qd.iteritems() if k.endswith('_uplimit'))
669
#         )
670
# 
671
#         def add_fields((k, v)):
672
#             self.fields[k] = forms.BooleanField(
673
#                 required=False,
674
#                 #widget=forms.HiddenInput()
675
#             )
676
#         map(add_fields,
677
#             ((k, v) for k,v in qd.iteritems() if k.startswith('is_selected_'))
678
#         )
679
# 
680
#     def policies(self):
681
#         self.clean()
682
#         policies = []
683
#         append = policies.append
684
#         for name, uplimit in self.cleaned_data.iteritems():
685
# 
686
#             subs = name.split('_uplimit')
687
#             if len(subs) == 2:
688
#                 prefix, suffix = subs
689
#                 s, sep, r = prefix.partition(RESOURCE_SEPARATOR)
690
#                 resource = Resource.objects.get(service__name=s, name=r)
691
# 
692
#                 # keep only resource limits for selected resource groups
693
#                 if self.cleaned_data.get(
694
#                     'is_selected_%s' % resource.group, False
695
#                 ):
696
#                     append(dict(service=s, resource=r, uplimit=uplimit))
697
#         return policies
698
# 
699
# class AstakosGroupCreationSummaryForm(forms.ModelForm):
700
#     kind = forms.ModelChoiceField(
701
#         queryset=GroupKind.objects.all(),
702
#         label="",
703
#         widget=forms.HiddenInput()
704
#     )
705
#     name = forms.CharField(
706
#         widget=forms.TextInput(attrs={'placeholder': 'eg. foo.ece.ntua.gr'}),
707
#         help_text="Name should be in the form of dns"
708
#     )
709
#     moderation_enabled = forms.BooleanField(
710
#         help_text="Check if you want to approve members participation manually",
711
#         required=False,
712
#         initial=True
713
#     )
714
#     max_participants = forms.IntegerField(
715
#         required=False, min_value=1
716
#     )
717
# 
718
#     class Meta:
719
#         model = AstakosGroup
720
# 
721
#     def __init__(self, *args, **kwargs):
722
#         #update QueryDict
723
#         args = list(args)
724
#         qd = args.pop(0).copy()
725
#         members_unlimited = qd.pop('members_unlimited', False)
726
#         members_uplimit = qd.pop('members_uplimit', None)
727
# 
728
#         #substitue QueryDict
729
#         args.insert(0, qd)
730
# 
731
#         super(AstakosGroupCreationSummaryForm, self).__init__(*args, **kwargs)
732
#         self.fields.keyOrder = ['kind', 'name', 'homepage', 'desc',
733
#                                 'issue_date', 'expiration_date',
734
#                                 'moderation_enabled', 'max_participants']
735
#         def add_fields((k, v)):
736
#             self.fields[k] = forms.IntegerField(
737
#                 required=False,
738
#                 widget=forms.TextInput(),
739
#                 min_value=1
740
#             )
741
#         map(add_fields,
742
#             ((k, v) for k,v in qd.iteritems() if k.endswith('_uplimit'))
743
#         )
744
# 
745
#         def add_fields((k, v)):
746
#             self.fields[k] = forms.BooleanField(
747
#                 required=False,
748
#                 widget=forms.HiddenInput()
749
#             )
750
#         map(add_fields,
751
#             ((k, v) for k,v in qd.iteritems() if k.startswith('is_selected_'))
752
#         )
753
#         for f in self.fields.values():
754
#             f.widget = forms.HiddenInput()
755
# 
756
#     def clean(self):
757
#         super(AstakosGroupCreationSummaryForm, self).clean()
758
#         self.cleaned_data['policies'] = []
759
#         append = self.cleaned_data['policies'].append
760
#         #tbd = [f for f in self.fields if (f.startswith('is_selected_') and (not f.endswith('_proxy')))]
761
#         tbd = [f for f in self.fields if f.startswith('is_selected_')]
762
#         for name, uplimit in self.cleaned_data.iteritems():
763
#             subs = name.split('_uplimit')
764
#             if len(subs) == 2:
765
#                 tbd.append(name)
766
#                 prefix, suffix = subs
767
#                 s, sep, r = prefix.partition(RESOURCE_SEPARATOR)
768
#                 resource = Resource.objects.get(service__name=s, name=r)
769
# 
770
#                 # keep only resource limits for selected resource groups
771
#                 if self.cleaned_data.get(
772
#                     'is_selected_%s' % resource.group, False
773
#                 ):
774
#                     append(dict(service=s, resource=r, uplimit=uplimit))
775
#         for name in tbd:
776
#             self.cleaned_data.pop(name, None)
777
#         return self.cleaned_data
778
# 
779
# class AstakosGroupUpdateForm(forms.ModelForm):
780
#     class Meta:
781
#         model = AstakosGroup
782
#         fields = ( 'desc','homepage', 'moderation_enabled')
783
# 
784
# 
785
# class AddGroupMembersForm(forms.Form):
786
#     q = forms.CharField(
787
#         max_length=800, widget=forms.Textarea, label=_('Add members'),
788
#         help_text=_(astakos_messages.ADD_GROUP_MEMBERS_Q_HELP),
789
#         required=True)
790
# 
791
#     def clean(self):
792
#         q = self.cleaned_data.get('q') or ''
793
#         users = q.split(',')
794
#         users = list(u.strip() for u in users if u)
795
#         db_entries = AstakosUser.objects.filter(email__in=users)
796
#         unknown = list(set(users) - set(u.email for u in db_entries))
797
#         if unknown:
798
#             raise forms.ValidationError(_(astakos_messages.UNKNOWN_USERS) % ','.join(unknown))
799
#         self.valid_users = db_entries
800
#         return self.cleaned_data
801
# 
802
#     def get_valid_users(self):
803
#         """Should be called after form cleaning"""
804
#         try:
805
#             return self.valid_users
806
#         except:
807
#             return ()
808
# 
809
# 
810
# class AstakosGroupSearchForm(forms.Form):
811
#     q = forms.CharField(max_length=200, label='Search project')
812
# 
813
# 
814
# class TimelineForm(forms.Form):
815
#     entity = forms.ModelChoiceField(
816
#         queryset=AstakosUser.objects.filter(is_active=True)
817
#     )
818
#     resource = forms.ModelChoiceField(
819
#         queryset=Resource.objects.all()
820
#     )
821
#     start_date = forms.DateTimeField()
822
#     end_date = forms.DateTimeField()
823
#     details = forms.BooleanField(required=False, label="Detailed Listing")
824
#     operation = forms.ChoiceField(
825
#         label='Charge Method',
826
#         choices=(('', '-------------'),
827
#                  ('charge_usage', 'Charge Usage'),
828
#                  ('charge_traffic', 'Charge Traffic'), )
829
#     )
830
# 
831
#     def clean(self):
832
#         super(TimelineForm, self).clean()
833
#         d = self.cleaned_data
834
#         if 'resource' in d:
835
#             d['resource'] = str(d['resource'])
836
#         if 'start_date' in d:
837
#             d['start_date'] = d['start_date'].strftime(
838
#                 "%Y-%m-%dT%H:%M:%S.%f")[:24]
839
#         if 'end_date' in d:
840
#             d['end_date'] = d['end_date'].strftime("%Y-%m-%dT%H:%M:%S.%f")[:24]
841
#         if 'entity' in d:
842
#             d['entity'] = d['entity'].email
843
#         return d
844
# 
845
# 
846
# class AstakosGroupSortForm(forms.Form):
847
#     sorting = forms.ChoiceField(
848
#         label='Sort by',
849
#         choices=(
850
#             ('groupname', 'Name'),
851
#             ('issue_date', 'Issue Date'),
852
#             ('expiration_date', 'Expiration Date'),
853
#             ('approved_members_num', 'Participants'),
854
#             ('moderation_enabled', 'Moderation'),
855
#             ('membership_status', 'Enrollment Status')
856
#         ),
857
#         required=True
858
#     )
859
# 
860
# class MembersSortForm(forms.Form):
861
#     sorting = forms.ChoiceField(
862
#         label='Sort by',
863
#         choices=(('person__email', 'User Id'),
864
#                  ('person__first_name', 'Name'),
865
#                  ('date_joined', 'Status')
866
#         ),
867
#         required=True
868
#     )
869
# 
870
# class PickResourceForm(forms.Form):
871
#     resource = forms.ModelChoiceField(
872
#         queryset=Resource.objects.select_related().all()
873
#     )
874
#     resource.widget.attrs["onchange"] = "this.form.submit()"
875

    
876

    
877
class ExtendedSetPasswordForm(SetPasswordForm):
878
    """
879
    Extends SetPasswordForm by enabling user
880
    to optionally renew also the token.
881
    """
882
    if not NEWPASSWD_INVALIDATE_TOKEN:
883
        renew = forms.BooleanField(
884
            label='Renew token',
885
            required=False,
886
            initial=True,
887
            help_text='Unsetting this may result in security risk.'
888
        )
889

    
890
    def __init__(self, user, *args, **kwargs):
891
        super(ExtendedSetPasswordForm, self).__init__(user, *args, **kwargs)
892

    
893
    @transaction.commit_on_success()
894
    def save(self, commit=True):
895
        try:
896
            self.user = AstakosUser.objects.get(id=self.user.id)
897
            if NEWPASSWD_INVALIDATE_TOKEN or self.cleaned_data.get('renew'):
898
                self.user.renew_token()
899
            #self.user.flush_sessions()
900
            if not self.user.has_auth_provider('local'):
901
                self.user.add_auth_provider('local', auth_backend='astakos')
902

    
903
        except BaseException, e:
904
            logger.exception(e)
905
        return super(ExtendedSetPasswordForm, self).save(commit=commit)
906

    
907

    
908
class ProjectApplicationForm(forms.ModelForm):
909
    name = forms.CharField(
910
        validators=[validators.RegexValidator(
911
            DOMAIN_VALUE_REGEX,
912
            _(astakos_messages.DOMAIN_VALUE_ERR),
913
            'invalid'
914
        )],
915
        widget=forms.TextInput(attrs={'placeholder': 'eg. foo.ece.ntua.gr'}),
916
        help_text="Name should be in the form of dns"
917
    )
918
    comments = forms.CharField(widget=forms.Textarea, required=False)
919
    
920
    class Meta:
921
        model = ProjectApplication
922
        exclude = (
923
            'resource_grants', 'id', 'applicant', 'owner',
924
            'precursor_application', 'state', 'issue_date')
925

    
926
    def clean(self):
927
        userid = self.data.get('user', None)
928
        self.user = None
929
        if userid:
930
            try:
931
                self.user = AstakosUser.objects.get(id=userid)
932
            except AstakosUser.DoesNotExist:
933
                pass
934
        if not self.user:
935
            raise forms.ValidationError(_(astakos_messages.NO_APPLICANT))
936
        super(ProjectApplicationForm, self).clean()
937
        return self.cleaned_data
938
    
939
    @property
940
    def resource_policies(self):
941
        policies = []
942
        append = policies.append
943
        for name, value in self.data.iteritems():
944
            if not value:
945
                continue
946
            uplimit = value
947
            if name.endswith('_uplimit'):
948
                subs = name.split('_uplimit')
949
                prefix, suffix = subs
950
                s, sep, r = prefix.partition(RESOURCE_SEPARATOR)
951
                resource = Resource.objects.get(service__name=s, name=r)
952

    
953
                # keep only resource limits for selected resource groups
954
#                 if self.data.get(
955
#                     'is_selected_%s' % resource.group, False
956
#                 ):
957
                if uplimit:
958
                    append(dict(service=s, resource=r, uplimit=uplimit))
959
        return policies
960

    
961
    def save(self, commit=True):
962
        application = super(ProjectApplicationForm, self).save(commit=False)
963
        applicant = self.user
964
        comments = self.cleaned_data.pop('comments', None)
965
        try:
966
            precursor_application = self.instance.precursor_application
967
        except:
968
            precursor_application = None
969
        return submit_application(
970
            application,
971
            self.resource_policies,
972
            applicant,
973
            comments,
974
            precursor_application
975
        )
976

    
977
class ProjectSortForm(forms.Form):
978
    sorting = forms.ChoiceField(
979
        label='Sort by',
980
        choices=(('name', 'Sort by Name'),
981
                 ('issue_date', 'Sort by Issue date'),
982
                 ('start_date', 'Sort by Start Date'),
983
                 ('end_date', 'Sort by End Date'),
984
#                  ('approved_members_num', 'Sort by Participants'),
985
                 ('state', 'Sort by Status'),
986
                 ('member_join_policy__description', 'Sort by Member Join Policy'),
987
                 ('member_leave_policy__description', 'Sort by Member Leave Policy')
988
        ),
989
        required=True
990
    )
991

    
992
class AddProjectMembersForm(forms.Form):
993
    q = forms.CharField(
994
        max_length=800, widget=forms.Textarea, label=_('Add members'),
995
        help_text=_(astakos_messages.ADD_PROJECT_MEMBERS_Q_HELP),
996
        required=True)
997

    
998
    def clean(self):
999
        q = self.cleaned_data.get('q') or ''
1000
        users = q.split(',')
1001
        users = list(u.strip() for u in users if u)
1002
        db_entries = AstakosUser.objects.filter(email__in=users)
1003
        unknown = list(set(users) - set(u.email for u in db_entries))
1004
        if unknown:
1005
            raise forms.ValidationError(_(astakos_messages.UNKNOWN_USERS) % ','.join(unknown))
1006
        self.valid_users = db_entries
1007
        return self.cleaned_data
1008

    
1009
    def get_valid_users(self):
1010
        """Should be called after form cleaning"""
1011
        try:
1012
            return self.valid_users
1013
        except:
1014
            return ()
1015

    
1016
class ProjectMembersSortForm(forms.Form):
1017
    sorting = forms.ChoiceField(
1018
        label='Sort by',
1019
        choices=(('person__email', 'User Id'),
1020
                 ('person__first_name', 'Name'),
1021
                 ('acceptance_date', 'Acceptance date')
1022
        ),
1023
        required=True
1024
    )
1025

    
1026
class ProjectSearchForm(forms.Form):
1027
    q = forms.CharField(max_length=200, label='Search project')