Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (32.9 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
        if 'username' in self.cleaned_data:
363
            return self.cleaned_data['username'].lower()
364

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

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

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

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

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

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

    
409

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

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

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

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

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

    
447

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

    
456

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

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

    
466

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

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

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

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

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

    
512

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

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

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

    
534

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

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

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

    
549

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

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

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

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

    
569

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

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

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

    
594

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

    
640
    class Meta:
641
        model = AstakosGroup
642

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

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

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

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

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

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

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

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

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

    
716
    class Meta:
717
        model = AstakosGroup
718

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

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

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

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

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

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

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

    
782

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

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

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

    
807

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

    
811

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

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

    
843

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

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

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

    
874

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

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

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

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