Merge remote-tracking branch 'origin' into future
authorSofia Papagiannaki <papagian@gmail.com>
Mon, 22 Oct 2012 14:31:46 +0000 (17:31 +0300)
committerSofia Papagiannaki <papagian@gmail.com>
Mon, 22 Oct 2012 14:31:46 +0000 (17:31 +0300)
Conflicts:
snf-astakos-app/README
snf-astakos-app/astakos/im/forms.py
snf-astakos-app/astakos/im/settings.py
snf-astakos-app/astakos/im/static/im/js/forms.js
snf-astakos-app/astakos/im/target/redirect.py
snf-astakos-app/astakos/im/target/shibboleth.py
snf-astakos-app/astakos/im/urls.py
snf-astakos-app/astakos/im/util.py
snf-astakos-app/astakos/im/views.py
snf-astakos-app/conf/20-snf-astakos-app-settings.conf

1  2 
snf-astakos-app/README
snf-astakos-app/astakos/im/forms.py
snf-astakos-app/astakos/im/management/commands/user-modify.py
snf-astakos-app/astakos/im/settings.py
snf-astakos-app/astakos/im/target/local.py
snf-astakos-app/astakos/im/target/redirect.py
snf-astakos-app/astakos/im/target/shibboleth.py
snf-astakos-app/astakos/im/urls.py
snf-astakos-app/astakos/im/util.py
snf-astakos-app/astakos/im/views.py
snf-astakos-app/conf/20-snf-astakos-app-settings.conf

diff --combined snf-astakos-app/README
@@@ -89,14 -89,8 +89,18 @@@ ASTAKOS_ADMIN_NOTIFICATION_EMAIL_SUBJEC
  ASTAKOS_HELPDESK_NOTIFICATION_EMAIL_SUBJECT '%s alpha2 testing account activated (%%(user)s)' % SITENAME                    Account activation helpdesk notification email subject
  ASTAKOS_EMAIL_CHANGE_EMAIL_SUBJECT          'Email change on %s alpha2 testing' % SITENAME                                  Email change subject               
  ASTAKOS_PASSWORD_RESET_EMAIL_SUBJECT        'Password reset on %s alpha2 testing' % SITENAME                                Password change email subject
++
 +ASTAKOS_QUOTA_HOLDER_URL                    ''                                                                              The quota holder URI
 +                                                                                                                    e.g. ``http://localhost:8080/api/quotaholder/v``
 +ASTAKOS_SERVICES                            {'cyclades': {'url':'https://node1.example.com/ui/', 'quota': {'vm': 2}},       Cloud service default url and quota      
 +                                            'pithos+':  {'url':'https://node2.example.com/ui/', 'quota': {                  
 +                                            'diskspace': 50 * 1024 * 1024 * 1024}}})                                        
 +ASTAKOS_AQUARIUM_URL                        ''                                                                              The billing (aquarium) URI
 +                                                                                                                    e.g. ``http://localhost:8888/user``
 +ASTAKOS_PAGINATE_BY                         10                                                                              Number of object to be displayed per page
++
+ ASTAKOS_NEWPASSWD_INVALIDATE_TOKEN          True                                                                            Enforce token renewal on password change/reset. If set to False, user can optionally decide
+                                                                                                                             whether to renew the token or not.
  =========================================== =============================================================================   ===========================================================================================
  
  Administrator functions
@@@ -122,4 -116,11 +126,4 @@@ showuser         Show user inf
  
  To update user credibility from the billing system (Aquarium), enable the queue, install snf-pithos-tools and use ``pithos-dispatcher``::
  
 -    pithos-dispatcher --exchange=aquarium --callback=astakos.im.queue.listener.on_creditevent
 -
 -Load groups:
 -------------
 -
 -To set the initial user groups load the followind fixture:
 -
 -    snf-manage loaddata groups
 +    pithos-dispatcher --exchange=aquarium --callback=astakos.im.endpoints.aquarium.consumer.on_creditevent
  # interpreted as representing official policies, either expressed
  # or implied, of GRNET S.A.
  from urlparse import urljoin
  
  from django import forms
  from django.utils.translation import ugettext as _
 -from django.contrib.auth.forms import UserCreationForm, AuthenticationForm, \
 -    PasswordResetForm, PasswordChangeForm, SetPasswordForm
 +from django.contrib.auth.forms import (UserCreationForm, AuthenticationForm,
-                                        PasswordResetForm, PasswordChangeForm
-                                        )
++                                       PasswordResetForm, PasswordChangeForm,
++                                       SetPasswordForm)
  from django.core.mail import send_mail
  from django.contrib.auth.tokens import default_token_generator
  from django.template import Context, loader
  from django.utils.http import int_to_base36
  from django.core.urlresolvers import reverse
 -from django.utils.functional import lazy
  from django.utils.safestring import mark_safe
 -from django.contrib import messages
  from django.utils.encoding import smart_str
 -
 -from astakos.im.models import AstakosUser, Invitation, get_latest_terms, EmailChange
 -from astakos.im.settings import INVITATIONS_PER_LEVEL, DEFAULT_FROM_EMAIL, \
 -    BASEURL, SITENAME, RECAPTCHA_PRIVATE_KEY, DEFAULT_CONTACT_EMAIL, \
 -    RECAPTCHA_ENABLED, LOGGING_LEVEL, PASSWORD_RESET_EMAIL_SUBJECT, \
 -    NEWPASSWD_INVALIDATE_TOKEN
 +from django.forms.extras.widgets import SelectDateWidget
 +from django.conf import settings
 +
 +from astakos.im.models import (AstakosUser, EmailChange, AstakosGroup,
 +                               Invitation, Membership, GroupKind, Resource,
 +                               get_latest_terms)
 +from astakos.im.settings import (INVITATIONS_PER_LEVEL, BASEURL, SITENAME,
 +                                 RECAPTCHA_PRIVATE_KEY, RECAPTCHA_ENABLED,
 +                                 DEFAULT_CONTACT_EMAIL, LOGGING_LEVEL,
-                                  PASSWORD_RESET_EMAIL_SUBJECT)
++                                 PASSWORD_RESET_EMAIL_SUBJECT, 
++                                 NEWPASSWD_INVALIDATE_TOKEN)
  from astakos.im.widgets import DummyWidget, RecaptchaWidget
  from astakos.im.functions import send_change_email
  
 -# since Django 1.4 use django.core.urlresolvers.reverse_lazy instead
 -from astakos.im.util import reverse_lazy, reserved_email, get_query
 +from astakos.im.util import reserved_email, get_query
  
  import logging
  import hashlib
@@@ -67,7 -65,6 +67,7 @@@ from random import rando
  
  logger = logging.getLogger(__name__)
  
 +
  class LocalUserCreationForm(UserCreationForm):
      """
      Extends the built in UserCreationForm in several ways:
      * User created is not active.
      """
      recaptcha_challenge_field = forms.CharField(widget=DummyWidget)
 -    recaptcha_response_field = forms.CharField(widget=RecaptchaWidget, label='')
 +    recaptcha_response_field = forms.CharField(
 +        widget=RecaptchaWidget, label='')
  
      class Meta:
          model = AstakosUser
 -        fields = ("email", "first_name", "last_name", "has_signed_terms", "has_signed_terms")
 +        fields = ("email", "first_name", "last_name",
 +                  "has_signed_terms", "has_signed_terms")
  
      def __init__(self, *args, **kwargs):
          """
  
          if RECAPTCHA_ENABLED:
              self.fields.keyOrder.extend(['recaptcha_challenge_field',
 -                                         'recaptcha_response_field',])
 +                                         'recaptcha_response_field', ])
          if get_latest_terms():
              self.fields.keyOrder.append('has_signed_terms')
  
              # Overriding field label since we need to apply a link
              # to the terms within the label
              terms_link_html = '<a href="%s" target="_blank">%s</a>' \
 -                    % (reverse('latest_terms'), _("the terms"))
 +                % (reverse('latest_terms'), _("the terms"))
              self.fields['has_signed_terms'].label = \
 -                    mark_safe("I agree with %s" % terms_link_html)
 +                mark_safe("I agree with %s" % terms_link_html)
  
      def clean_email(self):
          email = self.cleaned_data['email']
          rrf = self.cleaned_data['recaptcha_response_field']
          check = captcha.submit(rcf, rrf, RECAPTCHA_PRIVATE_KEY, self.ip)
          if not check.is_valid:
 -            raise forms.ValidationError(_('You have not entered the correct words'))
 +            raise forms.ValidationError(
 +                _('You have not entered the correct words'))
  
      def save(self, commit=True):
          """
          user.renew_token()
          if commit:
              user.save()
 -            logger._log(LOGGING_LEVEL, 'Created user %s' % user.email, [])
 +            logger.log(LOGGING_LEVEL, 'Created user %s' % user.email)
          return user
  
 +
  class InvitedLocalUserCreationForm(LocalUserCreationForm):
      """
      Extends the LocalUserCreationForm: email is readonly.
          ro = ('email', 'username',)
          for f in ro:
              self.fields[f].widget.attrs['readonly'] = True
 -
 -
 +    
      def save(self, commit=True):
          user = super(InvitedLocalUserCreationForm, self).save(commit=False)
          level = user.invitation.inviter.level + 1
              user.save()
          return user
  
 +
  class ThirdPartyUserCreationForm(forms.ModelForm):
      class Meta:
          model = AstakosUser
 -        fields = ("email", "first_name", "last_name", "third_party_identifier", "has_signed_terms")
 -
 +        fields = ("email", "first_name", "last_name",
 +                  "third_party_identifier", "has_signed_terms")
 +    
      def __init__(self, *args, **kwargs):
          """
          Changes the order of fields, and removes the username field.
          if self.request:
              kwargs.pop('request')
          super(ThirdPartyUserCreationForm, self).__init__(*args, **kwargs)
 -        self.fields.keyOrder = ['email', 'first_name', 'last_name', 'third_party_identifier']
 +        self.fields.keyOrder = ['email', 'first_name', 'last_name',
 +                                'third_party_identifier']
          if get_latest_terms():
              self.fields.keyOrder.append('has_signed_terms')
          #set readonly form fields
              # Overriding field label since we need to apply a link
              # to the terms within the label
              terms_link_html = '<a href="%s" target="_blank">%s</a>' \
 -                    % (reverse('latest_terms'), _("the terms"))
 +                % (reverse('latest_terms'), _("the terms"))
              self.fields['has_signed_terms'].label = \
 -                    mark_safe("I agree with %s" % terms_link_html)
 -
 +                mark_safe("I agree with %s" % terms_link_html)
 +    
      def clean_email(self):
          email = self.cleaned_data['email']
          if not email:
          user.provider = get_query(self.request).get('provider')
          if commit:
              user.save()
 -            logger._log(LOGGING_LEVEL, 'Created user %s' % user.email, [])
 +            logger.log(LOGGING_LEVEL, 'Created user %s' % user.email)
          return user
  
 +
  class InvitedThirdPartyUserCreationForm(ThirdPartyUserCreationForm):
      """
      Extends the ThirdPartyUserCreationForm: email is readonly.
          """
          Changes the order of fields, and removes the username field.
          """
 -        super(InvitedThirdPartyUserCreationForm, self).__init__(*args, **kwargs)
 +        super(
 +            InvitedThirdPartyUserCreationForm, self).__init__(*args, **kwargs)
  
          #set readonly form fields
          ro = ('email',)
              self.fields[f].widget.attrs['readonly'] = True
  
      def save(self, commit=True):
 -        user = super(InvitedThirdPartyUserCreationForm, self).save(commit=False)
 +        user = super(
 +            InvitedThirdPartyUserCreationForm, self).save(commit=False)
          level = user.invitation.inviter.level + 1
          user.level = level
          user.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
              user.save()
          return user
  
 -class ShibbolethUserCreationForm(ThirdPartyUserCreationForm):
 -    additional_email = forms.CharField(widget=forms.HiddenInput(), label='', required = False)
  
 +class ShibbolethUserCreationForm(ThirdPartyUserCreationForm):
 +    additional_email = forms.CharField(
 +        widget=forms.HiddenInput(), label='', required=False)
 +    
      def __init__(self, *args, **kwargs):
          super(ShibbolethUserCreationForm, self).__init__(*args, **kwargs)
          self.fields.keyOrder.append('additional_email')
          # copy email value to additional_mail in case user will change it
          name = 'email'
          field = self.fields[name]
 -        self.initial['additional_email'] = self.initial.get(name, field.initial)
 -
 +        self.initial['additional_email'] = self.initial.get(name,
 +                                                            field.initial)
 +    
      def clean_email(self):
          email = self.cleaned_data['email']
 -        for user in AstakosUser.objects.filter(email = email):
 +        for user in AstakosUser.objects.filter(email=email):
              if user.provider == 'shibboleth':
                  raise forms.ValidationError(_("This email is already associated with another shibboleth account."))
              elif not user.is_active:
          super(ShibbolethUserCreationForm, self).clean_email()
          return email
  
 +
  class InvitedShibbolethUserCreationForm(ShibbolethUserCreationForm, InvitedThirdPartyUserCreationForm):
      pass
  
 +
  class LoginForm(AuthenticationForm):
      username = forms.EmailField(label=_("Email"))
      recaptcha_challenge_field = forms.CharField(widget=DummyWidget)
 -    recaptcha_response_field = forms.CharField(widget=RecaptchaWidget, label='')
 -
 +    recaptcha_response_field = forms.CharField(
 +        widget=RecaptchaWidget, label='')
 +    
      def __init__(self, *args, **kwargs):
          was_limited = kwargs.get('was_limited', False)
          request = kwargs.get('request', None)
          self.fields.keyOrder = ['username', 'password']
          if was_limited and RECAPTCHA_ENABLED:
              self.fields.keyOrder.extend(['recaptcha_challenge_field',
 -                                         'recaptcha_response_field',])
 -
 +                                         'recaptcha_response_field', ])
 +    
 +    def clean_username(self):
 +        if 'username' in self.cleaned_data:
 +            return self.cleaned_data['username'].lower()
 +    
      def clean_recaptcha_response_field(self):
          if 'recaptcha_challenge_field' in self.cleaned_data:
              self.validate_captcha()
          rrf = self.cleaned_data['recaptcha_response_field']
          check = captcha.submit(rcf, rrf, RECAPTCHA_PRIVATE_KEY, self.ip)
          if not check.is_valid:
 -            raise forms.ValidationError(_('You have not entered the correct words'))
 -
 +            raise forms.ValidationError(
 +                _('You have not entered the correct words'))
 +    
      def clean(self):
          super(LoginForm, self).clean()
          if self.user_cache and self.user_cache.provider not in ('local', ''):
              raise forms.ValidationError(_('Local login is not the current authentication method for this account.'))
          return self.cleaned_data
  
 +
  class ProfileForm(forms.ModelForm):
      """
      Subclass of ``ModelForm`` for permiting user to edit his/her profile.
  
      class Meta:
          model = AstakosUser
 -        fields = ('email', 'first_name', 'last_name', 'auth_token', 'auth_token_expires')
 +        fields = ('email', 'first_name', 'last_name', 'auth_token',
 +                  'auth_token_expires')
  
      def __init__(self, *args, **kwargs):
          super(ProfileForm, self).__init__(*args, **kwargs)
              user.save()
          return user
  
 +
  class FeedbackForm(forms.Form):
      """
      Form for writing feedback.
      feedback_data = forms.CharField(widget=forms.HiddenInput(), label='',
                                      required=False)
  
 +
  class SendInvitationForm(forms.Form):
      """
      Form for sending an invitations
      """
  
 -    email = forms.EmailField(required = True, label = 'Email address')
 -    first_name = forms.EmailField(label = 'First name')
 -    last_name = forms.EmailField(label = 'Last name')
 +    email = forms.EmailField(required=True, label='Email address')
 +    first_name = forms.EmailField(label='First name')
 +    last_name = forms.EmailField(label='Last name')
 +
  
  class ExtendedPasswordResetForm(PasswordResetForm):
      """
          try:
              user = AstakosUser.objects.get(email=email, is_active=True)
              if not user.has_usable_password():
 -                raise forms.ValidationError(_("This account has not a usable password."))
 -        except AstakosUser.DoesNotExist, e:
 +                raise forms.ValidationError(
 +                    _("This account has not a usable password."))
 +        except AstakosUser.DoesNotExist:
              raise forms.ValidationError(_('That e-mail address doesn\'t have an associated user account. Are you sure you\'ve registered?'))
          return email
  
 -    def save(self, domain_override=None, email_template_name='registration/password_reset_email.html',
 -             use_https=False, token_generator=default_token_generator, request=None):
 +    def save(
 +        self, domain_override=None, email_template_name='registration/password_reset_email.html',
 +            use_https=False, token_generator=default_token_generator, request=None):
          """
          Generates a one-use only link for resetting password and sends to the user.
          """
          for user in self.users_cache:
              url = reverse('django.contrib.auth.views.password_reset_confirm',
 -                          kwargs={'uidb36':int_to_base36(user.id),
 -                                  'token':token_generator.make_token(user)})
 +                          kwargs={'uidb36': int_to_base36(user.id),
 +                                  'token': token_generator.make_token(user)
 +                                  }
 +                          )
              url = urljoin(BASEURL, url)
              t = loader.get_template(email_template_name)
              c = {
                  'baseurl': BASEURL,
                  'support': DEFAULT_CONTACT_EMAIL
              }
 -            from_email = DEFAULT_FROM_EMAIL
 +            from_email = settings.SERVER_EMAIL
              send_mail(_(PASSWORD_RESET_EMAIL_SUBJECT),
 -                t.render(Context(c)), from_email, [user.email])
 +                      t.render(Context(c)), from_email, [user.email])
 +
  
  class EmailChangeForm(forms.ModelForm):
      class Meta:
      def save(self, email_template_name, request, commit=True):
          ec = super(EmailChangeForm, self).save(commit=False)
          ec.user = request.user
 -        activation_key = hashlib.sha1(str(random()) + smart_str(ec.new_email_address))
 -        ec.activation_key=activation_key.hexdigest()
 +        activation_key = hashlib.sha1(
 +            str(random()) + smart_str(ec.new_email_address))
 +        ec.activation_key = activation_key.hexdigest()
          if commit:
              ec.save()
          send_change_email(ec, request, email_template_name=email_template_name)
  
 +
  class SignApprovalTermsForm(forms.ModelForm):
      class Meta:
          model = AstakosUser
              raise forms.ValidationError(_('You have to agree with the terms'))
          return has_signed_terms
  
 +
  class InvitationForm(forms.ModelForm):
      username = forms.EmailField(label=_("Email"))
  
      def clean_username(self):
          username = self.cleaned_data['username']
          try:
 -            Invitation.objects.get(username = username)
 -            raise forms.ValidationError(_('There is already invitation for this email.'))
 +            Invitation.objects.get(username=username)
 +            raise forms.ValidationError(
 +                _('There is already invitation for this email.'))
          except Invitation.DoesNotExist:
              pass
          return username
  
 +
  class ExtendedPasswordChangeForm(PasswordChangeForm):
      """
      Extends PasswordChangeForm by enabling user
      to optionally renew also the token.
      """
-     renew = forms.BooleanField(label='Renew token', required=False)
+     if not NEWPASSWD_INVALIDATE_TOKEN:
+         renew = forms.BooleanField(label='Renew token', required=False,
+                                    initial=True,
+                                    help_text='Unsetting this may result in security risk.')
  
      def __init__(self, user, *args, **kwargs):
          super(ExtendedPasswordChangeForm, self).__init__(user, *args, **kwargs)
  
      def save(self, commit=True):
          user = super(ExtendedPasswordChangeForm, self).save(commit=False)
-         if self.cleaned_data.get('renew'):
+         if NEWPASSWD_INVALIDATE_TOKEN or self.cleaned_data.get('renew'):
              user.renew_token()
          if commit:
              user.save()
          return user
  
 +
 +class AstakosGroupCreationForm(forms.ModelForm):
 +    kind = forms.ModelChoiceField(
 +        queryset=GroupKind.objects.all(),
 +        label="",
 +        widget=forms.HiddenInput()
 +    )
 +    name = forms.URLField()
 +    moderation_enabled = forms.BooleanField(
 +        help_text="Check if you want to approve members participation manually",
 +        required=False   
 +    )
 +    
 +    class Meta:
 +        model = AstakosGroup
 +
 +    def __init__(self, *args, **kwargs):
 +        try:
 +            resources = kwargs.pop('resources')
 +        except KeyError:
 +            resources = {}
 +        super(AstakosGroupCreationForm, self).__init__(*args, **kwargs)
 +        self.fields.keyOrder = ['kind', 'name', 'homepage', 'desc', 'issue_date',
 +                                'expiration_date', 'estimated_participants',
 +                                'moderation_enabled']
 +        for id, r in resources.iteritems():
 +            self.fields['resource_%s' % id] = forms.IntegerField(
 +                label=r,
 +                required=False,
 +                help_text=_('Leave it blank for no additional quota.')
 +            )
 +
 +    def resources(self):
 +        for name, value in self.cleaned_data.items():
 +            prefix, delimiter, suffix = name.partition('resource_')
 +            if suffix:
 +                # yield only those having a value
 +                if not value:
 +                    continue
 +                yield (suffix, value)
 +
 +class AstakosGroupUpdateForm(forms.ModelForm):
 +    class Meta:
 +        model = AstakosGroup
 +        fields = ('homepage', 'desc')
 +
 +class AddGroupMembersForm(forms.Form):
 +    q = forms.CharField(max_length=800, widget=forms.Textarea, label=_('Search users'),
 +                        help_text=_('Add comma separated user emails'),
 +                        required=True)
 +    
 +    def clean(self):
 +        q = self.cleaned_data.get('q') or ''
 +        users = q.split(',')
 +        users = list(u.strip() for u in users if u)
 +        db_entries = AstakosUser.objects.filter(email__in=users)
 +        unknown = list(set(users) - set(u.email for u in db_entries))
 +        if unknown:
 +            raise forms.ValidationError(
 +                _('Unknown users: %s' % ','.join(unknown)))
 +        self.valid_users = db_entries
 +        return self.cleaned_data
 +    
 +    def get_valid_users(self):
 +        """Should be called after form cleaning"""
 +        try:
 +            return self.valid_users
 +        except:
 +            return ()
 +
 +
 +class AstakosGroupSearchForm(forms.Form):
 +    q = forms.CharField(max_length=200, label='Search group')
 +
 +class TimelineForm(forms.Form):
 +#    entity = forms.CharField(
 +#        widget=forms.HiddenInput(), label='')
 +    entity = forms.ModelChoiceField(
 +        queryset=AstakosUser.objects.filter(is_active = True)
 +    )
 +    resource = forms.ModelChoiceField(
 +        queryset=Resource.objects.all()
 +    )
 +    start_date = forms.DateTimeField()
 +    end_date = forms.DateTimeField()
 +    details = forms.BooleanField(required=False, label="Detailed Listing")
 +    operation = forms.ChoiceField(
 +                        label   = 'Charge Method',
 +                        choices = ( ('',                '-------------'),
 +                                    ('charge_usage',    'Charge Usage'),
 +                                    ('charge_traffic',  'Charge Traffic'), )
 +                )
 +    def clean(self):
 +        super(TimelineForm, self).clean()
 +        d = self.cleaned_data
 +        if 'resource' in d:
 +            d['resource'] = str(d['resource'])
 +        if 'start_date' in d:
 +            d['start_date'] = d['start_date'].strftime("%Y-%m-%dT%H:%M:%S.%f")[:24]
 +        if 'end_date' in d:
 +            d['end_date'] = d['end_date'].strftime("%Y-%m-%dT%H:%M:%S.%f")[:24]
 +      if 'entity' in d:
 +            d['entity'] = d['entity'].email 
 +        return d
 +
 +class AstakosGroupSortForm(forms.Form):
 +    sort_by = forms.ChoiceField(label='Sort by',
 +                                choices=(('groupname', 'Name'),
 +                                         ('kindname', 'Type'),
 +                                         ('issue_date', 'Issue Date'),
 +                                         ('expiration_date', 'Expiration Date'),
 +                                         ('approved_members_num', 'Participants'),
 +                                         ('is_enabled', 'Status'),
 +                                         ('moderation_enabled', 'Moderation'),
 +                                         ('membership_status','Enrollment Status')
 +                                         ),
 +                                required=False)
 +
 +class MembersSortForm(forms.Form):
 +    sort_by = forms.ChoiceField(label='Sort by',
 +                                choices=(('person__email', 'User Id'),
 +                                         ('person__first_name', 'Name'),
 +                                         ('date_joined', 'Status')
 +                                         ),
 +                                required=False)
 +
 +class PickResourceForm(forms.Form):
 +    resource = forms.ModelChoiceField(
 +        queryset=Resource.objects.select_related().all()
 +    )
-     resource.widget.attrs["onchange"]="this.form.submit()"
++    resource.widget.attrs["onchange"]="this.form.submit()"
++
+ class ExtendedSetPasswordForm(SetPasswordForm):
+     """
+     Extends SetPasswordForm by enabling user
+     to optionally renew also the token.
+     """
+     if not NEWPASSWD_INVALIDATE_TOKEN:
+         renew = forms.BooleanField(label='Renew token', required=False,
+                                    initial=True,
+                                    help_text='Unsetting this may result in security risk.')
+     
+     def __init__(self, user, *args, **kwargs):
+         super(ExtendedSetPasswordForm, self).__init__(user, *args, **kwargs)
+     
+     def save(self, commit=True):
+         user = super(ExtendedSetPasswordForm, self).save(commit=False)
+         if NEWPASSWD_INVALIDATE_TOKEN or self.cleaned_data.get('renew'):
+             try:
+                 user = AstakosUser.objects.get(id=user.id)
+             except AstakosUser.DoesNotExist:
+                 pass
+             else:
+                 user.renew_token()
+         if commit:
+             user.save()
+         return user
  # or implied, of GRNET S.A.
  
  from optparse import make_option
 +from datetime import datetime
  
  from django.core.management.base import BaseCommand, CommandError
 -from django.contrib.auth.models import Group, Permission
 -from django.contrib.contenttypes.models import ContentType
  from django.core.exceptions import ValidationError
 +from django.db.utils import IntegrityError
  
 -from astakos.im.models import AstakosUser
 +from astakos.im.models import (AstakosUser, AstakosGroup, Membership, Resource,
 +                               AstakosUserQuota)
 +from astakos.im.endpoints.aquarium.producer import report_user_credits_event
  from ._common import remove_user_permission, add_user_permission
  
 +
  class Command(BaseCommand):
      args = "<user ID>"
      help = "Modify a user's attributes"
 -    
 -    option_list = BaseCommand.option_list + (
 +
 +    option_list = list(BaseCommand.option_list) + [
          make_option('--invitations',
 -            dest='invitations',
 -            metavar='NUM',
 -            help="Update user's invitations"),
 +                    dest='invitations',
 +                    metavar='NUM',
 +                    help="Update user's invitations"),
          make_option('--level',
 -            dest='level',
 -            metavar='NUM',
 -            help="Update user's level"),
 +                    dest='level',
 +                    metavar='NUM',
 +                    help="Update user's level"),
          make_option('--password',
 -            dest='password',
 -            metavar='PASSWORD',
 -            help="Set user's password"),
 +                    dest='password',
 +                    metavar='PASSWORD',
 +                    help="Set user's password"),
          make_option('--provider',
 -            dest='provider',
 -            metavar='PROVIDER',
 -            help="Set user's provider"),
 +                    dest='provider',
 +                    metavar='PROVIDER',
 +                    help="Set user's provider"),
          make_option('--renew-token',
 -            action='store_true',
 -            dest='renew_token',
 -            default=False,
 -            help="Renew the user's token"),
 +                    action='store_true',
 +                    dest='renew_token',
 +                    default=False,
 +                    help="Renew the user's token"),
          make_option('--renew-password',
 -            action='store_true',
 -            dest='renew_password',
 -            default=False,
 -            help="Renew the user's password"),
 +                    action='store_true',
 +                    dest='renew_password',
 +                    default=False,
 +                    help="Renew the user's password"),
          make_option('--set-admin',
 -            action='store_true',
 -            dest='admin',
 -            default=False,
 -            help="Give user admin rights"),
 +                    action='store_true',
 +                    dest='admin',
 +                    default=False,
 +                    help="Give user admin rights"),
          make_option('--set-noadmin',
 -            action='store_true',
 -            dest='noadmin',
 -            default=False,
 -            help="Revoke user's admin rights"),
 +                    action='store_true',
 +                    dest='noadmin',
 +                    default=False,
 +                    help="Revoke user's admin rights"),
          make_option('--set-active',
 -            action='store_true',
 -            dest='active',
 -            default=False,
 -            help="Change user's state to inactive"),
 +                    action='store_true',
 +                    dest='active',
 +                    default=False,
 +                    help="Change user's state to inactive"),
          make_option('--set-inactive',
 -            action='store_true',
 -            dest='inactive',
 -            default=False,
 -            help="Change user's state to inactive"),
 +                    action='store_true',
 +                    dest='inactive',
 +                    default=False,
 +                    help="Change user's state to inactive"),
          make_option('--add-group',
 -            dest='add-group',
 -            help="Add user group"),
 +                    dest='add-group',
 +                    help="Add user group"),
          make_option('--delete-group',
 -            dest='delete-group',
 -            help="Delete user group"),
 +                    dest='delete-group',
 +                    help="Delete user group"),
          make_option('--add-permission',
 -            dest='add-permission',
 -            help="Add user permission"),
 +                    dest='add-permission',
 +                    help="Add user permission"),
          make_option('--delete-permission',
 -            dest='delete-permission',
 -            help="Delete user permission"),
 -        )
 +                    dest='delete-permission',
 +                    help="Delete user permission"),
 +        make_option('--refill-credits',
 +                    action='store_true',
 +                    dest='refill',
 +                    default=False,
 +                    help="Refill user credits"),
 +    ]
 +    resources = Resource.objects.select_related().all()
 +    append = option_list.append
 +    for r in resources:
 +        append(make_option('--%s-set-quota' % r,
 +                    dest='%s-set-quota' % r,
 +                    metavar='QUANTITY',
 +                    help="Set resource quota"))
      
      def handle(self, *args, **options):
          if len(args) != 1:
              raise CommandError("Please provide a user ID")
 -        
 +
          if args[0].isdigit():
 -            user = AstakosUser.objects.get(id=int( args[0]))
 +            user = AstakosUser.objects.get(id=int(args[0]))
          else:
              raise CommandError("Invalid ID")
 -        
 +
          if not user:
              raise CommandError("Unknown user")
 -        
 +
          if options.get('admin'):
              user.is_superuser = True
          elif options.get('noadmin'):
              user.is_superuser = False
 -        
 +
          if options.get('active'):
              user.is_active = True
          elif options.get('inactive'):
              user.is_active = False
 -        
 +
          invitations = options.get('invitations')
          if invitations is not None:
              user.invitations = int(invitations)
 -        
 +
          groupname = options.get('add-group')
          if groupname is not None:
              try:
 -                group = Group.objects.get(name=groupname)
 -                user.groups.add(group)
 -            except Group.DoesNotExist, e:
 -                self.stdout.write("Group named %s does not exist\n" % groupname)
 -        
 +                group = AstakosGroup.objects.get(name=groupname)
 +                m = Membership(
 +                    person=user, group=group, date_joined=datetime.now())
 +                m.save()
 +            except AstakosGroup.DoesNotExist, e:
 +                self.stdout.write(
 +                    "Group named %s does not exist\n" % groupname)
 +            except IntegrityError, e:
 +                self.stdout.write("User is already member of %s\n" % groupname)
 +
          groupname = options.get('delete-group')
          if groupname is not None:
              try:
 -                group = Group.objects.get(name=groupname)
 -                user.groups.remove(group)
 -            except Group.DoesNotExist, e:
 -                self.stdout.write("Group named %s does not exist\n" % groupname)
 -        
 +                group = AstakosGroup.objects.get(name=groupname)
 +                m = Membership.objects.get(person=user, group=group)
 +                m.delete()
 +            except AstakosGroup.DoesNotExist, e:
 +                self.stdout.write(
 +                    "Group named %s does not exist\n" % groupname)
 +            except Membership.DoesNotExist, e:
 +                self.stdout.write("User is not a member of %s\n" % groupname)
 +
          pname = options.get('add-permission')
          if pname is not None:
              try:
                  r, created = add_user_permission(user, pname)
                  if created:
 -                    self.stdout.write('Permission: %s created successfully\n' % pname)
 +                    self.stdout.write(
 +                        'Permission: %s created successfully\n' % pname)
                  if r > 0:
 -                    self.stdout.write('Permission: %s added successfully\n' % pname)
 -                elif r==0:
 -                    self.stdout.write('User has already permission: %s\n' % pname)
 +                    self.stdout.write(
 +                        'Permission: %s added successfully\n' % pname)
 +                elif r == 0:
 +                    self.stdout.write(
 +                        'User has already permission: %s\n' % pname)
              except Exception, e:
                  raise CommandError(e)
 -        
 -        pname  = options.get('delete-permission')
 +
 +        pname = options.get('delete-permission')
          if pname is not None and not user.has_perm(pname):
              try:
                  r = remove_user_permission(user, pname)
                  if r < 0:
 -                    self.stdout.write('Invalid permission codename: %s\n' % pname)
 +                    self.stdout.write(
 +                        'Invalid permission codename: %s\n' % pname)
                  elif r == 0:
                      self.stdout.write('User has not permission: %s\n' % pname)
                  elif r > 0:
 -                    self.stdout.write('Permission: %s removed successfully\n' % pname)
 +                    self.stdout.write(
 +                        'Permission: %s removed successfully\n' % pname)
              except Exception, e:
                  raise CommandError(e)
 -        
 +
          level = options.get('level')
          if level is not None:
              user.level = int(level)
 -        
 +
          password = options.get('password')
          if password is not None:
              user.set_password(password)
 -        
 +
          provider = options.get('provider')
          if provider is not None:
              user.provider = provider
 -        
 -        
 +
          password = None
          if options['renew_password']:
              password = AstakosUser.objects.make_random_password()
              user.set_password(password)
 -        
 +
          if options['renew_token']:
              user.renew_token()
 -        
 +
 +        if options['refill']:
 +            report_user_credits_event(user)
 +
          try:
              user.save()
          except ValidationError, e:
              raise CommandError(e)
 -        
 +
          if password:
              self.stdout.write('User\'s new password: %s\n' % password)
 +        
 +        for r in self.resources:
 +            limit = options.get('%s-set-quota' % r)
 +            if not limit:
 +                continue
 +            if not limit.isdigit():
 +                raise CommandError('Invalid limit')
 +            
 +            q = AstakosUserQuota.objects
 +            q, created = q.get_or_create(resource=r, user=user,
 +                                         defaults={'uplimit': limit})
 +            verb = 'set' if created else 'updated'
 +            self.stdout.write('User\'s quota %s successfully\n' % verb)
@@@ -11,16 -11,17 +11,16 @@@ TWITTER_SECRET = getattr(settings, 'AST
  DEFAULT_USER_LEVEL = getattr(settings, 'ASTAKOS_DEFAULT_USER_LEVEL', 4)
  
  INVITATIONS_PER_LEVEL = getattr(settings, 'ASTAKOS_INVITATIONS_PER_LEVEL', {
 -    0   :   100,
 -    1   :   2,
 -    2   :   0,
 -    3   :   0,
 -    4   :   0
 +    0: 100,
 +    1: 2,
 +    2: 0,
 +    3: 0,
 +    4: 0
  })
  
  # Address to use for outgoing emails
 -DEFAULT_FROM_EMAIL = getattr(settings, 'ASTAKOS_DEFAULT_FROM_EMAIL', 'GRNET Cloud <no-reply@grnet.gr>')
 -DEFAULT_CONTACT_EMAIL = getattr(settings, 'ASTAKOS_DEFAULT_CONTACT_EMAIL', 'support@cloud.grnet.gr')
 -DEFAULT_ADMIN_EMAIL = getattr(settings, 'ASTAKOS_DEFAULT_ADMIN_EMAIL', 'support@cloud.grnet.gr')
 +DEFAULT_CONTACT_EMAIL = getattr(
 +    settings, 'ASTAKOS_DEFAULT_CONTACT_EMAIL', 'support@cloud.grnet.gr')
  
  # Identity Management enabled modules
  IM_MODULES = getattr(settings, 'ASTAKOS_IM_MODULES', ['local', 'shibboleth'])
@@@ -50,7 -51,7 +50,7 @@@ SITENAME = getattr(settings, 'ASTAKOS_S
  RECAPTCHA_PUBLIC_KEY = getattr(settings, 'ASTAKOS_RECAPTCHA_PUBLIC_KEY', '')
  RECAPTCHA_PRIVATE_KEY = getattr(settings, 'ASTAKOS_RECAPTCHA_PRIVATE_KEY', '')
  RECAPTCHA_OPTIONS = getattr(settings, 'ASTAKOS_RECAPTCHA_OPTIONS',
 -                            {'theme' : 'custom', 'custom_theme_widget': 'okeanos_recaptcha'})
 +                            {'theme': 'custom', 'custom_theme_widget': 'okeanos_recaptcha'})
  RECAPTCHA_USE_SSL = getattr(settings, 'ASTAKOS_RECAPTCHA_USE_SSL', True)
  RECAPTCHA_ENABLED = getattr(settings, 'ASTAKOS_RECAPTCHA_ENABLED', True)
  
  BILLING_FIELDS = getattr(settings, 'ASTAKOS_BILLING_FIELDS', ['is_active'])
  
  # Queue for billing.
 -QUEUE_CONNECTION = getattr(settings, 'ASTAKOS_QUEUE_CONNECTION', None) # Example: 'rabbitmq://guest:guest@localhost:5672/astakos'
 +QUEUE_CONNECTION = getattr(settings, 'ASTAKOS_QUEUE_CONNECTION', None)  # Example: 'rabbitmq://guest:guest@localhost:5672/astakos'
  
  # Set where the user should be redirected after logout
  LOGOUT_NEXT = getattr(settings, 'ASTAKOS_LOGOUT_NEXT', '')
  
  # Set user email patterns that are automatically activated
 -RE_USER_EMAIL_PATTERNS = getattr(settings, 'ASTAKOS_RE_USER_EMAIL_PATTERNS', [])
 +RE_USER_EMAIL_PATTERNS = getattr(
 +    settings, 'ASTAKOS_RE_USER_EMAIL_PATTERNS', [])
  
  # Messages to display on login page header
  # e.g. {'warning': 'This warning message will be displayed on the top of login page'}
@@@ -87,16 -87,14 +87,16 @@@ GLOBAL_MESSAGES = getattr(settings, 'AS
  # e.g. {'https://cms.okeanos.grnet.gr/': 'Back to ~okeanos'}
  PROFILE_EXTRA_LINKS = getattr(settings, 'ASTAKOS_PROFILE_EXTRA_LINKS', {})
  
 -# The number of unsuccessful login requests per minute allowed for a specific email
 -RATELIMIT_RETRIES_ALLOWED = getattr(settings, 'ASTAKOS_RATELIMIT_RETRIES_ALLOWED', 3)
 +# The number of unsuccessful login requests per minute allowed for a specific user
 +RATELIMIT_RETRIES_ALLOWED = getattr(
 +    settings, 'ASTAKOS_RATELIMIT_RETRIES_ALLOWED', 3)
  
  # If False the email change mechanism is disabled
  EMAILCHANGE_ENABLED = getattr(settings, 'ASTAKOS_EMAILCHANGE_ENABLED', False)
  
  # Set the expiration time (in days) of email change requests
 -EMAILCHANGE_ACTIVATION_DAYS = getattr(settings, 'ASTAKOS_EMAILCHANGE_ACTIVATION_DAYS', 10)
 +EMAILCHANGE_ACTIVATION_DAYS = getattr(
 +    settings, 'ASTAKOS_EMAILCHANGE_ACTIVATION_DAYS', 10)
  
  # Set the astakos main functions logging severity (None to disable)
  from logging import INFO
@@@ -120,36 -118,5 +120,21 @@@ EMAIL_CHANGE_EMAIL_SUBJECT = getattr(se
  PASSWORD_RESET_EMAIL_SUBJECT = getattr(settings, 'ASTAKOS_PASSWORD_RESET_EMAIL_SUBJECT',
          'Password reset on %s alpha2 testing' % SITENAME)
  
- # Configurable email subjects
- INVITATION_EMAIL_SUBJECT = getattr(settings, 'ASTAKOS_INVITATION_EMAIL_SUBJECT',
-         'Invitation to %s alpha2 testing' % SITENAME)
- GREETING_EMAIL_SUBJECT = getattr(settings, 'ASTAKOS_GREETING_EMAIL_SUBJECT',
-         'Welcome to %s alpha2 testing' % SITENAME)
- FEEDBACK_EMAIL_SUBJECT = getattr(settings, 'ASTAKOS_FEEDBACK_EMAIL_SUBJECT',
-         'Feedback from %s alpha2 testing' % SITENAME)
- VERIFICATION_EMAIL_SUBJECT = getattr(settings, 'ASTAKOS_VERIFICATION_EMAIL_SUBJECT',
-         '%s alpha2 testing account activation is needed' % SITENAME)
- ADMIN_NOTIFICATION_EMAIL_SUBJECT = getattr(settings, 'ASTAKOS_ADMIN_NOTIFICATION_EMAIL_SUBJECT',
-         '%s alpha2 testing account created (%%(user)s)' % SITENAME)
- HELPDESK_NOTIFICATION_EMAIL_SUBJECT = getattr(settings, 'ASTAKOS_HELPDESK_NOTIFICATION_EMAIL_SUBJECT',
-         '%s alpha2 testing account activated (%%(user)s)' % SITENAME)
- EMAIL_CHANGE_EMAIL_SUBJECT = getattr(settings, 'ASTAKOS_EMAIL_CHANGE_EMAIL_SUBJECT',
-         'Email change on %s alpha2 testing' % SITENAME)
- PASSWORD_RESET_EMAIL_SUBJECT = getattr(settings, 'ASTAKOS_PASSWORD_RESET_EMAIL_SUBJECT',
-         'Password reset on %s alpha2 testing' % SITENAME)
 +# Set the quota holder component URI
 +QUOTA_HOLDER_URL = getattr(settings, 'ASTAKOS_QUOTA_HOLDER_URL', '')
 +
 +# Set the cloud service properties
 +SERVICES = getattr(settings, 'ASTAKOS_SERVICES',
 +                   {'cyclades': {'url':'https://node1.example.com/ui/',
 +                                 'quota': {'vm': 2}},
 +                    'pithos+':  {'url':'https://node2.example.com/ui/',
 +                                 'quota': {'diskspace': 50 * 1024 * 1024 * 1024}}})
 +
 +# Set the billing URI
 +AQUARIUM_URL = getattr(settings, 'ASTAKOS_AQUARIUM_URL', '')
 +
 +# Set how many objects should be displayed per page
- PAGINATE_BY = getattr(settings, 'ASTAKOS_PAGINATE_BY', 8)
++PAGINATE_BY = getattr(settings, 'ASTAKOS_PAGINATE_BY', 8)
++
+ # Enforce token renewal on password change/reset
+ NEWPASSWD_INVALIDATE_TOKEN = getattr(settings, 'ASTAKOS_NEWPASSWD_INVALIDATE_TOKEN', True)
  # interpreted as representing official policies, either expressed
  # or implied, of GRNET S.A.
  
 -from django.http import HttpResponseBadRequest
  from django.shortcuts import render_to_response
  from django.template import RequestContext
  from django.contrib import messages
  from django.utils.translation import ugettext as _
  from django.views.decorators.csrf import csrf_exempt
+ from django.views.decorators.http import require_http_methods
  
  from astakos.im.util import prepare_response, get_query
  from astakos.im.views import requires_anonymous
 -from astakos.im.models import AstakosUser
  from astakos.im.forms import LoginForm
  from astakos.im.settings import RATELIMIT_RETRIES_ALLOWED
  
  from ratelimit.decorators import ratelimit
  
 -retries = RATELIMIT_RETRIES_ALLOWED-1
 -rate = str(retries)+'/m'
 +retries = RATELIMIT_RETRIES_ALLOWED - 1
 +rate = str(retries) + '/m'
 +
  
+ @require_http_methods(["GET", "POST"])
  @csrf_exempt
  @requires_anonymous
  @ratelimit(field='username', method='POST', rate=rate)
@@@ -56,27 -60,25 +58,27 @@@ def login(request, on_failure='im/login
      on_failure: the template name to render on login failure
      """
      was_limited = getattr(request, 'limited', False)
 -    form = LoginForm(data=request.POST, was_limited=was_limited, request=request)
 +    form = LoginForm(data=request.POST,
 +                     was_limited=was_limited,
 +                     request=request)
      next = get_query(request).get('next', '')
      if not form.is_valid():
          return render_to_response(on_failure,
 -                                  {'login_form':form,
 -                                   'next':next},
 +                                  {'login_form': form,
 +                                   'next': next},
                                    context_instance=RequestContext(request))
      # get the user from the cash
      user = form.user_cache
 -    
 +
      message = None
      if not user:
          message = _('Cannot authenticate account')
      elif not user.is_active:
          message = _('Inactive account')
      if message:
 -        messages.add_message(request, messages.ERROR, message)
 +        messages.error(request, message)
          return render_to_response(on_failure,
 -                                  {'form':form},
 +                                  {'form': form},
                                    context_instance=RequestContext(request))
 -    
 +
      return prepare_response(request, user, next)
  # or implied, of GRNET S.A.
  
  from django.core.urlresolvers import reverse
 -from django.shortcuts import redirect
  from django.utils.translation import ugettext as _
 -from django.contrib import messages
  from django.utils.http import urlencode
  from django.contrib.auth import authenticate
  from django.http import HttpResponse, HttpResponseBadRequest
  from django.core.exceptions import ValidationError
+ from django.views.decorators.http import require_http_methods
  
 -from urllib import quote
 -from urlparse import urlunsplit, urlsplit, urlparse, parse_qsl
 +from urlparse import urlunsplit, urlsplit, parse_qsl
  
  from astakos.im.settings import COOKIE_NAME, COOKIE_DOMAIN
  from astakos.im.util import set_cookie
@@@ -48,7 -52,7 +49,8 @@@ import loggin
  
  logger = logging.getLogger(__name__)
  
 +
+ @require_http_methods(["GET", "POST"])
  def login(request):
      """
      If there is no ``next`` request parameter redirects to astakos index page
      if request.user.is_authenticated():
          # if user has not signed the approval terms
          # redirect to approval terms with next the request path
 -        if not request.user.signed_terms():
 +        if not request.user.signed_terms:
              # first build next parameter
              parts = list(urlsplit(request.build_absolute_uri()))
              params = dict(parse_qsl(parts[3], keep_blank_values=True))
              # delete force parameter
              parts[3] = urlencode(params)
              next = urlunsplit(parts)
 -            
 +
              # build url location
              parts[2] = reverse('latest_terms')
 -            params = {'next':next}
 +            params = {'next': next}
              parts[3] = urlencode(params)
              url = urlunsplit(parts)
              response['Location'] = url
              except ValidationError, e:
                  return HttpResponseBadRequest(e)
              # authenticate before login
 -            user = authenticate(email=request.user.email, auth_token=request.user.auth_token)
 +            user = authenticate(email=request.user.email,
 +                                auth_token=request.user.auth_token
 +                                )
              auth_login(request, user)
              set_cookie(response, user)
              logger.info('Token reset for %s' % request.user.email)
          parts = list(urlsplit(next))
 -        parts[3] = urlencode({'user': request.user.email, 'token': request.user.auth_token})
 +        parts[3] = urlencode({'user': request.user.email,
 +                              'token': request.user.auth_token
 +                              }
 +                             )
          url = urlunsplit(parts)
          response['Location'] = url
          response.status_code = 302
          return response
      else:
          # redirect to login with next the request path
 -        
 +
          # first build next parameter
          parts = list(urlsplit(request.build_absolute_uri()))
          params = dict(parse_qsl(parts[3], keep_blank_values=True))
              del params['force']
          parts[3] = urlencode(params)
          next = urlunsplit(parts)
 -        
 +
          # build url location
 -        parts[2] = reverse('astakos.im.views.index')
 -        params = {'next':next}
 +        parts[2] = reverse('index')
 +        params = {'next': next}
          parts[3] = urlencode(params)
          url = urlunsplit(parts)
          response['Location'] = url
          response.status_code = 302
 -        return response
 +        return response
@@@ -1,18 -1,18 +1,18 @@@
  # Copyright 2011-2012 GRNET S.A. All rights reserved.
 -# 
 +#
  # Redistribution and use in source and binary forms, with or
  # without modification, are permitted provided that the following
  # conditions are met:
 -# 
 +#
  #   1. Redistributions of source code must retain the above
  #      copyright notice, this list of conditions and the following
  #      disclaimer.
 -# 
 +#
  #   2. Redistributions in binary form must reproduce the above
  #      copyright notice, this list of conditions and the following
  #      disclaimer in the documentation and/or other materials
  #      provided with the distribution.
 -# 
 +#
  # THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
  # OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
@@@ -25,7 -25,7 +25,7 @@@
  # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
  # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  # POSSIBILITY OF SUCH DAMAGE.
 -# 
 +#
  # The views and conclusions contained in the software and
  # documentation are those of the authors and should not be
  # interpreted as representing official policies, either expressed
@@@ -35,17 -35,18 +35,18 @@@ from django.http import HttpResponseBad
  from django.utils.translation import ugettext as _
  from django.contrib import messages
  from django.template import RequestContext
+ from django.views.decorators.http import require_http_methods
  
 -from astakos.im.util import prepare_response, get_context, get_invitation
 +from astakos.im.util import prepare_response, get_context
  from astakos.im.views import requires_anonymous, render_response
 -from astakos.im.settings import DEFAULT_USER_LEVEL
 -from astakos.im.models import AstakosUser, Invitation, AdditionalMail
 +from astakos.im.models import AstakosUser
  from astakos.im.forms import LoginForm
  from astakos.im.activation_backends import get_backend, SimpleBackend
  
 +
  class Tokens:
      # these are mapped by the Shibboleth SP software
 -    SHIB_EPPN = "HTTP_EPPN" # eduPersonPrincipalName
 +    SHIB_EPPN = "HTTP_EPPN"  # eduPersonPrincipalName
      SHIB_NAME = "HTTP_SHIB_INETORGPERSON_GIVENNAME"
      SHIB_SURNAME = "HTTP_SHIB_PERSON_SURNAME"
      SHIB_CN = "HTTP_SHIB_PERSON_COMMONNAME"
      SHIB_SESSION_ID = "HTTP_SHIB_SESSION_ID"
      SHIB_MAIL = "HTTP_SHIB_MAIL"
  
 +
+ @require_http_methods(["GET", "POST"])
  @requires_anonymous
 -def login(request,  backend=None, on_login_template='im/login.html', on_creation_template='im/third_party_registration.html', extra_context={}):
 +def login(request, backend=None, on_login_template='im/login.html',
 +          on_creation_template='im/third_party_registration.html',
 +          extra_context=None
 +          ):
      tokens = request.META
 -    
 +
      try:
          eppn = tokens[Tokens.SHIB_EPPN]
      except KeyError:
          return HttpResponseBadRequest("Missing unique token in request")
 -    
 +
      if Tokens.SHIB_DISPLAYNAME in tokens:
          realname = tokens[Tokens.SHIB_DISPLAYNAME]
      elif Tokens.SHIB_CN in tokens:
          realname = tokens[Tokens.SHIB_NAME] + ' ' + tokens[Tokens.SHIB_SURNAME]
      else:
          return HttpResponseBadRequest("Missing user name in request")
 -    
 +
      affiliation = tokens.get(Tokens.SHIB_EP_AFFILIATION, '')
      email = tokens.get(Tokens.SHIB_MAIL, None)
 -    
 +
      try:
 -        user = AstakosUser.objects.get(provider='shibboleth', third_party_identifier=eppn)
 +        user = AstakosUser.objects.get(provider='shibboleth',
-                                        third_party_identifier=eppn
-                                        )
++                                       third_party_identifier=eppn)
          if user.is_active:
              return prepare_response(request,
                                      user,
@@@ -90,9 -86,9 +91,9 @@@
                                      'renew' in request.GET)
          else:
              message = _('Inactive account')
 -            messages.add_message(request, messages.ERROR, message)
 +            messages.error(request, message)
              return render_response(on_login_template,
 -                                   login_form = LoginForm(request=request),
 +                                   login_form=LoginForm(request=request),
                                     context_instance=RequestContext(request))
      except AstakosUser.DoesNotExist, e:
          user = AstakosUser(third_party_identifier=eppn, realname=realname,
          try:
              if not backend:
                  backend = get_backend(request)
 -            form = backend.get_signup_form(provider='shibboleth', instance=user)
 +            form = backend.get_signup_form(
 +                provider='shibboleth', instance=user)
          except Exception, e:
 -            form = SimpleBackend(request).get_signup_form(provider='shibboleth', instance=user)
 -            messages.add_message(request, messages.ERROR, e)
 +            form = SimpleBackend(request).get_signup_form(
 +                provider='shibboleth',
-                 instance=user
-             )
++                instance=user)
 +            messages.error(request, e)
          return render_response(on_creation_template,
 -                               signup_form = form,
 -                               provider = 'shibboleth',
 -                               context_instance=get_context(request, extra_context))
 +                               signup_form=form,
 +                               provider='shibboleth',
-                                context_instance=get_context(
-                                request,
-                                extra_context
-                                )
-                                )
++                               context_instance=get_context(request,
++                                                            extra_context))
  # interpreted as representing official policies, either expressed
  # or implied, of GRNET S.A.
  
 -from django.conf.urls.defaults import patterns, include, url
 -from django.contrib.auth.views import password_change
 +from django.conf.urls.defaults import patterns, url
  
- from astakos.im.forms import ExtendedPasswordResetForm, ExtendedPasswordChangeForm, LoginForm
+ from astakos.im.forms import (ExtendedPasswordResetForm,
+                               ExtendedPasswordChangeForm,
+                               ExtendedSetPasswordForm, LoginForm)
  from astakos.im.settings import IM_MODULES, INVITATIONS_ENABLED, EMAILCHANGE_ENABLED
  
  urlpatterns = patterns('astakos.im.views',
 -    url(r'^$', 'index', {}, name='index'),
 -    url(r'^login/?$', 'index', {}, name='login'),
 -    url(r'^profile/?$', 'edit_profile'),
 -    url(r'^feedback/?$', 'feedback'),
 -    url(r'^signup/?$', 'signup', {'on_success':'im/login.html', 'extra_context':{'login_form':LoginForm()}}),
 -    url(r'^logout/?$', 'logout', {'template':'im/login.html', 'extra_context':{'login_form':LoginForm()}}),
 -    url(r'^activate/?$', 'activate'),
 -    url(r'^approval_terms/?$', 'approval_terms', {}, name='latest_terms'),
 -    url(r'^approval_terms/(?P<term_id>\d+)/?$', 'approval_terms'),
 -    url(r'^password/?$', 'change_password', {}, name='password_change'),
 -)
 +                       url(r'^$', 'index', {}, name='index'),
 +                       url(r'^login/?$', 'index', {}, name='login'),
 +                       url(r'^profile/?$',
 +                           'edit_profile', {}, name='edit_profile'),
 +                       url(r'^feedback/?$', 'feedback', {}, name='feedback'),
 +                       url(r'^signup/?$', 'signup',
 +                           {'on_success': 'im/login.html',
 +                            'extra_context': {'login_form': LoginForm()}},
 +                           name='signup'),
 +                       url(r'^logout/?$', 'logout',
 +                           {'template': 'im/login.html',
 +                            'extra_context': {'login_form': LoginForm()}},
 +                           name='logout'),
 +                       url(r'^activate/?$', 'activate', {}, name='activate'),
 +                       url(r'^approval_terms/?$',
 +                           'approval_terms', {}, name='latest_terms'),
 +                       url(r'^approval_terms/(?P<term_id>\d+)/?$',
 +                           'approval_terms'),
 +                       url(r'^password/?$',
 +                           'change_password', {}, name='password_change'),
 +                       url(r'^resources/?$',
 +                           'resource_list', {}, name='resource_list'),
 +                       url(r'^billing/?$', 'billing', {}, name='billing'),
 +                       url(r'^timeline/?$', 'timeline', {}, name='timeline'),
 +                       url(r'^group/add/(?P<kind_name>\w+)?$',
 +                           'group_add', {}, name='group_add'),
 +                       url(r'^group/list/?$',
 +                           'group_list', {}, name='group_list'),
 +                       url(r'^group/(?P<group_id>\d+)/?$', 'group_detail',
 +                           {}, name='group_detail'),
 +                       url(r'^group/search/?$',
 +                           'group_search', {}, name='group_search'),
 +                       url(r'^group/all/?$',
 +                           'group_all', {}, name='group_all'),
 +                       url(r'^group/(?P<group_id>\d+)/join/?$', 'group_join',
 +                           {}, name='group_join'),
 +                       url(r'^group/(?P<group_id>\d+)/leave/?$', 'group_leave',
 +                           {}, name='group_leave'),
 +                       url(r'^group/(?P<group_id>\d+)/(?P<user_id>\d+)/approve/?$',
 +                           'approve_member', {}, name='approve_member'),
 +                       url(r'^group/(?P<group_id>\d+)/(?P<user_id>\d+)/disapprove/?$',
 +                           'disapprove_member', {}, name='disapprove_member'),
 +                       url(r'^group/create/?$', 'group_create_list', {},
 +                           name='group_create_list'),
 +                       )
  
  if EMAILCHANGE_ENABLED:
      urlpatterns += patterns('astakos.im.views',
 -        url(r'^email_change/?$', 'change_email', {}, name='email_change'),
 -        url(r'^email_change/confirm/(?P<activation_key>\w+)/', 'change_email', {},
 -            name='email_change_confirm')
 -)
 -    
 +                            url(r'^email_change/?$',
 +                                'change_email', {}, name='email_change'),
 +                            url(
 +                            r'^email_change/confirm/(?P<activation_key>\w+)/', 'change_email', {},
 +                            name='email_change_confirm')
 +                            )
 +
  urlpatterns += patterns('astakos.im.target',
 -    url(r'^login/redirect/?$', 'redirect.login')
 -)
 +                        url(r'^login/redirect/?$', 'redirect.login')
 +                        )
  
  if 'local' in IM_MODULES:
      urlpatterns += patterns('astakos.im.target',
 -        url(r'^local/?$', 'local.login')
 -    )
 +                            url(r'^local/?$', 'local.login')
 +                            )
      urlpatterns += patterns('django.contrib.auth.views',
-                             url(r'^local/password_reset/?$', 'password_reset',
-                                 {'email_template_name': 'registration/password_email.txt',
-                                  'password_reset_form': ExtendedPasswordResetForm}),
-                             url(r'^local/password_reset_done/?$',
-                                 'password_reset_done'),
-                             url(
-                                 r'^local/reset/confirm/(?P<uidb36>[0-9A-Za-z]+)-(?P<token>.+)/?$',
-                             'password_reset_confirm'),
-                             url(r'^local/password/reset/complete/?$',
-                                 'password_reset_complete'),
-                             url(
-                             r'^password_change/?$', 'password_change', {'post_change_redirect': 'profile',
-                                                                         'password_change_form': ExtendedPasswordChangeForm})
-                             )
+         url(r'^local/password_reset/?$', 'password_reset',
+          {'email_template_name':'registration/password_email.txt',
+           'password_reset_form':ExtendedPasswordResetForm}),
+         url(r'^local/password_reset_done/?$', 'password_reset_done'),
+         url(r'^local/reset/confirm/(?P<uidb36>[0-9A-Za-z]+)-(?P<token>.+)/?$',
+          'password_reset_confirm', {'set_password_form':ExtendedSetPasswordForm}),
+         url(r'^local/password/reset/complete/?$', 'password_reset_complete'),
+         url(r'^password_change/?$', 'password_change', {'post_change_redirect':'profile',
+             'password_change_form':ExtendedPasswordChangeForm})
+     )
  
  if INVITATIONS_ENABLED:
      urlpatterns += patterns('astakos.im.views',
 -        url(r'^invite/?$', 'invite')
 -    )
 +                            url(r'^invite/?$', 'invite', {}, name='invite')
 +                            )
  
  if 'shibboleth' in IM_MODULES:
      urlpatterns += patterns('astakos.im.target',
 -        url(r'^login/shibboleth/?$', 'shibboleth.login')
 -    )
 +                            url(r'^login/shibboleth/?$', 'shibboleth.login')
 +                            )
  
  if 'twitter' in IM_MODULES:
      urlpatterns += patterns('astakos.im.target',
 -        url(r'^login/twitter/?$', 'twitter.login'),
 -        url(r'^login/twitter/authenticated/?$', 'twitter.authenticated')
 -    )
 +                            url(r'^login/twitter/?$', 'twitter.login'),
 +                            url(r'^login/twitter/authenticated/?$',
 +                                'twitter.authenticated')
 +                            )
 +
 +urlpatterns += patterns('astakos.im.api',
 +                        url(r'^get_services/?$', 'get_services'),
 +                        url(r'^get_menu/?$', 'get_menu'),
 +                        )
  
  urlpatterns += patterns('astakos.im.api.admin',
 -    url(r'^authenticate/?$', 'authenticate_old'),
 -    #url(r'^authenticate/v2/?$', 'authenticate'),
 -    url(r'^get_services/?$', 'get_services'),
 -    url(r'^get_menu/?$', 'get_menu'),
 -    url(r'^admin/api/v2.0/users/?$', 'get_user_by_email'),
 -    url(r'^admin/api/v2.0/users/(?P<user_id>.+?)/?$', 'get_user_by_username'),
 -)
 +                        url(r'^authenticate/?$', 'authenticate_old'),
 +                        #url(r'^authenticate/v2/?$', 'authenticate'),
 +                        url(r'^admin/api/v2.0/users/?$', 'get_user_by_email'),
 +                        url(r'^admin/api/v2.0/users/(?P<user_id>.+?)/?$',
 +                            'get_user_by_username'),
 +                        )
  
  urlpatterns += patterns('astakos.im.api.service',
 -    #url(r'^service/api/v2.0/tokens/(?P<token_id>.+?)/?$', 'validate_token'),
 -    url(r'^service/api/v2.0/feedback/?$', 'send_feedback'),
 -    url(r'^service/api/v2.0/users/?$', 'get_user_by_email'),
 -    url(r'^service/api/v2.0/users/(?P<user_id>.+?)/?$', 'get_user_by_username'),
 -)
 +                        #url(r'^service/api/v2.0/tokens/(?P<token_id>.+?)/?$', 'validate_token'),
 +                        url(r'^service/api/v2.0/feedback/?$', 'send_feedback'),
 +                        url(r'^service/api/v2.0/users/?$',
 +                            'get_user_by_email'),
 +                        url(r'^service/api/v2.0/users/(?P<user_id>.+?)/?$',
 +                            'get_user_by_username'),
 +                        )
@@@ -1,18 -1,18 +1,18 @@@
  # Copyright 2011-2012 GRNET S.A. All rights reserved.
 -# 
 +#
  # Redistribution and use in source and binary forms, with or
  # without modification, are permitted provided that the following
  # conditions are met:
 -# 
 +#
  #   1. Redistributions of source code must retain the above
  #      copyright notice, this list of conditions and the following
  #      disclaimer.
 -# 
 +#
  #   2. Redistributions in binary form must reproduce the above
  #      copyright notice, this list of conditions and the following
  #      disclaimer in the documentation and/or other materials
  #      provided with the distribution.
 -# 
 +#
  # THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
  # OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
@@@ -25,7 -25,7 +25,7 @@@
  # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
  # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  # POSSIBILITY OF SUCH DAMAGE.
 -# 
 +#
  # The views and conclusions contained in the software and
  # documentation are those of the authors and should not be
  # interpreted as representing official policies, either expressed
@@@ -36,6 -36,7 +36,6 @@@ import datetim
  import time
  
  from urllib import quote
 -from urlparse import urlsplit, urlunsplit
  
  from datetime import tzinfo, timedelta
  from django.http import HttpResponse, HttpResponseBadRequest, urlencode
@@@ -45,45 -46,41 +45,45 @@@ from django.contrib.auth import authent
  from django.core.urlresolvers import reverse
  from django.core.exceptions import ValidationError
  
 -from astakos.im.models import AstakosUser, Invitation, ApprovalTerms
 -from astakos.im.settings import INVITATIONS_PER_LEVEL, COOKIE_NAME, \
 +from astakos.im.models import AstakosUser, Invitation
 +from astakos.im.settings import COOKIE_NAME, \
      COOKIE_DOMAIN, COOKIE_SECURE, FORCE_PROFILE_UPDATE, LOGGING_LEVEL
  from astakos.im.functions import login
  
  logger = logging.getLogger(__name__)
  
 +
  class UTC(tzinfo):
 -   def utcoffset(self, dt):
 -       return timedelta(0)
 +    def utcoffset(self, dt):
 +        return timedelta(0)
 +
 +    def tzname(self, dt):
 +        return 'UTC'
  
 -   def tzname(self, dt):
 -       return 'UTC'
 +    def dst(self, dt):
 +        return timedelta(0)
  
 -   def dst(self, dt):
 -       return timedelta(0)
  
  def isoformat(d):
 -   """Return an ISO8601 date string that includes a timezone."""
 +    """Return an ISO8601 date string that includes a timezone."""
 +
 +    return d.replace(tzinfo=UTC()).isoformat()
  
 -   return d.replace(tzinfo=UTC()).isoformat()
  
  def epoch(datetime):
 -    return int(time.mktime(datetime.timetuple())*1000)
 +    return int(time.mktime(datetime.timetuple()) * 1000)
  
 -def get_context(request, extra_context={}, **kwargs):
 -    if not extra_context:
 -        extra_context = {}
 +
 +def get_context(request, extra_context=None, **kwargs):
 +    extra_context = extra_context or {}
      extra_context.update(kwargs)
      return RequestContext(request, extra_context)
  
 +
  def get_invitation(request):
      """
      Returns the invitation identified by the ``code``.
 -    
 +
      Raises ValueError if the invitation is consumed or there is another account
      associated with this email.
      """
          code = request.POST.get('code')
      if not code:
          return
 -    invitation = Invitation.objects.get(code = code)
 +    invitation = Invitation.objects.get(code=code)
      if invitation.is_consumed:
          raise ValueError(_('Invitation is used'))
      if reserved_email(invitation.username):
          raise ValueError(_('Email: %s is reserved' % invitation.username))
      return invitation
  
 +
  def prepare_response(request, user, next='', renew=False):
      """Return the unique username and the token
         as 'X-Auth-User' and 'X-Auth-Token' headers,
         or redirect to the URL provided in 'next'
         with the 'user' and 'token' as parameters.
 -       
 +
         Reissue the token even if it has not yet
         expired, if the 'renew' parameter is present
         or user has not a valid token.
      """
      renew = renew or (not user.auth_token)
 -    renew = renew or (user.auth_token_expires and user.auth_token_expires < datetime.datetime.now())
 +    renew = renew or (user.auth_token_expires < datetime.datetime.now())
      if renew:
          user.renew_token()
          try:
              user.save()
          except ValidationError, e:
 -            return HttpResponseBadRequest(e) 
 -    
 +            return HttpResponseBadRequest(e)
 +
      if FORCE_PROFILE_UPDATE and not user.is_verified and not user.is_superuser:
          params = ''
          if next:
              params = '?' + urlencode({'next': next})
 -        next = reverse('astakos.im.views.edit_profile') + params
 -    
 +        next = reverse('edit_profile') + params
 +
      response = HttpResponse()
 -    
 +
      # authenticate before login
      user = authenticate(email=user.email, auth_token=user.auth_token)
      login(request, user)
      set_cookie(response, user)
      request.session.set_expiry(user.auth_token_expires)
 -    
 +
      if not next:
 -        next = reverse('astakos.im.views.index')
 -    
 +        next = reverse('index')
 +
      response['Location'] = next
      response.status_code = 302
      return response
  
 +
  def set_cookie(response, user):
      expire_fmt = user.auth_token_expires.strftime('%a, %d-%b-%Y %H:%M:%S %Z')
      cookie_value = quote(user.email + '|' + user.auth_token)
      response.set_cookie(COOKIE_NAME, value=cookie_value,
                          expires=expire_fmt, path='/',
 -                        domain=COOKIE_DOMAIN, secure=COOKIE_SECURE)
 -    msg = 'Cookie [expiring %s] set for %s' % (user.auth_token_expires, user.email)
 -    logger._log(LOGGING_LEVEL, msg, [])
 +                        domain=COOKIE_DOMAIN, secure=COOKIE_SECURE
 +                        )
 +    msg = 'Cookie [expiring %s] set for %s' % (
 +        user.auth_token_expires,
 +        user.email
 +    )
 +    logger.log(LOGGING_LEVEL, msg)
 +
  
  class lazy_string(object):
      def __init__(self, function, *args, **kwargs):
 -        self.function=function
 -        self.args=args
 -        self.kwargs=kwargs
 -        
 +        self.function = function
 +        self.args = args
 +        self.kwargs = kwargs
 +
      def __str__(self):
          if not hasattr(self, 'str'):
 -            self.str=self.function(*self.args, **self.kwargs)
 +            self.str = self.function(*self.args, **self.kwargs)
          return self.str
  
 +
  def reverse_lazy(*args, **kwargs):
      return lazy_string(reverse, *args, **kwargs)
  
 +
  def reserved_email(email):
 -    return AstakosUser.objects.filter(email = email).count() != 0
 +    return AstakosUser.objects.filter(email__iexact=email).count() != 0
 +
  
  def get_query(request):
      try:
          return request.__getattribute__(request.method)
      except AttributeError:
-         return request.GET
+         return {}
  # or implied, of GRNET S.A.
  
  import logging
 -import socket
 +import calendar
  
 -from smtplib import SMTPException
  from urllib import quote
  from functools import wraps
 +from datetime import datetime, timedelta
 +from collections import defaultdict
  
 -from django.core.mail import send_mail
 -from django.http import HttpResponse, HttpResponseBadRequest
 -from django.shortcuts import redirect
 -from django.template.loader import render_to_string
 -from django.utils.translation import ugettext as _
 -from django.core.urlresolvers import reverse
 -from django.contrib.auth.decorators import login_required
  from django.contrib import messages
 -from django.db import transaction
 -from django.utils.http import urlencode
 -from django.http import HttpResponseRedirect, HttpResponseBadRequest
 -from django.db.utils import IntegrityError
 +from django.contrib.auth.decorators import login_required
  from django.contrib.auth.views import password_change
 -from django.core.exceptions import ValidationError
 +from django.core.urlresolvers import reverse
 +from django.db import transaction
  from django.db.models import Q
++<<<<<<< HEAD
 +from django.db.utils import IntegrityError
 +from django.forms.fields import URLField
 +from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, \
 +    HttpResponseRedirect, HttpResponseBadRequest, Http404
 +from django.shortcuts import redirect
 +from django.template import RequestContext, loader as template_loader
 +from django.utils.http import urlencode
 +from django.utils.translation import ugettext as _
 +from django.views.generic.create_update import (create_object, delete_object,
 +                                                get_model_and_form_class)
 +from django.views.generic.list_detail import object_list, object_detail
 +from django.http import HttpResponseBadRequest
 +from django.core.xheaders import populate_xheaders
 +
 +from astakos.im.models import (
 +    AstakosUser, ApprovalTerms, AstakosGroup, Resource,
 +    EmailChange, GroupKind, Membership, AstakosGroupQuota)
+ from django.views.decorators.http import require_http_methods
 -from astakos.im.models import AstakosUser, Invitation, ApprovalTerms
  from astakos.im.activation_backends import get_backend, SimpleBackend
  from astakos.im.util import get_context, prepare_response, set_cookie, get_query
 -from astakos.im.forms import *
 -from astakos.im.functions import send_greeting, send_feedback, SendMailError, \
 -    invite as invite_func, logout as auth_logout, activate as activate_func, switch_account_to_shibboleth
 -from astakos.im.settings import DEFAULT_CONTACT_EMAIL, DEFAULT_FROM_EMAIL, COOKIE_NAME, COOKIE_DOMAIN, IM_MODULES, SITENAME, LOGOUT_NEXT, LOGGING_LEVEL
 +from astakos.im.forms import (LoginForm, InvitationForm, ProfileForm,
 +                              FeedbackForm, SignApprovalTermsForm,
 +                              ExtendedPasswordChangeForm, EmailChangeForm,
 +                              AstakosGroupCreationForm, AstakosGroupSearchForm,
 +                              AstakosGroupUpdateForm, AddGroupMembersForm,
 +                              AstakosGroupSortForm, MembersSortForm,
 +                              TimelineForm, PickResourceForm)
 +from astakos.im.functions import (send_feedback, SendMailError,
 +                                  invite as invite_func, logout as auth_logout,
 +                                  activate as activate_func,
 +                                  switch_account_to_shibboleth,
 +                                  send_admin_notification,
 +                                  SendNotificationError)
 +from astakos.im.endpoints.quotaholder import timeline_charge
 +from astakos.im.settings import (
 +    COOKIE_NAME, COOKIE_DOMAIN, SITENAME, LOGOUT_NEXT,
 +    LOGGING_LEVEL, PAGINATE_BY)
 +from astakos.im.tasks import request_billing
  
  logger = logging.getLogger(__name__)
  
 -def render_response(template, tab=None, status=200, reset_cookie=False, context_instance=None, **kwargs):
 +
 +DB_REPLACE_GROUP_SCHEME = """REPLACE(REPLACE("auth_group".name, 'http://', ''),
 +                                     'https://', '')"""
 +
 +def render_response(template, tab=None, status=200, reset_cookie=False,
 +                    context_instance=None, **kwargs):
      """
      Calls ``django.template.loader.render_to_string`` with an additional ``tab``
      keyword argument and returns an ``django.http.HttpResponse`` with the
      if tab is None:
          tab = template.partition('_')[0].partition('.html')[0]
      kwargs.setdefault('tab', tab)
 -    html = render_to_string(template, kwargs, context_instance=context_instance)
 +    html = template_loader.render_to_string(
 +        template, kwargs, context_instance=context_instance)
      response = HttpResponse(html, status=status)
      if reset_cookie:
          set_cookie(response, context_instance['request'].user)
@@@ -121,7 -95,6 +124,7 @@@ def requires_anonymous(func)
          return func(request, *args)
      return wrapper
  
 +
  def signed_terms_required(func):
      """
      Decorator checkes whether the request.user is Anonymous and in that case
      """
      @wraps(func)
      def wrapper(request, *args, **kwargs):
 -        if request.user.is_authenticated() and not request.user.signed_terms():
 +        if request.user.is_authenticated() and not request.user.signed_terms:
              params = urlencode({'next': request.build_absolute_uri(),
 -                              'show_form':''})
 +                                'show_form': ''})
              terms_uri = reverse('latest_terms') + '?' + params
              return HttpResponseRedirect(terms_uri)
          return func(request, *args, **kwargs)
      return wrapper
  
 +
+ @require_http_methods(["GET", "POST"])
  @signed_terms_required
 -def index(request, login_template_name='im/login.html', profile_template_name='im/profile.html', extra_context={}):
 +def index(request, login_template_name='im/login.html', extra_context=None):
      """
      If there is logged on user renders the profile page otherwise renders login page.
  
      """
      template_name = login_template_name
      if request.user.is_authenticated():
 -        return HttpResponseRedirect(reverse('astakos.im.views.edit_profile'))
 +        return HttpResponseRedirect(reverse('edit_profile'))
      return render_response(template_name,
 -                           login_form = LoginForm(request=request),
 -                           context_instance = get_context(request, extra_context))
 +                           login_form=LoginForm(request=request),
 +                           context_instance=get_context(request, extra_context))
 +
  
+ @require_http_methods(["GET", "POST"])
  @login_required
  @signed_terms_required
  @transaction.commit_manually
 -def invite(request, template_name='im/invitations.html', extra_context={}):
 +def invite(request, template_name='im/invitations.html', extra_context=None):
      """
      Allows a user to invite somebody else.
  
  
      * LOGIN_URL: login uri
      * ASTAKOS_DEFAULT_CONTACT_EMAIL: service support email
 -    * ASTAKOS_DEFAULT_FROM_EMAIL: from email
      """
      status = None
      message = None
      form = InvitationForm()
 -    
 +
      inviter = request.user
      if request.method == 'POST':
          form = InvitationForm(request.POST)
                  try:
                      invitation = form.save()
                      invite_func(invitation, inviter)
 -                    status = messages.SUCCESS
                      message = _('Invitation sent to %s' % invitation.username)
 +                    messages.success(request, message)
                  except SendMailError, e:
 -                    status = messages.ERROR
                      message = e.message
 +                    messages.error(request, message)
                      transaction.rollback()
                  except BaseException, e:
 -                    status = messages.ERROR
                      message = _('Something went wrong.')
 +                    messages.error(request, message)
                      logger.exception(e)
                      transaction.rollback()
                  else:
                      transaction.commit()
          else:
 -            status = messages.ERROR
              message = _('No invitations left')
 -    messages.add_message(request, status, message)
 +            messages.error(request, message)
  
      sent = [{'email': inv.username,
               'realname': inv.realname,
               'is_consumed': inv.is_consumed}
 -             for inv in request.user.invitations_sent.all()]
 +            for inv in request.user.invitations_sent.all()]
      kwargs = {'inviter': inviter,
 -              'sent':sent}
 +              'sent': sent}
      context = get_context(request, extra_context, **kwargs)
      return render_response(template_name,
 -                           invitation_form = form,
 -                           context_instance = context)
 +                           invitation_form=form,
 +                           context_instance=context)
 +
  
+ @require_http_methods(["GET", "POST"])
  @login_required
  @signed_terms_required
 -def edit_profile(request, template_name='im/profile.html', extra_context={}):
 +def edit_profile(request, template_name='im/profile.html', extra_context=None):
      """
      Allows a user to edit his/her profile.
  
  
      * LOGIN_URL: login uri
      """
 +    extra_context = extra_context or {}
      form = ProfileForm(instance=request.user)
      extra_context['next'] = request.GET.get('next')
      reset_cookie = False
                  next = request.POST.get('next')
                  if next:
                      return redirect(next)
 -                msg = _('<p>Profile has been updated successfully</p>')
 -                messages.add_message(request, messages.SUCCESS, msg)
 +                msg = _('Profile has been updated successfully')
 +                messages.success(request, msg)
              except ValueError, ve:
 -                messages.add_message(request, messages.ERROR, ve)
 +                messages.success(request, ve)
      elif request.method == "GET":
 -        request.user.is_verified = True
 -        request.user.save()
 +        if not request.user.is_verified:
 +            request.user.is_verified = True
 +            request.user.save()
      return render_response(template_name,
 -                           reset_cookie = reset_cookie,
 -                           profile_form = form,
 -                           context_instance = get_context(request,
 -                                                          extra_context))
 +                           reset_cookie=reset_cookie,
 +                           profile_form=form,
 +                           context_instance=get_context(request,
 +                                                        extra_context))
  
 +
 +@transaction.commit_manually
+ @require_http_methods(["GET", "POST"])
 -def signup(request, template_name='im/signup.html', on_success='im/signup_complete.html', extra_context={}, backend=None):
 +def signup(request, template_name='im/signup.html', on_success='im/signup_complete.html', extra_context=None, backend=None):
      """
      Allows a user to create a local account.
  
      if present, otherwise to the ``astakos.im.activation_backends.InvitationBackend``
      if settings.ASTAKOS_INVITATIONS_ENABLED is True or ``astakos.im.activation_backends.SimpleBackend`` if not
      (see activation_backends);
 -    
 +
      Upon successful user creation, if ``next`` url parameter is present the user is redirected there
      otherwise renders the same page with a success message.
 -    
 +
      On unsuccessful creation, renders ``template_name`` with an error message.
 -    
 +
      **Arguments**
 -    
 +
      ``template_name``
          A custom template to render. This is optional;
          if not specified, this will default to ``im/signup.html``.
          An dictionary of variables to add to the template context.
  
      **Template:**
 -    
 +
      im/signup.html or ``template_name`` keyword argument.
 -    im/signup_complete.html or ``on_success`` keyword argument. 
 +    im/signup_complete.html or ``on_success`` keyword argument.
      """
      if request.user.is_authenticated():
 -        return HttpResponseRedirect(reverse('astakos.im.views.edit_profile'))
 -    
 +        return HttpResponseRedirect(reverse('edit_profile'))
 +
      provider = get_query(request).get('provider', 'local')
      try:
          if not backend:
          form = backend.get_signup_form(provider)
      except Exception, e:
          form = SimpleBackend(request).get_signup_form(provider)
 -        messages.add_message(request, messages.ERROR, e)
 +        messages.error(request, e)
      if request.method == 'POST':
          if form.is_valid():
              user = form.save(commit=False)
                      additional_email = form.cleaned_data['additional_email']
                      if additional_email != user.email:
                          user.additionalmail_set.create(email=additional_email)
 -                        msg = 'Additional email: %s saved for user %s.' % (additional_email, user.email)
 -                        logger._log(LOGGING_LEVEL, msg, [])
 +                        msg = 'Additional email: %s saved for user %s.' % (
 +                            additional_email, user.email)
 +                        logger.log(LOGGING_LEVEL, msg)
                  if user and user.is_active:
                      next = request.POST.get('next', '')
 +                    transaction.commit()
                      return prepare_response(request, user, next=next)
                  messages.add_message(request, status, message)
 +                transaction.commit()
                  return render_response(on_success,
                                         context_instance=get_context(request, extra_context))
              except SendMailError, e:
 -                status = messages.ERROR
                  message = e.message
 -                messages.add_message(request, status, message)
 +                messages.error(request, message)
 +                transaction.rollback()
              except BaseException, e:
 -                status = messages.ERROR
                  message = _('Something went wrong.')
 -                messages.add_message(request, status, message)
 +                messages.error(request, message)
                  logger.exception(e)
 +                transaction.rollback()
      return render_response(template_name,
 -                           signup_form = form,
 -                           provider = provider,
 +                           signup_form=form,
 +                           provider=provider,
                             context_instance=get_context(request, extra_context))
  
 +
+ @require_http_methods(["GET", "POST"])
  @login_required
  @signed_terms_required
 -def feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context={}):
 +def feedback(request, template_name='im/feedback.html', email_template_name='im/feedback_mail.txt', extra_context=None):
      """
      Allows a user to send feedback.
  
              try:
                  send_feedback(msg, data, request.user, email_template_name)
              except SendMailError, e:
 -                message = e.message
 -                status = messages.ERROR
 +                messages.error(request, message)
              else:
                  message = _('Feedback successfully sent')
 -                status = messages.SUCCESS
 -            messages.add_message(request, status, message)
 +                messages.success(request, message)
      return render_response(template_name,
 -                           feedback_form = form,
 -                           context_instance = get_context(request, extra_context))
 +                           feedback_form=form,
 +                           context_instance=get_context(request, extra_context))
 +
  
+ @require_http_methods(["GET", "POST"])
 -def logout(request, template='registration/logged_out.html', extra_context={}):
 +@signed_terms_required
 +def logout(request, template='registration/logged_out.html', extra_context=None):
      """
      Wraps `django.contrib.auth.logout` and delete the cookie.
      """
          auth_logout(request)
          response.delete_cookie(COOKIE_NAME, path='/', domain=COOKIE_DOMAIN)
          msg = 'Cookie deleted for %s' % email
 -        logger._log(LOGGING_LEVEL, msg, [])
 +        logger.log(LOGGING_LEVEL, msg)
      next = request.GET.get('next')
      if next:
          response['Location'] = next
          response['Location'] = LOGOUT_NEXT
          response.status_code = 301
          return response
 -    messages.add_message(request, messages.SUCCESS, _('<p>You have successfully logged out.</p>'))
 +    messages.success(request, _('You have successfully logged out.'))
      context = get_context(request, extra_context)
 -    response.write(render_to_string(template, context_instance=context))
 +    response.write(template_loader.render_to_string(template, context_instance=context))
      return response
  
 +
+ @require_http_methods(["GET", "POST"])
  @transaction.commit_manually
 -def activate(request, greeting_email_template_name='im/welcome_email.txt', helpdesk_email_template_name='im/helpdesk_notification.txt'):
 +def activate(request, greeting_email_template_name='im/welcome_email.txt',
 +             helpdesk_email_template_name='im/helpdesk_notification.txt'):
      """
      Activates the user identified by the ``auth`` request parameter, sends a welcome email
      and renews the user token.
          user = AstakosUser.objects.get(auth_token=token)
      except AstakosUser.DoesNotExist:
          return HttpResponseBadRequest(_('No such user'))
 -    
 +
      if user.is_active:
          message = _('Account already active.')
 -        messages.add_message(request, messages.ERROR, message)
 +        messages.error(request, message)
          return index(request)
 -    
 +
      try:
 -        local_user = AstakosUser.objects.get(~Q(id = user.id), email=user.email, is_active=True)
 +        local_user = AstakosUser.objects.get(
 +            ~Q(id=user.id),
 +            email=user.email,
 +            is_active=True
 +        )
      except AstakosUser.DoesNotExist:
          try:
 -            activate_func(user, greeting_email_template_name, helpdesk_email_template_name, verify_email=True)
 +            activate_func(
 +                user,
 +                greeting_email_template_name,
 +                helpdesk_email_template_name,
 +                verify_email=True
 +            )
              response = prepare_response(request, user, next, renew=True)
              transaction.commit()
              return response
          except SendMailError, e:
              message = e.message
 -            messages.add_message(request, messages.ERROR, message)
 +            messages.error(request, message)
              transaction.rollback()
              return index(request)
          except BaseException, e:
 -            status = messages.ERROR
              message = _('Something went wrong.')
 -            messages.add_message(request, messages.ERROR, message)
 +            messages.error(request, message)
              logger.exception(e)
              transaction.rollback()
              return index(request)
      else:
          try:
 -            user = switch_account_to_shibboleth(user, local_user, greeting_email_template_name)
 +            user = switch_account_to_shibboleth(
 +                user,
 +                local_user,
 +                greeting_email_template_name
 +            )
              response = prepare_response(request, user, next, renew=True)
              transaction.commit()
              return response
          except SendMailError, e:
              message = e.message
 -            messages.add_message(request, messages.ERROR, message)
 +            messages.error(request, message)
              transaction.rollback()
              return index(request)
          except BaseException, e:
 -            status = messages.ERROR
              message = _('Something went wrong.')
 -            messages.add_message(request, messages.ERROR, message)
 +            messages.error(request, message)
              logger.exception(e)
              transaction.rollback()
              return index(request)
  
 +
+ @require_http_methods(["GET", "POST"])
 -def approval_terms(request, term_id=None, template_name='im/approval_terms.html', extra_context={}):
 +def approval_terms(request, term_id=None, template_name='im/approval_terms.html', extra_context=None):
      term = None
      terms = None
      if not term_id:
              pass
      else:
          try:
 -             term = ApprovalTerms.objects.get(id=term_id)
 -        except ApprovalTermDoesNotExist, e:
 +            term = ApprovalTerms.objects.get(id=term_id)
 +        except ApprovalTerms.DoesNotExist, e:
              pass
  
      if not term:
 -        return HttpResponseRedirect(reverse('astakos.im.views.index'))
 +        return HttpResponseRedirect(reverse('index'))
      f = open(term.location, 'r')
      terms = f.read()
  
      if request.method == 'POST':
          next = request.POST.get('next')
          if not next:
 -            next = reverse('astakos.im.views.index')
 +            next = reverse('index')
          form = SignApprovalTermsForm(request.POST, instance=request.user)
          if not form.is_valid():
              return render_response(template_name,
 -                           terms = terms,
 -                           approval_terms_form = form,
 -                           context_instance = get_context(request, extra_context))
 +                                   terms=terms,
 +                                   approval_terms_form=form,
 +                                   context_instance=get_context(request, extra_context))
          user = form.save()
          return HttpResponseRedirect(next)
      else:
          form = None
 -        if request.user.is_authenticated() and not request.user.signed_terms():
 +        if request.user.is_authenticated() and not request.user.signed_terms:
              form = SignApprovalTermsForm(instance=request.user)
          return render_response(template_name,
 -                               terms = terms,
 -                               approval_terms_form = form,
 -                               context_instance = get_context(request, extra_context))
 +                               terms=terms,
 +                               approval_terms_form=form,
 +                               context_instance=get_context(request, extra_context))
 +
  
+ @require_http_methods(["GET", "POST"])
  @signed_terms_required
  def change_password(request):
      return password_change(request,
 -                            post_change_redirect=reverse('astakos.im.views.edit_profile'),
 -                            password_change_form=ExtendedPasswordChangeForm)
 +                           post_change_redirect=reverse('edit_profile'),
 +                           password_change_form=ExtendedPasswordChangeForm)
  
- @signed_terms_required
+ @require_http_methods(["GET", "POST"])
  @login_required
+ @signed_terms_required
  @transaction.commit_manually
  def change_email(request, activation_key=None,
                   email_template_name='registration/email_change_email.txt',
                   form_template_name='registration/email_change_form.html',
                   confirm_template_name='registration/email_change_done.html',
 -                 extra_context={}):
 +                 extra_context=None):
      if activation_key:
          try:
              user = EmailChange.objects.change_email(activation_key)
              if request.user.is_authenticated() and request.user == user:
                  msg = _('Email changed successfully.')
 -                messages.add_message(request, messages.SUCCESS, msg)
 +                messages.success(request, msg)
                  auth_logout(request)
                  response = prepare_response(request, user)
                  transaction.commit()
                  return response
          except ValueError, e:
 -            messages.add_message(request, messages.ERROR, e)
 +            messages.error(request, e)
          return render_response(confirm_template_name,
 -                               modified_user = user if 'user' in locals() else None,
 -                               context_instance = get_context(request,
 -                                                              extra_context))
 -    
 +                               modified_user=user if 'user' in locals(
 +                               ) else None,
 +                               context_instance=get_context(request,
 +                                                            extra_context))
 +
      if not request.user.is_authenticated():
          path = quote(request.get_full_path())
 -        url = request.build_absolute_uri(reverse('astakos.im.views.index'))
 +        url = request.build_absolute_uri(reverse('index'))
          return HttpResponseRedirect(url + '?next=' + path)
      form = EmailChangeForm(request.POST or None)
      if request.method == 'POST' and form.is_valid():
          try:
              ec = form.save(email_template_name, request)
          except SendMailError, e:
 -            status = messages.ERROR
              msg = e
 +            messages.error(request, msg)
              transaction.rollback()
          except IntegrityError, e:
 -            status = messages.ERROR
              msg = _('There is already a pending change email request.')
 +            messages.error(request, msg)
          else:
 -            status = messages.SUCCESS
              msg = _('Change email request has been registered succefully.\
                      You are going to receive a verification email in the new address.')
 +            messages.success(request, msg)
              transaction.commit()
 -        messages.add_message(request, status, msg)
      return render_response(form_template_name,
 -                           form = form,
 -                           context_instance = get_context(request,
 -                                                          extra_context))
 +                           form=form,
 +                           context_instance=get_context(request,
 +                                                        extra_context))
 +
 +
 +@signed_terms_required
 +@login_required
 +def group_add(request, kind_name='default'):
 +    try:
 +        kind = GroupKind.objects.get(name=kind_name)
 +    except:
 +        return HttpResponseBadRequest(_('No such group kind'))
 +
 +    post_save_redirect = '/im/group/%(id)s/'
 +    context_processors = None
 +    model, form_class = get_model_and_form_class(
 +        model=None,
 +        form_class=AstakosGroupCreationForm
 +    )
 +    resources = dict(
 +        (str(r.id), r) for r in Resource.objects.select_related().all())
 +    policies = []
 +    if request.method == 'POST':
 +        form = form_class(request.POST, request.FILES, resources=resources)
 +        if form.is_valid():
 +            new_object = form.save()
 +
 +            # save owner
 +            new_object.owners = [request.user]
 +
 +            # save quota policies
 +            for (rid, uplimit) in form.resources():
 +                try:
 +                    r = resources[rid]
 +                except KeyError, e:
 +                    logger.exception(e)
 +                    # TODO Should I stay or should I go???
 +                    continue
 +                else:
 +                    new_object.astakosgroupquota_set.create(
 +                        resource=r,
 +                        uplimit=uplimit
 +                    )
 +                policies.append('%s %d' % (r, uplimit))
 +            msg = _("The %(verbose_name)s was created successfully.") %\
 +                {"verbose_name": model._meta.verbose_name}
 +            messages.success(request, msg, fail_silently=True)
 +
 +            # send notification
 +            try:
 +                send_admin_notification(
 +                    template_name='im/group_creation_notification.txt',
 +                    dictionary={
 +                        'group': new_object,
 +                        'owner': request.user,
 +                        'policies': policies,
 +                    },
 +                    subject='%s alpha2 testing group creation notification' % SITENAME
 +                )
 +            except SendNotificationError, e:
 +                messages.error(request, e, fail_silently=True)
 +            return HttpResponseRedirect(post_save_redirect % new_object.__dict__)
 +    else:
 +        now = datetime.now()
 +        data = {
 +            'kind': kind
 +        }
 +        form = form_class(data, resources=resources)
 +
 +    # Create the template, context, response
 +    template_name = "%s/%s_form.html" % (
 +        model._meta.app_label,
 +        model._meta.object_name.lower()
 +    )
 +    t = template_loader.get_template(template_name)
 +    c = RequestContext(request, {
 +        'form': form,
 +        'kind': kind,
 +    }, context_processors)
 +    return HttpResponse(t.render(c))
 +
 +
 +@signed_terms_required
 +@login_required
 +def group_list(request):
 +    none = request.user.astakos_groups.none()
 +    q = AstakosGroup.objects.raw("""
 +        SELECT auth_group.id,
 +        %s AS groupname,
 +        im_groupkind.name AS kindname,
 +        im_astakosgroup.*,
 +        owner.email AS groupowner,
 +        (SELECT COUNT(*) FROM im_membership
 +            WHERE group_id = im_astakosgroup.group_ptr_id
 +            AND date_joined IS NOT NULL) AS approved_members_num,
 +        (SELECT CASE WHEN(
 +                    SELECT date_joined FROM im_membership
 +                    WHERE group_id = im_astakosgroup.group_ptr_id
 +                    AND person_id = %s) IS NULL
 +                    THEN 0 ELSE 1 END) AS membership_status
 +        FROM im_astakosgroup
 +        INNER JOIN im_membership ON (
 +            im_astakosgroup.group_ptr_id = im_membership.group_id)
 +        INNER JOIN auth_group ON(im_astakosgroup.group_ptr_id = auth_group.id)
 +        INNER JOIN im_groupkind ON (im_astakosgroup.kind_id = im_groupkind.id)
 +        LEFT JOIN im_astakosuser_owner ON (
 +            im_astakosuser_owner.astakosgroup_id = im_astakosgroup.group_ptr_id)
 +        LEFT JOIN auth_user as owner ON (
 +            im_astakosuser_owner.astakosuser_id = owner.id)
 +        WHERE im_membership.person_id = %s
 +        """ % (DB_REPLACE_GROUP_SCHEME, request.user.id, request.user.id))
 +    d = defaultdict(list)
 +    for g in q:
 +        if request.user.email == g.groupowner:
 +            d['own'].append(g)
 +        else:
 +            d['other'].append(g)
 +    
 +    # validate sorting
 +    fields = ('own', 'other')
 +    for f in fields:
 +        v = globals()['%s_sorting' % f] = request.GET.get('%s_sorting' % f)
 +        if v:
 +            form = AstakosGroupSortForm({'sort_by': v})
 +            if not form.is_valid():
 +                globals()['%s_sorting' % f] = form.cleaned_data.get('sort_by')
 +    return object_list(request, queryset=none,
 +                       extra_context={'is_search':False,
 +                                      'mine': d['own'],
 +                                      'other': d['other'],
 +                                      'own_sorting': own_sorting,
 +                                      'other_sorting': other_sorting,
 +                                      'own_page': request.GET.get('own_page', 1),
 +                                      'other_page': request.GET.get('other_page', 1)
 +                                      })
 +
 +
 +@signed_terms_required
 +@login_required
 +def group_detail(request, group_id):
 +    q = AstakosGroup.objects.select_related().filter(pk=group_id)
 +    q = q.extra(select={
 +        'is_member': """SELECT CASE WHEN EXISTS(
 +                            SELECT id FROM im_membership
 +                            WHERE group_id = im_astakosgroup.group_ptr_id
 +                            AND person_id = %s)
 +                        THEN 1 ELSE 0 END""" % request.user.id,
 +        'is_owner': """SELECT CASE WHEN EXISTS(
 +                        SELECT id FROM im_astakosuser_owner
 +                        WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
 +                        AND astakosuser_id = %s)
 +                        THEN 1 ELSE 0 END""" % request.user.id,
 +        'kindname': """SELECT name FROM im_groupkind
 +                       WHERE id = im_astakosgroup.kind_id"""})
 +    
 +    model = q.model
 +    context_processors = None
 +    mimetype = None
 +    try:
 +        obj = q.get()
 +    except AstakosGroup.DoesNotExist:
 +        raise Http404("No %s found matching the query" % (
 +            model._meta.verbose_name))
 +    
 +    update_form = AstakosGroupUpdateForm(instance=obj)
 +    addmembers_form = AddGroupMembersForm()
 +    if request.method == 'POST':
 +        update_data = {}
 +        addmembers_data = {}
 +        for k,v in request.POST.iteritems():
 +            if k in update_form.fields:
 +                update_data[k] = v
 +            if k in addmembers_form.fields:
 +                addmembers_data[k] = v
 +        update_data = update_data or None
 +        addmembers_data = addmembers_data or None
 +        update_form = AstakosGroupUpdateForm(update_data, instance=obj)
 +        addmembers_form = AddGroupMembersForm(addmembers_data)
 +        if update_form.is_valid():
 +            update_form.save()
 +        if addmembers_form.is_valid():
 +            map(obj.approve_member, addmembers_form.valid_users)
 +            addmembers_form = AddGroupMembersForm()
 +    
 +    template_name = "%s/%s_detail.html" % (model._meta.app_label, model._meta.object_name.lower())
 +    t = template_loader.get_template(template_name)
 +    c = RequestContext(request, {
 +        'object': obj,
 +    }, context_processors)
 +    
 +    # validate sorting
 +    sorting= request.GET.get('sorting')
 +    if sorting:
 +        form = MembersSortForm({'sort_by': sorting})
 +        if form.is_valid():
 +            sorting = form.cleaned_data.get('sort_by')
 +         
 +    extra_context = {'update_form': update_form,
 +                     'addmembers_form': addmembers_form,
 +                     'page': request.GET.get('page', 1),
 +                     'sorting': sorting}
 +    for key, value in extra_context.items():
 +        if callable(value):
 +            c[key] = value()
 +        else:
 +            c[key] = value
 +    response = HttpResponse(t.render(c), mimetype=mimetype)
 +    populate_xheaders(request, response, model, getattr(obj, obj._meta.pk.name))
 +    return response
 +
 +
 +@signed_terms_required
 +@login_required
 +def group_search(request, extra_context=None, **kwargs):
 +    q = request.GET.get('q')
 +    sorting = request.GET.get('sorting')
 +    if request.method == 'GET':
 +        form = AstakosGroupSearchForm({'q': q} if q else None)
 +    else:
 +        form = AstakosGroupSearchForm(get_query(request))
 +        if form.is_valid():
 +            q = form.cleaned_data['q'].strip()
 +    if q:
 +        queryset = AstakosGroup.objects.select_related()
 +        queryset = queryset.filter(name__contains=q)
 +        queryset = queryset.filter(approval_date__isnull=False)
 +        queryset = queryset.extra(select={
 +                'groupname': DB_REPLACE_GROUP_SCHEME,
 +                'kindname': "im_groupkind.name",
 +                'approved_members_num': """
 +                    SELECT COUNT(*) FROM im_membership
 +                    WHERE group_id = im_astakosgroup.group_ptr_id
 +                    AND date_joined IS NOT NULL""",
 +                'membership_approval_date': """
 +                    SELECT date_joined FROM im_membership
 +                    WHERE group_id = im_astakosgroup.group_ptr_id
 +                    AND person_id = %s""" % request.user.id,
 +                'is_member': """
 +                    SELECT CASE WHEN EXISTS(
 +                    SELECT date_joined FROM im_membership
 +                    WHERE group_id = im_astakosgroup.group_ptr_id
 +                    AND person_id = %s)
 +                    THEN 1 ELSE 0 END""" % request.user.id,
 +                'is_owner': """
 +                    SELECT CASE WHEN EXISTS(
 +                    SELECT id FROM im_astakosuser_owner
 +                    WHERE astakosgroup_id = im_astakosgroup.group_ptr_id
 +                    AND astakosuser_id = %s)
 +                    THEN 1 ELSE 0 END""" % request.user.id})
 +        if sorting:
 +            # TODO check sorting value
 +            queryset = queryset.order_by(sorting)
 +    else:
 +        queryset = AstakosGroup.objects.none()
 +    return object_list(
 +        request,
 +        queryset,
 +        paginate_by=PAGINATE_BY,
 +        page=request.GET.get('page') or 1,
 +        template_name='im/astakosgroup_list.html',
 +        extra_context=dict(form=form,
 +                           is_search=True,
 +                           q=q,
 +                           sorting=sorting))
 +
 +@signed_terms_required
 +@login_required
 +def group_all(request, extra_context=None, **kwargs):
 +    q = AstakosGroup.objects.select_related()
 +    q = q.filter(approval_date__isnull=False)
 +    q = q.extra(select={
 +                'groupname': DB_REPLACE_GROUP_SCHEME,
 +                'kindname': "im_groupkind.name",
 +                'approved_members_num': """
 +                    SELECT COUNT(*) FROM im_membership
 +                    WHERE group_id = im_astakosgroup.group_ptr_id
 +                    AND date_joined IS NOT NULL""",
 +                'membership_approval_date': """
 +                    SELECT date_joined FROM im_membership
 +                    WHERE group_id = im_astakosgroup.group_ptr_id
 +                    AND person_id = %s""" % request.user.id,
 +                'is_member': """
 +                    SELECT CASE WHEN EXISTS(
 +                    SELECT date_joined FROM im_membership
 +                    WHERE group_id = im_astakosgroup.group_ptr_id
 +                    AND person_id = %s)
 +                    THEN 1 ELSE 0 END""" % request.user.id})
 +    sorting = request.GET.get('sorting')
 +    if sorting:
 +        # TODO check sorting value
 +        q = q.order_by(sorting)
 +    return object_list(
 +                request,
 +                q,
 +                paginate_by=PAGINATE_BY,
 +                page=request.GET.get('page') or 1,
 +                template_name='im/astakosgroup_list.html',
 +                extra_context=dict(form=AstakosGroupSearchForm(),
 +                                   is_search=True,
 +                                   sorting=sorting))
 +
 +
 +@signed_terms_required
 +@login_required
 +def group_join(request, group_id):
 +    m = Membership(group_id=group_id,
 +                   person=request.user,
 +                   date_requested=datetime.now())
 +    try:
 +        m.save()
 +        post_save_redirect = reverse(
 +            'group_detail',
 +            kwargs=dict(group_id=group_id))
 +        return HttpResponseRedirect(post_save_redirect)
 +    except IntegrityError, e:
 +        logger.exception(e)
 +        msg = _('Failed to join group.')
 +        messages.error(request, msg)
 +        return group_search(request)
 +
 +
 +@signed_terms_required
 +@login_required
 +def group_leave(request, group_id):
 +    try:
 +        m = Membership.objects.select_related().get(
 +            group__id=group_id,
 +            person=request.user)
 +    except Membership.DoesNotExist:
 +        return HttpResponseBadRequest(_('Invalid membership.'))
 +    if request.user in m.group.owner.all():
 +        return HttpResponseForbidden(_('Owner can not leave the group.'))
 +    return delete_object(
 +        request,
 +        model=Membership,
 +        object_id=m.id,
 +        template_name='im/astakosgroup_list.html',
 +        post_delete_redirect=reverse(
 +            'group_detail',
 +            kwargs=dict(group_id=group_id)))
 +
 +
 +def handle_membership(func):
 +    @wraps(func)
 +    def wrapper(request, group_id, user_id):
 +        try:
 +            m = Membership.objects.select_related().get(
 +                group__id=group_id,
 +                person__id=user_id)
 +        except Membership.DoesNotExist:
 +            return HttpResponseBadRequest(_('Invalid membership.'))
 +        else:
 +            if request.user not in m.group.owner.all():
 +                return HttpResponseForbidden(_('User is not a group owner.'))
 +            func(request, m)
 +            return group_detail(request, group_id)
 +    return wrapper
 +
 +
 +@signed_terms_required
 +@login_required
 +@handle_membership
 +def approve_member(request, membership):
 +    try:
 +        membership.approve()
 +        realname = membership.person.realname
 +        msg = _('%s has been successfully joined the group.' % realname)
 +        messages.success(request, msg)
 +    except BaseException, e:
 +        logger.exception(e)
 +        realname = membership.person.realname
 +        msg = _('Something went wrong during %s\'s approval.' % realname)
 +        messages.error(request, msg)
 +
 +
 +@signed_terms_required
 +@login_required
 +@handle_membership
 +def disapprove_member(request, membership):
 +    try:
 +        membership.disapprove()
 +        realname = membership.person.realname
 +        msg = _('%s has been successfully removed from the group.' % realname)
 +        messages.success(request, msg)
 +    except BaseException, e:
 +        logger.exception(e)
 +        msg = _('Something went wrong during %s\'s disapproval.' % realname)
 +        messages.error(request, msg)
 +
 +
 +@signed_terms_required
 +@login_required
 +def resource_list(request):
 +    if request.method == 'POST':
 +        form = PickResourceForm(request.POST)
 +        if form.is_valid():
 +            r = form.cleaned_data.get('resource')
 +            if r:
 +                groups = request.user.membership_set.only('group').filter(
 +                    date_joined__isnull=False)
 +                groups = [g.group_id for g in groups]
 +                q = AstakosGroupQuota.objects.select_related().filter(
 +                    resource=r, group__in=groups)
 +    else:
 +        form = PickResourceForm()
 +        q = AstakosGroupQuota.objects.none()
 +    return object_list(request, q,
 +                       template_name='im/astakosuserquota_list.html',
 +                       extra_context={'form': form})
 +
 +
 +def group_create_list(request):
 +    form = PickResourceForm()
 +    return render_response(
 +        template='im/astakosgroup_create_list.html',
 +        context_instance=get_context(request),)
 +
 +
 +@signed_terms_required
 +@login_required
 +def billing(request):
 +    
 +    today = datetime.today()
 +    month_last_day= calendar.monthrange(today.year, today.month)[1]
 +    
 +    start = request.POST.get('datefrom', None)
 +    if start:
 +        today = datetime.fromtimestamp(int(start))
 +        month_last_day= calendar.monthrange(today.year, today.month)[1]
 +    
 +    start = datetime(today.year, today.month, 1).strftime("%s")
 +    end = datetime(today.year, today.month, month_last_day).strftime("%s")
 +    r = request_billing.apply(args=('pgerakios@grnet.gr',
 +                                    int(start) * 1000,
 +                                    int(end) * 1000))
 +    data = {}
 +    
 +    try:
 +        status, data = r.result
 +        data=_clear_billing_data(data)
 +        if status != 200:
 +            messages.error(request, _('Service response status: %d' % status))
 +    except:
 +        messages.error(request, r.result)
 +    
 +    print type(start)
 +    
 +    return render_response(
 +        template='im/billing.html',
 +        context_instance=get_context(request),
 +        data=data,
 +        zerodate=datetime(month=1,year=1970, day=1),
 +        today=today,
 +        start=int(start),
 +        month_last_day=month_last_day)  
 +    
 +def _clear_billing_data(data):
 +    
 +    # remove addcredits entries
 +    def isnotcredit(e):
 +        return e['serviceName'] != "addcredits"
 +    
 +    
 +    
 +    # separate services    
 +    def servicefilter(service_name):
 +        service = service_name
 +        def fltr(e):
 +            return e['serviceName'] == service
 +        return fltr
 +        
 +    
 +    data['bill_nocredits'] = filter(isnotcredit, data['bill'])
 +    data['bill_vmtime'] = filter(servicefilter('vmtime'), data['bill'])
 +    data['bill_diskspace'] = filter(servicefilter('diskspace'), data['bill'])
 +    data['bill_addcredits'] = filter(servicefilter('addcredits'), data['bill'])
 +        
 +    return data    
 +
 +@signed_terms_required
 +@login_required
 +def timeline(request):
 +#    data = {'entity':request.user.email}
 +    timeline_body = ()
 +    timeline_header = ()
 +#    form = TimelineForm(data)
 +    form = TimelineForm()
 +    if request.method == 'POST':
 +        data = request.POST
 +        form = TimelineForm(data)
 +        if form.is_valid():
 +            data = form.cleaned_data
 +            timeline_header = ('entity', 'resource',
 +                               'event name', 'event date',
 +                               'incremental cost', 'total cost')
 +            timeline_body = timeline_charge(
 +                                    data['entity'],     data['resource'],
 +                                    data['start_date'], data['end_date'],
 +                                    data['details'],    data['operation'])
 +        
 +    return render_response(template='im/timeline.html',
 +                           context_instance=get_context(request),
 +                           form=form,
 +                           timeline_header=timeline_header,
 +                           timeline_body=timeline_body)
 +    return data
  #from logging import INFO
  #ASTAKOS_LOGGING_LEVEL = INFO
  
 -# Email subjects configuration. For admin/helper notification emails %(user)s 
 +# Email subjects configuration. For admin/helper notification emails %(user)s
  # maps to registered/activated user email.
  #ASTAKOS_INVITATION_EMAIL_SUBJECT = 'Invitation to %s alpha2 testing' % SITENAME
  #ASTAKOS_GREETING_EMAIL_SUBJECT = 'Welcome to %s alpha2 testing' % SITENAME
  #ASTAKOS_EMAIL_CHANGE_EMAIL_SUBJECT = 'Email change on %s alpha2 testing' % SITENAME
  #ASTAKOS_PASSWORD_RESET_EMAIL_SUBJECT = 'Password reset on %s alpha2 testing' % SITENAME
  
 +# Set the quota holder component URI
 +#ASTAKOS_QUOTA_HOLDER_URL = ''
 +
 +# Set the cloud service properties
 +# SERVICES = getattr(settings, 'ASTAKOS_SERVICES',
 +#                    {'cyclades': {'url':'https://node1.example.com/ui/',
 +#                                  'quota': {'vm': 2}},
 +#                     'pithos+':  {'url':'https://node2.example.com/ui/',
 +#                                  'quota': {'diskspace': 50 * 1024 * 1024 * 1024}}})
 +
 +# Set the billing URI
 +#ASTAKOS_AQUARIUM_URL = ''
 +
 +# Set how many objects should be displayed per page
- #PAGINATE_BY = getattr(settings, 'ASTAKOS_PAGINATE_BY', 10)
++#PAGINATE_BY = getattr(settings, 'ASTAKOS_PAGINATE_BY', 10)
++
+ # Enforce token renewal on password change/reset
+ NEWPASSWD_INVALIDATE_TOKEN = getattr(settings, 'ASTAKOS_NEWPASSWD_INVALIDATE_TOKEN', True)