Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (32.3 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 (UserCreationForm, AuthenticationForm,
39
                                       PasswordResetForm, PasswordChangeForm,
40
                                       SetPasswordForm)
41
from django.core.mail import send_mail
42
from django.contrib.auth.tokens import default_token_generator
43
from django.template import Context, loader
44
from django.utils.http import int_to_base36
45
from django.core.urlresolvers import reverse
46
from django.utils.safestring import mark_safe
47
from django.utils.encoding import smart_str
48
from django.conf import settings
49
from django.forms.models import fields_for_model
50
from django.db import transaction
51
from django.utils.encoding import smart_unicode
52
from django.core import validators
53

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

    
67
from astakos.im.util import reserved_email, get_query
68

    
69
import astakos.im.messages as astakos_messages
70

    
71
import logging
72
import hashlib
73
import recaptcha.client.captcha as captcha
74
import re
75

    
76
logger = logging.getLogger(__name__)
77

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

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

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

    
97

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
193

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

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

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

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

    
221

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

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

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

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

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

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

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

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

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

    
282

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

    
292

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

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

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

    
317

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

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

    
330

    
331
class InvitedShibbolethUserCreationForm(ShibbolethUserCreationForm,
332
                                        InvitedThirdPartyUserCreationForm):
333
    pass
334

    
335

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

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

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

    
355
        self.fields.keyOrder = ['username', 'password']
356
        if was_limited and RECAPTCHA_ENABLED:
357
            self.fields.keyOrder.extend(['recaptcha_challenge_field',
358
                                         'recaptcha_response_field', ])
359

    
360
    def clean_username(self):
361
        if 'username' in self.cleaned_data:
362
            return self.cleaned_data['username'].lower()
363

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

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

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

    
381
    def clean(self):
382
        """
383
        Override default behavior in order to check user's activation later
384
        """
385
        try:
386
            super(LoginForm, self).clean()
387
        except forms.ValidationError, e:
388
#            if self.user_cache is None:
389
#                raise
390
            if self.request:
391
                if not self.request.session.test_cookie_worked():
392
                    raise
393
        return self.cleaned_data
394

    
395

    
396
class ProfileForm(forms.ModelForm):
397
    """
398
    Subclass of ``ModelForm`` for permiting user to edit his/her profile.
399
    Most of the fields are readonly since the user is not allowed to change
400
    them.
401

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

    
407
    class Meta:
408
        model = AstakosUser
409
        fields = ('email', 'first_name', 'last_name', 'auth_token',
410
                  'auth_token_expires')
411

    
412
    def __init__(self, *args, **kwargs):
413
        self.session_key = kwargs.pop('session_key', None)
414
        super(ProfileForm, self).__init__(*args, **kwargs)
415
        instance = getattr(self, 'instance', None)
416
        ro_fields = ('email', 'auth_token', 'auth_token_expires')
417
        if instance and instance.id:
418
            for field in ro_fields:
419
                self.fields[field].widget.attrs['readonly'] = True
420

    
421
    def save(self, commit=True):
422
        user = super(ProfileForm, self).save(commit=False)
423
        user.is_verified = True
424
        if self.cleaned_data.get('renew'):
425
            user.renew_token(
426
                flush_sessions=True,
427
                current_key=self.session_key
428
            )
429
        if commit:
430
            user.save()
431
        return user
432

    
433

    
434
class FeedbackForm(forms.Form):
435
    """
436
    Form for writing feedback.
437
    """
438
    feedback_msg = forms.CharField(widget=forms.Textarea, label=u'Message')
439
    feedback_data = forms.CharField(widget=forms.HiddenInput(), label='',
440
                                    required=False)
441

    
442

    
443
class SendInvitationForm(forms.Form):
444
    """
445
    Form for sending an invitations
446
    """
447

    
448
    email = forms.EmailField(required=True, label='Email address')
449
    first_name = forms.EmailField(label='First name')
450
    last_name = forms.EmailField(label='Last name')
451

    
452

    
453
class ExtendedPasswordResetForm(PasswordResetForm):
454
    """
455
    Extends PasswordResetForm by overriding save method:
456
    passes a custom from_email in send_mail.
457

458
    Since Django 1.3 this is useless since ``django.contrib.auth.views.reset_password``
459
    accepts a from_email argument.
460
    """
461
    def clean_email(self):
462
        email = super(ExtendedPasswordResetForm, self).clean_email()
463
        try:
464
            user = AstakosUser.objects.get(email__iexact=email, is_active=True)
465
            if not user.has_usable_password():
466
                raise forms.ValidationError(_(astakos_messages.UNUSABLE_PASSWORD))
467

    
468
            if not user.can_change_password():
469
                raise forms.ValidationError(_('Password change for this account'
470
                                              ' is not supported.'))
471

    
472
        except AstakosUser.DoesNotExist, e:
473
            raise forms.ValidationError(_(astakos_messages.EMAIL_UNKNOWN))
474
        return email
475

    
476
    def save(
477
        self, domain_override=None, email_template_name='registration/password_reset_email.html',
478
            use_https=False, token_generator=default_token_generator, request=None):
479
        """
480
        Generates a one-use only link for resetting password and sends to the user.
481
        """
482
        for user in self.users_cache:
483
            url = user.astakosuser.get_password_reset_url(token_generator)
484
            url = urljoin(BASEURL, url)
485
            t = loader.get_template(email_template_name)
486
            c = {
487
                'email': user.email,
488
                'url': url,
489
                'site_name': SITENAME,
490
                'user': user,
491
                'baseurl': BASEURL,
492
                'support': DEFAULT_CONTACT_EMAIL
493
            }
494
            from_email = settings.SERVER_EMAIL
495
            send_mail(_(PASSWORD_RESET_EMAIL_SUBJECT),
496
                      t.render(Context(c)), from_email, [user.email])
497

    
498

    
499
class EmailChangeForm(forms.ModelForm):
500
    class Meta:
501
        model = EmailChange
502
        fields = ('new_email_address',)
503

    
504
    def clean_new_email_address(self):
505
        addr = self.cleaned_data['new_email_address']
506
        if AstakosUser.objects.filter(email__iexact=addr):
507
            raise forms.ValidationError(_(astakos_messages.EMAIL_USED))
508
        return addr
509

    
510
    def save(self, email_template_name, request, commit=True):
511
        ec = super(EmailChangeForm, self).save(commit=False)
512
        ec.user = request.user
513
        activation_key = hashlib.sha1(
514
            str(random()) + smart_str(ec.new_email_address))
515
        ec.activation_key = activation_key.hexdigest()
516
        if commit:
517
            ec.save()
518
        send_change_email(ec, request, email_template_name=email_template_name)
519

    
520

    
521
class SignApprovalTermsForm(forms.ModelForm):
522
    class Meta:
523
        model = AstakosUser
524
        fields = ("has_signed_terms",)
525

    
526
    def __init__(self, *args, **kwargs):
527
        super(SignApprovalTermsForm, self).__init__(*args, **kwargs)
528

    
529
    def clean_has_signed_terms(self):
530
        has_signed_terms = self.cleaned_data['has_signed_terms']
531
        if not has_signed_terms:
532
            raise forms.ValidationError(_(astakos_messages.SIGN_TERMS))
533
        return has_signed_terms
534

    
535

    
536
class InvitationForm(forms.ModelForm):
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(_(astakos_messages.INVITATION_EMAIL_EXISTS))
551
        except Invitation.DoesNotExist:
552
            pass
553
        return username
554

    
555

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

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

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

    
580

    
581
class AstakosGroupCreationForm(forms.ModelForm):
582
    kind = forms.ModelChoiceField(
583
        queryset=GroupKind.objects.all(),
584
        label="",
585
        widget=forms.HiddenInput()
586
    )
587
    name = forms.CharField(
588
        validators=[validators.RegexValidator(
589
            DOMAIN_VALUE_REGEX,
590
            _(astakos_messages.DOMAIN_VALUE_ERR), 'invalid'
591
        )],
592
        widget=forms.TextInput(attrs={'placeholder': 'myproject.mylab.ntua.gr'}),
593
        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 "
594
    )
595
    homepage = forms.URLField(
596
        label= 'Homepage Url',
597
        widget=forms.TextInput(attrs={'placeholder': 'http://myproject.com'}),
598
        help_text="This should be a URL pointing at your project's site. e.g.: http://myproject.com ",
599
        required=False
600
    )
601
    desc = forms.CharField(
602
        label= 'Description',
603
        widget=forms.Textarea,
604
        help_text= "Please provide a short but descriptive abstract of your Project, so that anyone searching can quickly understand what this Project is about. "
605
    )
606
    issue_date = forms.DateTimeField(
607
        label= 'Start date',
608
        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."
609
    )
610
    expiration_date = forms.DateTimeField(
611
        label= 'End date',
612
        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.  "
613
    )
614
    moderation_enabled = forms.BooleanField(
615
        label= 'Moderated',
616
        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. ",
617
        required=False,
618
        initial=True
619
    )
620
    max_participants = forms.IntegerField(
621
        label='Total number of members',
622
        required=True, min_value=1,
623
        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. "
624
    )
625

    
626
    class Meta:
627
        model = AstakosGroup
628

    
629
    def __init__(self, *args, **kwargs):
630
        #update QueryDict
631
        args = list(args)
632
        qd = args.pop(0).copy()
633
        members_unlimited = qd.pop('members_unlimited', False)
634
        members_uplimit = qd.pop('members_uplimit', None)
635

    
636
        #substitue QueryDict
637
        args.insert(0, qd)
638

    
639
        super(AstakosGroupCreationForm, self).__init__(*args, **kwargs)
640

    
641
        self.fields.keyOrder = ['kind', 'name', 'homepage', 'desc',
642
                                'issue_date', 'expiration_date',
643
                                'moderation_enabled', 'max_participants']
644
        def add_fields((k, v)):
645
            k = k.partition('_proxy')[0]
646
            self.fields[k] = forms.IntegerField(
647
                required=False,
648
                widget=forms.HiddenInput(),
649
                min_value=1
650
            )
651
        map(add_fields,
652
            ((k, v) for k,v in qd.iteritems() if k.endswith('_uplimit'))
653
        )
654

    
655
        def add_fields((k, v)):
656
            self.fields[k] = forms.BooleanField(
657
                required=False,
658
                #widget=forms.HiddenInput()
659
            )
660
        map(add_fields,
661
            ((k, v) for k,v in qd.iteritems() if k.startswith('is_selected_'))
662
        )
663

    
664
    def policies(self):
665
        self.clean()
666
        policies = []
667
        append = policies.append
668
        for name, uplimit in self.cleaned_data.iteritems():
669

    
670
            subs = name.split('_uplimit')
671
            if len(subs) == 2:
672
                prefix, suffix = subs
673
                s, sep, r = prefix.partition(RESOURCE_SEPARATOR)
674
                resource = Resource.objects.get(service__name=s, name=r)
675

    
676
                # keep only resource limits for selected resource groups
677
                if self.cleaned_data.get(
678
                    'is_selected_%s' % resource.group, False
679
                ):
680
                    append(dict(service=s, resource=r, uplimit=uplimit))
681
        return policies
682

    
683
class AstakosGroupCreationSummaryForm(forms.ModelForm):
684
    kind = forms.ModelChoiceField(
685
        queryset=GroupKind.objects.all(),
686
        label="",
687
        widget=forms.HiddenInput()
688
    )
689
    name = forms.CharField(
690
        widget=forms.TextInput(attrs={'placeholder': 'eg. foo.ece.ntua.gr'}),
691
        help_text="Name should be in the form of dns"
692
    )
693
    moderation_enabled = forms.BooleanField(
694
        help_text="Check if you want to approve members participation manually",
695
        required=False,
696
        initial=True
697
    )
698
    max_participants = forms.IntegerField(
699
        required=False, min_value=1
700
    )
701

    
702
    class Meta:
703
        model = AstakosGroup
704

    
705
    def __init__(self, *args, **kwargs):
706
        #update QueryDict
707
        args = list(args)
708
        qd = args.pop(0).copy()
709
        members_unlimited = qd.pop('members_unlimited', False)
710
        members_uplimit = qd.pop('members_uplimit', None)
711

    
712
        #substitue QueryDict
713
        args.insert(0, qd)
714

    
715
        super(AstakosGroupCreationSummaryForm, self).__init__(*args, **kwargs)
716
        self.fields.keyOrder = ['kind', 'name', 'homepage', 'desc',
717
                                'issue_date', 'expiration_date',
718
                                'moderation_enabled', 'max_participants']
719
        def add_fields((k, v)):
720
            self.fields[k] = forms.IntegerField(
721
                required=False,
722
                widget=forms.TextInput(),
723
                min_value=1
724
            )
725
        map(add_fields,
726
            ((k, v) for k,v in qd.iteritems() if k.endswith('_uplimit'))
727
        )
728

    
729
        def add_fields((k, v)):
730
            self.fields[k] = forms.BooleanField(
731
                required=False,
732
                widget=forms.HiddenInput()
733
            )
734
        map(add_fields,
735
            ((k, v) for k,v in qd.iteritems() if k.startswith('is_selected_'))
736
        )
737
        for f in self.fields.values():
738
            f.widget = forms.HiddenInput()
739

    
740
    def clean(self):
741
        super(AstakosGroupCreationSummaryForm, self).clean()
742
        self.cleaned_data['policies'] = []
743
        append = self.cleaned_data['policies'].append
744
        #tbd = [f for f in self.fields if (f.startswith('is_selected_') and (not f.endswith('_proxy')))]
745
        tbd = [f for f in self.fields if f.startswith('is_selected_')]
746
        for name, uplimit in self.cleaned_data.iteritems():
747
            subs = name.split('_uplimit')
748
            if len(subs) == 2:
749
                tbd.append(name)
750
                prefix, suffix = subs
751
                s, sep, r = prefix.partition(RESOURCE_SEPARATOR)
752
                resource = Resource.objects.get(service__name=s, name=r)
753

    
754
                # keep only resource limits for selected resource groups
755
                if self.cleaned_data.get(
756
                    'is_selected_%s' % resource.group, False
757
                ):
758
                    append(dict(service=s, resource=r, uplimit=uplimit))
759
        for name in tbd:
760
            self.cleaned_data.pop(name, None)
761
        return self.cleaned_data
762

    
763
class AstakosGroupUpdateForm(forms.ModelForm):
764
    class Meta:
765
        model = AstakosGroup
766
        fields = ( 'desc','homepage', 'moderation_enabled')
767

    
768

    
769
class AddGroupMembersForm(forms.Form):
770
    q = forms.CharField(
771
        max_length=800, widget=forms.Textarea, label=_('Add members'),
772
        help_text=_(astakos_messages.ADD_GROUP_MEMBERS_Q_HELP),
773
        required=True)
774

    
775
    def clean(self):
776
        q = self.cleaned_data.get('q') or ''
777
        users = q.split(',')
778
        users = list(u.strip() for u in users if u)
779
        db_entries = AstakosUser.objects.filter(email__in=users)
780
        unknown = list(set(users) - set(u.email for u in db_entries))
781
        if unknown:
782
            raise forms.ValidationError(_(astakos_messages.UNKNOWN_USERS) % ','.join(unknown))
783
        self.valid_users = db_entries
784
        return self.cleaned_data
785

    
786
    def get_valid_users(self):
787
        """Should be called after form cleaning"""
788
        try:
789
            return self.valid_users
790
        except:
791
            return ()
792

    
793

    
794
class AstakosGroupSearchForm(forms.Form):
795
    q = forms.CharField(max_length=200, label='Search project')
796

    
797

    
798
class TimelineForm(forms.Form):
799
    entity = forms.ModelChoiceField(
800
        queryset=AstakosUser.objects.filter(is_active=True)
801
    )
802
    resource = forms.ModelChoiceField(
803
        queryset=Resource.objects.all()
804
    )
805
    start_date = forms.DateTimeField()
806
    end_date = forms.DateTimeField()
807
    details = forms.BooleanField(required=False, label="Detailed Listing")
808
    operation = forms.ChoiceField(
809
        label='Charge Method',
810
        choices=(('', '-------------'),
811
                 ('charge_usage', 'Charge Usage'),
812
                 ('charge_traffic', 'Charge Traffic'), )
813
    )
814

    
815
    def clean(self):
816
        super(TimelineForm, self).clean()
817
        d = self.cleaned_data
818
        if 'resource' in d:
819
            d['resource'] = str(d['resource'])
820
        if 'start_date' in d:
821
            d['start_date'] = d['start_date'].strftime(
822
                "%Y-%m-%dT%H:%M:%S.%f")[:24]
823
        if 'end_date' in d:
824
            d['end_date'] = d['end_date'].strftime("%Y-%m-%dT%H:%M:%S.%f")[:24]
825
        if 'entity' in d:
826
            d['entity'] = d['entity'].email
827
        return d
828

    
829

    
830
class AstakosGroupSortForm(forms.Form):
831
    sorting = forms.ChoiceField(
832
        label='Sort by',
833
        choices=(
834
            ('groupname', 'Name'),
835
            ('issue_date', 'Issue Date'),
836
            ('expiration_date', 'Expiration Date'),
837
            ('approved_members_num', 'Participants'),
838
            ('moderation_enabled', 'Moderation'),
839
            ('membership_status', 'Enrollment Status')
840
        ),
841
        required=True
842
    )
843

    
844
class MembersSortForm(forms.Form):
845
    sorting = forms.ChoiceField(
846
        label='Sort by',
847
        choices=(('person__email', 'User Id'),
848
                 ('person__first_name', 'Name'),
849
                 ('date_joined', 'Status')
850
        ),
851
        required=True
852
    )
853

    
854
class PickResourceForm(forms.Form):
855
    resource = forms.ModelChoiceField(
856
        queryset=Resource.objects.select_related().all()
857
    )
858
    resource.widget.attrs["onchange"] = "this.form.submit()"
859

    
860

    
861
class ExtendedSetPasswordForm(SetPasswordForm):
862
    """
863
    Extends SetPasswordForm by enabling user
864
    to optionally renew also the token.
865
    """
866
    if not NEWPASSWD_INVALIDATE_TOKEN:
867
        renew = forms.BooleanField(
868
            label='Renew token',
869
            required=False,
870
            initial=True,
871
            help_text='Unsetting this may result in security risk.'
872
        )
873

    
874
    def __init__(self, user, *args, **kwargs):
875
        super(ExtendedSetPasswordForm, self).__init__(user, *args, **kwargs)
876

    
877
    @transaction.commit_on_success()
878
    def save(self, commit=True):
879
        try:
880
            self.user = AstakosUser.objects.get(id=self.user.id)
881
            if NEWPASSWD_INVALIDATE_TOKEN or self.cleaned_data.get('renew'):
882
                self.user.renew_token()
883
            #self.user.flush_sessions()
884
            if not self.user.has_auth_provider('local'):
885
                self.user.add_auth_provider('local', auth_backend='astakos')
886

    
887
        except BaseException, e:
888
            logger.exception(e)
889
        return super(ExtendedSetPasswordForm, self).save(commit=commit)