Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / forms.py @ 4bdd7e3d

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

    
70
import astakos.im.messages as astakos_messages
71

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

    
77
logger = logging.getLogger(__name__)
78

    
79
DOMAIN_VALUE_REGEX = re.compile(
80
    r'^(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.){0,126}(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?))$',
81
    re.IGNORECASE
82
)
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
        if reserved_email(email):
267
            raise forms.ValidationError(_(astakos_messages.EMAIL_USED))
268
        return email
269

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

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

    
283

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

    
293

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

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

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

    
318

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

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

    
331

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

    
336

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

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

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

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

    
361
    def clean_username(self):
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
        username = self.cleaned_data.get('username')
386

    
387
        try:
388
            user = AstakosUser.objects.get(email=username)
389
            if not user.has_auth_provider('local'):
390
                provider = auth_providers.get_provider('local')
391
                raise forms.ValidationError(
392
                    _(provider.get_message('NOT_ACTIVE_FOR_USER_LOGIN')))
393
        except AstakosUser.DoesNotExist:
394
            pass
395

    
396
        try:
397
            super(LoginForm, self).clean()
398
        except forms.ValidationError, e:
399
            if self.user_cache is None:
400
                raise
401
            if not self.user_cache.is_active:
402
                raise forms.ValidationError(self.user_cache.get_inactive_message())
403
            if self.request:
404
                if not self.request.session.test_cookie_worked():
405
                    raise
406
        return self.cleaned_data
407

    
408

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

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

    
420
    class Meta:
421
        model = AstakosUser
422
        fields = ('email', 'first_name', 'last_name', 'auth_token',
423
                  'auth_token_expires')
424

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

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

    
446

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

    
455

    
456
class SendInvitationForm(forms.Form):
457
    """
458
    Form for sending an invitations
459
    """
460

    
461
    email = forms.EmailField(required=True, label='Email address')
462
    first_name = forms.EmailField(label='First name')
463
    last_name = forms.EmailField(label='Last name')
464

    
465

    
466
class ExtendedPasswordResetForm(PasswordResetForm):
467
    """
468
    Extends PasswordResetForm by overriding save method:
469
    passes a custom from_email in send_mail.
470

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

    
481
            if not user.can_change_password():
482
                raise forms.ValidationError(_('Password change for this account'
483
                                              ' is not supported.'))
484

    
485
        except AstakosUser.DoesNotExist, e:
486
            raise forms.ValidationError(_(astakos_messages.EMAIL_UNKNOWN))
487
        return email
488

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

    
511

    
512
class EmailChangeForm(forms.ModelForm):
513
    class Meta:
514
        model = EmailChange
515
        fields = ('new_email_address',)
516

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

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

    
533

    
534
class SignApprovalTermsForm(forms.ModelForm):
535
    class Meta:
536
        model = AstakosUser
537
        fields = ("has_signed_terms",)
538

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

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

    
548

    
549
class InvitationForm(forms.ModelForm):
550
    username = forms.EmailField(label=_("Email"))
551

    
552
    def __init__(self, *args, **kwargs):
553
        super(InvitationForm, self).__init__(*args, **kwargs)
554

    
555
    class Meta:
556
        model = Invitation
557
        fields = ('username', 'realname')
558

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

    
568

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

    
579
    def __init__(self, user, *args, **kwargs):
580
        self.session_key = kwargs.pop('session_key', None)
581
        super(ExtendedPasswordChangeForm, self).__init__(user, *args, **kwargs)
582

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

    
593

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

    
639
    class Meta:
640
        model = AstakosGroup
641

    
642
    def __init__(self, *args, **kwargs):
643
        #update QueryDict
644
        args = list(args)
645
        qd = args.pop(0).copy()
646
        members_unlimited = qd.pop('members_unlimited', False)
647
        members_uplimit = qd.pop('members_uplimit', None)
648

    
649
        #substitue QueryDict
650
        args.insert(0, qd)
651

    
652
        super(AstakosGroupCreationForm, self).__init__(*args, **kwargs)
653

    
654
        self.fields.keyOrder = ['kind', 'name', 'homepage', 'desc',
655
                                'issue_date', 'expiration_date',
656
                                'moderation_enabled', 'max_participants']
657
        def add_fields((k, v)):
658
            k = k.partition('_proxy')[0]
659
            self.fields[k] = forms.IntegerField(
660
                required=False,
661
                widget=forms.HiddenInput(),
662
                min_value=1
663
            )
664
        map(add_fields,
665
            ((k, v) for k,v in qd.iteritems() if k.endswith('_uplimit'))
666
        )
667

    
668
        def add_fields((k, v)):
669
            self.fields[k] = forms.BooleanField(
670
                required=False,
671
                #widget=forms.HiddenInput()
672
            )
673
        map(add_fields,
674
            ((k, v) for k,v in qd.iteritems() if k.startswith('is_selected_'))
675
        )
676

    
677
    def policies(self):
678
        self.clean()
679
        policies = []
680
        append = policies.append
681
        for name, uplimit in self.cleaned_data.iteritems():
682

    
683
            subs = name.split('_uplimit')
684
            if len(subs) == 2:
685
                prefix, suffix = subs
686
                s, sep, r = prefix.partition(RESOURCE_SEPARATOR)
687
                resource = Resource.objects.get(service__name=s, name=r)
688

    
689
                # keep only resource limits for selected resource groups
690
                if self.cleaned_data.get(
691
                    'is_selected_%s' % resource.group, False
692
                ):
693
                    append(dict(service=s, resource=r, uplimit=uplimit))
694
        return policies
695

    
696
class AstakosGroupCreationSummaryForm(forms.ModelForm):
697
    kind = forms.ModelChoiceField(
698
        queryset=GroupKind.objects.all(),
699
        label="",
700
        widget=forms.HiddenInput()
701
    )
702
    name = forms.CharField(
703
        widget=forms.TextInput(attrs={'placeholder': 'eg. foo.ece.ntua.gr'}),
704
        help_text="Name should be in the form of dns"
705
    )
706
    moderation_enabled = forms.BooleanField(
707
        help_text="Check if you want to approve members participation manually",
708
        required=False,
709
        initial=True
710
    )
711
    max_participants = forms.IntegerField(
712
        required=False, min_value=1
713
    )
714

    
715
    class Meta:
716
        model = AstakosGroup
717

    
718
    def __init__(self, *args, **kwargs):
719
        #update QueryDict
720
        args = list(args)
721
        qd = args.pop(0).copy()
722
        members_unlimited = qd.pop('members_unlimited', False)
723
        members_uplimit = qd.pop('members_uplimit', None)
724

    
725
        #substitue QueryDict
726
        args.insert(0, qd)
727

    
728
        super(AstakosGroupCreationSummaryForm, self).__init__(*args, **kwargs)
729
        self.fields.keyOrder = ['kind', 'name', 'homepage', 'desc',
730
                                'issue_date', 'expiration_date',
731
                                'moderation_enabled', 'max_participants']
732
        def add_fields((k, v)):
733
            self.fields[k] = forms.IntegerField(
734
                required=False,
735
                widget=forms.TextInput(),
736
                min_value=1
737
            )
738
        map(add_fields,
739
            ((k, v) for k,v in qd.iteritems() if k.endswith('_uplimit'))
740
        )
741

    
742
        def add_fields((k, v)):
743
            self.fields[k] = forms.BooleanField(
744
                required=False,
745
                widget=forms.HiddenInput()
746
            )
747
        map(add_fields,
748
            ((k, v) for k,v in qd.iteritems() if k.startswith('is_selected_'))
749
        )
750
        for f in self.fields.values():
751
            f.widget = forms.HiddenInput()
752

    
753
    def clean(self):
754
        super(AstakosGroupCreationSummaryForm, self).clean()
755
        self.cleaned_data['policies'] = []
756
        append = self.cleaned_data['policies'].append
757
        #tbd = [f for f in self.fields if (f.startswith('is_selected_') and (not f.endswith('_proxy')))]
758
        tbd = [f for f in self.fields if f.startswith('is_selected_')]
759
        for name, uplimit in self.cleaned_data.iteritems():
760
            subs = name.split('_uplimit')
761
            if len(subs) == 2:
762
                tbd.append(name)
763
                prefix, suffix = subs
764
                s, sep, r = prefix.partition(RESOURCE_SEPARATOR)
765
                resource = Resource.objects.get(service__name=s, name=r)
766

    
767
                # keep only resource limits for selected resource groups
768
                if self.cleaned_data.get(
769
                    'is_selected_%s' % resource.group, False
770
                ):
771
                    append(dict(service=s, resource=r, uplimit=uplimit))
772
        for name in tbd:
773
            self.cleaned_data.pop(name, None)
774
        return self.cleaned_data
775

    
776
class AstakosGroupUpdateForm(forms.ModelForm):
777
    class Meta:
778
        model = AstakosGroup
779
        fields = ( 'desc','homepage', 'moderation_enabled')
780

    
781

    
782
class AddGroupMembersForm(forms.Form):
783
    q = forms.CharField(
784
        max_length=800, widget=forms.Textarea, label=_('Add members'),
785
        help_text=_(astakos_messages.ADD_GROUP_MEMBERS_Q_HELP),
786
        required=True)
787

    
788
    def clean(self):
789
        q = self.cleaned_data.get('q') or ''
790
        users = q.split(',')
791
        users = list(u.strip() for u in users if u)
792
        db_entries = AstakosUser.objects.filter(email__in=users)
793
        unknown = list(set(users) - set(u.email for u in db_entries))
794
        if unknown:
795
            raise forms.ValidationError(_(astakos_messages.UNKNOWN_USERS) % ','.join(unknown))
796
        self.valid_users = db_entries
797
        return self.cleaned_data
798

    
799
    def get_valid_users(self):
800
        """Should be called after form cleaning"""
801
        try:
802
            return self.valid_users
803
        except:
804
            return ()
805

    
806

    
807
class AstakosGroupSearchForm(forms.Form):
808
    q = forms.CharField(max_length=200, label='Search project')
809

    
810

    
811
class TimelineForm(forms.Form):
812
    entity = forms.ModelChoiceField(
813
        queryset=AstakosUser.objects.filter(is_active=True)
814
    )
815
    resource = forms.ModelChoiceField(
816
        queryset=Resource.objects.all()
817
    )
818
    start_date = forms.DateTimeField()
819
    end_date = forms.DateTimeField()
820
    details = forms.BooleanField(required=False, label="Detailed Listing")
821
    operation = forms.ChoiceField(
822
        label='Charge Method',
823
        choices=(('', '-------------'),
824
                 ('charge_usage', 'Charge Usage'),
825
                 ('charge_traffic', 'Charge Traffic'), )
826
    )
827

    
828
    def clean(self):
829
        super(TimelineForm, self).clean()
830
        d = self.cleaned_data
831
        if 'resource' in d:
832
            d['resource'] = str(d['resource'])
833
        if 'start_date' in d:
834
            d['start_date'] = d['start_date'].strftime(
835
                "%Y-%m-%dT%H:%M:%S.%f")[:24]
836
        if 'end_date' in d:
837
            d['end_date'] = d['end_date'].strftime("%Y-%m-%dT%H:%M:%S.%f")[:24]
838
        if 'entity' in d:
839
            d['entity'] = d['entity'].email
840
        return d
841

    
842

    
843
class AstakosGroupSortForm(forms.Form):
844
    sorting = forms.ChoiceField(
845
        label='Sort by',
846
        choices=(
847
            ('groupname', 'Name'),
848
            ('issue_date', 'Issue Date'),
849
            ('expiration_date', 'Expiration Date'),
850
            ('approved_members_num', 'Participants'),
851
            ('moderation_enabled', 'Moderation'),
852
            ('membership_status', 'Enrollment Status')
853
        ),
854
        required=True
855
    )
856

    
857
class MembersSortForm(forms.Form):
858
    sorting = forms.ChoiceField(
859
        label='Sort by',
860
        choices=(('person__email', 'User Id'),
861
                 ('person__first_name', 'Name'),
862
                 ('date_joined', 'Status')
863
        ),
864
        required=True
865
    )
866

    
867
class PickResourceForm(forms.Form):
868
    resource = forms.ModelChoiceField(
869
        queryset=Resource.objects.select_related().all()
870
    )
871
    resource.widget.attrs["onchange"] = "this.form.submit()"
872

    
873

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

    
887
    def __init__(self, user, *args, **kwargs):
888
        super(ExtendedSetPasswordForm, self).__init__(user, *args, **kwargs)
889

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

    
900
        except BaseException, e:
901
            logger.exception(e)
902
        return super(ExtendedSetPasswordForm, self).save(commit=commit)