Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (38.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
from astakos.im import auth_providers
71

    
72
import astakos.im.messages as astakos_messages
73

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

    
79
logger = logging.getLogger(__name__)
80

    
81
DOMAIN_VALUE_REGEX = re.compile(
82
    r'^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,6}$',
83
    re.IGNORECASE)
84

    
85
class StoreUserMixin(object):
86

    
87
    @transaction.commit_on_success
88
    def store_user(self, user, request):
89
        user.save()
90
        self.post_store_user(user, request)
91
        return user
92

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

    
100

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
196

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

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

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

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

    
224

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

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

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

    
251
        super(ThirdPartyUserCreationForm, self).__init__(*args, **kwargs)
252

    
253
        if latest_terms:
254
            self.fields.keyOrder.append('has_signed_terms')
255

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

    
264
    def clean_email(self):
265
        email = self.cleaned_data['email']
266
        if not email:
267
            raise forms.ValidationError(_(astakos_messages.REQUIRED_FIELD))
268
        if reserved_email(email):
269
            raise forms.ValidationError(_(astakos_messages.EMAIL_USED))
270
        return email
271

    
272
    def clean_has_signed_terms(self):
273
        has_signed_terms = self.cleaned_data['has_signed_terms']
274
        if not has_signed_terms:
275
            raise forms.ValidationError(_(astakos_messages.SIGN_TERMS))
276
        return has_signed_terms
277

    
278
    def post_store_user(self, user, request):
279
        pending = PendingThirdPartyUser.objects.get(
280
                                token=request.POST.get('third_party_token'),
281
                                third_party_identifier= \
282
            self.cleaned_data.get('third_party_identifier'))
283
        return user.add_pending_auth_provider(pending)
284

    
285

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

    
295

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

    
307
        #set readonly form fields
308
        ro = ('email',)
309
        for f in ro:
310
            self.fields[f].widget.attrs['readonly'] = True
311

    
312
    def save(self, commit=True):
313
        user = super(InvitedThirdPartyUserCreationForm, self).save(commit=False)
314
        user.set_invitation_level()
315
        user.email_verified = True
316
        if commit:
317
            user.save()
318
        return user
319

    
320

    
321
class ShibbolethUserCreationForm(ThirdPartyUserCreationForm):
322
    additional_email = forms.CharField(
323
        widget=forms.HiddenInput(), label='', required=False)
324

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

    
333

    
334
class InvitedShibbolethUserCreationForm(ShibbolethUserCreationForm,
335
                                        InvitedThirdPartyUserCreationForm):
336
    pass
337

    
338

    
339
class LoginForm(AuthenticationForm):
340
    username = forms.EmailField(label=_("Email"))
341
    recaptcha_challenge_field = forms.CharField(widget=DummyWidget)
342
    recaptcha_response_field = forms.CharField(
343
        widget=RecaptchaWidget, label='')
344

    
345
    def __init__(self, *args, **kwargs):
346
        was_limited = kwargs.get('was_limited', False)
347
        request = kwargs.get('request', None)
348
        if request:
349
            self.ip = request.META.get('REMOTE_ADDR',
350
                                       request.META.get('HTTP_X_REAL_IP', None))
351

    
352
        t = ('request', 'was_limited')
353
        for elem in t:
354
            if elem in kwargs.keys():
355
                kwargs.pop(elem)
356
        super(LoginForm, self).__init__(*args, **kwargs)
357

    
358
        self.fields.keyOrder = ['username', 'password']
359
        if was_limited and RECAPTCHA_ENABLED:
360
            self.fields.keyOrder.extend(['recaptcha_challenge_field',
361
                                         'recaptcha_response_field', ])
362

    
363
    def clean_username(self):
364
        return self.cleaned_data['username'].lower()
365

    
366
    def clean_recaptcha_response_field(self):
367
        if 'recaptcha_challenge_field' in self.cleaned_data:
368
            self.validate_captcha()
369
        return self.cleaned_data['recaptcha_response_field']
370

    
371
    def clean_recaptcha_challenge_field(self):
372
        if 'recaptcha_response_field' in self.cleaned_data:
373
            self.validate_captcha()
374
        return self.cleaned_data['recaptcha_challenge_field']
375

    
376
    def validate_captcha(self):
377
        rcf = self.cleaned_data['recaptcha_challenge_field']
378
        rrf = self.cleaned_data['recaptcha_response_field']
379
        check = captcha.submit(rcf, rrf, RECAPTCHA_PRIVATE_KEY, self.ip)
380
        if not check.is_valid:
381
            raise forms.ValidationError(_(astakos_messages.CAPTCHA_VALIDATION_ERR))
382

    
383
    def clean(self):
384
        """
385
        Override default behavior in order to check user's activation later
386
        """
387
        username = self.cleaned_data.get('username')
388

    
389
        if username:
390
            try:
391
                user = AstakosUser.objects.get_by_identifier(username)
392
                if not user.has_auth_provider('local'):
393
                    provider = auth_providers.get_provider('local')
394
                    raise forms.ValidationError(
395
                        _(provider.get_message('NOT_ACTIVE_FOR_USER')))
396
            except AstakosUser.DoesNotExist:
397
                pass
398

    
399
        try:
400
            super(LoginForm, self).clean()
401
        except forms.ValidationError, e:
402
            if self.user_cache is None:
403
                raise
404
            if not self.user_cache.is_active:
405
                raise forms.ValidationError(self.user_cache.get_inactive_message())
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
472

473
    save method: to pass a custom from_email in send_mail.
474
    clean_email: to handle local auth provider checks
475
    """
476
    def clean_email(self):
477
        email = super(ExtendedPasswordResetForm, self).clean_email()
478
        try:
479
            user = AstakosUser.objects.get_by_identifier(email)
480

    
481
            if not user.is_active:
482
                raise forms.ValidationError(_(astakos_messages.ACCOUNT_INACTIVE))
483

    
484
            if not user.has_usable_password():
485
                raise forms.ValidationError(_(astakos_messages.UNUSABLE_PASSWORD))
486

    
487
            if not user.can_change_password():
488
                raise forms.ValidationError(_(astakos_messages.AUTH_PROVIDER_CANNOT_CHANGE_PASSWORD))
489
        except AstakosUser.DoesNotExist, e:
490
            raise forms.ValidationError(_(astakos_messages.EMAIL_UNKNOWN))
491
        return email
492

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

    
515

    
516
class EmailChangeForm(forms.ModelForm):
517

    
518
    class Meta:
519
        model = EmailChange
520
        fields = ('new_email_address',)
521

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

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

    
538

    
539
class SignApprovalTermsForm(forms.ModelForm):
540

    
541
    class Meta:
542
        model = AstakosUser
543
        fields = ("has_signed_terms",)
544

    
545
    def __init__(self, *args, **kwargs):
546
        super(SignApprovalTermsForm, self).__init__(*args, **kwargs)
547

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

    
554

    
555
class InvitationForm(forms.ModelForm):
556

    
557
    username = forms.EmailField(label=_("Email"))
558

    
559
    def __init__(self, *args, **kwargs):
560
        super(InvitationForm, self).__init__(*args, **kwargs)
561

    
562
    class Meta:
563
        model = Invitation
564
        fields = ('username', 'realname')
565

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

    
575

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

    
586
    def __init__(self, user, *args, **kwargs):
587
        self.session_key = kwargs.pop('session_key', None)
588
        super(ExtendedPasswordChangeForm, self).__init__(user, *args, **kwargs)
589

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

    
600

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

    
880

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

    
893
    def __init__(self, user, *args, **kwargs):
894
        super(ExtendedSetPasswordForm, self).__init__(user, *args, **kwargs)
895

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

    
906
        except BaseException, e:
907
            logger.exception(e)
908
        return super(ExtendedSetPasswordForm, self).save(commit=commit)
909

    
910

    
911
class ProjectApplicationForm(forms.ModelForm):
912
    name = forms.CharField(
913
        validators=[validators.RegexValidator(
914
            DOMAIN_VALUE_REGEX,
915
            _(astakos_messages.DOMAIN_VALUE_ERR),
916
            'invalid'
917
        )],
918
        widget=forms.TextInput(attrs={'placeholder': 'myproject.mylab.ntua.gr'}),
919
        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 "
920
    )
921
    homepage = forms.URLField(
922
        help_text="This should be a URL pointing at your project's site. e.g.: http://myproject.com ",
923
        widget=forms.TextInput(attrs={'placeholder': 'http://myproject.com'}),
924

    
925
        required=False
926
     )
927
    comments = forms.CharField(widget=forms.Textarea, required=False)
928

    
929
    class Meta:
930
        model = ProjectApplication
931
        exclude = (
932
            'project',
933
            'resource_grants', 'id', 'applicant', 'owner',
934
            'precursor_application', 'state', 'issue_date')
935

    
936
    def __init__(self, *args, **kwargs):
937
        self.precursor_application = kwargs.get('instance')
938
        super(ProjectApplicationForm, self).__init__(*args, **kwargs)
939

    
940
    def clean(self):
941
        userid = self.data.get('user', None)
942
        self.user = None
943
        if userid:
944
            try:
945
                self.user = AstakosUser.objects.get(id=userid)
946
            except AstakosUser.DoesNotExist:
947
                pass
948
        if not self.user:
949
            raise forms.ValidationError(_(astakos_messages.NO_APPLICANT))
950
        super(ProjectApplicationForm, self).clean()
951
        return self.cleaned_data
952

    
953
    @property
954
    def resource_policies(self):
955
        policies = []
956
        append = policies.append
957
        for name, value in self.data.iteritems():
958
            if not value:
959
                continue
960
            uplimit = value
961
            if name.endswith('_uplimit'):
962
                subs = name.split('_uplimit')
963
                prefix, suffix = subs
964
                s, sep, r = prefix.partition(RESOURCE_SEPARATOR)
965
                resource = Resource.objects.get(service__name=s, name=r)
966

    
967
                # keep only resource limits for selected resource groups
968
                if self.data.get(
969
                    'is_selected_%s' % resource.group, "0"
970
                 ) == "1":
971
                    if uplimit:
972
                        append(dict(service=s, resource=r, uplimit=uplimit))
973
                    else:
974
                        append(dict(service=s, resource=r, uplimit=None))
975

    
976
        return policies
977

    
978

    
979
    def save(self, commit=True):
980
        application = super(ProjectApplicationForm, self).save(commit=False)
981
        applicant = self.user
982
        comments = self.cleaned_data.pop('comments', None)
983
        return submit_application(
984
            application,
985
            self.resource_policies,
986
            applicant,
987
            comments,
988
            self.precursor_application
989
        )
990

    
991
class ProjectSortForm(forms.Form):
992
    sorting = forms.ChoiceField(
993
        label='Sort by',
994
        choices=(('name', 'Sort by Name'),
995
                 ('issue_date', 'Sort by Issue date'),
996
                 ('start_date', 'Sort by Start Date'),
997
                 ('end_date', 'Sort by End Date'),
998
#                  ('approved_members_num', 'Sort by Participants'),
999
                 ('state', 'Sort by Status'),
1000
                 ('member_join_policy__description', 'Sort by Member Join Policy'),
1001
                 ('member_leave_policy__description', 'Sort by Member Leave Policy'),
1002
                 ('-name', 'Sort by Name'),
1003
                 ('-issue_date', 'Sort by Issue date'),
1004
                 ('-start_date', 'Sort by Start Date'),
1005
                 ('-end_date', 'Sort by End Date'),
1006
#                  ('-approved_members_num', 'Sort by Participants'),
1007
                 ('-state', 'Sort by Status'),
1008
                 ('-member_join_policy__description', 'Sort by Member Join Policy'),
1009
                 ('-member_leave_policy__description', 'Sort by Member Leave Policy')
1010
        ),
1011
        required=True
1012
    )
1013

    
1014
class AddProjectMembersForm(forms.Form):
1015
    q = forms.CharField(
1016
        max_length=800, widget=forms.Textarea, label=_('Add members'),
1017
        help_text=_(astakos_messages.ADD_PROJECT_MEMBERS_Q_HELP),
1018
        required=True)
1019

    
1020
    def clean(self):
1021
        q = self.cleaned_data.get('q') or ''
1022
        users = q.split(',')
1023
        users = list(u.strip() for u in users if u)
1024
        db_entries = AstakosUser.objects.filter(email__in=users)
1025
        unknown = list(set(users) - set(u.email for u in db_entries))
1026
        if unknown:
1027
            raise forms.ValidationError(_(astakos_messages.UNKNOWN_USERS) % ','.join(unknown))
1028
        self.valid_users = db_entries
1029
        return self.cleaned_data
1030

    
1031
    def get_valid_users(self):
1032
        """Should be called after form cleaning"""
1033
        try:
1034
            return self.valid_users
1035
        except:
1036
            return ()
1037

    
1038
class ProjectMembersSortForm(forms.Form):
1039
    sorting = forms.ChoiceField(
1040
        label='Sort by',
1041
        choices=(('person__email', 'User Id'),
1042
                 ('person__first_name', 'Name'),
1043
                 ('acceptance_date', 'Acceptance date')
1044
        ),
1045
        required=True
1046
    )
1047

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