Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / im / forms.py @ 7233d542

History | View | Annotate | Download (31.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.update_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
        return email
266

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

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

    
280

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

    
290

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

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

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

    
315

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

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

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

    
345

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

    
350

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

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

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

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

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

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

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

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

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

    
410

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

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

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

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

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

    
448

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

    
457

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

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

    
467

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

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

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

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

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

    
513

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

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

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

    
535

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

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

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

    
550

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

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

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

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

    
570

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

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

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

    
595

    
596
class AstakosGroupCreationForm(forms.ModelForm):
597
    kind = forms.ModelChoiceField(
598
        queryset=GroupKind.objects.all(),
599
        label="",
600
        widget=forms.HiddenInput()
601
    )
602
    name = forms.CharField(
603
        validators=[validators.RegexValidator(
604
            DOMAIN_VALUE_REGEX,
605
            _(astakos_messages.DOMAIN_VALUE_ERR), 'invalid'
606
        )],
607
        widget=forms.TextInput(attrs={'placeholder': 'eg. foo.ece.ntua.gr'}),
608
        help_text="Name should be in the form of dns"
609
    )
610
    moderation_enabled = forms.BooleanField(
611
        help_text="Check if you want to approve members participation manually",
612
        required=False,
613
        initial=True
614
    )
615
    max_participants = forms.IntegerField(
616
        required=True, min_value=1
617
    )
618

    
619
    class Meta:
620
        model = AstakosGroup
621

    
622
    def __init__(self, *args, **kwargs):
623
        #update QueryDict
624
        args = list(args)
625
        qd = args.pop(0).copy()
626
        members_unlimited = qd.pop('members_unlimited', False)
627
        members_uplimit = qd.pop('members_uplimit', None)
628

    
629
        #substitue QueryDict
630
        args.insert(0, qd)
631

    
632
        super(AstakosGroupCreationForm, self).__init__(*args, **kwargs)
633
        
634
        self.fields.keyOrder = ['kind', 'name', 'homepage', 'desc',
635
                                'issue_date', 'expiration_date',
636
                                'moderation_enabled', 'max_participants']
637
        def add_fields((k, v)):
638
            k = k.partition('_proxy')[0]
639
            self.fields[k] = forms.IntegerField(
640
                required=False,
641
                widget=forms.HiddenInput(),
642
                min_value=1
643
            )
644
        map(add_fields,
645
            ((k, v) for k,v in qd.iteritems() if k.endswith('_uplimit'))
646
        )
647

    
648
        def add_fields((k, v)):
649
            self.fields[k] = forms.BooleanField(
650
                required=False,
651
                #widget=forms.HiddenInput()
652
            )
653
        map(add_fields,
654
            ((k, v) for k,v in qd.iteritems() if k.startswith('is_selected_'))
655
        )
656

    
657
    def policies(self):
658
        self.clean()
659
        policies = []
660
        append = policies.append
661
        for name, uplimit in self.cleaned_data.iteritems():
662

    
663
            subs = name.split('_uplimit')
664
            if len(subs) == 2:
665
                prefix, suffix = subs
666
                s, sep, r = prefix.partition(RESOURCE_SEPARATOR)
667
                resource = Resource.objects.get(service__name=s, name=r)
668

    
669
                # keep only resource limits for selected resource groups
670
                if self.cleaned_data.get(
671
                    'is_selected_%s' % resource.group, False
672
                ):
673
                    append(dict(service=s, resource=r, uplimit=uplimit))
674
        return policies
675

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

    
695
    class Meta:
696
        model = AstakosGroup
697

    
698
    def __init__(self, *args, **kwargs):
699
        #update QueryDict
700
        args = list(args)
701
        qd = args.pop(0).copy()
702
        members_unlimited = qd.pop('members_unlimited', False)
703
        members_uplimit = qd.pop('members_uplimit', None)
704

    
705
        #substitue QueryDict
706
        args.insert(0, qd)
707

    
708
        super(AstakosGroupCreationSummaryForm, self).__init__(*args, **kwargs)
709
        self.fields.keyOrder = ['kind', 'name', 'homepage', 'desc',
710
                                'issue_date', 'expiration_date',
711
                                'moderation_enabled', 'max_participants']
712
        def add_fields((k, v)):
713
            self.fields[k] = forms.IntegerField(
714
                required=False,
715
                widget=forms.TextInput(),
716
                min_value=1
717
            )
718
        map(add_fields,
719
            ((k, v) for k,v in qd.iteritems() if k.endswith('_uplimit'))
720
        )
721

    
722
        def add_fields((k, v)):
723
            self.fields[k] = forms.BooleanField(
724
                required=False,
725
                widget=forms.HiddenInput()
726
            )
727
        map(add_fields,
728
            ((k, v) for k,v in qd.iteritems() if k.startswith('is_selected_'))
729
        )
730
        for f in self.fields.values():
731
            f.widget = forms.HiddenInput()
732

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

    
747
                # keep only resource limits for selected resource groups
748
                if self.cleaned_data.get(
749
                    'is_selected_%s' % resource.group, False
750
                ):
751
                    append(dict(service=s, resource=r, uplimit=uplimit))
752
        for name in tbd:
753
            self.cleaned_data.pop(name, None)
754
        return self.cleaned_data
755

    
756
class AstakosGroupUpdateForm(forms.ModelForm):
757
    class Meta:
758
        model = AstakosGroup
759
        fields = ( 'desc','homepage', 'moderation_enabled')
760

    
761

    
762
class AddGroupMembersForm(forms.Form):
763
    q = forms.CharField(
764
        max_length=800, widget=forms.Textarea, label=_('Add members'),
765
        help_text=_(astakos_messages.ADD_GROUP_MEMBERS_Q_HELP),
766
        required=True)
767

    
768
    def clean(self):
769
        q = self.cleaned_data.get('q') or ''
770
        users = q.split(',')
771
        users = list(u.strip() for u in users if u)
772
        db_entries = AstakosUser.objects.filter(email__in=users)
773
        unknown = list(set(users) - set(u.email for u in db_entries))
774
        if unknown:
775
            raise forms.ValidationError(_(astakos_messages.UNKNOWN_USERS) % ','.join(unknown))
776
        self.valid_users = db_entries
777
        return self.cleaned_data
778

    
779
    def get_valid_users(self):
780
        """Should be called after form cleaning"""
781
        try:
782
            return self.valid_users
783
        except:
784
            return ()
785

    
786

    
787
class AstakosGroupSearchForm(forms.Form):
788
    q = forms.CharField(max_length=200, label='Search project')
789

    
790

    
791
class TimelineForm(forms.Form):
792
    entity = forms.ModelChoiceField(
793
        queryset=AstakosUser.objects.filter(is_active=True)
794
    )
795
    resource = forms.ModelChoiceField(
796
        queryset=Resource.objects.all()
797
    )
798
    start_date = forms.DateTimeField()
799
    end_date = forms.DateTimeField()
800
    details = forms.BooleanField(required=False, label="Detailed Listing")
801
    operation = forms.ChoiceField(
802
        label='Charge Method',
803
        choices=(('', '-------------'),
804
                 ('charge_usage', 'Charge Usage'),
805
                 ('charge_traffic', 'Charge Traffic'), )
806
    )
807

    
808
    def clean(self):
809
        super(TimelineForm, self).clean()
810
        d = self.cleaned_data
811
        if 'resource' in d:
812
            d['resource'] = str(d['resource'])
813
        if 'start_date' in d:
814
            d['start_date'] = d['start_date'].strftime(
815
                "%Y-%m-%dT%H:%M:%S.%f")[:24]
816
        if 'end_date' in d:
817
            d['end_date'] = d['end_date'].strftime("%Y-%m-%dT%H:%M:%S.%f")[:24]
818
        if 'entity' in d:
819
            d['entity'] = d['entity'].email
820
        return d
821

    
822

    
823
class AstakosGroupSortForm(forms.Form):
824
    sorting = forms.ChoiceField(
825
        label='Sort by',
826
        choices=(
827
            ('groupname', 'Name'),
828
            ('issue_date', 'Issue Date'),
829
            ('expiration_date', 'Expiration Date'),
830
            ('approved_members_num', 'Participants'),
831
            ('moderation_enabled', 'Moderation'),
832
            ('membership_status', 'Enrollment Status')
833
        ),
834
        required=True
835
    )
836

    
837
class MembersSortForm(forms.Form):
838
    sorting = forms.ChoiceField(
839
        label='Sort by',
840
        choices=(('person__email', 'User Id'),
841
                 ('person__first_name', 'Name'),
842
                 ('date_joined', 'Status')
843
        ),
844
        required=True
845
    )
846

    
847
class PickResourceForm(forms.Form):
848
    resource = forms.ModelChoiceField(
849
        queryset=Resource.objects.select_related().all()
850
    )
851
    resource.widget.attrs["onchange"] = "this.form.submit()"
852

    
853

    
854
class ExtendedSetPasswordForm(SetPasswordForm):
855
    """
856
    Extends SetPasswordForm by enabling user
857
    to optionally renew also the token.
858
    """
859
    if not NEWPASSWD_INVALIDATE_TOKEN:
860
        renew = forms.BooleanField(
861
            label='Renew token',
862
            required=False,
863
            initial=True,
864
            help_text='Unsetting this may result in security risk.'
865
        )
866

    
867
    def __init__(self, user, *args, **kwargs):
868
        super(ExtendedSetPasswordForm, self).__init__(user, *args, **kwargs)
869

    
870
    @transaction.commit_on_success()
871
    def save(self, commit=True):
872
        try:
873
            self.user = AstakosUser.objects.get(id=self.user.id)
874
            if NEWPASSWD_INVALIDATE_TOKEN or self.cleaned_data.get('renew'):
875
                self.user.renew_token()
876
            #self.user.flush_sessions()
877
            if not self.user.has_auth_provider('local'):
878
                self.user.add_auth_provider('local', auth_backend='astakos')
879

    
880
        except BaseException, e:
881
            logger.exception(e)
882
        return super(ExtendedSetPasswordForm, self).save(commit=commit)