Merge branch 'devel-0.13' into multipleauthmethods
authorKostas Papadimitriou <kpap@grnet.gr>
Fri, 30 Nov 2012 12:58:44 +0000 (14:58 +0200)
committerKostas Papadimitriou <kpap@grnet.gr>
Fri, 30 Nov 2012 13:29:56 +0000 (15:29 +0200)
Conflicts:
snf-astakos-app/astakos/im/context_processors.py
snf-astakos-app/astakos/im/forms.py
snf-astakos-app/astakos/im/models.py
snf-astakos-app/astakos/im/target/local.py
snf-astakos-app/astakos/im/target/shibboleth.py
snf-astakos-app/astakos/im/urls.py
snf-astakos-app/astakos/im/views.py

12 files changed:
1  2 
snf-astakos-app/astakos/im/context_processors.py
snf-astakos-app/astakos/im/forms.py
snf-astakos-app/astakos/im/models.py
snf-astakos-app/astakos/im/synnefo_settings.py
snf-astakos-app/astakos/im/target/local.py
snf-astakos-app/astakos/im/target/shibboleth.py
snf-astakos-app/astakos/im/templates/im/profile.html
snf-astakos-app/astakos/im/templates/im/signup.html
snf-astakos-app/astakos/im/templates/im/third_party_registration.html
snf-astakos-app/astakos/im/tests.py
snf-astakos-app/astakos/im/urls.py
snf-astakos-app/astakos/im/views.py

  # interpreted as representing official policies, either expressed
  # or implied, of GRNET S.A.
  
- from astakos.im.settings import IM_MODULES, INVITATIONS_ENABLED, IM_STATIC_URL, \
-         COOKIE_NAME, LOGIN_MESSAGES, SIGNUP_MESSAGES, PROFILE_MESSAGES, \
-         GLOBAL_MESSAGES, PROFILE_EXTRA_LINKS
- from astakos.im.api.admin import get_menu
+ from astakos.im.settings import (
+     IM_MODULES, INVITATIONS_ENABLED, IM_STATIC_URL,
+     LOGIN_MESSAGES, SIGNUP_MESSAGES, PROFILE_MESSAGES,
+     GLOBAL_MESSAGES, PROFILE_EXTRA_LINKS
+ )
+ from astakos.im.api import get_menu
  from astakos.im.util import get_query
+ from astakos.im.models import GroupKind
 +from astakos.im.auth_providers import PROVIDERS as AUTH_PROVIDERS
  
- from django.conf import settings
- from django.core.urlresolvers import reverse
  from django.utils import simplejson as json
  
  def im_modules(request):
      return {'im_modules': IM_MODULES}
  
 +def auth_providers(request):
 +    return {'auth_providers': filter(lambda p:p.module_enabled,
 +                                     AUTH_PROVIDERS.itervalues())}
  
  def next(request):
-     return {'next' : get_query(request).get('next', '')}
+     return {'next': get_query(request).get('next', '')}
  
  def code(request):
-     return {'code' : request.GET.get('code', '')}
+     return {'code': request.GET.get('code', '')}
  
  def invitations(request):
-     return {'invitations_enabled' :INVITATIONS_ENABLED}
+     return {'invitations_enabled': INVITATIONS_ENABLED}
  
  def media(request):
-     return {'IM_STATIC_URL' : IM_STATIC_URL}
+     return {'IM_STATIC_URL': IM_STATIC_URL}
  
  def custom_messages(request):
      global GLOBAL_MESSAGES, SIGNUP_MESSAGES, LOGIN_MESSAGES, PROFILE_MESSAGES
@@@ -42,23 -42,19 +42,22 @@@ from django.contrib.auth.tokens import 
  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 django.conf import settings
 +from django.forms.models import fields_for_model
 +from django.db import transaction
  
  from astakos.im.models import (
-     AstakosUser, Invitation, get_latest_terms,
-     EmailChange, PendingThirdPartyUser
+     AstakosUser, EmailChange, AstakosGroup, Invitation, GroupKind,
+     Resource, PendingThirdPartyUser, get_latest_terms, RESOURCE_SEPARATOR
  )
- 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, MODERATION_ENABLED
+ from astakos.im.settings import (
+     INVITATIONS_PER_LEVEL, BASEURL, SITENAME, RECAPTCHA_PRIVATE_KEY,
+     RECAPTCHA_ENABLED, DEFAULT_CONTACT_EMAIL, LOGGING_LEVEL,
 -    PASSWORD_RESET_EMAIL_SUBJECT, NEWPASSWD_INVALIDATE_TOKEN
++    PASSWORD_RESET_EMAIL_SUBJECT, NEWPASSWD_INVALIDATE_TOKEN,
++    MODERATION_ENABLED
  )
- from astakos.im import settings
  from astakos.im.widgets import DummyWidget, RecaptchaWidget
  from astakos.im.functions import send_change_email
  
@@@ -126,16 -110,16 +128,16 @@@ class LocalUserCreationForm(UserCreatio
              # 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']
 +        email = self.cleaned_data['email'].lower()
          if not email:
-             raise forms.ValidationError(_("This field is required"))
+             raise forms.ValidationError(_(astakos_messages.REQUIRED_FIELD))
          if reserved_email(email):
-             raise forms.ValidationError(_("This email is already used"))
+             raise forms.ValidationError(_(astakos_messages.EMAIL_USED))
          return email
  
      def clean_has_signed_terms(self):
          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(_(astakos_messages.CAPTCHA_VALIDATION_ERR))
  
 +    def post_store_user(self, user, request):
 +        """
 +        Interface method for descendant backends to be able to do stuff within
 +        the transaction enabled by store_user.
 +        """
 +        user.add_auth_provider('local', auth_backend='astakos')
 +        user.set_password(self.cleaned_data['password1'])
 +
      def save(self, commit=True):
          """
          Saves the email, first_name and last_name properties, after the normal
          save behavior is complete.
          """
          user = super(LocalUserCreationForm, self).save(commit=False)
 +        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.
          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.level = level
 -        user.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
 +        user.update_invitations_level()
          user.email_verified = True
          if commit:
              user.save()
@@@ -231,54 -208,46 +233,55 @@@ class ThirdPartyUserCreationForm(forms.
          self.request = kwargs.get('request', None)
          if self.request:
              kwargs.pop('request')
--                
++
          latest_terms = get_latest_terms()
          if latest_terms:
              self._meta.fields.append('has_signed_terms')
--        
++
          super(ThirdPartyUserCreationForm, self).__init__(*args, **kwargs)
--        
++
          if latest_terms:
              self.fields.keyOrder.append('has_signed_terms')
--        
++
          if 'has_signed_terms' in self.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)
--    
++
      def clean_email(self):
 -        email = self.cleaned_data['email']
 +        email = self.cleaned_data['email'].lower()
          if not email:
-             raise forms.ValidationError(_("This field is required"))
+             raise forms.ValidationError(_(astakos_messages.REQUIRED_FIELD))
          return email
  
      def clean_has_signed_terms(self):
          has_signed_terms = self.cleaned_data['has_signed_terms']
          if not has_signed_terms:
-             raise forms.ValidationError(_('You have to agree with the terms'))
+             raise forms.ValidationError(_(astakos_messages.SIGN_TERMS))
          return has_signed_terms
  
 +    def post_store_user(self, user, request):
 +        pending = PendingThirdPartyUser.objects.get(
 +                                token=request.POST.get('third_party_token'),
 +                                third_party_identifier= \
 +            self.cleaned_data.get('third_party_identifier'))
 +        return user.add_pending_auth_provider(pending)
 +
 +
      def save(self, commit=True):
          user = super(ThirdPartyUserCreationForm, self).save(commit=False)
          user.set_unusable_password()
 -        user.provider = get_query(self.request).get('provider')
 +        user.is_local = False
 +        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 InvitedThirdPartyUserCreationForm(ThirdPartyUserCreationForm):
      """
      Extends the ThirdPartyUserCreationForm: email is readonly.
@@@ -329,11 -304,26 +335,13 @@@ class ShibbolethUserCreationForm(ThirdP
                  raise forms.ValidationError(_("This email is already used"))
          super(ShibbolethUserCreationForm, self).clean_email()
          return email
 -    
 -    def save(self, commit=True):
 -        user = super(ShibbolethUserCreationForm, self).save(commit=False)
 -        try:
 -            p = PendingThirdPartyUser.objects.get(
 -                provider=user.provider,
 -                third_party_identifier=user.third_party_identifier
 -            )
 -        except:
 -            pass
 -        else:
 -            p.delete()
 -        return user
  
  
- class InvitedShibbolethUserCreationForm(ShibbolethUserCreationForm, InvitedThirdPartyUserCreationForm):
+ class InvitedShibbolethUserCreationForm(ShibbolethUserCreationForm,
+                                         InvitedThirdPartyUserCreationForm):
      pass
  
  class LoginForm(AuthenticationForm):
      username = forms.EmailField(label=_("Email"))
      recaptcha_challenge_field = forms.CharField(widget=DummyWidget)
          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(_(astakos_messages.CAPTCHA_VALIDATION_ERR))
 -    
++
      def clean(self):
          """
          Override default behavior in order to check user's activation later
@@@ -451,22 -452,16 +470,21 @@@ class ExtendedPasswordResetForm(Passwor
      def clean_email(self):
          email = super(ExtendedPasswordResetForm, self).clean_email()
          try:
-             user = AstakosUser.objects.get(email=email, is_active=True)
+             user = AstakosUser.objects.get(email__iexact=email, is_active=True)
              if not user.has_usable_password():
-                 raise forms.ValidationError(_("This account has not a usable password."))
+                 raise forms.ValidationError(_(astakos_messages.UNUSABLE_PASSWORD))
 -        except AstakosUser.DoesNotExist:
 +
 +            if not user.can_change_password():
 +                raise forms.ValidationError(_('Password change for this account'
 +                                              ' is not supported.'))
 +
 +        except AstakosUser.DoesNotExist, e:
-             raise forms.ValidationError(_('That e-mail address doesn\'t have an'
-                                           ' associated user account. Are you sure'
-                                           ' you\'ve registered?'))
+             raise forms.ValidationError(_(astakos_messages.EMAIL_UNKNOWN))
          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.
          """
@@@ -565,6 -567,260 +586,260 @@@ class ExtendedPasswordChangeForm(Passwo
              pass
          return super(ExtendedPasswordChangeForm, self).save(commit=commit)
  
+ class AstakosGroupCreationForm(forms.ModelForm):
+     kind = forms.ModelChoiceField(
+         queryset=GroupKind.objects.all(),
+         label="",
+         widget=forms.HiddenInput()
+     )
+     name = forms.URLField(widget=forms.TextInput(attrs={'placeholder': 'eg. foo.ece.ntua.gr'}), help_text="Name should be in the form of dns",)
+     moderation_enabled = forms.BooleanField(
+         help_text="Check if you want to approve members participation manually",
+         required=False,
+         initial=True
+     )
+     max_participants = forms.IntegerField(
+         required=True, min_value=1
+     )
+     class Meta:
+         model = AstakosGroup
+     def __init__(self, *args, **kwargs):
+         #update QueryDict
+         args = list(args)
+         qd = args.pop(0).copy()
+         members_unlimited = qd.pop('members_unlimited', False)
+         members_uplimit = qd.pop('members_uplimit', None)
+ #         max_participants = None if members_unlimited else members_uplimit
+ #         qd['max_participants']= max_participants.pop(0) if max_participants else None
 -        
++
+         #substitue QueryDict
+         args.insert(0, qd)
 -        
++
+         super(AstakosGroupCreationForm, self).__init__(*args, **kwargs)
+         self.fields.keyOrder = ['kind', 'name', 'homepage', 'desc',
+                                 'issue_date', 'expiration_date',
+                                 'moderation_enabled', 'max_participants']
+         def add_fields((k, v)):
+             k = k.partition('_proxy')[0]
+             self.fields[k] = forms.IntegerField(
+                 required=False,
+                 widget=forms.HiddenInput(),
+                 min_value=1
+             )
+         map(add_fields,
+             ((k, v) for k,v in qd.iteritems() if k.endswith('_uplimit'))
+         )
 -        
++
+         def add_fields((k, v)):
+             self.fields[k] = forms.BooleanField(
+                 required=False,
+                 #widget=forms.HiddenInput()
+             )
+         map(add_fields,
+             ((k, v) for k,v in qd.iteritems() if k.startswith('is_selected_'))
+         )
 -    
++
+     def policies(self):
+         self.clean()
+         policies = []
+         append = policies.append
+         for name, uplimit in self.cleaned_data.iteritems():
 -            
++
+             subs = name.split('_uplimit')
+             if len(subs) == 2:
+                 prefix, suffix = subs
+                 s, sep, r = prefix.partition(RESOURCE_SEPARATOR)
+                 resource = Resource.objects.get(service__name=s, name=r)
 - 
++
+                 # keep only resource limits for selected resource groups
+                 if self.cleaned_data.get(
+                     'is_selected_%s' % resource.group, False
+                 ):
+                     append(dict(service=s, resource=r, uplimit=uplimit))
+         return policies
+ class AstakosGroupCreationSummaryForm(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,
+         initial=True
+     )
+     max_participants = forms.IntegerField(
+         required=False, min_value=1
+     )
+     class Meta:
+         model = AstakosGroup
+     def __init__(self, *args, **kwargs):
+         #update QueryDict
+         args = list(args)
+         qd = args.pop(0).copy()
+         members_unlimited = qd.pop('members_unlimited', False)
+         members_uplimit = qd.pop('members_uplimit', None)
+ #         max_participants = None if members_unlimited else members_uplimit
+ #         qd['max_participants']= max_participants.pop(0) if max_participants else None
 -        
++
+         #substitue QueryDict
+         args.insert(0, qd)
 -        
++
+         super(AstakosGroupCreationSummaryForm, self).__init__(*args, **kwargs)
+         self.fields.keyOrder = ['kind', 'name', 'homepage', 'desc',
+                                 'issue_date', 'expiration_date',
+                                 'moderation_enabled', 'max_participants']
+         def add_fields((k, v)):
+             self.fields[k] = forms.IntegerField(
+                 required=False,
+                 widget=forms.TextInput(),
+                 min_value=1
+             )
+         map(add_fields,
+             ((k, v) for k,v in qd.iteritems() if k.endswith('_uplimit'))
+         )
 -        
++
+         def add_fields((k, v)):
+             self.fields[k] = forms.BooleanField(
+                 required=False,
+                 widget=forms.HiddenInput()
+             )
+         map(add_fields,
+             ((k, v) for k,v in qd.iteritems() if k.startswith('is_selected_'))
+         )
+         for f in self.fields.values():
+             f.widget = forms.HiddenInput()
+     def clean(self):
+         super(AstakosGroupCreationSummaryForm, self).clean()
+         self.cleaned_data['policies'] = []
+         append = self.cleaned_data['policies'].append
+         #tbd = [f for f in self.fields if (f.startswith('is_selected_') and (not f.endswith('_proxy')))]
+         tbd = [f for f in self.fields if f.startswith('is_selected_')]
+         for name, uplimit in self.cleaned_data.iteritems():
+             subs = name.split('_uplimit')
+             if len(subs) == 2:
+                 tbd.append(name)
+                 prefix, suffix = subs
+                 s, sep, r = prefix.partition(RESOURCE_SEPARATOR)
+                 resource = Resource.objects.get(service__name=s, name=r)
 -                
++
+                 # keep only resource limits for selected resource groups
+                 if self.cleaned_data.get(
+                     'is_selected_%s' % resource.group, False
+                 ):
+                     append(dict(service=s, resource=r, uplimit=uplimit))
+         for name in tbd:
+             self.cleaned_data.pop(name, None)
+         return self.cleaned_data
+ class AstakosGroupUpdateForm(forms.ModelForm):
+     class Meta:
+         model = AstakosGroup
+         fields = ( 'desc','homepage')
+ class AddGroupMembersForm(forms.Form):
+     q = forms.CharField(
+         max_length=800, widget=forms.Textarea, label=_('Add members'),
+         help_text=_(astakos_messages.ADD_GROUP_MEMBERS_Q_HELP),
+         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(_(astakos_messages.UNKNOWN_USERS) % ','.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 project')
+ class TimelineForm(forms.Form):
+     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()"
  class ExtendedSetPasswordForm(SetPasswordForm):
      """
      Extends SetPasswordForm by enabling user
              initial=True,
              help_text='Unsetting this may result in security risk.'
          )
--    
++
      def __init__(self, user, *args, **kwargs):
          super(ExtendedSetPasswordForm, self).__init__(user, *args, **kwargs)
  
              self.user = AstakosUser.objects.get(id=self.user.id)
              if NEWPASSWD_INVALIDATE_TOKEN or self.cleaned_data.get('renew'):
                  self.user.renew_token()
 -            self.user.flush_sessions()
 +            #self.user.flush_sessions()
 +            if not self.user.has_auth_provider('local'):
 +                self.user.add_auth_provider('local', auth_backend='astakos')
 +
          except BaseException, e:
              logger.exception(e)
-             pass
          return super(ExtendedSetPasswordForm, self).save(commit=commit)
@@@ -39,50 -38,267 +38,286 @@@ import loggin
  from time import asctime
  from datetime import datetime, timedelta
  from base64 import b64encode
 +from urlparse import urlparse
 +from urllib import quote
  from random import randint
+ from collections import defaultdict
  
  from django.db import models, IntegrityError
- from django.contrib.auth.models import User, UserManager, Group
+ from django.contrib.auth.models import User, UserManager, Group, Permission
  from django.utils.translation import ugettext as _
- from django.core.exceptions import ValidationError
- from django.template.loader import render_to_string
- from django.core.mail import send_mail
  from django.db import transaction
- from django.db.models.signals import post_save, pre_save, post_syncdb
+ from django.core.exceptions import ValidationError
+ from django.db.models.signals import (
+     pre_save, post_save, post_syncdb, post_delete
+ )
+ from django.contrib.contenttypes.models import ContentType
+ from django.dispatch import Signal
  from django.db.models import Q
 +from django.core.urlresolvers import reverse
 +from django.utils.http import int_to_base36
 +from django.contrib.auth.tokens import default_token_generator
  from django.conf import settings
  from django.utils.importlib import import_module
 +from django.core.validators import email_re
  
- from astakos.im.settings import (
-     DEFAULT_USER_LEVEL, INVITATIONS_PER_LEVEL,
-     AUTH_TOKEN_DURATION, BILLING_FIELDS, QUEUE_CONNECTION, SITENAME,
-     EMAILCHANGE_ACTIVATION_DAYS, LOGGING_LEVEL
+ from astakos.im.settings import (DEFAULT_USER_LEVEL, INVITATIONS_PER_LEVEL,
+                                  AUTH_TOKEN_DURATION, BILLING_FIELDS,
+                                  EMAILCHANGE_ACTIVATION_DAYS, LOGGING_LEVEL)
+ from astakos.im.endpoints.qh import (
+     register_users, send_quota, register_resources
  )
 +from astakos.im import auth_providers
+ from astakos.im.endpoints.aquarium.producer import report_user_event
+ from astakos.im.functions import send_invitation
+ from astakos.im.tasks import propagate_groupmembers_quota
  
- QUEUE_CLIENT_ID = 3 # Astakos.
+ import astakos.im.messages as astakos_messages
  
  logger = logging.getLogger(__name__)
  
+ DEFAULT_CONTENT_TYPE = None
+ try:
+     content_type = ContentType.objects.get(app_label='im', model='astakosuser')
+ except:
+     content_type = DEFAULT_CONTENT_TYPE
+ RESOURCE_SEPARATOR = '.'
+ inf = float('inf')
+ class Service(models.Model):
+     name = models.CharField('Name', max_length=255, unique=True, db_index=True)
+     url = models.FilePathField()
+     icon = models.FilePathField(blank=True)
+     auth_token = models.CharField('Authentication Token', max_length=32,
+                                   null=True, blank=True)
+     auth_token_created = models.DateTimeField('Token creation date', null=True)
+     auth_token_expires = models.DateTimeField(
+         'Token expiration date', null=True)
+     def renew_token(self):
+         md5 = hashlib.md5()
+         md5.update(self.name.encode('ascii', 'ignore'))
+         md5.update(self.url.encode('ascii', 'ignore'))
+         md5.update(asctime())
+         self.auth_token = b64encode(md5.digest())
+         self.auth_token_created = datetime.now()
+         self.auth_token_expires = self.auth_token_created + \
+             timedelta(hours=AUTH_TOKEN_DURATION)
+     def __str__(self):
+         return self.name
+     @property
+     def resources(self):
+         return self.resource_set.all()
+     @resources.setter
+     def resources(self, resources):
+         for s in resources:
+             self.resource_set.create(**s)
 -    
++
+     def add_resource(self, service, resource, uplimit, update=True):
+         """Raises ObjectDoesNotExist, IntegrityError"""
+         resource = Resource.objects.get(service__name=service, name=resource)
+         if update:
+             AstakosUserQuota.objects.update_or_create(user=self,
+                                                       resource=resource,
+                                                       defaults={'uplimit': uplimit})
+         else:
+             q = self.astakosuserquota_set
+             q.create(resource=resource, uplimit=uplimit)
+ class ResourceMetadata(models.Model):
+     key = models.CharField('Name', max_length=255, unique=True, db_index=True)
+     value = models.CharField('Value', max_length=255)
+ class Resource(models.Model):
+     name = models.CharField('Name', max_length=255, unique=True, db_index=True)
+     meta = models.ManyToManyField(ResourceMetadata)
+     service = models.ForeignKey(Service)
+     desc = models.TextField('Description', null=True)
+     unit = models.CharField('Name', null=True, max_length=255)
+     group = models.CharField('Group', null=True, max_length=255)
+     def __str__(self):
+         return '%s%s%s' % (self.service, RESOURCE_SEPARATOR, self.name)
+ class GroupKind(models.Model):
+     name = models.CharField('Name', max_length=255, unique=True, db_index=True)
+     def __str__(self):
+         return self.name
+ class AstakosGroup(Group):
+     kind = models.ForeignKey(GroupKind)
+     homepage = models.URLField(
+         'Homepage Url', max_length=255, null=True, blank=True)
+     desc = models.TextField('Description', null=True)
+     policy = models.ManyToManyField(
+         Resource,
+         null=True,
+         blank=True,
+         through='AstakosGroupQuota'
+     )
+     creation_date = models.DateTimeField(
+         'Creation date',
+         default=datetime.now()
+     )
+     issue_date = models.DateTimeField('Issue date', null=True)
+     expiration_date = models.DateTimeField(
+         'Expiration date',
+          null=True
+     )
+     moderation_enabled = models.BooleanField(
+         'Moderated membership?',
+         default=True
+     )
+     approval_date = models.DateTimeField(
+         'Activation date',
+         null=True,
+         blank=True
+     )
+     estimated_participants = models.PositiveIntegerField(
+         'Estimated #members',
+         null=True,
+         blank=True,
+     )
+     max_participants = models.PositiveIntegerField(
+         'Maximum numder of participants',
+         null=True,
+         blank=True
+     )
 -    
++
+     @property
+     def is_disabled(self):
+         if not self.approval_date:
+             return True
+         return False
+     @property
+     def is_enabled(self):
+         if self.is_disabled:
+             return False
+         if not self.issue_date:
+             return False
+         if not self.expiration_date:
+             return True
+         now = datetime.now()
+         if self.issue_date > now:
+             return False
+         if now >= self.expiration_date:
+             return False
+         return True
+     def enable(self):
+         if self.is_enabled:
+             return
+         self.approval_date = datetime.now()
+         self.save()
+         quota_disturbed.send(sender=self, users=self.approved_members)
+         propagate_groupmembers_quota.apply_async(
+             args=[self], eta=self.issue_date)
+         propagate_groupmembers_quota.apply_async(
+             args=[self], eta=self.expiration_date)
+     def disable(self):
+         if self.is_disabled:
+             return
+         self.approval_date = None
+         self.save()
+         quota_disturbed.send(sender=self, users=self.approved_members)
+     @transaction.commit_manually
+     def approve_member(self, person):
+         m, created = self.membership_set.get_or_create(person=person)
+         try:
+             m.approve()
+         except:
+             transaction.rollback()
+             raise
+         else:
+             transaction.commit()
+ #     def disapprove_member(self, person):
+ #         self.membership_set.remove(person=person)
+     @property
+     def members(self):
+         q = self.membership_set.select_related().all()
+         return [m.person for m in q]
 -    
++
+     @property
+     def approved_members(self):
+         q = self.membership_set.select_related().all()
+         return [m.person for m in q if m.is_approved]
+     @property
+     def quota(self):
+         d = defaultdict(int)
+         for q in self.astakosgroupquota_set.select_related().all():
+             d[q.resource] += q.uplimit or inf
+         return d
 -    
++
+     def add_policy(self, service, resource, uplimit, update=True):
+         """Raises ObjectDoesNotExist, IntegrityError"""
+         resource = Resource.objects.get(service__name=service, name=resource)
+         if update:
+             AstakosGroupQuota.objects.update_or_create(
+                 group=self,
+                 resource=resource,
+                 defaults={'uplimit': uplimit}
+             )
+         else:
+             q = self.astakosgroupquota_set
+             q.create(resource=resource, uplimit=uplimit)
 -    
++
+     @property
+     def policies(self):
+         return self.astakosgroupquota_set.select_related().all()
+     @policies.setter
+     def policies(self, policies):
+         for p in policies:
+             service = p.get('service', None)
+             resource = p.get('resource', None)
+             uplimit = p.get('uplimit', 0)
+             update = p.get('update', True)
+             self.add_policy(service, resource, uplimit, update)
 -    
++
+     @property
+     def owners(self):
+         return self.owner.all()
+     @property
+     def owner_details(self):
+         return self.owner.select_related().all()
+     @owners.setter
+     def owners(self, l):
+         self.owner = l
+         map(self.approve_member, l)
 +
 +class AstakosUserManager(models.Manager):
 +
 +    def get_auth_provider_user(self, provider, **kwargs):
 +        """
 +        Retrieve AstakosUser instance associated with the specified third party
 +        id.
 +        """
 +        kwargs = dict(map(lambda x: ('auth_providers__%s' % x[0], x[1]),
 +                          kwargs.iteritems()))
 +        return self.get(auth_providers__module=provider, **kwargs)
 +
  class AstakosUser(User):
      """
      Extends ``django.contrib.auth.models.User`` by defining additional fields.
      email_verified = models.BooleanField('Email verified?', default=False)
  
      has_credits = models.BooleanField('Has credits?', default=False)
-     has_signed_terms = models.BooleanField('Agree with the terms?', default=False)
-     date_signed_terms = models.DateTimeField('Signed terms date', null=True, blank=True)
-     
-     activation_sent = models.DateTimeField('Activation sent data', null=True, blank=True)
-     
+     has_signed_terms = models.BooleanField(
+         'I agree with the terms', default=False)
+     date_signed_terms = models.DateTimeField(
+         'Signed terms date', null=True, blank=True)
+     activation_sent = models.DateTimeField(
+         'Activation sent data', null=True, blank=True)
+     policy = models.ManyToManyField(
+         Resource, null=True, through='AstakosUserQuota')
+     astakos_groups = models.ManyToManyField(
+         AstakosGroup, verbose_name=_('agroups'), blank=True,
+         help_text=_(astakos_messages.ASTAKOSUSER_GROUPS_HELP),
+         through='Membership')
      __has_signed_terms = False
-     __groupnames = []
+     disturbed_quota = models.BooleanField('Needs quotaholder syncing',
+                                            default=False, db_index=True)
  
 +    objects = AstakosUserManager()
+     owner = models.ManyToManyField(
+         AstakosGroup, related_name='owner', null=True)
+     class Meta:
+         unique_together = ("provider", "third_party_identifier")
  
      def __init__(self, *args, **kwargs):
          super(AstakosUser, self).__init__(*args, **kwargs)
          if not self.id:
              # set username
              while not self.username:
 -                username = uuid.uuid4().hex[:30]
 +                username =  self.email
                  try:
-                     AstakosUser.objects.get(username = username)
-                 except AstakosUser.DoesNotExist, e:
+                     AstakosUser.objects.get(username=username)
+                 except AstakosUser.DoesNotExist:
                      self.username = username
 -            if not self.provider:
 -                self.provider = 'local'
 -            self.email = self.email.lower()
 +
-         report_user_event(self)
          self.validate_unique_email_isactive()
          if self.is_active and self.activation_sent:
              # reset the activation sent
              self.activation_sent = None
          super(AstakosUser, self).save(**kwargs)
-         
-         # set default group if does not exist
-         groupname = 'default'
-         if groupname not in self.__groupnames:
-             try:
-                 group = Group.objects.get(name = groupname)
-                 self.groups.add(group)
-             except Group.DoesNotExist, e:
-                 logger.exception(e)
--    
++
      def renew_token(self, flush_sessions=False, current_key=None):
          md5 = hashlib.md5()
          md5.update(settings.SECRET_KEY)
          md5.update(self.username)
          md5.update(self.realname.encode('ascii', 'ignore'))
          md5.update(asctime())
--        
++
          self.auth_token = b64encode(md5.digest())
          self.auth_token_created = datetime.now()
          self.auth_token_expires = self.auth_token_created + \
          q = self.sessions
          if current_key:
              q = q.exclude(session_key=current_key)
--        
++
          keys = q.values_list('session_key', flat=True)
          if keys:
              msg = 'Flushing sessions: %s' % ','.join(keys)
              return False
          return True
  
 -    def store_disturbed_quota(self, set=True):
 -        self.disturbed_quota = set
 -        self.save()
 +    def set_invitations_level(self):
 +        """
 +        Update user invitation level
 +        """
 +        level = self.invitation.inviter.level + 1
 +        self.level = level
 +        self.invitations = INVITATIONS_PER_LEVEL.get(level, 0)
 +
 +    def can_login_with_auth_provider(self, provider):
 +        if not self.has_auth_provider(provider):
 +            return False
 +        else:
 +            return auth_providers.get_provider(provider).is_available_for_login()
 +
 +    def can_add_provider(self, provider, **kwargs):
 +        provider_settings = auth_providers.get_provider(provider)
 +        if not provider_settings.is_available_for_login():
 +            return False
 +        if self.has_auth_provider(provider) and \
 +           provider_settings.one_per_user:
 +            return False
 +        return True
 +
 +    def can_remove_auth_provider(self, provider):
 +        if len(self.get_active_auth_providers()) <= 1:
 +            return False
 +        return True
 +
 +    def can_change_password(self):
 +        return self.has_auth_provider('local', auth_backend='astakos')
 +
 +    def has_auth_provider(self, provider, **kwargs):
 +        return bool(self.auth_providers.filter(module=provider,
 +                                               **kwargs).count())
 +
 +    def add_auth_provider(self, provider, **kwargs):
 +        self.auth_providers.create(module=provider, active=True, **kwargs)
 +
 +    def add_pending_auth_provider(self, pending):
 +        """
 +        Convert PendingThirdPartyUser object to AstakosUserAuthProvider entry for
 +        the current user.
 +        """
 +        if not isinstance(pending, PendingThirdPartyUser):
 +            pending = PendingThirdPartyUser.objects.get(token=pending)
 +
 +        provider = self.add_auth_provider(pending.provider,
 +                               identifier=pending.third_party_identifier)
 +
 +        if email_re.match(pending.email) and pending.email != self.email:
 +            self.additionalmail_set.get_or_create(email=pending.email)
 +
 +        pending.delete()
 +        return provider
 +
 +    def remove_auth_provider(self, provider, **kwargs):
 +        self.auth_providers.get(module=provider, **kwargs).delete()
 +
 +    # user urls
 +    def get_resend_activation_url(self):
 +        return reverse('send_activation', {'user_id': self.pk})
 +
 +    def get_activation_url(self, nxt=False):
 +        url = "%s?auth=%s" % (reverse('astakos.im.views.activate'),
 +                                 quote(self.auth_token))
 +        if nxt:
 +            url += "&next=%s" % quote(nxt)
 +        return url
 +
 +    def get_password_reset_url(self, token_generator=default_token_generator):
 +        return reverse('django.contrib.auth.views.password_reset_confirm',
 +                          kwargs={'uidb36':int_to_base36(self.id),
 +                                  'token':token_generator.make_token(self)})
 +
 +    def get_auth_providers(self):
 +        return self.auth_providers.all()
 +
 +    def get_available_auth_providers(self):
 +        """
 +        Returns a list of providers available for user to connect to.
 +        """
 +        providers = []
 +        for module, provider_settings in auth_providers.PROVIDERS.iteritems():
 +            if self.can_add_provider(module):
 +                providers.append(provider_settings(self))
 +
 +        return providers
 +
 +    def get_active_auth_providers(self):
 +        providers = []
 +        for provider in self.auth_providers.active():
 +            if auth_providers.get_provider(provider.module).is_available_for_login():
 +                providers.append(provider)
 +        return providers
 +
 +
 +class AstakosUserAuthProviderManager(models.Manager):
 +
 +    def active(self):
 +        return self.filter(active=True)
 +
 +
 +class AstakosUserAuthProvider(models.Model):
 +    """
 +    Available user authentication methods.
 +    """
 +    affiliation = models.CharField('Affiliation', max_length=255, blank=True,
 +                                   null=True, default=None)
 +    user = models.ForeignKey(AstakosUser, related_name='auth_providers')
 +    module = models.CharField('Provider', max_length=255, blank=False,
 +                                default='local')
 +    identifier = models.CharField('Third-party identifier',
 +                                              max_length=255, null=True,
 +                                              blank=True)
 +    active = models.BooleanField(default=True)
 +    auth_backend = models.CharField('Backend', max_length=255, blank=False,
 +                                   default='astakos')
 +
 +    objects = AstakosUserAuthProviderManager()
 +
 +    class Meta:
 +        unique_together = (('identifier', 'module', 'user'), )
 +
 +    @property
 +    def settings(self):
 +        return auth_providers.get_provider(self.module)
 +
 +    @property
 +    def details_display(self):
-         print self.settings.details_tpl
 +        return self.settings.details_tpl % self.__dict__
 +
 +    def can_remove(self):
 +        return self.user.can_remove_auth_provider(self.module)
 +
 +    def delete(self, *args, **kwargs):
 +        ret = super(AstakosUserAuthProvider, self).delete(*args, **kwargs)
 +        self.user.set_unusable_password()
 +        self.user.save()
 +        return ret
  
  
+ class Membership(models.Model):
+     person = models.ForeignKey(AstakosUser)
+     group = models.ForeignKey(AstakosGroup)
+     date_requested = models.DateField(default=datetime.now(), blank=True)
+     date_joined = models.DateField(null=True, db_index=True, blank=True)
+     class Meta:
+         unique_together = ("person", "group")
+     def save(self, *args, **kwargs):
+         if not self.id:
+             if not self.group.moderation_enabled:
+                 self.date_joined = datetime.now()
+         super(Membership, self).save(*args, **kwargs)
+     @property
+     def is_approved(self):
+         if self.date_joined:
+             return True
+         return False
+     def approve(self):
+         if self.is_approved:
+             return
+         if self.group.max_participants:
+             assert len(self.group.approved_members) + 1 <= self.group.max_participants, \
+             'Maximum participant number has been reached.'
+         self.date_joined = datetime.now()
+         self.save()
+         quota_disturbed.send(sender=self, users=(self.person,))
+     def disapprove(self):
+         self.delete()
+         quota_disturbed.send(sender=self, users=(self.person,))
+ class AstakosQuotaManager(models.Manager):
+     def _update_or_create(self, **kwargs):
+         assert kwargs, \
+             'update_or_create() must be passed at least one keyword argument'
+         obj, created = self.get_or_create(**kwargs)
+         defaults = kwargs.pop('defaults', {})
+         if created:
+             return obj, True, False
+         else:
+             try:
+                 params = dict(
+                     [(k, v) for k, v in kwargs.items() if '__' not in k])
+                 params.update(defaults)
+                 for attr, val in params.items():
+                     if hasattr(obj, attr):
+                         setattr(obj, attr, val)
+                 sid = transaction.savepoint()
+                 obj.save(force_update=True)
+                 transaction.savepoint_commit(sid)
+                 return obj, False, True
+             except IntegrityError, e:
+                 transaction.savepoint_rollback(sid)
+                 try:
+                     return self.get(**kwargs), False, False
+                 except self.model.DoesNotExist:
+                     raise e
+     update_or_create = _update_or_create
+ class AstakosGroupQuota(models.Model):
+     objects = AstakosQuotaManager()
+     limit = models.PositiveIntegerField('Limit', null=True)    # obsolete field
+     uplimit = models.BigIntegerField('Up limit', null=True)
+     resource = models.ForeignKey(Resource)
+     group = models.ForeignKey(AstakosGroup, blank=True)
+     class Meta:
+         unique_together = ("resource", "group")
+ class AstakosUserQuota(models.Model):
+     objects = AstakosQuotaManager()
+     limit = models.PositiveIntegerField('Limit', null=True)    # obsolete field
+     uplimit = models.BigIntegerField('Up limit', null=True)
+     resource = models.ForeignKey(Resource)
+     user = models.ForeignKey(AstakosUser)
+     class Meta:
+         unique_together = ("resource", "user")
  class ApprovalTerms(models.Model):
      """
      Model for approval terms
@@@ -591,7 -796,7 +958,7 @@@ class PendingThirdPartyUser(models.Mode
              self.last_name = parts[1]
          else:
              self.last_name = parts[0]
--    
++
      def save(self, **kwargs):
          if not self.id:
              # set username
@@@ -644,5 -908,21 +1075,21 @@@ def renew_token(sender, instance, **kwa
      if not instance.id:
          instance.renew_token()
  
+ post_syncdb.connect(fix_superusers)
+ post_save.connect(user_post_save, sender=User)
+ pre_save.connect(astakosuser_pre_save, sender=AstakosUser)
+ post_save.connect(astakosuser_post_save, sender=AstakosUser)
+ post_save.connect(resource_post_save, sender=Resource)
+ quota_disturbed = Signal(providing_args=["users"])
+ quota_disturbed.connect(on_quota_disturbed)
+ post_delete.connect(send_quota_disturbed, sender=AstakosGroup)
+ post_delete.connect(send_quota_disturbed, sender=Membership)
+ post_save.connect(send_quota_disturbed, sender=AstakosUserQuota)
+ post_delete.connect(send_quota_disturbed, sender=AstakosUserQuota)
+ post_save.connect(send_quota_disturbed, sender=AstakosGroupQuota)
+ post_delete.connect(send_quota_disturbed, sender=AstakosGroupQuota)
  pre_save.connect(renew_token, sender=AstakosUser)
--pre_save.connect(renew_token, sender=Service)
++pre_save.connect(renew_token, sender=Service)
@@@ -43,21 -42,20 +42,22 @@@ from django.core.urlresolvers import re
  from django.contrib.auth.decorators import login_required
  
  from astakos.im.util import prepare_response, get_query
- from astakos.im.views import requires_anonymous, signed_terms_required, \
-         requires_auth_provider
- from astakos.im.models import AstakosUser, PendingThirdPartyUser
- from astakos.im.forms import LoginForm, ExtendedPasswordChangeForm, \
-         ExtendedSetPasswordForm
+ from astakos.im.views import requires_anonymous, signed_terms_required
+ from astakos.im.models import PendingThirdPartyUser
+ from astakos.im.forms import LoginForm, ExtendedPasswordChangeForm
 -from astakos.im.settings import RATELIMIT_RETRIES_ALLOWED
 -from astakos.im.settings import ENABLE_LOCAL_ACCOUNT_MIGRATION
 -
 +from astakos.im.settings import (RATELIMIT_RETRIES_ALLOWED,
 +                                ENABLE_LOCAL_ACCOUNT_MIGRATION)
+ import astakos.im.messages as astakos_messages
++from astakos.im.views import requires_auth_provider
 +from astakos.im import settings
  
  from ratelimit.decorators import ratelimit
  
- retries = RATELIMIT_RETRIES_ALLOWED-1
- rate = str(retries)+'/m'
+ retries = RATELIMIT_RETRIES_ALLOWED - 1
+ rate = str(retries) + '/m'
  
 +@requires_auth_provider('local', login=True)
  @require_http_methods(["GET", "POST"])
  @csrf_exempt
  @requires_anonymous
@@@ -67,10 -65,12 +67,12 @@@ 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', '')
 -    username = get_query(request).get('key')
 -    
 +    third_party_token = get_query(request).get('key', False)
 +
      if not form.is_valid():
          return render_to_response(
              on_failure,
          )
      # get the user from the cash
      user = form.user_cache
-     
      message = None
      if not user:
-         message = _('Cannot authenticate account')
+         message = _(astakos_messages.ACCOUNT_AUTHENTICATION_FAILED)
      elif not user.is_active:
          if not user.activation_sent:
-             message = _('Your request is pending activation')
+             message = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION)
          else:
 -            send_activation_url = reverse('send_activation', kwargs={'user_id':user.id})
 -            message = _(astakos_messages.ACCOUNT_RESEND_ACTIVATION) % locals()
 -    elif user.provider not in ('local', ''):
++                      # TODO: USE astakos_messages
 +            url = reverse('send_activation', kwargs={'user_id':user.id})
 +            msg = _('You have not followed the activation link.')
 +            if settings.MODERATION_ENABLED:
 +                msg_extra = ' ' + _('Please contact support.')
 +            else:
 +                msg_extra = _('<a href="%s">Resend activation email?</a>') % url
 +
 +            message = msg + msg_extra
 +    elif not user.can_login_with_auth_provider('local'):
-         message = _(
-             'Local login is not the current authentication method for this account.'
-         )
+         message = _(astakos_messages.NO_LOCAL_AUTH)
 -    
 +
      if message:
          messages.error(request, message)
          return render_to_response(on_failure,
 -                                  {'login_form':form},
 +                                  {'login_form': form},
                                    context_instance=RequestContext(request))
 -    
 -    # hook for switching account to use third party authentication
 -    if ENABLE_LOCAL_ACCOUNT_MIGRATION and username:
 +
 +    response = prepare_response(request, user, next)
 +    if third_party_token:
 +        # use requests to assign the account he just authenticated with with
 +        # a third party provider account
++        # TODO: USE astakos_messages
          try:
 -            new = PendingThirdPartyUser.objects.get(
 -                username=username)
 -        except:
 -            messages.error(
 -                request,
 -                _(astakos_messages.SWITCH_ACCOUNT_FAILURE)
 -            )
 -            return render_to_response(
 -                on_failure,
 -                {'login_form':form,
 -                 'next':next},
 -                context_instance=RequestContext(request)
 -            )
 -        else:
 -            user.provider = new.provider
 -            user.third_party_identifier = new.third_party_identifier
 -            user.save()
 -            new.delete()
 -            messages.success(
 -                request,
 -                _(astakos_messages.SWITCH_ACCOUNT_SUCCESS_WITH_PROVIDER) % user.__dict__
 -            )
 -    return prepare_response(request, user, next)
 +          request.user.add_pending_auth_provider(third_party_token)
 +          messages.success(request, _('Your new login method has been added'))
 +        except PendingThirdPartyUser.DoesNotExist:
 +          messages.error(request, _('Account method assignment failed'))
 +
 +    return response
  
  @require_http_methods(["GET", "POST"])
  @signed_terms_required
@@@ -36,25 -36,19 +36,24 @@@ from django.utils.translation import ug
  from django.contrib import messages
  from django.template import RequestContext
  from django.views.decorators.http import require_http_methods
- from django.db.models import Q
- from django.core.exceptions import ValidationError
  from django.http import HttpResponseRedirect
  from django.core.urlresolvers import reverse
- from django.utils.http import urlencode
+ from django.core.exceptions import ImproperlyConfigured
 +from django.shortcuts import get_object_or_404
 +
 +from urlparse import urlunsplit, urlsplit
  
- 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 ENABLE_LOCAL_ACCOUNT_MIGRATION
 +from astakos.im.views import requires_anonymous, render_response, \
 +        requires_auth_provider
 +from astakos.im.settings import ENABLE_LOCAL_ACCOUNT_MIGRATION, BASEURL
  from astakos.im.models import AstakosUser, PendingThirdPartyUser
  from astakos.im.forms import LoginForm
 -from astakos.im.activation_backends import get_backend
 +from astakos.im.activation_backends import get_backend, SimpleBackend
 +from astakos.im import settings
  
+ import astakos.im.messages as astakos_messages
  import logging
  
  logger = logging.getLogger(__name__)
@@@ -70,11 -64,13 +69,12 @@@ class Tokens
      SHIB_SESSION_ID = "HTTP_SHIB_SESSION_ID"
      SHIB_MAIL = "HTTP_SHIB_MAIL"
  
 -
 +@requires_auth_provider('local', login=True)
  @require_http_methods(["GET", "POST"])
+ @requires_anonymous
  def login(
      request,
 -    login_template='im/login.html',
 -    signup_template='im/third_party_check_local.html',
 +    template='im/third_party_check_local.html',
      extra_context=None
  ):
      extra_context = extra_context or {}
          elif Tokens.SHIB_NAME in tokens and Tokens.SHIB_SURNAME in tokens:
              realname = tokens[Tokens.SHIB_NAME] + ' ' + tokens[Tokens.SHIB_SURNAME]
          else:
-             raise KeyError(_('Missing provider user information'))
+             raise KeyError(_(astakos_messages.SHIBBOLETH_MISSING_NAME))
      except KeyError, e:
 -        extra_context['login_form'] = LoginForm(request=request)
 -        messages.error(request, e)
 -        return render_response(
 -            login_template,
 -            context_instance=get_context(request, extra_context)
 -        )
 -    
 +        # invalid shibboleth headers, redirect to login, display message
-         messages.error(request, e)
++        messages.error(request, e.message)
 +        return HttpResponseRedirect(reverse('login'))
 +
      affiliation = tokens.get(Tokens.SHIB_EP_AFFILIATION, '')
      email = tokens.get(Tokens.SHIB_MAIL, '')
 -    
 +
 +    # an existing user accessed the view
 +    if request.user.is_authenticated():
 +        if request.user.has_auth_provider('shibboleth', identifier=eppn):
 +            return HttpResponseRedirect(reverse('edit_profile'))
 +
 +        # automatically add eppn provider to user
 +        user = request.user
 +        user.add_provider('shibboleth', identifier=eppn)
 +        return HttpResponseRedirect('edit_profile')
 +
      try:
 -        user = AstakosUser.objects.get(
 -            provider='shibboleth',
 -            third_party_identifier=eppn
 +        # astakos user exists ?
 +        user = AstakosUser.objects.get_auth_provider_user(
 +            'shibboleth',
 +            identifier=eppn
          )
          if user.is_active:
 +            # authenticate user
              return prepare_response(request,
                                      user,
                                      request.GET.get('next'),
                                      'renew' in request.GET)
          elif not user.activation_sent:
 -            message = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION)
 +            message = _('Your request is pending activation')
++                      #TODO: use astakos_messages
 +            if not settings.MODERATION_ENABLED:
 +                url = user.get_resend_activation_url()
 +                msg_extra = _('<a href="%s">Resend activation email?</a>') % url
 +                message = message + u' ' + msg_extra
 +
              messages.error(request, message)
 +            return HttpResponseRedirect(reverse('login'))
 +
          else:
 -            urls = {}
 -            urls['send_activation_url'] = reverse(
 -                'send_activation',
 -                kwargs={'user_id':user.id}
 -            )
 -            urls['signup_url'] = reverse(
 -                'shibboleth_signup',
 -                args= [user.username]
 -            )   
 -            message = _(astakos_messages.INACTIVE_ACCOUNT_CHANGE_EMAIL) % urls
++                      #TODO: use astakos_messages
 +            message = _(u'Account disabled. Please contact support')
              messages.error(request, message)
 -        return render_response(login_template,
 -                               login_form = LoginForm(request=request),
 -                               context_instance=RequestContext(request))
 +            return HttpResponseRedirect(reverse('login'))
 +
      except AstakosUser.DoesNotExist, e:
 -        # First time
 -        try:
 -            user, created = PendingThirdPartyUser.objects.get_or_create(
 -                third_party_identifier=eppn,
 -                provider='shibboleth',
 -                defaults=dict(
 -                    realname=realname,
 -                    affiliation=affiliation,
 -                    email=email
 -                )
 -            )
 -            user.save()
 -        except BaseException, e:
 -            logger.exception(e)
 -            template = login_template
 -            extra_context['login_form'] = LoginForm(request=request)
 -            messages.error(request, _(astakos_messages.GENERIC_ERROR))
 -        else:
 -            if not ENABLE_LOCAL_ACCOUNT_MIGRATION:
 -                url = reverse(
 -                    'shibboleth_signup',
 -                    args= [user.username]
 -                )
 -                return HttpResponseRedirect(url)
 -            else:
 -                template = signup_template
 -                extra_context['username'] = user.username
 -        
 -        extra_context['provider']='shibboleth'
++              #TODO: use astakos_messages
 +        # eppn not stored in astakos models, create pending profile
 +        user, created = PendingThirdPartyUser.objects.get_or_create(
 +            third_party_identifier=eppn,
 +            provider='shibboleth',
 +        )
 +        # update pending user
 +        user.realname = realname
 +        user.affiliation = affiliation
 +        user.email = email
 +        user.generate_token()
 +        user.save()
 +
 +        extra_context['provider'] = 'shibboleth'
 +        extra_context['token'] = user.token
 +
          return render_response(
              template,
              context_instance=get_context(request, extra_context)
  @requires_anonymous
  def signup(
      request,
 -    username,
 +    token,
      backend=None,
      on_creation_template='im/third_party_registration.html',
 -    extra_context=None
 -):
 +    extra_context=None):
 +
      extra_context = extra_context or {}
 -    if not username:
 -        return HttpResponseBadRequest(_(astakos_messages.MISSING_KEY_PARAMETER))
 -    try:
 -        pending = PendingThirdPartyUser.objects.get(username=username)
 -    except PendingThirdPartyUser.DoesNotExist:
 -        try:
 -            user = AstakosUser.objects.get(username=username)
 -        except AstakosUser.DoesNotExist:
 -            return HttpResponseBadRequest(_(astakos_messages.INVALID_KEY_PARAMETER))
 -    else:
 -        d = pending.__dict__
 -        d.pop('_state', None)
 -        d.pop('id', None)
 -        user = AstakosUser(**d)
 +    if not token:
++              #TODO: use astakos_messages
 +        return HttpResponseBadRequest(_('Missing key parameter.'))
 +
 +    pending = get_object_or_404(PendingThirdPartyUser, token=token)
 +    d = pending.__dict__
 +    d.pop('_state', None)
 +    d.pop('id', None)
 +    d.pop('token', None)
 +    d.pop('created', None)
 +    user = AstakosUser(**d)
 +
      try:
          backend = backend or get_backend(request)
      except ImproperlyConfigured, e:
          <input type="submit" class="submit altcol" value="UPDATE" />
      </div>
  
 +    <div class="auth_methods">
 +      <br /><br />
 +        <div class="assigned">
 +          <h4>Authentication methods</h4>
 +          <p>You can login to your account using the following methods</p>
 +          <ul class="auth_providers">
 +            {% for provider in user_providers %}
 +            <li>
 +            <h2>
 +                {{ provider.settings.title }}
 +                <span class="actions" style="margin-left: 40px">
 +                  {% for name, url in provider.settings.extra_actions %}
 +                  <a href="{{ url }}" title="{{ name }}">{{ name }}</a>
 +                  {% endfor %}
 +                  {% if provider.can_remove %}
 +                      <a href="{% url remove_auth_provider provider.pk %}" title="disble">Remove</a>
 +                  {% endif %}
 +                </span>
 +            </h2>
 +            <p>{{ provider.details_display }}</p>
 +            <br />
 +            </li>
 +            {% empty %}
 +            <li>No available authentication methods</li>
 +            {% endfor %}
 +          </ul>
 +        </div>
 +        <div class="notassigned">
 +          <p>You can add the following authentication methods to your account </p>
 +          <ul class="auth_providers">
 +            {% for provider in user_available_providers %}
 +            <li>
 +            <h2><a href="{{ provider.add_url }}">{{ provider.title }}</a></h2>
 +            <p>{{ provider.add_description }}</p>
 +            <br />
 +            </li>
 +            {% empty %}
 +            No available providers.
 +            {% endfor %}
 +          </ul>
 +        </div>
 +    </div>
 +
  </form>
+ <div class="two-cols-links">
+       <p><a href="{% url password_change %}">Change Password</a></p>
+       <p>
+               <a href="https://okeanos.grnet.gr/home/">Back to ~okeanos</a>
+               <a href="https://cyclades.okeanos.grnet.gr/ui/">Take me to cyclades</a>
+               <a href="https://pithos.okeanos.grnet.gr/ui/">Take me to pithos+</a>
+       </p>
+ </div>
  {% endblock body %}
index e464bdf,0000000..dc4cb6b
mode 100644,000000..100644
--- /dev/null
@@@ -1,423 -1,0 +1,432 @@@
 +# Copyright 2011 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
 +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
 +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
 +# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
 +# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 +# 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
 +# or implied, of GRNET S.A.
 +
 +import datetime
 +
 +from django.test import TestCase, Client
 +from django.conf import settings
 +from django.core import mail
 +
 +from astakos.im.target.shibboleth import Tokens as ShibbolethTokens
 +from astakos.im.models import *
 +from astakos.im import functions
 +from astakos.im import settings as astakos_settings
 +
 +from urllib import quote
 +
++from astakos.im import messages
++
 +class ShibbolethClient(Client):
 +    """
 +    A shibboleth agnostic client.
 +    """
 +    VALID_TOKENS = filter(lambda x: not x.startswith("_"), dir(ShibbolethTokens))
 +
 +    def __init__(self, *args, **kwargs):
 +        self.tokens = kwargs.pop('tokens', {})
 +        super(ShibbolethClient, self).__init__(*args, **kwargs)
 +
 +    def set_tokens(self, **kwargs):
 +        for key, value in kwargs.iteritems():
 +            key = 'SHIB_%s' % key.upper()
 +            if not key in self.VALID_TOKENS:
 +                raise Exception('Invalid shibboleth token')
 +
 +            self.tokens[key] = value
 +
 +    def unset_tokens(self, *keys):
 +        for key in keys:
 +            key = 'SHIB_%s' % param.upper()
 +            if key in self.tokens:
 +                del self.tokens[key]
 +
 +    def reset_tokens(self):
 +        self.tokens = {}
 +
 +    def get_http_token(self, key):
 +        http_header = getattr(ShibbolethTokens, key)
 +        return http_header
 +
 +    def request(self, **request):
 +        """
 +        Transform valid shibboleth tokens to http headers
 +        """
 +        for token, value in self.tokens.iteritems():
 +            request[self.get_http_token(token)] = value
 +
 +        for param in request.keys():
 +            key = 'SHIB_%s' % param.upper()
 +            if key in self.VALID_TOKENS:
 +                request[self.get_http_token(key)] = request[param]
 +                del request[param]
 +
 +        return super(ShibbolethClient, self).request(**request)
 +
 +
 +def get_local_user(username, **kwargs):
 +        try:
 +            return AstakosUser.objects.get(email=username)
 +        except:
 +            user_params = {
 +                'username': username,
 +                'email': username,
 +                'is_active': True,
 +                'activation_sent': datetime.now(),
 +                'email_verified': True,
 +                'provider': 'local'
 +            }
 +            user_params.update(kwargs)
 +            user = AstakosUser(**user_params)
 +            user.set_password(kwargs.get('password', 'password'))
 +            user.save()
 +            user.add_auth_provider('local', auth_backend='astakos')
 +            if kwargs.get('is_active', True):
 +                user.is_active = True
 +            else:
 +                user.is_active = False
 +            user.save()
 +            return user
 +
 +
 +def get_mailbox(email):
 +    mails = []
 +    for sent_email in mail.outbox:
 +        for recipient in sent_email.recipients():
 +            if email in recipient:
 +                mails.append(sent_email)
 +    return mails
 +
 +
 +class ShibbolethTests(TestCase):
 +    """
 +    Testing shibboleth authentication.
 +    """
 +
 +    fixtures = ['groups']
 +
 +    def setUp(self):
 +        self.client = ShibbolethClient()
 +        settings.ASTAKOS_IM_MODULES = ['local', 'shibboleth']
 +
 +    def test_create_account(self):
 +        client = ShibbolethClient()
 +
 +        # shibboleth views validation
 +        # eepn required
 +        r = client.get('/im/login/shibboleth?', follow=True)
-         self.assertContains(r, 'Missing provider token')
++        self.assertContains(r, messages.SHIBBOLETH_MISSING_EPPN)
 +        client.set_tokens(eppn="kpapeppn")
 +        # shibboleth user info required
 +        r = client.get('/im/login/shibboleth?', follow=True)
-         self.assertContains(r, 'Missing provider user information')
++        self.assertContains(r, messages.SHIBBOLETH_MISSING_NAME)
 +
 +        # shibboleth logged us in
 +        client.set_tokens(mail="kpap@grnet.gr", eppn="kpapeppn", cn="1", )
 +        r = client.get('/im/login/shibboleth?')
 +
 +        # astakos asks if we want to add shibboleth
 +        self.assertContains(r, "Already have an account?")
 +
 +        # a new pending user created
 +        pending_user = PendingThirdPartyUser.objects.get(
 +            third_party_identifier="kpapeppn")
 +        self.assertEqual(PendingThirdPartyUser.objects.count(), 1)
 +        token = pending_user.token
 +        # from now on no shibboleth headers are sent to the server
 +        client.reset_tokens()
 +
 +        # we choose to signup as a new user
 +        r = client.get('/im/shibboleth/signup/%s' % pending_user.username)
 +        self.assertEqual(r.status_code, 404)
 +
 +        r = client.get('/im/shibboleth/signup/%s' % token)
 +        form = r.context['form']
 +        post_data = {'email': 'kpap@grnet.gr',
 +                     'third_party_identifier': pending_user.third_party_identifier,
 +                     'first_name': 'Kostas',
 +                     'third_party_token': token,
 +                     'last_name': 'Mitroglou',
 +                     'additional_email': 'kpap@grnet.gr',
 +                     'provider': 'shibboleth'
 +                    }
 +        r = client.post('/im/signup', post_data)
 +        self.assertEqual(r.status_code, 200)
 +        self.assertEqual(AstakosUser.objects.count(), 1)
 +        self.assertEqual(PendingThirdPartyUser.objects.count(), 0)
 +        self.assertEqual(AstakosUserAuthProvider.objects.count(), 1)
 +
 +
 +        client.set_tokens(mail="kpap@grnet.gr", eppn="kpapeppn", cn="1", )
 +        r = client.get("/im/login/shibboleth?", follow=True)
 +        self.assertContains(r, "Your request is pending activation")
 +        r = client.get("/im/profile", follow=True)
 +        self.assertRedirects(r, 'http://testserver/im/?next=%2Fim%2Fprofile')
 +
 +        u = AstakosUser.objects.get()
 +        functions.activate(u)
 +        self.assertEqual(u.is_active, True)
 +
 +        r = client.get("/im/login/shibboleth?")
 +        self.assertRedirects(r, '/im/profile')
 +
 +    def test_existing(self):
 +        existing_user = get_local_user('kpap@grnet.gr')
 +
 +        client = ShibbolethClient()
 +        # shibboleth logged us in, notice that we use different email
 +        client.set_tokens(mail="kpap@shibboleth.gr", eppn="kpapeppn", cn="1", )
 +        r = client.get("/im/login/shibboleth?")
 +        # astakos asks if we want to switch a local account to shibboleth
 +        self.assertContains(r, "Already have an account?")
 +
 +        # a new pending user created
 +        pending_user = PendingThirdPartyUser.objects.get()
 +        self.assertEqual(PendingThirdPartyUser.objects.count(), 1)
 +        pending_key = pending_user.token
 +        client.reset_tokens()
 +
 +        # we choose to add shibboleth to an our existing account
 +        # we get redirected to login page with the pending token set
 +        r = client.get('/im/login?key=%s' % pending_key)
 +        post_data = {'password': 'password',
 +                     'username': 'kpap@grnet.gr',
 +                     'key': pending_key}
 +        r = client.post('/im/local', post_data, follow=True)
 +        self.assertContains(r, "Your new login method has been added")
 +
 +        user = AstakosUser.objects.get(username="kpap@grnet.gr",
 +                                       email="kpap@grnet.gr")
 +        self.assertTrue(user.has_auth_provider('shibboleth'))
 +        self.assertTrue(user.has_auth_provider('local', auth_backend='astakos'))
 +        client.logout()
 +
 +        # again ???? show her a message
 +        r = client.get('/im/login?key=%s' % pending_key)
 +        post_data = {'password': 'password',
 +                     'username': 'kpap@grnet.gr',
 +                     'key': pending_key}
 +        r = self.client.post('/im/local', post_data, follow=True)
 +        self.assertContains(r, "Account method assignment failed")
 +        self.client.logout()
 +        client.logout()
 +
 +        # look Ma, i can login with both my shibboleth and local account
 +        client.set_tokens(mail="kpap@shibboleth.gr", eppn="kpapeppn", cn="1")
 +        r = client.get("/im/login/shibboleth?", follow=True)
 +        self.assertTrue(r.context['request'].user.is_authenticated())
 +        self.assertTrue(r.context['request'].user.email == "kpap@grnet.gr")
 +        r = client.get("/im/profile")
 +        self.assertEquals(r.status_code,200)
 +        client.logout()
 +        client.reset_tokens()
 +        r = client.get("/im/profile", follow=True)
 +        self.assertFalse(r.context['request'].user.is_authenticated())
 +
 +        post_data = {'password': 'password',
 +                     'username': 'kpap@grnet.gr'}
 +        r = self.client.post('/im/local', post_data, follow=True)
 +        self.assertTrue(r.context['request'].user.is_authenticated())
 +        r = self.client.get("/im/profile")
 +        self.assertEquals(r.status_code,200)
 +
 +        r = client.post('/im/local', post_data, follow=True)
 +        client.set_tokens(mail="secondary@shibboleth.gr", eppn="kpapeppn", cn="1", )
 +        r = client.get("/im/login/shibboleth?", follow=True)
 +        client.reset_tokens()
 +
 +        client.logout()
 +        client.set_tokens(mail="kpap@grnet.gr", eppn="kpapeppninvalid", cn="1")
 +        r = client.get("/im/login/shibboleth?", follow=True)
 +        self.assertFalse(r.context['request'].user.is_authenticated())
 +
++        user2 = get_local_user('kpap@grnet.gr')
++
 +
 +class LocalUserTests(TestCase):
 +
 +    fixtures = ['groups']
 +
++    def setUp(self):
++        from django.conf import settings
++        settings.ADMINS = (('admin', 'support@cloud.grnet.gr'),)
++        settings.SERVER_EMAIL = 'no-reply@grnet.gr'
++
 +    def test_invitations(self):
 +        return
 +
 +    def test_local_provider(self):
 +        r = self.client.get("/im/signup")
 +        self.assertEqual(r.status_code, 200)
 +
 +        data = {'email':'kpap@grnet.gr', 'password1':'password',
 +                'password2':'password', 'first_name': 'Kostas',
 +                'last_name': 'Mitroglou', 'provider': 'local'}
 +        r = self.client.post("/im/signup", data)
 +        self.assertEqual(AstakosUser.objects.count(), 1)
 +        user = AstakosUser.objects.get(username="kpap@grnet.gr",
 +                                       email="kpap@grnet.gr")
 +        self.assertEqual(user.username, 'kpap@grnet.gr')
 +        self.assertEqual(user.has_auth_provider('local'), True)
 +        self.assertFalse(user.is_active)
 +
 +        # admin gets notified
 +        self.assertEqual(len(get_mailbox('support@cloud.grnet.gr')), 1)
 +        # and sends user activation email
 +        functions.send_activation(user)
 +
 +        # user activation fields updated
 +        user = AstakosUser.objects.get(pk=user.pk)
 +        self.assertTrue(user.activation_sent)
 +        self.assertFalse(user.email_verified)
 +        # email sent to user
 +        self.assertEqual(len(get_mailbox('kpap@grnet.gr')), 1)
 +
 +        # user forgot she got registered and tries to submit registration
 +        # form. Notice the upper case in email
 +        data = {'email':'KPAP@grnet.gr', 'password1':'password',
 +                'password2':'password', 'first_name': 'Kostas',
 +                'last_name': 'Mitroglou', 'provider': 'local'}
 +        r = self.client.post("/im/signup", data)
-         self.assertContains(r, "This email is already used")
++        self.assertContains(r, messages.EMAIL_USED)
 +
 +        # hmmm, email exists; lets get the password
 +        r = self.client.get('/im/local/password_reset')
 +        self.assertEqual(r.status_code, 200)
 +        r = self.client.post('/im/local/password_reset', {'email':
 +                                                          'kpap@grnet.gr'})
 +        # she can't because account is not active yet
 +        self.assertContains(r, "doesn&#39;t have an associated user account")
 +
 +        # moderation is enabled so no automatic activation can be send
 +        r = self.client.get('/im/send/activation/%d' % user.pk)
 +        self.assertEqual(r.status_code, 403)
 +        self.assertEqual(len(get_mailbox('kpap@grnet.gr')), 1)
 +        # also she cannot login
 +        r = self.client.post('/im/local', {'username': 'kpap@grnet.gr',
 +                                                 'password': 'password'})
 +        self.assertContains(r, 'You have not followed the activation link')
 +        self.assertNotContains(r, 'Resend activation')
 +        self.assertFalse(r.context['request'].user.is_authenticated())
 +        self.assertFalse('_pithos2_a' in self.client.cookies)
 +
 +        # lets disable moderation
 +        astakos_settings.MODERATION_ENABLED = False
 +        r = self.client.post('/im/local/password_reset', {'email':
 +                                                          'kpap@grnet.gr'})
 +        self.assertContains(r, "doesn&#39;t have an associated user account")
 +        r = self.client.post('/im/local', {'username': 'kpap@grnet.gr',
 +                                                 'password': 'password'})
 +        self.assertContains(r, 'You have not followed the activation link')
 +        self.assertContains(r, 'Resend activation')
 +        self.assertFalse(r.context['request'].user.is_authenticated())
 +        self.assertFalse('_pithos2_a' in self.client.cookies)
 +        # user sees the message and resends activation
 +        r = self.client.get('/im/send/activation/%d' % user.pk)
 +        # email sent
 +        self.assertEqual(len(get_mailbox('kpap@grnet.gr')), 2)
 +
 +        # switch back moderation setting
 +        astakos_settings.MODERATION_ENABLED = True
 +        # lets activate the user
 +        r = self.client.get(user.get_activation_url(), follow=True)
 +        self.assertRedirects(r, "/im/profile")
 +        self.assertContains(r, "kpap@grnet.gr")
 +        self.assertEqual(len(get_mailbox('kpap@grnet.gr')), 3)
 +
 +        user = AstakosUser.objects.get(pk=user.pk)
 +        # user activated and logged in, token cookie set
 +        self.assertTrue(r.context['request'].user.is_authenticated())
 +        self.assertTrue('_pithos2_a' in self.client.cookies)
 +        cookies = self.client.cookies
 +        self.assertTrue(quote(user.auth_token) in cookies.get('_pithos2_a').value)
 +        r = self.client.get('/im/logout', follow=True)
 +        r = self.client.get('/im/')
 +        # user logged out, token cookie removed
 +        self.assertFalse(r.context['request'].user.is_authenticated())
 +        self.assertFalse(self.client.cookies.get('_pithos2_a').value)
 +        # https://docs.djangoproject.com/en/dev/topics/testing/#persistent-state
 +        del self.client.cookies['_pithos2_a']
 +
 +        # user can login
 +        r = self.client.post('/im/local', {'username': 'kpap@grnet.gr',
 +                                           'password': 'password'},
 +                                          follow=True)
 +        self.assertTrue(r.context['request'].user.is_authenticated())
 +        self.assertTrue('_pithos2_a' in self.client.cookies)
 +        cookies = self.client.cookies
 +        self.assertTrue(quote(user.auth_token) in cookies.get('_pithos2_a').value)
 +        self.client.get('/im/logout', follow=True)
 +
 +        # user forgot password
 +        old_pass = user.password
 +        r = self.client.get('/im/local/password_reset')
 +        self.assertEqual(r.status_code, 200)
 +        r = self.client.post('/im/local/password_reset', {'email':
 +                                                          'kpap@grnet.gr'})
 +        self.assertEqual(r.status_code, 302)
 +        # email sent
 +        self.assertEqual(len(get_mailbox('kpap@grnet.gr')), 4)
 +
 +        # user visits change password link
 +        r = self.client.get(user.get_password_reset_url())
 +        r = self.client.post(user.get_password_reset_url(),
 +                            {'new_password1':'newpass',
 +                             'new_password2':'newpass'})
 +
 +        user = AstakosUser.objects.get(pk=user.pk)
 +        self.assertNotEqual(old_pass, user.password)
 +
 +        # old pass is not usable
 +        r = self.client.post('/im/local', {'username': 'kpap@grnet.gr',
 +                                           'password': 'password'})
 +        self.assertContains(r, 'Please enter a correct username and password')
 +        r = self.client.post('/im/local', {'username': 'kpap@grnet.gr',
 +                                           'password': 'newpass'},
 +                                           follow=True)
 +        self.assertTrue(r.context['request'].user.is_authenticated())
 +        self.client.logout()
 +
 +        # tests of special local backends
 +        user = AstakosUser.objects.get(pk=user.pk)
 +        user.auth_providers.filter(module='local').update(auth_backend='ldap')
 +        user.save()
 +
 +        # non astakos local backends do not support password reset
 +        r = self.client.get('/im/local/password_reset')
 +        self.assertEqual(r.status_code, 200)
 +        r = self.client.post('/im/local/password_reset', {'email':
 +                                                          'kpap@grnet.gr'})
 +        # she can't because account is not active yet
 +        self.assertContains(r, "Password change for this account is not"
 +                                " supported")
 +
@@@ -42,19 -41,31 +41,33 @@@ from astakos.im.settings import IM_MODU
  urlpatterns = patterns('astakos.im.views',
      url(r'^$', 'index', {}, name='index'),
      url(r'^login/?$', 'index', {}, name='login'),
-     url(r'^profile/?$', 'edit_profile', name='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'^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'^send/activation/(?P<user_id>\d+)/?$', 'send_activation', {},
-         name='send_activation'),
+     url(r'^send/activation/(?P<user_id>\d+)/?$', 'send_activation', {}, name='send_activation'),
+     url(r'^resources/?$', 'resource_list', {}, name='resource_list'),
+     url(r'^billing/?$', 'billing', {}, name='billing'),
+     url(r'^timeline/?$', 'timeline', {}, name='timeline'),
+     url(r'^group/add/complete/?$', 'group_add_complete', {}, name='group_add_complete'),
+     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')
++    url(r'^group/create/?$', 'group_create_list', {}, name='group_create_list'),
 +    url(r'^remove_auth_provider/(?P<pk>\d+)?$', 'remove_auth_provider', {},
 +        name='remove_auth_provider')
  )
  
  if EMAILCHANGE_ENABLED:
      urlpatterns += patterns('astakos.im.views',
          url(r'^email_change/?$', 'change_email', {}, name='email_change'),
  # or implied, of GRNET S.A.
  
  import logging
- import socket
+ import calendar
+ import inflect
+ engine = inflect.engine()
  
- from smtplib import SMTPException
  from urllib import quote
  from functools import wraps
+ from datetime import datetime
  
- from django.core.mail import send_mail
- from django.http import (
-     HttpResponse, HttpResponseBadRequest, HttpResponseRedirect
- )
- 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.contrib.auth.decorators import login_required
+ from django.core.urlresolvers import reverse
  from django.db import transaction
- from django.utils.http import urlencode
  from django.db.utils import IntegrityError
- from django.contrib.auth.views import password_change
+ 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 (delete_object,
+                                                 get_model_and_form_class)
+ from django.views.generic.list_detail import object_list
+ from django.core.xheaders import populate_xheaders
 +from django.core.exceptions import ValidationError, PermissionDenied
- from django.views.decorators.http import require_http_methods
  
- from astakos.im.models import AstakosUser, Invitation, ApprovalTerms
+ from django.template.loader import render_to_string
+ from django.views.decorators.http import require_http_methods
  from astakos.im.activation_backends import get_backend, SimpleBackend
- from astakos.im.util import (
-     get_context, prepare_response, get_query, restrict_next
- )
- 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,
-     send_activation as send_activation_func
- )
- from astakos.im.settings import (
-     DEFAULT_CONTACT_EMAIL, DEFAULT_FROM_EMAIL, COOKIE_DOMAIN, IM_MODULES,
-     SITENAME, LOGOUT_NEXT, LOGGING_LEVEL
- )
+ from astakos.im.models import (AstakosUser, ApprovalTerms, AstakosGroup,
+                                EmailChange, GroupKind, Membership,
+                                RESOURCE_SEPARATOR)
+ from astakos.im.util import get_context, prepare_response, get_query, restrict_next
+ from astakos.im.forms import (LoginForm, InvitationForm, ProfileForm,
+                               FeedbackForm, SignApprovalTermsForm,
+                               EmailChangeForm,
+                               AstakosGroupCreationForm, AstakosGroupSearchForm,
+                               AstakosGroupUpdateForm, AddGroupMembersForm,
+                               MembersSortForm,
+                               TimelineForm, PickResourceForm,
+                               AstakosGroupCreationSummaryForm)
+ from astakos.im.functions import (send_feedback, SendMailError,
+                                   logout as auth_logout,
+                                   activate as activate_func,
+                                   send_activation as send_activation_func,
+                                   send_group_creation_notification,
+                                   SendNotificationError)
+ from astakos.im.endpoints.qh import timeline_charge
+ from astakos.im.settings import (COOKIE_DOMAIN, LOGOUT_NEXT,
+                                  LOGGING_LEVEL, PAGINATE_BY, RESOURCES_PRESENTATION_DATA, PAGINATE_BY_ALL)
+ from astakos.im.tasks import request_billing
+ from astakos.im.api.callpoint import AstakosCallpoint
+ import astakos.im.messages as astakos_messages
 +from astakos.im import settings
 +from astakos.im import auth_providers
  
  logger = logging.getLogger(__name__)
  
@@@ -165,7 -169,7 +192,7 @@@ def index(request, login_template_name=
      template_name = login_template_name
      if request.user.is_authenticated():
          return HttpResponseRedirect(reverse('astakos.im.views.edit_profile'))
--    
++
      return render_response(
          template_name,
          login_form = LoginForm(request=request),
@@@ -310,24 -314,16 +337,24 @@@ def edit_profile(request, template_name
                  )
                  if next:
                      return redirect(next)
-                 msg = _('<p>Profile has been updated successfully</p>')
-                 messages.add_message(request, messages.SUCCESS, msg)
+                 msg = _(astakos_messages.PROFILE_UPDATED)
+                 messages.success(request, msg)
              except ValueError, ve:
-                 messages.add_message(request, messages.ERROR, ve)
+                 messages.success(request, ve)
      elif request.method == "GET":
 -        if not request.user.is_verified:
 -            request.user.is_verified = True
 -            request.user.save()
 +        request.user.is_verified = True
 +        request.user.save()
 +
 +    # existing providers
 +    user_providers = request.user.get_active_auth_providers()
 +
 +    # providers that user can add
 +    user_available_providers = request.user.get_available_auth_providers()
 +
      return render_response(template_name,
                             profile_form = form,
 +                           user_providers = user_providers,
 +                           user_available_providers = user_available_providers,
                             context_instance = get_context(request,
                                                            extra_context))
  
@@@ -369,12 -367,9 +398,12 @@@ def signup(request, template_name='im/s
      """
      extra_context = extra_context or {}
      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')
 +    if not auth_providers.get_provider(provider).is_available_for_create():
 +        raise PermissionDenied
 +
      id = get_query(request).get('id')
      try:
          instance = AstakosUser.objects.get(id=id) if id else None
@@@ -677,16 -677,691 +717,703 @@@ def send_activation(request, user_id, t
          )
      )
  
+ class ResourcePresentation():
 -    
++
+     def __init__(self, data):
+         self.data = data
 -        
++
+     def update_from_result(self, result):
+         if result.is_success:
+             for r in result.data:
+                 rname = '%s%s%s' % (r.get('service'), RESOURCE_SEPARATOR, r.get('name'))
+                 if not rname in self.data['resources']:
+                     self.data['resources'][rname] = {}
 -                    
++
+                 self.data['resources'][rname].update(r)
+                 self.data['resources'][rname]['id'] = rname
+                 group = r.get('group')
+                 if not group in self.data['groups']:
+                     self.data['groups'][group] = {}
 -                    
++
+                 self.data['groups'][r.get('group')].update({'name': r.get('group')})
 -    
++
+     def test(self, quota_dict):
+         for k, v in quota_dict.iteritems():
+             rname = k
+             value = v
+             if not rname in self.data['resources']:
+                 self.data['resources'][rname] = {}
 -                    
 - 
++
++
+             self.data['resources'][rname]['value'] = value
 -            
 -    
++
++
+     def update_from_result_report(self, result):
+         if result.is_success:
+             for r in result.data:
+                 rname = r.get('name')
+                 if not rname in self.data['resources']:
+                     self.data['resources'][rname] = {}
 -                    
++
+                 self.data['resources'][rname].update(r)
+                 self.data['resources'][rname]['id'] = rname
+                 group = r.get('group')
+                 if not group in self.data['groups']:
+                     self.data['groups'][group] = {}
 -                    
++
+                 self.data['groups'][r.get('group')].update({'name': r.get('group')})
 -                
++
+     def get_group_resources(self, group):
+         return dict(filter(lambda t: t[1].get('group') == group, self.data['resources'].iteritems()))
 -    
++
+     def get_groups_resources(self):
+         for g in self.data['groups']:
+             yield g, self.get_group_resources(g)
 -    
++
+     def get_quota(self, group_quotas):
+         for r, v in group_quotas.iteritems():
+             rname = str(r)
+             quota = self.data['resources'].get(rname)
+             quota['value'] = v
+             yield quota
 -    
 -    
++
++
+     def get_policies(self, policies_data):
+         for policy in policies_data:
+             rname = '%s%s%s' % (policy.get('service'), RESOURCE_SEPARATOR, policy.get('resource'))
+             policy.update(self.data['resources'].get(rname))
+             yield policy
 -        
++
+     def __repr__(self):
+         return self.data.__repr__()
 -                
++
+     def __iter__(self, *args, **kwargs):
+         return self.data.__iter__(*args, **kwargs)
 -    
++
+     def __getitem__(self, *args, **kwargs):
+         return self.data.__getitem__(*args, **kwargs)
 -    
++
+     def get(self, *args, **kwargs):
+         return self.data.get(*args, **kwargs)
 -        
 -        
++
++
+ @require_http_methods(["GET", "POST"])
+ @signed_terms_required
+ @login_required
+ def group_add(request, kind_name='default'):
 -    
++
+     result = callpoint.list_resources()
+     resource_catalog = ResourcePresentation(RESOURCES_PRESENTATION_DATA)
+     resource_catalog.update_from_result(result)
 -    
++
+     if not result.is_success:
+         messages.error(
+             request,
+             'Unable to retrieve system resources: %s' % result.reason
+     )
 -    
++
+     try:
+         kind = GroupKind.objects.get(name=kind_name)
+     except:
+         return HttpResponseBadRequest(_(astakos_messages.GROUPKIND_UNKNOWN))
 -    
 -    
++
++
+     post_save_redirect = '/im/group/%(id)s/'
+     context_processors = None
+     model, form_class = get_model_and_form_class(
+         model=None,
+         form_class=AstakosGroupCreationForm
+     )
 -    
++
+     if request.method == 'POST':
+         form = form_class(request.POST, request.FILES)
+         if form.is_valid():
+             return render_response(
+                 template='im/astakosgroup_form_summary.html',
+                 context_instance=get_context(request),
+                 form = AstakosGroupCreationSummaryForm(form.cleaned_data),
+                 policies = resource_catalog.get_policies(form.policies()),
+                 resource_catalog= resource_catalog,
+             )
 -         
++
+     else:
+         now = datetime.now()
+         data = {
+             'kind': kind,
+         }
+         for group, resources in resource_catalog.get_groups_resources():
+             data['is_selected_%s' % group] = False
+             for resource in resources:
+                 data['%s_uplimit' % resource] = ''
 -        
++
+         form = form_class(data)
+     # 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,
+         'resource_catalog':resource_catalog,
+     }, context_processors)
+     return HttpResponse(t.render(c))
+ #@require_http_methods(["POST"])
+ @require_http_methods(["GET", "POST"])
+ @signed_terms_required
+ @login_required
+ def group_add_complete(request):
+     model = AstakosGroup
+     form = AstakosGroupCreationSummaryForm(request.POST)
+     if form.is_valid():
+         d = form.cleaned_data
+         d['owners'] = [request.user]
+         result = callpoint.create_groups((d,)).next()
+         if result.is_success:
+             new_object = result.data[0]
+             msg = _(astakos_messages.OBJECT_CREATED) %\
+                 {"verbose_name": model._meta.verbose_name}
+             messages.success(request, msg, fail_silently=True)
 -            
++
+             # send notification
+             try:
+                 send_group_creation_notification(
+                     template_name='im/group_creation_notification.txt',
+                     dictionary={
+                         'group': new_object,
+                         'owner': request.user,
+                         'policies': d.get('policies', [])
+                     }
+                 )
+             except SendNotificationError, e:
+                 messages.error(request, e, fail_silently=True)
+             post_save_redirect = '/im/group/%(id)s/'
+             return HttpResponseRedirect(post_save_redirect % new_object)
+         else:
+             d = {"verbose_name": model._meta.verbose_name,
+                  "reason":result.reason}
 -            msg = _(astakos_messages.OBJECT_CREATED_FAILED) % d 
++            msg = _(astakos_messages.OBJECT_CREATED_FAILED) % d
+             messages.error(request, msg, fail_silently=True)
+     return render_response(
+         template='im/astakosgroup_form_summary.html',
+         context_instance=get_context(request),
+         form=form)
+ #@require_http_methods(["GET"])
+ @require_http_methods(["GET", "POST"])
+ @signed_terms_required
+ @login_required
+ def group_list(request):
+     none = request.user.astakos_groups.none()
+     sorting = request.GET.get('sorting')
+     query = """
+         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 
++        WHERE im_membership.person_id = %s
+         """ % (DB_REPLACE_GROUP_SCHEME, request.user.id, request.user.id)
 -       
++
+     if sorting:
 -        query = query+" ORDER BY %s ASC" %sorting    
++        query = query+" ORDER BY %s ASC" %sorting
+     else:
 -        query = query+" ORDER BY groupname ASC"     
++        query = query+" ORDER BY groupname ASC"
+     q = AstakosGroup.objects.raw(query)
 -       
 -       
++
++
+     # Create the template, context, response
+     template_name = "%s/%s_list.html" % (
+         q.model._meta.app_label,
+         q.model._meta.object_name.lower()
+     )
+     extra_context = dict(
+         is_search=False,
+         q=q,
+         sorting=request.GET.get('sorting'),
+         page=request.GET.get('page', 1)
+     )
+     return render_response(template_name,
+                            context_instance=get_context(request, extra_context)
+     )
+ @require_http_methods(["GET", "POST"])
+ @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():
+             try:
+                 map(obj.approve_member, addmembers_form.valid_users)
+             except AssertionError:
+                 msg = _(astakos_messages.GROUP_MAX_PARTICIPANT_NUMBER_REACHED)
+                 messages.error(request, msg)
+             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')
 -    
++
+     else:
+         form = MembersSortForm({'sort_by': 'person_first_name'})
 -    
++
+     result = callpoint.list_resources()
+     resource_catalog = ResourcePresentation(RESOURCES_PRESENTATION_DATA)
+     resource_catalog.update_from_result(result)
 -    
++
+     if not result.is_success:
+         messages.error(
+             request,
+             'Unable to retrieve system resources: %s' % result.reason
+     )
 -    
++
+     extra_context = {'update_form': update_form,
+                      'addmembers_form': addmembers_form,
+                      'page': request.GET.get('page', 1),
+                      'sorting': sorting,
+                      'resource_catalog':resource_catalog,
+                      'quota':resource_catalog.get_quota(obj.quota)}
+     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
+ @require_http_methods(["GET", "POST"])
+ @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,
+                     '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, 
++                        THEN 1 ELSE 0 END""" % request.user.id,
+                     })
+         if sorting:
+             # TODO check sorting value
+             queryset = queryset.order_by(sorting)
+         else:
+             queryset = queryset.order_by("groupname")
+     else:
+         queryset = AstakosGroup.objects.none()
+     return object_list(
+         request,
+         queryset,
+         paginate_by=PAGINATE_BY_ALL,
+         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))
+ @require_http_methods(["GET", "POST"])
+ @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,
+                  '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,   })
+     sorting = request.GET.get('sorting')
+     if sorting:
+         # TODO check sorting value
+         q = q.order_by(sorting)
+     else:
+         q = q.order_by("groupname")
 -        
++
+     return object_list(
+         request,
+         q,
+         paginate_by=PAGINATE_BY_ALL,
+         page=request.GET.get('page') or 1,
+         template_name='im/astakosgroup_list.html',
+         extra_context=dict(form=AstakosGroupSearchForm(),
+                            is_search=True,
+                            sorting=sorting))
+ #@require_http_methods(["POST"])
+ @require_http_methods(["POST", "GET"])
+ @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 = _(astakos_messages.GROUP_JOIN_FAILURE)
+         messages.error(request, msg)
+         return group_search(request)
+ @require_http_methods(["POST"])
+ @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(_(astakos_messages.NOT_MEMBER))
+     if request.user in m.group.owner.all():
+         return HttpResponseForbidden(_(astakos_messages.OWNER_CANNOT_LEAVE_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(_(astakos_messages.NOT_MEMBER))
+         else:
+             if request.user not in m.group.owner.all():
+                 return HttpResponseForbidden(_(astakos_messages.NOT_OWNER))
+             func(request, m)
+             return group_detail(request, group_id)
+     return wrapper
+ #@require_http_methods(["POST"])
+ @require_http_methods(["POST", "GET"])
+ @signed_terms_required
+ @login_required
+ @handle_membership
+ def approve_member(request, membership):
+     try:
+         membership.approve()
+         realname = membership.person.realname
+         msg = _(astakos_messages.MEMBER_JOINED_GROUP) % locals()
+         messages.success(request, msg)
+     except AssertionError:
+         msg = _(astakos_messages.GROUP_MAX_PARTICIPANT_NUMBER_REACHED)
+         messages.error(request, msg)
+     except BaseException, e:
+         logger.exception(e)
+         realname = membership.person.realname
+         msg = _(astakos_messages.GENERIC_ERROR)
+         messages.error(request, msg)
+ @signed_terms_required
+ @login_required
+ @handle_membership
+ def disapprove_member(request, membership):
+     try:
+         membership.disapprove()
+         realname = membership.person.realname
+         msg = astakos_messages.MEMBER_REMOVED % realname
+         messages.success(request, msg)
+     except BaseException, e:
+         logger.exception(e)
+         msg = _(astakos_messages.GENERIC_ERROR)
+         messages.error(request, msg)
+ #@require_http_methods(["GET"])
+ @require_http_methods(["POST", "GET"])
+ @signed_terms_required
+ @login_required
+ def resource_list(request):
+     def with_class(entry):
+         entry['load_class'] = 'red'
+         max_value = float(entry['maxValue'])
+         curr_value = float(entry['currValue'])
+         if max_value > 0 :
+             entry['ratio'] = (curr_value / max_value) * 100
+         else:
 -            entry['ratio'] = 0 
++            entry['ratio'] = 0
+         if entry['ratio'] < 66:
+             entry['load_class'] = 'yellow'
+         if entry['ratio'] < 33:
+             entry['load_class'] = 'green'
+         return entry
+     def pluralize(entry):
+         entry['plural'] = engine.plural(entry.get('name'))
+         return entry
+     result = callpoint.get_user_status(request.user.id)
+     if result.is_success:
+         backenddata = map(with_class, result.data)
+         data = map(pluralize, result.data)
+     else:
+         data = None
+         messages.error(request, result.reason)
+     resource_catalog = ResourcePresentation(RESOURCES_PRESENTATION_DATA)
+     resource_catalog.update_from_result_report(result)
 -    
 -    
++
++
+     return render_response('im/resource_list.html',
+                            data=data,
+                            context_instance=get_context(request),
+                            resource_catalog=resource_catalog,
+                            result=result)
+ def group_create_list(request):
+     form = PickResourceForm()
+     return render_response(
+         template='im/astakosgroup_create_list.html',
+         context_instance=get_context(request),)
+ #@require_http_methods(["GET"])
+ @require_http_methods(["POST", "GET"])
+ @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, _(astakos_messages.BILLING_ERROR) % status)
+     except:
+         messages.error(request, r.result)
+     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
 -     
 -     
++
++
+ #@require_http_methods(["GET"])
+ @require_http_methods(["POST", "GET"])
+ @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
++
 +@require_http_methods(["GET", "POST"])
 +@login_required
 +@signed_terms_required
 +def remove_auth_provider(request, pk):
 +    provider = request.user.auth_providers.get(pk=pk)
-     print provider
 +    if provider.can_remove():
 +        provider.delete()
 +        return HttpResponseRedirect(reverse('edit_profile'))
 +    else:
 +        messages.error(_('Authentication method cannot be removed'))
 +        return HttpResponseRedirect(reverse('edit_profile'))