Merge remote-tracking branch 'origin/0.12' into devel-0.13
authorSofia Papagiannaki <papagian@gmail.com>
Thu, 29 Nov 2012 15:08:26 +0000 (17:08 +0200)
committerSofia Papagiannaki <papagian@gmail.com>
Thu, 29 Nov 2012 15:08:26 +0000 (17:08 +0200)
Conflicts:
snf-astakos-app/astakos/im/activation_backends.py
snf-astakos-app/astakos/im/api/admin.py
snf-astakos-app/astakos/im/auth_backends.py
snf-astakos-app/astakos/im/forms.py
snf-astakos-app/astakos/im/functions.py
snf-astakos-app/astakos/im/management/commands/group-list.py
snf-astakos-app/astakos/im/management/commands/invitation-list.py
snf-astakos-app/astakos/im/management/commands/service-list.py
snf-astakos-app/astakos/im/management/commands/user-add.py
snf-astakos-app/astakos/im/management/commands/user-list.py
snf-astakos-app/astakos/im/middleware.py
snf-astakos-app/astakos/im/models.py
snf-astakos-app/astakos/im/settings.py
snf-astakos-app/astakos/im/target/local.py
snf-astakos-app/astakos/im/target/redirect.py
snf-astakos-app/astakos/im/target/shibboleth.py
snf-astakos-app/astakos/im/urls.py
snf-astakos-app/astakos/im/util.py
snf-astakos-app/astakos/im/views.py
snf-astakos-app/conf/20-snf-astakos-app-settings.conf~

42 files changed:
1  2 
snf-astakos-app/README
snf-astakos-app/astakos/im/activation_backends.py
snf-astakos-app/astakos/im/api/__init__.py
snf-astakos-app/astakos/im/api/admin.py
snf-astakos-app/astakos/im/api/backends/lib/django/__init__.py
snf-astakos-app/astakos/im/api/backends/notifications.py
snf-astakos-app/astakos/im/api/callpoint.py
snf-astakos-app/astakos/im/api/spec.py
snf-astakos-app/astakos/im/auth_backends.py
snf-astakos-app/astakos/im/context_processors.py
snf-astakos-app/astakos/im/cookie.py
snf-astakos-app/astakos/im/endpoints/aquarium/producer.py
snf-astakos-app/astakos/im/endpoints/qh.py
snf-astakos-app/astakos/im/forms.py
snf-astakos-app/astakos/im/functions.py
snf-astakos-app/astakos/im/management/commands/group-list.py
snf-astakos-app/astakos/im/management/commands/group-permissions-add.py
snf-astakos-app/astakos/im/management/commands/invitation-list.py
snf-astakos-app/astakos/im/management/commands/quotaholder-sync.py
snf-astakos-app/astakos/im/management/commands/resource-list.py
snf-astakos-app/astakos/im/management/commands/resource-remove.py
snf-astakos-app/astakos/im/management/commands/service-add.py
snf-astakos-app/astakos/im/management/commands/service-list.py
snf-astakos-app/astakos/im/management/commands/user-add.py
snf-astakos-app/astakos/im/management/commands/user-invite.py
snf-astakos-app/astakos/im/management/commands/user-list.py
snf-astakos-app/astakos/im/management/commands/user-update.py
snf-astakos-app/astakos/im/messages.py
snf-astakos-app/astakos/im/middleware.py
snf-astakos-app/astakos/im/migrations/0031_populate_group_data.py
snf-astakos-app/astakos/im/models.py
snf-astakos-app/astakos/im/settings.py
snf-astakos-app/astakos/im/target/local.py
snf-astakos-app/astakos/im/target/redirect.py
snf-astakos-app/astakos/im/target/shibboleth.py
snf-astakos-app/astakos/im/templates/im/third_party_registration.html
snf-astakos-app/astakos/im/templatetags/astakos_tags.py
snf-astakos-app/astakos/im/templatetags/filters.py
snf-astakos-app/astakos/im/urls.py
snf-astakos-app/astakos/im/util.py
snf-astakos-app/astakos/im/views.py
snf-astakos-app/conf/20-snf-astakos-app-settings.conf

@@@ -90,40 -89,9 +90,41 @@@ ASTAKOS_GROUP_CREATION_SUBJEC
  ASTAKOS_HELPDESK_NOTIFICATION_EMAIL_SUBJECT '%s alpha2 testing account activated (%%(user)s)' % SITENAME                    Account activation helpdesk notification email subject
  ASTAKOS_EMAIL_CHANGE_EMAIL_SUBJECT          'Email change on %s alpha2 testing' % SITENAME                                  Email change subject               
  ASTAKOS_PASSWORD_RESET_EMAIL_SUBJECT        'Password reset on %s alpha2 testing' % SITENAME                                Password change email subject
 +
 +ASTAKOS_QUOTA_HOLDER_URL                    ''                                                                              The quota holder URI
 +                                                                                                                            e.g. ``http://localhost:8080/api/quotaholder/v``
 +ASTAKOS_SERVICES                            {'cyclades': {'resources': [{'desc': 'Number of virtual machines',              Default cloud service information
 +                                            'group': 'compute',
 +                                            'name': 'vm',
 +                                            'uplimit': 2},
 +                                            {'desc': 'Virtual machine disk size',
 +                                            'group': 'compute',
 +                                            'name': 'diskspace',
 +                                            'unit': 'GB',
 +                                            'uplimit': 5},
 +                                            {'desc': 'Number of virtual machine processors',
 +                                            'group': 'compute',
 +                                            'name': 'cpu',
 +                                            'uplimit': 1},
 +                                            {'desc': 'Virtual machines',
 +                                            'group': 'compute',
 +                                            'name': 'ram',
 +                                            'unit': 'MB',
 +                                            'uplimit': 1024}],
 +                                            'url': 'https://node1.example.com/ui/'},
 +                                            'pithos+': {'resources': [{'desc': 'Pithos account diskspace',
 +                                            'group': 'storage',
 +                                            'name': 'diskspace',
 +                                            'unit': 'bytes',
 +                                            'uplimit': 5368709120}],
 +                                            'url': 'https://node2.example.com/ui/'}}                                                                               
 +ASTAKOS_AQUARIUM_URL                        ''                                                                              The billing (aquarium) URI
 +                                                                                                                            e.g. ``http://localhost:8888/user``
 +ASTAKOS_PAGINATE_BY                         10                                                                              Number of object to be displayed per page
 +
  ASTAKOS_NEWPASSWD_INVALIDATE_TOKEN          True                                                                            Enforce token renewal on password change/reset. If set to False, user can optionally decide
                                                                                                                              whether to renew the token or not.
+ ASTAKOS_ENABLE_LOCAL_ACCOUNT_MIGRATION      True                                                                            Permit local account migration to third party account
  =========================================== =============================================================================   ===========================================================================================
  
  Administrator functions
  
  from django.utils.importlib import import_module
  from django.core.exceptions import ImproperlyConfigured
 -from django.core.mail import send_mail
 -from django.template.loader import render_to_string
 -from django.contrib import messages
 -from django.core.urlresolvers import reverse
  from django.utils.translation import ugettext as _
 -from django.db import transaction
  
 -from urlparse import urljoin
 -
 -from astakos.im.models import AstakosUser, Invitation
 -from astakos.im.forms import *
 +from astakos.im.models import AstakosUser
- from astakos.im.forms import LocalUserCreationForm, ShibbolethUserCreationForm
  from astakos.im.util import get_invitation
- from astakos.im.functions import (send_verification, send_activation,
-                                   send_account_creation_notification,
-                                   send_group_creation_notification, activate)
- from astakos.im.settings import (INVITATIONS_ENABLED, MODERATION_ENABLED,
-     SITENAME, RE_USER_EMAIL_PATTERNS
 -from astakos.im.functions import send_verification, send_activation, \
 -    send_admin_notification, activate, SendMailError
 -from astakos.im.settings import INVITATIONS_ENABLED, DEFAULT_CONTACT_EMAIL, \
 -    DEFAULT_FROM_EMAIL, MODERATION_ENABLED, SITENAME, DEFAULT_ADMIN_EMAIL, RE_USER_EMAIL_PATTERNS
++from astakos.im.functions import (
++    send_activation, send_account_creation_notification, activate
++)
++from astakos.im.settings import (
++    INVITATIONS_ENABLED, MODERATION_ENABLED, RE_USER_EMAIL_PATTERNS
 +)
 +
 +import astakos.im.messages as astakos_messages
  
 -import socket
  import logging
  import re
  
@@@ -227,26 -220,19 +220,19 @@@ class ActivationResult(object)
      def __init__(self, message):
          self.message = message
  
 +
  class VerificationSent(ActivationResult):
      def __init__(self):
 -        message = _('Verification sent.')
 +        message = _(astakos_messages.VERIFICATION_SENT)
          super(VerificationSent, self).__init__(message)
  
- class SwitchAccountsVerificationSent(ActivationResult):
-     def __init__(self, email):
-         message = _(astakos_messages.SWITCH_ACCOUNT_LINK_SENT)
-         super(SwitchAccountsVerificationSent, self).__init__(message)
  class NotificationSent(ActivationResult):
      def __init__(self):
-         message = _(astakos_messages.NOTIFACATION_SENT)
 -        message = _('Your request for an account was successfully received and is now pending \
 -                    approval. You will be notified by email in the next few days. Thanks for \
 -                    your interest in ~okeanos! The GRNET team.')
++        message = _(astakos_messages.NOTIFICATION_SENT)
          super(NotificationSent, self).__init__(message)
  
 +
  class RegistationCompleted(ActivationResult):
      def __init__(self):
 -        message = _('Registration completed. You can now login.')
 +        message = _(astakos_messages.REGISTRATION_COMPLETED)
-         super(RegistationCompleted, self).__init__(message)
+         super(RegistationCompleted, self).__init__(message)
@@@ -134,142 -87,4 +134,127 @@@ def _get_user_by_email(email)
          response.content = json.dumps(user_info)
          response['Content-Type'] = 'application/json; charset=UTF-8'
          response['Content-Length'] = len(response.content)
 -        return response
 +        return response
 +
 +
 +@api_method(http_method='GET')
 +def get_services(request):
 +    callback = request.GET.get('callback', None)
 +    services = Service.objects.all()
 +    data = tuple({'id': s.pk, 'name': s.name, 'url': s.url, 'icon':
 +                 s.icon} for s in services)
 +    data = json.dumps(data)
 +    mimetype = 'application/json'
 +
 +    if callback:
 +        mimetype = 'application/javascript'
 +        data = '%s(%s)' % (callback, data)
 +
 +    return HttpResponse(content=data, mimetype=mimetype)
 +
 +
 +@api_method()
 +def get_menu(request, with_extra_links=False, with_signout=True):
 +    user = request.user
-     if not isinstance(user, AstakosUser):
-         cookie = unquote(request.COOKIES.get(COOKIE_NAME, ''))
-         email = cookie.partition('|')[0]
-         try:
-             if email:
-                 user = AstakosUser.objects.get(email=email, is_active=True)
-         except AstakosUser.DoesNotExist:
-             pass
-     if not isinstance(user, AstakosUser):
-         index_url = reverse('index')
-         l = [{'url': absolute(request, index_url), 'name': "Sign in"}]
-     else:
++    index_url = reverse('index')
++    l = [{'url': absolute(request, index_url), 'name': "Sign in"}]
++    if user.is_authenticated():
 +        l = []
 +        append = l.append
 +        item = MenuItem
 +        item.current_path = absolute(request, request.path)
 +        append(item(
 +               url=absolute(request, reverse('index')),
 +               name=user.email))
 +        append(item(url=absolute(request, reverse('edit_profile')),
 +               name="My account"))
 +        if with_extra_links:
-             """
 +            if user.has_usable_password() and user.provider in ('local', ''):
 +                append(item(
 +                       url=absolute(request, reverse('password_change')),
 +                       name="Change password"))
-             """
 +            if EMAILCHANGE_ENABLED:
 +                append(item(
 +                       url=absolute(request, reverse('email_change')),
 +                       name="Change email"))
-             """
 +            if INVITATIONS_ENABLED:
 +                append(item(
 +                       url=absolute(request, reverse('invite')),
 +                       name="Invitations"))
-             """
 +            
 +            append(item(
 +                   url=absolute(request, reverse('group_list')),
 +                   name="Groups",
 +                   submenu=(item(
 +                            url=absolute(request,
 +                                         reverse('group_list')),
 +                            name="Overview"),
 +                            item(
 +                                url=absolute(request,
 +                                             reverse('group_create_list')),
 +                                name="Create"),
 +                            item(
 +                                url=absolute(request,
 +                                             reverse('group_search')),
 +                                name="Join"),)))
 +            append(item(
 +                   url=absolute(request, reverse('resource_list')),
 +                   name="Report"))
 +            append(item(
 +                   url=absolute(request, reverse('feedback')),
 +                   name="Feedback"))
-             """
 +            append(item(
 +                   url=absolute(request, reverse('billing')),
 +                   name="Billing"))
 +            append(item(
 +                   url=absolute(request, reverse('timeline')),
 +                   name="Timeline"))
-             """
 +        if with_signout:
 +            append(item(
 +                   url=absolute(request, reverse('logout')),
 +                   name="Sign out"))
 +
 +    callback = request.GET.get('callback', None)
 +    data = json.dumps(tuple(l))
 +    mimetype = 'application/json'
 +
 +    if callback:
 +        mimetype = 'application/javascript'
 +        data = '%s(%s)' % (callback, data)
 +
 +    return HttpResponse(content=data, mimetype=mimetype)
 +
 +
 +class MenuItem(dict):
 +    current_path = ''
 +
 +    def __init__(self, *args, **kwargs):
 +        super(MenuItem, self).__init__(*args, **kwargs)
 +        if kwargs.get('url') or kwargs.get('submenu'):
 +            self.__set_is_active__()
 +
 +    def __setitem__(self, key, value):
 +        super(MenuItem, self).__setitem__(key, value)
 +        if key in ('url', 'submenu'):
 +            self.__set_is_active__()
 +
 +    def __set_is_active__(self):
 +        if self.get('is_active'):
 +            return
 +        if self.current_path == self.get('url'):
 +            self.__setitem__('is_active', True)
 +        else:
 +            submenu = self.get('submenu', ())
 +            current = (i for i in submenu if i.get('url') == self.current_path)
 +            try:
 +                current_node = current.next()
 +                if not current_node.get('is_active'):
 +                    current_node.__setitem__('is_active', True)
 +                self.__setitem__('is_active', True)
 +            except StopIteration:
 +                return
 +
 +    def __setattribute__(self, name, value):
 +        super(MenuItem, self).__setattribute__(name, value)
 +        if name == 'current_path':
 +            self.__set_is_active__()
  # or implied, of GRNET S.A.
  
  import logging
 -import urllib
  
  from functools import wraps
 -from traceback import format_exc
  from time import time, mktime
 -from urllib import quote
 -from urlparse import urlparse
 -from collections import defaultdict
  
 -from django.conf import settings
  from django.http import HttpResponse
  from django.utils import simplejson as json
 -from django.core.urlresolvers import reverse
  
 -from astakos.im.api.faults import *
 -from astakos.im.models import AstakosUser, Service
 -from astakos.im.settings import INVITATIONS_ENABLED, EMAILCHANGE_ENABLED
 +from astakos.im.api.faults import (
 +    Fault, Unauthorized, InternalServerError, BadRequest,
-     Forbidden)
++    Forbidden
++)
 +from astakos.im.api import render_fault, _get_user_by_email, _get_user_by_username
 +from astakos.im.models import AstakosUser
  from astakos.im.util import epoch
 -from astakos.im.api import _get_user_by_email, _get_user_by_username
  
  logger = logging.getLogger(__name__)
  format = ('%a, %d %b %Y %H:%M:%S GMT')
index 8ac1a66,0000000..38c3629
mode 100644,000000..100644
--- /dev/null
@@@ -1,305 -1,0 +1,307 @@@
 +# Copyright 2011-2012 GRNET S.A. All rights reserved.
 +#
 +# Redistribution and use in source and binary forms, with or
 +# without modification, are permitted provided that the following
 +# conditions are met:
 +#
 +#   1. Redistributions of source code must retain the above
 +#      copyright notice, this list of conditions and the following
 +#      disclaimer.
 +#
 +#   2. Redistributions in binary form must reproduce the above
 +#      copyright notice, this list of conditions and the following
 +#      disclaimer in the documentation and/or other materials
 +#      provided with the distribution.
 +#
 +# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
 +# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 +# 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.
 +
 +from django.db import IntegrityError, transaction
 +from django.core.exceptions import ObjectDoesNotExist
 +
 +from functools import wraps
 +from smtplib import SMTPException
 +
 +from astakos.im.models import (
 +    AstakosUser, AstakosGroup, GroupKind, Resource, Service, RESOURCE_SEPARATOR
 +)
 +from astakos.im.api.backends.base import BaseBackend, SuccessResult, FailureResult
 +from astakos.im.api.backends.errors import (
 +    ItemNotExists, ItemExists, MissingIdentifier, MultipleItemsExist
 +)
 +from astakos.im.util import reserved_email, model_to_dict
 +from astakos.im.endpoints.qh import get_quota
 +
 +import logging
 +
 +logger = logging.getLogger(__name__)
 +
 +DEFAULT_CONTENT_TYPE = None
 +
 +
 +def safe(func):
 +    """Decorator function for views that implement an API method."""
 +    @transaction.commit_manually
 +    @wraps(func)
 +    def wrapper(self, *args, **kwargs):
 +        logger.debug('%s %s %s' % (func, args, kwargs))
 +        try:
 +            data = func(self, *args, **kwargs) or ()
 +        except Exception, e:
 +            logger.exception(e)
 +            transaction.rollback()
 +            return FailureResult(e)
 +        else:
 +            transaction.commit()
 +            return SuccessResult(data)
 +    return wrapper
 +
 +
 +class DjangoBackend(BaseBackend):
 +    def _lookup_object(self, model, **kwargs):
 +        """
 +        Returns an object of the specific model matching the given lookup
 +        parameters.
 +        """
 +        if not kwargs:
 +            raise MissingIdentifier
 +        try:
 +            return model.objects.get(**kwargs)
 +        except model.DoesNotExist:
 +            raise ItemNotExists(model._meta.verbose_name, **kwargs)
 +        except model.MultipleObjectsReturned:
 +            raise MultipleItemsExist(model._meta.verbose_name, **kwargs)
 +
 +    def _lookup_user(self, id):
 +        """
 +        Returns an AstakosUser having this id.
 +        """
 +        if not isinstance(id, int):
 +            raise TypeError('User id should be of type int')
 +        return self._lookup_object(AstakosUser, id=id)
 +
 +    def _lookup_service(self, id):
 +        """
 +        Returns an Service having this id.
 +        """
 +        if not isinstance(id, int):
 +            raise TypeError('Service id should be of type int')
 +        return self._lookup_object(Service, id=id)
 +
 +    def _list(self, model, filter=()):
 +        q = model.objects.all()
 +        if filter:
 +            q = q.filter(id__in=filter)
 +        return map(lambda o: model_to_dict(o, exclude=[]), q)
 +
 +    def _create_object(self, model, **kwargs):
 +        o = model.objects.create(**kwargs)
 +        o.save()
 +        return o
 +
 +    def _update_object(self, model, id, save=True, **kwargs):
 +        o = self._lookup_object(model, id=id)
 +        if kwargs:
 +            o.__dict__.update(kwargs)
 +        if save:
 +            o.save()
 +        return o
 +
 +    @safe
 +    def update_user(self, user_id, renew_token=False, **kwargs):
 +        user = self._update_object(AstakosUser, user_id, save=False, **kwargs)
 +        if renew_token:
 +            user.renew_token()
 +        if kwargs or renew_token:
 +            user.save()
 +
 +    @safe
 +    def create_user(self, **kwargs):
 +        policies = kwargs.pop('policies', ())
 +        permissions = kwargs.pop('permissions', ())
 +        groups = kwargs.pop('groups', ())
 +        password = kwargs.pop('password', None)
 +
 +        u = self._create_object(AstakosUser, **kwargs)
 +
 +        if password:
 +            u.set_password(password)
 +        u.permissions = permissions
 +        u.policies = policies
 +        u.extended_groups = groups
 +        return self._list(AstakosUser, filter=(u.id,))
 +
 +    @safe
 +    def add_policies(self, user_id, update=False, policies=()):
 +        user = self._lookup_user(user_id)
 +        rejected = []
 +        append = rejected.append
 +        for p in policies:
 +            service = p.get('service')
 +            resource = p.get('resource')
 +            uplimit = p.get('uplimit')
 +            try:
 +                user.add_policy(service, resource, uplimit, update)
 +            except (ObjectDoesNotExist, IntegrityError), e:
 +                append((service, resource, e))
 +        return rejected
 +    
 +    @safe
 +    def remove_policies(self, user_id, policies=()):
 +        user = self._lookup_user(user_id)
 +        if not user:
 +            return user_id
 +        rejected = []
 +        append = rejected.append
 +        for p in policies:
 +            service = p.get('service')
 +            resource = p.get('resource')
 +            try:
 +                user.delete_policy(service, resource)
 +            except ObjectDoesNotExist, e:
 +                append((service, resource, e))
 +        return rejected
 +    @safe
 +    def add_permissions(self, user_id, permissions=()):
 +        user = self._lookup_user(user_id)
 +        rejected = []
 +        append = rejected.append
 +        for p in permissions:
 +            try:
 +                user.add_permission(p)
 +            except IntegrityError, e:
 +                append((p, e))
 +        return rejected
 +    
 +    @safe
 +    def remove_permissions(self, user_id, permissions=()):
 +        user = self._lookup_user(user_id)
 +        rejected = []
 +        append = rejected.append
 +        for p in permissions:
 +            try:
 +                user.remove_permission(p)
 +            except (ObjectDoesNotExist, IntegrityError), e:
 +                append((p, e))
 +        return rejected
 +    
 +    @safe
 +    def invite_users(self, senderid, recipients=()):
 +        user = self._lookup_user(senderid)
 +        rejected = []
 +        append = rejected.append
 +        for r in recipients:
++            email = r.get('email')
++            realname = r.get('realname')
 +            try:
-                 user.invite(r.get('email'), r.get('realname'))
++                user.invite(email, realname)
 +            except (IntegrityError, SMTPException), e:
 +                append((email, e))
 +        return rejected
 +    
 +    @safe
 +    def list_users(self, filter=()):
 +        return self._list(AstakosUser, filter=filter)
 +
 +    @safe
 +    def get_resource_usage(self, user_id):
 +        user = self._lookup_user(user_id)
 +        c, data = get_quota((user,))
 +        resources = []
 +        append = resources.append
 +        for t in data:
 +            t = (i if i else 0 for i in t)
 +            (entity, name, quantity, capacity, importLimit, exportLimit,
 +             imported, exported, returned, released, flags) = t
 +            service, sep, resource = name.partition(RESOURCE_SEPARATOR)
 +            resource = Resource.objects.select_related().get(
 +                service__name=service, name=resource)
 +            d = dict(name=name,
 +                     description=resource.desc,
 +                     unit=resource.unit or '',
 +                     maxValue=quantity + capacity,
 +                     currValue=quantity + imported - released - exported + returned)
 +            append(d)
 +        return resources
 +
 +    @safe
 +    def list_resources(self, filter=()):
 +        return self._list(Resource, filter=filter)
 +
 +    @safe
 +    def create_service(self, **kwargs):
 +        resources = kwargs.pop('resources', ())
 +        s = self._create_object(Service, **kwargs)
 +        s.resources = resources
 +        return self._list(Service, filter=(s.id,))
 +
 +    @safe
 +    def remove_services(self, ids=()):
 +        # TODO return information for unknown ids
 +        q = Service.objects.filter(id__in=ids)
 +        q.delete()
 +    
 +    @safe
 +    def update_service(self, service_id, renew_token=False, **kwargs):
 +        s = self._update_object(Service, service_id, save=False, **kwargs)
 +        if renew_token:
 +            s.renew_token()
 +
 +        if kwargs or renew_token:
 +            s.save()
 +
 +    @safe
 +    def add_resources(self, service_id, update=False, resources=()):
 +        s = self._lookup_service(service_id)
 +        rejected = []
 +        append = rejected.append
 +        for r in resources:
 +            try:
 +                rr = r.copy()
 +                resource_id = rr.pop('id', None)
 +                if update:
 +                    if not resource_id:
 +                        raise MissingIdentifier
 +                    resource = self._update_object(Resource, resource_id, **rr)
 +                else:
 +                    resource = self._create_object(Resource, service=s, **rr)
 +            except Exception, e:
 +                append((r, e))
 +        return rejected
 +    
 +    @safe
 +    def remove_resources(self, service_id, ids=()):
 +        # TODO return information for unknown ids
 +        q = Resource.objects.filter(service__id=service_id,
 +                                id__in=ids)
 +        q.delete()
 +    
 +    @safe
 +    def create_group(self, **kwargs):
 +        policies = kwargs.pop('policies', ())
 +        permissions = kwargs.pop('permissions', ())
 +        members = kwargs.pop('members', ())
 +        owners = kwargs.pop('owners', ())
 +
 +        g = self._create_object(AstakosGroup, **kwargs)
 +
 +        g.permissions = permissions
 +        g.policies = policies
 +#         g.members = members
 +        g.owners = owners
 +        return self._list(AstakosGroup, filter=(g.id,))
index a69ff22,0000000..cd72179
mode 100644,000000..100644
--- /dev/null
@@@ -1,42 -1,0 +1,97 @@@
++# Copyright 2011-2012 GRNET S.A. All rights reserved.
++#
++# Redistribution and use in source and binary forms, with or
++# without modification, are permitted provided that the following
++# conditions are met:
++#
++#   1. Redistributions of source code must retain the above
++#      copyright notice, this list of conditions and the following
++#      disclaimer.
++#
++#   2. Redistributions in binary form must reproduce the above
++#      copyright notice, this list of conditions and the following
++#      disclaimer in the documentation and/or other materials
++#      provided with the distribution.
++#
++# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
++# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
++# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
++# 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 logging
++import socket
++
++from smtplib import SMTPException
++
++from django.conf import settings
++from django.core.mail import send_mail
++from django.template.loader import render_to_string
++
++import astakos.im.messages as astakos_messages
++
++from astakos.im.settings import LOGGING_LEVEL
++
++logger = logging.getLogger(__name__)
++
 +def _send_admin_notification(template_name,
 +                             dictionary=None,
 +                             subject='alpha2 testing notification',):
 +    """
 +    Send notification email to settings.ADMINS.
 +
 +    Raises SendNotificationError
 +    """
 +    if not settings.ADMINS:
 +        return
 +    dictionary = dictionary or {}
 +    message = render_to_string(template_name, dictionary)
 +    sender = settings.SERVER_EMAIL
 +    try:
 +        send_mail(subject,
 +                  message, sender, [i[1] for i in settings.ADMINS])
 +    except (SMTPException, socket.error) as e:
 +        logger.exception(e)
 +        raise SendNotificationError()
 +    else:
 +        msg = 'Sent admin notification for user %s' % dictionary
 +        logger.log(LOGGING_LEVEL, msg)
 +
 +class EmailNotification(Notification):
 +    def send(self):
 +        send_mail(
 +            subject,
 +            message,
 +            sender,
 +            recipients
 +        )
-     )
 +
 +class Notification(object):
 +    def __init__(self, sender, recipients, subject, message):
 +        self.sender = sender
 +        self.recipients = recipients
 +        self.subject = subject
 +        self.message = message
 +    
 +    def send(self):
-         pass
++        pass
++
++class SendMailError(Exception):
++    pass
++
++class SendNotificationError(SendMailError):
++    def __init__(self):
++        self.message = _(astakos_messages.NOTIFICATION_SEND_ERR)
++        super(SendNotificationError, self).__init__()
index 5779abd,0000000..fa0c90c
mode 100644,000000..100644
--- /dev/null
@@@ -1,162 -1,0 +1,151 @@@
 +# Copyright 2011-2012 GRNET S.A. All rights reserved.
 +#
 +# Redistribution and use in source and binary forms, with or
 +# without modification, are permitted provided that the following
 +# conditions are met:
 +#
 +#   1. Redistributions of source code must retain the above
 +#      copyright notice, this list of conditions and the following
 +#      disclaimer.
 +#
 +#   2. Redistributions in binary form must reproduce the above
 +#      copyright notice, this list of conditions and the following
 +#      disclaimer in the documentation and/or other materials
 +#      provided with the distribution.
 +#
 +# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
 +# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 +# 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.
 +
 +from astakos.im.api.spec import AstakosAPI
 +from astakos.im.api.backends import get_backend
 +
- from commissioning import (Callpoint,
-                            #                             CommissionException,
-                            #                             CorruptedError, InvalidDataError,
-                            #                             InvalidKeyError, NoEntityError,
-                            #                             NoQuantityError, NoCapacityError,
-                            #                             ExportLimitError, ImportLimitError
-                            )
++from commissioning import CorruptedError
 +
- # from commissioning.utils.newname import newname
- # from django.db.models import Model, BigIntegerField, CharField, ForeignKey, Q
- # from django.db import transaction, IntegrityError
- # from .models import (Holder, Entity, Policy, Holding,
- #                      Commission, Provision, ProvisionLog, now)
++from django.db import transaction
 +
 +class AstakosCallpoint():
 +
 +    api_spec = AstakosAPI()
 +
 +#     http_exc_lookup = {
 +#         CorruptedError:   550,
 +#         InvalidDataError: 400,
 +#         InvalidKeyError:  401,
 +#         NoEntityError:    404,
 +#         NoQuantityError:  413,
 +#         NoCapacityError:  413,
 +#     }
 +
 +    def init_connection(self, connection):
 +        if connection is not None:
 +            raise ValueError("Cannot specify connection args with %s" %
 +                             type(self).__name__)
 +        pass
 +
 +    def commit(self):
 +        transaction.commit()
 +
 +    def rollback(self):
 +        transaction.rollback()
 +
 +    def do_make_call(self, call_name, data):
 +        call_fn = getattr(self, call_name, None)
 +        if not call_fn:
 +            m = "cannot find call '%s'" % (call_name,)
 +            raise CorruptedError(m)
 +
 +        return call_fn(**data)
 +
 +    def create_users(self, users=()):
 +        b = get_backend()
 +        rejected = (b.create_user(**u) for u in users)
 +        return rejected
 +
 +    def update_users(self, users=()):
 +        b = get_backend()
 +        rejected = (b.update_user(**u) for u in users)
 +        return rejected
 +
 +    def add_user_policies(self, user_id, update=False, policies=()):
 +        b = get_backend()
 +        rejected = b.add_policies(user_id, update, policies)
 +        return rejected
 +
 +    def remove_user_policies(self, user_id, policies=()):
 +        b = get_backend()
 +        rejected = b.remove_policies(user_id, policies)
 +        return rejected
 +
 +    def add_user_permissions(self, user_id, permissions=()):
 +        b = get_backend()
 +        rejected = b.add_permissions(user_id, permissions)
 +        return rejected
 +
 +    def remove_user_permissions(self, user_id, permissions=()):
 +        b = get_backend()
 +        rejected = b.remove_permissions(user_id, permissions)
 +        return rejected
 +
 +    def invite_users(self, sender_id, recipients=()):
 +        b = get_backend()
 +        rejected = b.invite_users(sender_id, recipients)
 +        return rejected
 +
 +    def list_users(self, filter=()):
 +        b = get_backend()
 +        return b.list_users(filter)
 +
 +    def get_user_status(self, user_id):
 +        b = get_backend()
 +        return b.get_resource_usage(user_id)
 +
 +    def list_resources(self, filter=()):
 +        b = get_backend()
 +        return b.list_resources(filter)
 +
 +    def add_services(self, services=()):
 +        b = get_backend()
 +        rejected = (b.create_service(**s) for s in services)
 +        return rejected
 +
 +    def update_services(self, services=()):
 +        b = get_backend()
 +        rejected = (b.update_service(**s) for s in services)
 +        return rejected
 +
 +    def remove_services(self, ids=()):
 +        b = get_backend()
 +        rejected = b.remove_services(ids)
 +        return rejected
 +
 +    def add_resources(self, service_id, update=False, resources=()):
 +        b = get_backend()
 +        rejected = b.add_resources(service_id, update, resources)
 +        return rejected
 +    
 +    def remove_resources(self, service_id, ids=()):
 +        b = get_backend()
 +        rejected = b.remove_resources(service_id, ids)
 +        return rejected
 +    
 +    def create_groups(self, groups=()):
 +        b = get_backend()
 +        rejected = (b.create_group(**g) for g in groups)
 +        return rejected
 +
 +API_Callpoint = AstakosCallpoint
index 161feda,0000000..f355856
mode 100644,000000..100644
--- /dev/null
@@@ -1,421 -1,0 +1,420 @@@
 +from commissioning.specificator import (
-     CanonifyException, SpecifyException,
-     Specificator, Null, Integer, Text,
-     Tuple, ListOf, Dict, Args)
++    Specificator, Integer, Text, ListOf
++)
 +
 +
 +class Name(Text):
 +    def init(self):
 +        self.opts.update({'regex': "[\w.:]+", 'maxlen': 512})
 +Name = Name()
 +
 +
 +class Email(Text):
 +    def init(self):
 +        pattern = "[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?"
 +        self.opts.update({'regex': pattern, 'maxlen': 512})
 +Email = Email()
 +
 +
 +class Url(Text):
 +    def init(self):
 +        pattern = "(((f|ht){1}tp://)[-a-zA-Z0-9@:%_\+.~#?&//=]+)"
 +        self.opts.update({'regex': pattern, 'maxlen': 512})
 +Url = Url()
 +
 +
 +class Filepath(Text):
 +    def init(self):
 +        self.opts.update({'regex': "", 'maxlen': 512})
 +Filepath = Filepath()
 +
 +
 +class Nonnegative(Integer):
 +    def init(self):
 +        self.opts.update({'minimum': 0})
 +Nonnegative = Nonnegative()
 +
 +
 +class Boolean(Integer):
 +    def init(self):
 +        self.opts.update({'minimum': 0, 'maximum': 1})
 +Boolean = Boolean()
 +
 +
 +# class GroupKind(Integer):
 +#     def init(self):
 +#         self.opts.update({'minimum': 1, 'maximum': 5})
 +# GroupKind = GroupKind()
 +
 +Timepoint = Text(classname='Timepoint', maxlen=24)
 +
 +
 +class AstakosAPI(Specificator):
 +    def create_users(
 +        self,
 +        users=ListOf(
 +            email=Email,
 +            first_name=Name,
 +            last_name=Name,
 +            is_active=Boolean,
 +            is_superuser=Boolean,
 +            affiliation=Name,
 +            password=Name,
 +            provider=Name,
 +            level=Nonnegative,
 +            invitations=Nonnegative,
 +            is_verified=Boolean,
 +            third_party_identifier=Name,
 +            email_verified=Boolean),
 +        policies=ListOf(resource=Name, supimit=Nonnegative),
 +        groups=ListOf(Name),
 +        permissions=ListOf(Name)
 +    ):
 +        rejected = ListOf(user=Email, reason=Text())
 +        return rejected
 +
 +    def update_users(
 +        self,
 +        users=ListOf(
 +            pk=Nonnegative,
 +            renew_token=Boolean,
 +            data=ListOf(
 +                first_name=Name,
 +                last_name=Name,
 +                is_active=Boolean,
 +                is_superuser=Boolean,
 +                affiliation=Name,
 +                password=Name,
 +                provider=Name,
 +                level=Nonnegative,
 +                invitations=Nonnegative,
 +                is_verified=Boolean,
 +                third_party_identifier=Name,
 +                email_verified=Boolean
 +            )
 +        )
 +    ):
 +        rejected = ListOf(user_id=Nonnegative, reason=Text())
 +        return rejected
 +
 +    def add_user_policies(
 +        self,
 +        pk=Nonnegative,
 +        update=Boolean,
 +        policies=ListOf(service=Name, resource=Name, upimit=Nonnegative)
 +    ):
 +        rejected = ListOf(resource=Name, reason=Text())
 +        return rejected
 +
 +    def remove_user_policies(
 +        self,
 +        pk=Nonnegative,
 +        policies=ListOf(service=Name, resource=Name)
 +    ):
 +        rejected = ListOf(service=Name, resource=Name)
 +        return rejected
 +
 +    def add_user_permissions(
 +        self,
 +        pk=Nonnegative,
 +        permissions=ListOf(permission=Name)
 +    ):
 +        rejected = ListOf(permission=Name)
 +        return rejected
 +
 +    def remove_user_permissions(
 +        self,
 +        pk=Nonnegative,
 +        permissions=ListOf(permission=Name)
 +    ):
 +        rejected = ListOf(permission=Name)
 +        return rejected
 +
 +    def invite_users(
 +        self,
 +        sender=Email,
 +        data=ListOf(email=Email, realname=Name)
 +    ):
 +        rejected = ListOf(receiver=Email)
 +        return rejected
 +
 +    def list_users(
 +        self,
 +        filter=ListOf(id=Nonnegative)
 +    ):
 +        return ListOf(
 +            activation_sent=Timepoint,
 +            affiliation=Name,
 +            auth_token=Name,
 +            auth_token_created=Timepoint,
 +            auth_token_expires=Timepoint,
 +            date_joined=Timepoint,
 +            date_signed_terms=Timepoint,
 +            email=Email,
 +            email_verified=Boolean,
 +            first_name=Name,
 +            has_credits=Boolean,
 +            has_signed_terms=Boolean,
 +            id=Nonnegative,
 +            invitations=Nonnegative,
 +            invitations_sent=ListOf(
 +                code=Name,
 +                consumed=Boolean,
 +                created=Timepoint,
 +                id=Nonnegative,
 +                realname=Name,
 +                username=Email
 +            ),
 +            is_active=Boolean,
 +            is_staff=Boolean,
 +            is_superuser=Boolean,
 +            is_verified=Boolean,
 +            last_login=Timepoint,
 +            last_name=Name,
 +            level=Nonnegative,
 +            password=Name,
 +            provider=Name,
 +            third_party_identifier=Name,
 +            updated=Timepoint,
 +            user_permissions=ListOf(
 +                codename=Name,
 +                id=Nonnegative,
 +                name=Name
 +            ),
 +            username=Name,
 +            astakos_groups=ListOf(
 +                approval_date=Timepoint,
 +                creation_date=Timepoint,
 +                desc=Text(),
 +                max_participants=Nonnegative,
 +                expiration_date=Timepoint,
 +                group_ptr=Url,
 +                homepage=Url,
 +                id=Nonnegative,
 +                issue_date=Timepoint,
 +                kind=Name,
 +                moderation_enabled=Boolean,
 +                name=Name,
 +                #permissions=ListOf(),
 +                policy=ListOf(id=Nonnegative, name=Name)
 +            )
 +        )
 +
 +    def get_user_status(
 +        self,
 +        user_id=Nonnegative
 +    ):
 +        return ListOf(
 +            name=Name,
 +            description=Text(),
 +            unit=Name,
 +            maxValue=Integer(),
 +            currValue=Integer()
 +        )
 +
 +    def list_resources(self, filter=ListOf(id=Nonnegative)):
 +        return ListOf(
 +            desc=Text(),
 +            group=Name,
 +            id=Nonnegative,
 +            meta=ListOf(key=Name, value=Name),
 +            name=Name,
 +            service=Name,
 +            unit=Name
 +        )
 +
 +    def add_services(
 +        self,
 +        services=ListOf(
 +            name=Name,
 +            url=Url,
 +            icon=Filepath,
 +            resources=ListOf(
 +                name=Name,
 +                desc=Text(),
 +                unit=Name,
 +                group=Name
 +            )
 +        )
 +    ):
 +        rejected = ListOf(service=Name)
 +        return rejected
 +
 +    def update_services(
 +        self,
 +        services=ListOf(id=Nonnegative, url=Url, icon=Filepath)
 +    ):
 +        rejected = ListOf(service=Name)
 +        return rejected
 +
 +    def remove_services(self, ids=ListOf(Nonnegative)):
 +        rejected = ListOf(service=Name)
 +        return rejected
 +
 +    def add_resources(
 +        self,
 +        service_id=Nonnegative,
 +        update=Boolean,
 +        resources=ListOf(
 +            name=Name,
 +            resources=ListOf(
 +                name=Name,
 +                desc=Text(),
 +                unit=Name,
 +                group=Name)
 +        )
 +    ):
 +        rejected = ListOf(service=Name)
 +        return rejected
 +
 +    def remove_resources(
 +        self,
 +        service_id=Nonnegative,
 +        ids=ListOf(Nonnegative)
 +    ):
 +        rejected = ListOf(Name)
 +        return rejected
 +
 +    def create_groups(
 +        self,
 +        groups=ListOf(
 +            name=Name,
 +            kind=Name,
 +            homepage=Url,
 +            desc=Text(),
 +            policies=ListOf(resource=Name, upimit=Nonnegative),
 +            issue_date=Timepoint,
 +            expiration_date=Timepoint,
 +            moderation_enabled=Boolean,
 +            participants=Nonnegative,
 +            permissions=ListOf(permission=Name),
 +            members=ListOf(user=Email, is_approved=Boolean),
 +            owners=ListOf(user=Email)
 +        )
 +    ):
 +        rejected = ListOf(group=Name)
 +        return rejected
 +
 +    def enable_groups(self, data=ListOf(group=Name)):
 +        rejected = ListOf(group=Name)
 +        return rejected
 +
 +    def search_groups(self, key=Name):
 +        return ListOf(
 +            group=Name,
 +            kind=Nonnegative,
 +            homepage=Url,
 +            desc=Text(),
 +            creation_date=Timepoint,
 +            issue_date=Timepoint,
 +            expiration_date=Timepoint,
 +            moderation_enabled=Boolean,
 +            participants=Nonnegative,
 +            owner=ListOf(user=Email),
 +            policies=ListOf(resource=Name, upimit=Nonnegative),
 +            members=ListOf(user=Email, is_approved=Boolean)
 +        )
 +
 +    def list_groups(self):
 +        return ListOf(
 +            group=Name,
 +            kind=Nonnegative,
 +            homepage=Url,
 +            desc=Text(),
 +            creation_date=Timepoint,
 +            issue_date=Timepoint,
 +            expiration_date=Timepoint,
 +            moderation_enabled=Boolean,
 +            participants=Nonnegative,
 +            owners=ListOf(user=Email),
 +            policies=ListOf(resource=Name, upimit=Nonnegative),
 +            members=ListOf(user=Email, is_approved=Boolean)
 +        )
 +
 +    def add_owners(
 +        self,
 +        data=ListOf(group=Name, owners=ListOf(user=Email))
 +    ):
 +        rejected = ListOf(user=Email)
 +        return rejected
 +
 +    def remove_owners(
 +        self,
 +        data=ListOf(group=Name, owners=ListOf(user=Email))
 +    ):
 +        rejected = ListOf(user=Email)
 +        return rejected
 +
 +    def add_members(
 +        self,
 +        data=ListOf(group=Name, members=ListOf(user=Email))
 +    ):
 +        rejected = ListOf(user=Email)
 +        return rejected
 +
 +    def remove_members(
 +        self,
 +        data=ListOf(group=Name, members=ListOf(user=Email))
 +    ):
 +        rejected = ListOf(user=Email)
 +        return rejected
 +
 +    def add_policies(
 +        self,
 +        data=ListOf(group=Name, resource=Name, upimit=Nonnegative)
 +    ):
 +        rejected = ListOf(group=Name, resource=Name)
 +        return rejected
 +
 +    def remove_group_policies(
 +        self,
 +        data=ListOf(group=Name, resource=Name, upimit=Nonnegative)
 +    ):
 +        rejected = ListOf(group=Name, resource=Name)
 +        return rejected
 +
 +    def update_group_policies(
 +        self, data=ListOf(group=Name, resource=Name, upimit=Nonnegative)
 +    ):
 +        rejected = ListOf(group=Name, resource=Name)
 +        return rejected
 +
 +    def approve_members(
 +        self,
 +        data=ListOf(group=Name, members=ListOf(user=Email))
 +    ):
 +        rejected = ListOf(user=Email)
 +        return rejected
 +
 +    def disapprove_members(
 +        self,
 +        data=ListOf(group=Name, members=ListOf(user=Email))
 +    ):
 +        rejected = ListOf(user=Email)
 +        return rejected
 +
 +    def add_group_permissions(
 +        self,
 +        data=ListOf(group=Name, permission=Name)
 +    ):
 +        rejected = ListOf(group=Name, permission=Name)
 +        return rejected
 +
 +    def delete_group_permissions(
 +        self,
 +        data=ListOf(group=Name, permission=Name)
 +    ):
 +        rejected = ListOf(group=Name, permission=Name)
 +        return rejected
 +
 +    def list_resource_units(self):
 +        return ListOf(Name)
 +
-     def get_approval_terms(term=Nonnegative):
++    def get_approval_terms(self, term=Nonnegative):
 +        return Text()
 +
-     def add_approval_terms(location=Filepath):
++    def add_approval_terms(self, location=Filepath):
 +        return Nonnegative
 +
 +#     def change_emails():
 +#         pass
@@@ -35,8 -35,12 +35,13 @@@ from django.contrib.auth.backends impor
  from django.core.validators import email_re
  
  from astakos.im.models import AstakosUser
+ from astakos.im.settings import LOGGING_LEVEL
+ import logging
+ logger = logging.getLogger(__name__)
  
 +
  class TokenBackend(ModelBackend):
      """
      AuthenticationBackend used to authenticate using token instead
@@@ -79,7 -87,11 +89,11 @@@ class EmailBackend(ModelBackend)
                  return None
          if user.check_password(password):
              return user
+         else:
+             msg = 'Invalid password during authentication for %s' % username
+             logger._log(LOGGING_LEVEL, msg, [])
 -            
 -    
++
 +
      def get_user(self, user_id):
          try:
              return AstakosUser.objects.get(pk=user_id)
  # 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, \
-     LOGIN_MESSAGES, SIGNUP_MESSAGES, PROFILE_MESSAGES, \
 -        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 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}
  
index 0000000,8dca8ff..47a4a4c
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,105 +1,107 @@@
+ # Copyright 2011-2012 GRNET S.A. All rights reserved.
+ # 
+ # Redistribution and use in source and binary forms, with or
+ # without modification, are permitted provided that the following
+ # conditions are met:
+ # 
+ #   1. Redistributions of source code must retain the above
+ #      copyright notice, this list of conditions and the following
+ #      disclaimer.
+ # 
+ #   2. Redistributions in binary form must reproduce the above
+ #      copyright notice, this list of conditions and the following
+ #      disclaimer in the documentation and/or other materials
+ #      provided with the distribution.
+ # 
+ # THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
+ # OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ # 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 logging
+ from urllib import quote, unquote
+ from django.contrib.auth.models import AnonymousUser
+ from django.http import HttpRequest
+ from django.utils.translation import ugettext as _
+ from astakos.im.settings import (
+     COOKIE_NAME, COOKIE_DOMAIN, COOKIE_SECURE, LOGGING_LEVEL
+ )
++import astakos.im.messages as astakos_messages
++
+ logger = logging.getLogger(__name__)
+ class Cookie():
+     def __init__(self, request, response=None):
+         cookies = getattr(request, 'COOKIES', {})
+         cookie = unquote(cookies.get(COOKIE_NAME, ''))
+         self.email, sep, self.auth_token = cookie.partition('|')
+         self.request = request
+         self.response = response
+     
+     @property
+     def email(self):
+         return getattr(self, 'email', '')
+     
+     @property
+     def auth_token(self):
+         return getattr(self, 'auth_token', '')
+     
+     @property
+     def is_set(self):
 -        no_token = not self.auth_token 
++        no_token = not self.auth_token
+         return not no_token
+     
+     @property
+     def is_valid(self):
+         return self.email == getattr(self.user, 'email', '') and \
+             self.auth_token == getattr(self.user, 'auth_token', '')
+     
+     @property
+     def user(self):
+         return getattr(self.request, 'user', AnonymousUser())
+     
+     def __set(self):
+         if not self.response:
 -            raise ValueError(_('There is no response.'))
++            raise ValueError(_(astakos_messages.NO_RESPONSE))
+         user = self.user
+         expire_fmt = user.auth_token_expires.strftime('%a, %d-%b-%Y %H:%M:%S %Z')
+         cookie_value = quote(user.email + '|' + user.auth_token)
+         self.response.set_cookie(
+             COOKIE_NAME, value=cookie_value, expires=expire_fmt, path='/',
+             domain=COOKIE_DOMAIN, secure=COOKIE_SECURE
+         )
+         msg = 'Cookie [expiring %(auth_token_expires)s] set for %(email)s' % user.__dict__
+         logger._log(LOGGING_LEVEL, msg, [])
+     
+     def __delete(self):
+         if not self.response:
 -            raise ValueError(_('There is no response.'))
++            raise ValueError(_(astakos_messages.NO_RESPONSE))
+         self.response.delete_cookie(COOKIE_NAME, path='/', domain=COOKIE_DOMAIN)
+         msg = 'Cookie deleted for %(email)s' % self.__dict__
+         logger._log(LOGGING_LEVEL, msg, [])
+     
+     def fix(self, response=None):
+         self.response = response or self.response
+         if self.user.is_authenticated():
+             if not self.is_set or not self.is_valid:
+                 self.__set()
+         else:
+             if self.is_set:
+                 self.__delete()
index 7c868c5,0000000..6ea4f26
mode 100644,000000..100644
--- /dev/null
@@@ -1,97 -1,0 +1,97 @@@
 +# Copyright 2011-2012 GRNET S.A. All rights reserved.
 +#
 +# Redistribution and use in source and binary forms, with or
 +# without modification, are permitted provided that the following
 +# conditions are met:
 +#
 +#   1. Redistributions of source code must retain the above
 +#      copyright notice, this list of conditions and the following
 +#      disclaimer.
 +#
 +#   2. Redistributions in binary form must reproduce the above
 +#      copyright notice, this list of conditions and the following
 +#      disclaimer in the documentation and/or other materials
 +#      provided with the distribution.
 +#
 +# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
 +# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 +# 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 logging
 +
 +from functools import wraps
 +from urlparse import urlparse
 +
 +from astakos.im.settings import QUEUE_CONNECTION
 +
 +if QUEUE_CONNECTION:
 +    from synnefo.lib.queue import (exchange_connect, exchange_send,
 +                                   exchange_close, UserEvent, Receipt
 +                                   )
 +
 +QUEUE_CLIENT_ID = '3'  # Astakos.
 +INSTANCE_ID = '1'
 +RESOURCE = 'addcredits'
 +DEFAULT_CREDITS = 1000
 +
 +logging.basicConfig(format='%(asctime)s [%(levelname)s] %(name)s %(message)s',
 +                    datefmt='%Y-%m-%d %H:%M:%S'
 +                    )
 +logger = logging.getLogger('endpoint.aquarium')
 +
 +
- def wrapper(func):
++def call(func):
 +    @wraps(func)
 +    def wrapper(*args, **kwargs):
 +        if not QUEUE_CONNECTION:
 +            return
 +
 +        try:
 +            body, key = func(*args, **kwargs)
 +            conn = exchange_connect(QUEUE_CONNECTION)
 +            parts = urlparse(QUEUE_CONNECTION)
 +            exchange = parts.path[1:]
 +            routing_key = key % exchange
 +            exchange_send(conn, routing_key, body)
 +            exchange_close(conn)
 +        except BaseException, e:
 +            logger.exception(e)
 +    return wrapper
 +
 +
- @wrapper
++@call
 +def report_user_event(user, create=False):
 +    eventType = 'create' if not create else 'modify'
 +    body = UserEvent(QUEUE_CLIENT_ID, user.email, user.is_active, eventType, {}
 +                     ).format()
 +    routing_key = '%s.user'
 +    return body, routing_key
 +
 +
- @wrapper
++@call
 +def report_user_credits_event(user):
 +    body = Receipt(QUEUE_CLIENT_ID, user.email, INSTANCE_ID, RESOURCE,
 +                   DEFAULT_CREDITS, details={}
 +                   ).format()
 +    routing_key = '%s.resource'
 +    return body, routing_key
 +
 +
 +def report_credits_event():
 +    # better approach?
 +    from astakos.im.models import AstakosUser
 +    map(report_user_credits_event, AstakosUser.objects.all())
index 90f37b1,0000000..648c7dd
mode 100644,000000..100644
--- /dev/null
@@@ -1,297 -1,0 +1,295 @@@
 +# Copyright 2011-2012 GRNET S.A. All rights reserved.
 +#
 +# Redistribution and use in source and binary forms, with or
 +# without modification, are permitted provided that the following
 +# conditions are met:
 +#
 +#   1. Redistributions of source code must retain the above
 +#      copyright notice, this list of conditions and the following
 +#      disclaimer.
 +#
 +#   2. Redistributions in binary form must reproduce the above
 +#      copyright notice, this list of conditions and the following
 +#      disclaimer in the documentation and/or other materials
 +#      provided with the distribution.
 +#
 +# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
 +# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 +# 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 socket
 +import logging
 +import itertools
 +
 +from functools import wraps
- from itertools import tee
 +
 +from django.utils.translation import ugettext as _
 +
 +from astakos.im.settings import QUOTA_HOLDER_URL, LOGGING_LEVEL
 +
 +if QUOTA_HOLDER_URL:
 +    from kamaki.clients.quotaholder import QuotaholderClient
 +
 +ENTITY_KEY = '1'
 +
 +inf = float('inf')
 +
 +logger = logging.getLogger(__name__)
 +
 +inf = float('inf')
 +
 +def call(func_name):
 +    """Decorator function for Quotaholder client calls."""
 +    def decorator(payload_func):
 +        @wraps(payload_func)
 +        def wrapper(entities=(), client=None, **kwargs):
 +            if not entities:
 +                return client, ()
 +
 +            if not QUOTA_HOLDER_URL:
 +                return client, ()
 +
 +            c = client or QuotaholderClient(QUOTA_HOLDER_URL)
 +            func = c.__dict__.get(func_name)
 +            if not func:
 +                return c, ()
 +
 +            data = payload_func(entities, client, **kwargs)
 +            if not data:
 +                return c, data
 +
 +            funcname = func.__name__
 +            kwargs = {'context': {}, funcname: data}
 +            rejected = func(**kwargs)
 +            msg = _('%s: %s - Rejected: %s' % (funcname, data, rejected,))
 +            logger.log(LOGGING_LEVEL, msg)
 +            return c, rejected
 +        return wrapper
 +    return decorator
 +
 +
 +@call('set_quota')
 +def send_quota(users, client=None):
 +    data = []
 +    append = data.append
 +    for user in users:
 +        for resource, uplimit in user.quota.iteritems():
 +            key = ENTITY_KEY
 +            quantity = None
 +            capacity = uplimit if uplimit != inf else None
 +            import_limit = None
 +            export_limit = None
 +            flags = 0
 +            args = (
 +                user.email, resource, key, quantity, capacity, import_limit,
 +                export_limit, flags)
 +            append(args)
 +    return data
 +
 +
 +@call('set_quota')
 +def send_resource_quantities(resources, client=None):
 +    data = []
 +    append = data.append
 +    for resource in resources:
 +        key = ENTITY_KEY
 +        quantity = resource.meta.filter(key='quantity') or None
 +        capacity = None
 +        import_limit = None
 +        export_limit = None
 +        flags = 0
 +        args = (resource.service, resource, key, quantity, capacity,
 +                import_limit, export_limit, flags)
 +        append(args)
 +    return data
 +
 +
 +@call('get_quota')
 +def get_quota(users, client=None):
 +    data = []
 +    append = data.append
 +    for user in users:
 +        try:
 +            entity = user.email
 +        except AttributeError:
 +            continue
 +        else:
 +            for r in user.quota.keys():
 +                args = entity, r, ENTITY_KEY
 +                append(args)
 +    return data
 +
 +
 +@call('create_entity')
 +def create_entities(entities, client=None, field=''):
 +    data = []
 +    append = data.append
 +    for entity in entities:
 +        try:
 +            entity = entity.__getattribute__(field)
 +        except AttributeError:
 +            continue
 +        owner = 'system'
 +        key = ENTITY_KEY
 +        ownerkey = ''
 +        args = entity, owner, key, ownerkey
 +        append(args)
 +    return data
 +
 +
 +def register_users(users, client=None):
 +    users, copy = itertools.tee(users)
 +    client, rejected = create_entities(entities=users,
 +                                       client=client,
 +                                       field='email')
 +    created = (e for e in copy if unicode(e) not in rejected)
 +    return send_quota(created, client)
 +
 +
 +def register_resources(resources, client=None):
 +    resources, copy = itertools.tee(resources)
 +    client, rejected = create_entities(entities=resources,
 +                                       client=client,
 +                                       field='service')
 +    created = (e for e in copy if unicode(e) not in rejected)
 +    return send_resource_quantities(created, client)
 +
 +
 +from datetime import datetime
 +
 +strptime = datetime.strptime
 +timefmt = '%Y-%m-%dT%H:%M:%S.%f'
 +
 +SECOND_RESOLUTION = 1
 +
 +
 +def total_seconds(timedelta_object):
 +    return timedelta_object.seconds + timedelta_object.days * 86400
 +
 +
 +def iter_timeline(timeline, before):
 +    if not timeline:
 +        return
 +
 +    for t in timeline:
 +        yield t
 +
 +    t = dict(t)
 +    t['issue_time'] = before
 +    yield t
 +
 +
 +def _usage_units(timeline, after, before, details=0):
 +
 +    t_total = 0
 +    uu_total = 0
 +    t_after = strptime(after, timefmt)
 +    t_before = strptime(before, timefmt)
 +    t0 = t_after
 +    u0 = 0
 +
 +    for point in iter_timeline(timeline, before):
 +        issue_time = point['issue_time']
 +
 +        if issue_time <= after:
 +            u0 = point['target_allocated_through']
 +            continue
 +
 +        t = strptime(issue_time, timefmt) if issue_time <= before else t_before
 +        t_diff = int(total_seconds(t - t0) * SECOND_RESOLUTION)
 +        t_total += t_diff
 +        uu_cost = u0 * t_diff
 +        uu_total += uu_cost
 +        t0 = t
 +        u0 = point['target_allocated_through']
 +
 +        target = point['target']
 +        if details:
 +            yield  (target,
 +                    point['resource'],
 +                    point['name'],
 +                    issue_time,
 +                    uu_cost,
 +                    uu_total)
 +
 +    if not t_total:
 +        return
 +
 +    yield  (target,
 +            'total',
 +            point['resource'],
 +            issue_time,
 +            uu_total / t_total,
 +            uu_total)
 +
 +
 +def usage_units(timeline, after, before, details=0):
 +    return list(_usage_units(timeline, after, before, details=details))
 +
 +
 +def traffic_units(timeline, after, before, details=0):
 +    tu_total = 0
 +    target = None
 +    issue_time = None
 +
 +    for point in timeline:
 +        issue_time = point['issue_time']
 +        if issue_time <= after:
 +            continue
 +        if issue_time > before:
 +            break
 +
 +        target = point['target']
 +        tu = point['target_allocated_through']
 +        tu_total += tu
 +
 +        if details:
 +            yield  (target,
 +                    point['resource'],
 +                    point['name'],
 +                    issue_time,
 +                    tu,
 +                    tu_total)
 +
 +    if not tu_total:
 +        return
 +
 +    yield  (target,
 +            'total',
 +            point['resource'],
 +            issue_time,
 +            tu_total // len(timeline),
 +            tu_total)
 +
 +
 +def timeline_charge(entity, resource, after, before, details, charge_type):
 +    key = '1'
 +    if charge_type == 'charge_usage':
 +        charge_units = usage_units
 +    elif charge_type == 'charge_traffic':
 +        charge_units = traffic_units
 +    else:
 +        m = 'charge type %s not supported' % charge_type
 +        raise ValueError(m)
 +
 +    quotaholder = QuotaholderClient(QUOTA_HOLDER_URL)
 +    timeline = quotaholder.get_timeline(
 +        context={},
 +        after=after,
 +        before=before,
 +        get_timeline=[[entity, resource, key]])
 +    cu = charge_units(timeline, after, before, details=details)
 +    return cu
@@@ -42,19 -42,21 +42,19 @@@ 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.forms.extras.widgets import SelectDateWidget
 -from django.forms.models import fields_for_model
 +from django.conf import settings
  
- from astakos.im.models import (AstakosUser, EmailChange, AstakosGroup,
-                                Invitation, Membership, GroupKind, Resource,
-                                get_latest_terms, RESOURCE_SEPARATOR)
- 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)
+ 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
++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
+ )
  from astakos.im.widgets import DummyWidget, RecaptchaWidget
  from astakos.im.functions import send_change_email
  
@@@ -152,13 -149,11 +151,12 @@@ class LocalUserCreationForm(UserCreatio
          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.
              user.save()
          return user
  
 +
  class ThirdPartyUserCreationForm(forms.ModelForm):
+     id = forms.CharField(
+         widget=forms.HiddenInput(),
+         label='',
+         required=False
+     )
+     third_party_identifier = forms.CharField(
+         widget=forms.HiddenInput(),
+         label=''
+     )
      class Meta:
          model = AstakosUser
-         fields = ("email", "first_name", "last_name",
-                   "third_party_identifier", "has_signed_terms")
+         fields = ['id', 'email', 'third_party_identifier', 'first_name', 'last_name']
  
      def __init__(self, *args, **kwargs):
          """
              # Overriding field label since we need to apply a link
              # to the terms within the label
              terms_link_html = '<a href="%s" target="_blank">%s</a>' \
 -                    % (reverse('latest_terms'), _("the terms"))
 +                % (reverse('latest_terms'), _("the terms"))
              self.fields['has_signed_terms'].label = \
-                 mark_safe("I agree with %s" % terms_link_html)
+                     mark_safe("I agree with %s" % terms_link_html)
+     
      def clean_email(self):
          email = self.cleaned_data['email']
          if not email:
@@@ -280,26 -277,42 +285,45 @@@ class ShibbolethUserCreationForm(ThirdP
          # copy email value to additional_mail in case user will change it
          name = 'email'
          field = self.fields[name]
-         self.initial['additional_email'] = self.initial.get(name,
-                                                             field.initial)
+         self.initial['additional_email'] = self.initial.get(name, field.initial)
+         self.initial['email'] = None
+     
      def clean_email(self):
          email = self.cleaned_data['email']
+         if self.instance:
+             if self.instance.email == email:
+                 raise forms.ValidationError(_("This is your current email."))
          for user in AstakosUser.objects.filter(email=email):
              if user.provider == 'shibboleth':
-                 raise forms.ValidationError(_(astakos_messages.SHIBBOLETH_EMAIL_USED))
-             elif not user.is_active:
-                 raise forms.ValidationError(_(astakos_messages.SHIBBOLETH_INACTIVE_ACC))
+                 raise forms.ValidationError(_(
+                         "This email is already associated with another \
+                          shibboleth account."
+                     )
+                 )
+             else:
+                 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):
-         super(LoginForm, self).clean()
-         if self.user_cache and self.user_cache.provider not in ('local', ''):
-             raise forms.ValidationError(_(astakos_messages.SUSPENDED_LOCAL_ACC))
+         """
+         Override default behavior in order to check user's activation later
+         """
+         try:
+             super(LoginForm, self).clean()
+         except forms.ValidationError, e:
+             if self.user_cache is None:
+                 raise
+             if self.request:
+                 if not self.request.session.test_cookie_worked():
+                     raise
          return self.cleaned_data
  
 +
  class ProfileForm(forms.ModelForm):
      """
      Subclass of ``ModelForm`` for permiting user to edit his/her profile.
  
      class Meta:
          model = AstakosUser
 -        fields = ('email', 'first_name', 'last_name', 'auth_token', 'auth_token_expires')
 +        fields = ('email', 'first_name', 'last_name', 'auth_token',
 +                  'auth_token_expires')
  
      def __init__(self, *args, **kwargs):
+         self.session_key = kwargs.pop('session_key', None)
          super(ProfileForm, self).__init__(*args, **kwargs)
          instance = getattr(self, 'instance', None)
          ro_fields = ('email', 'auth_token', 'auth_token_expires')
@@@ -521,285 -528,38 +558,291 @@@ class ExtendedPasswordChangeForm(Passwo
          super(ExtendedPasswordChangeForm, self).__init__(user, *args, **kwargs)
  
      def save(self, commit=True):
-         if NEWPASSWD_INVALIDATE_TOKEN or self.cleaned_data.get('renew'):
-             self.user.renew_token()
+         try:
+             if NEWPASSWD_INVALIDATE_TOKEN or self.cleaned_data.get('renew'):
+                 self.user.renew_token()
+             self.user.flush_sessions(current_key=self.session_key)
+         except AttributeError:
+             # if user model does has not such methods
+             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
-                 print '###', resource.group, s, r, uplimit, self.cleaned_data
 +                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
-         print '#', self.cleaned_data
 +        #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():
-             print '####', name, uplimit
 +            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)
 +                
-                 print '#### ####', resource
 +                # 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 = ('homepage', 'desc')
 +
 +
 +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 group')
 +
 +
 +class TimelineForm(forms.Form):
- #    entity = forms.CharField(
- #        widget=forms.HiddenInput(), label='')
 +    entity = forms.ModelChoiceField(
 +        queryset=AstakosUser.objects.filter(is_active=True)
 +    )
 +    resource = forms.ModelChoiceField(
 +        queryset=Resource.objects.all()
 +    )
 +    start_date = forms.DateTimeField()
 +    end_date = forms.DateTimeField()
 +    details = forms.BooleanField(required=False, label="Detailed Listing")
 +    operation = forms.ChoiceField(
 +        label='Charge Method',
 +        choices=(('', '-------------'),
 +                 ('charge_usage', 'Charge Usage'),
 +                 ('charge_traffic', 'Charge Traffic'), )
 +    )
 +
 +    def clean(self):
 +        super(TimelineForm, self).clean()
 +        d = self.cleaned_data
 +        if 'resource' in d:
 +            d['resource'] = str(d['resource'])
 +        if 'start_date' in d:
 +            d['start_date'] = d['start_date'].strftime(
 +                "%Y-%m-%dT%H:%M:%S.%f")[:24]
 +        if 'end_date' in d:
 +            d['end_date'] = d['end_date'].strftime("%Y-%m-%dT%H:%M:%S.%f")[:24]
 +        if 'entity' in d:
 +            d['entity'] = d['entity'].email
 +        return d
 +
 +
 +class AstakosGroupSortForm(forms.Form):
 +    sort_by = forms.ChoiceField(label='Sort by',
 +                                choices=(('groupname', 'Name'),
 +                                         ('kindname', 'Type'),
 +                                         ('issue_date', 'Issue Date'),
 +                                         ('expiration_date',
 +                                          'Expiration Date'),
 +                                         ('approved_members_num',
 +                                          'Participants'),
 +                                         ('is_enabled', 'Status'),
 +                                         ('moderation_enabled', 'Moderation'),
 +                                         ('membership_status',
 +                                          'Enrollment Status')
 +                                         ),
 +                                required=False)
 +
 +
 +class MembersSortForm(forms.Form):
 +    sort_by = forms.ChoiceField(label='Sort by',
 +                                choices=(('person__email', 'User Id'),
 +                                         ('person__first_name', 'Name'),
 +                                         ('date_joined', 'Status')
 +                                         ),
 +                                required=False)
 +
 +
 +class PickResourceForm(forms.Form):
 +    resource = forms.ModelChoiceField(
 +        queryset=Resource.objects.select_related().all()
 +    )
 +    resource.widget.attrs["onchange"] = "this.form.submit()"
 +
 +
  class ExtendedSetPasswordForm(SetPasswordForm):
      """
      Extends SetPasswordForm by enabling user
      to optionally renew also the token.
      """
      if not NEWPASSWD_INVALIDATE_TOKEN:
-         renew = forms.BooleanField(label='Renew token', required=False,
-                                    initial=True,
-                                    help_text='Unsetting this may result in security risk.')
+         renew = forms.BooleanField(
+             label='Renew token',
+             required=False,
+             initial=True,
+             help_text='Unsetting this may result in security risk.'
+         )
+     
      def __init__(self, user, *args, **kwargs):
          super(ExtendedSetPasswordForm, self).__init__(user, *args, **kwargs)
 -    
 +
      def save(self, commit=True):
-         if NEWPASSWD_INVALIDATE_TOKEN or self.cleaned_data.get('renew'):
-             if isinstance(self.user, AstakosUser):
+         try:
+             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()
+         except BaseException, e:
+             logger.exception(e)
 -            pass
          return super(ExtendedSetPasswordForm, self).save(commit=commit)
@@@ -38,12 -38,14 +38,13 @@@ from django.utils.translation import ug
  from django.template.loader import render_to_string
  from django.core.mail import send_mail
  from django.core.urlresolvers import reverse
--from django.core.exceptions import ValidationError
  from django.template import Context, loader
- from django.contrib.auth import login as auth_login, logout as auth_logout
- from django.http import HttpRequest
+ from django.contrib.auth import (
+     login as auth_login,
 -    logout as auth_logout,
 -    SESSION_KEY
++    logout as auth_logout
+ )
 -from django.http import HttpRequest
 +from django.conf import settings
 +from django.contrib.auth.models import AnonymousUser
  
  from urllib import quote
  from urlparse import urljoin
@@@ -51,17 -53,14 +52,15 @@@ from smtplib import SMTPExceptio
  from datetime import datetime
  from functools import wraps
  
- from astakos.im.settings import (DEFAULT_CONTACT_EMAIL, SITENAME, BASEURL,
-                                  LOGGING_LEVEL, VERIFICATION_EMAIL_SUBJECT,
-                                  ACCOUNT_CREATION_SUBJECT,
-                                  GROUP_CREATION_SUBJECT,
-                                  HELPDESK_NOTIFICATION_EMAIL_SUBJECT,
-                                  INVITATION_EMAIL_SUBJECT,
-                                  GREETING_EMAIL_SUBJECT,
-                                  FEEDBACK_EMAIL_SUBJECT,
-                                  EMAIL_CHANGE_EMAIL_SUBJECT)
- import astakos.im.models
+ from astakos.im.settings import (
 -    DEFAULT_CONTACT_EMAIL, DEFAULT_FROM_EMAIL,
 -    SITENAME, BASEURL, DEFAULT_ADMIN_EMAIL, LOGGING_LEVEL,
 -    VERIFICATION_EMAIL_SUBJECT, ADMIN_NOTIFICATION_EMAIL_SUBJECT,
 -    HELPDESK_NOTIFICATION_EMAIL_SUBJECT, INVITATION_EMAIL_SUBJECT,
 -    GREETING_EMAIL_SUBJECT, FEEDBACK_EMAIL_SUBJECT, EMAIL_CHANGE_EMAIL_SUBJECT
++    DEFAULT_CONTACT_EMAIL, SITENAME, BASEURL, LOGGING_LEVEL,
++    VERIFICATION_EMAIL_SUBJECT, ACCOUNT_CREATION_SUBJECT,
++    GROUP_CREATION_SUBJECT, HELPDESK_NOTIFICATION_EMAIL_SUBJECT,
++    INVITATION_EMAIL_SUBJECT, GREETING_EMAIL_SUBJECT, FEEDBACK_EMAIL_SUBJECT,
++    EMAIL_CHANGE_EMAIL_SUBJECT
+ )
 -from astakos.im.models import Invitation, AstakosUser, SessionCatalog
 +import astakos.im.messages as astakos_messages
++import astakos.im.models
  
  logger = logging.getLogger(__name__)
  
@@@ -82,10 -81,14 +81,17 @@@ def logged(func, msg)
          return r
      return with_logging
  
- login = logged(auth_login, '%s logged in.')
+ def login(request, user):
+     auth_login(request, user)
 -    SessionCatalog(session_key=request.session.session_key, user=user).save()
++    astakos.im.models.SessionCatalog(
++        session_key=request.session.session_key,
++        user=user).save()
+ login = logged(login, '%s logged in.')
  logout = logged(auth_logout, '%s logged out.')
  
 +
  def send_verification(user, template_name='im/activation_email.txt'):
      """
      Send email to user to verify his/her email and activate his/her account.
@@@ -201,10 -187,7 +207,10 @@@ def send_invitation(invitation, templat
          raise SendInvitationError()
      else:
          msg = 'Sent invitation %s' % invitation
 -        logger._log(LOGGING_LEVEL, msg, [])
 +        logger.log(LOGGING_LEVEL, msg)
-         invitation.inviter.invitations = max(0, self.invitations - 1)
++        invitation.inviter.invitations = max(0, invitation.inviter.invitations - 1)
 +        invitation.inviter.save()
 +
  
  def send_greeting(user, email_template_name='im/welcome_email.txt'):
      """
  
  from optparse import make_option
  
--from django.core.management.base import BaseCommand, CommandError
 -from django.contrib.auth.models import Group
++from django.core.management.base import NoArgsCommand
  
 -from astakos.im.models import AstakosUser
 +from astakos.im.models import AstakosGroup
  
  from ._common import format_bool
  
  
--class Command(BaseCommand):
++class Command(NoArgsCommand):
      help = "List groups"
-     option_list = BaseCommand.option_list + (
+     
 -    option_list = BaseCommand.option_list + (
++    option_list = NoArgsCommand.option_list + (
          make_option('-c',
 -            action='store_true',
 -            dest='csv',
 -            default=False,
 -            help="Use pipes to separate values"),
 +                    action='store_true',
 +                    dest='csv',
 +                    default=False,
 +                    help="Use pipes to separate values"),
 +        make_option('-p',
 +                    action='store_true',
 +                    dest='pending',
 +                    default=False,
 +                    help="List only groups pending enable"),
      )
 -    
 -    def handle(self, *args, **options):
 -        if args:
 -            raise CommandError("Command doesn't accept any arguments")
 -        
 -        groups = Group.objects.all().order_by('id')
 -        
 -        labels = ('id', 'name', 'permissions')
 -        columns = (3, 12, 50)
 -        
 -        if not options['csv']:
 +
-     def handle(self, *args, **options):
++    def handle_noargs(self, **options):
 +        groups = AstakosGroup.objects.all()
 +
 +        if options.get('pending'):
 +            groups = filter(lambda g: g.is_disabled, groups)
 +
 +        labels = ('id', 'name', 'enabled', 'moderation', 'permissions')
 +        columns = (3, 25, 12, 12, 50)
 +
 +        if not options.get('csv'):
              line = ' '.join(l.rjust(w) for l, w in zip(labels, columns))
              self.stdout.write(line + '\n')
              sep = '-' * len(line)
  # interpreted as representing official policies, either expressed
  # or implied, of GRNET S.A.
  
--from optparse import make_option
--
  from django.core.management.base import BaseCommand, CommandError
--from django.contrib.auth.models import Group, Permission
++from django.contrib.auth.models import Group
  from django.contrib.contenttypes.models import ContentType
--from django.core.exceptions import ValidationError
  
--from astakos.im.models import AstakosUser
  from ._common import add_group_permission
  
 +
  class Command(BaseCommand):
      args = "<groupname> <permission> [<permissions> ...]"
      help = "Add group permissions"
@@@ -54,12 -54,12 +54,12 @@@ class Command(BaseCommand)
      def handle(self, *args, **options):
          if args:
              raise CommandError("Command doesn't accept any arguments")
-         invitations = Invitation.objects.all()
+         
+         invitations = Invitation.objects.all().order_by('id')
+         
          labels = ('id', 'inviter', 'email', 'real name', 'code', 'consumed')
          columns = (3, 24, 24, 24, 20, 4, 8)
 -        
 +
          if not options['csv']:
              line = ' '.join(l.rjust(w) for l, w in zip(labels, columns))
              self.stdout.write(line + '\n')
index fb76504,0000000..307a271
mode 100644,000000..100644
--- /dev/null
@@@ -1,53 -1,0 +1,52 @@@
 +# Copyright 2012 GRNET S.A. All rights reserved.
 +#
 +# Redistribution and use in source and binary forms, with or
 +# without modification, are permitted provided that the following
 +# conditions are met:
 +#
 +#   1. Redistributions of source code must retain the above
 +#      copyright notice, this list of conditions and the following
 +#      disclaimer.
 +#
 +#   2. Redistributions in binary form must reproduce the above
 +#      copyright notice, this list of conditions and the following
 +#      disclaimer in the documentation and/or other materials
 +#      provided with the distribution.
 +#
 +# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
 +# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 +# 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.
 +
 +from django.core.management.base import BaseCommand, CommandError
- from django.db.utils import IntegrityError
 +
 +from astakos.im.models import AstakosUser, Resource
 +from astakos.im.endpoints.qh import register_users, register_resources
 +
 +import logging
 +logger = logging.getLogger(__name__)
 +
 +
 +class Command(BaseCommand):
 +    help = "Send user information and resource quota in the Quotaholder"
 +
 +    def handle(self, *args, **options):
 +        try:
 +            register_resources(Resource.objects.all())
 +            register_users(AstakosUser.objects.all())
 +        except BaseException, e:
 +            logger.exception(e)
 +            raise CommandError("Syncing failed.")
index e6e4a5d,0000000..93bd736
mode 100644,000000..100644
--- /dev/null
@@@ -1,72 -1,0 +1,72 @@@
 +# Copyright 2012 GRNET S.A. All rights reserved.
 +#
 +# Redistribution and use in source and binary forms, with or
 +# without modification, are permitted provided that the following
 +# conditions are met:
 +#
 +#   1. Redistributions of source code must retain the above
 +#      copyright notice, this list of conditions and the following
 +#      disclaimer.
 +#
 +#   2. Redistributions in binary form must reproduce the above
 +#      copyright notice, this list of conditions and the following
 +#      disclaimer in the documentation and/or other materials
 +#      provided with the distribution.
 +#
 +# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
 +# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 +# 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.
 +
 +from optparse import make_option
 +
- from django.core.management.base import BaseCommand, CommandError
++from django.core.management.base import BaseCommand
 +
 +from astakos.im.models import Resource
 +
 +
 +class Command(BaseCommand):
 +    help = "List resources"
 +
 +    option_list = BaseCommand.option_list + (
 +        make_option('-c',
 +                    action='store_true',
 +                    dest='csv',
 +                    default=False,
 +                    help="Use pipes to separate values"),
 +    )
 +
 +    def handle(self, *args, **options):
 +        resources = Resource.objects.select_related().all()
 +
 +        labels = ('id', 'service', 'name')
 +        columns = (3, 40, 40)
 +
 +        if not options['csv']:
 +            line = ' '.join(l.rjust(w) for l, w in zip(labels, columns))
 +            self.stdout.write(line + '\n')
 +            sep = '-' * len(line)
 +            self.stdout.write(sep + '\n')
 +
 +        for r in resources:
 +            fields = (str(r.id), r.service.name, r.name)
 +
 +            if options['csv']:
 +                line = '|'.join(fields)
 +            else:
 +                line = ' '.join(f.rjust(w) for f, w in zip(fields, columns))
 +
 +            self.stdout.write(line.encode('utf8') + '\n')
index b07c562,0000000..da2415f
mode 100644,000000..100644
--- /dev/null
@@@ -1,58 -1,0 +1,57 @@@
 +# Copyright 2012 GRNET S.A. All rights reserved.
 +#
 +# Redistribution and use in source and binary forms, with or
 +# without modification, are permitted provided that the following
 +# conditions are met:
 +#
 +#   1. Redistributions of source code must retain the above
 +#      copyright notice, this list of conditions and the following
 +#      disclaimer.
 +#
 +#   2. Redistributions in binary form must reproduce the above
 +#      copyright notice, this list of conditions and the following
 +#      disclaimer in the documentation and/or other materials
 +#      provided with the distribution.
 +#
 +# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
 +# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 +# 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.
 +
 +from django.core.management.base import BaseCommand, CommandError
- from django.db.utils import IntegrityError
 +
 +from astakos.im.models import Resource
 +
 +
 +class Command(BaseCommand):
 +    args = "<resource>"
 +    help = "Add a resource"
 +
 +    def handle(self, *args, **options):
 +        if len(args) < 1:
 +            raise CommandError("Invalid number of arguments")
 +
 +        kwargs = {}
 +        if args[0].isdigit():
 +            kwargs['id'] = args[0]
 +        else:
 +            kwargs['name'] = args[0]
 +
 +        try:
 +            r = Resource.objects.get(**kwargs)
 +        except Resource.DoesNotExist, e:
 +            raise CommandError("Invalid resource")
 +        r.delete()
@@@ -33,8 -33,7 +33,7 @@@
  
  from django.core.management.base import BaseCommand, CommandError
  
--from astakos.im.models import Service
 +from astakos.im.api.callpoint import AstakosCallpoint
  
  class Command(BaseCommand):
      args = "<name> <url> [<icon>]"
  
  from optparse import make_option
  
--from django.core.management.base import BaseCommand, CommandError
++from django.core.management.base import NoArgsCommand
  
  from astakos.im.models import Service
  
 -class Command(BaseCommand):
 +
- class Command(BaseCommand):
++class Command(NoArgsCommand):
      help = "List services"
  
--    option_list = BaseCommand.option_list + (
++    option_list = NoArgsCommand.option_list + (
          make_option('-c',
 -            action='store_true',
 -            dest='csv',
 -            default=False,
 -            help="Use pipes to separate values"),
 +                    action='store_true',
 +                    dest='csv',
 +                    default=False,
 +                    help="Use pipes to separate values"),
      )
  
--    def handle(self, *args, **options):
-         services = Service.objects.all()
 -        if args:
 -            raise CommandError("Command doesn't accept any arguments")
 -
++    def handle_noargs(self, **options):
+         services = Service.objects.all().order_by('id')
  
          labels = ('id', 'name', 'url', 'auth_token', 'icon')
          columns = (3, 12, 40, 20, 20)
  import socket
  
  from optparse import make_option
- from uuid import uuid4
- from datetime import datetime
 -from random import choice
 -from string import digits, lowercase, uppercase
 -from uuid import uuid4
  
  from django.core.management.base import BaseCommand, CommandError
  from django.core.validators import validate_email
  from django.core.exceptions import ValidationError
 -from django.contrib.auth.models import Group
  
  from astakos.im.models import AstakosUser
 -from astakos.im.util import reserved_email
 +from astakos.im.api.callpoint import AstakosCallpoint
 +
- from ._common import add_user_permission
 +def filter_custom_options(options):
 +    base_dests = list(
 +        getattr(o, 'dest', None) for o in BaseCommand.option_list)
 +    return dict((k, v) for k, v in options.iteritems() if k not in base_dests)
  
 -from ._common import add_user_permission
  
  class Command(BaseCommand):
 -    args = "<email> <first name> <last name> <affiliation>"
 +    args = "<email>"
      help = "Create a user"
 -    
 +
      option_list = BaseCommand.option_list + (
 +        make_option('--first-name',
 +                    dest='first_name',
 +                    metavar='NAME',
 +                    help="Set user's first name"),
 +        make_option('--last-name',
 +                    dest='last_name',
 +                    metavar='NAME',
 +                    help="Set user's last name"),
 +        make_option('--affiliation',
 +                    dest='affiliation',
 +                    metavar='AFFILIATION',
 +                    help="Set user's affiliation"),
 +        make_option('--password',
 +                    dest='password',
 +                    metavar='PASSWORD',
 +                    help="Set user's password"),
          make_option('--active',
 -            action='store_true',
 -            dest='active',
 -            default=False,
 -            help="Activate user"),
 +                    action='store_true',
 +                    dest='is_active',
 +                    default=False,
 +                    help="Activate user"),
          make_option('--admin',
 -            action='store_true',
 -            dest='admin',
 -            default=False,
 -            help="Give user admin rights"),
 -        make_option('--password',
 -            dest='password',
 -            metavar='PASSWORD',
 -            help="Set user's password"),
 -        make_option('--add-group',
 -            dest='add-group',
 -            help="Add user group"),
 -        make_option('--add-permission',
 -            dest='add-permission',
 -            help="Add user permission")
 -        )
 -    
 +                    action='store_true',
 +                    dest='is_superuser',
 +                    default=False,
 +                    help="Give user admin rights"),
 +        make_option('-g',
 +                    action='append',
 +                    dest='groups',
 +                    help="Add user group (may be used multiple times)"),
 +        make_option('-p',
 +                    action='append',
 +                    dest='permissions',
 +                    help="Add user permission (may be used multiple times)")
 +    )
 +
      def handle(self, *args, **options):
 -        if len(args) != 4:
 +        if len(args) != 1:
              raise CommandError("Invalid number of arguments")
 -        
 -        args = [a.decode('utf8') for a in args]
 -        email, first, last, affiliation = args
 -        
 +
 +        email = args[0].decode('utf8')
 +
          try:
 -            validate_email( email )
 +            validate_email(email)
          except ValidationError:
              raise CommandError("Invalid email")
 -        
 -        username =  uuid4().hex[:30]
 -        password = options.get('password')
 -        if password is None:
 -            password = AstakosUser.objects.make_random_password()
 -        
 -        if reserved_email(email):
 -            raise CommandError("A user with this email already exists")
 -        
 -        user = AstakosUser(username=username, first_name=first, last_name=last,
 -                           email=email, affiliation=affiliation,
 -                           provider='local')
 -        user.set_password(password)
 -        
 -        if options['active']:
 -            user.is_active = True
 -        if options['admin']:
 -            user.is_admin = True
 -        
 +
 +        u = {'email': email}
 +        u.update(filter_custom_options(options))
 +        if not u.get('password'):
 +            u['password'] = AstakosUser.objects.make_random_password()
 +
          try:
 -            user.save()
 +            c = AstakosCallpoint()
 +            r = c.create_users((u,))
          except socket.error, e:
              raise CommandError(e)
          except ValidationError, e:
              raise CommandError(e)
          else:
 -            msg = "Created user id %d" % (user.id,)
 -            if options['password'] is None:
 -                msg += " with password '%s'" % (password,)
 -            self.stdout.write(msg + '\n')
 -            
 -            groupname = options.get('add-group')
 -            if groupname is not None:
 -                try:
 -                    group = Group.objects.get(name=groupname)
 -                    user.groups.add(group)
 -                    self.stdout.write('Group: %s added successfully\n' % groupname)
 -                except Group.DoesNotExist, e:
 -                    self.stdout.write('Group named %s does not exist\n' % groupname)
 -            
 -            pname = options.get('add-permission')
 -            if pname is not None:
 -                try:
 -                    r, created = add_user_permission(user, pname)
 -                    if created:
 -                        self.stdout.write('Permission: %s created successfully\n' % pname)
 -                    if r > 0:
 -                        self.stdout.write('Permission: %s added successfully\n' % pname)
 -                    elif r==0:
 -                        self.stdout.write('User has already permission: %s\n' % pname)
 -                except Exception, e:
 -                    raise CommandError(e)
 +            failed = (res for res in r if not res.is_success)
 +            for r in failed:
 +                if not r.is_success:
 +                    raise CommandError(r.reason)
 +            if not failed:
 +                self.stdout.write('User created successfully')
 +                if not u.get('password'):
-                     self.stdout.write('with password: %s' % u['password'])
++                    self.stdout.write('with password: %s' % u['password'])
@@@ -35,8 -37,8 +35,7 @@@ from django.core.management.base impor
  from django.db.utils import IntegrityError
  from django.db import transaction
  
 -from astakos.im.functions import invite, SendMailError
 -from astakos.im.models import Invitation
 +from astakos.im.functions import SendMailError
- from astakos.im.models import Invitation
  
  from ._common import get_user
  
  
  from optparse import make_option
  
--from django.core.management.base import BaseCommand, CommandError
++from django.core.management.base import NoArgsCommand
  
  from astakos.im.models import AstakosUser
  
  from ._common import format_bool
  
  
--class Command(BaseCommand):
++class Command(NoArgsCommand):
      help = "List users"
 -    
 -    option_list = BaseCommand.option_list + (
 +
-     option_list = BaseCommand.option_list + (
++    option_list = NoArgsCommand.option_list + (
          make_option('-c',
 -            action='store_true',
 -            dest='csv',
 -            default=False,
 -            help="Use pipes to separate values"),
 +                    action='store_true',
 +                    dest='csv',
 +                    default=False,
 +                    help="Use pipes to separate values"),
          make_option('-p',
 -            action='store_true',
 -            dest='pending',
 -            default=False,
 -            help="List only users pending activation"),
 +                    action='store_true',
 +                    dest='pending',
 +                    default=False,
 +                    help="List only users pending activation"),
          make_option('-n',
 -            action='store_true',
 -            dest='pending_send_mail',
 -            default=False,
 -            help="List only users who have not received activation"),
 -        )
 -    
 -    def handle(self, *args, **options):
 -        if args:
 -            raise CommandError("Command doesn't accept any arguments")
 -        
 +                    action='store_true',
 +                    dest='pending_send_mail',
 +                    default=False,
 +                    help="List only users who have not received activation"),
 +    )
 +
-     def handle(self, *args, **options):
-         if args:
-             raise CommandError("Command doesn't accept any arguments")
-         users = AstakosUser.objects.all()
++    def handle_noargs(self, **options):
+         users = AstakosUser.objects.all().order_by('id')
          if options['pending']:
              users = users.filter(is_active=False)
          elif options['pending_send_mail']:
              id = str(user.id)
              active = format_bool(user.is_active)
              admin = format_bool(user.is_superuser)
 -            fields = (id, user.email, user.realname, active, admin, user.provider,
 -                      ','.join([g.name for g in user.groups.all()]))
 -            
 +            fields = (
-                 id, user.email, user.realname, active, admin, user.provider)
++                id, user.email, user.realname, active, admin, user.provider
++            )
 +
              if options['csv']:
                  line = '|'.join(fields)
              else:
@@@ -34,8 -34,8 +34,7 @@@
  from optparse import make_option
  
  from django.core.management.base import BaseCommand, CommandError
--from django.contrib.auth.models import Group, Permission
--from django.contrib.contenttypes.models import ContentType
++from django.contrib.auth.models import Group
  from django.core.exceptions import ValidationError
  
  from astakos.im.models import AstakosUser
index 4595f4e,0000000..ac1b024
mode 100644,000000..100644
--- /dev/null
@@@ -1,113 -1,0 +1,128 @@@
 +# Copyright 2011-2012 GRNET S.A. All rights reserved.
 +#
 +# Redistribution and use in source and binary forms, with or
 +# without modification, are permitted provided that the following
 +# conditions are met:
 +#
 +#   1. Redistributions of source code must retain the above
 +#      copyright notice, this list of conditions and the following
 +#      disclaimer.
 +#
 +#   2. Redistributions in binary form must reproduce the above
 +#      copyright notice, this list of conditions and the following
 +#      disclaimer in the documentation and/or other materials
 +#      provided with the distribution.
 +#
 +# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
 +# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 +# 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.
 +
 +ACCOUNT_AUTHENTICATION_FAILED           =   'Cannot authenticate account.'
- ACCOUNT_INACTIVE                        =   'Inactive account.'
 +ACCOUNT_ALREADY_ACTIVE                  =   'Account is already active.'
++ACCOUNT_PENDING_ACTIVATION              =   'Your request is pending activation.'
++ACCOUNT_RESEND_ACTIVATION               =   'You have not followed the activation link. <a href="%(send_activation_url)s">Resend activation email?</a>'
++INACTIVE_ACCOUNT_CHANGE_EMAIL           =   ''.join([ACCOUNT_RESEND_ACTIVATION, ' or <a href="%(signup_url)s">Provide new email?</a>'])
++
++ACCOUNT_UNKNOWN                         =   'There is no such account.'
 +TOKEN_UNKNOWN                           =   'There is no user matching this token.'
 +
- INVITATION_SENT                         =   'Invitation sent to %(email)s.'
 +PROFILE_UPDATED                         =   'Profile has been updated successfully.'
 +FEEDBACK_SENT                           =   'Feedback successfully sent.'
 +EMAIL_CHANGED                           =   'Account email has been changed successfully.'
 +EMAIL_CHANGE_REGISTERED                 =   'Change email request has been registered succefully. \
 +                                               You are going to receive a verification email in the new address.'
 +
 +OBJECT_CREATED                          =   'The %(verbose_name)s was created successfully.'
 +MEMBER_JOINED_GROUP                     =   '%(realname)s has been successfully joined the group.'
 +MEMBER_REMOVED                          =   '%(realname)s has been successfully removed from the group.'
 +BILLING_ERROR                           =   'Service response status: %(status)d' 
 +LOGOUT_SUCCESS                          =   'You have successfully logged out.'
 +
 +GENERIC_ERROR                           =   'Something wrong has happened. \
 +                                               Please contact the administrators for more details.'
 +
 +MAX_INVITATION_NUMBER_REACHED   =           'There are no invitations left.'
 +GROUP_MAX_PARTICIPANT_NUMBER_REACHED    =   'Group maximum participant number has been reached.'
 +NO_APPROVAL_TERMS                       =   'There are no approval terms.'
 +PENDING_EMAIL_CHANGE_REQUEST            =   'There is already a pending change email request.'
 +OBJECT_CREATED_FAILED                   =   'The %(verbose_name)s creation failed: %(reason)s.'
 +GROUP_JOIN_FAILURE                      =   'Failed to join group.'
 +GROUPKIND_UNKNOWN                       =   'There is no such a group kind'
 +NOT_MEMBER                              =   'User is not member of the group.'
 +NOT_OWNER                               =   'User is not a group owner.'
 +OWNER_CANNOT_LEAVE_GROUP                =   'Owner cannot leave the group.'
 +
 +# Field validation fields
 +REQUIRED_FIELD                          =   'This field is required.'
 +EMAIL_USED                              =   'This email address is already in use. Please supply a different email address.'
 +SHIBBOLETH_EMAIL_USED                   =   'This email is already associated with another shibboleth account.'
 +SHIBBOLETH_INACTIVE_ACC                 =   'This email is already associated with an inactive account. \
 +                                               You need to wait to be activated before being able to switch to a shibboleth account.'   
++SHIBBOLETH_MISSING_EPPN                 =   'Missing unique token in request.'
++SHIBBOLETH_MISSING_NAME                 =   'Missing user name in request.'
 +
 +SIGN_TERMS                              =   'You have to agree with the terms.'
 +CAPTCHA_VALIDATION_ERR                  =   'You have not entered the correct words.'
 +SUSPENDED_LOCAL_ACC                     =   'Local login is not the current authentication method for this account.'
 +UNUSABLE_PASSWORD                       =   'This account has not a usable password.'
 +EMAIL_UNKNOWN                           =   'That e-mail address doesn\'t have an associated user account. \
 +                                               Are you sure you\'ve registered?'
 +INVITATION_EMAIL_EXISTS                 =   'There is already invitation for this email.'
 +INVITATION_CONSUMED_ERR                 =   'Invitation is used.'
 +UNKNOWN_USERS                           =   'Unknown users: %s'
 +UNIQUE_EMAIL_IS_ACTIVE_CONSTRAIN_ERR    =   'Another account with the same email & is_active combination found.'
 +INVALID_ACTIVATION_KEY                  =   'Invalid activation key.'
 +NEW_EMAIL_ADDR_RESERVED                 =   'The new email address is reserved.'
 +EMAIL_RESERVED                          =   'Email: %(email)s is reserved'
++NO_LOCAL_AUTH                           =   'Local login is not the current authentication method for this account.'
++SWITCH_ACCOUNT_FAILURE                  =   'Account failed to switch. Invalid parameters.'
++SWITCH_ACCOUNT_SUCCESS_WITH_PROVIDER    =   'Account failed to switch to %(provider)s.' 
++SWITCH_ACCOUNT_SUCCESS                  =   'Account successfully switched to %(provider)s.'
 +
 +# Field help text
 +ADD_GROUP_MEMBERS_Q_HELP                =   'Add comma separated user emails, eg. user1@user.com, user2@user.com'
 +ASTAKOSUSER_GROUPS_HELP                 =   'In addition to the permissions manually assigned, \
 +                                               this user will also get all permissions granted to each group he/she is in.'
 +EMAIL_CHANGE_NEW_ADDR_HELP              =   'Your old email address will be used until you verify your new one.'
 +
 +EMAIL_SEND_ERR                          =   'Failed to send %s.'
 +ADMIN_NOTIFICATION_SEND_ERR             =   EMAIL_SEND_ERR % 'admin notification'
 +VERIFICATION_SEND_ERR                   =   EMAIL_SEND_ERR % 'verification'
 +INVITATION_SEND_ERR                     =   EMAIL_SEND_ERR % 'invitation'
 +GREETING_SEND_ERR                       =   EMAIL_SEND_ERR % 'greeting'
 +FEEDBACK_SEND_ERR                       =   EMAIL_SEND_ERR % 'feedback'
 +CHANGE_EMAIL_SEND_ERR                   =   EMAIL_SEND_ERR % 'feedback'
 +NOTIFICATION_SEND_ERR                   =   EMAIL_SEND_ERR % 'notification'
 +
 +MISSING_NEXT_PARAMETER                  =   'No next parameter'
 +
++INVITATION_SENT                         =   'Invitation sent to %(email)s.'
 +VERIFICATION_SENT                       =   'Verification sent.'
 +SWITCH_ACCOUNT_LINK_SENT                =   'This email is already associated with another local account. \
 +                                               To change this account to a shibboleth one follow the link in the verification email sent to %(email)s. \
 +                                               Otherwise just ignore it.'
- NOTIFACATION_SENT                       =   'Your request for an account was successfully received and is now pending approval. \
++NOTIFICATION_SENT                       =   'Your request for an account was successfully received and is now pending approval. \
 +                                               You will be notified by email in the next few days. \
 +                                               Thanks for your interest in ~okeanos! The GRNET team.'
- REGISTRATION_COMPLETED                  =   'Registration completed. You can now login.'
++ACTIVATION_SENT                         =   'Activation sent.'
++
++REGISTRATION_COMPLETED                  =   'Registration completed. You can now login.'
++
++NO_RESPONSE                             =   'There is no response.'
++NOT_ALLOWED_NEXT_PARAM                  =   'Not allowed next parameter.'
++MISSING_KEY_PARAMETER                   =   'Missing key parameter.'
++INVALID_KEY_PARAMETER                   =   'Invalid key.'
  # interpreted as representing official policies, either expressed
  # or implied, of GRNET S.A.
  
--from urllib import unquote
- from django.contrib.auth import authenticate
+ from urlparse import urlunsplit, urlsplit
  
- from astakos.im.settings import COOKIE_NAME
- from astakos.im.functions import login
+ from django.http import HttpResponse
+ from django.utils.http import urlencode
+ from astakos.im.cookie import Cookie
 -from astakos.im.settings import COOKIE_NAME
+ from astakos.im.util import get_query
  
 +
  class CookieAuthenticationMiddleware(object):
      def process_request(self, request):
-         assert hasattr(request, 'session'), "The Django authentication middleware requires session middleware to be installed. Edit your MIDDLEWARE_CLASSES setting to insert 'django.contrib.sessions.middleware.SessionMiddleware'."
-         if request.user.is_authenticated():
-             return None
-         cookie = unquote(request.COOKIES.get(COOKIE_NAME, ''))
-         email, sep, auth_token = cookie.partition('|')
-         if not sep:
-             return None
-         try:
-             user = authenticate(email=email, auth_token=auth_token)
-             if user:
-                 request.user = user
-             login(request, user)
-         except:
-             pass
-         return None
+         cookie = Cookie(request)
+         if cookie.is_valid:
+             return
+         
+         response = HttpResponse(status=302)
+         
+         parts = list(urlsplit(request.path))
+         params = get_query(request)
+         parts[3] = urlencode(params)
+         url = urlunsplit(parts)
+         
+         response['Location'] = url
+         cookie.fix(response)
+         return response
+     
+     def process_response(self, request, response):
+         Cookie(request, response).fix()
+         return response
index 00a534b,0000000..53bbd24
mode 100644,000000..100644
--- /dev/null
@@@ -1,236 -1,0 +1,236 @@@
 +# encoding: utf-8
 +import datetime
 +from south.v2 import DataMigration
 +from django.db.models import Count
 +
 +
 +class Migration(DataMigration):
 +    def forwards(self, orm):
 +
 +        def _create_groupkind(name):
 +            try:
 +                orm.GroupKind(name=name).save()
 +            except:
 +                pass
 +
 +        t = ('default', 'course', 'project', 'laboratory', 'organization')
 +        map(_create_groupkind, t)
 +
 +        default = orm.GroupKind.objects.get(name='default')
 +
 +        groups = {}
 +
 +        def _create_astakogroup(name):
 +            try:
-                 groups[name] = orm.AstakosGroup.objects.get(name=name)
++                groups[name] = orm['im.AstakosGroup'].objects.get(name=name)
 +            except orm.AstakosGroup.DoesNotExist:
 +                try:
 +                    g = orm['auth.Group'].objects.get(name=name)
 +                    groups[
 +                        name] = extended = orm.AstakosGroup(group_ptr_id=g.pk)
 +                    extended.__dict__.update(g.__dict__)
 +                    extended.kind = default
 +                    extended.approval_date = datetime.datetime.now()
 +                    extended.issue_date = datetime.datetime.now()
 +                    extended.moderation_enabled = False
 +                    extended.save()
 +                    map(lambda u: orm.Membership(group=extended,
 +                                                 person=orm.AstakosUser.objects.get(id=u.id),
 +                                                 date_joined=datetime.datetime.now()
 +                                                 ).save(),
 +                        g.user_set.all())
 +                except orm['auth.Group'].DoesNotExist:
 +                    groups[name] = orm.AstakosGroup(name=name,
 +                                                    kind=default,
 +                                                    approval_date=datetime.datetime.now(),
 +                                                    issue_date=datetime.datetime.now(),
 +                                                    moderation_enabled=False
 +                                                    )
 +                    groups[name].save()
 +
 +        # catch integrate
 +        t = ('default', 'shibboleth', 'helpdesk', 'faculty',
 +             'ugrad', 'grad', 'researcher', 'associate')
 +        map(_create_astakogroup, t)
 +
 +        orphans = orm.AstakosUser.objects.annotate(
 +            num_groups=Count('astakos_groups')).filter(num_groups=0)
 +        map(lambda u: orm.Membership(group=groups['default'],
 +                                     person=u, date_joined=datetime.datetime.now()).save(), orphans)
 +
 +    def backwards(self, orm):
 +        def _delete_groupkind(name):
 +            try:
 +                orm.GroupKind.objects.get(name=name).delete()
 +            except orm.GroupKind.DoesNotExist:
 +                pass
 +
 +        def _delete_astakosgroup(name):
 +            try:
 +                orm.AstakosGroup.objects.get(name=name).delete()
 +            except orm.AstakosGroup.DoesNotExist:
 +                pass
 +
 +        t = ('default', 'shibboleth', 'helpdesk', 'faculty',
 +             'ugrad', 'grad', 'researcher', 'associate')
 +        map(_delete_astakosgroup, t)
 +
 +        t = ('default', 'course', 'project', 'laboratory', 'organization')
 +        map(_delete_groupkind, t)
 +
 +    models = {
 +        'auth.group': {
 +            'Meta': {'object_name': 'Group'},
 +            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
 +            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
 +            'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
 +        },
 +        'auth.permission': {
 +            'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
 +            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
 +            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
 +            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
 +            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
 +        },
 +        'auth.user': {
 +            'Meta': {'object_name': 'User'},
 +            'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
 +            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
 +            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
 +            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
 +            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
 +            'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
 +            'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
 +            'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
 +            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
 +            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
 +            'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
 +            'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
 +            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
 +        },
 +        'contenttypes.contenttype': {
 +            'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
 +            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
 +            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
 +            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
 +            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
 +        },
 +        'im.additionalmail': {
 +            'Meta': {'object_name': 'AdditionalMail'},
 +            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}),
 +            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
 +            'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"})
 +        },
 +        'im.approvalterms': {
 +            'Meta': {'object_name': 'ApprovalTerms'},
 +            'date': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2012, 8, 8, 12, 40, 8, 181485)', 'db_index': 'True'}),
 +            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
 +            'location': ('django.db.models.fields.CharField', [], {'max_length': '255'})
 +        },
 +        'im.astakosgroup': {
 +            'Meta': {'object_name': 'AstakosGroup', '_ormbases': ['auth.Group']},
 +            'approval_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
 +            'creation_date': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2012, 8, 8, 12, 40, 8, 175548)'}),
 +            'desc': ('django.db.models.fields.TextField', [], {'null': 'True'}),
 +            'estimated_participants': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}),
 +            'expiration_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
 +            'group_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.Group']", 'unique': 'True', 'primary_key': 'True'}),
 +            'issue_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
 +            'kind': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.GroupKind']"}),
 +            'moderation_enabled': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
 +            'policy': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['im.Resource']", 'null': 'True', 'through': "orm['im.AstakosGroupQuota']", 'blank': 'True'})
 +        },
 +        'im.astakosgroupquota': {
 +            'Meta': {'unique_together': "(('resource', 'group'),)", 'object_name': 'AstakosGroupQuota'},
 +            'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosGroup']", 'blank': 'True'}),
 +            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
 +            'limit': ('django.db.models.fields.PositiveIntegerField', [], {}),
 +            'resource': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Resource']"})
 +        },
 +        'im.astakosuser': {
 +            'Meta': {'unique_together': "(('provider', 'third_party_identifier'),)", 'object_name': 'AstakosUser', '_ormbases': ['auth.User']},
 +            'activation_sent': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
 +            'affiliation': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
 +            'astakos_groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['im.AstakosGroup']", 'symmetrical': 'False', 'through': "orm['im.Membership']", 'blank': 'True'}),
 +            'auth_token': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'blank': 'True'}),
 +            'auth_token_created': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
 +            'auth_token_expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
 +            'date_signed_terms': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
 +            'email_verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
 +            'has_credits': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
 +            'has_signed_terms': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
 +            'invitations': ('django.db.models.fields.IntegerField', [], {'default': '100'}),
 +            'is_verified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
 +            'level': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
 +            'owner': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'owner'", 'null': 'True', 'to': "orm['im.AstakosGroup']"}),
 +            'policy': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['im.Resource']", 'null': 'True', 'through': "orm['im.AstakosUserQuota']", 'symmetrical': 'False'}),
 +            'provider': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
 +            'third_party_identifier': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
 +            'updated': ('django.db.models.fields.DateTimeField', [], {}),
 +            'user_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True', 'primary_key': 'True'})
 +        },
 +        'im.astakosuserquota': {
 +            'Meta': {'unique_together': "(('resource', 'user'),)", 'object_name': 'AstakosUserQuota'},
 +            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
 +            'limit': ('django.db.models.fields.PositiveIntegerField', [], {}),
 +            'resource': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Resource']"}),
 +            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"})
 +        },
 +        'im.emailchange': {
 +            'Meta': {'object_name': 'EmailChange'},
 +            'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40', 'db_index': 'True'}),
 +            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
 +            'new_email_address': ('django.db.models.fields.EmailField', [], {'max_length': '75'}),
 +            'requested_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2012, 8, 8, 12, 40, 8, 183025)'}),
 +            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'emailchange_user'", 'unique': 'True', 'to': "orm['im.AstakosUser']"})
 +        },
 +        'im.groupkind': {
 +            'Meta': {'object_name': 'GroupKind'},
 +            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
 +            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'})
 +        },
 +        'im.invitation': {
 +            'Meta': {'object_name': 'Invitation'},
 +            'code': ('django.db.models.fields.BigIntegerField', [], {'db_index': 'True'}),
 +            'consumed': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
 +            'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
 +            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
 +            'inviter': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'invitations_sent'", 'null': 'True', 'to': "orm['im.AstakosUser']"}),
 +            'is_consumed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
 +            'realname': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
 +            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'})
 +        },
 +        'im.membership': {
 +            'Meta': {'unique_together': "(('person', 'group'),)", 'object_name': 'Membership'},
 +            'date_joined': ('django.db.models.fields.DateField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}),
 +            'date_requested': ('django.db.models.fields.DateField', [], {'default': 'datetime.datetime(2012, 8, 8, 12, 40, 8, 179349)', 'blank': 'True'}),
 +            'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosGroup']"}),
 +            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
 +            'person': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.AstakosUser']"})
 +        },
 +        'im.resource': {
 +            'Meta': {'object_name': 'Resource'},
 +            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
 +            'meta': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['im.ResourceMetadata']", 'symmetrical': 'False'}),
 +            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'}),
 +            'service': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['im.Service']"})
 +        },
 +        'im.resourcemetadata': {
 +            'Meta': {'object_name': 'ResourceMetadata'},
 +            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
 +            'key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'}),
 +            'value': ('django.db.models.fields.CharField', [], {'max_length': '255'})
 +        },
 +        'im.service': {
 +            'Meta': {'object_name': 'Service'},
 +            'auth_token': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'blank': 'True'}),
 +            'auth_token_created': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
 +            'auth_token_expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
 +            'icon': ('django.db.models.fields.FilePathField', [], {'max_length': '100', 'blank': 'True'}),
 +            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
 +            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255', 'db_index': 'True'}),
 +            'url': ('django.db.models.fields.FilePathField', [], {'max_length': '100'})
 +        }
 +    }
 +
 +    complete_apps = ['im']
@@@ -38,272 -39,31 +38,267 @@@ import loggin
  from time import asctime
  from datetime import datetime, timedelta
  from base64 import b64encode
 -from urlparse import urlparse
  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 import transaction
- from django.db.models.signals import (pre_save, post_save, post_syncdb,
-                                       post_delete)
++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.conf import settings
+ from django.utils.importlib import import_module
  
 -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.endpoints.qh import (
++    register_users, send_quota, register_resources
+ )
 +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
- from astakos.im.functions import send_invitation
  
 -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 save(self, **kwargs):
-         if not self.id:
-             self.renew_token()
-         super(Service, self).save(**kwargs)
 +    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)
-       # update date_joined in any case
 +        try:
-           m.approve()
++            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"""
-         print '#', locals()
 +        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 AstakosUser(User):
      """
      Extends ``django.contrib.auth.models.User`` by defining additional fields.
          if self.is_active and self.activation_sent:
              # reset the activation sent
              self.activation_sent = None
 +
          super(AstakosUser, self).save(**kwargs)
-     def renew_token(self):
 -        
 -        # 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 + \
-             timedelta(hours=AUTH_TOKEN_DURATION)
+                                   timedelta(hours=AUTH_TOKEN_DURATION)
+         if flush_sessions:
+             self.flush_sessions(current_key)
          msg = 'Token renewed for %s' % self.email
 -        logger._log(LOGGING_LEVEL, msg, [])
 +        logger.log(LOGGING_LEVEL, msg)
  
+     def flush_sessions(self, current_key=None):
+         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)
 -            logger._log(LOGGING_LEVEL, msg, [])
++            logger.log(LOGGING_LEVEL, msg, [])
+         engine = import_module(settings.SESSION_ENGINE)
+         for k in keys:
+             s = engine.SessionStore(k)
+             s.flush()
      def __unicode__(self):
 -        return self.username
 -    
 +        return '%s (%s)' % (self.realname, self.email)
 +
      def conflicting_email(self):
 -        q = AstakosUser.objects.exclude(username = self.username)
 -        q = q.filter(email = self.email)
 +        q = AstakosUser.objects.exclude(username=self.username)
 +        q = q.filter(email=self.email)
          if q.count() != 0:
              return True
          return False
          """
          Implements a unique_together constraint for email and is_active fields.
          """
-         q = AstakosUser.objects.exclude(username=self.username)
-         q = q.filter(email=self.email)
-         q = q.filter(is_active=self.is_active)
+         q = AstakosUser.objects.all()
+         q = q.filter(email = self.email)
+         q = q.filter(is_active = self.is_active)
+         if self.id:
+             q = q.filter(~Q(id = self.id))
          if q.count() != 0:
 -            raise ValidationError({'__all__':[_('Another account with the same email & is_active combination found.')]})
 -    
 +            raise ValidationError({'__all__': [_(astakos_messages.UNIQUE_EMAIL_IS_ACTIVE_CONSTRAIN_ERR)]})
 +
 +    @property
      def signed_terms(self):
          term = get_latest_terms()
          if not term:
              return False
          return True
  
 +    def store_disturbed_quota(self, set=True):
-         self.disturbed_qutoa = set
++        self.disturbed_quota = set
 +        self.save()
 +
 +
 +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.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.'
++            '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
@@@ -736,131 -395,80 +750,179 @@@ class AdditionalMail(models.Model)
      owner = models.ForeignKey(AstakosUser)
      email = models.EmailField()
  
 +
 +def _generate_invitation_code():
 +    while True:
 +        code = randint(1, 2L ** 63 - 1)
 +        try:
 +            Invitation.objects.get(code=code)
 +            # An invitation with this code already exists, try again
 +        except Invitation.DoesNotExist:
 +            return code
 +
 +
 +def get_latest_terms():
 +    try:
 +        term = ApprovalTerms.objects.order_by('-id')[0]
 +        return term
 +    except IndexError:
 +        pass
 +    return None
 +
+ class PendingThirdPartyUser(models.Model):
+     """
+     Model for registring successful third party user authentications
+     """
+     third_party_identifier = models.CharField('Third-party identifier', max_length=255, null=True, blank=True)
+     provider = models.CharField('Provider', max_length=255, blank=True)
+     email = models.EmailField(_('e-mail address'), blank=True, null=True)
+     first_name = models.CharField(_('first name'), max_length=30, blank=True)
+     last_name = models.CharField(_('last name'), max_length=30, blank=True)
+     affiliation = models.CharField('Affiliation', max_length=255, blank=True)
+     username = models.CharField(_('username'), max_length=30, unique=True, help_text=_("Required. 30 characters or fewer. Letters, numbers and @/./+/-/_ characters"))
+     
+     class Meta:
+         unique_together = ("provider", "third_party_identifier")
+     @property
+     def realname(self):
+         return '%s %s' %(self.first_name, self.last_name)
+     @realname.setter
+     def realname(self, value):
+         parts = value.split(' ')
+         if len(parts) == 2:
+             self.first_name = parts[0]
+             self.last_name = parts[1]
+         else:
+             self.last_name = parts[0]
+     
+     def save(self, **kwargs):
+         if not self.id:
+             # set username
+             while not self.username:
+                 username =  uuid.uuid4().hex[:30]
+                 try:
+                     AstakosUser.objects.get(username = username)
+                 except AstakosUser.DoesNotExist, e:
+                     self.username = username
+         super(PendingThirdPartyUser, self).save(**kwargs)
+ class SessionCatalog(models.Model):
+     session_key = models.CharField(_('session key'), max_length=40)
+     user = models.ForeignKey(AstakosUser, related_name='sessions', null=True)
 +
  def create_astakos_user(u):
      try:
          AstakosUser.objects.get(user_ptr=u.pk)
      except AstakosUser.DoesNotExist:
          extended_user = AstakosUser(user_ptr_id=u.pk)
          extended_user.__dict__.update(u.__dict__)
- #         extended_user.renew_token()
          extended_user.save()
 -    except:
 -        pass
 +    except BaseException, e:
 +        logger.exception(e)
-         pass
 +
  
 -def superuser_post_syncdb(sender, **kwargs):
 -    # if there was created a superuser
 -    # associate it with an AstakosUser
 +def fix_superusers(sender, **kwargs):
 +    # Associate superusers with AstakosUser
      admins = User.objects.filter(is_superuser=True)
      for u in admins:
          create_astakos_user(u)
  
 -post_syncdb.connect(superuser_post_syncdb)
  
 -def superuser_post_save(sender, instance, **kwargs):
 -    if instance.is_superuser:
 -        create_astakos_user(instance)
 +def user_post_save(sender, instance, created, **kwargs):
 +    if not created:
 +        return
 +    create_astakos_user(instance)
  
 -def astakosuser_post_save(sender, instance, **kwargs):
 -    pass
  
 -post_save.connect(superuser_post_save, sender=User)
 +def set_default_group(user):
 +    try:
 +        default = AstakosGroup.objects.get(name='default')
 +        Membership(
 +            group=default, person=user, date_joined=datetime.now()).save()
 +    except AstakosGroup.DoesNotExist, e:
 +        logger.exception(e)
 +
 +
 +def astakosuser_pre_save(sender, instance, **kwargs):
 +    instance.aquarium_report = False
 +    instance.new = False
 +    try:
 +        db_instance = AstakosUser.objects.get(id=instance.id)
 +    except AstakosUser.DoesNotExist:
 +        # create event
 +        instance.aquarium_report = True
 +        instance.new = True
 +    else:
 +        get = AstakosUser.__getattribute__
 +        l = filter(lambda f: get(db_instance, f) != get(instance, f),
 +                   BILLING_FIELDS)
 +        instance.aquarium_report = True if l else False
 +
 +
 +def astakosuser_post_save(sender, instance, created, **kwargs):
 +    if instance.aquarium_report:
 +        report_user_event(instance, create=instance.new)
 +    if not created:
 +        return
 +    set_default_group(instance)
 +    # TODO handle socket.error & IOError
 +    register_users((instance,))
 +    instance.renew_token()
 +
 +
 +def resource_post_save(sender, instance, created, **kwargs):
 +    if not created:
 +        return
 +    register_resources((instance,))
 +
 +
 +def send_quota_disturbed(sender, instance, **kwargs):
 +    users = []
 +    extend = users.extend
 +    if sender == Membership:
 +        if not instance.group.is_enabled:
 +            return
 +        extend([instance.person])
 +    elif sender == AstakosUserQuota:
 +        extend([instance.user])
 +    elif sender == AstakosGroupQuota:
 +        if not instance.group.is_enabled:
 +            return
 +        extend(instance.group.astakosuser_set.all())
 +    elif sender == AstakosGroup:
 +        if not instance.is_enabled:
 +            return
 +    quota_disturbed.send(sender=sender, users=users)
 +
 +
 +def on_quota_disturbed(sender, users, **kwargs):
-     print '>>>', locals()
++#     print '>>>', locals()
 +    if not users:
 +        return
 +    send_quota(users)
  
+ def renew_token(sender, instance, **kwargs):
+     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)
@@@ -103,151 -101,25 +103,154 @@@ from logging import INF
  LOGGING_LEVEL = getattr(settings, 'ASTAKOS_LOGGING_LEVEL', INFO)
  
  # Configurable email subjects
 -INVITATION_EMAIL_SUBJECT = getattr(settings, 'ASTAKOS_INVITATION_EMAIL_SUBJECT',
 -        'Invitation to %s alpha2 testing' % SITENAME)
 +INVITATION_EMAIL_SUBJECT = getattr(
 +    settings, 'ASTAKOS_INVITATION_EMAIL_SUBJECT',
 +    'Invitation to %s alpha2 testing' % SITENAME)
  GREETING_EMAIL_SUBJECT = getattr(settings, 'ASTAKOS_GREETING_EMAIL_SUBJECT',
 -        'Welcome to %s alpha2 testing' % SITENAME)
 +                                 'Welcome to %s alpha2 testing' % SITENAME)
  FEEDBACK_EMAIL_SUBJECT = getattr(settings, 'ASTAKOS_FEEDBACK_EMAIL_SUBJECT',
 -        'Feedback from %s alpha2 testing' % SITENAME)
 -VERIFICATION_EMAIL_SUBJECT = getattr(settings, 'ASTAKOS_VERIFICATION_EMAIL_SUBJECT',
 -        '%s alpha2 testing account activation is needed' % SITENAME)
 -ADMIN_NOTIFICATION_EMAIL_SUBJECT = getattr(settings, 'ASTAKOS_ADMIN_NOTIFICATION_EMAIL_SUBJECT',
 -        '%s alpha2 testing account created (%%(user)s)' % SITENAME)
 -HELPDESK_NOTIFICATION_EMAIL_SUBJECT = getattr(settings, 'ASTAKOS_HELPDESK_NOTIFICATION_EMAIL_SUBJECT',
 -        '%s alpha2 testing account activated (%%(user)s)' % SITENAME)
 -EMAIL_CHANGE_EMAIL_SUBJECT = getattr(settings, 'ASTAKOS_EMAIL_CHANGE_EMAIL_SUBJECT',
 -        'Email change on %s alpha2 testing' % SITENAME)
 -PASSWORD_RESET_EMAIL_SUBJECT = getattr(settings, 'ASTAKOS_PASSWORD_RESET_EMAIL_SUBJECT',
 -        'Password reset on %s alpha2 testing' % SITENAME)
 +                                 'Feedback from %s alpha2 testing' % SITENAME)
 +VERIFICATION_EMAIL_SUBJECT = getattr(
 +    settings, 'ASTAKOS_VERIFICATION_EMAIL_SUBJECT',
 +    '%s alpha2 testing account activation is needed' % SITENAME)
 +ACCOUNT_CREATION_SUBJECT = getattr(
 +    settings, 'ASTAKOS_ACCOUNT_CREATION_SUBJECT',
 +    '%s alpha2 testing account created (%%(user)s)' % SITENAME)
 +GROUP_CREATION_SUBJECT = getattr(settings, 'ASTAKOS_GROUP_CREATION_SUBJECT',
 +                                 '%s alpha2 testing group created (%%(group)s)' % SITENAME)
 +HELPDESK_NOTIFICATION_EMAIL_SUBJECT = getattr(
 +    settings, 'ASTAKOS_HELPDESK_NOTIFICATION_EMAIL_SUBJECT',
 +    '%s alpha2 testing account activated (%%(user)s)' % SITENAME)
 +EMAIL_CHANGE_EMAIL_SUBJECT = getattr(
 +    settings, 'ASTAKOS_EMAIL_CHANGE_EMAIL_SUBJECT',
 +    'Email change on %s alpha2 testing' % SITENAME)
 +PASSWORD_RESET_EMAIL_SUBJECT = getattr(
 +    settings, 'ASTAKOS_PASSWORD_RESET_EMAIL_SUBJECT',
 +    'Password reset on %s alpha2 testing' % SITENAME)
 +
 +# Set the quota holder component URI
 +QUOTA_HOLDER_URL = getattr(settings, 'ASTAKOS_QUOTA_HOLDER_URL', '')
 +
 +# Set the cloud service properties
 +SERVICES = getattr(settings, 'ASTAKOS_SERVICES', {
 +    'cyclades': {
 +        'url': 'https://node1.example.com/ui/',
 +        'resources': [{
 +            'name':'vm',
 +            'group':'compute',
 +            'uplimit':2,
 +            'desc': 'Number of virtual machines'
 +            },{
 +            'name':'disk',
 +            'group':'compute',
 +            'uplimit':30*1024*1024*1024,
 +            'unit':'bytes',
 +            'desc': 'Virtual machine disk size'
 +            },{
 +            'name':'cpu',
 +            'group':'compute',
 +            'uplimit':6,
 +            'desc': 'Number of virtual machine processors'
 +            },{
 +            'name':'ram',
 +            'group':'compute',
 +            'uplimit':6*1024*1024*1024,
 +            'unit':'bytes',
 +            'desc': 'Virtual machines'
 +            },{
 +            'name':'network.private',
 +            'group':'network',
 +            'uplimit':1,
 +            'desc': 'Private networks'
 +            }
 +        ]
 +    },
 +    'pithos+': {
 +        'url': 'https://node2.example.com/ui/',
 +        'resources':[{
 +            'name':'diskspace',
 +            'group':'storage',
 +            'uplimit':5 * 1024 * 1024 * 1024,
 +            'unit':'bytes',
 +            'desc': 'Pithos account diskspace'
 +            }]
 +    }
 +})
 +
 +# Set the billing URI
 +AQUARIUM_URL = getattr(settings, 'ASTAKOS_AQUARIUM_URL', '')
 +
 +# Set how many objects should be displayed per page
 +PAGINATE_BY = getattr(settings, 'ASTAKOS_PAGINATE_BY', 8)
  
  # Enforce token renewal on password change/reset
 -NEWPASSWD_INVALIDATE_TOKEN = getattr(settings, 'ASTAKOS_NEWPASSWD_INVALIDATE_TOKEN', True)
 +NEWPASSWD_INVALIDATE_TOKEN = getattr(
 +    settings, 'ASTAKOS_NEWPASSWD_INVALIDATE_TOKEN', True)
 +
 +
 +RESOURCES_PRESENTATION_DATA = getattr(
 +    settings, 'ASTAKOS_RESOURCES_PRESENTATION_DATA', {
 +        'groups': {
 +             'compute': {
 +                'help_text':'group compute help text',
 +                'is_abbreviation':False,
 +                'report_desc':'',
 +                 'verbose_name':'compute', 
 +            },
 +            'storage': {
 +                'help_text':'group storage help text',
 +                'is_abbreviation':False,
 +                'report_desc':'',
 +                 'verbose_name':'storage', 
 +            },
 +        },
 +        'resources': {
 +            'pithos+.diskspace': {
 +                'help_text':'resource pithos+.diskspace help text',
 +                'is_abbreviation':False,
 +                'report_desc':'Pithos+ Diskspace',
 +                'placeholder':'eg. 10GB',
 +                'verbose_name':'diskspace', 
 +            },
 +            'cyclades.vm': {
 +                'help_text':'resource cyclades.vm help text resource cyclades.vm help text resource cyclades.vm help text resource cyclades.vm help text',
 +                'is_abbreviation':True,
 +                'report_desc':'Virtual Machines',
 +                'placeholder':'eg. 2',
 +                'verbose_name':'vm', 
 +            },
 +            'cyclades.disk': {
 +                'help_text':'resource cyclades.disk help text',
 +                'is_abbreviation':False,
 +                'report_desc':'Disk',
 +                'placeholder':'eg. 5GB, 2GB etc',
 +                'verbose_name':'disk'
 +            },
 +            'cyclades.ram': {
 +                'help_text':'resource cyclades.ram help text',
 +                'is_abbreviation':True,
 +                'report_desc':'RAM',
 +                'placeholder':'eg. 4GB',
 +                'verbose_name':'ram'
 +            },
 +            'cyclades.cpu': {
 +                'help_text':'resource cyclades.cpu help text',
 +                'is_abbreviation':True,
 +                'report_desc':'CPUs',
 +                'placeholder':'eg. 1',
 +                'verbose_name':'cpu'
 +            },
 +            'cyclades.network.private': {
 +                'help_text':'resource cyclades.network.private help text',
 +                'is_abbreviation':False,
 +                'report_desc':'Network',
 +                'placeholder':'eg. 1',
 +                'verbose_name':'private network'
 +            }
 +        
 +        }
 +        
 +    })
+ # Permit local account migration
+ ENABLE_LOCAL_ACCOUNT_MIGRATION = getattr(settings, 'ASTAKOS_ENABLE_LOCAL_ACCOUNT_MIGRATION', True)
  # interpreted as representing official policies, either expressed
  # or implied, of GRNET S.A.
  
 -from django.http import HttpResponseBadRequest, HttpResponseRedirect
++from django.http import HttpResponseRedirect
  from django.shortcuts import render_to_response
  from django.template import RequestContext
 -from django.contrib.auth import authenticate
  from django.contrib import messages
  from django.utils.translation import ugettext as _
  from django.views.decorators.csrf import csrf_exempt
  from django.views.decorators.http import require_http_methods
+ from django.core.urlresolvers import reverse
+ from django.contrib.auth.decorators import login_required
  
  from astakos.im.util import prepare_response, get_query
- from astakos.im.views import requires_anonymous
- from astakos.im.forms import LoginForm
+ from astakos.im.views import requires_anonymous, signed_terms_required
 -from astakos.im.models import AstakosUser, PendingThirdPartyUser
++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
  
 +import astakos.im.messages as astakos_messages
 +
  from ratelimit.decorators import ratelimit
  
 -retries = RATELIMIT_RETRIES_ALLOWED-1
 -rate = str(retries)+'/m'
 +retries = RATELIMIT_RETRIES_ALLOWED - 1
 +rate = str(retries) + '/m'
 +
  
  @require_http_methods(["GET", "POST"])
  @csrf_exempt
@@@ -60,27 -63,87 +65,86 @@@ 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')
+     
      if not form.is_valid():
-         return render_to_response(on_failure,
-                                   {'login_form': form,
-                                    'next': next},
-                                   context_instance=RequestContext(request))
+         return render_to_response(
+             on_failure,
+             {'login_form':form,
+              'next':next,
+              'key':username},
+             context_instance=RequestContext(request)
+         )
      # 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:
-         message = _(astakos_messages.ACCOUNT_INACTIVE)
+         if not user.activation_sent:
 -            message = _('Your request is pending activation')
++            message = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION)
+         else:
 -            url = reverse('send_activation', kwargs={'user_id':user.id})
 -            message = _('You have not followed the activation link. \
 -            <a href="%s">Resend activation email?</a>' % url)
++            send_activation_url = reverse('send_activation', kwargs={'user_id':user.id})
++            message = _(astakos_messages.ACCOUNT_RESEND_ACTIVATION) % locals()
+     elif user.provider not in ('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,
-                                   {'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:
+         try:
+             new = PendingThirdPartyUser.objects.get(
+                 username=username)
+         except:
+             messages.error(
+                 request,
 -                _('Account failed to switch to %(provider)s' % locals())
++                _(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,
 -                _('Account successfully switched to %(provider)s' % user.__dict__)
++                _(astakos_messages.SWITCH_ACCOUNT_SUCCESS_WITH_PROVIDER) % user.__dict__
+             )
      return prepare_response(request, user, next)
+ @require_http_methods(["GET", "POST"])
+ @signed_terms_required
+ @login_required
+ def password_change(request, template_name='registration/password_change_form.html',
+                     post_change_redirect=None, password_change_form=ExtendedPasswordChangeForm):
+     if post_change_redirect is None:
+         post_change_redirect = reverse('django.contrib.auth.views.password_change_done')
+     if request.method == "POST":
+         form = password_change_form(
+             user=request.user,
+             data=request.POST,
+             session_key=request.session.session_key
+         )
+         if form.is_valid():
+             form.save()
+             return HttpResponseRedirect(post_change_redirect)
+     else:
+         form = password_change_form(user=request.user)
+     return render_to_response(template_name, {
+         'form': form,
 -    }, context_instance=RequestContext(request))
++    }, context_instance=RequestContext(request))
  # or implied, of GRNET S.A.
  
  from django.core.urlresolvers import reverse
 -from django.shortcuts import redirect
  from django.utils.translation import ugettext as _
 -from django.contrib import messages
  from django.utils.http import urlencode
  from django.contrib.auth import authenticate
- from django.http import HttpResponse, HttpResponseBadRequest
+ from django.http import (
+     HttpResponse, HttpResponseBadRequest, HttpResponseForbidden
+ )
  from django.core.exceptions import ValidationError
  from django.views.decorators.http import require_http_methods
  
 -from urllib import quote
 -from urlparse import urlunsplit, urlsplit, urlparse, parse_qsl
 +from urlparse import urlunsplit, urlsplit, parse_qsl
  
- from astakos.im.settings import COOKIE_NAME, COOKIE_DOMAIN
- from astakos.im.util import set_cookie
+ from astakos.im.settings import COOKIE_DOMAIN
+ from astakos.im.util import restrict_next
  from astakos.im.functions import login as auth_login, logout
  
 +import astakos.im.messages as astakos_messages
 +
  import logging
  
  logger = logging.getLogger(__name__)
@@@ -64,7 -66,11 +66,11 @@@ def login(request)
      """
      next = request.GET.get('next')
      if not next:
 -        return HttpResponseBadRequest(_('No next parameter'))
 +        return HttpResponseBadRequest(_(astakos_messages.MISSING_NEXT_PARAMETER))
+     if not restrict_next(
+         next, domain=COOKIE_DOMAIN, allowed_schemes=('pithos',)
+     ):
 -        return HttpResponseForbidden(_('Not allowed next parameter'))
++        return HttpResponseForbidden(_(astakos_messages.NOT_ALLOWED_NEXT_PARAM))
      force = request.GET.get('force', None)
      response = HttpResponse()
      if force == '':
              except ValidationError, e:
                  return HttpResponseBadRequest(e)
              # authenticate before login
 -            user = authenticate(email=request.user.email, auth_token=request.user.auth_token)
 +            user = authenticate(email=request.user.email,
 +                                auth_token=request.user.auth_token
 +                                )
              auth_login(request, user)
-             set_cookie(response, user)
              logger.info('Token reset for %s' % request.user.email)
          parts = list(urlsplit(next))
 -        parts[3] = urlencode({'user': request.user.email, 'token': request.user.auth_token})
 +        parts[3] = urlencode({'user': request.user.email,
 +                              'token': request.user.auth_token
 +                              }
 +                             )
          url = urlunsplit(parts)
          response['Location'] = url
          response.status_code = 302
@@@ -36,18 -36,28 +36,26 @@@ 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 urlparse import urlunsplit, urlsplit
 -from django.utils.http import urlencode
++from django.core.exceptions import ImproperlyConfigured
  
 -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.models import AstakosUser
 -from astakos.im.settings import ENABLE_LOCAL_ACCOUNT_MIGRATION, BASEURL
 -
++from astakos.im.settings import ENABLE_LOCAL_ACCOUNT_MIGRATION
+ from astakos.im.models import AstakosUser, PendingThirdPartyUser
  from astakos.im.forms import LoginForm
--from astakos.im.activation_backends import get_backend, SimpleBackend
++from astakos.im.activation_backends import get_backend
 +
 +import astakos.im.messages as astakos_messages
  
+ import logging
+ logger = logging.getLogger(__name__)
  class Tokens:
      # these are mapped by the Shibboleth SP software
 -    SHIB_EPPN = "HTTP_EPPN" # eduPersonPrincipalName
 +    SHIB_EPPN = "HTTP_EPPN"  # eduPersonPrincipalName
      SHIB_NAME = "HTTP_SHIB_INETORGPERSON_GIVENNAME"
      SHIB_SURNAME = "HTTP_SHIB_PERSON_SURNAME"
      SHIB_CN = "HTTP_SHIB_PERSON_COMMONNAME"
      SHIB_SESSION_ID = "HTTP_SHIB_SESSION_ID"
      SHIB_MAIL = "HTTP_SHIB_MAIL"
  
 +
  @require_http_methods(["GET", "POST"])
  @requires_anonymous
- def login(request, backend=None, on_login_template='im/login.html',
-           on_creation_template='im/third_party_registration.html',
-           extra_context=None):
+ def login(
+     request,
+     login_template='im/login.html',
+     signup_template='im/third_party_check_local.html',
+     extra_context=None
+ ):
+     extra_context = extra_context or {}
      tokens = request.META
 -    
 +
      try:
-         eppn = tokens[Tokens.SHIB_EPPN]
-     except KeyError:
-         return HttpResponseBadRequest("Missing unique token in request")
-     if Tokens.SHIB_DISPLAYNAME in tokens:
-         realname = tokens[Tokens.SHIB_DISPLAYNAME]
-     elif Tokens.SHIB_CN in tokens:
-         realname = tokens[Tokens.SHIB_CN]
-     elif Tokens.SHIB_NAME in tokens and Tokens.SHIB_SURNAME in tokens:
-         realname = tokens[Tokens.SHIB_NAME] + ' ' + tokens[Tokens.SHIB_SURNAME]
-     else:
-         return HttpResponseBadRequest("Missing user name in request")
+         eppn = tokens.get(Tokens.SHIB_EPPN)
+         if not eppn:
 -            raise KeyError(_('Missing unique token in request'))
++            raise KeyError(_(astakos_messages.SHIBBOLETH_MISSING_EPPN))
+         if Tokens.SHIB_DISPLAYNAME in tokens:
+             realname = tokens[Tokens.SHIB_DISPLAYNAME]
+         elif Tokens.SHIB_CN in tokens:
+             realname = tokens[Tokens.SHIB_CN]
+         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 user name in request'))
++            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)
+         )
+     
      affiliation = tokens.get(Tokens.SHIB_EP_AFFILIATION, '')
-     email = tokens.get(Tokens.SHIB_MAIL, None)
+     email = tokens.get(Tokens.SHIB_MAIL, '')
+     
      try:
-         user = AstakosUser.objects.get(provider='shibboleth',
-                                        third_party_identifier=eppn)
+         user = AstakosUser.objects.get(
+             provider='shibboleth',
+             third_party_identifier=eppn
+         )
          if user.is_active:
              return prepare_response(request,
                                      user,
                                      request.GET.get('next'),
                                      'renew' in request.GET)
+         elif not user.activation_sent:
 -            message = _('Your request is pending activation')
++            message = _(astakos_messages.ACCOUNT_PENDING_ACTIVATION)
+             messages.error(request, message)
          else:
-             message = _(astakos_messages.ACCOUNT_INACTIVE)
+             urls = {}
 -            urls['send_activation'] = reverse(
++            urls['send_activation_url'] = reverse(
+                 'send_activation',
+                 kwargs={'user_id':user.id}
+             )
 -            urls['signup'] = reverse(
++            urls['signup_url'] = reverse(
+                 'shibboleth_signup',
+                 args= [user.username]
+             )   
 -            message = _(
 -                'You have not followed the activation link. \
 -                <a href="%(send_activation)s">Resend activation email?</a> or \
 -                <a href="%(signup)s">Provide new email?</a>' % urls
 -            )
++            message = _(astakos_messages.INACTIVE_ACCOUNT_CHANGE_EMAIL) % urls
              messages.error(request, message)
-             return render_response(on_login_template,
-                                    login_form=LoginForm(request=request),
-                                    context_instance=RequestContext(request))
+         return render_response(login_template,
+                                login_form = LoginForm(request=request),
+                                context_instance=RequestContext(request))
      except AstakosUser.DoesNotExist, e:
-         user = AstakosUser(third_party_identifier=eppn, realname=realname,
-                            affiliation=affiliation, provider='shibboleth',
-                            email=email)
+         # First time
          try:
-             if not backend:
-                 backend = get_backend(request)
-             form = backend.get_signup_form(
-                 provider='shibboleth', instance=user)
-         except Exception, e:
-             form = SimpleBackend(request).get_signup_form(
+             user, created = PendingThirdPartyUser.objects.get_or_create(
+                 third_party_identifier=eppn,
                  provider='shibboleth',
-                 instance=user)
-             messages.error(request, e)
-         return render_response(on_creation_template,
-                                signup_form=form,
-                                provider='shibboleth',
-                                context_instance=get_context(request,
-                                                             extra_context))
+                 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, _('Something went wrong.'))
++            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'
+         return render_response(
+             template,
+             context_instance=get_context(request, extra_context)
+         )
+ @require_http_methods(["GET"])
+ @requires_anonymous
+ def signup(
+     request,
+     username,
+     backend=None,
+     on_creation_template='im/third_party_registration.html',
+     extra_context=None
+ ):
+     extra_context = extra_context or {}
+     if not username:
 -        return HttpResponseBadRequest(_('Missing key parameter.'))
++        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(_('Invalid key.'))
++            return HttpResponseBadRequest(_(astakos_messages.INVALID_KEY_PARAMETER))
+     else:
+         d = pending.__dict__
+         d.pop('_state', None)
+         d.pop('id', None)
+         user = AstakosUser(**d)
+     try:
+         backend = backend or get_backend(request)
+     except ImproperlyConfigured, e:
+         messages.error(request, e)
+     else:
+         extra_context['form'] = backend.get_signup_form(
+             provider='shibboleth',
+             instance=user
+         )
+     extra_context['provider']='shibboleth'
+     return render_response(
+             on_creation_template,
+             context_instance=get_context(request, extra_context)
 -    )
++    )
@@@ -15,9 -15,9 +15,9 @@@
  {% block body.right %}
      {% if "local" in im_modules %}
        <div class="form-stacked">
 -        <form action="{% url astakos.im.views.signup %}" method="post"
 +        <form action="{% url signup %}" method="post"
              class="innerlabels signup">{% csrf_token %}
-           <h2><span>Provide an email address to complete the registration:</span></h2>
+           <h2><span>SIGN UP</span></h2>
              <input type="hidden" name="next" value="{{ next }}">
              <input type="hidden" name="code" value="{{ code }}">
              <input type="hidden" name="provider" value={{ provider|default:"local" }}>
@@@ -1,5 -1,5 +1,5 @@@
  from django import template
--from django.core.urlresolvers import reverse, resolve
++from django.core.urlresolvers import resolve
  from django.conf import settings
  
  register = template.Library()
@@@ -47,148 -35,6 +47,148 @@@ from astakos.im.settings import PAGINAT
  
  register = template.Library()
  
 +DELIM = ','
 +
 +
 +@register.filter
 +def monthssince(joined_date):
 +    now = datetime.datetime.now()
 +    date = datetime.datetime(
 +        year=joined_date.year, month=joined_date.month, day=1)
 +    months = []
 +
 +    month = date.month
 +    year = date.year
 +    timestamp = calendar.timegm(date.utctimetuple())
 +
 +    while date < now:
 +        months.append((year, month, timestamp))
 +
 +        if date.month < 12:
 +            month = date.month + 1
 +            year = date.year
 +        else:
 +            month = 1
 +            year = date.year + 1
 +
 +        date = datetime.datetime(year=year, month=month, day=1)
 +        timestamp = calendar.timegm(date.utctimetuple())
 +
 +    return months
 +
 +
  @register.filter
  def lookup(d, key):
 -    return d[key]
 +    print d, key
 +    return d.get(key)
 +
 +@register.filter
 +def lookup_uni(d, key):
 +    return d.get(unicode(key))
 +
 +
 +@register.filter
 +def dkeys(d):
 +    return d.keys()
 +
 +
 +@register.filter
 +def month_name(month_number):
 +    return calendar.month_name[month_number]
 +
 +
 +@register.filter
 +def todate(value, arg=''):
 +    secs = int(value) / 1000
 +    return datetime.datetime.fromtimestamp(secs)
 +
 +
 +@register.filter
 +def rcut(value, chars='/'):
 +    return value.rstrip(chars)
 +
 +
 +@register.filter
 +def paginate(l, args):
-    l = l or []
-    page, delim, sorting = args.partition(DELIM)
-    if sorting:
-        if isinstance(l, QuerySet):
-            l = l.order_by(sorting)
-        elif isinstance(l, list):
-            default = ''
-            if sorting.endswith('_date'):
-                default = datetime.datetime.utcfromtimestamp(0)
-            l.sort(key=lambda i: getattr(i, sorting)
-                   if getattr(i, sorting) else default)
-    paginator = Paginator(l, PAGINATE_BY)
-    try:
-        paginator.len
-    except AttributeError:
-        paginator._count = len(list(l))
-    
-    try:
-        page_number = int(page)
-    except ValueError:
-        if page == 'last':
-            page_number = paginator.num_pages
-        else:
-            page_number = 1
-    try:
-        page = paginator.page(page_number)
-    except EmptyPage:
-        page = paginator.page(1)
-    return page
++    l = l or []
++    page, delim, sorting = args.partition(DELIM)
++    if sorting:
++        if isinstance(l, QuerySet):
++            l = l.order_by(sorting)
++        elif isinstance(l, list):
++            default = ''
++            if sorting.endswith('_date'):
++                default = datetime.datetime.utcfromtimestamp(0)
++            l.sort(key=lambda i: getattr(i, sorting)
++                   if getattr(i, sorting) else default)
++    paginator = Paginator(l, PAGINATE_BY)
++    try:
++        paginator.len
++    except AttributeError:
++        paginator._count = len(list(l))
++    
++    try:
++        page_number = int(page)
++    except ValueError:
++        if page == 'last':
++            page_number = paginator.num_pages
++        else:
++            page_number = 1
++    try:
++        page = paginator.page(page_number)
++    except EmptyPage:
++        page = paginator.page(1)
++    return page
 +
 +
 +@register.filter
 +def concat(str1, str2):
 +    if not str2:
 +        return str(str1)
 +    return '%s%s%s' % (str1, DELIM, str2)
 +
 +
 +@register.filter
 +def items(d):
 +    if isinstance(d, defaultdict):
 +        return d.iteritems()
 +    return d
 +
 +
 +@register.filter
 +def get_value_after_dot(value):
 +    return value.split(".")[1]
 +
 +@register.filter
 +def strip_http(value):
 +    return value.replace('http://','')[:-1]
 +
 +
 +from math import log
 +unit_list = zip(['bytes', 'kB', 'MB', 'GB', 'TB', 'PB'], [0, 0, 0, 0, 0, 0])
 +
 +@register.filter
 +def sizeof_fmt(num):
 +    
 +    """Human friendly file size"""
 +    if math.isinf(num):
 +        return 'Unlimited'
 +    if num > 1:
 +        exponent = min(int(log(num, 1024)), len(unit_list) - 1)
 +        quotient = float(num) / 1024**exponent
 +        unit, num_decimals = unit_list[exponent]
 +        format_string = '{0:.%sf} {1}' % (num_decimals)
 +        return format_string.format(quotient, unit)
 +    if num == 0:
 +        return '0 bytes'
 +    if num == 1:
 +        return '1 byte'
 +    else:
 +       return '';
 +   
 +@register.filter
 +def isinf(v):
 +    if math.isinf(v):
 +        return 'Unlimited'
 +    else:
 +        return v
@@@ -39,68 -40,28 +39,43 @@@ from astakos.im.forms import (ExtendedP
  from astakos.im.settings import IM_MODULES, INVITATIONS_ENABLED, EMAILCHANGE_ENABLED
  
  urlpatterns = patterns('astakos.im.views',
-                        url(r'^$', 'index', {}, name='index'),
-                        url(r'^login/?$', 'index', {}, name='login'),
-                        url(r'^profile/?$',
-                            'edit_profile', {}, name='edit_profile'),
-                        url(r'^feedback/?$', 'feedback', {}, name='feedback'),
-                        url(r'^signup/?$', 'signup',
-                            {'on_success': 'im/login.html',
-                             'extra_context': {'login_form': LoginForm()}},
-                            name='signup'),
-                        url(r'^logout/?$', 'logout',
-                            {'template': 'im/login.html',
-                             'extra_context': {'login_form': LoginForm()}},
-                            name='logout'),
-                        url(r'^activate/?$', 'activate', {}, name='activate'),
-                        url(r'^approval_terms/?$',
-                            'approval_terms', {}, name='latest_terms'),
-                        url(r'^approval_terms/(?P<term_id>\d+)/?$',
-                            'approval_terms'),
-                        url(r'^password/?$',
-                            'change_password', {}, name='password_change'),
-                        url(r'^resources/?$',
-                            'resource_list', {}, name='resource_list'),
-                        url(r'^billing/?$', 'billing', {}, name='billing'),
-                        url(r'^timeline/?$', 'timeline', {}, name='timeline'),
-                        url(r'^group/add/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'^$', 'index', {}, name='index'),
+     url(r'^login/?$', 'index', {}, name='login'),
 -    url(r'^profile/?$', 'edit_profile'),
 -    url(r'^feedback/?$', 'feedback'),
 -    url(r'^signup/?$', 'signup', {'on_success':'im/login.html', 'extra_context':{'login_form':LoginForm()}}),
 -    url(r'^logout/?$', 'logout', {'template':'im/login.html', 'extra_context':{'login_form':LoginForm()}}),
 -    url(r'^activate/?$', 'activate'),
++    url(r'^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')
+ )
 +
  if EMAILCHANGE_ENABLED:
      urlpatterns += patterns('astakos.im.views',
-                             url(r'^email_change/?$',
-                                 'change_email', {}, name='email_change'),
-                             url(
-                             r'^email_change/confirm/(?P<activation_key>\w+)/', 'change_email', {},
-                             name='email_change_confirm')
-                             )
+         url(r'^email_change/?$', 'change_email', {}, name='email_change'),
+         url(r'^email_change/confirm/(?P<activation_key>\w+)/?$', 'change_email', {},
+             name='email_change_confirm')
+ )
+     
  urlpatterns += patterns('astakos.im.target',
 -    url(r'^login/redirect/?$', 'redirect.login')
 -)
 +                        url(r'^login/redirect/?$', 'redirect.login')
 +                        )
  
  if 'local' in IM_MODULES:
      urlpatterns += patterns('astakos.im.target',
@@@ -35,51 -35,47 +35,50 @@@ import loggin
  import datetime
  import time
  
--from urllib import quote
 -from urlparse import urlsplit, urlunsplit, urlparse
 -
++from urlparse import urlparse
  from datetime import tzinfo, timedelta
++
  from django.http import HttpResponse, HttpResponseBadRequest, urlencode
  from django.template import RequestContext
--from django.utils.translation import ugettext as _
  from django.contrib.auth import authenticate
  from django.core.urlresolvers import reverse
 -from django.core.exceptions import ValidationError
 -from django.contrib.sessions.backends.base import SessionBase
 +from django.core.exceptions import ValidationError, ObjectDoesNotExist
- from django.db.models.fields import Field
 +from django.utils.translation import ugettext as _
  
 -from astakos.im.models import AstakosUser, Invitation, ApprovalTerms
 +from astakos.im.models import AstakosUser, Invitation
- from astakos.im.settings import COOKIE_NAME, \
-     COOKIE_DOMAIN, COOKIE_SECURE, FORCE_PROFILE_UPDATE, LOGGING_LEVEL
+ from astakos.im.settings import (
 -    INVITATIONS_PER_LEVEL, COOKIE_DOMAIN, FORCE_PROFILE_UPDATE, LOGGING_LEVEL
++    COOKIE_DOMAIN, FORCE_PROFILE_UPDATE
+ )
  from astakos.im.functions import login
  
 +import astakos.im.messages as astakos_messages
 +
  logger = logging.getLogger(__name__)
  
 +
  class UTC(tzinfo):
 -   def utcoffset(self, dt):
 -       return timedelta(0)
 +    def utcoffset(self, dt):
 +        return timedelta(0)
 +
 +    def tzname(self, dt):
 +        return 'UTC'
  
 -   def tzname(self, dt):
 -       return 'UTC'
 +    def dst(self, dt):
 +        return timedelta(0)
  
 -   def dst(self, dt):
 -       return timedelta(0)
  
  def isoformat(d):
 -   """Return an ISO8601 date string that includes a timezone."""
 +    """Return an ISO8601 date string that includes a timezone."""
 +
 +    return d.replace(tzinfo=UTC()).isoformat()
  
 -   return d.replace(tzinfo=UTC()).isoformat()
  
  def epoch(datetime):
 -    return int(time.mktime(datetime.timetuple())*1000)
 +    return int(time.mktime(datetime.timetuple()) * 1000)
 +
  
 -def get_context(request, extra_context={}, **kwargs):
 -    if not extra_context:
 -        extra_context = {}
 +def get_context(request, extra_context=None, **kwargs):
 +    extra_context = extra_context or {}
      extra_context.update(kwargs)
      return RequestContext(request, extra_context)
  
@@@ -96,14 -91,57 +95,58 @@@ def get_invitation(request)
          code = request.POST.get('code')
      if not code:
          return
 -    invitation = Invitation.objects.get(code = code)
 +    invitation = Invitation.objects.get(code=code)
      if invitation.is_consumed:
 -        raise ValueError(_('Invitation is used'))
 +        raise ValueError(_(astakos_messages.INVITATION_CONSUMED_ERR))
      if reserved_email(invitation.username):
 -        raise ValueError(_('Email: %s is reserved' % invitation.username))
 +        email = invitation.username
-         raise ValueError(_(astakos_messages.EMAIL_RESRVED) % locals())
++        raise ValueError(_(astakos_messages.EMAIL_RESERVED) % locals())
      return invitation
  
+ def restrict_next(url, domain=None, allowed_schemes=()):
+     """
+     Return url if having the supplied ``domain`` (if present) or one of the ``allowed_schemes``.
+     Otherwise return None.
+     
+     >>> print restrict_next('/im/feedback', '.okeanos.grnet.gr')
+     /im/feedback
+     >>> print restrict_next('pithos.okeanos.grnet.gr/im/feedback', '.okeanos.grnet.gr')
+     //pithos.okeanos.grnet.gr/im/feedback
+     >>> print restrict_next('https://pithos.okeanos.grnet.gr/im/feedback', '.okeanos.grnet.gr')
+     https://pithos.okeanos.grnet.gr/im/feedback
+     >>> print restrict_next('pithos://127.0.0,1', '.okeanos.grnet.gr')
+     None
+     >>> print restrict_next('pithos://127.0.0,1', '.okeanos.grnet.gr', allowed_schemes=('pithos'))
+     pithos://127.0.0,1
+     >>> print restrict_next('node1.example.com', '.okeanos.grnet.gr')
+     None
+     >>> print restrict_next('//node1.example.com', '.okeanos.grnet.gr')
+     None
+     >>> print restrict_next('https://node1.example.com', '.okeanos.grnet.gr')
+     None
+     >>> print restrict_next('https://node1.example.com')
+     https://node1.example.com
+     >>> print restrict_next('//node1.example.com')
+     //node1.example.com
+     >>> print restrict_next('node1.example.com')
+     //node1.example.com
+     """
+     if not url:
+         return
+     parts = urlparse(url, scheme='http')
+     if not parts.netloc and not parts.path.startswith('/'):
+         # fix url if does not conforms RFC 1808
+         url = '//%s' % url
+         parts = urlparse(url, scheme='http')
+     # TODO more scientific checks?
+     if not parts.netloc:    # internal url
+         return url
+     elif not domain:
+         return url
+     elif parts.netloc.endswith(domain):
+         return url
+     elif parts.scheme in allowed_schemes:
+         return url
  
  def prepare_response(request, user, next='', renew=False):
      """Return the unique username and the token
         or user has not a valid token.
      """
      renew = renew or (not user.auth_token)
 -    renew = renew or (user.auth_token_expires and user.auth_token_expires < datetime.datetime.now())
 +    renew = renew or (user.auth_token_expires < datetime.datetime.now())
      if renew:
-         user.renew_token()
+         user.renew_token(
+             flush_sessions=True,
+             current_key=request.session.session_key
+         )
          try:
              user.save()
          except ValidationError, e:
      # authenticate before login
      user = authenticate(email=user.email, auth_token=user.auth_token)
      login(request, user)
-     set_cookie(response, user)
      request.session.set_expiry(user.auth_token_expires)
 -    
 +
      if not next:
-         next = reverse('index')
+         next = reverse('astakos.im.views.index')
+         
      response['Location'] = next
      response.status_code = 302
      return response
  
- def set_cookie(response, user):
-     expire_fmt = user.auth_token_expires.strftime('%a, %d-%b-%Y %H:%M:%S %Z')
-     cookie_value = quote(user.email + '|' + user.auth_token)
-     response.set_cookie(COOKIE_NAME, value=cookie_value,
-                         expires=expire_fmt, path='/',
-                         domain=COOKIE_DOMAIN, secure=COOKIE_SECURE)
-     msg = 'Cookie [expiring %s] set for %s' % (
-         user.auth_token_expires,
-         user.email
-     )
-     logger.log(LOGGING_LEVEL, msg)
  class lazy_string(object):
      def __init__(self, function, *args, **kwargs):
 -        self.function=function
 -        self.args=args
 -        self.kwargs=kwargs
 -        
 +        self.function = function
 +        self.args = args
 +        self.kwargs = kwargs
 +
      def __str__(self):
          if not hasattr(self, 'str'):
 -            self.str=self.function(*self.args, **self.kwargs)
 +            self.str = self.function(*self.args, **self.kwargs)
          return self.str
  
 +
  def reverse_lazy(*args, **kwargs):
      return lazy_string(reverse, *args, **kwargs)
  
  # 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, timedelta
- from collections import defaultdict
++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.contrib.auth.views import password_change
 +from django.core.urlresolvers import reverse
  from django.db import transaction
- from django.db.models import Q
 -from django.utils.http import urlencode
  from django.db.utils import IntegrityError
- from django.forms.fields import URLField
 -from django.contrib.auth.views import password_change
 -from django.core.exceptions import ValidationError
 -from django.views.decorators.http import require_http_methods
 +from django.http import (HttpResponse, HttpResponseBadRequest,
 +                         HttpResponseForbidden, HttpResponseRedirect,
 +                         HttpResponseBadRequest, Http404)
 +from django.shortcuts import redirect
 +from django.template import RequestContext, loader as template_loader
 +from django.utils.http import urlencode
 +from django.utils.translation import ugettext as _
- from django.views.generic.create_update import (create_object, delete_object,
++from django.views.generic.create_update import (delete_object,
 +                                                get_model_and_form_class)
- from django.views.generic.list_detail import object_list, object_detail
- from django.http import HttpResponseBadRequest
++from django.views.generic.list_detail import object_list
 +from django.core.xheaders import populate_xheaders
  
- from astakos.im.models import (AstakosUser, ApprovalTerms, AstakosGroup,
-                                Resource, EmailChange, GroupKind, Membership,
-                                AstakosGroupQuota, RESOURCE_SEPARATOR)
 -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 django.db.models.query import QuerySet
  from astakos.im.activation_backends import get_backend, SimpleBackend
- from astakos.im.util import get_context, prepare_response, set_cookie, get_query
 -from astakos.im.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,
-                               ExtendedPasswordChangeForm, EmailChangeForm,
++                              EmailChangeForm,
 +                              AstakosGroupCreationForm, AstakosGroupSearchForm,
 +                              AstakosGroupUpdateForm, AddGroupMembersForm,
-                               AstakosGroupSortForm, MembersSortForm,
++                              MembersSortForm,
 +                              TimelineForm, PickResourceForm,
 +                              AstakosGroupCreationSummaryForm)
 +from astakos.im.functions import (send_feedback, SendMailError,
 +                                  logout as auth_logout,
 +                                  activate as activate_func,
-                                   switch_account_to_shibboleth,
++                                  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_NAME, COOKIE_DOMAIN, LOGOUT_NEXT,
++from astakos.im.settings import (COOKIE_DOMAIN, LOGOUT_NEXT,
 +                                 LOGGING_LEVEL, PAGINATE_BY, RESOURCES_PRESENTATION_DATA)
 +from astakos.im.tasks import request_billing
 +from astakos.im.api.callpoint import AstakosCallpoint
 +
 +import astakos.im.messages as astakos_messages
  
  logger = logging.getLogger(__name__)
  
 +DB_REPLACE_GROUP_SCHEME = """REPLACE(REPLACE("auth_group".name, 'http://', ''),
 +                                     'https://', '')"""
 +
 +callpoint = AstakosCallpoint()
 +
- def render_response(template, tab=None, status=200, reset_cookie=False,
-                     context_instance=None, **kwargs):
+ def render_response(template, tab=None, status=200, context_instance=None, **kwargs):
      """
      Calls ``django.template.loader.render_to_string`` with an additional ``tab``
      keyword argument and returns an ``django.http.HttpResponse`` with the
      if tab is None:
          tab = template.partition('_')[0].partition('.html')[0]
      kwargs.setdefault('tab', tab)
 -    html = render_to_string(template, kwargs, context_instance=context_instance)
 +    html = template_loader.render_to_string(
 +        template, kwargs, context_instance=context_instance)
      response = HttpResponse(html, status=status)
-     if reset_cookie:
-         set_cookie(response, context_instance['request'].user)
      return response
  
  
@@@ -149,10 -115,9 +140,10 @@@ def signed_terms_required(func)
          return func(request, *args, **kwargs)
      return wrapper
  
 +
  @require_http_methods(["GET", "POST"])
  @signed_terms_required
- def index(request, login_template_name='im/login.html', extra_context=None):
+ def index(request, login_template_name='im/login.html', profile_template_name='im/profile.html', extra_context=None):
      """
      If there is logged on user renders the profile page otherwise renders login page.
  
      im/profile.html or im/login.html or ``template_name`` keyword argument.
  
      """
+     extra_context = extra_context or {}
      template_name = login_template_name
      if request.user.is_authenticated():
-         return HttpResponseRedirect(reverse('edit_profile'))
-     return render_response(template_name,
-                            login_form=LoginForm(request=request),
-                            context_instance=get_context(request, extra_context))
+         return HttpResponseRedirect(reverse('astakos.im.views.edit_profile'))
+     
+     return render_response(
+         template_name,
+         login_form = LoginForm(request=request),
+         context_instance = get_context(request, extra_context)
+     )
  
 +
  @require_http_methods(["GET", "POST"])
  @login_required
  @signed_terms_required
@@@ -217,8 -185,10 +212,8 @@@ def invite(request, template_name='im/i
      The view expectes the following settings are defined:
  
      * LOGIN_URL: login uri
--    * ASTAKOS_DEFAULT_CONTACT_EMAIL: service support email
 -    * ASTAKOS_DEFAULT_FROM_EMAIL: from email
      """
+     extra_context = extra_context or {}
      status = None
      message = None
      form = InvitationForm()
@@@ -303,27 -278,28 +304,31 @@@ def edit_profile(request, template_name
              try:
                  prev_token = request.user.auth_token
                  user = form.save()
-                 reset_cookie = user.auth_token != prev_token
-                 form = ProfileForm(instance=user)
-                 next = request.POST.get('next')
+                 form = ProfileForm(
+                     instance=user,
+                     session_key=request.session.session_key
+                 )
+                 next = restrict_next(
+                     request.POST.get('next'),
+                     domain=COOKIE_DOMAIN
+                 )
                  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":
 -        request.user.is_verified = True
 -        request.user.save()
 +        if not request.user.is_verified:
 +            request.user.is_verified = True
 +            request.user.save()
      return render_response(template_name,
-                            reset_cookie=reset_cookie,
-                            profile_form=form,
-                            context_instance=get_context(request,
-                                                         extra_context))
+                            profile_form = form,
+                            context_instance = get_context(request,
+                                                           extra_context))
  
 +
 +@transaction.commit_manually
  @require_http_methods(["GET", "POST"])
  def signup(request, template_name='im/signup.html', on_success='im/signup_complete.html', extra_context=None, backend=None):
      """
          An dictionary of variables to add to the template context.
  
      **Template:**
 -    
 +
      im/signup.html or ``template_name`` keyword argument.
 -    im/signup_complete.html or ``on_success`` keyword argument. 
 +    im/signup_complete.html or ``on_success`` keyword argument.
      """
+     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')
+     id = get_query(request).get('id')
+     try:
+         instance = AstakosUser.objects.get(id=id) if id else None
+     except AstakosUser.DoesNotExist:
+         instance = None
      try:
          if not backend:
              backend = get_backend(request)
-         form = backend.get_signup_form(provider)
+         form = backend.get_signup_form(provider, instance)
      except Exception, e:
          form = SimpleBackend(request).get_signup_form(provider)
 -        messages.add_message(request, messages.ERROR, e)
 +        messages.error(request, e)
      if request.method == 'POST':
          if form.is_valid():
              user = form.save(commit=False)
                      if additional_email != user.email:
                          user.additionalmail_set.create(email=additional_email)
                          msg = 'Additional email: %s saved for user %s.' % (
-                             additional_email, user.email)
-                         logger.log(LOGGING_LEVEL, msg)
+                             additional_email,
+                             user.email
+                         )
+                         logger._log(LOGGING_LEVEL, msg, [])
                  if user and user.is_active:
                      next = request.POST.get('next', '')
 -                    return prepare_response(request, user, next=next)
 +                    response = prepare_response(request, user, next=next)
 +                    transaction.commit()
 +                    return response
                  messages.add_message(request, status, message)
-                 transaction.commit()
-                 return render_response(on_success,
-                                        context_instance=get_context(request, extra_context))
+                 return render_response(
+                     on_success,
+                     context_instance=get_context(
+                         request,
+                         extra_context
+                     )
+                 )
              except SendMailError, e:
+                 logger.exception(e)
+                 status = messages.ERROR
                  message = e.message
 -                messages.add_message(request, status, message)
 +                messages.error(request, message)
 +                transaction.rollback()
              except BaseException, e:
+                 logger.exception(e)
 -                status = messages.ERROR
 -                message = _('Something went wrong.')
 -                messages.add_message(request, status, message)
 +                message = _(astakos_messages.GENERIC_ERROR)
 +                messages.error(request, message)
                  logger.exception(e)
 +                transaction.rollback()
      return render_response(template_name,
 -                           signup_form = form,
 -                           provider = provider,
 +                           signup_form=form,
 +                           provider=provider,
                             context_instance=get_context(request, extra_context))
  
 +
  @require_http_methods(["GET", "POST"])
  @login_required
  @signed_terms_required
@@@ -438,8 -426,9 +459,8 @@@ def feedback(request, template_name='im
      **Settings:**
  
      * LOGIN_URL: login uri
--    * ASTAKOS_DEFAULT_CONTACT_EMAIL: List of feedback recipients
      """
+     extra_context = extra_context or {}
      if request.method == 'GET':
          form = FeedbackForm()
      if request.method == 'POST':
              try:
                  send_feedback(msg, data, request.user, email_template_name)
              except SendMailError, e:
 -                message = e.message
 -                status = messages.ERROR
 +                messages.error(request, message)
              else:
 -                message = _('Feedback successfully sent')
 -                status = messages.SUCCESS
 -            messages.add_message(request, status, message)
 +                message = _(astakos_messages.FEEDBACK_SENT)
 +                messages.success(request, message)
      return render_response(template_name,
 -                           feedback_form = form,
 -                           context_instance = get_context(request, extra_context))
 +                           feedback_form=form,
 +                           context_instance=get_context(request, extra_context))
 +
  
- @require_http_methods(["GET", "POST"])
+ @require_http_methods(["GET"])
 +@signed_terms_required
  def logout(request, template='registration/logged_out.html', extra_context=None):
      """
-     Wraps `django.contrib.auth.logout` and delete the cookie.
+     Wraps `django.contrib.auth.logout`.
      """
+     extra_context = extra_context or {}
      response = HttpResponse()
      if request.user.is_authenticated():
          email = request.user.email
      elif LOGOUT_NEXT:
          response['Location'] = LOGOUT_NEXT
          response.status_code = 301
-         return response
-     messages.success(request, _(astakos_messages.LOGOUT_SUCCESS))
-     context = get_context(request, extra_context)
-     response.write(
-         template_loader.render_to_string(template, context_instance=context))
+     else:
 -        messages.add_message(request, messages.SUCCESS, _('<p>You have successfully logged out.</p>'))
++        messages.add_message(request, messages.SUCCESS, _(astakos_messages.LOGOUT_SUCCESS))
+         context = get_context(request, extra_context)
+         response.write(render_to_string(template, context_instance=context))
      return response
  
 +
  @require_http_methods(["GET", "POST"])
  @transaction.commit_manually
 -def activate(request, greeting_email_template_name='im/welcome_email.txt', helpdesk_email_template_name='im/helpdesk_notification.txt'):
 +def activate(request, greeting_email_template_name='im/welcome_email.txt',
 +             helpdesk_email_template_name='im/helpdesk_notification.txt'):
      """
      Activates the user identified by the ``auth`` request parameter, sends a welcome email
      and renews the user token.
      try:
          user = AstakosUser.objects.get(auth_token=token)
      except AstakosUser.DoesNotExist:
 -        return HttpResponseBadRequest(_('No such user'))
 -    
 +        return HttpResponseBadRequest(_(astakos_messages.ACCOUNT_UNKNOWN))
 +
      if user.is_active:
 -        message = _('Account already active.')
 -        messages.add_message(request, messages.ERROR, message)
 +        message = _(astakos_messages.ACCOUNT_ALREADY_ACTIVE)
 +        messages.error(request, message)
          return index(request)
 -    
 +
      try:
-         local_user = AstakosUser.objects.get(
-             ~Q(id=user.id),
-             email=user.email,
-             is_active=True
-         )
-     except AstakosUser.DoesNotExist:
-         try:
-             activate_func(
-                 user,
-                 greeting_email_template_name,
-                 helpdesk_email_template_name,
-                 verify_email=True
-             )
-             response = prepare_response(request, user, next, renew=True)
-             transaction.commit()
-             return response
-         except SendMailError, e:
-             message = e.message
-             messages.error(request, message)
-             transaction.rollback()
-             return index(request)
-         except BaseException, e:
-             message = _(astakos_messages.GENERIC_ERROR)
-             messages.error(request, message)
-             logger.exception(e)
-             transaction.rollback()
-             return index(request)
-     else:
-         try:
-             user = switch_account_to_shibboleth(
-                 user,
-                 local_user,
-                 greeting_email_template_name
-             )
-             response = prepare_response(request, user, next, renew=True)
-             transaction.commit()
-             return response
-         except SendMailError, e:
-             message = e.message
-             messages.error(request, message)
-             transaction.rollback()
-             return index(request)
-         except BaseException, e:
-             message = _(astakos_messages.GENERIC_ERROR)
-             messages.error(request, message)
-             logger.exception(e)
-             transaction.rollback()
-             return index(request)
+         activate_func(user, greeting_email_template_name, helpdesk_email_template_name, verify_email=True)
+         response = prepare_response(request, user, next, renew=True)
+         transaction.commit()
+         return response
+     except SendMailError, e:
+         message = e.message
+         messages.add_message(request, messages.ERROR, message)
+         transaction.rollback()
+         return index(request)
+     except BaseException, e:
+         status = messages.ERROR
 -        message = _('Something went wrong.')
++        message = _(astakos_messages.GENERIC_ERROR)
+         messages.add_message(request, messages.ERROR, message)
+         logger.exception(e)
+         transaction.rollback()
+         return index(request)
  
 +
  @require_http_methods(["GET", "POST"])
  def approval_terms(request, term_id=None, template_name='im/approval_terms.html', extra_context=None):
+     extra_context = extra_context or {}
      term = None
      terms = None
      if not term_id:
      terms = f.read()
  
      if request.method == 'POST':
-         next = request.POST.get('next')
+         next = restrict_next(
+             request.POST.get('next'),
+             domain=COOKIE_DOMAIN
+         )
          if not next:
 -            next = reverse('astakos.im.views.index')
 +            next = reverse('index')
          form = SignApprovalTermsForm(request.POST, instance=request.user)
          if not form.is_valid():
              return render_response(template_name,
          return HttpResponseRedirect(next)
      else:
          form = None
 -        if request.user.is_authenticated() and not request.user.signed_terms():
 +        if request.user.is_authenticated() and not request.user.signed_terms:
              form = SignApprovalTermsForm(instance=request.user)
          return render_response(template_name,
 -                               terms = terms,
 -                               approval_terms_form = form,
 -                               context_instance = get_context(request, extra_context))
 +                               terms=terms,
 +                               approval_terms_form=form,
 +                               context_instance=get_context(request, extra_context))
 +
  
  @require_http_methods(["GET", "POST"])
- @signed_terms_required
- def change_password(request):
-     return password_change(request,
-                            post_change_redirect=reverse('edit_profile'),
-                            password_change_form=ExtendedPasswordChangeForm)
- @require_http_methods(["GET", "POST"])
- @signed_terms_required
  @login_required
+ @signed_terms_required
  @transaction.commit_manually
  def change_email(request, activation_key=None,
                   email_template_name='registration/email_change_email.txt',
          try:
              ec = form.save(email_template_name, request)
          except SendMailError, e:
 -            status = messages.ERROR
              msg = e
 +            messages.error(request, msg)
              transaction.rollback()
          except IntegrityError, e:
 -            status = messages.ERROR
 -            msg = _('There is already a pending change email request.')
 +            msg = _(astakos_messages.PENDING_EMAIL_CHANGE_REQUEST)
 +            messages.error(request, msg)
          else:
 -            status = messages.SUCCESS
 -            msg = _('Change email request has been registered succefully.\
 -                    You are going to receive a verification email in the new address.')
 +            msg = _(astakos_messages.EMAIL_CHANGE_REGISTERED)
 +            messages.success(request, msg)
              transaction.commit()
-     return render_response(form_template_name,
-                            form=form,
-                            context_instance=get_context(request,
-                                                         extra_context))
 -        messages.add_message(request, status, msg)
 -    return render_response(form_template_name,
 -                           form = form,)
++    return render_response(
++        form_template_name,
++        form=form,
++        context_instance=get_context(request, extra_context)
++    )
+ def send_activation(request, user_id, template_name='im/login.html', extra_context=None):
+     extra_context = extra_context or {}
+     try:
+         u = AstakosUser.objects.get(id=user_id)
+     except AstakosUser.DoesNotExist:
 -        messages.error(request, _('Invalid user id'))
++        messages.error(request, _(astakos_messages.ACCOUNT_UNKNOWN))
+     else:
+         try:
+             send_activation_func(u)
 -            msg = _('Activation sent.')
++            msg = _(astakos_messages.ACTIVATION_SENT)
+             messages.success(request, msg)
+         except SendMailError, e:
+             messages.error(request, e)
+     return render_response(
+         template_name,
 -        login_form = LoginForm(request=request), 
++        login_form = LoginForm(request=request),
+         context_instance = get_context(
+             request,
+             extra_context
+         )
+     )
 +
 +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] = {}
 +                    
 + 
 +            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):
-         print '!!!!!', 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 
 +            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 
 +        """ % (DB_REPLACE_GROUP_SCHEME, request.user.id, request.user.id)
 +       
 +    if sorting:
 +        query = query+" ORDER BY %s ASC" %sorting    
 +    else:
 +        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')
 +    
 + 
 +    
 +    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
 +    )
 +    
-     print '######', obj.quota
-    
 +    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, 
 +                    })
 +        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,
 +        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,
 +        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_A_MEMBER))
++        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 = MEMBER_REMOVED % 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):
- #     if request.method == 'POST':
- #         form = PickResourceForm(request.POST)
- #         if form.is_valid():
- #             r = form.cleaned_data.get('resource')
- #             if r:
- #                 groups = request.user.membership_set.only('group').filter(
- #                     date_joined__isnull=False)
- #                 groups = [g.group_id for g in groups]
- #                 q = AstakosGroupQuota.objects.select_related().filter(
- #                     resource=r, group__in=groups)
- #     else:
- #         form = PickResourceForm()
- #         q = AstakosGroupQuota.objects.none()
- #
- #     return object_list(request, q,
- #                        template_name='im/astakosuserquota_list.html',
- #                        extra_context={'form': form, 'data':data})
 +    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'] = (curr_value / max_value) * 100
++        else:
++            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,
++                           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]
-     data['resources'] = map(with_class, data['resources'])
 +    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)
 +
-     print type(start)
 +    return render_response(
 +        template='im/billing.html',
 +        context_instance=get_context(request),
 +        data=data,
 +        zerodate=datetime(month=1, year=1970, day=1),
 +        today=today,
 +        start=int(start),
 +        month_last_day=month_last_day)
 +
 +
 +def _clear_billing_data(data):
 +
 +    # remove addcredits entries
 +    def isnotcredit(e):
 +        return e['serviceName'] != "addcredits"
 +
 +    # separate services
 +    def servicefilter(service_name):
 +        service = service_name
 +
 +        def fltr(e):
 +            return e['serviceName'] == service
 +        return fltr
 +
 +    data['bill_nocredits'] = filter(isnotcredit, data['bill'])
 +    data['bill_vmtime'] = filter(servicefilter('vmtime'), data['bill'])
 +    data['bill_diskspace'] = filter(servicefilter('diskspace'), data['bill'])
 +    data['bill_addcredits'] = filter(servicefilter('addcredits'), data['bill'])
 +
 +    return data
 +     
 +     
 +#@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
++    return data
  #ASTAKOS_EMAIL_CHANGE_EMAIL_SUBJECT = 'Email change on %s alpha2 testing' % SITENAME
  #ASTAKOS_PASSWORD_RESET_EMAIL_SUBJECT = 'Password reset on %s alpha2 testing' % SITENAME
  
 +# Set the quota holder component URI
 +#ASTAKOS_QUOTA_HOLDER_URL = ''
 +
 +# Set the cloud service properties
 +# SERVICES = getattr(settings, 'ASTAKOS_SERVICES',
 +#                    {'cyclades': {'url':'https://node1.example.com/ui/',
 +#                                  'quota': {'vm': 2}},
 +#                     'pithos+':  {'url':'https://node2.example.com/ui/',
 +#                                  'quota': {'diskspace': 50 * 1024 * 1024 * 1024}}})
 +
 +# Set the billing URI
 +#ASTAKOS_AQUARIUM_URL = ''
 +
 +# Set how many objects should be displayed per page
 +#PAGINATE_BY = getattr(settings, 'ASTAKOS_PAGINATE_BY', 10)
 +
  # Enforce token renewal on password change/reset
- NEWPASSWD_INVALIDATE_TOKEN = getattr(settings, 'ASTAKOS_NEWPASSWD_INVALIDATE_TOKEN', True)
+ # NEWPASSWD_INVALIDATE_TOKEN = getattr(settings, 'ASTAKOS_NEWPASSWD_INVALIDATE_TOKEN', True)
+ # Permit local account migration
+ # ENABLE_LOCAL_ACCOUNT_MIGRATION = getattr(settings, 'ASTAKOS_ENABLE_LOCAL_ACCOUNT_MIGRATION', True)