e39b8bb475582cf03679c1195f0faa417289dbdc
[astakos] / snf-astakos-app / astakos / im / forms.py
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
35 from django import forms
36 from django.utils.translation import ugettext as _
37 from django.contrib.auth.forms import (UserCreationForm, AuthenticationForm,
38                                        PasswordResetForm, PasswordChangeForm,
39                                        SetPasswordForm)
40 from django.core.mail import send_mail
41 from django.contrib.auth.tokens import default_token_generator
42 from django.template import Context, loader
43 from django.utils.http import int_to_base36
44 from django.core.urlresolvers import reverse
45 from django.utils.safestring import mark_safe
46 from django.utils.encoding import smart_str
47 from django.forms.extras.widgets import SelectDateWidget
48 from django.conf import settings
49
50 from astakos.im.models import (AstakosUser, EmailChange, AstakosGroup,
51                                Invitation, Membership, GroupKind, Resource,
52                                get_latest_terms, RESOURCE_SEPARATOR)
53 from astakos.im.settings import (INVITATIONS_PER_LEVEL, BASEURL, SITENAME,
54                                  RECAPTCHA_PRIVATE_KEY, RECAPTCHA_ENABLED,
55                                  DEFAULT_CONTACT_EMAIL, LOGGING_LEVEL,
56                                  PASSWORD_RESET_EMAIL_SUBJECT,
57                                  NEWPASSWD_INVALIDATE_TOKEN)
58 from astakos.im.widgets import DummyWidget, RecaptchaWidget
59 from astakos.im.functions import send_change_email
60
61 from astakos.im.util import reserved_email, get_query
62
63 import logging
64 import hashlib
65 import recaptcha.client.captcha as captcha
66 from random import random
67
68 logger = logging.getLogger(__name__)
69
70
71 class LocalUserCreationForm(UserCreationForm):
72     """
73     Extends the built in UserCreationForm in several ways:
74
75     * Adds email, first_name, last_name, recaptcha_challenge_field, recaptcha_response_field field.
76     * The username field isn't visible and it is assigned a generated id.
77     * User created is not active.
78     """
79     recaptcha_challenge_field = forms.CharField(widget=DummyWidget)
80     recaptcha_response_field = forms.CharField(
81         widget=RecaptchaWidget, label='')
82
83     class Meta:
84         model = AstakosUser
85         fields = ("email", "first_name", "last_name",
86                   "has_signed_terms", "has_signed_terms")
87
88     def __init__(self, *args, **kwargs):
89         """
90         Changes the order of fields, and removes the username field.
91         """
92         request = kwargs.get('request', None)
93         if request:
94             kwargs.pop('request')
95             self.ip = request.META.get('REMOTE_ADDR',
96                                        request.META.get('HTTP_X_REAL_IP', None))
97
98         super(LocalUserCreationForm, self).__init__(*args, **kwargs)
99         self.fields.keyOrder = ['email', 'first_name', 'last_name',
100                                 'password1', 'password2']
101
102         if RECAPTCHA_ENABLED:
103             self.fields.keyOrder.extend(['recaptcha_challenge_field',
104                                          'recaptcha_response_field', ])
105         if get_latest_terms():
106             self.fields.keyOrder.append('has_signed_terms')
107
108         if 'has_signed_terms' in self.fields:
109             # Overriding field label since we need to apply a link
110             # to the terms within the label
111             terms_link_html = '<a href="%s" target="_blank">%s</a>' \
112                 % (reverse('latest_terms'), _("the terms"))
113             self.fields['has_signed_terms'].label = \
114                 mark_safe("I agree with %s" % terms_link_html)
115
116     def clean_email(self):
117         email = self.cleaned_data['email']
118         if not email:
119             raise forms.ValidationError(_("This field is required"))
120         if reserved_email(email):
121             raise forms.ValidationError(_("This email is already used"))
122         return email
123
124     def clean_has_signed_terms(self):
125         has_signed_terms = self.cleaned_data['has_signed_terms']
126         if not has_signed_terms:
127             raise forms.ValidationError(_('You have to agree with the terms'))
128         return has_signed_terms
129
130     def clean_recaptcha_response_field(self):
131         if 'recaptcha_challenge_field' in self.cleaned_data:
132             self.validate_captcha()
133         return self.cleaned_data['recaptcha_response_field']
134
135     def clean_recaptcha_challenge_field(self):
136         if 'recaptcha_response_field' in self.cleaned_data:
137             self.validate_captcha()
138         return self.cleaned_data['recaptcha_challenge_field']
139
140     def validate_captcha(self):
141         rcf = self.cleaned_data['recaptcha_challenge_field']
142         rrf = self.cleaned_data['recaptcha_response_field']
143         check = captcha.submit(rcf, rrf, RECAPTCHA_PRIVATE_KEY, self.ip)
144         if not check.is_valid:
145             raise forms.ValidationError(
146                 _('You have not entered the correct words'))
147
148     def save(self, commit=True):
149         """
150         Saves the email, first_name and last_name properties, after the normal
151         save behavior is complete.
152         """
153         user = super(LocalUserCreationForm, self).save(commit=False)
154         user.renew_token()
155         if commit:
156             user.save()
157             logger.log(LOGGING_LEVEL, 'Created user %s' % user.email)
158         return user
159
160
161 class InvitedLocalUserCreationForm(LocalUserCreationForm):
162     """
163     Extends the LocalUserCreationForm: email is readonly.
164     """
165     class Meta:
166         model = AstakosUser
167         fields = ("email", "first_name", "last_name", "has_signed_terms")
168
169     def __init__(self, *args, **kwargs):
170         """
171         Changes the order of fields, and removes the username field.
172         """
173         super(InvitedLocalUserCreationForm, self).__init__(*args, **kwargs)
174
175         #set readonly form fields
176         ro = ('email', 'username',)
177         for f in ro:
178             self.fields[f].widget.attrs['readonly'] = True
179
180     def save(self, commit=True):
181         user = super(InvitedLocalUserCreationForm, self).save(commit=False)
182         level = user.invitation.inviter.level + 1
183         user.level = level
184         user.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
185         user.email_verified = True
186         if commit:
187             user.save()
188         return user
189
190
191 class ThirdPartyUserCreationForm(forms.ModelForm):
192     class Meta:
193         model = AstakosUser
194         fields = ("email", "first_name", "last_name",
195                   "third_party_identifier", "has_signed_terms")
196
197     def __init__(self, *args, **kwargs):
198         """
199         Changes the order of fields, and removes the username field.
200         """
201         self.request = kwargs.get('request', None)
202         if self.request:
203             kwargs.pop('request')
204         super(ThirdPartyUserCreationForm, self).__init__(*args, **kwargs)
205         self.fields.keyOrder = ['email', 'first_name', 'last_name',
206                                 'third_party_identifier']
207         if get_latest_terms():
208             self.fields.keyOrder.append('has_signed_terms')
209         #set readonly form fields
210         ro = ["third_party_identifier"]
211         for f in ro:
212             self.fields[f].widget.attrs['readonly'] = True
213
214         if 'has_signed_terms' in self.fields:
215             # Overriding field label since we need to apply a link
216             # to the terms within the label
217             terms_link_html = '<a href="%s" target="_blank">%s</a>' \
218                 % (reverse('latest_terms'), _("the terms"))
219             self.fields['has_signed_terms'].label = \
220                 mark_safe("I agree with %s" % terms_link_html)
221
222     def clean_email(self):
223         email = self.cleaned_data['email']
224         if not email:
225             raise forms.ValidationError(_("This field is required"))
226         return email
227
228     def clean_has_signed_terms(self):
229         has_signed_terms = self.cleaned_data['has_signed_terms']
230         if not has_signed_terms:
231             raise forms.ValidationError(_('You have to agree with the terms'))
232         return has_signed_terms
233
234     def save(self, commit=True):
235         user = super(ThirdPartyUserCreationForm, self).save(commit=False)
236         user.set_unusable_password()
237         user.renew_token()
238         user.provider = get_query(self.request).get('provider')
239         if commit:
240             user.save()
241             logger.log(LOGGING_LEVEL, 'Created user %s' % user.email)
242         return user
243
244
245 class InvitedThirdPartyUserCreationForm(ThirdPartyUserCreationForm):
246     """
247     Extends the ThirdPartyUserCreationForm: email is readonly.
248     """
249     def __init__(self, *args, **kwargs):
250         """
251         Changes the order of fields, and removes the username field.
252         """
253         super(
254             InvitedThirdPartyUserCreationForm, self).__init__(*args, **kwargs)
255
256         #set readonly form fields
257         ro = ('email',)
258         for f in ro:
259             self.fields[f].widget.attrs['readonly'] = True
260
261     def save(self, commit=True):
262         user = super(
263             InvitedThirdPartyUserCreationForm, self).save(commit=False)
264         level = user.invitation.inviter.level + 1
265         user.level = level
266         user.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
267         user.email_verified = True
268         if commit:
269             user.save()
270         return user
271
272
273 class ShibbolethUserCreationForm(ThirdPartyUserCreationForm):
274     additional_email = forms.CharField(
275         widget=forms.HiddenInput(), label='', required=False)
276
277     def __init__(self, *args, **kwargs):
278         super(ShibbolethUserCreationForm, self).__init__(*args, **kwargs)
279         self.fields.keyOrder.append('additional_email')
280         # copy email value to additional_mail in case user will change it
281         name = 'email'
282         field = self.fields[name]
283         self.initial['additional_email'] = self.initial.get(name,
284                                                             field.initial)
285
286     def clean_email(self):
287         email = self.cleaned_data['email']
288         for user in AstakosUser.objects.filter(email=email):
289             if user.provider == 'shibboleth':
290                 raise forms.ValidationError(_("This email is already associated with another shibboleth account."))
291             elif not user.is_active:
292                 raise forms.ValidationError(_("This email is already associated with an inactive account. \
293                                               You need to wait to be activated before being able to switch to a shibboleth account."))
294         super(ShibbolethUserCreationForm, self).clean_email()
295         return email
296
297
298 class InvitedShibbolethUserCreationForm(ShibbolethUserCreationForm,
299                                         InvitedThirdPartyUserCreationForm):
300     pass
301
302
303 class LoginForm(AuthenticationForm):
304     username = forms.EmailField(label=_("Email"))
305     recaptcha_challenge_field = forms.CharField(widget=DummyWidget)
306     recaptcha_response_field = forms.CharField(
307         widget=RecaptchaWidget, label='')
308
309     def __init__(self, *args, **kwargs):
310         was_limited = kwargs.get('was_limited', False)
311         request = kwargs.get('request', None)
312         if request:
313             self.ip = request.META.get('REMOTE_ADDR',
314                                        request.META.get('HTTP_X_REAL_IP', None))
315
316         t = ('request', 'was_limited')
317         for elem in t:
318             if elem in kwargs.keys():
319                 kwargs.pop(elem)
320         super(LoginForm, self).__init__(*args, **kwargs)
321
322         self.fields.keyOrder = ['username', 'password']
323         if was_limited and RECAPTCHA_ENABLED:
324             self.fields.keyOrder.extend(['recaptcha_challenge_field',
325                                          'recaptcha_response_field', ])
326
327     def clean_username(self):
328         if 'username' in self.cleaned_data:
329             return self.cleaned_data['username'].lower()
330
331     def clean_recaptcha_response_field(self):
332         if 'recaptcha_challenge_field' in self.cleaned_data:
333             self.validate_captcha()
334         return self.cleaned_data['recaptcha_response_field']
335
336     def clean_recaptcha_challenge_field(self):
337         if 'recaptcha_response_field' in self.cleaned_data:
338             self.validate_captcha()
339         return self.cleaned_data['recaptcha_challenge_field']
340
341     def validate_captcha(self):
342         rcf = self.cleaned_data['recaptcha_challenge_field']
343         rrf = self.cleaned_data['recaptcha_response_field']
344         check = captcha.submit(rcf, rrf, RECAPTCHA_PRIVATE_KEY, self.ip)
345         if not check.is_valid:
346             raise forms.ValidationError(
347                 _('You have not entered the correct words'))
348
349     def clean(self):
350         super(LoginForm, self).clean()
351         if self.user_cache and self.user_cache.provider not in ('local', ''):
352             raise forms.ValidationError(_('Local login is not the current authentication method for this account.'))
353         return self.cleaned_data
354
355
356 class ProfileForm(forms.ModelForm):
357     """
358     Subclass of ``ModelForm`` for permiting user to edit his/her profile.
359     Most of the fields are readonly since the user is not allowed to change
360     them.
361
362     The class defines a save method which sets ``is_verified`` to True so as the
363     user during the next login will not to be redirected to profile page.
364     """
365     renew = forms.BooleanField(label='Renew token', required=False)
366
367     class Meta:
368         model = AstakosUser
369         fields = ('email', 'first_name', 'last_name', 'auth_token',
370                   'auth_token_expires')
371
372     def __init__(self, *args, **kwargs):
373         super(ProfileForm, self).__init__(*args, **kwargs)
374         instance = getattr(self, 'instance', None)
375         ro_fields = ('email', 'auth_token', 'auth_token_expires')
376         if instance and instance.id:
377             for field in ro_fields:
378                 self.fields[field].widget.attrs['readonly'] = True
379
380     def save(self, commit=True):
381         user = super(ProfileForm, self).save(commit=False)
382         user.is_verified = True
383         if self.cleaned_data.get('renew'):
384             user.renew_token()
385         if commit:
386             user.save()
387         return user
388
389
390 class FeedbackForm(forms.Form):
391     """
392     Form for writing feedback.
393     """
394     feedback_msg = forms.CharField(widget=forms.Textarea, label=u'Message')
395     feedback_data = forms.CharField(widget=forms.HiddenInput(), label='',
396                                     required=False)
397
398
399 class SendInvitationForm(forms.Form):
400     """
401     Form for sending an invitations
402     """
403
404     email = forms.EmailField(required=True, label='Email address')
405     first_name = forms.EmailField(label='First name')
406     last_name = forms.EmailField(label='Last name')
407
408
409 class ExtendedPasswordResetForm(PasswordResetForm):
410     """
411     Extends PasswordResetForm by overriding save method:
412     passes a custom from_email in send_mail.
413
414     Since Django 1.3 this is useless since ``django.contrib.auth.views.reset_password``
415     accepts a from_email argument.
416     """
417     def clean_email(self):
418         email = super(ExtendedPasswordResetForm, self).clean_email()
419         try:
420             user = AstakosUser.objects.get(email=email, is_active=True)
421             if not user.has_usable_password():
422                 raise forms.ValidationError(
423                     _("This account has not a usable password."))
424         except AstakosUser.DoesNotExist:
425             raise forms.ValidationError(_('That e-mail address doesn\'t have an associated user account. Are you sure you\'ve registered?'))
426         return email
427
428     def save(
429         self, domain_override=None, email_template_name='registration/password_reset_email.html',
430             use_https=False, token_generator=default_token_generator, request=None):
431         """
432         Generates a one-use only link for resetting password and sends to the user.
433         """
434         for user in self.users_cache:
435             url = reverse('django.contrib.auth.views.password_reset_confirm',
436                           kwargs={'uidb36': int_to_base36(user.id),
437                                   'token': token_generator.make_token(user)
438                                   }
439                           )
440             url = urljoin(BASEURL, url)
441             t = loader.get_template(email_template_name)
442             c = {
443                 'email': user.email,
444                 'url': url,
445                 'site_name': SITENAME,
446                 'user': user,
447                 'baseurl': BASEURL,
448                 'support': DEFAULT_CONTACT_EMAIL
449             }
450             from_email = settings.SERVER_EMAIL
451             send_mail(_(PASSWORD_RESET_EMAIL_SUBJECT),
452                       t.render(Context(c)), from_email, [user.email])
453
454
455 class EmailChangeForm(forms.ModelForm):
456     class Meta:
457         model = EmailChange
458         fields = ('new_email_address',)
459
460     def clean_new_email_address(self):
461         addr = self.cleaned_data['new_email_address']
462         if AstakosUser.objects.filter(email__iexact=addr):
463             raise forms.ValidationError(_(u'This email address is already in use. Please supply a different email address.'))
464         return addr
465
466     def save(self, email_template_name, request, commit=True):
467         ec = super(EmailChangeForm, self).save(commit=False)
468         ec.user = request.user
469         activation_key = hashlib.sha1(
470             str(random()) + smart_str(ec.new_email_address))
471         ec.activation_key = activation_key.hexdigest()
472         if commit:
473             ec.save()
474         send_change_email(ec, request, email_template_name=email_template_name)
475
476
477 class SignApprovalTermsForm(forms.ModelForm):
478     class Meta:
479         model = AstakosUser
480         fields = ("has_signed_terms",)
481
482     def __init__(self, *args, **kwargs):
483         super(SignApprovalTermsForm, self).__init__(*args, **kwargs)
484
485     def clean_has_signed_terms(self):
486         has_signed_terms = self.cleaned_data['has_signed_terms']
487         if not has_signed_terms:
488             raise forms.ValidationError(_('You have to agree with the terms'))
489         return has_signed_terms
490
491
492 class InvitationForm(forms.ModelForm):
493     username = forms.EmailField(label=_("Email"))
494
495     def __init__(self, *args, **kwargs):
496         super(InvitationForm, self).__init__(*args, **kwargs)
497
498     class Meta:
499         model = Invitation
500         fields = ('username', 'realname')
501
502     def clean_username(self):
503         username = self.cleaned_data['username']
504         try:
505             Invitation.objects.get(username=username)
506             raise forms.ValidationError(
507                 _('There is already invitation for this email.'))
508         except Invitation.DoesNotExist:
509             pass
510         return username
511
512
513 class ExtendedPasswordChangeForm(PasswordChangeForm):
514     """
515     Extends PasswordChangeForm by enabling user
516     to optionally renew also the token.
517     """
518     if not NEWPASSWD_INVALIDATE_TOKEN:
519         renew = forms.BooleanField(label='Renew token', required=False,
520                                    initial=True,
521                                    help_text='Unsetting this may result in security risk.')
522
523     def __init__(self, user, *args, **kwargs):
524         super(ExtendedPasswordChangeForm, self).__init__(user, *args, **kwargs)
525
526     def save(self, commit=True):
527         if NEWPASSWD_INVALIDATE_TOKEN or self.cleaned_data.get('renew'):
528             self.user.renew_token()
529         return super(ExtendedPasswordChangeForm, self).save(commit=commit)
530
531
532 class AstakosGroupCreationForm(forms.ModelForm):
533     kind = forms.ModelChoiceField(
534         queryset=GroupKind.objects.all(),
535         label="",
536         widget=forms.HiddenInput()
537     )
538     name = forms.URLField()
539     moderation_enabled = forms.BooleanField(
540         help_text="Check if you want to approve members participation manually",
541         required=False,
542         initial=True
543     )
544     max_participants = forms.IntegerField(
545         required=False, min_value=1
546     )
547
548     class Meta:
549         model = AstakosGroup
550
551     def __init__(self, *args, **kwargs):
552         #update QueryDict
553         args = list(args)
554         qd = args.pop(0).copy()
555         members_unlimited = qd.pop('members_unlimited', False)
556         members_uplimit = qd.pop('members_uplimit', None)
557 #         max_participants = None if members_unlimited else members_uplimit
558 #         qd['max_participants']= max_participants.pop(0) if max_participants else None
559         
560         #substitue QueryDict
561         args.insert(0, qd)
562         
563         super(AstakosGroupCreationForm, self).__init__(*args, **kwargs)
564         self.fields.keyOrder = ['kind', 'name', 'homepage', 'desc',
565                                 'issue_date', 'expiration_date',
566                                 'moderation_enabled', 'max_participants']
567         def add_fields((k, v)):
568             print '####', k, v
569             k = k.partition('_proxy')[0]
570             self.fields[k] = forms.IntegerField(
571                 required=False,
572                 widget=forms.HiddenInput(),
573                 min_value=1
574             )
575         map(add_fields,
576             ((k, v) for k,v in qd.iteritems() if k.endswith('_uplimit_proxy'))
577         )
578         
579         def add_fields((k, v)):
580             self.fields[k] = forms.BooleanField(
581                 required=False,
582                 widget=forms.HiddenInput()
583             )
584         map(add_fields,
585             ((k, v) for k,v in qd.iteritems() if k.startswith('is_selected_'))
586         )
587     
588     def policies(self):
589         self.clean()
590         policies = []
591         append = policies.append
592         for name, uplimit in self.cleaned_data.iteritems():
593             subs = name.split('_uplimit')
594             if len(subs) == 2:
595                 prefix, suffix = subs
596                 s, sep, r = prefix.partition(RESOURCE_SEPARATOR)
597                 resource = Resource.objects.get(service__name=s, name=r)
598                 
599                 # keep only resource limits for selected resource groups
600                 if self.cleaned_data.get(
601                     'is_selected_%s' % resource.group, True
602                 ):
603                     append(dict(service=s, resource=r, uplimit=uplimit))
604         return policies
605
606 class AstakosGroupCreationSummaryForm(forms.ModelForm):
607     kind = forms.ModelChoiceField(
608         queryset=GroupKind.objects.all(),
609         label="",
610         widget=forms.HiddenInput()
611     )
612     name = forms.URLField()
613     moderation_enabled = forms.BooleanField(
614         help_text="Check if you want to approve members participation manually",
615         required=False,
616         initial=True
617     )
618     max_participants = forms.IntegerField(
619         required=False, min_value=1
620     )
621
622     class Meta:
623         model = AstakosGroup
624
625     def __init__(self, *args, **kwargs):
626         #update QueryDict
627         args = list(args)
628         qd = args.pop(0).copy()
629         members_unlimited = qd.pop('members_unlimited', False)
630         members_uplimit = qd.pop('members_uplimit', None)
631 #         max_participants = None if members_unlimited else members_uplimit
632 #         qd['max_participants']= max_participants.pop(0) if max_participants else None
633         
634         #substitue QueryDict
635         args.insert(0, qd)
636         
637         super(AstakosGroupCreationSummaryForm, self).__init__(*args, **kwargs)
638         self.fields.keyOrder = ['kind', 'name', 'homepage', 'desc',
639                                 'issue_date', 'expiration_date',
640                                 'moderation_enabled', 'max_participants']
641         def add_fields((k, v)):
642             self.fields[k] = forms.IntegerField(
643                 required=False,
644                 #widget=forms.TextInput(),
645                 min_value=1
646             )
647         map(add_fields,
648             ((k, v) for k,v in qd.iteritems() if k.endswith('_uplimit'))
649         )
650         
651         def add_fields((k, v)):
652             self.fields[k] = forms.BooleanField(
653                 required=False,
654                 #widget=forms.HiddenInput()
655             )
656         map(add_fields,
657             ((k, v) for k,v in qd.iteritems() if k.startswith('is_selected_'))
658         )
659         for f in self.fields.values():
660             f.widget = forms.HiddenInput()
661
662     def clean(self):
663         super(AstakosGroupCreationSummaryForm, self).clean()
664         self.cleaned_data['policies'] = []
665         append = self.cleaned_data['policies'].append
666         tbd = [f for f in self.fields if f.startswith('is_selected_')]
667         for name, uplimit in self.cleaned_data.iteritems():
668             subs = name.split('_uplimit')
669             if len(subs) == 2:
670                 tbd.append(name)
671                 prefix, suffix = subs
672                 s, sep, r = prefix.partition(RESOURCE_SEPARATOR)
673                 resource = Resource.objects.get(service__name=s, name=r)
674                 
675                 # keep only resource limits for selected resource groups
676                 if self.cleaned_data.get(
677                     'is_selected_%s' % resource.group, True
678                 ):
679                     append(dict(service=s, resource=r, uplimit=uplimit))
680         for name in tbd:
681             self.cleaned_data.pop(name, None)
682         return self.cleaned_data
683
684 class AstakosGroupUpdateForm(forms.ModelForm):
685     class Meta:
686         model = AstakosGroup
687         fields = ('homepage', 'desc')
688
689
690 class AddGroupMembersForm(forms.Form):
691     q = forms.CharField(
692         max_length=800, widget=forms.Textarea, label=_('Add users'),
693         help_text=_('Add comma separated user emails, eg. user1@user.com, user2@user.com'),
694         required=True)
695
696     def clean(self):
697         q = self.cleaned_data.get('q') or ''
698         users = q.split(',')
699         users = list(u.strip() for u in users if u)
700         db_entries = AstakosUser.objects.filter(email__in=users)
701         unknown = list(set(users) - set(u.email for u in db_entries))
702         if unknown:
703             raise forms.ValidationError(
704                 _('Unknown users: %s' % ','.join(unknown)))
705         self.valid_users = db_entries
706         return self.cleaned_data
707
708     def get_valid_users(self):
709         """Should be called after form cleaning"""
710         try:
711             return self.valid_users
712         except:
713             return ()
714
715
716 class AstakosGroupSearchForm(forms.Form):
717     q = forms.CharField(max_length=200, label='Search group')
718
719
720 class TimelineForm(forms.Form):
721 #    entity = forms.CharField(
722 #        widget=forms.HiddenInput(), label='')
723     entity = forms.ModelChoiceField(
724         queryset=AstakosUser.objects.filter(is_active=True)
725     )
726     resource = forms.ModelChoiceField(
727         queryset=Resource.objects.all()
728     )
729     start_date = forms.DateTimeField()
730     end_date = forms.DateTimeField()
731     details = forms.BooleanField(required=False, label="Detailed Listing")
732     operation = forms.ChoiceField(
733         label='Charge Method',
734         choices=(('', '-------------'),
735                  ('charge_usage', 'Charge Usage'),
736                  ('charge_traffic', 'Charge Traffic'), )
737     )
738
739     def clean(self):
740         super(TimelineForm, self).clean()
741         d = self.cleaned_data
742         if 'resource' in d:
743             d['resource'] = str(d['resource'])
744         if 'start_date' in d:
745             d['start_date'] = d['start_date'].strftime(
746                 "%Y-%m-%dT%H:%M:%S.%f")[:24]
747         if 'end_date' in d:
748             d['end_date'] = d['end_date'].strftime("%Y-%m-%dT%H:%M:%S.%f")[:24]
749         if 'entity' in d:
750             d['entity'] = d['entity'].email
751         return d
752
753
754 class AstakosGroupSortForm(forms.Form):
755     sort_by = forms.ChoiceField(label='Sort by',
756                                 choices=(('groupname', 'Name'),
757                                          ('kindname', 'Type'),
758                                          ('issue_date', 'Issue Date'),
759                                          ('expiration_date',
760                                           'Expiration Date'),
761                                          ('approved_members_num',
762                                           'Participants'),
763                                          ('is_enabled', 'Status'),
764                                          ('moderation_enabled', 'Moderation'),
765                                          ('membership_status',
766                                           'Enrollment Status')
767                                          ),
768                                 required=False)
769
770
771 class MembersSortForm(forms.Form):
772     sort_by = forms.ChoiceField(label='Sort by',
773                                 choices=(('person__email', 'User Id'),
774                                          ('person__first_name', 'Name'),
775                                          ('date_joined', 'Status')
776                                          ),
777                                 required=False)
778
779
780 class PickResourceForm(forms.Form):
781     resource = forms.ModelChoiceField(
782         queryset=Resource.objects.select_related().all()
783     )
784     resource.widget.attrs["onchange"] = "this.form.submit()"
785
786
787 class ExtendedSetPasswordForm(SetPasswordForm):
788     """
789     Extends SetPasswordForm by enabling user
790     to optionally renew also the token.
791     """
792     if not NEWPASSWD_INVALIDATE_TOKEN:
793         renew = forms.BooleanField(label='Renew token', required=False,
794                                    initial=True,
795                                    help_text='Unsetting this may result in security risk.')
796
797     def __init__(self, user, *args, **kwargs):
798         super(ExtendedSetPasswordForm, self).__init__(user, *args, **kwargs)
799
800     def save(self, commit=True):
801         if NEWPASSWD_INVALIDATE_TOKEN or self.cleaned_data.get('renew'):
802             if isinstance(self.user, AstakosUser):
803                 self.user.renew_token()
804         return super(ExtendedSetPasswordForm, self).save(commit=commit)